/* 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 . * * 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 #include #include #include #include "util/delay.h" #if LCD>0 #include 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<=255) { // If we want it to be completely on digitalWrite(port,HIGH); // make it a digital write pwm_active &= ~ (1<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 MAX_ADC_VARIATION && 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> 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->samplessamples++; // 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(samplestimer=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<= 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(libattvoltagebusv.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= 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, 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<