quick arduino bms

Joined
Aug 10, 2017
Messages
42
image_ndsisb.jpg

image_tkvojs.jpg

image_nqekqu.jpg

image_vztgny.jpg
Here's the quick and dirty bms i've thrown together from a handful of arduino pro-mins. okay, at the moment it's just a cell monitor, but adding a master node that collects the data and controls the charge/discharge mosfets as well as the balance resistors is the easy part. I first posted on the facebook group, but since I can't upload code there, I've added it here. Yes, I know this is sort of reinventing the wheel, and the version by Stewart Piscataway and Colin Hickey does the same thing, only better. The difference is that mine can read up to 8 temperature sensors and doesn't use that expensive digital isolator chip. All that's needed here are two optos, one pnp transistor, and three resistors. you will also need one 10k resistor for each thermistor.

Code:
/* This code is a total kludge, thrown together on a rainy day
* Thanks to Adafruit, Kevin Darrah, and Tinkerit for allowing me to lift some useful bits
*/
#define MODULENUM 5
#define POWERPIN 13
#define TRIGGERPIN 12
#define NUMSENSORS 3
// resistance at 25 degrees C
#define THERMISTORNOMINAL 10000  
// temp. for nominal resistance (almost always 25 C)
#define TEMPERATURENOMINAL 298.15
// how many samples to take and average, more takes longer
// but is more 'smooth'
#define NUMSAMPLES 5
// The beta coefficient of the thermistor (usually 3000-4000)
#define BCOEFFICIENT 3950
// the value of the 'other' resistor
#define SERIESRESISTOR 10000 

float readVcc()
 {
 long result; // Read 1.1V reference against AVcc
 ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
 delay(5); // Wait for Vref to settle
 ADCSRA |= _BV(ADSC); // Convert
 while (bit_is_set(ADCSRA,ADSC));
 result = ADCL; result |= ADCH<<8;
 result = 1100000L / result; // Back-calculate AVcc in
 return result/1000.0;
 }

float steinhart(float average)
 {
 float result;
 average = 1023 / average - 1;
 average = SERIESRESISTOR / average;
 result = average / THERMISTORNOMINAL;  // (R/Ro)
 result = log(result);         // ln(R/Ro)
 result /= BCOEFFICIENT;         // 1/B * ln(R/Ro)
 result += 1.0 / (TEMPERATURENOMINAL); // + (1/To)
 result = 1.0 / result;        // Invert
 return result;
 }

void setup(void)
 {
 Serial.begin(9600);
 pinMode(POWERPIN, OUTPUT);
 pinMode(TRIGGERPIN, OUTPUT);
 }


void loop(void)
 {
 uint8_t i,n;
 uint16_t samples[NUMSAMPLES];
 float averages[NUMSENSORS];
 float highest, lowest;

 for (n=0; n < NUMSENSORS; n++)
  {
  // take N samples in a row, with a slight delay
  for (i=0; i< NUMSAMPLES; i++)
   {
   digitalWrite(POWERPIN, HIGH);
   delay(2);
   samples[i] = analogRead('A' + n);
   digitalWrite(POWERPIN, LOW);
   delay(2);
   }
  // average all the samples out
  averages[n] = 0;
  for (i=0; i< NUMSAMPLES; i++)
   {
   averages[n] += samples[i];
   }
  averages[n] /= NUMSAMPLES;
  }

 highest = 0.0;
 lowest = 1023.0;

 for(i=0; i< NUMSENSORS; i++)
  {
  if(averages[i] > highest) highest = averages[i];
  if(averages[i] <= lowest) lowest = averages[i];
  }
 // highest and lowest are swapped after steinhart
 Serial.print(MODULENUM);
 Serial.print(' ');
 Serial.print(readVcc(),3);
 Serial.print("V ");
 Serial.print(steinhart(lowest),1);
 Serial.print("K ");
 Serial.print(steinhart(highest),1);
 Serial.print("K ");
 Serial.println();
 digitalWrite(TRIGGERPIN, HIGH);
 delay(10);
 digitalWrite(TRIGGERPIN, LOW);
 delay(20);
 ADCSRA &= ~(1 << 7); //shut down adc
 SMCR |= (1 << 2); //power down mode
 SMCR |= 1;//enable sleep
 MCUCR |= (3 << 5); //set both BODS and BODSE at the same time
 MCUCR = (MCUCR & ~(1 << 5)) | (1 << 6); //then set the BODS bit and clear the BODSE bit at the same time
 __asm__ __volatile__("sleep");//in line assembler to go to sleep
}
 
