ArduinoBMS/ArduinoBMS.ino

1021 lines
47 KiB
Arduino
Raw Permalink Normal View History

2024-08-22 09:26:48 +00:00
/* ArduinoBMS
* ----------
* This is a very basic Lithium Battery Management System. It can either function as a stand alone BMS or as a watchdog BMS to overrule the primary BMS if somehow the cell voltage limits are exceeded.
*
* More details and explanation of this project can be found at https://www.thefloatinglab.world/en/arduinobms.html
* This project is developed and maintained at https://git.thefloatinglab.world/thefloatinglab/arduinobms
*
* License
* -------
* ArduinoBMS, a very basic Lithium Battery Management System.
* Copyright 2024 Frans Veldman
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* Objective
* ---------
* Objective was to create a very basic yet functional BMS based on an Arduino architecture, without external libraries, and with as few expernal parts as possible.
* The idea was born when I quickly had to cobble a BMS together to protect an on board lithium battery for overcharging as well als overdischarging.
* Roaming around in some atoll in the South Pacific, an order for parts would take weeks if not months to get delivered, so it had to be achieved with whatever I had availabe on board.
*
* This project shows that it can be done with just an Arduino, a few resistors, and a transistor or MosFet to control the battery disconnect relais.
*
* Optional are a 1602A LCD display and/or some external LEDs.
*
* More details and a list of features can be found at https://www.thefloatinglab.world/en/arduinobms.html
*/
#include "ArduinoBMS.h"
#include <avr/eeprom.h>
#include <avr/sleep.h>
#include <avr/wdt.h>
#include <avr/power.h>
#include "util/delay.h"
#if LCD>0
#include <LiquidCrystal.h>
LiquidCrystal lcd(LCD_RS, LCD_EN, LCD_4, LCD_5, LCD_6, LCD_7);
#endif
// *********** Structures *****************
cal_t cal;
// These are the ADC structures
adc_t arefv = {bit (REFS0) | bit (MUX3) | bit (MUX2) | bit (MUX1), true, 0, 0, &cal.arefv, 0, nullptr, 0,0,0,0,0};
adc_t cell1 = {bit (REFS0) | (CELL1 & 7), false, 0, 0, &cal.cell1, 0, nullptr, 0,0,0,0,0};
adc_t cell2 = {bit (REFS0) | (CELL2 & 7), false, 0, 0, &cal.cell2, 0, &cell1, 0,0,0,0,0};
adc_t cell3 = {bit (REFS0) | (CELL3 & 7), false, 0, 0, &cal.cell3, 0, &cell2, 0,0,0,0,0};
adc_t cell4 = {bit (REFS0) | (CELL4 & 7), false, 0, 0, &cal.cell4, 0, &cell3, 0,0,0,0,0};
adc_t busv = {bit (REFS0) | (BUSV & 7), false, 0, 0, &cal.busv, 0, nullptr, 0,0,0,0,0};
// Put pointers to the ADC cell structures in an array for convenience
adc_t* cells[4]={&cell1,&cell2,&cell3,&cell4};
// This project has only one button, but we use a structure anyway for uniformity
button_t button = {BUTTON, false, 0};
// The relay structures hold the data associated with each relay
relay_t relay = {BUS_RELAY_ON, BUS_RELAY_OFF, false, 0, RELAYHOLD, RELAYPWM};
relay_t* buserelay = &relay;
#if DUALBUS>0
relay_t crelay = {CHG_RELAY_ON, CHG_RELAY_OFF, false, 0, RELAYHOLD, RELAYPWM};
relay_t* chargerelay = &crelay;
#else
// If we have a single bus installation, both the chargerelay and busrelay pointers point to the same relay
relay_t* chargerelay = &relay;
#endif
// ********** Global variables ********************
// The "nogo" variables maintain the "nogo" flags. In general, if a nogo is zero, it means the feature can run.
// So if the "charge_nogo" variable is zero, charging is enabled.
// Various routines can set a "nogo" flag, and they are responsible for clearing it when the "nogo" condition ceases to exist
uint16_t charge_nogo = X_STOPBUTTON; // Change to zero if you want the unit to auto start after a reboot
uint16_t discharge_nogo = X_STOPBUTTON; // Change to zero if you want the unit to auto start after a reboot
// The pwm-active variable keeps a record whether a port is putting out a PWM signal.
// We need to know that, because when a PWM signal is active, we shouldn't put the processor in a deep sleep.
uint16_t pwm_active = 0;
float highestCellVoltage;
float lowestCellVoltage;
float libattvoltage;
// As most of the time the processor will be in sleep mode, and hence the timer will be stopped, we need to keep track of the time ourself.
// In Setup, we measure the time it takes to read the voltages. We simply add this at every iteration to our pseudo timer.
// We also add the time spent when in deep sleep mode.
// It is not terribly precise, but we don't need a precise timing in this application.
uint32_t _millis=0;
uint32_t timeToReadVoltages=0;
//uint16_t _micros=0;
// *********** Interrupt Service Routines *************
// external interrupt 0 wakes the MCU
EMPTY_INTERRUPT(INT0_vect);
// when ADC completed, this interrupt is fired
EMPTY_INTERRUPT(ADC_vect);
// Watchdog interrupt for the sleep timer
ISR(WDT_vect) {
wdt_disable(); // disable watchdog
}
// ************ Replacement routines *****************
// This routine replaces the default analogWrite routine.
// Main feature is that it keeps track whether there is a PWM port active.
// If there is a PWM port active, we will not put the processor into deep sleep modes, as this would inhibit the PWM output
void _analogWrite(const uint8_t port, const uint8_t value) {
if(value==0) { // If we want it to be completely off
digitalWrite(port,LOW); // make it a digital write
pwm_active &= ~ (1<<port); // Flag this PWM port as non-active
} else
if(value>=255) { // If we want it to be completely on
digitalWrite(port,HIGH); // make it a digital write
pwm_active &= ~ (1<<port); // Flag this PWM port as non-active
} else {
analogWrite(port,value); // Ok, it is going to be a PWM output
pwm_active |= (1<<port); // Flag this PWM port as active
}
}
// *************** LCD variables & routines **********************
#if LCD>0
char dispbuff[(LCD_COLS + 1)*LCD_ROWS];
void setBackLight(const uint8_t value) {
if(value) {
pinMode(DISPBRIGHT,OUTPUT); // To output anything, set the port as an output
_analogWrite(DISPBRIGHT,value); // The rest will be handled by our analogWrite routine
}
else // We want the backlight to be completely off
pinMode(DISPBRIGHT,INPUT); // Set the port to input mode. We can then optionally use a resistor to let it remain faintly lit.
}
// This routine displays a message string from program memory on the LCD
const char* lcdDisplay(const char *txt) {
byte d = 0;
char c = pgm_read_byte_near(txt);
if (c == '^') { // We use this character for multi row messages
d += LCD_COLS + 1;
txt++;
c = pgm_read_byte_near(txt);
}
while (c) {
if (c == '^') { // Before going to the next row, empty the rest of the line
while (d % (LCD_COLS + 1) != 0)
dispbuff[d++] = ' ';
dispbuff[d] = 0;
}
else
dispbuff[d++] = c;
txt++;
c = pgm_read_byte_near(txt);
}
txt++;
return txt;
}
// This routine converts a FlashStringHelper msg to a program memory point and then uses the lcdDisplay routine above to display it
const char* lcdDisplay(const __FlashStringHelper *ifsh) {
const char *txt = (const char PROGMEM *)ifsh;
return lcdDisplay(txt);
}
void lcdUpdate() {
lcd.setCursor(0, 0);
lcd.print(dispbuff);
lcd.setCursor(0, 1);
lcd.print(dispbuff + LCD_COLS + 1);
}
#else
// Dummy routines in case we have no LCD
void setBackLight(const uint8_t value) {}
void lcdDisplay(const char *txt) {}
void lcdDisplay(const __FlashStringHelper *ifsh) {}
void lcdUpdate() {}
#endif
// *************** Low level ADC routines *****************
// This function fires the ADC conversion, then puts the processor into sleep mode, until the ADC conversion is ready
void getADCconversion(void) {
cli();
if(!pwm_active) // No PWM going, we can power down everything (except the ADC)
set_sleep_mode (SLEEP_MODE_ADC); // keep the ADC on during sleep
else // We need to get the PWM going
set_sleep_mode (SLEEP_MODE_IDLE); // keep everything on during sleep
sleep_enable();
ADCSRA |= bit (ADSC) | bit (ADIE); // start the conversion and go to sleep
interrupts(); // Make sure we can wake up again
sleep_cpu(); // Go to sleep
sleep_disable(); // We woke up, disable sleep
// awake again, an interrupt has fired
// in case another interrupt fired we woke up too early, but then we will just wait for the ADC conversion to be finished
while (bit_is_set (ADCSRA, ADSC)) {}
}
// This is an alternative to the analogRead() function, that allows the processor to sleep during the measurement.
// This reduces power consumption and gives a more stable ADC reading
uint16_t analogReadSleep(adc_t* adc) {
static uint8_t admux=0;
static uint8_t prevdelay=0;
ADCSRA = bit (ADEN) | bit (ADIF); // enable ADC, turn off any pending interrupt
ADCSRA |= bit (ADPS1) | bit (ADPS2); // prescaler of 64
if(admux!=adc->admux) { // Was the previous read on a different port?
ADMUX = adc->admux; // Then setup the ADMUX register
admux=adc->admux;
if(prevdelay || adc->delay) // Did the previous one or this one need a delay?
_delay_ms(2); // Inline delay (function "delay()" is not available)
prevdelay=adc->delay; // And remember it
getADCconversion(); // take one sample and throw it away
}
getADCconversion(); // Take a sample
return ADC; // Return the sample
}
// This function samples an ADC reading with optional oversampling.
void readADC(adc_t* adc, const uint8_t adc_res) {
uint8_t smpls=0; // Number of samples done
uint16_t sum=0; // We use a 16 bit integer here, so we can sum up only 64 ADC values without overflow. This corresponds to a maximum of 13 bits.
uint16_t valid_sum=0; // This holds the last valid sum of samples
bool failed=false;
uint8_t skips=0;
// We start with 10 bits, then 11, then 12, then 13.
uint8_t resolution=9; // one less because we want to exit with the correct value
while(resolution<adc_res) { // Continue until we achieved the desired oversampling
while(smpls < 1<<((resolution-9)*2)) { // Get the amount of samples we need for this step
uint16_t sample = analogReadSleep(adc);
// The goal is not to average here, but to get a valid oversampled result
// We are testing on a live system, so if the charge or discharge current changes abruptly, the result would be invalid
// There might also be a current spike
// So we test the sample to see if it doesn't deviate too much from the average, we can try a few times again, but otherwise we have to reject the attempt
while(smpls!=0 && abs((int16_t)(sum/smpls)-(int16_t)sample) > MAX_ADC_VARIATION && skips<MAX_ADC_SKIPS) {
sample = analogReadSleep(adc);
skips++;
}
if(skips>=MAX_ADC_SKIPS) {
failed=true; // Sample deviates too much
break; // so leave it for now and use the lower resolution we already had
}
sum+=sample; // Add this sample to the sum
smpls++;
}
if(failed)
break; // Failure, break out
// We successfully finished this resolution
valid_sum=sum; // We finishde this resolution, save what we have so far
resolution++; // try next resolution
}
// When we arrive here, we have the resolution in "resolution" and the sum in "valid_sum"s;
valid_sum >>= (resolution-10); // get rid of the unwanted bits
// We now have a number up to 1024 for 10 bits, 2048 for 11 bits, 4096 for 12 bits, 8192 for 13 bits
// Now let's scale them to the requested number of bits
valid_sum <<= (adc_res - resolution);
// If the resolution was lower than requested, add half of the step to the result
if(resolution<adc_res)
valid_sum |= 1<<((adc_res-resolution)-1);
// So, if the requested resolution was 13 bits, we scale up to 8192:
// in case of 13 bits, valid_sum >> 3, then valid_sum << 0 (>>3)
// in case of 12 bits, valid_sum >> 2, then valid_sum << 1 (>>1)
// in case of 11 bits, valid_sum >> 1, then valid_sum << 2 (<<1)
// in case of 10 bits, valid_sum >> 0, then valid_sum << 3 (<<3)
adc->raw=valid_sum;
adc->resolution=resolution;
adc->totalreads++; // Just some statistics
if(resolution==adc_res)
adc->totalresol++; // Just some statistics
}
void calcVoltage(adc_t* adc) {
// Some bit shifting to preserve as much precision while doing the math in 32 bits rather than floats
uint16_t val = (uint16_t)(( ( (((uint32_t)adc->raw)<<10) / (uint32_t)*adc->cal) * ( (((uint32_t)cal.arefv)<<10) / (uint32_t)arefv.raw))>>8);
// Now let's do some averaging. We need to do this because we don't measure the bus voltage and cell voltages all at the same time.
// A heavy load may come on or disappear somewhere in between. We don't overdo it, averaging over 4 samples is enough to take care of this.
// 4 Samples will fit into a 16 bit int, if you want more samples, you either have to adjust the arithmetic above or use a 32bit int for the sum.
if(adc->samples<AVG)
adc->samples++; // This is going to be an additional sample
else
adc->sum-=adc->avg; // Max amount of samples reached, substract average to make room for new value
adc->sum+=val; // Add the new value to the sum
adc->avg=adc->sum/(uint32_t)adc->samples; // Average is the sum divided by the amount of samples
adc->voltage = ((float)adc->avg)*CALVOLTAGE*CALIBRATION/(float)(1<<12);
}
// **************** Higher level voltage processing routines *********************
void getRaw(adc_t* adc) {
readADC(&arefv,ADC_RES); // Read reference voltage
readADC(adc,ADC_RES); // Read requested voltage
}
// Request a complete voltage reading
void getVoltage(adc_t* adc) {
getRaw(adc);
calcVoltage(adc); // Convert the ADC reading into a calibrated voltage
}
// Convert voltage readings into cell voltages, by substracting the voltage of the cell below
float cellVoltage(adc_t* adc) {
if(adc->stacksum != nullptr) // If we have a lower cell
return adc>voltage - adc->stacksum->voltage; // Substract the measured voltage
return adc->voltage;
}
// ***************** Voltage monitor routines **********************
void SerialPrintCellHeader(const uint8_t index) {
Serial.print(F(", Cell"));
Serial.print(index+1);
Serial.print(F(": "));
}
void print_status() {
Serial.print(F("Voltages: (correction factor is: "));
Serial.print((float)cal.arefv/(float)arefv.raw,3);
Serial.print(F(")"));
for(uint8_t i=0;i<4;i++) {
SerialPrintCellHeader(i);
Serial.print(cellVoltage(cells[i]),3);
}
Serial.print(F(", BusV: "));
Serial.println(busv.voltage,3);
Serial.print(F("Full resolution percentages: Aref: "));
Serial.print((arefv.totalresol*100)/arefv.totalreads);
Serial.print(F("%"));
arefv.totalreads=0; arefv.totalresol=0;
for(uint8_t i=0;i<4;i++) {
SerialPrintCellHeader(i);
Serial.print((cells[i]->totalresol*100)/cells[i]->totalreads);
Serial.print(F("%"));
cells[i]->totalresol=0; cells[i]->totalreads=0;
}
Serial.print(F(", BusV: "));
Serial.print((busv.totalresol*100)/busv.totalreads);
Serial.println(F("%"));
busv.totalreads=0; busv.totalresol=0;
}
void monRaw(adc_t* adc) {
delay(2); // allow serial.print to finish before entering sleep mode
readADC(adc,ADC_RES);
calcVoltage(adc);
Serial.print(adc->raw);
if(adc->resolution!=ADC_RES) {
Serial.print(" (@");
Serial.print(adc->resolution);
Serial.print(")");
}
}
void monitor(const bool relative) {
while(true) {
digitalWrite(LED,!digitalRead(LED)); // Toggle the LED. We might see a variation in the output due to the current draw
delay(5); // Wait for the current to settle
Serial.print(F("Readings: ArefV: "));
readADC(&arefv,ADC_RES);
Serial.print(arefv.raw);
for(uint8_t i=0;i<4;i++) {
SerialPrintCellHeader(i);
monRaw(cells[i]);
}
Serial.print(F(", BusV: "));
monRaw(&busv);
Serial.println();
print_status();
#if LCD>0
if(relative) {
// Display the 4 relative cellvoltages
dtostrf(cellVoltage(&cell1),8,3,dispbuff);
dtostrf(cellVoltage(&cell2),8,3,dispbuff+LCD_COLS/2);
dtostrf(cellVoltage(&cell3),8,3,dispbuff+LCD_COLS+1);
dtostrf(cellVoltage(&cell4),8,3,dispbuff+LCD_COLS+LCD_COLS/2+1);
} else {
// Display the 4 absolute cellvoltages
dtostrf(cell1.voltage,8,3,dispbuff);
dtostrf(cell2.voltage,8,3,dispbuff+LCD_COLS/2);
dtostrf(cell3.voltage,8,3,dispbuff+LCD_COLS+1);
dtostrf(cell4.voltage,8,3,dispbuff+LCD_COLS+LCD_COLS/2+1);
}
lcdUpdate();
#endif
delay(5000);
}
}
// Get a sample at the requested resolution.
uint8_t calsample(const adc_t* adc) {
uint8_t attempts=0;
do {
if(attempts>MAXCALATTEMPTS)
return 1;
attempts++;
readADC(adc,ADC_RES);
} while(adc->resolution!=ADC_RES); // Keep trying until we get the desired resolution.
return 0;
}
void calibrate() {
Serial.println(F("Calibrating..."));
lcdDisplay(F("Calibrating^Please wait.."));
lcdUpdate();
delay(1000); // Give time for the LCD message to be read
setBackLight(LCD_LO_BRIGHT); // Get the samples with the LCD in low brightness
uint32_t arefvsum=0; uint32_t cell1sum=0; uint32_t cell2sum=0; uint32_t cell3sum=0; uint32_t cell4sum=0; uint32_t busvsum=0;
uint8_t error=0;
uint8_t samples=0;
while(samples<CALSAMPLES && !error) {
digitalWrite(LED,!digitalRead(LED)); // Toggle the LED, just in case it makes a difference in the measurements
delay(5+samples/2); // Introduce a variable delay so we don't synchronize on some power supply ripple, hum or PWM signals
error+=calsample(&arefv); arefvsum+=(uint32_t)arefv.raw;
error+=calsample(&cell1); cell1sum+=(uint32_t)cell1.raw;
error+=calsample(&cell2); cell2sum+=(uint32_t)cell2.raw;
error+=calsample(&cell3); cell3sum+=(uint32_t)cell3.raw;
error+=calsample(&cell4); cell4sum+=(uint32_t)cell4.raw;
error+=calsample(&busv); busvsum+=(uint32_t)busv.raw;
samples++;
}
setBackLight(LCD_HI_BRIGHT); // Go to high brightness again
if(error) {
Serial.println(F("Sampling error!"));
lcdDisplay(F("Calibration^Error!^"));
lcdUpdate();
while(true);
}
cal.romsignature = ROMSIGNATURE;
cal.arefv=(uint16_t)((arefvsum+CALSAMPLES/2)/CALSAMPLES);
cal.cell1=(uint16_t)((cell1sum+CALSAMPLES/2)/CALSAMPLES);
cal.cell2=(uint16_t)((cell2sum+CALSAMPLES/2)/CALSAMPLES);
cal.cell3=(uint16_t)((cell3sum+CALSAMPLES/2)/CALSAMPLES);
cal.cell4=(uint16_t)((cell4sum+CALSAMPLES/2)/CALSAMPLES);
cal.busv=(uint16_t)((busvsum+CALSAMPLES/2)/CALSAMPLES);
Serial.println(F("Calibration values:"));
Serial.print(F("Aref: ")); Serial.print(cal.arefv);
SerialPrintCellHeader(0); Serial.print(cal.cell1);
SerialPrintCellHeader(1); Serial.print(cal.cell2);
SerialPrintCellHeader(2); Serial.print(cal.cell3);
SerialPrintCellHeader(3); Serial.print(cal.cell4);
Serial.print(F(", Busv: ")); Serial.println(cal.busv);
pwm_active++; // Fake a PWM port active, to prevent the processor from sleeping (and switching off the timer)
cal.ttrv = millis(); // Record the time to run the timing
for(uint8_t i=0;i<50;i++) {
getRaw(&cell1); // All the cells have equal timing, so let's just do only cell1 for simplicity
}
cal.ttrv=millis()-cal.ttrv; // Record the time spent to read the samples
cal.ttrv=(cal.ttrv+5)/10; // We read 5 of these per loop (4 cells plus the bus voltage).
pwm_active--; // Restore the pwm_active variable
Serial.print(F("It takes "));
Serial.print(cal.ttrv);
Serial.println(F(" ms to read the voltages."));
eeprom_write_block((void*)&cal, (void*)(EEPROM_CAL_ADDR), sizeof(cal)); // Save the calibration settings to ROM
monitor(false);
}
void setup() {
// First initialize some ports
pinMode(BUTTON, INPUT_PULLUP);
pinMode(LED, OUTPUT);
// ********* Init relays **********
// Initialize the relays and make sure the buses are disconnected until we know what's going on.
relayInit(chargerelay);
relayInit(busrelay);
// Wait until the relay coils have been de-energized
do {
_millis=millis(); // The relayLoop routine uses our pseudo timer. For now feed it with the real timer.
} while(relayLoop(chargerelay) || relayLoop(busrelay));
chargerelay->timer=millis()-RECONTIMEOUT; // Allow an initial quick startup
busrelay->timer=millis()-RECONTIMEOUT; // Allow an initial quick startup
// ********* Let's save some power *********
// Switch off the digital input circuitry of the analog ports to reduce power consumption and enhance accuracy.
DIDR0 |= ((1<<CELL1 | 1<<CELL2 | 1<<CELL3 | 1<<CELL4 | 1<<BUSV) & 0x3F);
// We have only switched off the digital input circuitry of the analog ports we use. If you don't use any of the remaining analog ports for digital I/O, you can switch them off altogether by uncommenting the next line:
// DIDR0=0x3F; // Switch off the digital input circuitry of all analog ports
// Disable some modules we are not using
power_spi_disable(); // Nope, we don't use the SPI, so shut it down
power_twi_disable(); // Nope, we don't use the two wire interface, so shut it down
#if LCD_LO_BRIGHT == 0 && LCD_HI_BRIGHT >= 255
power_timer1_disable(); // If we don't use the LCD brightness PWM feature, we don't need timer1
#endif
#if RELAYPWM==0 || RELAYPMW >=255
power_timer2_disable(); // If we don't need the mono-stable relay PWM feature, we don't use timer2
#endif
// ******** Initialize serial interface *********
Serial.begin(UARTBAUD);
Serial.println(F("Serial interface started."));
delay(5);
#if LCD>0
// ********* Init LCD module **********
setBackLight(LCD_HI_BRIGHT); // Let's start with the backlight on
// Setup the LCD
lcd.begin(LCD_COLS,LCD_ROWS);
lcd.print(F("ArduinoBMS " VER ));
lcd.setCursor(0, 1);
lcd.print(F("by Frans Veldman")); // Yup that's me!
uint8_t twoDot[] = { B11000, B00100, B01000, B10000, B11100, B00011, B00011, B00000 };
uint8_t threeDot[] = { B11110, B00010, B01100, B00010, B11100, B00011, B00011, B00000 };
uint8_t lightning[]= { B00011, B00110, B01100, B11110, B00110, B01100, B01000, B10000 };
lcd.createChar(1,lightning); // Special character for "charging indicator"
lcd.createChar(2,twoDot); // Special character 2 with a decimal in it, to save display space
lcd.createChar(3,threeDot); // Special character 3 with a decimal in it, to save display space
delay(2000); // Show the message for 2 seconds
lcd.clear(); // And then wipe it
#else
// With no LCD, we use two LED's to display the status
pinMode(FULL_LED,OUTPUT);
pinMode(EMPTY_LED,OUTPUT);
digitalWrite(FULL_LED,HIGH);
digitalWrite(EMPTY_LED,HIGH);
delay(1000); // Just one second to check the LED's are working
digitalWrite(FULL_LED,LOW);
digitalWrite(EMPTY_LED,LOW);
#endif
// If the button is pressed during power up, go into monitor mode
if(digitalRead(BUTTON)==LOW)
monitor(true); // This one never exits
// ******** Let's start the real thing! **********
// Read all voltages
getRaw(&cell1);
getRaw(&cell2);
getRaw(&cell3);
getRaw(&cell4);
getRaw(&busv);
// Now we need to check whether all voltages are equal and in the expected range for calibration.
// With a resistor divider of 1:3, all port readings should be somewhere around 33% of the reference voltage. So with a total precision of 15%, the result should be somewhere between 18% and 48%.
uint16_t max=(uint16_t)( ( ((uint32_t)(1024L << (ADC_RES-10))*R2*(CALVOLTAGE*1000UL)) / ((R1+R2)*(REFVOLTAGE*1000UL))) * (100UL+RPRECISION)/100UL);
uint16_t min=(uint16_t)( ( ((uint32_t)(1024L << (ADC_RES-10))*R2*(CALVOLTAGE*1000UL)) / ((R1+R2)*(REFVOLTAGE*1000UL))) * (100UL-RPRECISION)/100UL);
if( cell1.raw > min && cell1.raw < max && cell2.raw > min && cell2.raw < max && cell3.raw > min && cell3.raw < max && cell4.raw > min && cell4.raw < max && busv.raw > min && busv.raw < max )
calibrate();
eeprom_read_block((void*)&cal, (void*)(EEPROM_CAL_ADDR), sizeof(cal)); // Read the callibration settings from ROM.
// If there is no rom signature, we have never calibrated. End with an error.
if(cal.romsignature != ROMSIGNATURE) {
lcdDisplay(F("Calibration^required!"));
lcdUpdate();
Serial.println(F("Calibration required!"));
while(true); // No escape from here
}
power_timer0_disable(); // Switch off timer0, we have no use for it anymore.
_millis=millis(); // Initialize our pseudo timer.
wdt_enable(WDTO_4S); // Enable watchdog, Use a 4 second interval
}
// ******** Relay routines *********
void relayInit(relay_t* relay) {
pinMode(relay->pin_on, OUTPUT); // Make the pin an output pin
if(relay->pin_off) // Is it a bi-stable relay?
pinMode(relay->pin_off, OUTPUT); // Then set the "Off" pin as an output too
relay->state=true; // Assume the relay was left in the on position
relaySwitch(relay, false); // Now switch the relay to the off position
}
void relaySwitch(relay_t* relay, const bool state) {
if (relay->state == state)
return; // Relay is already in requested state, nothing to do
digitalWrite(relay->pin_on, LOW); // depower the coils, in case we are still in a transition.
if(relay->pin_off) // Bi-stable relay?
digitalWrite(relay->pin_off, LOW); // Then depower the off-coil too
relay->timer = _millis; // Start the coil timer
if (state) // Switch it to the ON position?
digitalWrite(relay->pin_on, HIGH); // Energize the ON coil
else // We want to disconnect
if(relay->pin_off) // Bi-stable relay?
digitalWrite(relay->pin_off, HIGH); // Then energize the OFF coil
// For mono-stable relay, we already switched off the coil
relay->state = state; // Record the relay state
}
bool relayLoop(relay_t* relay) {
// If the coil timer expired, depower the coils
if (_millis - relay->timer > relay->holdtmr) {
if(relay->pin_off) { // Bi-stable relay?
digitalWrite(relay->pin_on, LOW); // Depower the coils
digitalWrite(relay->pin_off, LOW);
} else { // Mono stable relay!
if(relay->state) // Is it in the ON position?
_analogWrite(relay->pin_on,relay->pwm); // Save power by using PWM
}
// In case the relay stays engaged for weeks, it will eventually outrun the timer, and a subsequent relay switch just a few ms after that might fail
if(_millis - relay->timer > 1000UL*3600UL*49) // Anticipate roll-over problem
relay->timer=_millis-1000UL*3600UL*48; // Try to stay max 2 days behind
return false; // We're finished
}
return true; // Coil is still energized
}
// *************** Misc routines *****************
// This function handles the button functionality. Button presses are separated into long and short presses.
int16_t chkbutton(button_t* button) {
if (digitalRead(button->button) == LOW) { // button pressed?
if (button->pressed == 0)
button->pressed = _millis;
if (_millis - button->pressed >= LONGPRESS && !button->longpressed) {
button->longpressed = true;
return 2;
}
return 0;
} else { // button released
if (button->longpressed) {
button->longpressed = false;
button->pressed = 0;
}
if (button->pressed == 0)
return 0;
uint32_t pressed = _millis - button->pressed;
button->pressed = 0;
if (pressed < SHORTPRESS)
return 0;
if (pressed < LONGPRESS)
return 1;
return 0;
}
}
bool set_nogo(const uint16_t charge_error, const uint16_t discharge_error) {
bool ret=false;
if (charge_error && !(charge_nogo & charge_error)) {
charge_nogo |= charge_error;
ret=true;
}
if(discharge_error && !(discharge_nogo & discharge_error)) {
discharge_nogo |= discharge_error;
ret=true;
}
return ret;
}
bool clr_nogo(const uint16_t charge_error, const uint16_t discharge_error) {
if (charge_nogo & charge_error || discharge_nogo & discharge_error) {
charge_nogo &= ~charge_error;
discharge_nogo &= ~discharge_error;
return true;
}
return false;
}
void collectdata() {
highestCellVoltage = 0;
lowestCellVoltage = CELLOVERVOLT;
for (uint8_t i = 0; i < 4; i++) {
getVoltage(cells[i]);
if(cellVoltage(cells[i]) > highestCellVoltage) // Find the cell with the highest voltage
highestCellVoltage = cellVoltage(cells[i]);
if(cellVoltage(cells[i]) < lowestCellVoltage) // Find the cell with the lowest voltage
lowestCellVoltage = cellVoltage(cells[i]);
}
getVoltage(&busv);
libattvoltage = cell4.voltage; // Cell4 holds the battery voltage
_millis+=cal.ttrv; // Add the milliseconds spent while reading the voltages
}
void checksystem() {
if(cellVoltage(&busv)<8) // Bus voltage < 6, probably loose connector or other problem
if(set_nogo(X_SYSTEM, X_SYSTEM))
Serial.println(F("Fatal: Bus ondervolt"));
if(highestCellVoltage >= CELLOVERVOLT) // Cell overvolt? Shut down everything
if(set_nogo(X_OVERVOLT, X_OVERVOLT))
Serial.println(F("Fatal: Cell overvolt"));
if(lowestCellVoltage <= CELLDEFECT) // Cell undervolt? Shut down everything
if(set_nogo(X_UNDERVOLT, X_UNDERVOLT))
Serial.println(F("Fatal: Cell undervolt"));
// Check whether the charge relay is connected. We can only measure the voltage on the charge bus
if(chargerelay->state && _millis-chargerelay->timer>RYINRUSHTIME) {
if(libattvoltage<busv.voltage-RYVOLTLOSS) // Excessive voltage loss while charging?
if(set_nogo(X_CONNERROR, 0))
Serial.println(F("Charge relay error"));
if(libattvoltage>busv.voltage+RYVOLTLOSS) // Excessive voltage loss while discharging?
if(set_nogo(0, X_CONNERROR))
Serial.println(F("Consumer relay error"));
}
// Let's see if we can automatically restore X_CONNERROR's.
if(charge_nogo & X_CONNERROR && libattvoltage>busv.voltage-RYVOLTLOSS)
clr_nogo(X_CONNERROR,0); // It is safe to start allowing charging again
if(discharge_nogo & X_CONNERROR && libattvoltage<busv.voltage+RYVOLTLOSS)
clr_nogo(0, X_CONNERROR); // It is safe to start allowing discharging again
}
void checkcelllimits() {
if(highestCellVoltage >= MAXCVOLTAGE) // Reached maximum cell voltage?
if(set_nogo(X_HIVOLTLIMIT, 0))
Serial.println(F("Battery full"));
if(lowestCellVoltage <= MINCVOLTAGE) // Reached minimum cell voltage?
if(set_nogo(0, X_LOVOLTLIMIT))
Serial.println(F("Battery empty"));
// If the charge relay is not connected, and the battery voltage is higher than the bus voltage...
if(!chargerelay->state && libattvoltage>busv.voltage+VDEADBAND)
if(clr_nogo(X_HIVOLTLIMIT,0)) // ... it is safe to reconnect
Serial.println(F("BatteryV < BusV, HiVolt cleared"));
// If the discharge relay is not connected, and the battery voltage is less than the bus voltage...
if(!busrelay->state && libattvoltage<busv.voltage-VDEADBAND)
if(clr_nogo(0,X_LOVOLTLIMIT)) // ... it is safe to reconnect
Serial.println(F("BatteryV > BusV, LoVolt cleared"));
// If the charge relay is not connected, and the cell voltage dropped...
if(!chargerelay->state && highestCellVoltage<=DUAL_HI_RECONV)
if(clr_nogo(X_HIVOLTLIMIT,0)) // ... it is safe to reconnect
Serial.println(F("Cell voltage dropped, HiVolt cleared"));
// If the discharge relay is not connected, and the cell voltage raised...
if(!busrelay->state && lowestCellVoltage>=DUAL_LO_RECONV)
if(clr_nogo(0,X_LOVOLTLIMIT)) // ... it is safe to reconnect
Serial.println(F("Cell voltage raised, LoVolt cleared"));
}
// ********** main execution loop **************
void loop(void) {
relayLoop(chargerelay); // This takes care of depowering the relay coils after the hold time has expired
relayLoop(busrelay); // This takes care of depowering the relay coils after the hold time has expired
uint16_t butt = chkbutton(&button); // Check whether a button has pressed
#if LCD>0
#if LCDBACKLIGHTTMR>0
static uint32_t displaytmr=0;
bool prevChargeRelay=chargerelay->state;
bool prevBusRelay=busrelay->state;
if(!displaytmr) // Give it some one time head start after startup
displaytmr=_millis; // Yes, I know the glitch: if the timer would be zero (small chance), we inadvertently have a one time increase of the duration. But let's keep it simple...
if(_millis-displaytmr>=LCDBACKLIGHTTMR*1000) { // Is/should the display be in low brightness?
if(butt) { // Button pressed
setBackLight(LCD_HI_BRIGHT); // So switch to high brightness
displaytmr=_millis; // Reset the timer
butt=0; // Throw away this key press
} else {
setBackLight(LCD_LO_BRIGHT); // Go/maintain low brightness
}
}
if(butt) // Keep resetting the display timer with every key press
displaytmr=_millis;
#endif
#else
// For the low/under voltage status, we switch on the LED's here. They will be switched off again at the end of this loop, before the beginning of the 8 second sleep.
// This way the LED's flash only once in 8 seconds, to minimize the drain on the nearly empty battery.
if(discharge_nogo & X_LOVOLTLIMIT)
digitalWrite(EMPTY_LED,HIGH); // Signal the EMPTY LED in a normal EMPTY condition
if(discharge_nogo & X_UNDERVOLT)
digitalWrite(FULL_LED,HIGH); // Signal the FULL LED as well in case of an undervolt condition (which is a serious error)
#endif
// Collect all the required voltages
collectdata();
// If button is pressed
if(butt) {
if(!clr_nogo(X_CONNERROR,X_CONNERROR)) { // If there was a connection error, clear it and try again
if(!clr_nogo(X_STOPBUTTON,X_STOPBUTTON)) { // Check whether the stopbutton was pressed previously and if so, clear it.
if(butt==2) // Nope, no stopbutton was set, so set it now
set_nogo(X_STOPBUTTON,X_STOPBUTTON); // With a long press, inhibit charging and discharging
else
set_nogo(X_STOPBUTTON,0); // With a short press, inhibit only charging
clr_nogo(X_HIVOLTLIMIT,X_LOVOLTLIMIT); // Re-evaluate charge and discharge conditions
}
}
}
checksystem(); // Perform a sanity check
checkcelllimits(); // Check whether all cell readings are within limits
#if DUALBUS>0
if(!charge_nogo) {
if(_millis-chargerelay->timer>RECONTIMEOUT) // Long engouh ago since last attempt?
relaySwitch(chargerelay,true); // Reconnect (if not done already)
} else {
relaySwitch(chargerelay,false); // Disconnect charge bus
}
if(!discharge_nogo) {
#else
if(!charge_nogo && !discharge_nogo) { // No objections against connecting?
#endif
if(_millis-busrelay->timer>RECONTIMEOUT) { // Long engouh ago since last attempt?
relaySwitch(busrelay,true); // Reconnect (if not done already)
digitalWrite(LED,HIGH); // Signal that the discharge bus is on
} else
if(!busrelay->state) // Still pending?
digitalWrite(LED,!digitalRead(LED)); // Toggle the LED
} else {
relaySwitch(busrelay,false); // Disconnect discharge bus
digitalWrite(LED,LOW);
}
#if LCD>0
#if LCDBACKLIGHTTMR>0
if(prevChargeRelay!=chargerelay->state || prevBusRelay!=busrelay->state) {
setBackLight(LCD_HI_BRIGHT); // So switch to high brightness
displaytmr=_millis; // Reset the timer
}
#else
setBackLight(LCD_HI_BRIGHT); // Switch to high brightness in case the long sleep routine has switched it off
#endif
#endif
static uint32_t lcdrefresh=0;
if(_millis-lcdrefresh>LCDREFRESH) {
lcdrefresh=_millis;
print_status(); // Print status on serial port
#if LCD>0
dtostrf(busv.voltage,5,2,dispbuff); // Print bus voltage on LCD
sprintf(dispbuff+LCD_COLS+1,"%03d %03d %03d %03d ", (int16_t)((cellVoltage(&cell1)+0.005)*100), (int16_t)((cellVoltage(&cell2)+0.005)*100), (int16_t)((cellVoltage(&cell3)+0.005)*100), (int16_t)((cellVoltage(&cell4)+0.005)*100) );
// Replace the leading 2 and 3 of our the cell voltages with special characters that include a decimal. We only do this with the 2 and 3 because cell voltages are not supposed to be outside this range.
// Creating the other characters would be a waste of memory and clock cycles.
for(uint8_t i=0;i<4;i++) {
if(dispbuff[LCD_COLS+1+i*4]=='2')
dispbuff[LCD_COLS+1+i*4]=2;
if(dispbuff[LCD_COLS+1+i*4]=='3')
dispbuff[LCD_COLS+1+i*4]=3;
}
lcdUpdate();
lcd.setCursor(5,0);
lcd.write('V');
lcd.setCursor(7,0);
if((charge_nogo & X_STOPBUTTON) || (discharge_nogo & X_STOPBUTTON))
lcd.print(F("Pause "));
else if((charge_nogo & X_SYSTEM) || (discharge_nogo & X_SYSTEM))
lcd.print(F("Sys Err "));
else if((charge_nogo & X_CONNERROR) || (discharge_nogo & X_CONNERROR))
lcd.print(F("Ry Err "));
else if((charge_nogo & X_OVERVOLT))
lcd.print(F("OverVolt"));
else if((discharge_nogo & X_UNDERVOLT))
lcd.print(F("Cell Err"));
else if(charge_nogo & X_HIVOLTLIMIT)
lcd.print(F("Full "));
else if(discharge_nogo & X_LOVOLTLIMIT)
lcd.print(F("Empty "));
else if(!busrelay->state)
lcd.print(F("Pending "));
else
lcd.print(F("Online "));
lcd.setCursor(15,0);
if(chargerelay->state)
lcd.write(1);
else
lcd.write(' ');
#else
// In case of an error, we light both the FULL and EMPTY LED's together
if(charge_nogo & (X_SYSTEM | X_CONNERROR | X_OVERVOLT) || discharge_nogo & (X_SYSTEM | X_CONNERROR | X_OVERVOLT)) {
digitalWrite(FULL_LED, HIGH);
digitalWrite(EMPTY_LED, HIGH);
} else
if(charge_nogo & X_HIVOLTLIMIT)
digitalWrite(FULL_LED, HIGH);
else {
// If there is nothing wrong, we have the LED's off.
// We also want them to be off in any of the low voltage conditions.
// We put them on in the beginning of the loop, so they only lit during the voltage measurements.
// That is, a few ms every 8 seconds. Just enough to be visible but with minimal power drain.
digitalWrite(FULL_LED, LOW);
digitalWrite(EMPTY_LED, LOW);
}
_delay_ms(5); // Allow the serial print routine to finish
#endif
}
wdt_reset(); // pat the dog
// Let's get some sleep!
// We are going to use the Watchdog timer for this.
// We will shut down everything else.
// The sleep will end when either the watchdog timer or an interrupt (button press) fires.
uint8_t wdt_prescale = 2; // 64 ms
// If the battery is disconnected due to some low voltage condition, and timers are finished, go into a longer sleep.
#if LCD>0 && LCDBACKLIGHTTMR>0
if((discharge_nogo & (X_SYSTEM | X_UNDERVOLT | X_LOVOLTLIMIT)) && _millis-busrelay->timer>RECONTIMEOUT && _millis-displaytmr>=LCDBACKLIGHTTMR*1000) {
#else
if((discharge_nogo & (X_SYSTEM | X_UNDERVOLT | X_LOVOLTLIMIT)) && _millis-busrelay->timer>RECONTIMEOUT) {
#endif
wdt_prescale = 9; // 8 sec
setBackLight(LCD_LO_BRIGHT); // Switch to low brightness in case this has not been done already
}
uint8_t wdtcsr = bit(WDIE) | (wdt_prescale&7) | ((wdt_prescale&8)<<2);
wdt_disable(); // Disable the watchdog timer for now.
ADCSRA = 0; // disable the ADC
MCUSR = 0; // clear various "reset" flags
WDTCSR = bit (WDCE) | bit (WDE); // allow changes, disable reset
WDTCSR = wdtcsr; // set WDIE, and prescaler
wdt_reset(); // pat the dog
EICRA = _BV(ISC01); // configure INT0 to trigger on falling edge
EIMSK = _BV(INT0); // enable INT0
if(wdt_prescale<9 && pwm_active) // in short sleep AND a PWM signal active?
set_sleep_mode(SLEEP_MODE_IDLE); // Keep PWM running
else // Nope, safe to sleep
set_sleep_mode(SLEEP_MODE_PWR_DOWN); // Power down everything
sleep_enable();
sei(); // ensure interrupts enabled so we can wake up again
sleep_cpu(); // go to sleep
sleep_disable(); // wake up here
wdt_enable(WDTO_4S); // Re-enable the watch dog
_millis+=(16<<wdt_prescale); // Add the time spent sleeping to our timer
}