Project: Solar/Wind PIC controlled battery array

Thread Starter

nsaspook

Joined Aug 27, 2009
8,907
Well, most of the fun stuff is done. The basic model for two way comms and control of the system from the web host, near real-time system status on the web and javascript generated charts from data cached on the host database collected from the controller. Next is the boring part of creating all the web pages, writing the chart functions and finding the bugs in software.

Short Video: http://flic.kr/p/9D79E2


WebFX has a great free chart/graph system in Javascript.
http://webfx.eae.net/dhtml/chart/chart.html
 

Thread Starter

nsaspook

Joined Aug 27, 2009
8,907
It's been a while since an update. I have the full 500W Solar array up and running with 450Ah of battery and a 2kW pure-sine inverter. I had a real world test last week after a underground utility transformer shorted and took down the local power for a day. I started using MPLABX on Linux for development of the 8 and 32 PIC software. The latest MPLABX 1.1 version is light years ahead of the betas from last year.

http://farm8.staticflickr.com/7181/6822479210_b6bd6ef99a_z_d.jpg
http://farm8.staticflickr.com/7057/6966018139_66b49b9c49_z_d.jpg
http://farm8.staticflickr.com/7049/6884674473_5a90daa5f2_z_d.jpg

PIC18 Solar Energy Management Software Source in C18. Functional, but not totally done.
 

Attachments

Thread Starter

nsaspook

Joined Aug 27, 2009
8,907
This projects basic requirements are complete.

1. Run a 5000BTU AC during the day directly from the power generated from solar panels on a bright sunny day (~600W).:D
2. Have at least a 4 hour run time from batteries at a 500W power level when there is no sun.
3. PIC18 micro-controller based smart monitor to automate the allocation of limited power to keep the battery banks fully charged and to maximize battery life.
4. PIC32 Ethernet based Web and telnet front-ends to monitor and control system operation.

http://flic.kr/p/cAMQb5
http://flic.kr/p/cAMQv3

Project build Sideshow (lots of changes over the years): http://www.flickr.com//photos/nsaspook/sets/72157622934371746/show/

Project PIC 18/32 Software Page: http://code.google.com/p/solar-monitor/
 

Thread Starter

nsaspook

Joined Aug 27, 2009
8,907
Well it's summer 2014 and the shop AC is still running on solar. :D

I've added some TCP client code to the PIC32 system to monitor remote DAQ systems close to the panels but not much else has changed hardware wise.

Code: Most of which needs to be restructured but it's what happens when you have a hobby experiment.
https://github.com/nsaspook/mbmc
 

Attachments

Thread Starter

nsaspook

Joined Aug 27, 2009
8,907
Why you don't want to use 12vdc as the transmission voltage for solar energy storage unless you've got lots of heavy wire.
Finally running a small AC unit (80+ this week in Oregon) in the shop off the inverter. Ouch, the voltage drop is high from the remote array with only 8 GA interconnects.


Time to trench in a 000 jumper from the scrap cable bin.
 

Thread Starter

nsaspook

Joined Aug 27, 2009
8,907
Time for a FLA battery change after 5 years with a new (4x) GC-2 set from Costco. $83.99 for each 6V 208Ah battery is a pretty good price for RE power storage.
I'm guessing I put from 700 to 800 full cycles on the old set before they really started to die.



 

Thread Starter

nsaspook

Joined Aug 27, 2009
8,907
Still operational supplying the server/shop electrical power on this nice sunny spring day. Getting close to another battery replacement. This time I might try some solar rated AGM cells.


Hard to believe my first post on this was Jan 15 2010. Life is short.
 

Thread Starter

nsaspook

Joined Aug 27, 2009
8,907
Upgraded the battery technology to AGM for the current 12 volt system. The old flooded batteries will be be in a series string for testing a future 24 volt monitoring system using the PIC18F57K42 I'm currently designing for a4000 Watt Pure Sine 120/240Vac Split Phase Output inverter.



Heavy!

Thank goodness these monsters have good handles. https://www.renogy.com/deep-cycle-agm-battery-12-volt-200ah/

Operational.
 

Thread Starter

nsaspook

Joined Aug 27, 2009
8,907
Have some new hardware designed for the new 24 volt solar energy bank.


