How To Build A Bms 1S To 4S Charger / Tester For Lithium-Ion or Lifepo4 Cells
How To Build A Bms 1S To 4S Charger / Tester For Lithium-Ion or Lifepo4 Cells
How To Build A Bms 1S To 4S Charger / Tester For Lithium-Ion or Lifepo4 Cells
It can be strange to study a BMS in regard of the overall choice available on the market for quite a very low price.
However, the objective of this project is to build a charging and testing device that can be universal, for a different
among of cells to charge and for different technologies. The Arduino Uno based device, manage voltage readings, and
according to results manages cells’ shunt or a buzzer. A SSD screen displays where we are. A buck DC-DC converter
adjust the charging voltage for a constant charging current.
Whatever if you have the habit or if you are starting in the modern batteries world – e-bikes batteries repair, solar
powered systems, etc.… - you can notice that it is not easy to manage Li-ion nor LifePo4 cells:
When you repair a battery pack, if it is easy to detect a dead cell, how do you detect a half-dead one?
When you are building a new battery pack, BMS user manual requests that cells must be assembled once they
are equilibrated. How do you perform such an assembling pack?
With a usual BMS charger, you need a dedicated voltage value’s power supply. It is not so easy to fit with…
This manual describes how to build such a BMS 1S…4S charger / tester.
2
Table of content
Introduction ........................................................................................................................................................................... 4
List of materials ...................................................................................................................................................................... 4
Hardware study ...................................................................................................................................................................... 5
Cells monitoring ................................................................................................................................................................. 5
Cells shunt control.............................................................................................................................................................. 5
Constant charging current & short circuit safety ............................................................................................................... 7
DC-DC buck converter ........................................................................................................................................................ 7
All in one diagram circuit ................................................................................................................................................... 8
Software study ....................................................................................................................................................................... 9
Cells voltage acquisition ..................................................................................................................................................... 9
Voltages limits management.............................................................................................................................................. 9
Power management ......................................................................................................................................................... 10
Parameters setup ............................................................................................................................................................. 10
The final code ................................................................................................................................................................... 11
User manual ......................................................................................................................................................................... 18
Illustration in use .................................................................................................................................................................. 19
3
Introduction
The discharge voltage must never be lower than a Vmin (that is technology dependent - see below).
The charging voltage must never be over a Vmax (that is also technology dependent – see below).
These batteries are very sensitive to any overcharging. A particular care must be done to stop the charging
process as soon as the charging voltage Vmax is reached.
The charging current must be under a maximum value, that is usually under 1/10 the capacity current of the cell
(0.1C)
The BMS available on the market are based on shunts that prevent against cells overcharging, but there is no monitoring
to detect a nearly defective cell… Moreover if the Vmin is reached during discharging, the power delivery is stopped.
Once again, we do not know if a particular cell is lighter and should be replaced. In addition, it may be very critical
situations where power is needed even if the battery life is in balance...
The help of the serial monitor can adjust these limit voltages, and then new values are kept in EEPROM.
It should be great to go over a 4S BMS, but the Atmega328p of the Arduino Uno has only 4 analog inputs to measure 4
cells voltages; it could be 6 analog inputs, but without the possibility of any data display anyway.
List of materials
4
Hardware study
The cells monitoring that constantly measure the voltages between cells boards
The shunt control that decrease the load current through the cells once the limit charging voltage is reached
The load control e.g. the constant charging current with a safety against short circuit
The DC-DC buck converter defines the power voltage according to a constant load current.
Cells monitoring
Cells are connected between –B and B1, B1 and B2, B2 and B3, B3 and B4. The objective is to monitor voltage for each
cell. However, the Arduino is only able to make any measure referenced to GND. What we can notice is that
AnalogRead0 directly gives Vcell1, and AnalogRead3 gives the power supply voltage value. However it is possible to
deduce Vcell2 = AnalogRead1 – AnalogRead0, Vcell3 = AnalogRead 2 – AnalogRead1, and so on.
The second point is any AnalogRead must NOT go over 5V otherwise the Atmega328p will very quickly smoke…
Hence the presence of voltage dividers around R1 to R7: AnalogRead0 is directly read (with R1 as a protection),
AnalogRead1 is divided by 2 (R2 and R5), AnalogRead2 is divided by 3, etc.… each time the result fit a voltage value
under 5V.
I know…. The grumpy ones will say that with voltage divisors the reading precision will lose too much. OK so with an
analog to digital converter of 10 bits, the analog from 0 to 5V signal is converted to 1024 bits. So, a one bit precision is
4.8mV. The worst divider ration is 4, then the precision “falls” to 19.2mV…. 0.0192V. My opinion is it is good enough for
monitoring that a LifePo4 cell does not go over 3.7V, or even 3.65V. We only need a reading precision of 0.05V
The shunt consists of switching a power resistor to the cell, so that a part of the
charging current circulates in it, according to the rule I = V/R, with a Li-ion Vmax
= 4.2V and R = 10 Ω, the current is 0.42A.
So, R-value must be set according to the part of the current to be drifted:
A care about this resistor heat dissipation: P = V²/R, with a Li-ion Vmax = 4.2V and R = 10 Ω, P = 1.7W, 3.5W in case of R
= 5 Ω. Do not use single 1/4W resistors!
A question for the grumpy ones… Why am I using bipolar transistors instead of CMOS? Because bipolar transistors are
driven in current, in opposite CMOS are driven with voltage. At least 10V. If it is not a problem for the shunt of B1 nor
B2, the GND reference for B3 is 8.4V and 12.6V for B4. It is supposed to have somewhere at least 22.6V to drive it. With
a 19V laptop power supply, it is not easy to get.
The dark side by using bipolar transistor is that they may need a lot of current. Power bipolar transistors’ Hfe is about
40. That means if collector current is 1A, the Ib > 25mA. Less the transistor will not act as a switch. That explains the rule
of R9 to R12 that limit the base current just enough, depending of the GND reference of each of them. They form with
R13 to R16 the voltage divisor and block transistor (= open switch) in case of no current.
R9 to R12 should be 1/2W resistors.
Finally, a simple way to be able to give some current at any voltage is the use of a SN754410 buffer.
The SN754410 offers the advantage to ENABLE or DISABLE the buffer follower with pins 1 and 9. R30 and C8 make a
time delay before enabling the IC, therefor D7 disable instantly.
6
Constant charging current & short circuit safety
It would be nice to insert a current sensor like the ACS712 module, but unfortunately, an analog entry would miss with
the Arduino Uno. However, I do not expect to use a bigger MPU for just driving a 4S BMS….
However, there are some digital inputs free; the solution is to
build a logical current sensor.
One of the interests of this project is its capability to use either Li-ion or LifePo4 cells. However, we have yet seen in the
introduction chapter that their voltages characteristics are different and incompatible. When addressing four cells at the
same time the power supply must be 16.8V for Li-ion, 14.8V for LifePo4.
Moreover, this
power supply must
be able to
decrease
depending of the
current through
the load, to be able
to obtain a
constant load
current. Of course
exceed voltage
limit must be
defined.
7
adaptation from the 5V to the power supply (around 19V). When D9 is “high” Q7 enters in saturation, the current in
Collector makes a voltage in R28. Then Q6 enters in saturation, the voltage at the Gate of Q5 passes from power supply
to 0V.
The P-channel CMOS is very less efficient than a N-channel. But in cutting configuration the Source pin must at the most
negative reference for a N-channel CMOS, and in this case the most negative point would be the power which load the
cells. It is not very good. Moreover, the Gate signal must be at least 10V, 10V + 14.8V = 24.8V, we have yet seen such a
problem with the shunt for B4… That is why we choose a P-channel.
SW1 allows selecting between 1 to 4 Li-ion or lifePo4 cells, 8 positions / 3 bits required.
SW2 allows switching on or off the device,
U3 is a 9V or 12V regulator, as it is now safe to power the Arduino Uno under more than 15V.
SG1 is a buzzer to alert under voltage cells
Cells shunt resistors should better be 5Ω instead of 10Ω .
8
Software study
Like the 4 parts that make up the hardware, the software is itself divided into 3 main parts:
Every loop a series of analogRead get the analog values from GND for each cell. First of a better accuracy there is a
series of 10 analogRead, then the working values is an average of them. Note a delay(1) after each analoRead to let
the time to the internal ADC to translate the analog value to bytes.
Another interesting point is Vcalibration[i]. The usual resistors have a tolerance value of 5%. It is too much. Each
final value has its Vcalibration to compensate these 5% of error. By default Vcalibration = 1.0 (=100% of value),
but it is possible to adjust it in parameters setup with the serial monitor.
// the following is done after every analogReadsCount cycles of reading analog values
//-----------------------------------------------------------------------------------
if( ++analogReadsCount > 100 ) return;
// calculate voltages
for( byte i=0; i<4 ; i++ ) {
Vcell[i] = (VcellCumul[i] / (float)analogReadsCount / 1023.0) * 5.0 * Vcalibration[i] * (i+1);
VcellCumul[i] = 0;
}
analogReadsCount = 0;
Valim = Vcell[3];
for( byte i=3; i>0; i-- ) {
Vcell[i] -= Vcell[i-1]; // get the voltage for each cell
} // end of for
9
Note that there is no way to stop either the buzzer or the shunt. If such a reset task has to be done, either you press the
Arduino Uno reset push-button, either you wait for one minute the code to initiate the runOnce procedure that reset all
flags:
// this is run only one time or after any config change or every 5 minutes
//------------------------------------------------------------------------
if( runOnce ) {
static byte counter = 0;
static unsigned long memo_tempo_shuntON = 0;
buzzerON = false; // stop the buzzer if any
for( byte i=cellNumber; i<4; i++ ) digitalWrite( shuntPin[i], HIGH ); // shunt inactive cells
if( counter > 5 ) {
counter = 0;
runOnce = false;
}
else if( tempo - memo_tempo_shuntON > 300 ) {
memo_tempo_shuntON = tempo;
counter++;
for( byte i=0; i<cellNumber; i++ ) digitalWrite( shuntPin[i], HIGH ); delay(1);
for( byte i=0; i<cellNumber; i++ ) digitalWrite( shuntPin[i], LOW ); // active cells reset
shunt
}
} // end of test runOnce
Power management
First of all every loop the load current status is read: the Imax flag is true if the current reaches the target value
(remember, it is defined by R8).
Note: two ways for power voltage safety are available: either the power voltage cannot exceed the total of cells
maximum voltage, either there is only a protection for analog input A0 not exceed 5V.
Then, depending of the Imax flag the PWM signal increases or decreases. The load voltage is constrained not to grow up
to an excessive value.
Parameters setup
10
The serialEvent() function is a specific built-in function that wait after any serial input. It is very useful and easy to
implement for parameters adjustment. The code treats of characters type input instead of strings, for a safe memory
use.
//
// serialEvent() : Arduino builtin function for any console input
//____________________________________________________________________________________________
void serialEvent() {
if( Serial.available() ) {
char incomingChar = Serial.read(); // no timeout nor delay unlike Serial.readBytesUntil()
if( incomingChar != '\n' ) {
consoleInput[index] = incomingChar;
index++;
}
else {
consoleInput[index] ='\0'; // null character
index = 0;
The code first looks at the first character (lower or upper case) of any console keyword input, and it can start with:
“B” : for battery cell. The case instruction will treat of cell voltages calibrations (remember the 5% of resistor
tolerance we need to correct)
“H” : for high limit voltage. The case instruction will treat the Vmax new value
“L” : for low limit voltage. The Vmin new value
“S” : for save. New data is recorded and stored in EEPROM for any future operation.
“E” : for exit. New data is lost.
/*
BMS Li-ion/LifoPo4 automatic charger 1S 2S 3S 4S
_________________________________________________________________
| |
| author : Philippe de Craene <[email protected] |
| Free of use - Any feedback is welcome |
_________________________________________________________________
Calibration of measured voltages are done with the console. Connect the Arduino Uno to the usb of
the PC.
The model (Li-ion or LifePo4) of cell and the number (1 to 4) to be charged are set with the binary
rotary button.
11
When the yellow LED assigned to each cell is continually lighted, the cell is charged (the shunt is
active).
A0 ==> B1 input
A1 ==> B2 input
A2 ==> B3 input
A3 ==> B4 input
A4 ==> SSD1306 display SDA
A5 ==> SSD1306 display SCL
2 ==> B1 shunt output - pin2 of 1A / SN754410
3 ==> B2 shunt output - pin7 of 2A / SN754410
4 ==> B3 shunt output - pin10 of 3A / SN754410
5 ==> B4 shunt output - pin15 of 4A / SN754410
6 ==> input from Imax charging sensor
7 ==> buzzer
9 ==> PWM output for buck converter
10 ==> x1 BCD rotary contactor
11 ==> x10 BCD rotary contactor
12 ==> x100 BCD rotary contactor
13 ==> ENABLE output for SN754410
Versions history
----------------
version 0.1 - 21 august 2020 - first operational version
version 0.3 - 1 sept 2020 - add the limit charging current
version 1.0 - 16 sept 2020 - add the buck converter
Remarks
-------
About Serial.print(F("bla bla") usage see https://www.baldengineer.com/arduino-f-macro.html
RAM usage decrease from 81% to 34% inside this code
*/
#include <EEPROM.h>
#include "ssd1306.h" // https://github.com/lexus2k/ssd1306
// Parameters
//-----------
const bool FIRST_USE = false; // must be set "true" the very first use to record parameters in
EEPROM
bool cellModel = LOW; // LOW = Li-ion / HIGH = LifePo4
byte cellNumber = 4; // number of cells to charge
float Vmax[2] = { 4.2, 3.7 }; // maximum voltage for Li-ion / LifePo4 cells
float Vmin[2] = { 3.6, 3.2 }; // minimum volatge for Li-ion / LifePo4 cells
float Vcalibration[4] = { 1.0, 1.0, 1.0, 1.0 }; // calibration to fit real voltage measures
// Hardware connexion
//-------------------
byte VcellPin[4] = { 0, 1, 2, 3 }; // analog read of each cell : A0:B1, A1:B2, A2:B3, A3:B4
byte shuntPin[4] = { 2, 3, 4, 5 }; // output to shunt for each cell
const byte ImaxPin = 6; // digital input from Imax charging sensor/detector
const byte buzzerPin = 7; // alarm undervoltage cell
const byte pwmPin = 9; // pwm for buck converter
const byte UrcPin = 10; // x1 BCD rotary contactor
const byte DrcPin = 11; // x10 BCD rotary contactor
const byte CrcPin = 12; // x100 BCD rotary contactor
const byte enablePin = 13; // SN754410 ENABLE
// Global variables
//-----------------
void setup() {
ssd1306_128x32_i2c_init();
//ssd1306_128x64_i2c_init();
ssd1306_fillScreen(0x00);
ssd1306_setFixedFont(ssd1306xled_font6x8);
ssd1306_clearScreen();
// last tasks....
RotactorConfig(); // check configuration from binary rotary contactor
digitalWrite( enablePin, HIGH ); // enable SN754410
} // end of setup
//
// loop
//____________________________________________________________________________________________
void loop() {
static float VcellCumul[4] = { 0.0, 0.0, 0.0, 0.0 }; // cumulative analogRead in bytes
static unsigned int analogReadsCount = 0; // number of analogRead counter
static bool buzzerON = false;
static bool oneMinute = false;
// this is run only one time or after any config change or every 5 minutes
//------------------------------------------------------------------------
if( runOnce ) {
static byte counter = 0;
static unsigned long memo_tempo_shuntON = 0;
buzzerON = false; // stop the buzzer if any
for( byte i=cellNumber; i<4; i++ ) digitalWrite( shuntPin[i], HIGH ); // shunt inactive cells
if( counter > 5 ) {
counter = 0;
runOnce = false;
}
else if( tempo - memo_tempo_shuntON > 300 ) {
memo_tempo_shuntON = tempo;
counter++;
for( byte i=0; i<cellNumber; i++ ) digitalWrite( shuntPin[i], HIGH ); delay(1);
for( byte i=0; i<cellNumber; i++ ) digitalWrite( shuntPin[i], LOW ); // active cells reset
shunt
}
} // end of test runOnce
// the following is done after every analogReadsCount cycles of reading analog values
//-----------------------------------------------------------------------------------
if( ++analogReadsCount > 100 ) return;
// calculate voltages
for( byte i=0; i<4 ; i++ ) {
Vcell[i] = (VcellCumul[i] / (float)analogReadsCount / 1023.0) * 5.0 * Vcalibration[i] * (i+1);
VcellCumul[i] = 0;
}
analogReadsCount = 0;
Valim = Vcell[3];
for( byte i=3; i>0; i-- ) {
Vcell[i] -= Vcell[i-1]; // get the voltage for each cell
} // end of for
// console display
ConsoleDisplay( whatToDisplay );
// oled display
OledDisplay();
} // end of loop
//============================================================================================
// list of functions
//============================================================================================
void RotactorConfig() {
//
// TendancySet() : set the tendancy of cells voltage
//____________________________________________________________________________________________
//
// EEPROM_Get() : read values stored in the EEPROM
//____________________________________________________________________________________________
void EEPROM_Get() {
//
// EEPROM_Update() : update values stored in the EEPROM
//____________________________________________________________________________________________
void EEPROM_Update() {
for( byte i=0; i<4; i++ ) {
int var = 1000*Vcalibration[i];
EEPROM.update((2*i), highByte(var));
EEPROM.update(((2*i)+1), lowByte(var));
}
EEPROM.update(8, ((Vmax[0]*100.0)-300));
EEPROM.update(9, ((Vmax[1]*100.0)-300));
EEPROM.update(10, ((Vmin[0]*100.0)-300));
EEPROM.update(11, ((Vmin[1]*100.0)-300));
} // end of EEPROM_Update()
//
// OledDisplay() : display to Oled
//____________________________________________________________________________________________
void OledDisplay() {
char flt2str[6];
//first line
ssd1306_printFixed ( 0, 0, "BMS", STYLE_NORMAL);
dtostrf( cellNumber, 1, 0, flt2str ); // usage : ( number_value, number_of_digits,
nulber_of_decimal, char_output)
ssd1306_printFixed (20, 0, flt2str, STYLE_NORMAL);
ssd1306_printFixed (30, 0, "x", STYLE_NORMAL);
if( cellModel ) ssd1306_printFixed (40, 0, "LifePo4", STYLE_NORMAL);
else ssd1306_printFixed (40, 0, "Li-ion", STYLE_NORMAL);
//
15
// ConsoleDisplay() : console displays
//____________________________________________________________________________________________
} // end of ConsoleDisplay()
//
// serialEvent() : Arduino builtin function for any console input
//____________________________________________________________________________________________
void serialEvent() {
if( Serial.available() ) {
char incomingChar = Serial.read(); // no timeout nor delay unlike Serial.readBytesUntil()
if( incomingChar != '\n' ) {
consoleInput[index] = incomingChar;
index++;
}
else {
consoleInput[index] ='\0'; // null character
index = 0;
} // end of switch
} // en of else
} // end of test Serial.available()
} // end of serialEvent()
17
User manual
The first time you use the device, some setup adjustment must be performed: the cells voltage calibration.
With the help of a multimeter you need to adjust cells voltages displayed on the SSD so that they are as close as possible
to the multimeter.
For instance:
If the new voltage values are correct type S<ENTER> to save them.
5- Once a cell is charged, the according LED is ON as a witness to the shunt function. The shunt will stay on until an
Arduino reset or after a delay of 5 minutes that reset all flags. By the way if the cell is really charged to according
LED will remain bright within a short delay.
6- In opposite any deficient cell or under voltage will tone the buzzer. Once gain only an Arduino reset or a delay of 5
minutes that reset all flags.
7- Once all shunts are ON it is possible to disconnect the power supply. The cell are then discharging though their own
shunt resistor. Any defective cell is then easily noticeable.
18
Illustration in use
Here is an example during charging process with the power supply, when all cells are full charged except B1:
19
Here is a picture during charging 4 standalone LifePo4 7000mA cells:
Here is a picture during discharging test for 12x4 Li-ion battery pack (no power supply):
20