Project: Solar/Wind PIC controlled battery array

Thread Starter

nsaspook

Joined Aug 27, 2009
9,101
Dump load installed. Now there are some standard resistance loads (down to 2.5 ohms) for internal battery resistance and State of Charge calculations.
They get pretty hot quickly during 10 second test sequences so I need to install a better heat-sink than plywood.
;)
 

Thread Starter

nsaspook

Joined Aug 27, 2009
9,101
A few routines that use the resistor array for initial battery condition testing.

First we need some rough idea about the condition of the battery when the energy monitor first starts.
The first calculation is a State Of Charge using battery voltage with a nominal load. This uses a static table using data from common voltage vs SoC graphs. Not too accurate but we need something until the system starts tracking actual usage.

C:
const uint32_t BVSOC_TABLE[BVSOC_SLOTS][2] = {
    23000, 5,
    23400, 10,
    23600, 20,
    24120, 25,
    24200, 30,
    24440, 40,
    24540, 45,
    24600, 50,
    24646, 53,
    24700, 55,
    24750, 57,
    24800, 60,
    24850, 63,
    24925, 67,
    25000, 70,
    25020, 72,
    25040, 75,
    25050, 80,
    25060, 85,
    25080, 90,
    25096, 92,
    25122, 93,
    25140, 95,
    25160, 97,
    25180, 100,
    26500, 98 // charging voltage guess
};

// routine that uses the array
uint16_t Volts_to_SOC(uint32_t cvoltage)
{
    static uint8_t slot;

    C.soc = 0;
    for (slot = 0; slot < BVSOC_SLOTS; slot++) {
        if (cvoltage > BVSOC_TABLE[slot][0]) {
            C.soc = BVSOC_TABLE[slot][1];
        }
    }

    return C.soc;
}

// get the initial value and start the background SoC calculation task that also does a few other things
void init_bsoc(void)
{
    /*
     * use raw battery voltage
     */
    C.soc = Volts_to_SOC((uint32_t) conv_raw_result(V_BAT, CONV) * 1000.0);
    C.dynamic_ah = C.bank_ah * (C.soc / 100.0);
    TMR3_SetInterruptHandler(calc_bsoc);
}

/*
* low-pri interrupt ISR the runs every second for simple coulomb counting
*/
void calc_bsoc(void)
{
    uint8_t * log_ptr;
#ifdef DEBUG_BSOC1
    DEBUG1_SetHigh();
#endif
    C.dynamic_ah += (C.c_bat / SSLICE); // Ah
    if (C.dynamic_ah > (C.bank_ah))
        C.dynamic_ah = C.bank_ah;
    if (C.dynamic_ah < 0.1)
        C.dynamic_ah = 0.1;

    C.pv_ah += (C.c_pv / SSLICE);
    C.pvkw += (C.p_pv / SSLICE);
    C.invkw += (C.p_inverter / SSLICE);
    if (C.p_bat > 0.0)
        C.bkwi += (C.p_bat / SSLICE);
    if (C.p_bat < 0.0)
        C.bkwo += (C.p_bat / SSLICE);

    C.soc = ((uint16_t) ((C.dynamic_ah / C.bank_ah)*100.0) + 1);
    if (C.soc > 100)
        C.soc = 100;

    if (C.c_bat < 0.0) {
        C.runtime = (uint16_t) (-(C.dynamic_ah / C.c_bat));
    } else {
        C.runtime = 120;
    }
    if (C.runtime > 120)
        C.runtime = 120;

    V.lowint_count++;

    log_ptr = port_data_dma_ptr();
    sprintf((char*) log_ptr, " %lu,%4.4f,%4.4f,%4.4f,%4.4f,%4.3f,%4.3f,%4.3f,%4.3f,%4.3f,%4.3f,%4.3d,%4.3d,%2.6f\r\n",
        V.ticks,
        C.v_bat, C.v_pv, C.v_cc, C.v_inverter,
        C.p_bat, C.p_pv, C.p_load, C.p_inverter,
        C.dynamic_ah, C.pv_ah, C.soc, C.runtime,
        C.esr);
    StartTimer(TMR_DISPLAY, SOCDELAY); // sync the spi dma display updates
    send_port_data_dma(strlen((char*) log_ptr));
    C.update = false;
#ifdef DEBUG_BSOC1
    DEBUG1_SetLow();
#endif
}

// main code SoC test
#define ROR_LIMIT_LOW    0.0140
#define ROR_LIMIT_SET    0.0001
#define ROR_LIMIT_NOISE    0.0051