The pic18f57k42 processor board (designed for this communications project SECSII but with added circuitry for precision analog measurements) drives the SPI display (using background DMA transfers DMA) and I/O board that has the voltage regulators to supply 5v and 3.3v device power, 12vdc buss power and precision 4.095 (REF3440-EP) ADC and 5.00 (REF02) calibration reference voltages from the 24vdc battery input. The main ADC ref is the REF3440 so adjustable (4 turn 200 ohm trim pots for each analog channel) voltage dividers allow for exact calibration for the 5.2 and 33 volt max signal monitor lines. There are two hall current sensors (100A PV input, 200A battery current), each with it own REF02 precision voltage source for the sensor current signal regulated from the 12vdc general power buss.

Current sensors

System bench testing.

The goal is to get repeatable accuracy to 1mV and 10ma from the DAQ system for a precise energy usage and AGM battery SOC/energy storage condition calculation to at least 5% of actual run-time with dynamic load and charging profiles.

The DAQ front-end uses the ADC compute capability of the K42 to background capture and scan (using vector interrupts) all channels with 64 over-samples per channel and one extra bit for a total of 13-bit usable resolution.
http://ww1.microchip.com/downloads/en/Appnotes/Vectored-Interrupt-Controller-90003162C.pdf

ADC count and calibration voltage result for trimmed channels 0,1 (5 v sensor),2 (33v voltage). The last digit on the third line is the 1 second clock tick for the internal clock.


Debug toggles enabled for scope traces (Low speed ADC FRC clock). Upper: 500ms per start of scan scan rate. Lower: Top: per ADC channel switch toggle, bottom: per ADC conversion toggle.


High speed FOSC clock ADC sampling testing

The software uses the ADC threshold burst average hardware on the chip for most of the required calculations to total the 64 samples into a usable 13-bit result without main program intervention.
http://ww1.microchip.com/downloads/en/Appnotes/PIC16-PIC18-ADC2-90003194A.pdf

The code uses 16-bit bitmaps to select conversion channels and conversion to real number routines in background
C:
// daq.h
/*
* 12-bit analog 64 sample average per channel on ports A and B
* uses the compute burst average mode threshold interrupt to auto change
* channels during interrupt after a repeat count
*/

// This is a guard condition so that contents of this file are not included
// more than once.
#ifndef DAQ_H
#define    DAQ_H

#include "tests.h"

/*
* adc channel configuration
*/
#define LAST_ADC_CHAN    0XD
#define ADC_BUFFER_SIZE    16 // MUST BE AT LEAST 1
#define ADC_SCAN_SPEED    500 // sample timer speed in ms
#define ADC_SCAN_CHAN    0b0011110001110111 // convert these analog ports bitmap
/*
* current sensor configuration
*/
#define NUM_C_SENSORS    2
#define ADC_C_CHAN    0b0000000000000011 // 5 volt hall current sensor bitmap
#define ADC_C_CHAN_TYPE    0b0000000000000001 // 0 - 100A, 1=200A
/*
* temp sensor configuration
*/
#define NUM_T_SENSORS    1
#define ADC_T_CHAN    0b0000100000000000 // 5 volt temp sensor bitmap
#define ADC_T_CHAN_TYPE    0b0000100000000000 // type bitmap

/*
* conversion constants
*/
/*
* 13-bit adc result ADRPT 64 samples and a ADCRS of 5 [0..8190] steps
*/
#define C_SCALE        1.25/2.0
#define V_SCALE        8.250825/2.0

#define C_A200        143.85
#define C_A100        60.1

#define C_OFFSET0    2457.0
#define C_OFFSET1    2485.2

#include <xc.h> // include processor files - each processor file is guarded.
#include "mcc_generated_files/adcc.h"
#include "mcc_generated_files/pin_manager.h"

typedef enum {
    CONV, // auto system conversion
    O_CONV, // convert to voltage for calibration/offsets
} adc_conv_t;

bool start_adc_scan(void);
bool check_adc_scan(void);
void clear_adc_scan(void);
adc_result_t get_raw_result(adcc_channel_t);
float conv_raw_result(adcc_channel_t, adc_conv_t);

#endif

//daq.c
/*
* 12-bit analog 64 sample average per channel on ports A and B
* uses the compute burst average mode threshold interrupt to auto change
* channels during interrupt after a repeat count
*/

#include "daq.h"
#include <math.h>

