☰
STM32F103+AFE6812真实BMS源码解析:SOC估算与硬件闭环实现
2026/10/21 17:45:10 网站建设 项目流程

简介:本资源是一套国内自主研发的电池管理系统(BMS)嵌入式源码,面向电动汽车与储能系统领域的嵌入式工程师、BMS算法开发者及高校相关专业高年级学生,聚焦于多节点CAN通信架构下的实时电池监控与保护功能实现。包内共229个文件,以71个C源文件和81个头文件(.c/.h)为主体,涵盖XC2287M主机与MC9S08DZ60从机的底层驱动(ADC采样、温度传感、CAN 500kbps通信)、核心算法模块(SOC估算、均衡控制、过压/过温故障检测)及分层软件架构(硬件抽象层、应用逻辑层),辅以cmd链接脚本、map映射文件及launch调试配置,便于工程复现与深度剖析。压缩包仅951KB,结构紧凑、注释完整,已吸引8033人学习下载。读者可直接基于该代码理解BMS软硬协同设计范式,快速掌握国产化平台下CAN主从通信建模、安全阈值响应机制及嵌入式低功耗管理实践。

1. 这不是通用嵌入式模板:一个国内BMS源码包,能直接跑在STM32F103+AFE6812硬件上的真实闭环控制逻辑

你手头有一块带AFE6812采集芯片的BMS主控板,MCU是STM32F103C8T6,电池包是12串三元锂电,采样精度要求±2mV,SOC估算误差需控制在5%以内——这时候翻遍GitHub和CSDN,90%的所谓“BMS开源项目”要么只有ADC读取裸数据、要么SOC算法写死成线性查表、要么连均衡驱动都没配IO口定义。而这份国内团队实车验证过的BMS源码,恰恰卡在“能上车”和“可调试”之间:它不依赖任何商业SDK,所有驱动层(SPI通信AFE6812、CAN收发、NTC查表、被动均衡PWM占空比闭环)全部用标准外设库手写;核心状态机按GB/T 34657.1-2017拆解为“预充→充电→放电→故障→休眠”五态;最关键的是,它的SOC估算模块不是调个卡尔曼滤波API就完事,而是把开路电压(OCV)查表、安时积分补偿、温度衰减系数、电流倍率修正这四层耦合逻辑全摊开在soc_calc.c里,每个系数都有实测标定注释。适合正在做毕业设计硬件联调的本科生、接手产线BMS固件升级的FAE工程师,以及想搞懂“为什么我的卡尔曼滤波在满电区总漂移”的算法新手——它不教你理论,但每行代码都在回答“现场怎么活下来”。


2. 源码结构解析:从启动文件到故障树,六层目录如何对应真实BMS硬件链路

这份源码不是IDE工程压缩包,而是按功能域严格分层的嵌入式软件架构。我拿到后第一件事就是用VS Code打开根目录,确认六个核心文件夹的职责边界。这种分层不是为了好看,而是直接映射到BMS硬件信号流:AFE采集→MCU处理→CAN上报→执行器响应→故障诊断→低功耗管理。

2.1Drivers/:绕过HAL库的手动寄存器操作,为什么必须重写SPI时序

AFE6812对SPI时序极其敏感:SCLK上升沿采样、CS下降沿后需等待120ns才能发首字节、连续读取时CS不能释放。官方HAL库的HAL_SPI_TransmitReceive()默认插入的延时不可控,实测会导致AFE寄存器读取错位(比如本该读0x0A状态寄存器,却返回0x00)。源码在Drivers/afe6812/afe6812_spi.c中完全弃用HAL,改用GPIO模拟SPI:

// Drivers/afe6812/afe6812_spi.c 关键片段 void AFE6812_SPI_WriteByte(uint8_t byte) { for(uint8_t i = 0; i < 8; i++) { if(byte & 0x80) { HAL_GPIO_WritePin(SPI_MOSI_GPIO_Port, SPI_MOSI_Pin, GPIO_PIN_SET); } else { HAL_GPIO_WritePin(SPI_MOSI_GPIO_Port, SPI_MOSI_Pin, GPIO_PIN_RESET); } // 精确控制SCLK高电平时间:200ns(对应72MHz系统时钟下3个NOP) __NOP(); __NOP(); __NOP(); HAL_GPIO_WritePin(SPI_SCLK_GPIO_Port, SPI_SCLK_Pin, GPIO_PIN_SET); __NOP(); __NOP(); __NOP(); // SCLK低电平时间同样3个NOP HAL_GPIO_WritePin(SPI_SCLK_GPIO_Port, SPI_SCLK_Pin, GPIO_PIN_RESET); byte <<= 1; } }