#define ROR_WAIT    2000
#define ROR_TIMES    30

/*
* find rate of change of battery voltage under load
*/
void calc_ror_data(void)
{
    static float bvror = 0.0, bcror = 0.0; // must remember prior values

    C.bc_ror = fabs(conv_raw_result(C_BATT, CONV) - bcror);
    bcror = conv_raw_result(C_BATT, CONV);
    C.bv_ror = fabs(conv_raw_result(V_BAT, CONV) - bvror);
    if (C.bv_ror < ROR_LIMIT_NOISE) // skip noise values
        C.bv_ror = ROR_LIMIT_LOW + ROR_LIMIT_SET; // keep trying value
    bvror = conv_raw_result(V_BAT, CONV);
}

            /*
             * check for quickly changing battery voltage
             * to stabilize as to get a better static SOC value
             */
           i_ror = 1;
           do {
                calc_ror_data();
                sprintf(get_vterm_ptr(1, 0), "BV %2.4f         ", conv_raw_result(V_BAT, CONV));
                sprintf(get_vterm_ptr(2, 0), "S SOC %d %2.4f       ", i_ror, C.bv_ror);
                update_lcd(0);
                WaitMs(ROR_WAIT); // time between samples
                clear_adc_scan();
                start_adc_scan();
                WaitMs(500); // wait for updated ADC data
            } while ((i_ror++ < ROR_TIMES) && (C.bv_ror > ROR_LIMIT_LOW));

            static_soc(); // defaults
            init_bsoc(); // system calculations
            set_load_relay_one(false);
            set_load_relay_two(false);
            sprintf(get_vterm_ptr(0, 0), "Static SOC %d        ", C.soc);
            sprintf(get_vterm_ptr(1, 0), "Battery Ah %3.2f     ", C.dynamic_ah);
            update_lcd(0);
The next needed parameter is the internal resistance of the battery. This gives us a good idea about the actual condition of the battery electrochemistry if we have baseline data to compare it with under several different power conditions. The two-stage test is used here with the resistors at 10 ohms and then at 2.5 ohms.

From battery university.

C:
// function
/*
* check battery ESR, returns ESR value when done
*/
float esr_check(void)
{
    set_load_relay_one(false);
    set_load_relay_two(false);
    WaitMs(10000); // unloaded batter wait
    update_adc_result();
    C.bv_noload = conv_raw_result(V_BAT, CONV);

    set_load_relay_one(true);
    WaitMs(10000); // 10 ohm load wait
    update_adc_result();
    C.bv_one_load = conv_raw_result(V_BAT, CONV);
    C.load_i1 = conv_raw_result(C_BATT, CONV); // get current

    set_load_relay_two(true);
    WaitMs(10000); // 2.5 ohm wait
    update_adc_result();
    C.bv_full_load = conv_raw_result(V_BAT, CONV);
    C.load_i2 = conv_raw_result(C_BATT, CONV); // get current

    C.esr = fabs((C.bv_one_load - C.bv_full_load) / (C.load_i1 - C.load_i2)); // find internal resistance causing voltage drop (sorta)
    set_load_relay_one(false);
    set_load_relay_two(false);
    return C.esr;
}

// main code
            sprintf(get_vterm_ptr(0, 0), "Battery ESR     ");
            sprintf(get_vterm_ptr(1, 0), "Calculation     ");
            sprintf(get_vterm_ptr(2, 0), "Check 30 seconds");
            update_lcd(0);
            esr_check();
            sprintf(get_vterm_ptr(0, 0), "ESR  %2.6f           ", C.esr);
            sprintf(get_vterm_ptr(1, 0), "R1 %2.3f %3.4f           ", C.bv_one_load, C.load_i1);
            sprintf(get_vterm_ptr(2, 0), "R2 %2.3f %3.4f           ", C.bv_full_load, C.load_i2);
            update_lcd(0);
Typical ESR values are in the low milliohm range. This old tired battery set tests to about 50 milliohm.
First we see the system waiting for stable battery voltage for the SOC check, then execute the ESR check. The 2A 24vdc AC charger was on the battery but it still provided a sane values of the battery condition.

A more accurate ESR value is calculated with the resistor loads with just the battery online with normal usage loads.
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
9,101
A rewrite of the esr_check function code for usage during the normal mainline code FSM. The original version had blocking 10 second waits, this version is non-blocking.

C:
/*
 * check battery ESR, returns positive ESR value when done, 
 * a negative number code when running the sequence and
 * -1.0 when each FSM sequence is done 
 * (fsm 'true' will init the state machine and return the init code)
 */