typedef struct R_data { // internal variables
    adc_result_t raw_adc[ADC_BUFFER_SIZE];
    float c_offset[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,
    .c_offset[0] = C_OFFSET0,
    .c_offset[1] = C_OFFSET1,
};

static void adc_int_handler(void);
static void adc_int_t_handler(void);

/*
* start computed ADC results: 64 samples per average value per selected channel from
* AN channel zero to LAST_ADC_CHAN
* check_adc_scan returns true when sequence is complete
*/
bool start_adc_scan(void)
{
    if (R.done)
        return false;

    R.scan_index = 0;
    R.scan_select = (uint16_t) ((ANSELB << 8) + ANSELA) & ADC_SCAN_CHAN; // skip digital pins PORT A and B
    ADCC_SetADIInterruptHandler(adc_int_handler);
    ADCC_SetADTIInterruptHandler(adc_int_t_handler);
    ADCC_DischargeSampleCapacitor(); // short ADC sample cap before channel sampling
    ADCC_StartConversion(R.scan_index & 0xf);
#ifdef DEBUG_DAQ1
    DEBUG1_SetHigh();
#endif
#ifdef DEBUG_DAQ2
    DEBUG2_SetHigh();
#endif
    return true;
}

/*
* check scan done flag
*/
bool check_adc_scan(void)
{
    return R.done;
}

/*
* clear scan done flag
*/
void clear_adc_scan(void)
{
    R.done = false;
}

/*
* read average value of a channel after scan completion (done)
*/
adc_result_t get_raw_result(adcc_channel_t index)
{
    return R.raw_adc[index];
}

/*
* turn ADC values into standard program values
*/
float conv_raw_result(adcc_channel_t chan, adc_conv_t to_what)
{
    if (!(ADC_SCAN_CHAN >> chan & 0x1))
        return NAN;

    switch (to_what) {
    case CONV:
        if (ADC_C_CHAN >> chan & 0x1) { // current conversion
            if (ADC_C_CHAN_TYPE >> chan & 0x1) {
                return(((float) get_raw_result(chan) * C_SCALE) - R.c_offset[0]) * C_A200 / 1000.0;
            } else {
                return(((float) get_raw_result(chan) * C_SCALE) - R.c_offset[1]) * C_A100 / 1000.0;
            }
        } else {
            if (ADC_T_CHAN >> chan & 0x1) { // temp conversion
                return 25.0; // filler until sensor is selected
            } else { // voltage conversion
                return((float) get_raw_result(chan) * V_SCALE) / 1000.0;
            }
        }
        break;
    case O_CONV:
        if (ADC_C_CHAN >> chan & 0x1 || ADC_T_CHAN >> chan & 0x1)
            return((float) get_raw_result(chan) * C_SCALE) / 1000.0;

        return((float) get_raw_result(chan) * V_SCALE) / 1000.0;
        break;
    default:
        return 0.0;
        break;
    }
    return 0.0;
}

/*
* ADC per conversion interrupt
*/
static void adc_int_handler(void)
{
#ifdef DEBUG_DAQ2
    DEBUG2_Toggle();
#endif
}

/*
* ADC per channel average interrupt
*/
static void adc_int_t_handler(void)
{
    /*
     * use the filter result buffer for raw adc data
     */
    R.raw_adc[R.scan_index] = ((adc_result_t) ((ADFLTRH << 8) + ADFLTRL));
    do {
        if (++R.scan_index > LAST_ADC_CHAN) {
            R.done = true;
#ifdef DEBUG_DAQ1
            DEBUG1_SetLow();
#endif
            return;
        }
    } while (!((R.scan_select >> R.scan_index) &0x1)); // check for analog port bit
    ADCC_DischargeSampleCapacitor(); // short ADC sample cap before next channel sampling
    ADCC_StartConversion(R.scan_index & 0xf);
#ifdef DEBUG_DAQ1
    DEBUG1_Toggle();
#endif
}
The main code (optimizing the DAQ processing for now) just starts the ADC conversion sequence and waits in a state machine doing other processing for the ADC scan completion before value auto-conversion of system values for solar energy and battery condition calculations.

C:
//main.c code fragment
        if (TimerDone(TMR_ADC) && check_adc_scan()) {
            /*
             * download the system data variables
             */
            convert_adc_data();
            /*
             * restart the conversion process
             */
            clear_adc_scan();
            start_adc_scan();
            StartTimer(TMR_ADC, ADC_SCAN_SPEED);
        }

        if (V.ticks) {

        }

        if (TimerDone(TMR_DISPLAY)) { // limit update rate
            if (TimerDone(TMR_HELPDIS)) {
                set_display_info(DIS_STR);
            }
            sprintf(get_vterm_ptr(0, 0), "%d %2.4f   #", get_raw_result(C_BATT), C.calc[C_BATT]);
            sprintf(get_vterm_ptr(1, 0), "%d %2.4f   #", get_raw_result(C_PV), C.calc[C_PV]);
            sprintf(get_vterm_ptr(2, 0), "%d %2.4f, %lu   #", get_raw_result(V_CC), C.calc[V_CC], V.timerint_count);
            StartTimer(TMR_DISPLAY, DDELAY);
            update_lcd(0);
        }
Current code tree on github.
https://github.com/nsaspook/vtouch_v2/tree/mbmc/touch_v2.X
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
8,907
The old battery bank has been rewired for 24vdc for running the new 600W pure-sine inverter.


Computer load testing. The inverter pulls about 250 mA at AC power idle.

I need to split the bank and recharge at 12 volts until the 24 volt charge controller is installed.
 

Thread Starter

nsaspook

Joined Aug 27, 2009
8,907
Cyber-squirrel attack!

I test the incoming voltage on an old unused set of 10yo solar panels on the shed for a 24 vdc PV feed and get no voltage from any of the panels. Up the ladder to check and boom, I find this, cables eaten in half or wires broken or corroded away from being exposed when the insulation was eaten.


Organic Soy based wire insulation most likely.

I only need two (12v panels in series) of the five for a 24 volt feed so I'll just cut the wire close the the panel and rewire a new down-lead with Romex for now.
 

Thread Starter

nsaspook

Joined Aug 27, 2009
8,907
The testbed is done with a DIN 2.1A power supply and a cheap $15 10A charge controller. The CC has a MODBUS connector so that's a future software project for the data interface master.
 

Thread Starter

nsaspook

Joined Aug 27, 2009
8,907
External switch input design. The I/O board uses the MAX6818 CMOS Switch Debounce chip in Change-of-State interrupt mode.
https://datasheets.maximintegrated.com/en/ds/1896.pdf
The CH output of the MAX is routed to the external interrupt #1 pin (RB1) on the PIC and the MAX enable pin is connected to a PIC output pin RD7 held normally high to tri-state MAX6818 outputs. This allows you to have several MAX6818 devices on a common input bus but only one chip is used here.


Code testing.
Here, a jumper simulates a switch on INP pin 3. The last digit on the LCD line displays switch status. 1 = switch pressed NOW.
The bottom digit (base 10) is a bitmask of switches pressed since the last time bitmap data was cleared. Switches 2 (4) & 3 (8) have been pressed.

Code fragment of the low-priority vectored interrupt handler.
C:
void switch_handler(void)
{
    uint8_t i = 0, sw_value;

    /*
     * enable the outputs for reading and reset MAX Change-of-State pin
     */
    MAX_EN_SetLow();

#ifdef DEBUG_SWH1
    DEBUG1_SetHigh();
#endif

    // Nop for MAX chip output Propagation Delay after enable
    Nop();
    Nop();
    Nop();
    // start reading the various pic port input bits after the max chip is ready
    do {
        switch (i) {
        case SENTER:
            sw_value = ENTER_B_GetValue();
            break;
        case SSELECT:
            sw_value = SELECT_B_GetValue();
            break;
        case S1:
            sw_value = IO_RF1_GetValue();
            break;
        case S0:
            sw_value = IO_RF0_GetValue();
            break;
        default:
            sw_value = 1;
            break;
        }

        // update actual current button state
        if (sw_value) {
            if (V.button[i].sw == SW_ON) {
                V.button[i].sw = SW_OFF;
            }
        } else {
            if (V.button[i].sw == SW_OFF) {
                V.button[i].sw = SW_ON;
                V.button[i].count = V.timerint_count; // so we can check button SW_ON duration
                V.sw_bitmap |= 1 << i; // set switch pressed bit
            }
        }
    } while (++i < NUM_SWITCHES);

#ifdef DEBUG_SWH1
    DEBUG1_SetLow();
#endif
#ifdef DEBUG_SWH2
    DEBUG2_Toggle();
#endif

    MAX_EN_SetHigh(); // disable MAX output pins
}

void start_switch_handler(void)
{
    EXT_INT1_InterruptDisable();
    INT1_SetInterruptHandler(switch_handler);
    V.button[SNULL].sw = SW_INVALID; // set a error condition for invalid button number
    EXT_INT1_InterruptEnable();
}
~18us per interrupt for 4 switch state updates per interrupt.
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
8,907
The monitor is operational for 6 data points on the test-bed.

PV voltage, PV current
Battery Voltage, Battery/Load current
Charge Controller output voltage
Inverter DC input voltage.


Fused voltage monitor lines


Controller and I/O board connection testing


LCD readout (current sensors are in series for calibration)
Panel Voltage & Current
Battery Voltage & Current
Charge Controller & Inverter Voltage.

Next on the list is to work on the two button LCD UI and to rewrite the battery modeling routines from the original MBMC version.
 

Thread Starter

nsaspook

Joined Aug 27, 2009
8,907
Some system reading at various loads.

Inverter DC power on with no AC load. Inverter and controller static power.


Power display SELECT button pressed

DC power up of the inverter and a PC computer AC load startup.


Inverter DC power off.

 

Thread Starter

nsaspook

Joined Aug 27, 2009
8,907
Most of the basic voltage, current, power data-logging functions are done.

Inverter load startup and shutdown chart.

I transmit the data via a serial port in CSV format to the server (DL380 G5) for capture and display.
The usart TX SFR uses DMA at 115200 8N1 so there is no cpu or interrupt data transfer load during the logging background (low-priority interrupt task) process that overlaps with daq DMA and main code operations.
C:
// low-pri interrupt ISR the runs every second for simple coulomb counting
void calc_bsoc(void)
{
    uint8_t * log_ptr;
    uint16_t temp;
#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);

    temp = ((uint16_t) ((C.dynamic_ah / C.bank_ah)*100.0) + 1);
    C.soc = (Volts_to_SOC((uint32_t) C.v_bat * 1000.0) + temp) / 2;
    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, " %4.3f,%4.3f,%4.3f,%4.3f,%4.3f,%4.3f,%4.3f,%4.3f,%4.3f,%4.3f,%4.3d,%4.3d\r\n",
        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);
    send_port_data_dma(strlen((char*) log_ptr));
    C.update = false;
#ifdef DEBUG_BSOC1
    DEBUG1_SetLow();
#endif
}

