做锂电池管理系统也有一阵子了,从最早的电压监测板,到后来带上温度采集和充放电控制,一路踩坑踩过来,最后才把目光聚焦到SOC估算和均衡这两个真正决定电池组寿命和可用容量的核心问题上。这次要聊的项目,是一套基于STM32F4的BMS电池管理系统,重点围绕SOC估算和均衡控制展开。本文不打算讲那些照本宣科的理论,更多是从实际调试经验出发,把选型逻辑、采样电路、滤波算法、均衡策略这些环节里我踩过的坑和验证过的方案,原原本本摆出来,希望能给正在折腾电池管理的朋友一些参考。
先说明这套系统适合谁。如果你手里有电芯、有BMS评估板,或者正准备给自己的储能项目、电动车项目做个靠谱的电池管理方案,那这篇文章比较对路。如果你是刚接触嵌入式、还没搞清楚ADC和DMA怎么配合,那文中的代码思路和电路设计部分也能给你一些启发。总之,这是一篇以工程落地为导向的经验总结,不是什么教科书式的理论推演。
1. 整体设计与思路拆解
1.1 为什么选STM32F4而不是其他芯片
先说芯片选型。BMS对主控的要求其实不低,既要有多通道ADC采集能力,又要能跑复杂算法,还要有充足的GPIO去控制均衡开关、继电器和通信接口。我最早用某款Cortex-M0内核的芯片做过一轮原型,基础功能能跑,但一旦把卡尔曼滤波和均衡策略同时跑起来,主频和浮点运算的短板就暴露了,ADC采样率稍微提上来一点,CPU占用就飙到让人心里发慌的程度。
换成STM32F4系列之后,情况明显改观。以STM32F407为例,168MHz主频、带FPU硬件浮点单元,这对我跑SOC算法里的矩阵运算特别关键。很多人觉得SOC估算不就是查个表、做个积分吗,没必要上FPU。真做过就知道,开路电压法加安时积分法再加卡尔曼滤波融合,中间涉及的矩阵乘法和协方差更新,用纯整数运算写出来又臭又长,用浮点运算快得不是一点半点。F407内置的FPU直接让这部分代码量砍掉了一半不止。
另一个决定性因素是ADC。BMS采集电芯电压是最核心的功能,STM32F4内置3个12位ADC,最多支持24个外部通道。配上DMA,可以实现多通道连续采样而不过度占用CPU。我实际用的是ADC1的8个通道加DMA循环模式,每50ms完成一轮全部电芯电压采样,CPU负载基本可以忽略。这种硬件资源上的冗余,让后续算法迭代有了空间。
1.2 BMS系统整体架构
这套BMS从功能上划分成五层:采样层、计算层、执行层、通信层、存储层。采样层负责电芯电压、总压、电流和温度采集;计算层跑SOC估算、SOH评估和均衡决策;执行层控制均衡MOS、充放电MOS和预充电路;通信层跟充电机、上位机或者整车控制器打交道;存储层则把关键参数存到EEPROM或者Flash里,防止断电丢数据。
采样层是我花最多时间打磨的部分,因为后面所有的算法都建立在采样数据之上。电压采集用的是分压电阻加运放跟随的方案,再进STM32的ADC。电流采集用的是分流器加差分放大器,这里有个细节:分流器的温漂必须重视,普通锰铜分流器虽然便宜,但温度系数较大,我最后换成了低温漂合金分流器,采样电阻20毫欧,满载100A时压降2V,配合差分放大器的60倍增益,给到ADC的量程刚好覆盖。这样设计的原因很简单,电流采得不准,安时积分法算出来的SOC就是空中楼阁,越积越偏。
执行层最核心的是均衡电路。我搞的是被动均衡方案,就是每个电芯并联一个均衡电阻和一颗MOS管,由STM32控制MOS的导通和关断,把高SOC电芯的额外电量通过电阻放掉。这个方案虽然效率不高,胜在架构简单、可靠,适合小规模电池组原型验证。后面会详细讲均衡控制策略的具体实现。
1.3 SOC估算方法选型:不迷信单一算法
SOC估算是个老大难问题。业内常用的方法就那么几种:开路电压法、安时积分法、卡尔曼滤波法、神经网络法。神经网络在我这个项目里直接排除,原因不是它效果不好,而是它需要大量的训练数据做支撑,对原型验证阶段来说,性价比太低。开路电压法最大的问题是需要电池静置很久,电压才能稳定到可精确映射SOC的程度,动态工况下根本没法用。安时积分法的问题是初始SOC不准、电流采样有误差时,误差会不断累积。
我最终采用的方法是安时积分法为主、开路电压法做校准、卡尔曼滤波做融合的三层架构。这个思路是我在多次实验后总结出来的:先通过开路电压法在系统首次上电或者长时静置后确定初始SOC,运行过程中用安时积分法做实时递推,卡尔曼滤波器在这里的职责不是替代安时积分,而是把电压测量值和安时积分值做一个最优融合,对电流传感器的漂移做动态修正。这个组合兼顾了实时性和准确性,实测下来动态工况的SOC误差可以控制在5%以内,比纯安时积分法的10%左右要好看得多。
2. 核心细节解析与实操要点
2.1 电压采样电路:抗混叠滤波与阻抗匹配
电芯电压采样看起来简单,实际上最容易出问题。系统里电芯是串联的,每一节电芯的负极电位都不同,采样电路必须做电平转换或者用差分方式测量。我采用的是差分采样方式,每一节电芯独立接入一个差分放大器,把双端信号转成单端信号后再进ADC。这种方法的好处是抗共模干扰能力强,不会因为串联电芯的共模电压过高而导致测量失真。
差分放大前必须加RC低通滤波。这个不是可选项,而是必选项。电芯电压本身比较稳定,但BMS的工作环境里到处都是开关电源和MOS管动作产生的噪声,如果不做滤波,ADC采到的数据会抖得厉害。RC参数我取的是R=10kΩ,C=100nF,对应截止频率约159Hz。这个截止频率足够滤掉开关噪声,同时不会对电压信号的建立时间造成明显影响。还有一个容易忽略的点:RC滤波器和运放的输入阻抗是并联关系,如果R取值太大,会引入额外的分压误差,导致采样值系统性偏低。我实测过,R取100kΩ时,采样值比真实值低了将近60mV,这对SOC估算来说已经是不可忽略的误差了。
注意:分压电阻的精度等级建议选0.5%甚至0.1%,普通5%精度的电阻用在BMS采样上,每一节电芯的误差都不一样,后面做均衡判断时会出现一堆误动作。
2.2 电流采样:分流器选型与零点漂移问题
电流采样我试过两种方案。第一种是霍尔电流传感器,优点是与主回路隔离,接线简单,缺点也很明显——温漂诱人,而且小电流段线性度不好。对BMS来说,小电流段的精度恰恰很重要,因为待机功耗的估算、自放电的补偿都在这里面,这部分不准,长期静置后的SOC会悄悄跑偏。第二种是分流器加差分放大器,精度可以做得很高,线性度好,我最终选的就是这种方案。
选分流器有个关键参数是额定压降。市面上常见的分流器额定压降是50mV、75mV和100mV几种,我选的是20毫欧/100A规格,意味着满载时压降2V,配合增益60倍的差分放大器,ADC输入范围能拉到接近满量程。这个设计的用意是让ADC的分辨率尽量用满,否则小电流采样时量化噪声太突出。
零点漂移是电流采样里的经典坑。差分放大器本身有输入失调电压,PCB布局不合理还会引入热电偶效应,导致空载时ADC读数不为零。我遇到过一次很头疼的情况:空载电流读数稳定在0.3A左右,当时觉得可能是干扰,排查了好久才发现是差分放大器失调电压放大60倍之后的必然结果。这个问题通过软件校准解决,在每次系统上电且确认继电器断开时,采集一组数据作为零点基准,存到Flash里,之后的采样值都减去这个零点。这个方法简单有效,强烈建议做进去。
2.3 温度采集与保护联动
温度采集虽然不直接影响SOC计算,但对均衡控制影响很大。被动均衡的本质是让电芯通过电阻放电,会产生明显热量,如果均衡电阻选得太小,均衡电流太大,MOS管和电阻的温度会快速飙升。我用的均衡电阻是47Ω,均衡电流约70mA(按单节电芯3.3V计算),单个电阻功耗接近0.25W,长时间工作后温度能到60℃。这个发热量在可接受范围内,但必须有温度传感器做监控,否则电池包里热量堆积起来,安全隐患不小。
重要提示:均衡策略必须和温度保护联动。我在软件里设了一条规则:如果电池包内部温度超过55℃,立即暂停所有均衡动作,等温度降下来再恢复。这不是可有可无的保险,是保命的底线。
3. 实操过程与核心环节实现
3.1 硬件搭建与关键引脚分配
这套BMS裸板的电路不算复杂,但布局布线有几个讲究。先说板层结构,我用的是一块四层板,顶层和底层走信号,中间两层一个铺地一个铺电源。模拟采样电路集中放在板子的一个角落,数字电路和MOS驱动放在另一侧,中间用地线隔开。这样做的原因是模拟信号微弱,容易被数字信号的谐波干扰,物理隔离是成本最低的抗干扰手段。
关键引脚分配上,我的方案是这样的:
- ADC1_IN0~ADC1_IN7:8节电芯电压采样输入
- ADC2_IN0~ADC2_IN1:总压采样和电流采样
- 若干GPIO:均衡MOS控制(8路)、充电MOS控制、放电MOS控制
- USART2:与上位机通信
- I2C1:EEPROM存储
这里有个实战经验:MOS管的驱动不能直接用GPIO灌电流,必须经过栅极驱动电路。我用的是低压侧N沟道MOS加专用栅极驱动器,栅极串联一个10Ω电阻,再并联一个100kΩ下拉电阻。下拉电阻的作用是防止MOS在上电瞬间因为栅极浮空而误开通。我第一版设计没加这个下拉电阻,结果每次上电瞬间,均衡MOS都会闪通一下,导致电芯电压被短暂短路到均衡电阻上,虽然不致命,但白白的能量损耗和热冲击看着就心疼。
ADC和DMA的配置代码简单列一下:
void ADC_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; DMA_InitTypeDef DMA_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); GPIO_StructInit(&GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; GPIO_Init(GPIOA, &GPIO_InitStructure); DMA_DeInit(DMA2_Stream0); DMA_InitStructure.DMA_Channel = DMA_Channel_0; DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)adc_buf; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory; DMA_InitStructure.DMA_BufferSize = ADC_CH_NUM; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; DMA_Init(DMA2_Stream0, &DMA_InitStructure); ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b; ADC_InitStructure.ADC_ScanConvMode = ENABLE; ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = ADC_CH_NUM; ADC_Init(ADC1, &ADC_InitStructure); ADC_Cmd(ADC1, ENABLE); DMA_Cmd(DMA2_Stream0, ENABLE); ADC_DMACmd(ADC1, ENABLE); ADC_SoftwareStartConv(ADC1); }这段配置的核心是ADC1的扫描模式加连续转换加DMA循环模式。采样数据直接由DMA搬运到adc_buf数组,CPU只在需要时读一下数组,不用每转完一个通道就进一次中断。这样可以把采样周期压得很低,同时腾出大量CPU时间去跑SOC算法和均衡决策。
3.2 SOC估算的软件实现细节
SOC估算的软件实现是这个项目的重头戏。我分了三层来写:开路电压校准模块、安时积分模块、卡尔曼融合模块。
开路电压校准模块只在特定条件下触发。什么条件?第一,电池组静置时间超过2小时;第二,系统刚上电处于待机状态;第三,没有充放电电流。满足这三个条件后,软件采样一次电芯电压,查询OCV-SOC映射表,得到一个初始SOC值。这个映射表是电芯厂商提供的,我拿到的是常见磷酸铁锂的表格。这里有个很多人会踩的坑:OCV-SOC曲线在SOC中间区域的斜率非常平缓,电压变化一点点,SOC变化很大,所以这个表需要做插值,我用的是线性插值,虽然精度不如高阶插值,但胜在计算简单、不会过冲。
安时积分模块的计算非常简单,就是SOC(t) = SOC(t-1) + (I * Δt) / 总容量。但是有几个细节必须处理好。第一个是电流方向的定义,放电为负,充电为正,这个符号搞反了,整个系统都会跟着出错。第二个是效率系数,充电过程不是100%的效率,所以充入的电流要乘以一个库仑效率系数,我取0.98,不同电芯有差异,最好通过实验标定。第三个是温度的修正,低温下可用容量会缩水,这个修正我在项目中是用一个温度系数表实现的,温度每降10℃,容量系数降约3%。
卡尔曼融合模块,我用的是扩展卡尔曼滤波器。之所以不用线性卡尔曼,是因为电池的电压与SOC之间是非线性关系,这个非线性体现在OCV-SOC曲线上。扩展卡尔曼的思想是把这个非线性函数在每个时间点做线性化,然后套用卡尔曼滤波框架。状态变量有两个:SOC和极化电压。观测方程把这两个状态映射到端电压上。算法每50ms迭代一次,上一次的SOC估计值作为先验值,用当前电压测量值做校正。
卡尔曼滤波里有两个参数需要仔细调:过程噪声协方差Q和测量噪声协方差R。Q代表的是你对自己系统模型的信任程度,R代表的是你对传感器测量值的信任程度。我刚开始调的时候,把Q设得很大,结果SOC被电压测量值牵鼻子走,波动非常剧烈;后来把Q调小,又发现SOC对电流突变反应迟钝。最后是反复跑了一段模拟工况数据,才找到一个比较合适的折中值。这里给个参考:Q取0.01,R取0.25,但不同电池和不同传感方案,这两个值大概率要重新标定。
3.3 均衡控制的策略设计与实现
均衡控制的核心逻辑可以概括成“找差异、放多数、盯温度”。被动均衡就是把电压高的电芯放掉一点电,让整组电芯朝着最低的那节看齐。但这里有个细节:到底按什么基准来判断电压高还是低?我用的不是绝对电压阈值,而是整个电池组的平均电压加一个滞回区间。
具体做法是,每50ms采样一轮电压,算出8节电芯电压的平均值V_avg,然后算每节电芯的电压差值delta = V_i - V_avg。当delta > 50mV时,如果温度条件允许、电池处于静置状态,就打开这节电芯对应的均衡MOS,让它通过47Ω电阻放电。当delta降到30mV以内时,关闭均衡MOS。这里的50mV开启、30mV关断是一个滞回控制,目的很明显:防止电芯电压在阈值附近来回抖动导致均衡MOS频繁开关,频繁开关对MOS寿命不利,也会在PCB上产生热冲击。
再考虑一种情况:如果电芯本身容量已经有差异,仅仅做电压均衡是不够的。我在这套系统里算是把“电压均衡”和“SOC均衡”做了一个折中。均衡时机选在充满电之后效果最好,因为充满电后各节电芯电压差异最明显,欠充不均衡的状态下电压差异很小,均衡动作几乎不会触发,等于白装。这个规律我是用一次失败的联调换来的教训:第一次调试时,我在电池组半电状态下跑均衡,结果显示均衡电路动作很少,一开始以为是硬件故障,查了半天最后才想明白,半电状态下OCV曲线的斜率太平坦,电压差根本暴露不出来。
还有一点值得多说一句,就是均衡开启的时机要避开充电过程。充电时电芯电压本来就因为极化反应处于偏高状态,如果这时候做均衡,会把极化电压误当成SOC差异来处理,导致过度放电。我的策略是在充电结束后,静置5分钟再启动均衡逻辑,让电压先回落到稳定值再说。
3.4 存储与通信:让数据说话
BMS如果没有通信功能,就是一个瞎子。我在这套系统上做了两路通信:一路是USART接上位机,另一路是I2C接EEPROM做参数存储。上位机界面我自己写了一个简易的数据面板,实时显示8节电芯的电压、电流、温度、SOC和均衡状态。没有这一步,光靠串口打印原始数据,调试效率会低很多倍。
EEPROM里存的东西不多,但每一件都很关键:零点校准值、SOC初始值、满电容量(会受到老化影响不断修正)、上次关机时间、故障记录。SOC初始值的持久化很重要,因为安时积分法需要一个初始条件,每次开机后如果都靠开路电压重新校准,那动态工况下开机后的SOC误差会比较大。我选择的是每次关机前把当时的SOC值存入EEPROM,开机后先读出来作为初值,同时开机后如果满足静置条件,再通过开路电压法做一次校正。这套双保险机制,让我在大多数使用场景下开机SOC误差能控制在3%以内。
4. 常见问题与排查技巧实录
4.1 问题一:ADC采样值波动太大,波动幅度超过50mV
这是我第一版板子调试时遇到的头号问题。排查过程很典型,分享一下思路。先用万用表直接量电芯电压,发现电压本身非常稳定,说明问题出在采样链路上。然后看ADC读数,发现噪声幅度呈周期性变化,跟BMS板子上某个开关电源的频率对得上。这时候基本可以锁定是开关电源干扰耦合进了模拟采样回路。
解决措施有两个方面。硬件上,我给每个采样通道的RC滤波电容加大到220nF,进一步压高频噪声;同时在ADC的参考电压引脚VREF+和VREF-之间加了一颗10μF钽电容和100nF陶瓷电容做去耦。软件上,我在DMA搬运过来的数据基础上做了一级均值滤波,具体是连续采8次,去掉最大值和最小值,剩下6次取平均。这套组合拳打下来,ADC的采样波动从50mV压到了5mV以内,效果非常明显。
注意:均值滤波不能乱用。在电压信号本身变化很快的场景(比如脉冲充放电),过度滤波会引入明显延迟,导致SOC估算跟不上真实状态。BMS电压采样频率是50ms一次,充放电工况下电压本身又有趋势性变化,所以我只在“电池静置”状态下启用8次采样加深滤波,动态状态下只用5次采样做简单平均,兼顾实时性和平滑度。
4.2 问题二:SOC越用越飘,静置一段后又跳变
这个问题的根源,是SOC算法的修正机制没有理顺。具体表现是:系统跑了一段时间后,SOC显示值和真实电量越来越离谱,但一旦电池静置下来,校准模块跑一次开路电压法,SOC又跳到另一个完全不同的值上。跳跃的幅度甚至可以到15%这么大,这在实际使用中会造成里程焦虑,也会让人对系统彻底失去信心。
当时我把所有原因都列了一遍,最后定位到两个核心问题。第一个是安时积分模块里的库仑效率系数没有标定准。我用默认0.98,但实际电池在低温环境下的充电效率明显低于这个值,导致充进去的电在被积分时记多了。第二个问题是开路电压校准的触发条件不够严格,某些情况下电池并没有真正静置到位,极化电压还没消退,我这边就开始校准了,自然会把某些带偏的电压值当成OCV来查表。
解决方式是这样的:库仑效率系数改成随温度动态调整,具体数值是通过一次完整的充放电循环实验标定的。开路电压校准则额外加了一个条件:静置状态下,连续10次电压采样值的最大偏差不超过2mV,才允许进入校准流程。这个条件看起来简单,实际上筛选掉了绝大部分“假静置”的情况,跳变问题基本消失。
4.3 问题三:均衡开启一两分钟,MOS管烫得不敢摸
前文提到过均衡电阻功耗的问题。47Ω电阻在电芯3.3V电压下,功耗约0.23W,单个电阻长期跑这个功耗,温度会显著上升。但真正让我意外的不是电阻热,而是MOS管本身发热严重。后来查资料和实测发现,均衡电路用的是低压侧开关控制,MOS导通阻抗RDS(on)本身不大,但问题是均衡电阻的电流路径上,MOS的导热条件不好,热量全部通过引脚堆在管体上,没有任何散热的可能。
解决思路分两步。硬件上,我把均衡MOS改成SOP-8封装的型号,优点是这个封装的背焊盘面积大,焊接后经过过孔连到PCB的覆铜区,散热效果好很多。软件上,我做了均衡轮询机制:系统同时只允许最多2节电芯进行均衡,每打开一个均衡通道持续30秒,然后切换到下一组需要均衡的通道。这样虽然拉长了累计均衡时间,但避免了热量过度集中在一个点上。均衡本来就不是一个赶工期的活,让电池一点一点地自我修正,反而更安全。
4.4 问题四:界面显示的实际电量百分比和真实验证值对不上
这是所有问题里最磨人的一个,因为它不是单一原因导致的。后来我把问题拆成了两部分。第一部分是满电容量标定问题。BMS里存的满电容量是出厂默认值,但电池经过上百次循环后,实际可用容量已经衰减,安时积分算出来的SOC是拿实际电流对出厂容量做除法,这个分母偏大,算出来的SOC自然偏小。第二部分是放电末期的电压和SOC映射关系非线性,在低SOC区间,一小点电压波动就对应很大的SOC跳变。
最终的解决方案是引入动态容量修正。每次充满电后,记录本次充电过程中实际充入的电量,与上次记录的容量做一个加权平均,权重我取0.7对0.3,新一次充入电量占大头,这样容量会随着老化缓慢向下修正。实测跑了大几十个循环后,容量从标称值往下调整了约8%,SOC显示精度明显改善。这一步让我深刻体会到,BMS的参数不是设好就不管了,它是需要“活”的,要与电池的老化状态保持联动的。
5. 写在最后的体会与建议
这套BMS项目做到这个阶段,回头看最深的感受就是:硬件是骨架,算法才是灵魂。STM32F4的优秀算力让我有机会在单片机上跑卡尔曼滤波和复杂的均衡策略,但算法效果好不好,最终还是取决于底层采样数据够不够干净、够不够真实。我在这个项目里把一个原则贯彻到了底:每个环节都要为算法的准确性服务,而不是等算法出来以后再反过来补洞。ADC采样不稳,就去查布局和滤波;电流零点漂移,就做软件校准;容量衰减,就做动态修正。一步一个脚印地补,系统的表现就会逐渐好起来。
最后再分享一个实用小技巧。调试时给上位机加一个“数据记录回放”功能,把所有采样点的时间戳和SOC轨迹记录下来,然后在模拟工况下反复回放,观察算法对特定工况的响应。这个方法帮我发现了好几个只有在连续复杂工况下才会暴露的隐性bug。好记性不如烂笔头,调试BMS也一样,数据到手后再去分析,往往比现场瞎猜高效得多。