Project: Solar/Wind PIC controlled battery array

guardrad

Joined May 12, 2020
8
I'll likely use something like this to eliminate the converter and half-duplex jumper cable for a operational setup on a small controller with USB.
www.sparkfun.com/products/9822

Thanks for looking into this and sharing what you have found. Very helpful information. I am trying to read info from the same charge controller and having a heck of a time. If you were going to use this adapter from Sparkfun would you just hook up the orange, red, and black wires that you mapped out or is the 5v required for this application? I didn't see 5v being supplied by this adapter which is why I ask. The plan is to use pymodbus on a raspberry pi to talk to the controller and make the information available through json to grafana and my home automation system. Any help with getting the interface to work would be appreciated.
 

Thread Starter

nsaspook

Joined Aug 27, 2009
13,081
Thanks for looking into this and sharing what you have found. Very helpful information. I am trying to read info from the same charge controller and having a heck of a time. If you were going to use this adapter from Sparkfun would you just hook up the orange, red, and black wires that you mapped out or is the 5v required for this application? I didn't see 5v being supplied by this adapter which is why I ask. The plan is to use pymodbus on a raspberry pi to talk to the controller and make the information available through json to grafana and my home automation system. Any help with getting the interface to work would be appreciated.
The adapter has its own 5vdc from the USB cable, so the GND, RS485 A B signals should be all you need from the charge controller.
 

Thread Starter

nsaspook

Joined Aug 27, 2009
13,081
Alright, I wasn't sure if I needed to supply it to the controller to get it to communicate. Thanks
That part should be just fine. The actual data on the controller is of questionable quality. I've found it to be both inaccurate and low resolution for long term battery monitoring. As a general overview of system operation it's fine.

I'm finishing up the enclosure for the monitor system and MODBUS interface.
 

guardrad

Joined May 12, 2020
8
Sounds good! I picked up the 9822 and created a cable that connects GND, A, and B using your pinout but I am not able to communicate. I receive 0 bits when I try to read the registers using the following code:
Python:
from pymodbus.client.sync import ModbusSerialClient as ModbusClient
client = ModbusClient(method = 'rtu', port = '/dev/ttyUSB0', baudrate = 9600, stopbits = 1, bytesize = 8, parity = 'N')
client.connect()
rr = client.read_input_registers(address=10, count=1, unit=1)
I verified that /dev/ttyUSB0 is valid on my Raspberry Pi.

Debug returns the following:
Code:
DEBUG:pymodbus.transaction:Current transaction state - IDLE
DEBUG:pymodbus.transaction:Running transaction 1
DEBUG:pymodbus.transaction:SEND: 0x1 0x4 0x0 0xa 0x0 0x1 0x11 0xc8
DEBUG:pymodbus.client.sync:New Transaction state 'SENDING'
DEBUG:pymodbus.transaction:Changing transaction state from 'SENDING' to 'WAITING FOR REPLY'
DEBUG:pymodbus.transaction:Transaction failed. (Modbus Error: [Invalid Message] Incomplete message received, expected at least 2 bytes (0 received))
DEBUG:pymodbus.framer.rtu_framer:Frame - [] not ready
DEBUG:pymodbus.transaction:Getting transaction 1
DEBUG:pymodbus.transaction:Changing transaction state from 'PROCESSING REPLY' to 'TRANSACTION_COMPLETE'
The cable I made maps pins 5,7,8 from the RJ45 connector on the 9822 (Schematic) to pins 5,6,7 on the controller. 5-5, 7-7, 8-6. I believe I am reading both the pin out you provided and the provided schematic correctly. I am pretty green when it comes to this so any hand holding you could offer would be appreciated.
 

Thread Starter

nsaspook

Joined Aug 27, 2009
13,081
The first thing to check is if you are sending data from the 9822. Do you have a o-scope or logic probe to monitor the A B signals from the converter?

Command sent and response from the charge controller.

Swapping the A B pins on the 9822 might solve the problem if the cable is correct but give me a bit to check.
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
13,081
The pin to pin connections look correct on paper but you can never tell if A/B on one device (no pinout diagram on the charge controller) is really A/B on another with RS485.
https://www.janitza.us/communication-via-the-rs485-interface.html