Interesting setup. Pretty good overall.

Just one change so far, please put your code inside of a [ code ] [ /code ] block ;) it'll make it a lot easier to read :)
 
One thing I forgot to mention - in the sketch, the optocoupler on the right; all the collectors and emitters are paralleled together, and get one single pullup resistor on the collector line - which goes to RX on the master. The emitters all go to ground.
 
On the right hand opto coupler you are using high side switching. It is more typical to use low side. This is because the base needs to be at a higher voltage than the emitter. As the MCU is powered from the same rail as the transistor, this cannot happen, therefore your transistor will operate in the linear region and drop voltage across collector to emitter.

There shall be at least 0.6v across the transistor. To when your cell is at low voltage level you may not have enough current through your opto isolator to turn it on hard, so you could have data corruption issues.

If we assume 2v across the opto isolator. 0.6v across transistor and 0.1v across the MCU. Then though only have 0.3v across you 220 ohm resistor. It is possible that you will have more than 0.1v across the MCU. You need to consult datasheet for minimum high output voltage.

See attached excerpt from an MCU datasheet. If you look at the Voh specification, you will see that the guaranteed output voltage for a 3V supply rail (ie a discharged cell) is only 2v. This will not be sufficient to drive your transistor and light the opto-coupler. You circuit may work fine for high cell voltages, and indeed it *might* work for you, but it is not guaranteed to work across different ICs, voltages and temperatures. Change this to low side switching and you only need less than 1v output!


image_mhlepj.jpg
 
I could eliminate the transistor entirely if I could figure out how to invert the serial output so it's idle state is low.
 
Here you go.
Options for direct LED from MCU, or via low side switching if your MCU cannot supply enough voltage and/orcurrent for the LED directly. What's the forward voltage drop of your optoisolator LED, and what is the rated current?

image_ogambv.jpg
 
thanks. i don't know why i didn't think of that earlier. the arduino has no problem sourcing or sinking current... just eliminate the transistor and drive the cathode directly - anode goes to current limiting resistor to vcc. problem solved and 2 components eliminated.
 
Timothy_Hennessy said:
thanks. i don't know why i didn't think of that earlier. the arduino has no problem sourcing or sinking current... just eliminate the transistor and drive the cathode directly - anode goes to current limiting resistor to vcc. problem solved and 2 components eliminated.

Hi, very interesting project! I'm not a pro in Arduino coding, but wouldn't it be possible to use all the other Pins (2-9) than only 10-13? Or the Arduino prois not powerfull enough to handle this? Or another limitation? Or maybe another version of Arduino board would be more appropriated? Thanks.


I've seen an interesting project based on a Arduino Uno and a dedicated chip LTC6804-2 that can handle up to 12 cells in serial.
http://caditz.us/PowerElectronics/BMS_LTC6804
Layout of the board ans sketch is also available there: https://www.pcbway.com/project/shareproject/Arduino_BMS_Shield___12_Cell.html
 
shaved the code down as much as possible, used ints instead of floats, deleted the steinhart function, removed a long division from readvcc() compiles to 2592 bytes on the atmega168p. I figure i'll do all the math on the master - which can be just about anything from a nano to an esp32 to a raspberry pi. ran my 5 test units (328's at 16mhz) down to 2.7 volts no problem...BODS makes no difference. draws about 7 miliamps when transmitting, average of about 2.5. sleep mode current goes way up tho... 64microamps at 2.7 volts. can anyone explain this?
 
UPDATE - I got a batch of arduino pro mini 3.3v, 8mhz atmega168pa in from China. Put the code on them, and they go to sleep at 6 milliamps. remove the power led and it drops to 1.74. remove the LDO and it drops to 0.2 microamps! Still wakes up and does it's job, then goes back to sleep. Still won't function below 2.7 volts. Cannot get avrdude to set the fuse bits. I am using a nano as an isp. successfully burned a bootloader only once - not sure how.
 
