2、BMS软件架构设计原则:分层架构思想、模块化设计、高内聚低耦合、可移植性与可扩展性

做BMS软件这么多年,我最大的体会就是:架构设计决定了你能走多远。很多团队一开始图快,代码全塞在一个文件里,结果项目迭代到第三版就开始崩溃——改一个地方,炸一片功能。今天我就把BMS软件架构的几个核心原则掰开揉碎讲清楚。

2.1 分层架构思想:把复杂系统拆成“楼层”

分层架构,说白了就是给软件盖楼。每一层只关心自己的事,不越界。我习惯把BMS软件分成四层:

层级 职责 典型模块
应用层 业务逻辑、策略控制 SOC估算、均衡策略、故障诊断
中间层 算法封装、状态管理 卡尔曼滤波、状态机、数据融合
驱动层 硬件抽象、外设接口 ADC驱动、CAN驱动、GPIO封装
硬件层 物理芯片、传感器 AFE芯片、电流传感器、温度探头

为什么要这么分?我举个例子。有一次客户要求把AFE芯片从AD7280换成LTC6811。如果代码是分层写的,我只需要改驱动层,应用层的SOC算法完全不用动。但如果是“一锅粥”式的代码……嗯,那基本等于重写。

核心原则:上层依赖下层,但下层绝不能依赖上层。驱动层不知道什么叫“过充保护”,应用层也不关心寄存器地址是多少。

2.2 模块化设计:每个模块只做一件事

模块化设计,我理解就是“高内聚”——一个模块内部的功能要高度相关。比如电压采集模块,它只负责:

  • 配置ADC采样通道
  • 读取原始电压值
  • 做简单的滤波处理
  • 把结果通过接口抛出去

至于这个电压值是用来算SOC还是做均衡,电压模块一概不管。我在项目中遇到过有人把均衡逻辑直接塞进电压采集函数里,理由是“省一个函数调用开销”。结果后来均衡策略要改,电压采集也得跟着改,牵一发动全身。

我的习惯:每个模块的代码量控制在200-500行之间。超过500行,我就开始考虑要不要拆成子模块。你想想看,一个函数超过100行,读起来都费劲,更别说维护了。

2.3 高内聚低耦合:模块之间“少说话”

高内聚刚才说了,低耦合是什么意思?就是模块之间的依赖要尽可能少。我常用的手段是接口抽象

举个例子,BMS里有个“温度管理”模块,它需要知道当前电池温度。如果它直接调用“NTC温度传感器驱动”的函数,那耦合就太紧了。我建议的做法是:

/* 接口定义在 temperature_if.h */
typedef struct {
    int16_t (*get_temperature)(uint8_t sensor_id);
    uint8_t (*get_sensor_count)(void);
} Temperature_Interface_t;

/* 温度管理模块只依赖这个接口 */
void Thermal_Manager_Init(Temperature_Interface_t *iface);

这样一来,今天用NTC,明天换数字温度传感器,只要实现同样的接口,温度管理模块一行代码都不用改。这就是低耦合的好处。

避坑指南:我曾经在一个项目里看到全局变量满天飞——模块A直接写一个全局变量,模块B直接读。表面上看“效率高”,实际上调试起来简直噩梦。你根本不知道这个变量什么时候被谁改了。记住:能用接口传递的数据,绝不用全局变量

2.4 可移植性:换芯片不换逻辑

做BMS的都知道,芯片选型经常变。今天用STM32F4,明天可能换成GD32,后天又可能用NXP的S32K。如果代码和硬件绑得太死,每次换芯片都是大手术。

我实现可移植性的三板斧:

  1. 硬件抽象层(HAL):把所有芯片相关的操作封装在HAL里。比如延时函数,应用层调用 HAL_Delay(100),底层具体是用SysTick还是定时器,应用层不关心。
  2. 条件编译:用宏定义区分不同平台。比如:
#if defined(PLATFORM_STM32)
    #include "stm32f4xx_hal.h"
#elif defined(PLATFORM_GD32)
    #include "gd32f4xx.h"
#endif
  1. 避免直接使用裸机寄存器:我见过有人直接在应用层写 GPIOA->ODR |= 0x01。这种代码换芯片就废了。一定要封装成 GPIO_SetPin(GPIO_PIN_1, HIGH) 这样的函数。
经验之谈:可移植性不是一蹴而就的。我建议在项目初期就定好HAL接口规范,哪怕前期多花两天时间,后期能省下两周的移植时间。

2.5 可扩展性:给未来留个“接口”

BMS的功能是不断增加的。今天只有电压温度采集,明天要加绝缘检测,后天要加云平台通信。如果架构不支持扩展,每次加功能都要重构。

我常用的扩展手段是回调函数插件式架构。比如故障处理模块:

/* 注册故障处理回调 */
typedef void (*Fault_Handler_t)(Fault_Type_t fault, void *param);

void Fault_Register_Handler(Fault_Handler_t handler, void *param);

/* 当故障发生时,遍历所有注册的回调 */
void Fault_Process(Fault_Type_t fault) {
    for (int i = 0; i < g_handler_count; i++) {
        g_handlers[i](fault, g_params[i]);
    }
}

这样,以后要加“故障上报云平台”功能,只需要注册一个新的回调函数,原有代码完全不用动。这就是可扩展性。

小技巧:设计接口时,参数尽量用结构体指针而不是散落的参数。比如 void SOC_Update(SOC_Config_t *cfg),以后要加参数,只需要在结构体里加字段,接口签名不变,不影响已有调用方。

2.6 总结:这些原则怎么落地?

说了这么多,你可能觉得“道理我都懂,但实际做起来还是乱”。我建议你从一个小模块开始实践。比如先写一个“电压采集模块”,严格按照分层、模块化、接口抽象来做。跑通之后,再扩展到整个BMS系统。

记住:好的架构不是设计出来的,是迭代出来的。我做了十几年BMS,每个项目都在反思架构哪里可以改进。你不需要一步到位,但方向要对。

最后送大家一句话:架构设计省下的时间,永远比花掉的时间多。共勉。