FM80 solar charge controller datalogger

Thread Starter

nsaspook

Joined Aug 27, 2009
16,321
It's so nice to have a battery that's linear in the charge and discharge energy profile from near zero to 100%. It makes writing the energy tracking algorithm so easy, I'm done with lead-acid for energy storage.
Day one full battery charge profile. LiFePO4 is a near perfect energy sponge that's worth the extra money.
1687918588536.png
1687919891654.png
1687918657109.png
1687918775563.png

https://en.wikipedia.org/wiki/Lithium_iron_phosphate_battery
Better aging and cycle-life characteristics[edit]
LFP chemistry offers a considerably longer cycle life than other lithium-ion chemistries. Under most conditions it supports more than 3,000 cycles, and under optimal conditions it supports more than 10,000 cycles. NMC batteries support about 1,000 to 2,300 cycles, depending on conditions.[5]

LFP cells experience a slower rate of capacity loss (a.k.a. greater calendar-life) than lithium-ion battery chemistries such as cobalt (LiCoO
2) or manganese spinel (LiMn
2O
4) lithium-ion polymer batteries (LiPo battery) or lithium-ion batteries.[35]

Viable alternative to lead-acid batteries[edit]
Because of the nominal 3.2 V output, four cells can be placed in series for a nominal voltage of 12.8 V. This comes close to the nominal voltage of six-cell lead-acid batteries. Along with the good safety characteristics of LFP batteries, this makes LFP a good potential replacement for lead-acid batteries in applications such as automotive and solar applications, provided the charging systems are adapted not to damage the LFP cells through excessive charging voltages (beyond 3.6 volts DC per cell while under charge), temperature-based voltage compensation, equalisation attempts or continuous trickle charging. The LFP cells must be at least balanced initially before the pack is assembled and a protection system also needs to be implemented to ensure no cell can be discharged below a voltage of 2.5 V or severe damage will occur in most instances, due to irreversible deintercalation of LiFePO4 into FePO4.[36]
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
16,321
Working on CANBUS networking between control units. The basic 64-byte CAN-FD physical network is done so now it needs an application layer to share FlexMate data between units.
1688007411479.png
The interface board on the left is only connected 'C' to CANBUS (to the other board), the board on the right is connected 'BCM' to the FM80 charge controller, CANBUS and MODBUS (AC energy monitor).
 

Thread Starter

nsaspook

Joined Aug 27, 2009
16,321
A really cool battery terminal fuse set to replace the car audio ANL fuses currently used.
1688166486544.png
Also needed to fine-tune the FM80 MPPT to stop re-triggering the battery BMS when the battery is full and loads are at near full solar capacity. This is where a data-log/charting capability is really valuable to see what works.
PXL_20230630_195308643.jpg
The voltage would rise (at times triggering a over-volt warning on the inverter) after a MPPT reset to bulk charging. A simple adjustment on the Float and re-Bulk voltage thresholds stabilized the FM80 charge profile when the BMS sees the battery as full.
1688167111702.png
 

Thread Starter

nsaspook

Joined Aug 27, 2009
16,321
Finishing up the array end feed with a cheap combiner box. Fused inputs per string (using two of four string inputs), surge protector to Earth (rod/bonded ground) and master breaker. My first impression of quality is pretty good, just be sure to recheck/tighten every electrical connection in the box.
1688236891098.png
String VoC,.
1688236943756.png
String voltage at close to full load.
1688237024122.png
Charge controller voltage. I was going to use a remote voltage monitor inside the box but the losses are so low it's not worth the effort.
 

Thread Starter

nsaspook

Joined Aug 27, 2009
16,321
Testing the PIC18 energy tracking algorithm vs the battery BMS for battery full.
1688346001359.png
The orange line slope (240 -> 512) is the calculated amount of energy in the battery and the green line is the solar energy going into the battery to recharge it. The LiFePO4 battery internal BMS cuts off the charge just a little bit after my tracking calculations say it's full.
C:
#define IDLE_DRAIN    10.0f    // system operational losses in watts
#define INV_EFF_VAL    1.06f    // DC watts to AC watt inverter correction
#define BAT_EFF_VAL    0.985f    //  battery storage energy efficiency