ANOTHER UPDATE - I successfully changed the fuse bits to change the brownout protection to 1.8 volts, and reduced the part count again. I got one of these to function reliably down to 2.03 volts. power consumption during transmit varies between 2.05 miliamps at 2.03 volts up to 10.61 miliamps at 4.25 volts. clock speed has been reduced to 4mhz via clkpr register to improve low voltage reliability. idle current was measured at 56 nanoamps with a 100k resistor, and 10.8 nanoamps using a 1meg resistor. I have started programming the master, and i am using an arduino nano for that. mostly just a bunch of reading/parsing the software serial port and doing the steinhart math and the bandgap reference vs vbatt stuff. I plan on reading total pack voltage and current via an i2c 16 bit adc and an allegro acs758. haven't decided if i'm going to have seperate charge- and discharge- ports, or combine the two. source to source is easier to do the gate drive, but almost every pack i've ever seen uses drain to drain. can someone please explain how to drive the discharge- gate from a battery negative referenced supply?
 
Excellent work. Following this project very closely. What fuse setting are you using?
 
high fuse was changed from 0xdf to 0xde. i tried and failed using the nano as an isp, so i just got a usbasp from ebay.
 
Walde said:
Question: can the system also be used with 14 battery packs (14S)?

And can you provide the plans and code on Github?

Greetings Dirk

In theory, it should work on any pack up to the isolation voltage of the optos - something ridiculous like 600S. in practice it depends on how much noise the system can tolerate. i'm using an active low signal on a star network with a single pullup resistor. this is also uart, so there is no separate clock. on a breadboard with five modules, i was starting to have issues at 9600 baud, so i dropped the data rate to 2400 baud. with good connections and twisted pairs i don't see why this won't work with 14S or higher.however,in order to reduce the part count and improve reliability, i have removed the daisy-chain reset function. a single nano has enough outputs to drive 7 lines directly, but for more cells, you may have to resort to a couple of 74138's or a mega.

as for the plans and code, i'm trying to make this work on perfboard, so anyone with basic soldering skills can make one without using a pcb. I'll upload the code when I have an operational 7S system, even if all the features aren't implemented at that point.
 
if you give me a list of material, code and schematic I could test also, have Li-ion 7S config too.
 
this is the latest code for the cell modules:
Code:
/* This code is a total kludge, thrown together on a rainy day
* Thanks to Adafruit, Kevin Darrah, and Tinkerit for allowing me to lift some useful bits
*
* UPDATE 20 Nov 2018 - I have shaved this code down to the bare bones, will do all calculations
* and calibration on the master. this means the only change to each module is the
* MODULENUM define.
*/
#define POWERPIN 13
#define NUMSENSORS 4 // max 8
#define NUMSAMPLES 5 // max 32

uint16_t readVcc()
 {
 uint16_t result = 0;
 uint16_t sample;
 ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
 delay(2); // Wait for Vref to settle
 for(uint8_t i = 0; i < NUMSAMPLES; i++)
  {
  ADCSRA |= _BV(ADSC); // Convert
  while (bit_is_set(ADCSRA,ADSC));
  sample = ADCL; sample |= ADCH<<8;
  result += sample;
  delay(2);
  }
 return result;
 }

void setup(void)
 {
 noInterrupts();
 CLKPR = 0x80; //allow changing the prescaler
 CLKPR = 0x01; //divide by two
 interrupts();
 Serial.begin(4800);
 pinMode(POWERPIN, OUTPUT);
 pinMode(2, INPUT_PULLUP);
 attachInterrupt(0, digitalInterrupt, FALLING);
 }

void loop(void)
 {
 ADCSRA |= (1 << ADEN);
 uint8_t i,n;
 uint16_t sample, lowest, highest;

 lowest = (1024 * NUMSAMPLES) - 1;

 for (n=0; n < NUMSENSORS; n++)
  {
  sample = 0;
  // take N samples in a row, with a slight delay
  for (i=0; i< NUMSAMPLES; i++)
   {
   digitalWrite(POWERPIN, HIGH);
   delay(1);
   sample += analogRead('A' + n);
   delay(1);
   digitalWrite(POWERPIN, LOW);
   }
  if(sample > highest) highest = sample;
  if(sample <= lowest) lowest = sample;
  }
 sample = readVcc(); //re-used variable
 Serial.print(sample);
 Serial.print(' ');
 Serial.print(lowest);
 Serial.print(' ');
 Serial.print(highest);
 delay(30);
 ADCSRA &= ~(1 << 7); //shut down adc
 SMCR |= (1 << 2); //power down mode
 SMCR |= 1;//enable sleep
 MCUCR |= (3 << 5); //set both BODS and BODSE at the same time
 MCUCR = (MCUCR & ~(1 << 5)) | (1 << 6); //then set the BODS bit and clear the BODSE bit at the same time
 __asm__ __volatile__("sleep");//in line assembler to go to sleep
}

