第3章:BMS软件架构——分层设计、交互机制与RTOS选型
做BMS系统这么多年,我最大的体会就是:软件架构决定了系统的下限,而算法决定了上限。你想想看,一个BMS要管几十串电芯,要实时监控电压、电流、温度,还要做均衡、保护、通信——如果没有一个清晰的架构,代码很快就会变成一团乱麻。
今天我们就来聊聊BMS软件架构的三个核心问题:怎么分层、层与层之间怎么对话、以及实时操作系统该怎么选。
3.1 分层软件架构:把复杂问题拆成小块
我个人习惯把BMS软件分成三层,就像盖房子一样:
- 应用层:负责业务逻辑,比如SOC估算、SOH诊断、均衡策略
- 中间层:负责数据抽象和任务调度,比如ADC数据滤波、通信协议解析
- 底层驱动层:直接跟硬件打交道,比如SPI读写、GPIO控制、CAN收发
为什么要分层?说白了就是解耦。我在项目中遇到过一件事:某次换了ADC芯片,如果代码是揉在一起的,那得改几百行。但因为我们用了分层架构,只需要改底层驱动层的几行代码,应用层完全不用动。
核心原则:上层不能直接调用下层硬件寄存器,必须通过中间层提供的API接口。
这里我画了一张分层架构图,你看一眼就明白了:
小技巧:中间层可以设计成"可插拔"的。比如你想从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软件就稳了。