基于STM的BMS毕业设计:从状态机原理到电池管理实战
摘要:许多高校学生在完成“基于STM的BMS毕业设计”时,常因缺乏对状态机(State Machine)与电池管理系统(BMS)协同逻辑的理解,导致系统鲁棒性差、SOC估算不准或通信异常。本文从技术科普角度出发,详解如何利用状态机规范充放电、均衡、故障处理等核心流程,结合STM32硬件平台实现模块化解耦设计。读者将掌握可复用的状态机架构、关键代码实现及调试技巧,显著提升系统稳定性与答辩表现。
1. BMS 核心功能痛点:为什么“保护”总慢半拍?
本科毕设里,BMS 常被简化为“采集电压→判断阈值→关闭 MOS”。看似三步,落地却频发“鬼跳”:
过充保护响应延迟
采样周期 100 ms,ADC 滤波 32 点平均,再叠加软件去抖,延迟轻松突破 300 ms;而高倍率充电时 1 s 内电压可爬升 50 mV,极易触发二次保护。状态跳变混乱
用if-else硬编码:if(v>4.2) state=OV; else if(v<2.8) state=UV; else state=NORMAL;当电压在 4.19 ↔ 4.21 V 抖动,状态机会在 OV ↔ NORMAL 之间高频翻转,继电器“哒哒”作响,最终黏连失效。
均衡与 SOC 估算耦合
均衡开启后电流路径变化,若 SOC 算法仍按静置模型计算,误差 8 % 司空见惯,答辩时被一句“误差为什么这么大”问倒。
一句话:缺少“确定性”与“可验证性”;而状态机正是给系统带来“确定性”的银弹。
2. 状态机选型:FSM 还是 HSM?
| 维度 | 有限状态机 FSM | 层次状态机 HSM |
|---|---|---|
| 状态数量 | 扁平,~15 个 | 树状,可复用父状态 |
| 代码量 | 随状态指数增长 | 线性增长 |
| 扩展性 | 新增状态需改表 | 子状态继承父行为 |
| 典型实现 | 二维查表 | QP™、Miro Samek 算法 |
BMS 场景需要“充电子状态”与“放电子状态”复用同一“温度保护”逻辑,HSM 的“行为继承”天然契合;而 STM32F103C8T6 主频 72 MHz,QP-nano 实测 1 kHz 调度 CPU 占用 < 3 %,资源绰绰有余。故本文以HSM 为理论骨架,轻量级 C 实现为目标,既保留层次优势,又避开 RTOS 依赖。
3. 基于 STM32 HAL 的轻量级 HSM 实现
3.1 状态机骨架
/* bsm.h Battery State Machine */ typedef enum掀动事件 { EV_ENTRY, EV_EXIT, EV_INIT, EV_TICK_10ms, EV_CELL_OVP, EV_CELL_UVP, EV_TEMP_OTP, EV_USER_CHG_ENABLE, EV_USER_CHG_DISABLE, /* ... */ } Event; typedef struct State { char const *name; void (*dispatch)(struct State *me, Event e); /* 父状态指针,NULL 表示顶层 */ struct State *super; } State; /* 全局上下文 */ typedef struct { State *state; /* 当前活动状态 */ uint16_t cell_mV[14]; /* 电芯电压 */ int8_t temp_dC[4]; /* 温度,单位 0.1 ℃ */ uint8_t soc_per; /* SOC % */ } BmsCtx; extern BmsCtx bms;3.2 状态切换宏(带日志)
#define TRANS(target_) \ do{ \ printf("@%lu S:%s->%s\r\n", HAL_GetTick(), \ bms.state->name, (target_)->name); \ bms.state->dispatch(bms.state, EV_EXIT); \ bms.state = (target_); \ bms.state->dispatch(bms.state, EV_ENTRY); \ }while(0)3.3 顶层状态实现(节选)
static void Top_dispatch(State *me, Event e){ switch(e){ case EV_TICK_10ms: /* 广播到子状态 */ if(bms.state->super) bms.state->super->dispatch(me, e); /* 采样与滤波 */ ADC_ScanCells(); if(CellMax_mV() > 4200) bms.state->dispatch(bms.state, EV_CELL_OVP); break; } }3.4 充电状态层次
Top └─ Charging ├─ CC (恒流) ├─ CV (恒压) └─ Taper当任意电芯触发 OVP,Charging父状态统一处理,子状态无需重复代码:
static void Charging_dispatch(State *me, Event e){ switch(e){ case EV_CELL_OVP: TRANS(&Fault_ovp); return; /* 事件已处理 */ case EV_ENTRY: CHG_MOS_ON(); return; case EV_EXIT: CHG_MOS_OFF(); return; } /* 其余事件给子状态 */ if(me->super) me->super->dispatch(me, e); }3.5 SOC 更新回调(解耦)
static void CC_dispatch(State *me, Event e){ switch(e){ case EV_TICK_10ms: CoulombCounter_Update(CHG_CURRENT_MA); if(ABS(CHG_CURRENT_MA) < 50) TRANS(&Charging_cv); break; } }通过事件驱动,SOC 算法与状态机完全解耦;若后期换用 Kalman 滤波,仅需替换CoulombCounter_Update()内部实现,状态表纹丝不动。
4. 安全性与性能:让数字说话
| 测试项 | 条件 | 结果 |
|---|---|---|
| 状态切换延迟 | 示波器抓取 GPIO 边沿 | 92 µs(含 ADC 判断) |
| CPU 占用 | 1 kHz 调度,72 MHz | 2.7 % |
| 看门狗复位间隔 | IWDG 250 ms | 0 次误复位 |
| ADC 采样抗噪 | 注入 200 mV 峰峰值 100 kHz | 电压误差 < 5 mV |
关键代码:双缓冲 DMA + 中位值平均
#define ADC_BUF_LEN 32 static uint16_t adc1_buf[ADC_BUF_LEN]; HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc1_buf, ADC_BUF_LEN); uint16_t ADC_GetMedian(void){ uint16_t tmp[ADC_BUF_LEN]; memcpy(tmp, adc1_buf, sizeof(tmp)); sort_u16(tmp, ADC_BUF_LEN); return tmp[ADC_BUF_LEN/2]; /* 中位值 */ }中位值滤波对脉冲干扰抑制优于算术平均,且无需乘除,CM3 单周期移位完成。
5. 生产级避坑指南
状态持久化
断电瞬间正在均衡,重新上电应恢复均衡。将bms.state的枚举索引与CRC8一并存入 STM32 备份寄存器(20 B),上电校验通过后TRANS()恢复,否则进入Fault_unknown。事件队列溢出
使用环形队列保存外部中断事件;若队列满,丢弃最旧事件而非最新,保证“故障事件优先”。调试可视化
通过 SWO 以 225 kHz 输出压缩事件码,上位机解析后实时绘制状态树,答辩现场一秒出图,老师直呼专业。代码 Clean 原则
- 一个函数只做一件事:
Charging_cv()仅处理恒压逻辑,不插手温度。 - 宏全部大写,
TRANS()含括号,避免运算符优先级悲剧。 - 所有魔法数字收拢到
bms_config.h,方便后续移植到 STM32G0。
- 一个函数只做一件事:
6. 结语与展望
状态机让 BMS 从“一坨 if”升级为“可证明的确定性自动机”,配合 STM32 的丰富外设,本科毕设也能跑出工业级节奏。下一步,不妨思考:
如何扩展同一套 HSM 支持多节电池串并联?
提示:把“单节状态”抽象为复合状态的区域(region),每条 region 独立运行,再引入“系统级仲裁状态”处理环流、反向充电等场景。是否考虑把代码开源到 GitHub?
附上一份单元测试(基于 CMocka)与硬件在环(HIL)脚本,下一个 star 也许就来自未来的面试官。
毕业设计不是终点,让电池组更安全、更长寿,才是工程师的持久浪漫。祝调试顺利,答辩高分!