*
* track energy usage and storage of the system
* with LiFePO4 battery chem this is simple, direct with no major secondary effects over the discharge/charge curve
*
*/
void compute_bm_data(EB_data * EB)
{
    float net_energy, net_balance;

    net_balance = EB->FMw - (EB->ENw * INV_EFF_VAL); // make the energy comparison AC -> DC watts equal using inverter losses
    if (net_balance > 0.0001) { // more energy from panels than current load usage
        net_balance = net_balance * BAT_EFF_VAL; // actual battery energy storage correction, energy in vs energy out losses
    } else {
        net_balance = net_balance; // net drain, inverter correction already applied: possible future second order corrections here
    }
    net_energy = EB->bat_energy + net_balance; // inverter/battery power conversion correction
    /*
     * set battery energy limits
     */
    EB->bat_energy = net_energy - IDLE_DRAIN; // system electronic power drain

    if (EB->bat_energy > BAT_ENERGY) { // limit up energy
        EB->bat_energy = BAT_ENERGY;
    }
    if (EB->bat_energy <= 0.0f) { // limit down energy
        EB->bat_energy = 0.0001f;
    }
}
The integration and logging time is every 10 seconds.
C:
    if (B.ten_sec_flag) {
        B.ten_sec_flag = false;
        if (B.mx80_online) {
            MM_ERROR_C;
            /*
             * log CSV values to the serial port for data storage and processing
             */
            printf("^^^,%d.%01d,%d.%01d,%d,%d.%01d,%d,%d,%.1f,%.1f,%.1f,%4.1f,%.2f,%u,%u\r\n"
                , abuf[3] - 128, abuf[1]&0x0f, vw, vf, abuf[2] - 128, volt_whole, volt_fract, panel_watts, cc_mode, ((float) em.wl1) / 10.0f, ((float) em.val1) / 10.0f, ((float) em.varl1) / 10.0f, ((float) em.vl1l2) / 10.0f, EBD.bat_energy / 3600.0f, EBD.bat_cycles, B.rx_count++);
            snprintf(can_buffer, MAX_B_BUF, "^^^,%d.%01d,%d.%01d,%d,%d.%01d,%d,%d,%.1f,%.1f,%.1f,%4.1f,%.2f,%u,%u\r\n"
                , abuf[3] - 128, abuf[1]&0x0f, vw, vf, abuf[2] - 128, volt_whole, volt_fract, panel_watts, cc_mode, ((float) em.wl1) / 10.0f, ((float) em.val1) / 10.0f, ((float) em.varl1) / 10.0f, ((float) em.vl1l2) / 10.0f, EBD.bat_energy / 3600.0f, EBD.bat_cycles, B.rx_count);
            snprintf(buffer, MAX_B_BUF, "%d Watts %d.%01d Volts   ", panel_watts, volt_whole, volt_fract);
            eaDogM_WriteStringAtPos(2, 0, buffer);
            bat_amp_whole = abuf[3] - 128;
            snprintf(buffer, MAX_B_BUF, "%d.%01d Amps %d.%01d Volts   ", bat_amp_whole, abuf[1]&0x0f, vw, vf);
            eaDogM_WriteStringAtPos(3, 0, buffer);
            can_fd_tx(); // send the logging packet via CANBUS
            get_bm_data(&EBD);
            compute_bm_data(&EBD); // calculate battery energy at 10 second update rate
            if (!EBD.loaded) // save a copy to EEPROM if it wasn't loaded at boot
            {
                EBD.loaded = true;
                wr_bm_data((void*) &EBD);
                MM_ERROR_S;
            }
            if ((EBD_update++ >= BM_UPDATE) || ((EBD_update >= BM_UPDATE_RUN) && (cc_mode != STATUS_SLEEPING))) {
                EBD.loaded = true;
                wr_bm_data((void*) &EBD);
                EBD_update = 0;
                MM_ERROR_S;
            }
        }
    }
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
16,321
Testing the PIC18 energy tracking algorithm vs the battery BMS for battery empty.
1688662824871.png
A few days of system energy tracking to battery zero, inverter low voltage cut-off and recovery. The saw-tooth chart line is battery energy, lime is battery load (one server). Completed the first day with a second day energy crash.
1688663013448.png
Logscale cut-off detail: Three server load nights: Blue is the system energy tracker trace that closely matched the 'red' battery voltage falling to the cut-off point, killing the inverter.
 

