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芯片,应用层代码一个字都不用改。

我的经验:应用层最容易犯的错误就是「越权」。我曾经见过一个项目,应用层代码直接去读写SPI寄存器。结果换了MCU之后,整个应用层都得重写。记住:应用层只做决策,不干苦力活。

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,那是服务层的事。

重要提醒:驱动层一定要做异常处理。比如ADC转换超时、SPI通信失败,驱动层要返回错误码,而不是死等。我见过一个项目,ADC驱动里用了while循环等待转换完成,结果ADC芯片坏了,整个系统卡死。后来改成超时返回,问题就解决了。

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_ 前缀,而且每个函数只做一件事。比如 HAL_ADC_Init 只初始化ADC,HAL_ADC_Start 只启动转换。这样调试的时候,很容易定位问题。

2.5 层间交互机制:接口就是契约

四层架构搭好了,怎么让它们协同工作?答案是:接口。每一层只通过接口跟相邻层通信,不能跨层调用。

交互流程大概是这样的:

  1. 应用层调用服务层的接口获取数据或下发指令
  2. 服务层调用驱动层的接口操作硬件,或者对数据进行加工
  3. 驱动层调用HAL层的接口读写寄存器
  4. 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;

这样设计的好处是:接口清晰,依赖关系一目了然。而且单元测试的时候,可以轻松地替换掉底层实现,用模拟接口来测试应用层逻辑。

总结一下:分层架构的核心思想就是「各司其职、接口隔离」。应用层只管业务逻辑,服务层负责数据加工,驱动层操作硬件,HAL层屏蔽芯片差异。每一层都做好自己的事,别越界。这样做出来的BMS软件,可维护性、可移植性都会好很多。

我记得有一次,客户要求把MCU从STM32F103换成GD32F103。因为分层做得好,我只花了三天就完成了移植——重写了HAL层,驱动层改了少量代码,服务层和应用层一个字没动。这就是分层架构的价值。