This is my cable to the MODBUS board. It looks like I might have an A/B swap in the cable meaning my initial A/B designation on the charge controller socket is reversed.

Update A/B pins.
 
Last edited:

guardrad

Joined May 12, 2020
8
I don't have any probes but I took your advice and made another cable with A/B switched. I was able to get some information.
Code:
DEBUG:pymodbus.transaction:Current transaction state - IDLE
DEBUG:pymodbus.transaction:Running transaction 1
DEBUG:pymodbus.transaction:SEND: 0x1 0x4 0x0 0x14 0x0 0x4 0xb1 0xcd
DEBUG:pymodbus.client.sync:New Transaction state 'SENDING'
DEBUG:pymodbus.transaction:Changing transaction state from 'SENDING' to 'WAITING FOR REPLY'
DEBUG:pymodbus.transaction:Changing transaction state from 'WAITING FOR REPLY' to 'PROCESSING REPLY'
DEBUG:pymodbus.transaction:RECV: 0x1 0x84 0x1 0x82 0xc0
DEBUG:pymodbus.framer.rtu_framer:Getting Frame - 0x84 0x1
DEBUG:pymodbus.factory:Factory Response[132]
DEBUG:pymodbus.framer.rtu_framer:Frame advanced, resetting header!!
DEBUG:pymodbus.transaction:Adding transaction 1
DEBUG:pymodbus.transaction:Getting transaction 1
DEBUG:pymodbus.transaction:Changing transaction state from 'PROCESSING REPLY' to 'TRANSACTION_COMPLETE'
Now I am trying to follow your versioning example using pymodbus.

As I was typing this the code stopped working and now I am getting the 0 bytes received.
 

Thread Starter

nsaspook

Joined Aug 27, 2009
13,081
I don't have any probes but I took your advice and made another cable with A/B switched. I was able to get some information.
Code:
DEBUG:pymodbus.transaction:Current transaction state - IDLE
DEBUG:pymodbus.transaction:Running transaction 1
DEBUG:pymodbus.transaction:SEND: 0x1 0x4 0x0 0x14 0x0 0x4 0xb1 0xcd
DEBUG:pymodbus.client.sync:New Transaction state 'SENDING'
DEBUG:pymodbus.transaction:Changing transaction state from 'SENDING' to 'WAITING FOR REPLY'
DEBUG:pymodbus.transaction:Changing transaction state from 'WAITING FOR REPLY' to 'PROCESSING REPLY'
DEBUG:pymodbus.transaction:RECV: 0x1 0x84 0x1 0x82 0xc0
DEBUG:pymodbus.framer.rtu_framer:Getting Frame - 0x84 0x1
DEBUG:pymodbus.factory:Factory Response[132]
DEBUG:pymodbus.framer.rtu_framer:Frame advanced, resetting header!!
DEBUG:pymodbus.transaction:Adding transaction 1
DEBUG:pymodbus.transaction:Getting transaction 1
DEBUG:pymodbus.transaction:Changing transaction state from 'PROCESSING REPLY' to 'TRANSACTION_COMPLETE'
Now I am trying to follow your versioning example using pymodbus.

As I was typing this the code stopped working and now I am getting the 0 bytes received.
Good work! I need to revise the charge controller socket A/B pins on my built documents. The Rover_MODBUS documentation starting from page 16 has example commands and responses for common system parameters.

I've never used Python so I can't help with that.
 

guardrad

Joined May 12, 2020
8
I found an issue with the 9822. Something caused it to be powered but when a command was passed to it, it would die and come back. A hard reset of the device fixed the issue. Hopefully that is not a bug I will run into with it. Next step is to figure out how to pass the right request to the charge controller. If I understand correctly the first 04 should be 03 to read the register.
 

Thread Starter

nsaspook

Joined Aug 27, 2009
13,081
Do you have a copy of the ROVER_MODBUS documentation?
https://github.com/nsaspook/vtouch_v2/blob/mbmc/ROVER_MODBUS.pdf


Device address
Function code
Start address
No. of read words

Device address BYTE 01H to F7H
Read a Single or Multiple Word register(s) 2 bytes 03H