提示:这段代码里的__NOP()数量不是拍脑袋定的。源码包附带的timing_measure.md文档明确记录:用示波器抓取SCLK引脚,调整NOP数量使高/低电平各为200ns,此时AFE6812的BUSY引脚才会稳定拉低。若你的板子晶振是8MHz而非72MHz,必须按比例重算NOP次数——这是第一个必须动手改的参数。

2.2Core/StateMachines/:五态机如何用查表法规避if-else地狱

BMS最怕状态跳变引发的逻辑冲突(比如充电中突然进过压保护,又同时触发绝缘检测)。源码用二维状态转移表替代传统switch-case:

// Core/StateMachines/bms_state_machine.c typedef enum { STATE_PRECHARGE = 0, STATE_CHARGING, STATE_DISCHARGING, STATE_FAULT, STATE_SLEEP } bms_state_t; // 状态转移表:[当前状态][事件] -> 新状态 const bms_state_t state_transition_table[5][8] = { // STATE_PRECHARGE 行:预充状态下可能触发的事件 {STATE_PRECHARGE, STATE_CHARGING, STATE_DISCHARGING, STATE_FAULT, STATE_SLEEP, ...}, // STATE_CHARGING 行:充电状态下,EVENT_OVERVOLTAGE发生时跳转到FAULT {STATE_FAULT, STATE_CHARGING, STATE_CHARGING, STATE_FAULT, STATE_SLEEP, ...}, // 后续行省略,共5×8=40个元素 };

每个事件(EVENT_OVERVOLTAGE、EVENT_SOC_LOW等)由中断服务程序置位标志位,主循环调用BMS_StateMachine_Run()查表更新状态。这样做的好处是:新增故障类型只需在表里加一列,不用动状态机主逻辑;且编译后内存布局固定,避免动态分配导致的堆栈溢出——某高校学生曾因在FAULT状态里加了printf导致RAM爆掉,就是没理解这个设计意图。

2.3Algorithms/SOC/:四层耦合计算,为什么OCV查表要分温度区间

Algorithms/SOC/soc_calc.c是整套源码的算法心脏。它不采用单一卡尔曼滤波,而是分层校正:

层级计算方式输入参数作用
L1: OCV查表二维数组索引当前电压均值、电池温度提供SOC基准值,查表文件ocv_table_25C.h含25℃数据,ocv_table_0C.h含0℃数据
L2: 安时积分SOC += (I * dt) / (Q_total * 3600)实时电流、采样间隔、标称容量动态跟踪电量变化,但存在累积误差
L3: 温度补偿SOC *= temp_coeff[temperature_index]查表得温度系数(-20℃~60℃共16档)低温下锂离子活性下降,相同电压对应更高SOC
L4: 电流倍率修正SOC += k * (I / I_rated)^2实际电流与额定电流比值大电流放电时电压平台下移,需回填SOC

关键点在于L1的OCV查表:源码提供ocv_table_25C.h,但实际运行时根据BMS_GetTemperature()返回值选择不同头文件。如果你的NTC电路用的是10KΩ热敏电阻,而源码默认按100KΩ标定,ntc_calib.c里的R25宏定义必须从100000改为10000,否则温度读数偏差达15℃——这是第二个必改参数。


3. 编译与烧录:Keil MDK-ARM v5.37配置要点与常见报错直解

源码包未提供IAR或GCC工程,仅适配Keil MDK-ARM。我用v5.37实测通过,但默认配置有三处硬伤必须手动修复,否则编译失败或运行异常。

3.1 启动文件必须匹配芯片Flash大小

源码根目录的startup_stm32f103xb.s是为大容量芯片(256KB Flash)设计的,但你的板子若用的是STM32F103C8T6(64KB Flash),向量表末尾地址会越界。需修改两处:

; startup_stm32f103xb.s 第42行:原为 Stack_Size EQU 0x00000400 ; 必须改为(小容量芯片栈空间减半) Stack_Size EQU 0x00000200 ; 第128行:原为 __Vectors DCD __initial_sp ; Top of Stack ; 需确认__initial_sp定义位置,在startup文件末尾找到 ; 将其后的DCD指令数量从224减至184(因中断向量减少40个)

注意:改完后务必在Keil的"Options for Target → Device"中重新选择"STM32F103C8",否则Flash算法仍按256KB加载,烧录时会卡在"Verifying..."阶段。

3.2 AFE6812驱动需关闭编译器优化陷阱

Drivers/afe6812/afe6812_init.c中有一段关键代码:

// 等待AFE内部参考电压稳定(典型值10ms) for(volatile uint32_t i = 0; i < 1000000; i++); // 延时循环

若Keil开启Level 3优化(-O3),编译器会直接删掉这个空循环!必须在该文件属性中单独设置优化等级:

  • 右键afe6812_init.c→ "Options for File..."
  • 勾选"Override default" → 在"C/C++"页签下将"Optimization"改为"Level 0: No optimization"

同理,Core/StateMachines/bms_state_machine.c中的状态标志位变量bms_event_flags必须声明为volatile,否则优化后主循环可能永远读不到中断置位的标志。

3.3 CAN通信波特率计算公式验证

源码默认CAN波特率为500kbps,但实际能否跑通取决于你的晶振精度。计算公式为:

CAN_BTR = (BRP << 0) | ((TS1-1) << 4) | ((TS2-1) << 10) | (SJW << 12) 其中 BRP = (PCLK1 / (CAN_BAUDRATE * (TS1 + TS2 + 1))) - 1

若你的PCLK1=36MHz(APB1总线),代入得BRP=3,TS1=6,TS2=5,SJW=1。但实测发现:当晶振偏差>1%时,CAN节点无法同步。解决方案是在Core/Drivers/can_driver.c中增加自适应校准:

// 初始化后立即发送一帧测试报文,监听回环 CAN_Transmit(&hcan1, &TxHeader, aTxData, 10); HAL_Delay(1); if(can_rx_flag == 0) { // 无回环说明波特率不准 // 切换到备用波特率:250kbps(BRP=7, TS1=6, TS2=5) hcan1.Init.Prescaler = 7; HAL_CAN_Init(&hcan1); }

4. 避坑:五个让BMS反复重启、SOC跳变、均衡失效的血泪现场问题

这些坑我都踩过,有些甚至导致整块PCB报废。以下按现象、原因、解决三步拆解,拒绝模糊描述。

4.1 现象:上电后LED常亮不灭,串口无输出,J-Link识别到设备但无法halt

原因:system_stm32f1xx.c中SystemCoreClock变量未初始化为72000000。源码默认假设HSE=8MHz经PLL倍频到72MHz,但你的板子若用内部RC振荡器(HSI=8MHz),SetSysClockTo72()函数根本不会执行,导致SystemCoreClock保持0值。所有基于HAL_Delay()的延时函数(包括AFE初始化等待)全部失效,MCU卡死在while(1)。
解决:在main()开头强制赋值:

SystemCoreClock = 72000000; // 必须在HAL_Init()之前 HAL_Init();

4.2 现象:12串电池电压显示正常,但第5串始终比邻近串低50mV,且随温度升高偏差加大

原因:AFE6812的REFIN引脚接了外部基准源,但原理图中该基准源(ADR4540)的地线未单点连接到AFE的AGND,而是混入数字地。噪声耦合导致REFIN波动,尤其在温度变化时半导体基准源温漂加剧。
解决:飞线将ADR4540的GND引脚直接焊接到AFE6812的AGND焊盘,避开PCB铺铜地平面。实测偏差从50mV降至1.2mV。

4.3 现象:SOC从80%突降到20%,持续10秒后自动恢复,期间无任何故障码

原因:Algorithms/SOC/soc_calc.c中L3温度补偿系数表temp_coeff[]索引越界。当NTC读数为-25℃时,temperature_index = (-25 + 20) / 5 = -1,数组访问temp_coeff[-1]读取了栈上随机值,导致SOC被乘以一个极大系数(如2.8),后续安时积分快速耗尽虚拟电量。
解决:在索引计算后加边界检查:

int8_t idx = (temperature + 20) / 5; if(idx < 0) idx = 0; if(idx > 15) idx = 15; soc *= temp_coeff[idx];

4.4 现象:被动均衡开启后,对应MOSFET发热严重,PCB铜箔发黑,但电池压差未缩小

原因:源码中均衡驱动使用N-MOSFET(如AO3400),但原理图误用P-MOSFET(如SI2301)。N-MOSFET需栅极电压高于源极(即Vgs>2.5V)才能导通,而源码Drivers/equalize_driver.c直接将GPIO置高(3.3V),但MOSFET源极接电池正极(最高42V),导致Vgs=-38.7V,MOSFET处于反向击穿区。
解决:更换为N-MOSFET,并在驱动电路中加入电平转换(如TC4427驱动芯片),确保Vgs>10V。

4.5 现象:CAN总线偶尔丢帧,用CANalyzer抓包发现ID为0x180的报文重复发送三次

原因:Core/Drivers/can_driver.c中错误启用了自动重传(AutoRetransmission)。当总线负载率>70%时,仲裁失败的节点会不断重发,形成风暴。源码默认hcan1.Init.AutoRetransmission = ENABLE,但国标GB/T 34657.1明确要求BMS节点禁止自动重传,必须由应用层实现超时重发。
解决:在CAN初始化结构体中关闭:

hcan1.Init.AutoRetransmission = DISABLE; // 关键! hcan1.Init.Mode = CAN_MODE_NORMAL;

5. 故障注入与闭环验证:用三步法确认SOC估算是否可信

光看代码不等于能用,必须用物理手段验证算法有效性。我总结出一套无需专业设备的闭环验证法,重点验证SOC估算模块——因为这是BMS最容易“看起来正常实则失效”的部分。

5.1 步骤一:静态OCV查表校准(验证L1层)

断开充放电回路,让电池静置4小时以上。用高精度万用表(六位半)测量单体电压,同时读取BMS串口输出的OCV_SOC值。制作对照表:

实测电压(V)源码查表SOC(%)偏差(%)是否在±2%内
3.65098.2-0.3✓
3.52075.1+0.4✓
3.38042.8-1.1✗(需调整ocv_table_25C.h中3.38V对应行)

关键动作:若偏差超限,不要改算法,直接编辑ocv_table_25C.h。该文件是纯数组,格式为{3.650f, 98.5f}, {3.645f, 98.2f}, ...,按实测数据插值替换。记住:OCV查表是SOC的锚点,其他层都是围绕它修正。

5.2 步骤二:动态安时积分验证(验证L2层)

用电子负载以0.2C恒流放电(如12Ah电池用2.4A),每放出0.5Ah记录一次BMS显示SOC。绘制放电曲线:

放出容量(Ah) | BMS显示SOC(%) | 理论SOC(%) | 偏差(%) 0.0 | 100.0 | 100.0 | 0.0 0.5 | 95.8 | 95.8 | 0.0 1.0 | 91.2 | 91.7 | -0.5 1.5 | 86.5 | 87.5 | -1.0

若偏差持续增大(如到1.5Ah时达-3.0%),说明电流采样偏移未校准。进入Drivers/current_sensor/current_calib.c,调整CURRENT_OFFSET宏定义(默认0),每±1mA偏移对应约0.04% SOC误差。

5.3 步骤三:温度-电流耦合扰动测试(验证L3+L4层)

这是最容易翻车的场景。准备两个条件:

  • 条件A:电池在25℃环境,以0.5C放电至SOC=50%,记录此时电压;
  • 条件B:将电池放入恒温箱降温至0℃,静置2小时,再以0.5C放电至相同电压值,记录此时SOC。

理论上,0℃下相同电压对应更高SOC(因锂离子迁移慢)。若源码L3温度补偿生效,条件B的SOC应比条件A高3~5%。若无变化,检查soc_calc.c中temp_coeff[]数组是否被优化器删除(见3.2节),或NTC分压电阻值是否与ntc_calib.c中R_DIVIDER宏不一致。

从那以后我每次拿到新BMS源码,都强制走一遍这三步:先用万用表钉死OCV查表,再用电子负载打穿安时积分,最后用恒温箱制造温度梯度。不是为了炫技,而是因为SOC跳变这种问题,往往在车辆下线前最后一刻才爆发——而那时你已经没有时间重画PCB。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询