
简介这套基于STM32F103与BQ76920的锂电池BMS系统设计以C语言完成核心逻辑面向高校电子、自动化、计算机等专业师生及嵌入式开发工程师适用于课程实践、毕业设计、科研预研或电池管理原型开发。压缩包约26.76MB内含C语言源代码与完整工程文件覆盖电压/电流/温度采集、过压欠压过流过热保护、SOC估算与均衡控制等关键模块代码经实际运行验证可直接部署于STM32F103平台设计上预留了阈值调整与通信扩展空间便于开发者为不同容量电池组二次开发。目前已有59人学习下载工程内包含完整的初始化配置与中断处理流程对深入理解ADC采样、状态机调度和锂电保护策略很有帮助嵌入式初学者可结合实物板卡边读源码边验证。资源仅用于个人学习与技术交流请勿用于商业用途。1. 一块18V电池包背后的BMS为什么选STM32F103加BQ76920很多工程师第一次认真考虑自研BMS不是因为在做电动车项目而是被一个18V电钻电池包炸醒的专用保护IC只有过充过放客户却要你上报每节电芯电压、估算剩余电量、记录故障历史。这个时候保护板已经不只是保护板得换成一套能用C语言控制的电池管理系统。STM32F103和BQ76920这个组合是入门自研BMS最稳妥的一条路前者是工程师最熟悉的MCU后者是TI专为3到5节锂电设计的模拟前端两者通过I2C通信。你不需要一开始就啃复杂的电量计算法先把电压、电流、温度读准把保护逻辑跑稳就能覆盖绝大多数便携设备、电动工具和轻型储能场景。这篇文章不打算给你一个完整开源工程的逐行注释而是按硬件接线、C语言驱动、保护状态机、联调避坑、验证手段这条线讲清楚做这一套系统需要理解的关键设计点以及那些翻车之后才知道的细节。2. BMS硬件架构与接口选型BQ76920负责感知STM32F103负责策略任何一个可用的BMS硬件都绕不开“谁采集、谁决策、谁执行”这三个角色。BQ76920负责采集电芯电压、电流、温度也自带一组模拟比较器用于硬件级保护STM32F103则通过I2C读取这些采集结果跑保护策略、计算状态、控制充电和放电通路。这套分工决定了你画板子和写驱动时的模块边界。2.1 bms和bmu的区别BQ76920不是完整BMS它是模拟前端不少刚接触这个领域的硬件工程师会把BQ76920直接叫作“BMS芯片”这个说法在选型评审时容易被较真的人挑出来。准确一点讲BQ76920是BMUBattery Monitor Unit也就是电池监控单元负责把电芯的模拟量变成数字量并执行硬件级的过压、欠压、过流比较。完整意义上的BMS还要包含策略层和控制层——比如SOC估算、均衡策略、充放电MOS驱动、与外部主控的通信协议这些是STM32F103要干的活。BQ76920支持3到5节电芯串联内部集成了ADC、库仑计输入端、三个热敏电阻通道和充放电保护FET的驱动引脚。它有一个ALERT引脚采集到异常状态时会主动通知MCU不需要MCU每毫秒轮询。你要是只做一个4串的电动工具电池包BQ76920加上一颗STM32F103C8T6就完全够用如果目标是十几串的动力电池那就要换成BQ76930或者BQ76940这样的多串AFE但驱动框架和通信协议是同一套思路。这种BMU加MCU的方案最大的好处是保护和策略解耦。即使MCU死机或者固件跑飞BQ76920内部比较器依然能独立触发保护动作把充放电FET断开。这是很多集成式BMS芯片做不到的——它把鸡蛋放在一个篮子里。做产品化设计时这个冗余点往往是客户能否接受你方案的关键。2.2 stm32f103最小系统与关键引脚分配I2C、中断和充放电控制如果你手头有现成的stm32f103最小系统板验证阶段可以直接飞线接BQ76920评估板不用先画完整PCB。但正式做产品时我建议自己画一个精简最小系统把MCU、晶振、复位、SWD调试口和电源都集成到BMS板上避免用现成开发板带来的地线环路和装配问题。下面是这套方案里我常用的引脚分配重点是I2C、ALERT中断和充电放电使能信号MCU引脚方向说明I2C1_SCLPB6输出接BQ76920 SCL开漏加4.7k上拉I2C1_SDAPB7双向接BQ76920 SDA开漏加4.7k上拉BQ76920_ALERTPA0输入配置为EXTI下降沿中断CHG_ENPB0输出控制充电MOS的使能三极管DSG_ENPB1输出控制放电MOS的使能三极管UART2_TX/RXPA2/PA3输出/输入调试日志与上位机通信I2C1是STM32F103原生支持的一个硬件I2C外设但很多人最终会放弃它转用软件模拟I2C原因我放到第3章细讲。ALERT引脚必须接在支持外部中断的引脚上因为BQ76920的过压、欠压、过流告警都是通过这个引脚的电平变化通知MCU的如果靠主循环轮询寄存器反应时间会被循环周期拖慢。CHG_EN和DSG_EN这两个引脚控制的是“是否允许充电/放电”而不是直接驱动功率MOS。功率MOS的栅极由BQ76920的CHG和DSG引脚直接驱动MCU这边用GPIO控制一个三极管或MOS管去开关充放电回路的使能电源。这种两级控制的好处是即使MCU误拉高了CHG_ENBQ76920检测到过压后照样能从硬件层面拉低CHG输出形成冗余保护。2.3 外围电路三个必查点REGIN供电、分流电阻、NTC接法BQ76920的外围电路不复杂但三个点容易画错。第一是REGIN和REGOUT的关系。REGIN直接从电池组正端取电经过芯片内部稳压后从REGOUT输出一个低压电源这个电源可以用来给外部上拉电阻、光耦或者逻辑电路供电。很多第一次画板的人以为REGOUT会默认输出实际必须在SYS_CTRL1寄存器里置位REGOUT_EN才会打开。如果你的固件初始化顺序不对REGOUT永远是0VI2C上拉没有电源整条总线直接哑掉。第二是分流电阻的布局。BQ76920的电流采集是通过外部分流电阻两端差分电压实现的这个电阻一般选0.5mΩ到2mΩ取决于你的最大持续电流。画PCB时分流电阻的采样走线必须用开尔文接法从电阻焊盘内侧单独引一对细线到芯片的SRP和SRN引脚绝对不能直接铺铜连过去否则铜箔上的压降会把电流误差放大到不可接受的程度。第三是热敏电阻通道。BQ76920提供TS1、TS2、TS3三个温度采集引脚每个引脚内部有可配置的电流源接NTC后通过ADC测电压换算温度。这里有个新手容易犯的错误看到STM32F103内部也有温度传感器就想省掉NTC直接用MCU内部温度。答案是不行MCU内部温度传感器测的是芯片自身温度跟电芯温度没有任何关系而且误差经常超过正负5度。电池组的热管理必须用贴在电芯表面的NTC这个没得省。3. 用C语言写BQ76920驱动模拟I2C、PEC校验与初始化序列硬件画完接下来的核心工作就是用C语言把BQ76920的寄存器读写跑通。BQ76920本身不复杂它就是一颗I2C从机但你一旦启用PEC校验通信帧的拼装、校验和多字节读写的时序都要处理干净否则会因为偶发错帧导致保护寄存器被写成错误值。3.1 驱动分层与标准库V3.5.0工程组织把驱动从业务里剥出来工程组织上我习惯沿用stm32f103标准库V3.5.0的模板把代码分成四个目录core启动文件和时钟配置、periphGPIO、定时器、DMA等外设封装、bmsBQ76920驱动、保护逻辑、状态机、app主循环和业务逻辑。BMS驱动绝不直接散落在main.c里因为后续你要换BQ76930或者改成SPI接口的AFE芯片时只需要重写bms目录下的驱动文件策略代码一行都不用动。驱动层内部再分两个文件bq76920_i2c.c负责最底层的I2C字节收发bq76920.c负责寄存器读写、PEC计算和功能函数封装。业务层只跟bq76920.c的接口函数打交道不感知底层是软件I2C还是硬件I2C。这个分层在联调时价值很大出了问题能快速定位是通信层的问题还是策略层的问题不用拿着逻辑分析仪去逐行查业务代码。3.2 模拟I2C实现STM32F103硬件I2C的坑与绕行先给出我常用的软件I2C实现。GPIO配置为开漏输出外部接4.7kΩ上拉电阻到3.3V这样可以避免输出模式切换带来的毛刺#define I2C_SCL_PORT GPIOB #define I2C_SCL_PIN GPIO_Pin_6 #define I2C_SDA_PORT GPIOB #define I2C_SDA_PIN GPIO_Pin_7 #define SCL_HIGH() GPIO_SetBits(I2C_SCL_PORT, I2C_SCL_PIN) #define SCL_LOW() GPIO_ResetBits(I2C_SCL_PORT, I2C_SCL_PIN) #define SDA_HIGH() GPIO_SetBits(I2C_SDA_PORT, I2C_SDA_PIN) #define SDA_LOW() GPIO_ResetBits(I2C_SDA_PORT, I2C_SDA_PIN) #define SDA_READ() GPIO_ReadInputDataBit(I2C_SDA_PORT, I2C_SDA_PIN) void i2c_gpio_init(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); gpio.GPIO_Pin I2C_SCL_PIN | I2C_SDA_PIN; gpio.GPIO_Mode GPIO_Mode_OUT_OD; // 开漏输出配合外部上拉 gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(I2C_SCL_PORT, gpio); SCL_HIGH(); SDA_HIGH(); } static void i2c_delay(void) { volatile uint32_t t 400; while (t--) ; } static void i2c_start(void) { SDA_HIGH(); SCL_HIGH(); i2c_delay(); SDA_LOW(); i2c_delay(); SCL_LOW(); i2c_delay(); } static void i2c_stop(void) { SCL_LOW(); i2c_delay(); SDA_LOW(); i2c_delay(); SCL_HIGH(); i2c_delay(); SDA_HIGH(); i2c_delay(); } static uint8_t i2c_write_byte(uint8_t dat) { for (uint8_t i 0; i 8; i) { if (dat 0x80) SDA_HIGH(); else SDA_LOW(); dat 1; i2c_delay(); SCL_HIGH(); i2c_delay(); SCL_LOW(); i2c_delay(); } SDA_HIGH(); // 释放SDA等待从机ACK i2c_delay(); SCL_HIGH(); i2c_delay(); uint8_t ack (SDA_READ() 0) ? 1 : 0; SCL_LOW(); i2c_delay(); return ack; } static uint8_t i2c_read_byte(uint8_t ack) { uint8_t dat 0; SDA_HIGH(); // 释放SDA让从机驱动数据线 for (uint8_t i 0; i 8; i) { dat 1; SCL_HIGH(); i2c_delay(); if (SDA_READ()) dat | 0x01; SCL_LOW(); i2c_delay(); } if (ack) SDA_LOW(); else SDA_HIGH(); // 最后一个字节回NACK SCL_HIGH(); i2c_delay(); SCL_LOW(); i2c_delay(); SDA_HIGH(); return dat; }这段代码的逻辑很简单i2c_start和i2c_stop严格按I2C时序翻转SCL和SDAi2c_write_byte在8个时钟周期内把数据移位输出并在第9个时钟读取从机ACKi2c_read_byte在8个时钟周期内读取数据然后根据参数决定回ACK还是NACK。i2c_delay用空循环产生大约1微秒的延时整个时序跑在100kHz标准模式对BQ76920这种低速器件完全够用。为什么绕开STM32F103的硬件I2C因为F103的硬件I2C在总线发生异常时序后状态机容易卡在BUSY状态软件复位都未必能立刻拉回来。很多工程师把这个现象归结为玄学实际是芯片设计的问题社区里早就有了共识。你要在量产产品上用硬件I2C必须加大量的超时恢复逻辑得不偿失。软件I2C虽然占CPU但对于BQ76920这种几百Hz采样率的器件来说CPU开销可以忽略。3.3 带PEC的寄存器读写BQ76920通信帧完整实现BQ76920支持PECPacket Error Checking校验能让每个通信帧都带上CRC-8校验字节防止总线干扰导致寄存器被写成错误值。CRC算法使用多项式0x07初值为0计算范围覆盖从机地址、寄存器地址和数据字节。下面是校验函数和读写函数#define BQ76920_I2C_ADDR 0x18 // 7位地址 #define BQ76920_W (BQ76920_I2C_ADDR 1) // 0x30 #define BQ76920_R ((BQ76920_I2C_ADDR 1) | 1) // 0x31 static uint8_t bq76920_pec(uint8_t *buf, uint8_t len) { uint8_t crc 0; for (uint8_t i 0; i len; i) { crc ^ buf[i]; for (uint8_t b 0; b 8; b) { if (crc 0x80) crc (uint8_t)((crc 1) ^ 0x07); else crc 1; } } return crc; } uint8_t bq76920_write_reg(uint8_t reg, uint8_t val) { uint8_t buf[4] {0}; i2c_start(); if (!i2c_write_byte(BQ76920_W)) { i2c_stop(); return 1; } if (!i2c_write_byte(reg)) { i2c_stop(); return 1; } if (!i2c_write_byte(val)) { i2c_stop(); return 1; } buf[0] BQ76920_W; buf[1] reg; buf[2] val; if (!i2c_write_byte(bq76920_pec(buf, 3))) { i2c_stop(); return 1; } i2c_stop(); return 0; } uint8_t bq76920_read_reg(uint8_t reg, uint8_t *val) { uint8_t buf[4] {0}; uint8_t rx_pec; i2c_start(); if (!i2c_write_byte(BQ76920_W)) { i2c_stop(); return 1; } if (!i2c_write_byte(reg)) { i2c_stop(); return 1; } buf[0] BQ76920_W; buf[1] reg; if (!i2c_write_byte(bq76920_pec(buf, 2))) { i2c_stop(); return 1; } i2c_start(); if (!i2c_write_byte(BQ76920_R)) { i2c_stop(); return 1; } *val i2c_read_byte(1); // 第1个数据字节回ACK以继续读PEC rx_pec i2c_read_byte(0); // 第2个字节是PEC回NACK结束事务 i2c_stop(); buf[0] BQ76920_R; buf[1] reg; buf[2] *val; if (bq76920_pec(buf, 3) ! rx_pec) return 2; // PEC不匹配 return 0; }读写帧的结构需要仔细理解。写寄存器时主机发出从机地址、寄存器地址、数据字节之后还要追加一个PEC字节PEC的计算范围是“写地址寄存器地址数据”三个字节。读寄存器时分两段第一段发送写地址和寄存器地址并追加PEC第二段重新发送读地址然后从从机读取数据字节和PEC字节最后校验PEC是否一致。这个两段式结构是BQ76920系列比较特殊的地方不少人第一次用其他TI电量计芯片的习惯来写结果读回来的永远是0xFF。使能PEC之后任何不带PEC的帧都会被从机忽略。所以如果你在调试阶段发现寄存器写不进去先检查是不是初始化时开了PEC但读写函数没带校验字节。我在代码里建议始终启用PEC因为I2C总线在电机驱动或者MOS开关附近很容易受到干扰错一帧数据对保护逻辑来说可能就是灾难。3.4 初始化配置序列REGIN使能、PEC开启、ADC启动BQ76920上电后的初始化顺序是有讲究的。我习惯按下面这个流程配置void bms_hw_init(void) { uint8_t stat 0xFF; /* 第1步读SYS_STAT确认通信建立了再往下走 */ if (bq76920_read_reg(0x00, stat)) { /* 通信失败需要检查硬件和上拉 */ return; } /* 第2步SYS_CTRL1置位REGOUT_EN(0x80)和PEC_EN(0x40) */ bq76920_write_reg(0x02, 0xC0); /* 第3步配置ADC采样选项并启动转换 */ bq76920_write_reg(0x11, 0x41); // ADCOPT选择ADC时钟和采样模式 bq76920_write_reg(0x12, 0x00); // ADCCTL启动连续转换 /* 第4步写保护阈值编码值需要按电芯规格查手册换算 */ bq76920_write_reg(0x05, BQ76920_OV_TRIP_CODE); bq76920_write_reg(0x06, BQ76920_UV_TRIP_CODE); /* 第5步使能硬件级保护功能 */ bq76920_write_reg(0x04, BQ76920_PROTECT_EN_MASK); }初始化流程的第1步我建议保留上电后先读一次SYS_STAT寄存器确认I2C通信真正建立再往里写配置。这样做的好处是如果硬件接错或上拉电阻没焊你能在初始化阶段就发现而不是等到保护不动作时再排查。第3步里ADCOPT和ADCCTL的取值直接决定采样速度和功耗对电池包这种常驻系统来说ADC采样周期要根据你需要的响应速度来选不是越快越好越快功耗越高。初始化完成之后BQ76920会按配置周期性地转换电压、电流和温度结果存放在对应的数据寄存器里。STM32这边要做的事情就是按一定周期去读这些寄存器然后跑保护逻辑。读取周期我一般设置在100ms到500ms之间这个频率对电芯保护足够又不会把I2C总线占用得太厉害。4. 采集换算与保护状态机从寄存器原始值到充放电控制通信打通、驱动能读写寄存器之后真正体现BMS设计水平的地方在于怎么把原始码值换算成有物理意义的毫伏和毫安怎么设计保护阈值和恢复条件怎么用状态机把充放电过程管起来。4.1 电压与电流数据读取16位原始值怎么换算成mV和mABQ76920的电芯电压和电流都通过ADC转换数据存放在16位的寄存器中高位寄存器和低位寄存器在地址上是连续的。读取时我用一个通用函数避免每个数据项都写一遍I2C逻辑/* 电芯电压寄存器起始地址具体宏以手册为准 */ #define BQ76920_CELL1_LO 0x08 #define BQ76920_CELL1_HI 0x09 uint16_t bms_read_cell_mv(uint8_t cell_index) { uint8_t lo 0, hi 0; uint16_t raw; bq76920_read_reg(BQ76920_CELL1_LO cell_index * 2, lo); bq76920_read_reg(BQ76920_CELL1_HI cell_index * 2, hi); raw ((uint16_t)hi 8) | lo; /* 原始码值乘以校准系数得到毫伏系数为千分值避免浮点 */ return (uint16_t)((uint32_t)raw * g_cell_mv_per_lsb / 1000); } int16_t bms_read_current_ma(void) { uint8_t lo 0, hi 0; int16_t raw; bq76920_read_reg(BQ76920_CC_LO, lo); bq76920_read_reg(BQ76920_CC_HI, hi); raw (int16_t)((hi 8) | lo); return (int16_t)((int32_t)raw * g_i_ma_per_lsb / 1000); }这里的换算系数g_cell_mv_per_lsb和g_i_ma_per_lsb不用一开始就查手册填理论值。我一般先按1mV/LSB和1mA/LSB跑通代码然后拿万用表实测电芯电压对比原始码值用“实测毫伏除以原始码值”得出一个初始系数。多测两个电压点取平均再把这个系数固化成常量。电流换算同理用电子负载加恒定电流把读回来的原始码值跟实际电流对比算系数。校准系数是BMS产品化的一件后悔药同一个硬件换一批电芯或者换一个分流电阻批次系数就要微调。所以我建议在产品里留一个通过串口指令在线修改系数的后门产线校准的时候写进Flash而不是让生产工人去改源码重新编译。4.2 过充、过放、过流保护参数设计阈值、迟滞与恢复条件BQ76920的保护分为硬件保护和软件保护两层。硬件保护由芯片内部的模拟比较器完成阈值配置在OV_TRIP、UV_TRIP、PROTECT这些寄存器里触发后BQ76920直接控制CHG和DSG引脚断开MOS不需要MCU参与响应时间在毫秒级。软件保护则由STM32在自己的主循环里做判断好处是阈值可以随时修改还能加上温度、压差等硬件比较器不覆盖的条件。以常见的4串锂电包为例我通常这样配置保护项硬件阈值示例软件阈值恢复条件单节过充4.25V4.20V告警4.25V断充电电压低于4.10V且持续2秒单节欠压2.80V2.90V告警2.80V断放电电压高于3.00V且持续2秒充放电流硬件比较器限制软件按工况分级电流回落到阈值以下持续1秒温度不依赖硬件0至45度充-20至60度放温度回落到边界内持续5秒设计保护参数时阈值和恢复条件一定要分开不能用一个固定值既做触发又做恢复。比如过充触发是4.25V如果恢复条件也是4.25V电池在临界点来回抖动会导致MOS反复开关一个晚上就能把继电器触点或者MOS管折腾坏。正确的做法是加迟滞触发保护用一个较高阈值恢复条件降低一些再叠加时间条件过滤掉瞬时尖峰。4.3 主循环状态机实现IDLE、充电、放电、FAULTBMS主循环本质是一个状态机状态之间的迁移依据是采集到的电压、电流和温度。我实现了一个简化版本typedef enum { BMS_STATE_IDLE 0, BMS_STATE_CHARGE, BMS_STATE_DISCHARGE, BMS_STATE_FAULT, } bms_state_t; static bms_state_t g_state BMS_STATE_IDLE; static uint8_t g_fault_reason 0; #define BMS_FAULT_OV 0x01 #define BMS_FAULT_UV 0x02 #define BMS_FAULT_OC 0x04 #define BMS_FAULT_OT 0x08 void bms_loop(void) { uint16_t cell_mv[4]; int16_t cur_ma; uint8_t flags 0; /* 1. 采集最新数据 */ for (uint8_t i 0; i 4; i) { cell_mv[i] bms_read_cell_mv(i); } cur_ma bms_read_current_ma(); /* 2. 依次判定各保护条件汇总到flags */ for (uint8_t i 0; i 4; i) { if (cell_mv[i] BMS_OV_LIMIT_MV) flags | BMS_FAULT_OV; if (cell_mv[i] BMS_UV_LIMIT_MV) flags | BMS_FAULT_UV; } if (cur_ma BMS_DIS_OC_LIMIT_MA) flags | BMS_FAULT_OC; /* 3. 有故障则进入FAULT无故障则从FAULT恢复 */ if (flags) { bms_enter_fault(flags); } else { bms_clear_fault(flags); } /* 4. 根据电流方向或使能信号决定工作状态 */ if (g_state ! BMS_STATE_FAULT) { g_state bms_decide_run_state(cur_ma); } }状态机的逻辑要点在于故障进入要快故障恢复要慢。bms_enter_fault里除了记录故障原因还要立刻关掉CHG_EN和DSG_EN对应的GPIO同时把故障码存到备份寄存器或Flash方便事后排查。bms_clear_fault里实现恢复条件先检查故障标志是否消失再检查恢复延时是否满足两者都通过才允许从FAULT回到IDLE。bms_decide_run_state这个函数根据当前电流方向和系统使能信号决定状态有充电使能且电流为负进入充电态有放电使能且电流为正进入放电态两者都没有回到空载态。这里的电流方向正负取决于分流电阻的接入方向实际调试时以你定义的充电电流符号为准。5. 联调避坑I2C死锁、上电时序、串口丢数据与寄存器失效BMS硬件和固件都做好之后真正让人掉头发的是联调阶段。这里把我踩过的一些坑列出来每一条都是现象、原因、解决三段式方便你对照排查。5.1 STM32F103硬件I2C总线忙死锁是玄学但能解现象初始化代码执行到I2C_GenerateSTART()之后程序卡死在等待标志位的循环里读I2C-SR1发现BUSY位一直为1。或者更诡异第一次读写正常程序跑了一会儿再调I2C接口返回全是超时。原因STM32F103的硬件I2C在从机无ACK、总线被外部干扰拉低、或者主机在错误时序下发起操作时内部的仲裁状态机可能进入不可恢复的BUSY状态。GPIO复位寄存器只能恢复引脚的电气状态恢复不了I2C外设内部的状态机。解决我最后选择彻底放弃硬件I2C改用软件模拟I2C就是第3章那套代码。如果项目里一定要用硬件I2C至少要在每次操作前加超时判断超时后把I2C外设彻底去初始化再重新初始化并且把SCL线翻转9个时钟周期尝试释放可能被从机锁住的总线。但说实话这套恢复代码写下来比软件I2C的开销还要大没有必要。5.2 BQ76920上电后REGIN有了但REGOUT没输出查这三个点现象板子供电正常用万用表量REGIN引脚电压正确但REGOUT引脚始终是0VI2C通信完全无响应示波器看SDA上拉电压都拉不起来。原因第一固件里没有置位REGOUT_EN寄存器位芯片默认不输出第二REGOUT输出端的电容取值太大或者漏电导致启动时过流保护第三REGIN电压刚好落在芯片的欠压锁定门限附近芯片反复进入上电复位。解决初始化流程里第一件事就是配置SYS_CTRL1置位REGOUT_EN这个步骤不能省。REGOUT电容我一般用2.2uF陶瓷电容不要用钽电容或者大容量的电解电容。REGIN需要确认在芯片规定的正常工作范围之内如果用的是4串电池组空载电压偏高的情况中间要加一级预稳压或者适当降低串联电芯数。5.3 串口中断接收掉数据包调试口别拖垮BMS主循环现象通过串口每秒打印一次BMS状态数据在电池包带载放电时日志会随机丢掉几个字节甚至一整包偶尔还会出现打印内容错位。把波特率从115200降到9600之后现象减轻但仍存在。原因串口接收用的逐字节中断方式每接收一个字节就进一次中断。如果主循环正在执行I2C读写或者其他比较长的临界区串口中断得不到及时响应接收数据寄存器的内容还没被取走就被下一个字节覆盖产生ORE溢出错误。BMS主循环里I2C等待从机应答的时间是不确定的这两个事件很容易撞在一起。解决最简单的办法是把调试串口改成DMA接收配合空闲中断判断一包数据结束。实在调不出来也有个偏方用定时器实现软件串口把波特率降到38400以下给主循环留出足够的处理时间。但这只是过渡方案量产固件里建议彻底去掉调试串口只保留一个可配置的静默模式用的时候再开启。5.4 电压采集跳动和偏大校准系数与滤波顺序现象同一节电芯万用表量出来3.600VBMS读回来3.620V而且在3.610V到3.635V之间来回跳动跳动幅度超过20mV。原因硬件层面可能是采样走线太长引入了噪声或者分流电阻的地线没处理好导致ADC参考地被抬高软件层面可能是校准系数没标定且没做任何滤波处理直接把单次转换结果拿来做保护判断。解决先做硬件检查把电芯采样线的位置从MOS管附近挪开确保模拟地和功率地在PCB上单点连接。软件上我加了一个简单的滑动平均滤波取最近4次采样做平均并且把滤波放在保护判断之前。但要注意滤波会延迟保护响应过压这种高速保护用滤波后的值去做判断等于自废武功。正确做法是硬件保护用原始值快速触发软件层面只用滤波值做状态显示和SOC计算。5.5 写进去的寄存器不生效PEC、回读与喂狗三件事现象初始化代码执行完没有报错读回OV_TRIP寄存器也是期望值但真把电池充到设定电压时BQ76920没有触发保护MOS管照常导通。原因大概率是PEC计算错误写入的帧被从机拒绝但I2C主机侧没有感知到。也可能是BQ76920的内部看门狗超时把配置过的保护功能重置回了默认状态保护寄存器被清零而你读到的值是默认值恰好跟期望值一样。解决写完关键寄存器之后必做回读把回读值和写入值对比不一致就打印错误。同时检查PEC使能后的帧格式确认PEC计算范围覆盖到了从机地址和寄存器地址。关于看门狗BQ76920要求主机周期性地跟它通信否则会触发一个保护关闭动作把CHG和DSG输出禁掉。所以主循环里要有一个专门的喂狗操作我习惯用100ms定时器周期性地读一次SYS_STAT寄存器既喂了狗又顺带确认通信链路正常。6. 进阶验证技巧逻辑分析仪抓I2C帧离线仿真跑保护策略通信调通、保护逻辑跑起来之后不要急着拿真电池组做满充满放试验。先花半天时间做好验证能省下后面大量的返工时间。6.1 先抓一次完整I2C事务把BQ76920通信从黑匣子变成明盘把逻辑分析仪的通道接在SCL和SDA上采样率调到1MHz以上触发条件设置为I2C地址0x30。跑一遍初始化代码然后把逻辑分析仪的I2C协议解码打开逐帧核对波形。重点检查三个地方第一帧头有没有合法的起始条件和停止条件第二ACK位是否每个都在正确位置出现尤其是PEC字节后面的ACK第三PEC校验字节是否跟软件计算结果一致。这一步能一次性排除引脚接反、上拉电阻缺失、I2C时序不达标、PEC计算错误这几类最隐蔽的问题。不要对着示波器肉眼看波形判断ACK直接用协议解码器可以精确到每个字节。6.2 Renode和Simulink在BMS验证里能干什么、不能干什么如果没有硬件板子又想先验证STM32F103侧的代码逻辑可以在Renode里跑STM32F103的仿真环境。Renode能模拟GPIO、串口和定时器代码里基于软件I2C的逻辑可以在仿真环境里跑通。但这里有个边界Renode模拟不了BQ76920的模拟前端你没法在仿真环境里制造真实的过压场景。我的做法是给驱动层写一个mock接口在仿真环境下用一段伪数据模拟寄存器返回值用单元测试跑一遍状态机的迁移逻辑确保代码里没有死循环或者数组越界。如果你所在的团队习惯用Simulink做算法仿真可以把保护状态机在Simulink里建模输入电压、电流、温度的时间序列观察状态输出的时序是否符合预期。但Simulink模型只能验证策略层的逻辑正确性寄存器读写、I2C时序、PCB布局带来的干扰还是要拿到真板上验证。真板验证我建议按下面的表格做一轮最小用例集每一项记录实测动作时间测试项注入条件期望动作实测记录过充保护用程控电源将最高节电压缓慢升到4.30V充电MOS关闭ALERT拉低MCU记录故障动作时间应小于1秒过放保护电子负载放电至最低节2.80V放电MOS关闭系统进入休眠态动作时间应小于1秒过流保护负载电流拉升至超过阈值放电MOS关闭故障码记录动作时间应小于500ms温度保护NTC用电阻箱模拟高温充放电路径断开按产品需求定我现在开新板子习惯是先写好I2C回读和喂狗逻辑再画MOS驱动电路逻辑分析仪通道也留好测试点这些准备工作帮我挡掉过不少返工。做BMS就是这样前期通信层的每一步都验证扎实后期电池包整pack联调才有底气。希望帮到你。本文还有配套的精品资源点击获取