3.3 To read the controller's software version and hardware version, and the PDU addresses are known to be 0014H, 0015H, 0016H and 0017H in sequence
To send: 01 03 0014 0004 040D
To receive: 01 03 08 0003 0201 0001 0203 8A54
Parsing: (the highest byte OOH is not used) 030201H indicates the controller's software version is V03.02.01
(the highest byte OOH is not used) 010203H indicates the controller's hardware version is V01.02.03
 

guardrad

Joined May 12, 2020
8
I figured out the issue with the wrong function code being sent. I uploaded the work to github if interested. https://github.com/CyberRad/CoopSolar

By any chance did you see anything odd with the temps that are pulled from your controller? The example provided in the pdf shows querying 102 for the temp but the table shows 103. 102 seems to be valid per the table for charging current. Are you reading any input for the 2 values for 103? Below is my sent and received.

Code:
DEBUG:pymodbus.transaction:Current transaction state - IDLE
DEBUG:pymodbus.transaction:Running transaction 1
DEBUG:pymodbus.transaction:SEND: 0x1 0x3 0x1 0x3 0x0 0x2 0x35 0xf7
DEBUG:pymodbus.client.sync:New Transaction state 'SENDING'
DEBUG:pymodbus.transaction:Changing transaction state from 'SENDING' to 'WAITING FOR REPLY'
DEBUG:pymodbus.transaction:Changing transaction state from 'WAITING FOR REPLY' to 'PROCESSING REPLY'
DEBUG:pymodbus.transaction:RECV: 0x1 0x3 0x4 0x16 0x10 0x0 0x0 0xff 0xbe
DEBUG:pymodbus.framer.rtu_framer:Getting Frame - 0x3 0x4 0x16 0x10 0x0 0x0
DEBUG:pymodbus.factory:Factory Response[ReadHoldingRegistersResponse: 3]
DEBUG:pymodbus.framer.rtu_framer:Frame advanced, resetting header!!
DEBUG:pymodbus.transaction:Adding transaction 1
DEBUG:pymodbus.transaction:Getting transaction 1
DEBUG:pymodbus.transaction:Changing transaction state from 'PROCESSING REPLY' to 'TRANSACTION_COMPLETE'
Maybe 0x16 and 0x10 are actually separate measurements?
 

Thread Starter

nsaspook

Joined Aug 27, 2009
13,081
I use very little data from the controller. My only interest was tracking the charger mode state and error codes so I didn't do much to validate values other that looking at a few voltage/current readings that were imprecise in comparison to my own monitoring system. Nice job with your software.hardware setup.
 

Thread Starter

nsaspook

Joined Aug 27, 2009
13,081
The new AGM batteries on the old 12vdc system are impressive under load. Running the 5000 BTU AC in the shop without a glitch for hours and hours.
IMG_20200731_172831.jpgIMG_20200731_172611.jpg
 

Thread Starter

nsaspook

Joined Aug 27, 2009
13,081
It's time to put the old 12V system away. It's done a great job since Jan, 2010 but it long on the tooth and has no realistic upgrade path.
She's still running today with the panels for the new system attached but the losses are horrible above 500W unless there is a massive copper wiring upgrade (I have access to 0000 gauge scrap but it's too much of a pain to handle)

I ordered an Outback FM80 MPPT charge controller that can handle 150VDC panels strings to greatly reduce wire gauge requirements to the remote panels.
https://www.outbackpower.com/products/charge-controllers/flexmax-60-80

and will be the modifying the 24V system monitor from here for the MATE serial protocol: https://forum.allaboutcircuits.com/...c-controlled-battery-array.32879/post-1513872
to handle the data logging details to my own computer system. I'll be using a PIC18 controller and C but the interfacing info here is great. https://jared.geek.nz/pymate
https://github.com/nsaspook/Q84vtouch/tree/q84

I'm testing all the panels using the Doug-Fir ground mounting system in a corner of the backyard facing south. :eek:
1685224143787.png
I'll square things up and put some side braces later.
1685224175051.png :D
I have space for three rows once I kill the stuff growing on the fence.
1685224409194.png

