Intro
Recently I had a chance to work on an off-grid 4-cell (12-ish V), 3kWh battery pack. In an attempt to make it safer and more robust a simple BMS board was designed to balance cells, and protect against over-voltage (safety), and under-voltage (not killing the cells).
Completed and conformally coated board:
Hand-assembled and sillicone-coated BMS board
Here's validation of the balancing algorithm using some old 18650 cells and modified thresholds from LiFePO4 chemistry:
Cells 1,3,4 being discharged by BMS due to artificially discharged cell 2 to simulate unbalance
Design Requirements
Some things about the thing actually needs to do:
In heavy undervoltage conditions, shut down the 3kW mains inverter.
In light undervoltage conditions, alert the user via chimes and lights that the battery has low energy left.
Do some minimal State-Of-Charge calcs based solely on voltage.
In over-voltage conditions, alert the user via loud chimes and lights. Actually shutting off the pack isn't necessary since the solar charger handles that. This is only useful in fault scenarios.
Balance cells as they go out of balance overtime (anecdotally from some dude on the internet: ~2% per 6 months for LiFEPO4).
Point 1 and 2 are of most interest, since they will be the most inadvertently triggered ones by the user.
Circuit Design
The circuit is relatively simple due to low voltage. It was designed to use jellybean parts: generic transistors and a PIC24 MCU brains. The cell balancing stack consists of a discharge PNP transistor, and a level-shifting NPN transistor. The choice of PNP allows much higher max. stack voltages since it's not limited by the 15V-or-so maximum Vgs on p-FETs. Transistors used are MMBT2907 and MMBT3904. There's also an LED to indicate balancing (and pull in extra current!). A single cell circuit looks like this:
Balance circuit on one cell
When CELL3_DCH turns on, Q6 turns on, pulling current out of Q2's base, turning it on, and basically shorting out the CELL3 across R2, which is 39 Ohms (~100mA current or so). The only limitation is that R6 has to be sized on a per-cell basis to keep Q2's base current constant across the stack. In theory this would be good for stack voltages up to Q6's max. Vce voltage (40V in this case).
Each tap on the stack is measured via a circuit that switches it into a resistive divider to MCU's voltage levels. This saves a lot of energy by not having the divider active all the time. Here's a one-cell circuit:
CELL1's voltage measuring circuit
R31 and R32 are precision 0.1% low temperature coefficient resistors sized to specific stack location (no idea why really, ADC is only 10-bit). CELL1_M_TRIG node turns on Q16 for a bit, which turns on Q15 p-MOS, and the voltage is read out at CELL1_M node after the RC circuit is charged up. C3 (1u) and C11 (10n) offer some filtering, especially C11 since it forms an RC circuit with the divider. The problem I tried to avoid came up here, since the gate voltage on the highest cell is equal to battery voltage, which in this case for the 18650 cells was 16.8V, enough to blow the gate on any regular p-FET. This meant I had to use more exotic FETs with 20V Vgs rating (ZXM61P03FTA). In the future, it could probably be possible to have some zener-resistor arrangement, and clamp the gate voltage regardless of battery voltage. Another way would be to use PNP BJT like in the discharge circuit, but the non-resistive Vce would make the readings very inaccurate.
All other circuits are wholly straightforward: a 3.3V LDO to power everything, a buzzer powered with a transistor and freewheel diode, and some optocouplers for outputs that would turn the 3kW inverter on/off (click image for full res):
Full schematic of BMS
Layout
A whole evening an a half was put into the layout to try to fit it into a 5x5cm 2-layer board, 5 of which were $5 total DHL-shipped from ALLPCB in a 3-day turn (wtf? how do they make money).
Top side PCB: most traces
Bottom side: nice solid ground plane
Firmware
Most of the FW framework came from the opensky altimeter: the simple co-operative scheduler, and some minimal drivers. The scheduler runs at 10Hz, and there's only 2 tasks for now: main loop and beeper. The main loop samples all the cells, finds the lowest one, and then starts discharging other cells that are 2% or more higher than the lowest one. It stops discharging when they're within 0.5% for hysteresis. In fault conditions it also turns off an optocoupler that is in the switch path of the 3kW inverter, turning off all high-current loads. The beeper times all the beeps so it's not as obnoxious as a single solid BEEEEEEEEP at 2.3KHz. The current consumption is 3-4mA when it's not beeping or lighting up LEDs. The MCU runs at 1MHz with plenty of cycles to spare, and sleeps ~95% of the time.