/*
* return pointer to internal data buffer for DMA
*/
uint8_t* port_data_dma_ptr(void)
{
    return port_data;
}

/*
* channel 2 DMA, serial port 1 transmit
*/
void init_port_dma(void)
{
    DMA2CON1bits.DMODE = 0;
    DMA2CON1bits.DSTP = 0;
    DMA2CON1bits.SMODE = 1;
    DMA2CON1bits.SMR = 0;
    DMA2CON1bits.SSTP = 1;
    DMA2CON0bits.SIRQEN = 0;
    DMA2DSA = 0x3DEA; // U1TXB SERIAL PORT 1
    DMA2SSA = (uint32_t) port_data;
    DMA2CON0bits.DGO = 0;
}

/*
* uses DMA channel 2 for transfers
*/
void send_port_data_dma(uint16_t dsize)
{
    if (dsize > max_port_data)
        dsize = max_port_data;

    DMA2CON0bits.EN = 0; /* disable DMA to change source count */
    DMA2SSZ = dsize;
    DMA2DSZ = 1;
    DMA2CON0bits.EN = 1; /* enable DMA */
    DMA2CON0bits.DMA2SIRQEN = 1; /* start DMA trigger */
}
What's left is to port the dynamic battery model/lifetime from the old code to adjust bank Ah and runtimes under dynamic loads.
https://github.com/nsaspook/vtouch_v2/tree/mbmc/touch_v2.X
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
8,907
Another chart of battery drain vs voltage at 150W load.

~38Ah drain per the measurement system.


The red line is the AC charger DC voltage. The 150W load is applied, the charger is turned off. First there is a steep drop-off in battery voltage due to battery plate surface charge depleting. This is what fools voltage based SOC testing without proper battery loads during the tests.
Next is a slow decline over several hours (22983 seconds) in the electrochemical reactions that generate the battery voltage until the load is shutdown at 23.9 volts, then you see the voltage recovery as the battery cells regain static equilibrium. The noise band you see on the two lines as they slowly drift downward is not from the measurement system ADC or processing noise. I suspect it's mainly hash from the inverter and some electrochemical noise from old flooded lead-acid batteries I'm testing the system with.
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
8,907
Next up is the load relay module for the interface board 24vdc output switch (uln2803).

Four 10 ohm resistor that can be selected in groups of 1, 3 or 4 via computer control for battery condition testing.

 
Top