void digitalInterrupt(){}

This is what I have so far for the control module:

Code:
#include <SoftwareSerial.h>
#define THERMISTORNOMINAL 10000  
#define TEMPERATURENOMINAL 298.15
#define BCOEFFICIENT 3950
#define SERIESRESISTOR 10000 

const float num_samples = 5.0;

SoftwareSerial mySerial(10, 11);

String getValue(String data, char separator, int index)
{
 int found = 0;
 int strIndex[] = {0, -1};
 int maxIndex = data.length()-1;

 for(int i=0; i<=maxIndex && found<=index; i++){
  if(data.charAt(i)==separator || i==maxIndex){
    found++;
    strIndex[0] = strIndex[1]+1;
    strIndex[1] = (i == maxIndex) ? i+1 : i;
  }
 }

 return found>index ? data.substring(strIndex[0], strIndex[1]) : "";
}

float steinhart(float average)
 {
 float result;
 average = 1023 / average - 1;
 average = SERIESRESISTOR / average;
 result = average / THERMISTORNOMINAL;  // (R/Ro)
 result = log(result);         // ln(R/Ro)
 result /= BCOEFFICIENT;         // 1/B * ln(R/Ro)
 result += 1.0 / (TEMPERATURENOMINAL); // + (1/To)
 result = 1.0 / result;        // Invert
 return result;
 }

void setup() {
 Serial.begin(9600);
 mySerial.begin(2400);
 pinMode(12, OUTPUT);
}

void loop()
 {
 bool was_read = 0;
 int vraw, highest, lowest;
 String instring, s1, s2, s3;
 float v_batt, t_low, t_high, result;
 if (mySerial.available())
  {
  instring = mySerial.readString();
  was_read = true;
  }
while(was_read)
{
 s1 = getValue(instring, ' ', 0);
 s2 = getValue(instring, ' ', 1);
 s3 = getValue(instring, ' ', 2);
 vraw = s1.toInt();
 highest = s2.toInt();
 lowest = s3.toInt();

 result = vraw / num_samples;
 result = 1125300/result;
 result = result/1000;
 t_low = steinhart(lowest/num_samples) - 273.15;
 t_high = steinhart(highest/num_samples) - 273.15;
 Serial.print(result, 3);
 Serial.println(" Volts");
 Serial.print(t_low, 2);
 Serial.print(" C, ");
 Serial.print(t_high, 2);
 Serial.println(" C");
 was_read = false;

}
delay(5000);
digitalWrite(12, HIGH);
delay(10);
digitalWrite(12, LOW);
}

as you can see, I switched from triggering a reset to triggering an interrupt. the difference is the 1.5 seconds the bootloader takes if you trigger on the reset pin. idle current consumption is the same 108 nanoamps ( ignore earlier numbers - i can't math sometimes)

each cell module spits out the raw adc data as an integer. the master does the conversion to an actual voltage or temperature.

can anyone show me how to do a calibration via serial (that gets written to flash) versus going in and hard-coding stuff and having to re-compile?

Here's the schematic of the cell module. sorry for the hideous layout. first time ever using easyEDA.

image_pwvpgm.jpg
 
You have readVcc(), that's how you do your calibration. Google "Arduino Multimeter" examples show a lot of code and the math on how to do the calcs. You just need to expand that out a little.

Also, change your <code> tags to be [] instead ;)
 
I can do the calculation, since i'm measuring the internal bandgap against vcc, and every module is a little different- i just change the constant (1125300)slightly.this can usually be done with two compile and upload passes, i just think it would be nice if all the cell modules were the same, and all the configs were kept on the master.
 
Timothy_Hennessy said:
this is the latest code for the cell modules:
<code>/* This code is a total kludge, thrown together on a rainy day
* Thanks to Adafruit, Kevin Darrah, and Tinkerit for allowing me to lift some useful bits
*
* UPDATE 20 Nov 2018 - I have shaved this code down to the bare bones, will do all calculations
* and calibration on the master. this means the only change to each module is the
* MODULENUM define.


Here's the schematic of the cell module. sorry for the hideous layout. first time ever using easyEDA.

image_pwvpgm.jpg
Please Put all your Code and easyEDA Code on Github

That would help everyone and if there are improvements you can also help by Github.

That would be great. Thanks in advance from all here. :shy:
 
Back
Top