3、BMS软件架构设计:分层架构、任务调度与状态机
好,咱们进入正题。BMS的软件架构,说白了就是一套「怎么把代码组织起来」的规矩。我见过不少新手,上来就写逻辑,写到后面发现改一个地方要动整个代码,那叫一个痛苦。所以,先搭好架子,再填内容,这是过来人的经验。
3.1 分层架构:驱动层、中间层、应用层
我个人习惯把BMS软件分成三层。为什么是三层?因为三层刚好够用,再多就冗余了。你想想看,底层管硬件,上层管业务,中间层做隔离,各干各的,互不干扰。
3.1.1 驱动层(Driver Layer)
驱动层是离硬件最近的一层。它直接操作寄存器、SPI、I2C、CAN、ADC这些外设。说白了,就是给上层提供一套「硬件操作接口」。
举个例子,你要读一个电池电压,驱动层会提供一个函数:
// 驱动层接口
uint16_t ADC_ReadChannel(uint8_t ch);
float BQ76952_ReadCellVoltage(uint8_t cellIdx);
我在项目中遇到过一个问题:某次换了ADC芯片,驱动层改了,但应用层代码一行没动。这就是分层的好处。驱动层只负责「怎么读」,不关心「读来干嘛」。
ADC_、CAN_、GPIO_,这样一看就知道是哪家的驱动。
3.1.2 中间层(Middleware Layer)
中间层是承上启下的关键。它把驱动层的原始数据加工成「有意义的信息」。比如,驱动层读到了ADC原始值,中间层负责把它换算成电压、电流、温度。
中间层还负责一些通用功能:
- 数据滤波:比如滑动平均滤波,去掉采样噪声
- 协议解析:比如CAN报文解析,把ID和数据拆出来
- 错误检测:比如CRC校验、超时判断
嗯,这里要注意:中间层不要直接调用驱动层。它应该通过一个「接口层」来访问。我习惯在中间层定义一个结构体,把所有驱动接口指针放进去,这样方便单元测试。
3.1.3 应用层(Application Layer)
应用层是BMS的大脑。它负责:
- 状态机调度:什么时候充电、什么时候放电
- 保护逻辑:过压、欠压、过温、过流
- 均衡控制:被动均衡还是主动均衡
- SOC/SOH估算:卡尔曼滤波、安时积分
应用层只关心「业务逻辑」,不关心底层硬件。我曾经把一个BMS项目从STM32移植到GD32,应用层代码一个字没改,只改了驱动层和中间层的几个文件。这就是分层的威力。
3.2 任务调度:RTOS vs 裸机
这是BMS开发中绕不开的选择题。我两种都做过,说说我的感受。
3.2.1 裸机调度(Super Loop)
裸机就是主循环里跑所有任务。结构简单,资源占用少。适合功能简单的BMS,比如只有电压检测和过压保护。
void main(void)
{
Init_All();
while(1)
{
Read_Voltage(); // 读电压
Check_Protection();// 检查保护
Update_LED(); // 更新指示灯
Delay_ms(10); // 简单延时
}
}
但裸机有个大坑:如果某个任务卡住了,整个系统就挂了。我在早期一个项目中,因为ADC采样偶尔超时,导致LED闪烁都停了。排查了好久才发现是任务之间互相干扰。
3.2.2 RTOS调度(实时操作系统)
RTOS把任务拆成多个线程,每个线程有自己的优先级和栈空间。常用的有FreeRTOS、uC/OS、RT-Thread。
RTOS的好处很明显:
- 任务隔离:一个任务崩溃不会影响其他任务
- 优先级管理:紧急任务(比如过流保护)可以抢占低优先级任务
- 通信机制:消息队列、信号量、互斥锁,方便任务间传数据
// FreeRTOS 任务创建示例
xTaskCreate(vTask_VoltageMonitor, "Voltage", 256, NULL, 3, NULL);
xTaskCreate(vTask_ProtectionCheck, "Protect", 256, NULL, 4, NULL);
xTaskCreate(vTask_Communication, "Comm", 512, NULL, 2, NULL);
我个人建议:如果BMS功能超过5个模块,或者有实时性要求(比如1ms内响应过流),直接上RTOS。别犹豫,裸机后期维护成本太高。
| 对比项 | 裸机 | RTOS |
|---|---|---|
| 资源占用 | 低(RAM/ROM少) | 高(需要任务栈) |
| 实时性 | 依赖手动调度 | 抢占式,响应快 |
| 开发难度 | 低 | 中等(需理解任务、信号量) |
| 可维护性 | 差(功能耦合) | 好(模块独立) |
| 典型场景 | 简单保护板 | 智能BMS、多串数 |
3.3 状态机设计:初始化、待机、运行、故障、休眠
BMS本质上是一个状态机。不同状态下,行为完全不同。我习惯用「有限状态机(FSM)」来管理。
3.3.1 五大核心状态
- 初始化(Init):上电后执行硬件自检、参数加载、校准
- 待机(Standby):系统就绪,等待外部指令(比如充电枪插入)
- 运行(Running):正常充放电,持续监控和保护
- 故障(Fault):检测到异常,执行保护动作(断开MOS、报警)
- 休眠(Sleep):低功耗模式,定时唤醒检测
3.3.2 状态切换逻辑
状态切换不是随便跳的。比如从「运行」到「故障」,必须满足故障条件。从「故障」到「待机」,必须故障消除且经过一定延时。
typedef enum {
STATE_INIT,
STATE_STANDBY,
STATE_RUNNING,
STATE_FAULT,
STATE_SLEEP
} BMS_State_t;
BMS_State_t currentState = STATE_INIT;
void BMS_StateMachine(void)
{
switch(currentState)
{
case STATE_INIT:
if(Init_Complete()) currentState = STATE_STANDBY;
break;
case STATE_STANDBY:
if(ChargePlug_Inserted()) currentState = STATE_RUNNING;
if(Sleep_Condition()) currentState = STATE_SLEEP;
break;
case STATE_RUNNING:
if(Fault_Detected()) currentState = STATE_FAULT;
if(Charge_Complete()) currentState = STATE_STANDBY;
break;
case STATE_FAULT:
if(Fault_Cleared() && Delay_Elapsed(1000))
currentState = STATE_STANDBY;
break;
case STATE_SLEEP:
if(Wakeup_Event()) currentState = STATE_STANDBY;
break;
}
}
3.3.3 状态机设计要点
- 状态唯一性:同一时刻只能处于一个状态
- 状态转移清晰:每个转移条件都要明确,不要有模糊地带
- 状态入口/出口动作:进入故障状态时关MOS,退出故障状态时清标志
- 状态超时处理:每个状态都要有超时检测,防止死锁
我曾经在一个项目中,因为状态机里漏了「故障恢复延时」,导致电池刚断开MOS又立刻闭合,来回震荡,差点烧了电池。从那以后,我每个状态转移都加了延时和去抖逻辑。