Thread Starter

nsaspook

Joined Aug 27, 2009
16,321
Setting up a Linux CANFD node to collect data from the controller network.
https://www.kernel.org/doc/html/latest/networking/can.html
Have a cheap PU2CANFD USB adapter that seems to be a Chinese clone of this judging from the log messages when plugged in.
[5519457.823470] usb 3-5.3: Manufacturer: PEAK-System Technik GmbH
[5519458.030573] CAN device driver interface
[5519458.054425] peak_usb 3-5.3:1.0: PEAK-System PCAN-USB FD v1 fw v3.2.0 (1 channels)
[5519458.059344] peak_usb 3-5.3:1.0 can0: attached to PCAN-USB FD channel 0 (device 2164195328)
[5519458.059386] usbcore: registered new interface driver peak_usb
We then need to configure and turn-on the network device for proper CAN-FD speeds of 1MHz for the preamble and 5MHz for data.
ip link set can0 up type can fd on bitrate 1000000 dbitrate 5000000
1688834787463.png

We then tested the setup with WireShark using a CANBUS cable tap (0.1 headers soldered) between the two 8-bit controllers.
1688834851341.png
1688836142100.png
Two CAN-FD data packets on the wire. ID 2 and ID 3 from the FM80 data collector.

1688836416756.png
The 5MHz clocked data bits on the wire look very good. That's the max speed of the transceiver connected to the Q84 can TX/RX pins. https://www.ti.com/lit/ds/symlink/tcan334.pdf Data rates up to 5 Mbps (TCAN33xG devices)

1688835031446.png
Boom! We have the data captured on the Linux system. There are can-utilities on Linux that can automate the capture and processing of the data to be saved in the house database server.

Had cool temps with some cloud cover causing sun glints on the ground that really upped the power for a bit to almost 1400W.
solar_power.png
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
16,321
Modifying a canbus utility written in C to read the socketcan network device on Linux using the PU2CANFD USB adapter.

