第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通常资源有限)
  • 生态:社区活跃度、文档是否齐全
任务划分示例(基于FreeRTOS):

// 任务优先级定义
#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级,问题就解决了。

避坑指南:我曾经因为任务优先级设置不当,导致过流保护延迟了20ms,差点烧了电池。后来我总结了一个原则:保护任务优先级最高,采集任务次之,通信任务最低。

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芯片的驱动开发。到时候我会分享一些我在项目中遇到的“坑”,以及怎么填坑。嗯,今天就到这里,大家先消化一下。


专注资料整理