Half are serial connected to the 24VDC MPPT charge controller
1685224022598.png
while the other half is adding power to the 12V system C40 PWN charge controller.
1685224071685.png 1685226367327.png

I looking at using LiFePo4 batteries for the new energy bank.
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
13,081
Starting up the new Solar energy system with a cheap set of small commissioning batteries.
1685732557111.png
It's pretty big.
1685732593546.png1685732649024.png
Good specs. Clearing out a corner for it.
1685732695714.png
600W 25vdc pure sine inverter for testing.
1685732741260.png
A bit of a mess that will be rewired.
1685732804515.png1685732832443.png
Panel current -> Inverter current.
1685732928682.png
PIC18 data interface to the FM80 to replace the not so usable MATE interface via a network cable on the CC RJ-45 data plug. The FM80 Charge Controller MATE interface uses a 24 V current loop interface.
Using two 4N26 optos to convert that to TTL usable 9N1 9600 serial on UART1. Because it's 9-bit serial it takes a few extra
steps to generation and receive the proper data streams and generate the needed checksum. I'm not using the UART interrupt, I'm using s 500us timer interrupt to poll the TX/TX signals and flags because 9-bit
serial is crazy on this controller.
C:
#include "mxcmd.h"

static volatile uint8_t data = 0x00, dcount = 0, dstart = 0, rdstart = 0;
static volatile uint16_t tbuf[FM_BUFFER], rbuf[FM_BUFFER];
static uint16_t *p_tbuf = (uint16_t*) tbuf, *p_rbuf = (uint16_t*) rbuf;

/*
 * Check for TX transmission
 */
bool FM_tx_empty(void)
{
    if (dcount == 0) {
        return true;
    } else {
        return false;
    }
}

/*
 * after the tbuf has been loaded start the TX transfer
 */
uint8_t FM_tx(const uint16_t * data, uint8_t count)
{
    if (dcount == 0) {
        memcpy((void *) tbuf, (const void *) data, (size_t) (count << 2)); // copy 16-bit values
        dstart = 0;
        dcount = count;
    }
    return dstart;
}

/*
 * serial I/O ISR, TMR4 500us I/O sample rate
 * polls the required UART registers for 9-bit send and receive into 16-bit arrays
 */
void FM_io(void)
{
    static uint8_t pace = 0;

    MISC_SetHigh(); // serial CPU usage signal

    if (pace++ > 3) {
        if (dcount-- > 0) {
            if (tbuf[dstart] > 0xff) { // Check for bit-9
                U1P1L = (uint8_t) tbuf[dstart]; // send with bit-9 high
            } else {
                UART1_Write((uint8_t) tbuf[dstart]); // send with bit-9 low
            }
            dstart++;
        } else {
            dstart = 0;
            dcount = 0;
        }
        pace = 0;
    }

    /*
     * handle framing errors
     */
    if (U1ERRIRbits.RXFOIF) {
        rbuf[0] = U1RXB; // read bad data to clear error
        U1ERRIRbits.RXFOIF = 0;
        rdstart = 0; // reset buffer to start
    }

    /*
     * read serial data if polled interrupt flag is set
     */
    if (PIR4bits.U1RXIF) {
        if (U1ERRIRbits.FERIF) {
        }

        if (rdstart > FM_BUFFER - 1) { // overload buffer index
            rdstart = 0; // reset buffer to start
            MLED_SetHigh();
        }
        if (U1ERRIRbits.PERIF) {
            rbuf[rdstart] = 0x0100;
        } else {
            rbuf[rdstart] = 0x00;
        }
        rbuf[rdstart] += U1RXB;
        rdstart++;
    }
    MISC_SetLow();
}

uint8_t FM_rx(uint16_t * data)
{
    uint8_t count;

    RELAY_SetHigh();
    INTERRUPT_GlobalInterruptHighDisable();
    count = rdstart;
    memcpy(data, (const void *) rbuf, (size_t) (count << 2)); // copy 16-bit values
    rdstart = 0;
    INTERRUPT_GlobalInterruptHighEnable();
    RELAY_SetLow();
    return count;
}