You can read/write to the canbus using the normal UNIX socket library.
https://www.kernel.org/doc/html/latest/networking/can.html
C:
    printf("interface = %s, family = %d, type = %d, proto = %d\n",
        intf_name, family, type, proto);

    if ((sockfd = socket(family, type, proto)) < 0) {
        perror("socket");
        return 1;
    }

    if (echo_gen) {
        if (setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_RECV_OWN_MSGS,
            &enable_socket_option, sizeof(enable_socket_option)) == -1) {
            perror("setsockopt CAN_RAW_RECV_OWN_MSGS");
            return 1;
        }
    }

    if (is_can_fd) {
        if (setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FD_FRAMES,
            &enable_socket_option, sizeof(enable_socket_option)) == -1) {
            perror("setsockopt CAN_RAW_FD_FRAMES");
            return 1;
        }
    }

    addr.can_family = family;
    addr.can_ifindex = if_nametoindex(intf_name);
    if (!addr.can_ifindex) {
        perror("if_nametoindex");
        close(sockfd);
        return 1;
    }

    if (bind(sockfd, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
        perror("bind");
        close(sockfd);
        return 1;
    }
1691852332513.png
https://github.com/nsaspook/Q84vtou...nt.com_linux-can_can-utils_master_canfdtest.c
Logging data from the FM80 monitor CANBUS 10 second data record broadcast.
 

Thread Starter

nsaspook

Joined Aug 27, 2009
16,321
Finally trenched the solar panel conduit before the weather changes.
1694712436500.png

Software, competed most of the basic data logging with the Linux system saving daily logs and stored day history from the FM80 from the PIC18 monitor via CANBUS. The Linux server CANBUS connection also serves accurate time (UNIX 32-bit) for the monitor that's reformatted to the strange FM80 16-bit register format so it can keep the correct time.
C:
/*
 * pack UNIX tm time/date into FM80 compatible 16-bit values
 */
void update_time(struct tm * ts, EB_data * EB)
{
    EB->time = (
        (uint16_t) ((ts->tm_hour & 0x1F) << 11) |
        (uint16_t) ((ts->tm_min & 0x3F) << 5) |
        (uint16_t) ((ts->tm_sec & 0x1F) >> 1)
        );
    EB->date = (
        (uint16_t) (((ts->tm_year - 2000) & 0x7F) << 9) |
        (uint16_t) ((ts->tm_mon & 0x0F) << 5) |
        (uint16_t) (ts->tm_mday & 0x1F)
        );
}
1694712838005.png
Lines that begin with '#' are history logs where the last number on the line is the number of days from today's date seen in the daily log '^' marked lines.
C:
        case 'L':
        case 'l':
            B.log.select++; // pull the next days log data
            snprintf(s_buffer, 21, "Pwr %5.2fkWpk %5.2fkWh              ", (float) B.log.kilowatts_peak / 1000.0, (float) B.log.kilowatt_hours / 10.0);
            eaDogM_Scroll_String(s_buffer);
            if (rxData == 'L') {
                snprintf(s_buffer, 21, "#,%5.2f,%5.2f,", (float) B.log.kilowatts_peak / 1000.0, (float) B.log.kilowatt_hours / 10.0);
                printf("%s", s_buffer);
            }
            snprintf(s_buffer, 21, "Chg F%imin A%imin                ", B.log.float_time, B.log.absorb_time);
            eaDogM_Scroll_String(s_buffer);
            if (rxData == 'L') {
                snprintf(s_buffer, 21, "%i,%i,", B.log.float_time, B.log.absorb_time);
                printf("%s", s_buffer);
            }
            snprintf(s_buffer, 21, "%iVpk %4.1fApk %iAh       ", B.log.volts_peak, (float) B.log.amps_peak / 10.0, B.log.amp_hours);
            eaDogM_Scroll_String(s_buffer);
            if (rxData == 'L') {
                snprintf(s_buffer, 21, "%i,%4.1f,%i,", B.log.volts_peak, (float) B.log.amps_peak / 10.0, B.log.amp_hours);
                printf("%s", s_buffer);
            }
            snprintf(s_buffer, 21, "%4.1fV %4.1fV :Day %i       ", (float) B.log.bat_max / 10.0, (float) B.log.bat_min / 10.0, B.log.day);
            eaDogM_Scroll_String(s_buffer);
            if (rxData == 'L') {
                snprintf(s_buffer, 21, "%4.1f,%4.1f,%i,", (float) B.log.bat_max / 10.0, (float) B.log.bat_min / 10.0, B.log.day);
                printf("%s^\r\n", s_buffer);
            }
            break;
 

Thread Starter

nsaspook

Joined Aug 27, 2009
16,321
Added a few functions for the remote Q84 monitor to mirror the active FM80 monitor's Q84 display, The data buffer is simply transferred via CANBUS to any unit on the bus.
1695526598646.png
Improved I/O concurrency by moving the LCD display updates to DMA to SPI. The LCD SPI speed is slow (50kbs) and I did't want to add to the interrupt load of the 8-bit controller with an interrupt driven bus master driver.
C:
/*
* Init the NHD-0420D3Z-NSW-BBW-V3 in 8-bit serial mode
* channel 1 DMA if configured
*/
bool init_display(void)
{
    spi_link.txbuf = lcd_dma_buf; // use MCC DMA buffer variable
    memset(Sstr, ' ', NSB * LSB); // clear scroll buffer of junk

#ifdef USE_LCD_DMA
    DMA1_SetSCNTIInterruptHandler(clear_lcd_done);
    DMA1_SetORIInterruptHandler(spi_byte);
    DMA1_SetDMAPriority(2);
#endif
#ifdef NHD  // uses MODE 3 on the Q84, https://newhavendisplay.com/content/specs/NHD-0420D3Z-NSW-BBW-V3.pdf
#ifdef USEMCC_SPI
#else
    SPI1CON0bits.EN = 0;
    // mode 3
    SPI1CON1 = 0x20;
    // SSET disabled; RXR suspended if the RxFIFO is full; TXR required for a transfer;
    SPI1CON2 = 0x03;
    // BAUD 0;
    SPI1BAUD = 0x04; // 50kHz SCK
    // CLKSEL MFINTOSC;
    SPI1CLK = 0x02;
    // BMODE every byte; LSBF MSb first; EN enabled; MST bus master;
    SPI1CON0 = 0x83;
    SPI1CON0bits.EN = 1;
#endif
    if (powerup) {
        wdtdelay(350000); // > 400ms power up delay
    }

#ifdef USE_LCD_DMA
    SPI1INTFbits.SPI1TXUIF = 0;
    DMASELECT = 0; // use DMA1
    DMAnCON0bits.EN = 0;
    SPI1CON0bits.EN = 0;
    SPI1CON2 = 0x02; //  Received data is not stored in the FIFO
    SPI1CON0bits.EN = 1;
    DMAnCON1bits.DMODE = 0;
    DMAnCON1bits.DSTP = 0;
    DMAnCON1bits.SMODE = 1;
    DMAnCON1bits.SMR = 0;
    DMAnCON1bits.SSTP = 1;
    DMAnSSA = (uint24_t) spi_link.txbuf;
    DMAnCON0bits.DGO = 0;
    DMAnCON0bits.EN = 1; /* enable DMA */
    SPI1INTFbits.SPI1TXUIF = 1;
    send_lcd_cmd_dma(LCD_CMD_BRI); // set back-light level
    send_lcd_data_dma(NHD_BL_LOW);
    send_lcd_cmd_dma(LCD_CMD_CONT); // set display contrast
    send_lcd_data_dma(NHD_CONT);
    send_lcd_cmd_dma(LCD_CMD_ON); // display on
    send_lcd_cmd_dma(LCD_CMD_CLR); // clear screen
    wdtdelay(800);
    DMA1_StopTransfer();
#else
    send_lcd_cmd_long(LCD_CMD_HOME); // home cursor
    send_lcd_cmd(LCD_CMD_ON); // display on
    wdtdelay(80);
    send_lcd_cmd(NHD_BL_MED); // set back-light level
    send_lcd_data(NHD_BL_LOW);
    wdtdelay(80);
    send_lcd_cmd_long(LCD_CMD_CLR); // clear screen
#endif
#endif
    powerup = false; // only of the first display init call
    return true;
}
This allows overlapping LCD process with other I/O loads like the FM80 serial data stream. Purple trace is the LCD spi data, yellow FM80 data. The DMA setup and start timing for LCD update transmission start are a few tens of microseconds for each line.
1695527152189.png
C:
void eaDogM_WriteStringAtPos(const uint8_t r, const uint8_t c, char *strPtr)
{
    uint8_t row;

    if (scroll_lock) { // don't update LCD text when in scroll mode
        return;
    }

    switch (r) {
    case LCD1:
        row = 0x40;
        break;
    case LCD2:
        row = 0x14;
        break;
    case LCD3:
        row = 0x54;
        break;
    case LCD0:
        row = 0x00;
        break;
    default:
        row = 0x00;
        break;
    }

#ifdef USE_LCD_DMA
    send_lcd_pos_dma(row + c);
#else
    send_lcd_cmd(0x45);
    send_lcd_data(row + c);
#endif
    can_fd_lcd_mirror(r, strPtr);
    eaDogM_WriteString(strPtr);
}

/*
* CAN use DMA channel 1 for transfers
*/
void eaDogM_WriteString(char *strPtr)
{
    uint8_t len = (uint8_t) strlen(strPtr);

    wait_lcd_done();
    wait_lcd_set();
    CS_SetLow(); /* SPI select display */
    if (len > (uint8_t) max_strlen) {
        len = max_strlen;
    }
    memcpy(spi_link.txbuf, strPtr, len);
#ifdef USE_LCD_DMA
    DMAnCON0bits.EN = 0; /* disable DMA to change source count */
    DMA1_SetSourceSize(len);
    DMA1_SetDestinationSize(1);
    DMAnCON0bits.EN = 1; /* enable DMA */
#else
    SPI1_ExchangeBlock(spi_link.txbuf, len);
#endif
    start_lcd(); // start DMA transfer
}
Purple trace is the LCD spi data (50 kbps), yellow CANBUS data (5 Mbps).
1695527304005.png
1695527623010.png
Several LCD display update data bursts.

Purple trace is the LCD spi data (50 kbps), yellow MODBUS data 112 kbps).
1695561057035.png
The day started out fairly clear but started raining late in the day.
1695527815976.png
The gap to zero on the PV Panel Watts line is when the LiFePO4 battery BMS switched to float mode when it detected that battery was fully charged.
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
16,321
System run-time display and data-log graphs.
1696122913992.png
In the runtime display mode. 41.65 hours at the current system energy drain of -120.17W. There is a little solar energy on the panels to offset the system load of 137.9W.
1696123105968.png
10 second data point graph. Run-time max limit is 280 hours.
 

