第1章:BMS软件架构设计——从分层到状态机
各位同学,大家好。我是你们的老朋友,一个在BMS领域摸爬滚打了十几年的工程师。今天咱们开始聊BMS软件架构设计。说实话,架构这东西,听起来很虚,但做起来全是坑。我见过太多项目,代码写得飞起,最后联调时发现任务调度乱成一锅粥,状态机跑飞了——嗯,那场面,真是“一把辛酸泪”。
所以,这一章咱们把地基打牢。我会从分层架构、任务调度与RTOS选型、状态机设计三个维度,把BMS软件的骨架讲清楚。你想想看,一个BMS系统,少则几十个功能,多则上百个,如果没有清晰的架构,代码维护起来就是噩梦。
1.1 分层软件架构:驱动层、中间件层、应用层
我个人习惯把BMS软件分成三层:驱动层、中间件层、应用层。为什么这么分?说白了,就是为了解耦。每一层只管自己的事,上层调用下层,下层不依赖上层。这样,换一个MCU或者换一个传感器,你只需要改驱动层,应用层代码基本不用动。
1.1.1 驱动层
驱动层是离硬件最近的。它负责直接操作寄存器、读写外设。比如ADC采集电压、温度,GPIO控制继电器,SPI/I2C通信等。我在项目中遇到过一个问题:某次换了ADC芯片,驱动层改了三天,应用层一行代码没动——这就是分层的好处。
- 硬件初始化(时钟、GPIO、外设)
- 数据采集(电压、电流、温度)
- 通信接口(CAN、UART、SPI)
- 底层中断处理
1.1.2 中间件层
中间件层是承上启下的。它把驱动层的数据“加工”一下,变成应用层能直接用的东西。比如,驱动层给你一个ADC原始值,中间件层把它换算成实际的电压值(mV)。再比如,CAN协议栈、文件系统、RTOS的封装,都属于中间件层。
我曾经踩过一个坑:驱动层直接给应用层传原始数据,应用层到处做换算,结果换了传感器,换算公式变了,改代码改到吐。后来我强制要求:所有换算必须在中间件层完成,应用层只认物理量。
1.1.3 应用层
应用层是BMS的大脑。它负责状态机管理、SOC/SOH估算、均衡控制、故障诊断等。应用层不关心底层硬件,它只关心“数据对不对”、“状态该不该切换”。
举个例子:应用层要判断是否过压。它从中间件层拿到“电芯电压”这个物理量,然后跟阈值比较。至于这个电压是ADC采的、还是AFE芯片读的,应用层完全不管。
HAL_ADC_Start(),结果换了MCU,整个应用层重写。记住:分层就是“各扫门前雪”。
1.2 任务调度与RTOS选型
BMS是一个实时性要求很高的系统。比如,过流保护必须在几毫秒内响应,否则电池可能出问题。所以,任务调度是BMS软件的核心。
1.2.1 为什么需要RTOS?
裸机编程也能做BMS,但代码会越来越复杂。你想想看,一个while循环里塞几十个功能,还要处理各种中断优先级,调试起来简直要命。RTOS可以把任务拆开,每个任务有自己的优先级和周期,调度由内核负责。
我个人习惯用FreeRTOS,原因很简单:开源、轻量、资料多。当然,如果你用STM32,也可以选RT-Thread或者uC/OS。选型时主要看三点:
- 实时性:任务切换时间是否满足你的需求(一般BMS要求ms级响应)
- 资源占用:RAM/ROM是否够用(BMS的MCU通常资源有限)
- 生态:社区活跃度、文档是否齐全
// 任务优先级定义
#define TASK_PRIO_HIGH 3 // 保护任务
#define TASK_PRIO_MEDIUM 2 // 采集任务
#define TASK_PRIO_LOW 1 // 通信任务
// 任务周期定义
#define TASK_PERIOD_PROTECT 10 // 10ms
#define TASK_PERIOD_ACQUIRE 100 // 100ms
#define TASK_PERIOD_COMM 500 // 500ms
// 任务函数原型
void vTaskProtect(void *pvParameters); // 过压/过流保护
void vTaskAcquire(void *pvParameters); // 数据采集
void vTaskComm(void *pvParameters); // CAN通信
1.2.2 任务调度策略
RTOS的调度策略一般是“抢占式+时间片轮转”。高优先级任务可以打断低优先级任务。但要注意:优先级不要设太多,3-5级就够了。优先级太多,反而容易出问题。
我记得有一次,一个同事设了8个优先级,结果任务A和任务B互相抢占,导致采集任务一直得不到执行,电压数据更新不及时,保护逻辑误动作。后来我们把优先级压缩到4级,问题就解决了。
1.3 状态机设计:空闲、充电、放电、故障
BMS的状态机,说白了就是电池的“人生阶段”。它决定了电池当前能做什么、不能做什么。常见的状态有四个:空闲、充电、放电、故障。
1.3.1 状态定义
| 状态 | 描述 | 允许操作 |
|---|---|---|
| 空闲 | 电池未工作,等待指令 | 允许充电/放电请求 |
| 充电 | 电池正在充电 | 禁止放电,允许故障跳转 |
| 放电 | 电池正在放电 | 禁止充电,允许故障跳转 |
| 故障 | 检测到异常,停止所有操作 | 仅允许复位到空闲 |
1.3.2 状态切换逻辑
状态切换不是随便跳的。比如,从“充电”不能直接跳到“放电”,必须先回到“空闲”。故障状态是最高优先级,任何状态下检测到故障,都必须立即跳转到故障状态。
我画了一张状态机图,大家看看:
1.3.3 状态机实现要点
状态机实现,我推荐用“查表法”。把状态和事件做成一个二维表,查表决定下一个状态。这样代码清晰,容易维护。
// 状态枚举
typedef enum {
STATE_IDLE,
STATE_CHARGE,
STATE_DISCHARGE,
STATE_FAULT
} BMS_State_t;
// 事件枚举
typedef enum {
EVT_CHARGE_REQ,
EVT_DISCHARGE_REQ,
EVT_STOP,
EVT_FAULT,
EVT_RESET
} BMS_Event_t;
// 状态转移表
BMS_State_t stateTable[4][5] = {
// 空闲状态
{ STATE_CHARGE, STATE_DISCHARGE, STATE_IDLE, STATE_FAULT, STATE_IDLE },
// 充电状态
{ STATE_CHARGE, STATE_CHARGE, STATE_IDLE, STATE_FAULT, STATE_CHARGE },
// 放电状态
{ STATE_DISCHARGE, STATE_DISCHARGE, STATE_IDLE, STATE_FAULT, STATE_DISCHARGE },
// 故障状态
{ STATE_FAULT, STATE_FAULT, STATE_FAULT, STATE_FAULT, STATE_IDLE }
};
// 状态机处理函数
BMS_State_t BMS_StateMachine(BMS_State_t currentState, BMS_Event_t event) {
return stateTable[currentState][event];
}
小结
这一章咱们聊了BMS软件架构的三个核心:分层架构让代码更清晰,RTOS让任务调度更高效,状态机让逻辑更可控。这些都是我多年实战中总结出来的经验。你想想看,如果一开始就把架构搭好,后面开发会省很多事。
下一章,咱们会深入驱动层,聊聊ADC采集和AFE芯片的驱动开发。到时候我会分享一些我在项目中遇到的“坑”,以及怎么填坑。嗯,今天就到这里,大家先消化一下。