
1. 方案选型与整体架构先决定“用哪块单片机”和“怎么布采样链路”做基于单片机的电池管理系统最怕一上来就写代码。硬件拓扑没定、采样芯片没选明白后面写再多算法都是空中楼阁。这块板子折腾了大概两个月第一版就是把方案定死了才没翻车。1.1 主控选型为什么我选了STM32F103而不是STC8或51单片机先说结论如果只是做一个“能跑”的电池保护板51单片机、STC8完全够用但如果想做“能用、能调、能扩展”的BMS我建议直接上STM32F103。原因有三条都是实测踩出来的。第一ADC资源。BMS里面最核心的就是电压采样。5串电池要采6个采样点加上电流、温度一个STM32F103有3个ADC、10个通道做一轮采样只要十几微秒而且支持注入通道和DMA。用51单片机做多通道采样得靠外部多路开关还要自己控制切换时序程序复杂度上来了精度还未必好——51的ADC在很多型号上是10位而且参考电压就是电源电压电源一波动采样就漂。第二定时器和PWM。BMS里的被动均衡需要定时控制放电电阻的开关时间充电管理需要PWM控制预充电回路。STM32的TIM1/TIM2带高级定时功能能输出互补PWM、带死区后面想扩展有源均衡或者做DC-DC控制硬件上就不用改了。STC8也有PWM但输出通道数量和分辨率都差一截。第三通信接口。我这套系统需要给上位机上报电压、电流、SOC和故障状态走的Modbus RTU。STM32的USART带空闲中断和DMA接收不定长帧非常方便用51单片机收Modbus帧就得自己搭超时计数虽然也能做但代码维护成本高不少。后面如果升级到CAN总线STM32F103是自带bxCAN的而51单片机需要外挂CAN控制器芯片成本和布局复杂度都上去了。当然STC8也有它的优势——便宜、抗干扰好、上手快。如果是做工业现场的简单保护板比如一个12V铅酸电池的就地监控STC8A8K64D4就够了它内部有12位ADC资源还凑合。但如果要算SOC、要做均衡策略、要跑Modbus还留升级空间老老实实上STM32省得第二版重新画板子。1.2 系统架构分层采集、控制、通信解耦电池管理系统的本质是一个多输入、多输出的实时监控系统。输入是电压、电流、温度输出是充放电MOS管的通断、均衡电阻的开关、以及给上位机的数据帧。如果所有功能都堆在一个主循环里后面调试保护逻辑时会非常痛苦。我的做法是分层架构物理上分了三块采集层AFE模拟前端芯片负责电芯电压和温度的采集锰铜电阻差分放大器负责电流采样。采集层和主控之间用SPI或I2C通信主控读寄存器拿数据。控制层STM32承担所有策略判断——SOC估算、保护阈值比较、均衡策略、状态机迁移。这一层只做决策不直接碰硬件寄存器。执行层控制信号通过光耦隔离后驱动MOSFET开关通信接口做成单独的隔离模块避免功率回路对控制回路的干扰。为什么用AFE而不是直接用分压电阻ADC采电芯电压这是我在第一版里反复纠结的地方最后选了利好的路线5串以下的BMS用AFE主要是为了省麻烦。直接电阻分压采样的核心问题有两个。一是精度电芯内阻小电压变化范围只有2.5V到4.2V如果用百分之一精度的电阻分压累计误差很容易超过50mV而保护阈值精度要求通常在±30mV。二是共模电压采第3节电芯的时候分压点对地的电压已经超过8V普通ADC根本量不了必须用差分采样或者浮地技术非常麻烦。AFE芯片比如TI的BQ769205串或者BQ7693010串就解决了这个问题它内部集成了高精度ADC和电平移位电路可以直接读每一节电芯的电压还自带过压、欠压、过流比较器。我第一版用的就是BQ76920外围电路极少反而比全离散方案省了很大面积。1.3 功能指标拆解从“能保护”到“能管理”单片机和电池管理系统结合最容易做成的只是一个“高级保护板”——电压高了断开电流大了断开温度高了断开。但真正的BMS要解决三个更高层次的问题SOC估算、电池均衡、生命周期管理。我在项目立项时给自己定了这几个目标后面所有代码都是围绕它们写的指标目标值实现方式单体电压采样精度±10mVBQ76920内置ADC 软件校准电流采样精度±3%满量程锰铜电阻差分放大器零点校准SOC估算误差小于5%开路电压法安时积分修正被动均衡电流60mA/节100Ω均衡电阻 PWM控制通信接口Modbus RTU9600bps串口DMACRC16校验这套系统的应用场景我设定为18.5V/5Ah的电动工具锂电池组5节18650串联最大持续放电电流20A峰值30A。当然整个架构做出来之后只要改AFE型号和阈值参数同样适应用于48V电动自行车、小型储能电池组。2. 硬件电路详细设计与关键参数计算硬件设计是BMS项目的地基这一部分出了问题软件无论如何都补不回来。我在第一版打样时遇到过采样抖动、MOS管烧毁等问题总结下来都是参数预估不足造成的。下面把几个关键回路的设计过程写清楚。2.1 电芯电压采样AFE外围电路与滤波参数选BQ76920只是第一步外围电路设计才是真正容易踩坑的地方。我在第一版就犯过一个低级错误每个电芯的采样输入端都用了10K电阻和0.1uF电容组合的RC滤波但实际调试时发现电压读数有大约2mV的波动后来才发现是RC截止频率太低导致响应太慢。根据RC滤波公式截止频率 fc 1 / (2π × R × C)10KΩ × 0.1uF 对应的截止频率是159Hz这对于静态电压采样来说没问题但AFE内部还有滤波和转换周期整体响应时间会拖到几十毫秒级别。如果后面要支持动态电流下的SOC估算这个速度跟不上。我最后改成了1KΩ 0.22uF的配置截止频率723Hz动态响应大约2.2ms精度实测在±5mV以内。这里要提一个关键点电芯和AFE之间的走线一定要用开尔文接法即电压采样线直接接到电芯极柱而不能接到功率走线上否则大电流时功率线上的压降会被当成电芯电压误差可能达到几百毫伏。2.2 电流采样锰铜电阻和差分放大增益怎么算电流采样是BMS里最容易设计错的部分我用的是低边采样方案即在电池负极串联一个采样电阻通过差分放大器放大电阻上的压降。低边采样的优势是共模电压低、电路简单缺点是采样电阻在地回路里会让系统地线和功率地的参考电位略有偏移。核心计算如下最大放电电流30A选择锰铜电阻阻值为0.5mΩ那么采样电阻上的最大压降为Vr I × R 30A × 0.0005Ω 15mV这个15mV的电压经过差分放大器放大后再送入MCU的ADC。MCU的ADC参考电压是3.3V量程3.3V如果用INA240增益100V/V那么输出电压 15mV × 100 1.5V这个值在量程的45%位置既不靠近零点也不饱和动态范围比较理想。要注意的是INA240这类差分放大器有固定的增益档位选型时就要想好电流量程如果选增益过高大电流时输出会撞ADC参考电压如果选低了小电流分辨力不够。我建议选增益50V/V留一定裕量同时用软件做小电流校准效果比硬件硬扛要好。还有一点锰铜电阻的四端开尔文连接是必须的否则焊接点的接触电阻进入采样回路温漂会非常明显。2.3 温度采集和过温保护NTC分压电路的温度区间计算电池管理系统的热管理不可或缺特别是充放电过程中电芯发热严重时必须能提前预警。我使用了100KΩ NTC热敏电阻B值3950通过一个10KΩ固定电阻分压后接入ADC。热敏电阻的阻值与温度关系为R(T) R25 × exp(B × (1/T - 1/298.15))其中R25是25℃时的阻值100KΩB3950T是开尔文温度。计算三个关键温度点的分压比25℃时R(25℃)100KΩ分压输出 3.3 × 100/(10010) ≈ 3.0V50℃时R ≈ 36.1KΩ分压输出 ≈ 3.3 × 36.1/(36.110) ≈ 2.59V0℃时R ≈ 339KΩ分压输出 ≈ 3.3 × 339/(33910) ≈ 3.20V可以看到温度升高时NTC阻值降低分压输出电压下降。为了把保护阈值点设置准确我在软件里用查表线性插值的方式计算温度表点间距取5℃一格。这里有个经验不要把ADC原始值与温度直接线性关联NTC曲线本身就非线性尤其在低温区硬线性算很容易把0℃以下算成负温度。2.4 充放电控制回路MOS管驱动和续流保护BMS执行层最关键的部件是充放电控制MOS。我采用的是背靠背N型MOS方案——两只NMOS源极相连一只控制充电回路一只控制放电回路。之所以不用单只MOS是因为锂电充电时如果反接或者MOS体二极管导通会有从电池正极经负载反向流回电池负极的通路双MOS可以切断两条回路。MOS选型上我用了AON7410这是一颗30V、60A的N-MOS管导通内阻仅3.4mΩ。查数据手册要保证完全导通Vgs必须在10V左右。主控GPIO输出3.3V直接驱动NMOS是不够的我加了一级由三极管和电阻构成的电平转换电路把3.3V控制信号升到10V驱动栅极。另一个容易忽略的是MOS关断时感性负载产生的尖峰因为电池组到负载的线缆本身就带寄生电感。两个MOS的漏极之间我并了RC吸收电路R100ΩC1nF同时在PCB布局时把MOS管放在离电流采样电阻最远的地方避免开关噪声耦合到模拟信号。实测关断尖峰从原始方案的28V降到20V以内对MOS的应力减了不少。3. 软件状态机与核心算法实现硬件板子调通之后真正花精力的是软件。BMS软件设计的核心是状态机它保证任何异常条件下系统都能走到安全状态不会卡死在中间态。这一部分我把五个核心模块的写法、算法和调试经验都讲一遍。3.1 主状态机设计待机、充电、放电、保护、故障五态迁移状态机是BMS软件骨架所有策略判断都围绕当前状态展开。我定义了五个状态待机、充电、放电、保护、故障状态迁移的条件按优先级排列保护状态和故障状态拥有最高优先级的强制执行权。状态机伪代码如下typedef enum { STANDBY, CHARGING, DISCHARGING, PROTECTION, FAULT } bms_state_t; bms_state_t bms_state_machine(bms_state_t cur_state, bms_data_t *data) { // 故障优先出现任何硬件级异常直接进入故障态 if (data-fault_flag ! 0) { return FAULT; } // 保护判断过压、欠压、过流、过温 if (check_protection(data) PROTECTED) { return PROTECTION; } // 根据电流方向和大小切换充放电状态 switch (cur_state) { case STANDBY: if (data-current CHARGE_CURRENT_THRESHOLD) return CHARGING; if (data-current -DISCHARGE_CURRENT_THRESHOLD) return DISCHARGING; break; case CHARGING: if (data-current CHARGE_STOP_THRESHOLD) return STANDBY; break; case DISCHARGING: if (data-current -DISCHARGE_STOP_THRESHOLD) return STANDBY; break; case PROTECTION: // 保护释放后先回待机再根据电流方向重新进入充放电 if (data-current 0) return STANDBY; break; default: return FAULT; } return cur_state; }从代码里可以看到几个设计细节。第一每一个状态的退出条件都必须是稳定的信号不能是瞬时毛刺。比如充电状态切待机的阈值用正电流小于某个值而不是直接用电流等于0来判断因为采样噪声可能会让电流在0附近来回跳导致状态机抖动。第二保护状态和故障状态的区别在于保护是可恢复的故障通常是硬件级异常比如AFE通信丢失必须断电重启才能恢复。3.2 SOC估算开路电压法安时积分修正的工程实现SOCState of Charge是电池管理系统最核心也最头疼的指标。纯安时积分法会有累积误差纯开路电压法又无法在线实时测量我的做法是两者结合系统启动且电流为零时用开路电压查表修正运行过程中用安时积分同时定时用开路电压做校正。具体实现分三步第一步开路电压标定。拿到电池后先在静置超过2小时的情况下记录不同SOC下的开路电压拟合出OCV—SOC曲线。18650三元锂电池的曲线在中段SOC 30%~80%非常平缓电压变化只有100mV左右这段区间单纯靠OCV查表的误差很大所以安时积分的主体地位必须保住。第二步安时积分。SOC的计算公式SOC(t) SOC(t0) - (1/C_n) × ∫ I(t) dt其中C_n是额定容量。我在代码里用定时中断每100ms采样一次电流乘以时间间隔得到电荷增量累加到SOC总值里。注意电流方向放电为正会扣减电量充电为负会增加电量。为了提高积分精度我用的是32位变量保存累积安时数单位是mAh而不是直接用浮点防止长期运行产生漂移。第三步修正策略。每5分钟检测一次电流绝对值如果连续10秒电流都小于50mA就认为电池处于静置状态此时用OCV查表值覆盖安时积分值。这个修正必须加滞回即只有当开路电压查表结果与当前SOC差异超过2%时才做强制修正否则继续让安时积分跑避免频繁修正导致SOC跳变。实际使用中这套方法在25℃下能做到5%以内的误差但低温下0℃以下会有明显偏差因为温度影响化学反应活性放电容量会下降。现在常用的补充手段是引入温度修正系数我预留了修正表的位置后面用数据慢慢拟合。3.3 被动均衡策略怎么避免“均衡了A又失衡了B”的恶性循环锂电池组的单体电压不一致是必然的差异来自电芯制造公差、温度梯度和内阻不一致。BMS的均衡就是把这“木桶效应”控制在一定范围内。被动均衡的原理很简单将高电压电芯通过电阻放电让它的电压降到和其他电芯一致。我设定的均衡条件是相邻电芯压差超过30mV并且整体电压在3.5V-4.2V区间时启动均衡。具体实现上每个电芯并联一个100Ω均衡电阻和一颗小功率MOS。当软件检测到第N节电芯电压比其他节平均电压高30mV时打开对应的均衡MOS让电芯通过电阻释放约40mA电流实际电流 Vcell / R ≈ 4.0V / 100Ω。这里有一个工程师很容易忽略的点被动均衡会产生热量100Ω电阻在4V压差下耗散功率约为160mW如果多个电芯同时均衡板子局部温度会升高。我在PCB设计时把均衡电阻均匀分布尽量远离采样前端和AFE芯片同时在软件里加了最高均衡温度限制超过45℃就暂停所有均衡动作。还有一个经验值不要追求把压差均衡到零。电芯电压差异的一部分是由内阻差异造成的这种差异在负载电流下会实时变化强行把静置电压拉平反而会在动态负载下拉开更大差距。我的做法是均衡到15mV以内就停止让系统留有一定裕量。3.4 Modbus RTU通信实现帧接收、CRC校验和超时处理作为工业用户上位机需要实时读取BMS的状态数据。我用的是Modbus RTU从站协议通过串口以9600波特率通信数据帧格式为地址码(1字节) 功能码(1字节) 数据区(N字节) CRC16校验(2字节)。收发最关键的是帧接收和解析。我用了STM32的USART空闲中断DMA方式接收任意长度的数据帧空闲中断触发说明一帧数据接收完毕此时DMA已经把所有字节放到了缓冲区。这个方式好过逐字节接收再定时判帧因为它不占用CPU而且对帧长度的自适应能力强。CRC16校验的实现我直接贴出来用的是标准Modbus CRC-16多项式0xA001uint16_t modbus_crc16(uint8_t *buffer, uint16_t len) { uint16_t crc 0xFFFF; for (uint16_t i 0; i len; i) { crc ^ buffer[i]; for (uint8_t j 0; j 8; j) { if (crc 0x0001) { crc (crc 1) ^ 0xA001; } else { crc 1; } } } return crc; }这里要特别注意一个坑CRC的低字节在先发送、高字节在后。很多初学者容易把字节序搞反上位机收到的数据总是校验失败。我在调试时用了Modbus调试助手看到解析都是帧超时后才发现的这个问题。另外从站响应时间不能太长USB转串口适配器和上一级PLC一般都有超时限制。我的处理是主循环里每50ms扫描一次接收标志位如果收到合法请求立即在下一个循环里回复响应帧。实测从收到请求到发出响应在10ms以内工业上位机能正常识别。3.5 单片机死机后看门狗处理硬件狗还是软件狗单片机BMS系统最怕死机因为意味着监控和保护功能全部失效。我在项目里用了两个看门狗层级STM32内部的独立看门狗IWDG以及一颗外部的硬件看门狗芯片如MAX809。内部IWDG的喂狗周期设置为1秒在主循环里喂狗。如果程序跑飞或者卡死IWDG会触发系统复位。但只靠内部狗有一个风险如果程序死在一个恰好还能进入喂狗代码的死循环里内部狗就废了。所以在关键路径比如AFE读取函数、均衡控制函数里额外喂一次狗确保这些函数能被及时执行。外部硬件看门狗我接了一个GPIO程序正常时每500ms翻转一次电平硬件狗芯片监视这个电平翻转如果超过1.5s没有翻转它会直接拉低MCU复位引脚。这个比内部狗更可靠因为即使MCU完全锁死外部狗也能在毫秒级时间内复位系统。复位后要做的一件重要事情是保存复位原因和故障状态到EEPROM。我在系统启动时读取复位标志如果是看门狗复位则把上次的SOC、电芯电压、电流等关键数据读回来避免因为复位丢掉了用户关心的历史数据。4. 调试、标定和常见问题排查硬件和软件都做完了真正花时间的反而是调试和标定。我在这个项目里踩过不少坑整理成几个高频问题按照“现象—原因—解决办法”的格式记录在这里希望能帮后面做BMS的朋友少走弯路。4.1 电压采样为什么偏了60mV校准流程和零点偏差处理第一版样板回来后我做的第一件事是接上5节4.0V的测试电芯对照万用表读数检查AFE的采样精度。结果是第1节电压读数4.012V万用表4.006V差了6mV但第3节差了12mV。这个误差虽然没有超出AFE芯片的数据手册规范但对于BMS保护来说误差必须严格校准。问题根源主要是三个一是AFE芯片自身的offset二是采样电阻的误差三是PCB走线寄生参数导致的增益偏差。解决办法是软件校准。我在每个电芯通道上做了两点校准——使用精密电压源输出2.5V和4.0V两个标准值分别记录ADC读数然后算出每个通道的增益误差和零点误差存到FLASH里。每次启动时加载校准系数对采样结果做线性补偿。校准后实测精度从±12mV提升到±4mV这个值对SOC估算和保护阈值的精度都有明显帮助。4.2 大电流放电时电压读数跳变采样走线和滤波参数的关系第二次调试遇到一个诡异问题静止时电压读数很稳但一接上20A负载所有电芯电压读数都开始跳幅度有20mV左右。排查后发现采样线虽然开尔文接到了电芯极柱但采样线的公用段负极采样线和功率线有一段平行走线在20A电流下平行段的互感在采样回路里感应出了mV级的噪声。解决方法是双管齐下PCB布局上把采样信号线做成差分对两条线紧贴走同时采样回路增加共模电感软件上增加数字滤波对连续8次采样取中位值平均。这样处理后大电流下的电压波动降到了5mV以内。还有一个细节采样回调的周期要和主控的电流采样同步否则电压电流会出现相位差影响功率计算和SOC判断。4.3 Modbus通信偶尔超时CRC字节序和DMA半满中断陷阱Modbus通信的问题排查了很久一度以为是上位机的问题。现象是通信正常时一切OK但偶尔会有某一条读请求没有响应上位机报超时。排查过程第一步用逻辑分析仪抓串口波形发现MCU确实收到了完整的请求帧第二步检查中断优先级发现空闲中断和接收中断的优先级配平了第三步才是真正的问题——DMA的半传输中断和传输完成中断共用了回调函数我对半传输中断没有做处理导致在半传输点附近的请求帧被拆成两段DMA接收缓冲区被覆盖。解决办法是在DMA配置中关闭半传输中断只用传输完成中断和空闲中断来标记一帧数据接收完整。改完以后连续跑了一晚上19200条请求帧全部正常响应。这个问题的经验总结是用DMA收发串口数据不要偷懒把中断回调混在一起写一定要分清“接收一半”和“接收完成”的语义。4.4 待机功耗偏高跑掉整个电池的最后一根稻草最后说一个容易被忽视的问题——待机功耗。BMS系统在电池不工作时也要持续供电以保证保护功能和通信功能在线。如果待机功耗大了就像汽车开着大灯停一夜电池直接亏到不能启动。我实测过第一版样板的待机电流整整12mA。算一下如果电池组容量是5Ah12mA待机电流意味着416小时大约17天就能把电池耗空。这个指标完全不能接受。排查线像素是从AFE和主控的电源链路上找。BQ76920的规格书标注的静态电流约1uA但它的内部稳压器如果持续供电整体功耗会显著上升。另外STM32虽然支持待机模式但我的软件一开始只是进入睡眠而没有关闭外设时钟外设还在耗电。优化措施有三个MCU进入STOP模式同时关闭所有未使用的外设时钟仅保留RTC和外部唤醒中断将AFE的供电改为MOS管控制的动态电源STM32在STOP前先通过GPIO关掉AFE电源均衡电阻和LED指示灯的供电全部与待机状态隔离。优化后待机电流从12mA降到了0.35mA5Ah电池组的待机时间从17天提升到了约595天这才达到了实用要求。最后再分享一个调试习惯在线测功耗时不要只量系统总电流最好在每一路电源轨上串联采样电阻单独测。我第一版就是只看总电流结果从12mA压到3mA之后就再压不下去了后来逐个模块断开测才发现一个LED指示灯的限流电阻没关供电硬吃了7mA。这种问题靠猜是猜不出来的一个万用表加几个飞线就能定位关键是思路要清晰。