☰
单片机BMS实战:STM32电池管理系统设计与SOC均衡算法解析
2026/10/5 7:25:48 网站建设 项目流程

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的BQ76920(5串)或者BQ76930(10串)就解决了这个问题,它内部集成了高精度ADC和电平移位电路,可以直接读每一节电芯的电压,还自带过压、欠压、过流比较器。我第一版用的就是BQ76920,外围电路极少,反而比全离散方案省了很大面积。

1.3 功能指标拆解:从“能保护”到“能管理”

单片机和电池管理系统结合,最容易做成的只是一个“高级保护板”——电压高了断开,电流大了断开,温度高了断开。但真正的BMS要解决三个更高层次的问题:SOC估算、电池均衡、生命周期管理。

我在项目立项时给自己定了这几个目标,后面所有代码都是围绕它们写的:

指标目标值实现方式
单体电压采样精度±10mVBQ76920内置ADC + 软件校准
电流采样精度±3%(满量程)锰铜电阻+差分放大器+零点校准
SOC估算误差小于5%开路电压法+安时积分修正
被动均衡电流60mA/节100Ω均衡电阻 + PWM控制
通信接口Modbus RTU(9600bps)串口+DMA,CRC16校验

这套系统的应用场景我设定为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Ω,B=3950,T是开尔文温度。

计算三个关键温度点的分压比:

  • 25℃时,R(25℃)=100KΩ,分压输出 = 3.3 × 100/(100+10) ≈ 3.0V
  • 50℃时,R ≈ 36.1KΩ,分压输出 ≈ 3.3 × 36.1/(36.1+10) ≈ 2.59V
  • 0℃时,R ≈ 339KΩ,分压输出 ≈ 3.3 × 339/(339+10) ≈ 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吸收电路(R=100Ω,C=1nF),同时在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估算:开路电压法+安时积分+修正的工程实现

SOC(State 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(多项式0xA001):

uint16_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。算一下:如果电池组容量是5Ah,12mA待机电流意味着416小时,大约17天就能把电池耗空。这个指标完全不能接受。

排查线像素是从AFE和主控的电源链路上找。BQ76920的规格书标注的静态电流约1uA,但它的内部稳压器如果持续供电,整体功耗会显著上升。另外STM32虽然支持待机模式,但我的软件一开始只是进入睡眠而没有关闭外设时钟,外设还在耗电。

优化措施有三个:

  • MCU进入STOP模式,同时关闭所有未使用的外设时钟,仅保留RTC和外部唤醒中断;
  • 将AFE的供电改为MOS管控制的动态电源,STM32在STOP前先通过GPIO关掉AFE电源;
  • 均衡电阻和LED指示灯的供电全部与待机状态隔离。

优化后待机电流从12mA降到了0.35mA,5Ah电池组的待机时间从17天提升到了约595天,这才达到了实用要求。

最后再分享一个调试习惯:在线测功耗时,不要只量系统总电流,最好在每一路电源轨上串联采样电阻单独测。我第一版就是只看总电流,结果从12mA压到3mA之后就再压不下去了,后来逐个模块断开测,才发现一个LED指示灯的限流电阻没关供电,硬吃了7mA。这种问题靠猜是猜不出来的,一个万用表加几个飞线就能定位,关键是思路要清晰。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询