Modular BMS
Public Made by Adomby adom
Modular Battery Management System — a composable BMS built from Adom molecules. TI BQ76952 monitor/protector with MSP430 host firmware, supporting 18650 cylindrical and EVE LF105 prismatic cell carrie
| Name | Last updated |
|---|---|
| BQ769x2Header.h | 14d ago |
| main_MSP430FR2355_I2C.cpp | 14d ago |
| README.md | 14d ago |
BQ76952EVM + MSP430F5529 BMS Firmware
Firmware for the BQ76952EVM using an MSP430F5529 as the I²C host. This project is based on TI’s BQ769x2 demo framework and adds higher-level helpers for:
- n-cell configuration using
VCellMode - periodic measurement polling and alarm handling
- CHG/DSG FET control from two GPIO inputs (P3.3 and P3.4)
- structured output in human-readable or JSON notification format in Hydrogen Style
Hardware
- MSP430F5529LP eval board (for programming over SBW)
- MSP430F5529 (host MCU)
- BQ76952EVM (battery monitor + protection + balancing)
- I²C: UCB1 mapped to P4.4=SCL, P4.5=SDA
- UART: UCA1 mapped to P4.2=RX, P4.3=TX
- FET control inputs:
- P3.3 → enable Discharge (DSG)
- P3.4 → enable Charge (CHG)
- Both are configured as GPIO inputs with pulldown resistors
Build / Run
This code is intended to be built in PIO toolchain using the MSP430F5529 eval board for SBW programming.
At startup, main():
- Stops the watchdog
- Initializes GPIO, port mapping, UART, I²C
- Resets the BQ76952
- Applies RAM configuration via
BQ769x2_Init() - Enables FET control and disables sleep for full-speed measurement updates
- Enters the main polling loop
Configuration
Cell Count
This project is configured for 3 cells using VCellMode:
#define NUM_CELLS 3
#define NUM_CELLS_BITS 0x0007 // enable Cell1..Cell3
NUM_CELLS_BITS is written to the VCellMode register inside BQ769x2_Init().
Output Modes
A global output_mode controls whether telemetry is printed as:
OUTPUT_HUMAN: readable console logs viaprintfOUTPUT_JSON: JSON notification packets (Hydrogen-style wrapper)
Select mode:
set_output_mode(OUTPUT_HUMAN);
// or
set_output_mode(OUTPUT_JSON);
JSON Wrapper
JSON output is wrapped in a common “notification” envelope via:
send_json_notification_data(method, id, data_str)send_json_notification_fmt(method, id, data_fmt, ...)
All BMS-specific output functions ultimately route through these.
Key High-Level BQ76952 Functions
BQ769x2_Init()
Applies the core BQ76952 RAM configuration:
- enters
CONFIG_UPDATE - sets power / regulator / TS pin config
- sets
VCellModefor your pack (3S by default) - enables protections (SCD/OCD/OV/UV/etc.)
- sets alarm mask, balancing config, thresholds
- exits
CONFIG_UPDATE
Used in main() once after reset, before enabling FETs.
BQ769x2_ReadAlarmStatus()
Reads AlarmStatus (direct command) and returns a 16-bit bitmask.
In main() this is used as the “scheduler”:
- if FULLSCAN bit is set → new measurements are ready
- if SafetyStatus alarm bits set → fetch safety status
BQ769x2_ReadAllVoltages()
Updates:
CellVoltage[0..15]fromCell1Voltage..Cell16VoltageStack_VoltagefromStackVoltagePack_VoltagefromPACKPinVoltageLD_VoltagefromLDPinVoltage
Important note: on the EVM, Pack_Voltage is the PACK pin after the FETs, so it may read ~0V when DSG/CHG FETs are open.
Important note: on the EVM, Stack_Voltage is what we would usually call the "Battery Pack" voltage. Basically the voltage from top cell to GND.
BQ769x2_ReadVoltage(command)
Generic “direct command” voltage read:
- Cell voltages are returned in mV
- Stack/Pack/LD voltages are treated as 0.01V units and converted to mV
BQ769x2_ReadCurrent()
Reads CC2Current (direct command) and returns the raw 16-bit value.
Current direction: CC2 current is signed (two’s complement).
Recommended usage in this codebase:
int16_t current_mA = (int16_t)BQ769x2_ReadCurrent();
// positive = charging, negative = discharging
BQ769x2_ReadTemperature(command)
Reads a temperature direct command (e.g., TS1Temperature) and converts to °C:
Temperature_C = 0.1 * raw_K - 273.15
Protection / Fault Monitoring
BQ769x2_ReadSafetyStatus()
Reads:
SafetyStatusASafetyStatusBSafetyStatusC
And derives high-level flags:
UV_Fault,OV_Fault,SCD_Fault,OCD_FaultProtectionsTriggered
In main(), safety status is checked whenever AlarmStatus indicates safety-related bits or when a protection was previously active.
BQ769x2_ReadPFStatus()
Reads Permanent Failure status registers A/B/C. Useful when diagnosing “fuse LED” or persistent shutdown behavior.
FET Control (CHG/DSG)
BQ769x2_ReadFETStatus()
Reads FETStatus and updates globals:
CHG,DSG,PCHG,PDSG
Use this to confirm the actual driver states.
BQ769x2_WriteFETStatus(bool DSG, bool CHG)
Controls CHG/DSG using the FET_CONTROL subcommand.
This function uses the BQ76952 convention:
- writing
DSG_OFF=1forces DSG OFF - writing
CHG_OFF=1forces CHG OFF
So the function takes enable booleans and internally writes the “OFF” bits as needed.
Example:
BQ769x2_WriteFETStatus(true, false); // DSG enabled, CHG forced off
Note: this is host control. The BQ76952 may still keep FETs open if protections are active (UV/OV/OCD/SCD/PF).
update_fets_from_buttons()
Reads the two GPIO inputs and applies FET control:
P3.3 high→ enable discharge (DSG)P3.4 high→ enable charge (CHG)
Then:
BQ769x2_WriteFETStatus(dsg_enable, chg_enable);
This is called periodically from main() (currently every 20 cycles).
Output Helpers (Human + JSON)
These functions produce either console output or JSON notifications depending on output_mode.
output_bms_config(battery_type, cell_count)
Emits a config packet at startup and periodically.
output_bms_voltages(pack_mV, CellVoltage[], cell_count)
Emits pack + per-cell voltages.
output_fet_status(chg, dsg)
Emits CHG/DSG states as booleans/ints.
output_current_value(current_mA)
Emits signed current in mA.
Main Loop Behavior (Current Program)
The while(1) loop does:
- Read
AlarmStatus - If FULLSCAN complete → read voltages/current/temps → clear FULLSCAN bit
- If safety alarm bits present → read safety status → clear alarm bits
- Every ~20 cycles:
- output voltages
- update FETs from button inputs
- read and output FET states
- read and output current
- Every ~100 cycles:
- re-emit config and print faults if present
- Delay ~20 ms
How to Write Your Own main()
A minimal, clean template:
int main(void)
{
WDTCTL = WDTPW | WDTHOLD;
GPIO_initPins();
map_I2C_and_UART();
UART_Init();
I2C_initModule();
set_output_mode(OUTPUT_JSON);
CommandSubcommands(BQ769x2_RESET);
delayUS(60000);
BQ769x2_Init();
delayUS(10000);
CommandSubcommands(FET_ENABLE);
CommandSubcommands(SLEEP_DISABLE);
while (1)
{
uint16_t alarms = BQ769x2_ReadAlarmStatus();
if (alarms & 0x80) {
BQ769x2_ReadAllVoltages();
int16_t current_mA = (int16_t)BQ769x2_ReadCurrent();
// clear FULLSCAN complete bit
DirectCommands(AlarmStatus, 0x0080, W);
output_bms_voltages(Stack_Voltage, CellVoltage, NUM_CELLS);
output_current_value(current_mA);
}
update_fets_from_buttons();
if (alarms & 0xC000) {
BQ769x2_ReadSafetyStatus();
// clear safety alarm bits
DirectCommands(AlarmStatus, 0xF800, W);
}
delayUS(20000);
}
}
Recommended approach:
- Drive logic from
AlarmStatusFULLSCAN rather than reading constantly. - Keep FET control separate (e.g., button read each loop).
- Treat CC2 current as signed (cast to
int16_t).
Common Gotchas
- PACK pin voltage reading 0V is normal if the PACK node is not being driven and/or FETs are open.
- Current is signed; treat
CC2Currentas two’s-complement. - Protections (UV/OV/OCD/SCD/PF) can override host FET commands.
- If the EVM “FUSE” LED is on, check PF status and PF-related configuration.
Module Notes
BQ769x2Header.h: contains direct command IDs, subcommands, and RAM register addresses- Low-level comms:
I2C_ReadReg(),I2C_WriteReg(), ISR state machineDirectCommands(),Subcommands(),CommandSubcommands()