float esr_check(uint8_t fsm)
{
    static uint8_t esr_state = 0;

    if (fsm) {
        esr_state = 0;
        return -10.0;
    }

    switch (esr_state) {
    case 0:
        StartTimer(TMR_ESR, 10000); // start the sequence timer
        esr_state++; // move to the next state of the FSM
        break;
    case 1:
        /*
         * set the load resistors to all off
         */
        set_load_relay_one(false);
        set_load_relay_two(false);
        if (TimerDone(TMR_ESR)) { // check for expired timer
            StartTimer(TMR_ESR, 10000); // done, restart the timer, complete sequence, return -1.0
        } else {
            return -2.0; // nope, return with a progress code
        }
        /*
         * save unloaded battery voltage
         */
        update_adc_result();
        C.bv_noload = conv_raw_result(V_BAT, CONV);
        esr_state++; // move to the next state of the FSM
        break;
    case 2:
        set_load_relay_one(true);
        if (TimerDone(TMR_ESR)) {
            StartTimer(TMR_ESR, 10000);
        } else {
            return -3.0;
        }

        update_adc_result();
        C.bv_one_load = conv_raw_result(V_BAT, CONV);
        C.load_i1 = conv_raw_result(C_BATT, CONV); // get current
        esr_state++;
        break;
    case 3:
        set_load_relay_two(true);
        if (!TimerDone(TMR_ESR))
            return -4.0;

        update_adc_result();
        C.bv_full_load = conv_raw_result(V_BAT, CONV);
        C.load_i2 = conv_raw_result(C_BATT, CONV); // get current

        C.esr = fabs((C.bv_one_load - C.bv_full_load) / (C.load_i1 - C.load_i2)); // find internal resistance causing voltage drop (sorta)
        set_load_relay_one(false);
        set_load_relay_two(false);
        esr_state = 0;
        return C.esr;
        break;
    default:
        break;
    }
    return -1.0;
}
This allows other tasks to complete while check_ear is running.

C:
            uint16_t i_esr = 1;
            uint8_t shape = 0;
            float esr_temp;
            while ((esr_temp = esr_check(false)) < 0.0) {
                WaitMs(110); // limit display updates
                shape = (uint8_t) fabs(esr_temp);
                sprintf(get_vterm_ptr(2, 0), "Checking %c %c      ", spinners(0, false), spinners(shape, false));
                update_lcd(0);

                if (i_esr++ > 512)
                    break;
            };
            sprintf(get_vterm_ptr(0, 0), "ESR  %2.6f           ", C.esr);
            sprintf(get_vterm_ptr(1, 0), "R1 %2.3f %3.4f           ", C.bv_one_load, C.load_i1);
            sprintf(get_vterm_ptr(2, 0), "R2 %2.3f %3.4f           ", C.bv_full_load, C.load_i2);
            update_lcd(0);
In the boot sequence this allows for stupid progress indicators using spinners.
C:
/* spinner defines */
#define MAX_SHAPES  6
const char spin[MAX_SHAPES][20] = {
    "||//--", // classic LCD version with no \ character
    "||//--\\\\", // classic
    "OOOOOO--__-", // eye blink
    "vv<<^^>>", // point spinner
    "..**x#x#XX||--", // warp portal
    "..ooOOoo" // ball bouncer
};

/* Misc ACSII spinner character generator, stores position for each shape */
char spinners(uint8_t shape, uint8_t reset)
{
    static uint8_t s[MAX_SHAPES];
    char c;

    if (shape > (MAX_SHAPES - 1))
        shape = 0;
    if (reset)
        s[shape] = 0;
    c = spin[shape][s[shape]];
    if (++s[shape] >= strlen(spin[shape]))
        s[shape] = 0;
    return c;
}
 

Thread Starter

nsaspook

Joined Aug 27, 2009
9,101
Finally had some sun today for decent power readings.
it.png
Battery power goes negative when it starts supplying power to the inverter instead of the panels.
 

Thread Starter

nsaspook

Joined Aug 27, 2009
9,101
The cheap board works perfectly.

On-board 5.0vdc voltage standard.

ADC calibration to on-board standard. ADC count and scaled voltage.

The plan is to combine the boards to eliminate the stupid jumpers from a controller board designed for a different project. (SECS-GEM host)

I used Eagle 9.5.2 to make the modded cpu board a Design Block on the I/O board so I could route without connectors. Very nice.
https://www.autodesk.com/products/eagle/blog/whats-new-in-autodesk-eagle-modular-design-blocks/



