3. 数据结构设计:电池单体、模组与均衡状态机

好,咱们进入正题。数据结构这东西,说白了就是软件的骨架。骨架搭歪了,后面填再多的肉也跑不顺。我在BMS项目里吃过不少亏,最惨的一次就是单体数据结构设计得太死板,结果换了一款电芯,整个代码都得重写。嗯,从那以后,我设计数据结构时都会多留几个心眼。

3.1 电池单体数据结构

单体是BMS的最小管理单元。每个电芯的电压、温度、内阻,都得有个地方存着。我个人习惯用结构体来封装,这样清晰,也方便扩展。

typedef struct {
    uint16_t    cell_id;            // 单体编号,从0开始
    uint16_t    voltage_mV;         // 电压,单位mV,范围0~5000
    int16_t     temperature_dC;     // 温度,单位0.1℃,范围-400~1250
    uint16_t    resistance_uOhm;    // 内阻,单位μΩ
    uint8_t     soc_percent;        // 荷电状态,0~100%
    uint8_t     soh_percent;        // 健康状态,0~100%
    uint8_t     status_flags;       // 状态标志位
    uint32_t    cycle_count;        // 循环次数
    uint32_t    timestamp_ms;       // 数据采集时间戳
} CellData_t;

这里我重点说说 status_flags 这个字段。它是个位域,用来标记单体的各种状态。比如:

  • Bit0: 电压过高标志
  • Bit1: 电压过低标志
  • Bit2: 温度过高标志
  • Bit3: 温度过低标志
  • Bit4: 内阻异常标志
  • Bit5: 均衡使能标志
我的小技巧: 用位域而不是多个布尔变量,能省不少RAM。在资源受限的MCU上,这招很管用。我曾经在一个项目里,光单体状态就省了200多字节。

3.2 电池模组数据结构

模组是多个单体的集合。通常一个模组包含8到16个电芯。模组数据结构要能反映整体状态,也要能快速定位到具体单体。

typedef struct {
    uint8_t     module_id;          // 模组编号
    uint8_t     cell_count;         // 包含的单体数量
    CellData_t  cells[MAX_CELLS_PER_MODULE]; // 单体数组
    uint16_t    total_voltage_mV;   // 模组总电压
    int16_t     avg_temperature_dC; // 平均温度
    uint16_t    min_voltage_mV;     // 最低单体电压
    uint16_t    max_voltage_mV;     // 最高单体电压
    uint16_t    voltage_diff_mV;    // 压差
    uint8_t     min_voltage_cell;   // 最低电压单体编号
    uint8_t     max_voltage_cell;   // 最高电压单体编号
    uint8_t     balance_status;     // 均衡状态
    uint16_t    balance_current_mA; // 均衡电流
} ModuleData_t;

你想想看,为什么要把 min_voltage_cellmax_voltage_cell 单独拎出来?

因为均衡算法需要快速知道「谁最低、谁最高」。每次遍历所有单体去找,太慢了。我在做实时系统时,这些字段都是在采集完数据后立即更新的,保证均衡决策的实时性。

3.3 均衡状态机定义

均衡不是一直开着的。什么时候均衡、均衡多久、什么时候停,都需要状态机来管理。我见过不少新手把均衡做成一个死循环,结果电池包过热保护了还不知道怎么回事。

下面是我常用的均衡状态机:

typedef enum {
    BALANCE_IDLE        = 0,    // 空闲状态
    BALANCE_START       = 1,    // 启动均衡
    BALANCE_ACTIVE      = 2,    // 均衡进行中
    BALANCE_PAUSE       = 3,    // 暂停均衡(温度过高或电压异常)
    BALANCE_COMPLETE    = 4,    // 均衡完成
    BALANCE_FAULT       = 5     // 均衡故障
} BalanceState_t;

状态切换的逻辑是这样的:

  • IDLE → START:当压差超过阈值(比如50mV),且温度在允许范围内
  • START → ACTIVE:均衡电路自检通过,开始放电或充电
  • ACTIVE → PAUSE:温度超过安全阈值,或单体电压异常
  • PAUSE → ACTIVE:异常解除,恢复均衡
  • ACTIVE → COMPLETE:压差小于目标值(比如10mV),或均衡时间到
  • 任何状态 → FAULT:硬件故障、通信超时等
避坑指南: 我曾经在PAUSE状态里忘记加超时退出机制。结果有一次温度传感器坏了,均衡一直卡在PAUSE状态,系统以为还在均衡,实际上已经停了。后来我加了个定时器,PAUSE超过30秒就强制进入FAULT状态。

3.4 数据结构与状态机的联动

光有数据结构和状态机还不够,得让它们配合起来。我一般会在主循环里这样处理:

void BalanceTask(void) {
    for (uint8_t i = 0; i < MODULE_COUNT; i++) {
        ModuleData_t *mod = &modules[i];
        
        // 计算压差
        mod->voltage_diff_mV = mod->max_voltage_mV - mod->min_voltage_mV;
        
        // 根据压差和温度决定均衡状态
        switch (mod->balance_status) {
            case BALANCE_IDLE:
                if (mod->voltage_diff_mV > BALANCE_START_THRESHOLD) {
                    mod->balance_status = BALANCE_START;
                }
                break;
            case BALANCE_START:
                // 启动均衡电路
                if (BalanceHardwareInit(mod) == SUCCESS) {
                    mod->balance_status = BALANCE_ACTIVE;
                } else {
                    mod->balance_status = BALANCE_FAULT;
                }
                break;
            // ... 其他状态处理
        }
    }
}

这里有个细节:voltage_diff_mV 是在每次任务循环里实时计算的,而不是等外部事件触发。为什么?因为电压是动态变化的,尤其是大电流充放电时,压差会瞬间拉大。实时计算能保证均衡决策的准确性。

核心要点总结:

  1. 单体数据结构要预留扩展位,别写死
  2. 模组数据结构要包含「最值」字段,方便快速决策
  3. 均衡状态机必须有超时和异常处理机制
  4. 数据结构和状态机要联动,别各自为政

好了,这一章就到这里。数据结构设计看似基础,但真要在项目里用好,还是得靠经验积累。下一章咱们聊聊均衡策略的具体实现,到时候我会分享一些实际项目中的调参心得。