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;
    }
}
我的经验:状态机里一定要加「超时保护」。比如初始化超过5秒还没完成,强制进入故障状态。否则系统会卡死在某一个状态,你想想看,电池管理系统卡死了多危险。

3.3.3 状态机设计要点

  • 状态唯一性:同一时刻只能处于一个状态
  • 状态转移清晰:每个转移条件都要明确,不要有模糊地带
  • 状态入口/出口动作:进入故障状态时关MOS,退出故障状态时清标志
  • 状态超时处理:每个状态都要有超时检测,防止死锁

我曾经在一个项目中,因为状态机里漏了「故障恢复延时」,导致电池刚断开MOS又立刻闭合,来回震荡,差点烧了电池。从那以后,我每个状态转移都加了延时和去抖逻辑。

总结一下:分层架构让代码好维护,RTOS让任务好调度,状态机让逻辑好理解。这三板斧用好了,BMS软件架构就稳了。下一章我们聊聊具体的「电压采集电路与ADC驱动设计」,到时候我会分享一些采样噪声处理的实战技巧。