Thread Starter

nsaspook

Joined Aug 27, 2009
16,321
A little something with the Q84 CLC: https://ww1.microchip.com/downloads/en/Appnotes/90003133A.pdf
The chip has eight CLC units so I used two to make a I/O output and timer 'heart-beat' signal.
clc8.png
clc7.png
The output of CLC7 is assigned to a gpio output pin for capture.

1696621891382.png
1696623455360.png
Traces 2 & 3 are the 7 input to composite CLC outputs from the main and remote FM80 controllers. Not really useful for much but they were not being used before.
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
16,321
Remote CANBUS display mounted in an enclosure.
1696641571204.png
Zulkit Electronic Enclosures Blue Metal Enclosure Project Case DIY Box Junction Case Enclosure Preventive Case 6.7 x 4.3 x 3.1 inch(170 x130x80mm)
1696641608354.png1696642133165.png
Once it's done, I will hot glue the PCB to the MOD hold-down sticky block clips.
1696641629264.png
1696641653696.png
Need to print a thin bezel for it.
 

Thread Starter

nsaspook

Joined Aug 27, 2009
16,321
Rain and cloud cover for the eclipse today in Oregon. Too far north for the full effect but I could see it on the solar panel monitor.
1697302175571.png
1697303709452.png
We dropped out of the small morning solar energy production and the panels were in open circuit voltage monitor mode during the sun dip. The voltage spikes are the charge controller testing MPPT and finding almost no power.
 