Nice seeing all 12 processors @ 100% running the auto-router. Needs a lot of cleanup before a prototype board is ready but it
finished with no errors in ERC or DRC.
 

Thread Starter

nsaspook

Joined Aug 27, 2009
9,101
Next section is the PV/AC charger switching. The system uses a separate 'day' sensor voltage input but is connected to the PV input line (orange romex) before the relay in this demo setup. For the planned operational system (in a retirement tropical paradise ;)) the panels will likely be behind locked covers at night so a separate sensor is needed to activate doors and/or unlock a security system using the AC power contactor.



C:
/*
* should be called every second in the time keeper task
* returns true at dusk or dawn switch-over
*/
bool check_day_time(void)
{
    static uint8_t day_delay = 0;

    if (!day_delay++ && V.system_stable) {
        if (!C.day) {
            if (conv_raw_result(V_LIGHT_SENSOR, CONV) > DAWN_VOLTS) {
                C.day = true;
                C.day_start = V.ticks;
                if (get_ac_charger_relay()) { // USE PV charging during the day
                    set_ac_charger_relay(false);
                }
                return true;
            }
        } else {
            if (conv_raw_result(V_LIGHT_SENSOR, CONV) < DUSK_VOLTS) {
                C.day = false;
                C.day_end = V.ticks;
                /*
                 * at low battery condition charge with AC at night
                 */
                if ((C.soc < SOC_TOO_LOW)) {
                    set_ac_charger_relay(true);
                }
                return true;
            }
        }
    }
    if (day_delay >= DAY_DELAY)
        day_delay = 0;
    return false;
}
 

Thread Starter

nsaspook

Joined Aug 27, 2009
9,101
Finally received my single-board PCB's with ENIG finish for $16 per order extra. Quality is so, so is some places but acceptable for a functional hobby board.

vs the Purble Board.

Looks ok after hand-solder population.



 

Janicer

Joined Jan 9, 2020
3
That looks pretty nice.
So how would this stack up to other larger arrays?
It will operate in a similar fashion as the big big badass battery that ended up at the National Energy Technology Laboratory (NEL) and is now the Smithsonian.
 

Thread Starter

nsaspook

Joined Aug 27, 2009
9,101
Finished the prototype (PCB ordered) DAC module for two 12-bit 0 to 10 volt channels.
https://datasheets.maximintegrated.com/en/ds/MAX5322.pdf

Top SCK speed is 10 MHz but it only runs at 2 MHz here as the required analog drive signals will be pretty slow.

Yellow MOSI programming data to DAC registers
Red MISO readback from DAC registers
Blue SCK
Green CS 16 bit data blocks for channel A & B



C:
            set_dac_a(3.33);
            set_dac_b(6.66);
            set_dac();

typedef struct D_data {
    uint8_t dac0 : 8;
    uint8_t dac1 : 4;
    uint8_t cont : 4;
} D_data;

union bytes2 {
    uint16_t ld;
    uint8_t bd[2];
};

union dac_buf_type {
    uint16_t ld;
    uint8_t bd[2];
    struct D_data map;
};

typedef struct R_data { // internal variables
    adc_result_t raw_adc[ADC_BUFFER_SIZE];
    adc_result_t raw_dac[DAC_BUFFER_SIZE];
    union dac_buf_type max5322_cmd;
    int16_t n_offset[NUM_C_SENSORS];
    float n_zero[NUM_C_SENSORS];
    uint8_t scan_index;
    uint16_t scan_select;
    bool done;
} R_data;

static volatile R_data R = {
    .done = false,
    .scan_index = 0,
    .n_offset[0] = N_OFFSET0,
    .n_offset[1] = N_OFFSET1,
    .n_zero[0] = 0.0,
    .n_zero[1] = 0.0,
    .raw_dac[0] = 0xfff,
    .raw_dac[1] = 0x777,

};

