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_cell 和 max_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 是在每次任务循环里实时计算的,而不是等外部事件触发。为什么?因为电压是动态变化的,尤其是大电流充放电时,压差会瞬间拉大。实时计算能保证均衡决策的准确性。
核心要点总结:
- 单体数据结构要预留扩展位,别写死
- 模组数据结构要包含「最值」字段,方便快速决策
- 均衡状态机必须有超时和异常处理机制
- 数据结构和状态机要联动,别各自为政
好了,这一章就到这里。数据结构设计看似基础,但真要在项目里用好,还是得靠经验积累。下一章咱们聊聊均衡策略的具体实现,到时候我会分享一些实际项目中的调参心得。