Last edited:

Thread Starter

nsaspook

Joined Aug 27, 2009
16,321
Switch contact interface. Uses the port IOC - Interrupt-on-Change for individual pin so we don't need to poll, each switch press is detected and de-bounced when the interrupt is triggered. Can detect and de-bounce NO or NC switch contacts.
Overview The pins denoted in the table below can be configured to operate as Interrupt-on-Change (IOC) pins for this device. An interrupt can be generated by detecting a signal that has either a rising edge or a falling edge. Any individual PORT pin, or combination of PORT pins, can be configured to generate an interrupt.
...
A rising edge detector and a falling edge detector are present for each PORT pin. To enable a pin to detect a rising edge, the associated bit of the IOCxP register must be set. To enable a pin to detect a falling edge, the associated bit of the IOCxN register must be set. A PORT pin can be configured to detect rising and falling edges simultaneously by setting both associated bits of the IOCxP and IOCxN registers, respectively
C:
#ifndef DIO_H
#define    DIO_H

#ifdef    __cplusplus
extern "C" {
#endif

#include <xc.h>
#include "mateQ84.X/mcc_generated_files/pin_manager.h"

   // other stuff

    /*
     * assign a physical pin for each switch
     * ANALOG header
     * A: pin 5
     * L: pin 2
     * M: pin 1
     */
#define D_SW_A_PIN    A_SWITCH_GetValue()
#define D_SW_L_PIN    L_SWITCH_GetValue()
#define D_SW_M_PIN    M_SWITCH_GetValue()

#define debounce_time    200    // debounce counts, at least a few tens of milliseconds for a typical switch

#define SW_NO    0    // switch, normally open
#define SW_NC    1    // switch, normally closed

    /*
     * switch inputs and flags, uses the IOC interrupt and the software 
     * timing ISR for processing
     */
    enum D_SW {
        D_SW_A = 0,
        D_SW_L,
        D_SW_M,
        D_SW_COUNT // one extra for number of switches to check
    };

    /*
     * contact type per configured switch
     * used to verify the de-bounced switch condition for setting the button flag
     */
    const uint8_t sw_contact_types[D_SW_COUNT] = {
        SW_NO,
        SW_NO,
        SW_NC,
    };

    void init_all_switch(void); // setup data and interrupt functions for each switch
    void button_press_check(void); // for de-bounce timer ISR

    void all_relays_off(void);

#ifdef    __cplusplus
}
#endif

#endif    /* DIO_H */
C:
#include "dio.h"
#include "mateQ84.X/mxcmd.h"