/*
* set == true, set dac spi params
* set == false restore default spi params
*/
void dac_spi_control(bool set)
{
    static bool init = false;
    static uint8_t S0, S1, S2, SC, SB; // SPI device status backup

    if (set) {
        SPI1CON0bits.EN = 0;
        if (!init) {
            init = true;
            S0 = SPI1CON0;
            S1 = SPI1CON1;
            S2 = SPI1CON2;
            SC = SPI1CLK;
            SB = SPI1BAUD;
        }
        /*
         * set DAC SPI mode, speed and fifo
         */
        // mode 1
        SPI1CON1 = 0x00;
        SPI1CON1bits.CKE=1;
        SPI1CON1bits.CKP=0;
        SPI1CON1bits.SMP=0;
        // SSET disabled; RXR suspended if the RxFIFO is full; TXR required for a transfer;
        SPI1CON2 = 0x03;
        // BAUD ;
        SPI1BAUD = 0x0f; // 2MHz SCK
        // CLKSEL FOSC;
        SPI1CLK = 0x00;
        // BMODE every byte; LSBF MSb first; EN enabled; MST bus master;
        SPI1CON0 = 0x83;
        SPI1CON0bits.EN = 1;
    } else {
        if (init) {
            /*
             * restore default SPI mode
             */
            SPI1CON0bits.EN = 0;
            SPI1CON1 = S1;
            SPI1CON2 = S2;
            SPI1CLK = SC;
            SPI1BAUD = SB;
            SPI1CON0 = S0;
            SPI1CON0bits.EN = 1;
        }
    }
}


void set_dac(void)
{
    while (!SPI1STATUSbits.TXBE); // wait until TX buffer is empty
    CSB_SetHigh();
    CS_SDCARD_SetHigh();
    dac_spi_control(true);
    R.max5322_cmd.map.dac0 = R.raw_dac[DCHAN_A]&0xff;
    R.max5322_cmd.map.dac1 = (R.raw_dac[DCHAN_A] >> 8) &0xf;
    R.max5322_cmd.map.cont = DAC_LOAD_A; // update DAC A @ registers
    DAC_CS0_SetLow();
    SPI1_Exchange8bit(R.max5322_cmd.bd[1]);
    SPI1_Exchange8bit(R.max5322_cmd.bd[0]);
    DAC_CS0_SetHigh();
    R.max5322_cmd.map.dac0 = R.raw_dac[DCHAN_B]&0xff;
    R.max5322_cmd.map.dac1 = (R.raw_dac[DCHAN_B] >> 8) &0xf;
    R.max5322_cmd.map.cont = DAC_LOAD_B; // update DAC B @ registers
    DAC_CS0_SetLow();
    SPI1_Exchange8bit(R.max5322_cmd.bd[1]);
    SPI1_Exchange8bit(R.max5322_cmd.bd[0]);
    DAC_CS0_SetHigh();
    dac_spi_control(false);
}
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
9,101
Moved the two 100W panels to optimize the power on a partly cloudy day. A little more than half rated power at the peak before the cloud cover returned.

Using the minicom serial comm program to rs-232 log data from the controller for libreoffice charts. Each line is 10 seconds.

BLUE: Input PV power.
BROWN: Battery power.

minicom.png
 

Thread Starter

nsaspook

Joined Aug 27, 2009
9,101
Hola @nsaspook
At lost here; what are those?

That's a screenshot of my Eagle Cad 9 designed controller board running here. The EAGLE pcb board auto-router function is a CPU hog but it works. The board was designed to be a general purpose industrial DAQ AI/(24v/5v)AO(10v)/DIO (±24v/5.0v/3.3v ±15kV ESD-Protected inputs) system that also works as a solar energy monitor for hardware and software debugging. ;)
 

Thread Starter

nsaspook

Joined Aug 27, 2009
9,101
A few current sensor and DAC board configurations on the module PCB.


100A sensor and DAC on the left, 200A sensor only on the right.


SPI waveforms and decoded DAC register/command data for 3.33 volts on channel A and 6.66 volts on channel B.
 

Thread Starter

nsaspook

Joined Aug 27, 2009
9,101
The cheap PWM controller has been upgraded to a cheap MPPT controller

PV current sensor during voltage control PWM mode.

The battery monitor during PWM.


Need a few good sunny day to log a good charging profile from the new controller.
 

Thread Starter

nsaspook

Joined Aug 27, 2009
9,101
A monitor system chart during 'Boost' (absorption voltage @29.6V) phase of the battery charge cycle. 24vdc battery bank with three series nominal 12vdc 100W solar panels in series from the MPPT voltage input.


In the absorption stage working as the voltage is held steady the battery charging current slowly ramps down.
You can see the effect of loss of sunlight as the input voltages ramp down and panels go into diode bypass. At the end the unit goes into a idle mode where the panels are reading open voltages.
Voltage scales on the left, current scales on the right.


At this solar panel input current I was seeing about a ~5A charging current.
Now I need another current sensor for MPPT input current to log MPPT conversion efficiency. I planned for another 5vdc channel during the PCB design.
 
Top