2. BMS软件分层架构:应用层、服务层、驱动层、硬件抽象层的职责划分与交互机制
做BMS软件这几年,我最大的体会就是:分层架构是保命的。你想想看,一个BMS要管电压、温度、电流,还要跑SOC算法、均衡策略、故障诊断,如果所有代码都揉在一起,那改一个地方就得提心吊胆半天。
我个人习惯把BMS软件分成四层:应用层、服务层、驱动层、硬件抽象层。每一层各司其职,层与层之间通过接口交互。说白了,就是让上层不关心底层怎么干活,底层也不关心上层要干什么。
2.1 应用层:业务逻辑的决策者
应用层是BMS的大脑。它负责所有跟电池管理相关的业务逻辑,比如:
- 状态机管理:待机、充电、放电、故障等状态的切换
- SOC/SOH估算:用各种算法算电池还剩多少电、健康度如何
- 均衡策略:什么时候开启均衡、均衡电流多大
- 故障诊断:过压、欠压、过温、过流等异常的处理
- 通信协议处理:解析CAN报文、打包上报数据
应用层不直接操作硬件。它只调用服务层提供的接口。举个例子:
// 应用层代码示例:故障诊断任务
void FaultDiagnosis_Task(void)
{
float cellVoltage = SVC_GetMaxCellVoltage(); // 调用服务层接口
if (cellVoltage > OVER_VOLTAGE_THRESHOLD)
{
APP_SetFault(FAULT_OVER_VOLTAGE);
APP_SetState(STATE_FAULT); // 切换到故障状态
}
}
你看,应用层只管判断逻辑,它不知道电压是怎么采集来的。这样如果以后换了ADC芯片,应用层代码一个字都不用改。
2.2 服务层:承上启下的调度者
服务层是BMS的「腰部」。它把底层的原始数据加工成应用层需要的信息,同时把应用层的指令翻译成底层能理解的操作。
服务层主要干这些事:
- 数据聚合:把多路ADC采集的电压值汇总,算出最高电压、最低电压、压差
- 滤波处理:对原始采样值做均值滤波、中值滤波,去掉噪声
- 参数管理:维护电池参数表(容量、内阻、充放电曲线等)
- 指令路由:把应用层的均衡指令,拆解成具体的MOS管开关动作
- 时间管理:提供定时器、延时、超时检测等服务
服务层的接口设计很关键。我一般这样定义:
// 服务层接口示例
float SVC_GetMaxCellVoltage(void); // 获取最高单体电压
float SVC_GetMinCellVoltage(void); // 获取最低单体电压
float SVC_GetPackVoltage(void); // 获取总电压
float SVC_GetPackCurrent(void); // 获取总电流
void SVC_SetBalanceTarget(uint8_t cell, uint8_t enable); // 设置均衡目标
嗯,这里要注意:服务层的接口命名要统一。我习惯用 SVC_ 前缀,这样一看就知道是服务层的函数。
2.3 驱动层:硬件的直接操作者
驱动层是BMS的「手脚」。它直接跟MCU的外设寄存器打交道,负责:
- ADC驱动:配置采样通道、启动转换、读取结果
- GPIO驱动:控制MOS管的开关、读取数字信号
- SPI/I2C驱动:与AFE芯片、温度传感器通信
- CAN驱动:发送和接收CAN报文
- 定时器驱动:产生PWM波、计时
驱动层的代码跟MCU型号强相关。换一颗MCU,驱动层基本要重写。所以驱动层的接口要尽量简单、通用:
// 驱动层接口示例
void DRV_ADC_Init(void);
uint16_t DRV_ADC_ReadChannel(uint8_t ch);
void DRV_GPIO_SetPin(uint8_t pin, uint8_t level);
uint8_t DRV_GPIO_ReadPin(uint8_t pin);
void DRV_CAN_SendMessage(uint32_t id, uint8_t *data, uint8_t len);
驱动层不关心数据含义。它只管把寄存器的值读出来,或者把数据写进去。至于这个电压值是3.7V还是4.2V,那是服务层的事。
2.4 硬件抽象层:隔离变化的防火墙
硬件抽象层(HAL)是BMS的「翻译官」。它把不同MCU的寄存器操作,封装成统一的接口。说白了,就是让驱动层不用关心具体用的是STM32还是NXP还是TI的芯片。
HAL层主要做这些事:
- 寄存器映射:定义各外设的基地址、寄存器偏移
- 时钟配置:初始化系统时钟、外设时钟
- 中断管理:注册中断服务函数、设置优先级
- DMA配置:配置数据传输通道
举个例子,同样是GPIO输出高电平,不同MCU的写法完全不同:
// STM32的HAL写法
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);
// NXP的SDK写法
GPIO_PinWrite(GPIOB, 0, 1);
// 我们自己封装的HAL接口
HAL_GPIO_SetPin(PORT_B, PIN_0, LEVEL_HIGH);
有了HAL层,驱动层就只需要调用 HAL_GPIO_SetPin,不用管底层是哪个芯片。换MCU的时候,只需要重写HAL层,驱动层基本不用动。
HAL_ 前缀,而且每个函数只做一件事。比如 HAL_ADC_Init 只初始化ADC,HAL_ADC_Start 只启动转换。这样调试的时候,很容易定位问题。
2.5 层间交互机制:接口就是契约
四层架构搭好了,怎么让它们协同工作?答案是:接口。每一层只通过接口跟相邻层通信,不能跨层调用。
交互流程大概是这样的:
- 应用层调用服务层的接口获取数据或下发指令
- 服务层调用驱动层的接口操作硬件,或者对数据进行加工
- 驱动层调用HAL层的接口读写寄存器
- HAL层直接操作MCU寄存器
反过来,数据流向是:硬件 → HAL层 → 驱动层 → 服务层 → 应用层。
我一般用结构体来定义层间接口,这样方便管理:
// 服务层提供给应用层的接口表
typedef struct {
float (*GetMaxVoltage)(void);
float (*GetMinVoltage)(void);
float (*GetPackCurrent)(void);
void (*SetBalance)(uint8_t cell, uint8_t enable);
} SVC_Interface_t;
// 驱动层提供给服务层的接口表
typedef struct {
void (*ADC_Init)(void);
uint16_t (*ADC_Read)(uint8_t ch);
void (*GPIO_Write)(uint8_t pin, uint8_t level);
uint8_t (*GPIO_Read)(uint8_t pin);
} DRV_Interface_t;
这样设计的好处是:接口清晰,依赖关系一目了然。而且单元测试的时候,可以轻松地替换掉底层实现,用模拟接口来测试应用层逻辑。
我记得有一次,客户要求把MCU从STM32F103换成GD32F103。因为分层做得好,我只花了三天就完成了移植——重写了HAL层,驱动层改了少量代码,服务层和应用层一个字没动。这就是分层架构的价值。