static uint8_t a_debounce[D_SW_COUNT];
static uint8_t get_d_switch(uint8_t i);

static void aswitch(void);
static void lswitch(void);
static void mswitch(void);

// other stuff

/*
 * switch IOC handlers per PIN
 */
static void aswitch(void)
{
    B.a_trigger[D_SW_A] = true;
}

static void lswitch(void)
{
    B.a_trigger[D_SW_L] = true;
}

static void mswitch(void)
{
    B.a_trigger[D_SW_M] = true;
}

void init_all_switch(void)
{
    for (uint8_t i = 0; i < D_SW_COUNT; i++) {
        B.a_type[i] = sw_contact_types[i];
        B.a_trigger[i] = false;
        B.a_switch[i] = false;
        a_debounce[i] = 0;
    }
    IOCAF5_SetInterruptHandler(aswitch);
    IOCAF2_SetInterruptHandler(lswitch);
    IOCAF1_SetInterruptHandler(mswitch);
}

/*
 * debounce button pin status parser
 */
static uint8_t get_d_switch(uint8_t i)
{
    switch (i) {
    case D_SW_A:
        return D_SW_A_PIN;
        break;
    case D_SW_L:
        return D_SW_L_PIN;
        break;
    case D_SW_M:
        return D_SW_M_PIN;
        break;
    default:
        return D_SW_A_PIN;
        break;
    }
}

/*
 * button checking/de-bounce routine for 500us timing ISR , runs in software timer interrupt ISR timer #4
 */
void button_press_check(void)
{
    /*
     * check for button presses
     */
    for (uint8_t i = 0; i < D_SW_COUNT; i++) {
        if (B.a_trigger[i]) {
            if ((++a_debounce[i] > debounce_time) && (get_d_switch(i) == B.a_type[i])) {
                a_debounce[i] = 0;
                B.a_trigger[i] = false;
                B.a_switch[i] = true;
            }
        } else {
            a_debounce[i] = 0;
        }
    }
}
C:
/*
 * runs in timer #4 interrupt from FM_io(void)
 */
void timer_ms_tick(const uint32_t status, const uintptr_t context)
{
    //Decrement each software timer
    for (uint16_t i = 0; i < TMR_COUNT; i++) {
        if (tickCount[i] != 0) {
            tickCount[i]--;
        }
    }
    /*
     * check for button presses
     */
    button_press_check();
}

/*
 * 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) {
    INT_TRACE; // GPIO interrupt scope trace

    if (pace++ > BUFFER_SPACING) {
        if (dcount-- > 0) {
            if (tbuf[dstart] > 0xff) { // Check for bit-9
                U1P1L = (uint8_t) tbuf[dstart]; // send with bit-9 high, start of packet
            } 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) {
            // do nothing, will clear auto
        }

        if (rdstart > FM_BUFFER - 1) { // overload buffer index
            rdstart = 0; // reset buffer to start
            MLED_SetHigh();
        }
        if (U1ERRIRbits.PERIF) {
            rdstart = 0; // restart receive buffer when we see a 9-th bit high
            rbuf[rdstart] = 0x0100; // start of packet, bit 9 set
        } else {
            rbuf[rdstart] = 0x00;
        }
        rbuf[rdstart] += U1RXB;
        rdstart++;
    }

    timer_ms_tick(0, 0); // software timers update
}
RED button switch NC contacts.
BLACK button switch NO contacts.
1697738164069.png

RigolDS16.png
RED button NC bounce.
RigolDS17.png
BLACK button NO bounce, much nicer.
 

Attachments

Thread Starter

nsaspook

Joined Aug 27, 2009
16,321
My kid put the energy bank into under-volt protection by plugging in an electric heater to the inverter AC power line while talking on the phone in the shop at night. How could he just ignore the beeping before power shut off? If you ever go off-grid, have a good genset as a backup, you will need it.
1702048201557.png
The green line is the power balance, 1500W until tripped. Red is Ah used. I have a utility charged LA battery bank manual cross-connected as a backup to recharge at bit before the sun rises. It's been raining a months worth of rain for the last week, so the solar bank wasn't at full energy.

Cross connect charging current to LiFePO4 bank.
1702049010923.png
 
Last edited:
Top