第3章:BMS软件架构——分层设计、交互机制与RTOS选型

做BMS系统这么多年,我最大的体会就是:软件架构决定了系统的下限,而算法决定了上限。你想想看,一个BMS要管几十串电芯,要实时监控电压、电流、温度,还要做均衡、保护、通信——如果没有一个清晰的架构,代码很快就会变成一团乱麻。

今天我们就来聊聊BMS软件架构的三个核心问题:怎么分层、层与层之间怎么对话、以及实时操作系统该怎么选。

3.1 分层软件架构:把复杂问题拆成小块

我个人习惯把BMS软件分成三层,就像盖房子一样:

  • 应用层:负责业务逻辑,比如SOC估算、SOH诊断、均衡策略
  • 中间层:负责数据抽象和任务调度,比如ADC数据滤波、通信协议解析
  • 底层驱动层:直接跟硬件打交道,比如SPI读写、GPIO控制、CAN收发

为什么要分层?说白了就是解耦。我在项目中遇到过一件事:某次换了ADC芯片,如果代码是揉在一起的,那得改几百行。但因为我们用了分层架构,只需要改底层驱动层的几行代码,应用层完全不用动。

核心原则:上层不能直接调用下层硬件寄存器,必须通过中间层提供的API接口。

这里我画了一张分层架构图,你看一眼就明白了:

应用层(Application Layer) SOC估算 | SOH诊断 | 均衡策略 | 保护逻辑 业务逻辑,与硬件无关 中间层(Middleware Layer) 数据滤波 | 协议解析 | 任务调度 | 状态机 抽象接口,屏蔽硬件差异 底层驱动层(Driver Layer) ADC驱动 | SPI/I2C | CAN收发 | GPIO控制 直接操作寄存器,与硬件绑定 API接口 HAL抽象

小技巧:中间层可以设计成"可插拔"的。比如你想从FreeRTOS换成RT-Thread,只需要改中间层的调度接口,应用层代码一个字都不用动。

3.2 应用层与底层的交互:别让它们直接"谈恋爱"

应用层和底层怎么通信?这是很多新手容易搞错的地方。我见过有人直接在SOC估算函数里写HAL_ADC_Start()——嗯,这其实是个坏习惯。

正确的做法是:通过回调函数或消息队列来传递数据

举个例子,ADC采集完成后的处理流程:

// 底层驱动:ADC中断服务函数
void ADC_IRQHandler(void) {
    uint16_t raw_value = ADC->DR;
    // 把原始数据放入队列
    osMessageQueuePut(adc_queue, &raw_value, 0, 0);
}

// 中间层:数据滤波任务
void FilterTask(void *arg) {
    uint16_t raw;
    while(1) {
        osMessageQueueGet(adc_queue, &raw, 0, portMAX_DELAY);
        float filtered = moving_average_filter(raw);
        // 把滤波后的数据发布到主题
        bms_publish("cell_voltage", filtered);
    }
}

// 应用层:SOC估算任务
void SOC_Task(void *arg) {
    float voltage;
    while(1) {
        bms_subscribe("cell_voltage", &voltage);
        soc = calculate_soc(voltage);
    }
}

你看,底层只管采集,中间层负责处理,应用层只管算SOC。各司其职,互不干扰。

避坑指南:我曾经在一个项目里看到,应用层直接调用底层延时函数HAL_Delay(),结果导致整个系统卡死。因为底层驱动可能在中断上下文,而HAL_Delay()依赖SysTick中断——死锁就是这么来的。

3.3 实时操作系统选型:别盲目追新

BMS对实时性要求很高。你想想看,电芯过压了,必须在几毫秒内切断回路,否则就可能起火。所以裸机编程基本不够用,必须上RTOS。

市面上常见的RTOS有这么几个:

RTOS 内核大小 实时性 生态 适合场景
FreeRTOS ~6KB 优秀 非常丰富 中小型BMS,资源受限
RT-Thread ~8KB 优秀 丰富,有组件生态 功能复杂的BMS,需要网络
μC/OS-III ~12KB 极优 商业支持好 车规级BMS,安全要求高
Zephyr ~20KB 良好 Linux风格 大型BMS,多核处理器

我个人建议:如果MCU资源紧张(比如Flash只有64KB),选FreeRTOS准没错。它的任务切换时间可以做到1μs以内,完全够用。

但如果你的BMS需要支持OTA升级、文件系统、甚至TCP/IP协议栈,那RT-Thread会更合适。我记得有个项目,客户要求BMS能通过4G模块上传数据,我们用了RT-Thread的AT组件,两周就搞定了。

选型三要素

  • 实时性:任务切换时间 < 5μs,中断响应 < 1μs
  • 确定性:不能有非确定性的调度延迟
  • 资源占用:RAM和Flash不能超过MCU的60%

3.4 任务优先级设计:别让"饿死"发生

RTOS选好了,接下来就是任务优先级怎么分配。这其实是个艺术活。

我一般遵循这个原则:

  • 最高优先级:安全保护任务(过压、过温、过流检测)
  • 高优先级:实时采集任务(电压、电流、温度)
  • 中优先级:通信任务(CAN、UART)
  • 低优先级:非实时任务(日志记录、均衡控制)

为什么会这样?你想想看,如果均衡任务占用了CPU,导致过压检测没来得及响应——那后果就严重了。

经验之谈:我曾经把CAN通信任务的优先级设得比采集任务还高,结果发现电压数据总是延迟。后来才意识到,CAN通信虽然重要,但可以容忍几毫秒的延迟,而电压采集必须严格按时。调换优先级后,问题就解决了。

3.5 状态机设计:让BMS"知道自己在哪里"

BMS的运行状态不是一成不变的。从休眠到唤醒,从充电到放电,从正常到故障——每个状态下的行为都不一样。

我习惯用有限状态机(FSM)来管理:

typedef enum {
    BMS_STATE_IDLE,      // 空闲/休眠
    BMS_STATE_CHARGE,    // 充电
    BMS_STATE_DISCHARGE, // 放电
    BMS_STATE_FAULT,     // 故障
    BMS_STATE_PROTECT    // 保护
} BMS_State_t;

void BMS_StateMachine(BMS_State_t current_state, Event_t event) {
    switch(current_state) {
        case BMS_STATE_IDLE:
            if(event == EVENT_CHARGE_PLUGIN) {
                set_state(BMS_STATE_CHARGE);
                enable_charging();
            }
            break;
        case BMS_STATE_CHARGE:
            if(event == EVENT_OVERVOLTAGE) {
                set_state(BMS_STATE_PROTECT);
                disable_charging();
                log_fault("Overvoltage protection");
            }
            break;
        // ... 其他状态处理
    }
}

状态机的好处是:逻辑清晰,容易调试。我在项目中遇到过一个bug,电池在充电时突然跳到了放电状态。通过状态机日志,我们很快发现是CAN报文解析错误导致的事件误触发。

注意:状态切换一定要加"防抖"处理。比如过压保护,不能因为一次采样噪声就触发保护,至少要连续3次采样都超阈值才切换状态。

好了,关于BMS软件架构的核心内容就这些。分层设计让你代码好维护,RTOS选型让你系统更可靠,状态机让你逻辑更清晰。这三板斧用好了,BMS软件就稳了。


专注资料整理