bool FM_rx_ready(void)
{
    if (rdstart == 0) {
        return false;
    } else {
        return true;
    }
}

uint8_t FM_rx_count(void)
{
    uint8_t count;

    INTERRUPT_GlobalInterruptHighDisable();
    count = rdstart;
    INTERRUPT_GlobalInterruptHighEnable();
    return count;
}

void onesec_io(void)
{
    RLED_Toggle();
    MLED_SetLow();
}
1685734161203.png
1685733447153.png
Connects to the Linux PC using a USB to TLL dongle on UART2 using an old serial protocol converter PCB with a PIC18F14Q41. A proper PCB that also includes CANBUS is being designed for
the Q84 family.
C:
/**
  Generated Main Source File

  Company:
    Microchip Technology Inc.

  File Name:
    main.c

  Summary:
    This is the main file generated using PIC10 / PIC12 / PIC16 / PIC18 MCUs

  Description:
    This header file provides implementations for driver APIs for all modules selected in the GUI.
    Generation Information :
    Product Revision  :  PIC10 / PIC12 / PIC16 / PIC18 MCUs - 1.81.8
    Device            :  PIC18F14Q41
    Driver Version    :  2.00
*/

/*
    (c) 2018 Microchip Technology Inc. and its subsidiaries.
   
    Subject to your compliance with these terms, you may use Microchip software and any
    derivatives exclusively with Microchip products. It is your responsibility to comply with third party
    license terms applicable to your use of third party software (including open source software) that
    may accompany Microchip software.
   
    THIS SOFTWARE IS SUPPLIED BY MICROCHIP "AS IS". NO WARRANTIES, WHETHER
    EXPRESS, IMPLIED OR STATUTORY, APPLY TO THIS SOFTWARE, INCLUDING ANY
    IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY, AND FITNESS
    FOR A PARTICULAR PURPOSE.
   
    IN NO EVENT WILL MICROCHIP BE LIABLE FOR ANY INDIRECT, SPECIAL, PUNITIVE,
    INCIDENTAL OR CONSEQUENTIAL LOSS, DAMAGE, COST OR EXPENSE OF ANY KIND
    WHATSOEVER RELATED TO THE SOFTWARE, HOWEVER CAUSED, EVEN IF MICROCHIP
    HAS BEEN ADVISED OF THE POSSIBILITY OR THE DAMAGES ARE FORESEEABLE. TO
    THE FULLEST EXTENT ALLOWED BY LAW, MICROCHIP'S TOTAL LIABILITY ON ALL
    CLAIMS IN ANY WAY RELATED TO THIS SOFTWARE WILL NOT EXCEED THE AMOUNT
    OF FEES, IF ANY, THAT YOU HAVE PAID DIRECTLY TO MICROCHIP FOR THIS
    SOFTWARE.
*/
#pragma warning disable 520
#pragma warning disable 1498

#include "mxcmd.h"

#define PACE    31000    // commands delay in count units
#define CMD_LEN    8
#define REC_LEN 5

enum state_type {
    state_init,
    state_status,
    state_panel,
    state_battery,
    state_watts,
    state_misc,
    state_run,
    state_last,
};

uint16_t abuf[FM_BUFFER];
uint16_t volt_fract;
uint16_t volt_whole;
enum state_type state = state_init;
uint16_t pacing = 0, rx_count = 0;
/*
* show fixed point fractions
*/
void volt_f(uint16_t);

/*
* MX80 send/recv functions
*/
void send_mx_cmd(const uint16_t *);
void rec_mx_cmd(void (* DataHandler)(void));

/*
* callbacks to handle MX80 register data
*/
void state_init_cb(void);
void state_status_cb(void);
void state_panelv_cb(void);
void state_batteryv_cb(void);
void state_watts_cb(void);
void state_misc_cb(void);

/*
* Main application
*/
void main(void)
{
    // Initialize the device
    SYSTEM_Initialize();

    // If using interrupts in PIC18 High/Low Priority Mode you need to enable the Global High and Low Interrupts
    // If using interrupts in PIC Mid-Range Compatibility Mode you need to enable the Global Interrupts
    // Use the following macros to:

    // Enable high priority global interrupts
    INTERRUPT_GlobalInterruptHighEnable();

    // Enable low priority global interrupts.
    INTERRUPT_GlobalInterruptLowEnable();

    // Disable high priority global interrupts
    //INTERRUPT_GlobalInterruptHighDisable();

    // Disable low priority global interrupts.
    //INTERRUPT_GlobalInterruptLowDisable();

    TMR4_SetInterruptHandler(FM_io);
    TMR4_StartTimer();
    TMR0_SetInterruptHandler(onesec_io);
    TMR0_StartTimer();

    while (true) {
        // Add your application code
        switch (state) {
        case state_init:
            send_mx_cmd(cmd_id);
            rec_mx_cmd(state_init_cb);
            break;
        case state_status:
            send_mx_cmd(cmd_status);
            rec_mx_cmd(state_status_cb);
            break;
        case state_panel:
            send_mx_cmd(cmd_panelv);
            rec_mx_cmd(state_panelv_cb);
            break;
        case state_battery:
            send_mx_cmd(cmd_batteryv);
            rec_mx_cmd(state_batteryv_cb);
            break;
        case state_watts:
            send_mx_cmd(cmd_watts);
            rec_mx_cmd(state_watts_cb);
            break;
        case state_misc:
            send_mx_cmd(cmd_misc);
            rec_mx_cmd(state_misc_cb);
            break;
        default:
            send_mx_cmd(cmd_id);
            rec_mx_cmd(state_init_cb);
            break;
        }
    }
}

/*
* display  div 10 integer to fraction without FP
* %d.%01d  volt_whole, volt_fract
*/
void volt_f(uint16_t voltage)
{
    volt_fract = (uint16_t) abs(voltage % 10);
    volt_whole = voltage / 10;
}

void send_mx_cmd(const uint16_t * cmd)
{
    if (FM_tx_empty()) {
        if (pacing++ > PACE) {
            FM_tx(cmd, CMD_LEN); // send 8 9-bits command data stream
            pacing = 0;
        }
    }
}

/*
* process received data in abuf with callbacks
*/
void rec_mx_cmd(void (* DataHandler)(void))
{
    if (FM_rx_ready()) {
        if (FM_rx_count() >= REC_LEN) {
            FM_rx(abuf);
            DataHandler(); // execute callback
        }
    }
}

void state_init_cb(void)
{
    printf("\r\n\r\n%5d %3x %3x %3x %3x %3x   INIT: Found MX80 online\r\n", rx_count++, abuf[0], abuf[1], abuf[2], abuf[3], abuf[4]);
    state = state_status;
}

void state_status_cb(void)
{
    printf("%5d %3x %3x %3x %3x %3x STATUS: MX80 %s mode\r\n", rx_count++, abuf[0], abuf[1], abuf[2], abuf[3], abuf[4], state_name[abuf[2]]);
    if (abuf[2] != STATUS_SLEEPING) {
        state = state_panel;
    }
}

void state_panelv_cb(void)
{
    printf("%5d %3x %3x %3x %3x %3x   DATA: Panel Voltage %iVDC\r\n", rx_count++, abuf[0], abuf[1], abuf[2], abuf[3], abuf[4], (abuf[2] + (abuf[1] << 8)));
    state = state_battery;
}

void state_batteryv_cb(void)
{
    volt_f((abuf[2] + (abuf[1] << 8)));
    printf("%5d %3x %3x %3x %3x %3x   DATA: Battery Voltage %d.%01dVDC\r\n", rx_count++, abuf[0], abuf[1], abuf[2], abuf[3], abuf[4], volt_whole, volt_fract);
    state = state_watts;
}

void state_watts_cb(void)
{
    printf("%5d %3x %3x %3x %3x %3x   DATA: Panel Watts %iW\r\n", rx_count++, abuf[0], abuf[1], abuf[2], abuf[3], abuf[4], (abuf[2] + (abuf[1] << 8)));
    state = state_misc;
}

/*
* testing callback
*/
void state_misc_cb(void)
{
    state = state_status;
}
/**
End of File
*/
https://github.com/nsaspook/ll-test/tree/newboard/FM80_net.X
 
Last edited:
Top