电池管理系统的多板卡协作
一套由管理主控、单体采集单元、总压总流采集单元三部分组成的管理系统。 单体数量从几十节到几百节,全部数据汇总到主控的一张寄存器表里, 上位机通过以太网读取、通过无线模块上云。 采集单元与总压总流单元都能通过总线升级固件。 这篇记录的是方案设计与已经踩过的坑,也包括几处我至今认为没做好的地方。
一、这套系统由哪几块板组成
整个系统分三层,各板卡的职责边界划得很清楚:
| 板卡 | 主要职责 | 上行接口 | 下行接口 |
|---|---|---|---|
| 管理主控 | 汇总全部单体数据、状态估算、报警分级、均衡决策、绝缘检测调度、数据上云 | 以太网(对上位机)+ 无线模块(上云) | 两路 RS485(对采集单元)+ CAN |
| 单体采集单元 | 单体的电压、温度、内阻测量,执行均衡动作 | RS485 | —(直接连到单体) |
| 总压总流单元 | 整包总电压、总电流、绝缘电阻测量 | RS485 | —(直接连到高压侧采样) |
三块板的共同点是都挂在同一条 RS485 总线上,都用同一套参数存储结构、同一套总线升级方案。 这一点很重要——把“参数结构 + 升级方式”统一之后,产线和售后只需要会一套操作。
一个不太直观的地方是:采集单元本身不做任何决策。 它只负责“测准”和“执行”,均衡策略、报警阈值全部由主控下发。 这样做的代价是主控的计算量大一些、通信更频繁, 好处是策略调整不需要给几百块采集单元逐个升级—— 这一点在后期反复修改均衡算法时体现得非常明显。
二、采集单元:把三类量测准
采集单元要测三样东西,难度差别很大。
2.1 单体电压:靠两点标定覆盖批次差异
电压通道用多通道 ADC 加 DMA 自动扫描,每轮采多次取平均来压噪声。 真正需要设计的是标定:同一批板子的分压电阻有误差, 不做标定的话几百节之间的读数会有系统性差异,而均衡决策恰恰依赖“节与节之间比”。
我用的是两点线性标定:在产测时加两个已知电压, 反算出“斜率”与“偏移”两个系数并保存。这里有一个必须提前算的限制—— 系数要以整数保存并放大若干倍,放大倍数与系数的取值范围必须一起设计, 否则斜率稍大就会溢出。我当时是踩到之后才在文档里加了一句“斜率不允许超过某个上限”, 这是典型的“先写代码、后补约束”。更稳的做法是在产测环节就校验系数范围并拒绝异常值。
两点标定还有一个额外好处:不同硬件量程的版本可以用同一套代码, 量程差异全部体现在标定系数里,切换版本只需要改一个编译宏告诉固件“这一版是哪档量程”。
2.2 温度:先校正上臂电阻,再算阻值,最后算温度
温度用负温度系数热敏电阻,换算是一条三级链: 多次采样取平均 → 校正测温上臂电阻 → 求热敏电阻阻值 → 用阻值和两个器件参数算温度。
这里的两个器件参数(25 ℃ 标称阻值与 B 值)是每个型号不同的, 所以它们必须放在参数区里、可被产线写入,而不能写死在代码里。 我见过参数没写进去而固件里也没给默认值的情况——设备能开机、能通信, 只有温度是错的,这种问题最难发现。
2.3 内阻:需要“开路”和“带载”两组数据
内阻不能直接测,要靠“同一个单体在开路状态和带载状态下的电压差”间接算出来, 所以采集单元要分别保存两组电压序列,再由主控触发一次内阻测量、把结果收上去。 这一项对时序有要求,因此它在主控的轮询调度里是优先级最高的动作(见第五节)。
2.4 低功耗:不通信就关外设
采集单元数量多、长期在线,所以做了低功耗:不接收通信电文时关闭非必要外设的电源, 只保留通信相关的外设;收到电文后执行动作,动作完成后一段时间内无操作就自动进入休眠。 配合定时唤醒喂狗,避免休眠期间看门狗把设备复位。
三、总压总流单元:16 位 ADC 与“分级恢复”
总压总流单元的量程比单体大得多,所以用了外置的 16 位 ΔΣ 型 ADC,通过 I²C 访问。 采样后做软件平均滤波——这里有一个必须注意的取舍:平均次数越多越稳,但越慢, 所以次数要按“要求的更新率”倒推,而不是越多越好。
3.1 一个典型的 ΔΣ ADC 时序坑
原因:这类 ADC 写配置寄存器之后需要等待一次转换完成才能读结果, 而转换时间由采样率设置决定。配置完立刻读,读到的必然是上一次的结果。
办法:把驱动改成“配置 → 等待一段与采样率匹配的时间 → 再读”, 并把这段等待时间与采样率设置联动起来,而不是写一个固定值。
3.2 低功耗:500 ms 空闲就休眠
这块板子的低功耗闭环是:空闲超过一定时间进入休眠 → 串口有活动时唤醒 → 处理完再回到休眠。其中有两个细节值得记下来:
- 判帧与唤醒复用同一套逻辑。 没有依赖硬件空闲中断,而是用“接收长度连续若干毫秒不再变化”来判定一帧结束, 同时把这个事件当作唤醒信号。这样唤醒和收帧不需要两套机制。
- 休眠期间也要喂狗。 用一个定时器周期性置一个标志位,休眠的等待循环里检查这个标志, 置位就刷新看门狗。这样 CPU 仍然能停在低功耗等待指令上,只是每次唤醒顺手喂一次。
3.3 故障恢复:先加重启,再把它删掉
这块板子的故障恢复策略演进,是我觉得最有价值的一段过程:
- 第一版:检测到通信或采集异常就整机复位。想法是“复位一下就好了”。
- 发现问题:太激进。偶发的单次错误本来能在本地消化,一整机复位就把正在做的事打断了, 用户看到的是“设备莫名其妙重启”。
- 第二版(分级重初始化): 采集错误 → 只重新初始化那颗 ADC 芯片; 通信错误 → 累计若干次才重新初始化串口一次,不是每次错误都动; “连续多次错误就整机复位”的分支被注释掉,只保留打印。
这里有两个可以推广的原则:恢复动作要按层级来分,能用轻的手段解决就别用重的; 以及重初始化本身也要限流——如果每次错误都重新初始化外设, 在持续干扰的现场会变成“不停初始化”,通信永远建立不起来。
3.4 无效值必须有一个明确的表示
采集失败时,不能随便写一个“看起来正常”的数。我用的是哨兵值:
电压类写全 0xFF,电流类写一个物理量程之外的负值。
并且规定连续失败若干次才置故障标志,期间只写哨兵值、不影响其他逻辑,一旦成功立刻清零计数。
这样上位机看到哨兵值就知道“这一路当前没有有效数据”, 而不是把某个恰好等于 0 的读数当成真实测量结果。
四、管理主控:一张几千个寄存器的变量表
这套系统里最有价值的一个设计决定,是把全部数据映射成一张连续的寄存器表, 并让两个结构体指针零拷贝地指向它:前半段是“参数区”,后半段是“状态区”。
| 区域 | 内容 | 特点 |
|---|---|---|
| 参数区 | 各路通信参数、网络地址、单体数量、额定容量、均衡参数、各单体均衡配置、参数校验字 | 可读写;带校验字;可通过特殊命令保存、恢复默认、重载 |
| 状态区 | 整包状态(SOC / SOH / 总压 / 总流 / 绝缘电阻)、极值与极值编号、 每节单体的电压 / 温度 / 内阻 / 运行状态 / 报警位、整包报警位、 累计安时与千瓦时、继电器调试、运行时间 | 只读为主;单体相关字段按“每节一个数组”排布 |
这样做的收益很直接:上位机不需要懂内部结构,只需要按地址读寄存器。 而且因为寄存器地址就是文档,任何一个人拿到地址表就能写上位机。
五、轮询调度:把三类动作排进一个状态机
主控是 Modbus 主机,要轮询的对象有两类:单体采集单元和总压总流单元。 但轮询之外还有两种“要紧的事”会插进来:内阻测量和均衡广播。 三者都占用总线,必须串行化。
我用的是一个调度状态机,优先级从高到低:
- 内阻测量——对时序要求最高,一旦被触发就优先执行;
- 均衡广播——按设定的间隔发起,间隔到达才执行;
- 常规数据采集——前两者都不需要时的默认动作。
均衡的间隔计时有个小技巧:用“分钟数发生变化”来计时, 而不是自己累加毫秒。这样不需要额外的定时器,也不会因为轮询耗时不均而累积误差。
另一个后来补上的重要改动:把通信时序参数全部外提成可配置寄存器 (发送超时、接收超时、单播发送/接收间隔、广播间隔、读总压总流的间隔)。 原因是这些值在实验室和现场差别很大,写死在固件里意味着每换一个现场就要重新出一版固件。 外提之后,现场用上位机改几个寄存器就能适配。
六、报警分级与回差
单体电压、温度、内阻、整包总压、总流、SOC、SOH 这些量都需要报警, 而“报警”这件事如果只用一个阈值,现场会非常难受:测量值在阈值附近抖动时, 报警会反复出现和消失。
我的做法是每一项都配三个等级的阈值加一个回差:
| 要素 | 作用 |
|---|---|
| 一级阈值 | 提示级:记录并上报,不限制动作 |
| 二级阈值 | 告警级:上报并限制部分动作(如限制充电电流) |
| 三级阈值 | 保护级:上报并禁止动作(如断开充放电) |
| 回差 | 解除报警需要回到阈值以内一段距离,避免临界抖动 |
单体的报警信息用一个位域保存:通信状态占一位(在线/掉线), 电压过高、电压过低、温度过高、温度过低、内阻过高各占两位(表示三级)。 这样一节单体的全部报警状态只用一个整数就能表达,几百节排在一起也只占很小的空间, 而且上位机能一次读一大片、自己按位解析。
另外还有一个“运行状态”位,单独表示这一节是否正在被均衡。 把它和报警分开是有意的:均衡中不是异常,混在报警位里会让上位机的判断变复杂。
七、主动均衡:基准怎么选,同组为什么不能重复
均衡要解决的核心问题是:以谁为基准,把哪些节挑出来。 这个“基准”的选法直接决定策略的性格。
| 基准选法 | 做法 | 性格 |
|---|---|---|
| 众数 | 统计各电压档出现次数,取出现最多的那一档;并列时取偏高的那个 | 跟随“大多数”的单体,抗个别异常值能力强;代价是需要统计、计算量稍大 |
| 中值 | 排序后取中间值 | 更稳健、更简单;但在电压分布明显偏斜时代表性差一些 |
两种都实现了,由参数选择,实际使用中按电池组的离散程度决定用哪个。
7.1 同组内不能重复选中同一节
这是我在调均衡时踩到的一个实际问题:均衡是分组进行的(每组若干节), 如果同一组里某一节被同时当作“需要充电”和“需要放电”的对象, 两个动作会互相抵消——电能白耗,电池状态毫无改善, 而且在数据上看不出来,只表现为“均衡效果不好”。
解决办法是在挑选环节加一个唯一性约束:同一组内被选中的单体不能重复。 加上之后均衡效果才明显起来。这件事让我意识到: 算法类的功能,“看起来在跑”和“确实有效”之间往往差着一个约束条件。
7.2 开始差值与停止回差要分开
另一个必要的设计是:“开始均衡”的差值和“停止均衡”的差值不能是同一个数。 如果相同,电压刚好在边界时均衡会反复启停。 这一点和第六节的报警回差是同一个道理,凡是“带反馈的判定”都需要回差。
均衡还支持限定次数(用完就停)或不限次数(持续进行), 以及“均衡指令的间隔时间”——间隔太短会让总线长期被均衡广播占用,挤掉数据采集。
八、状态估算:安时积分踩的坑
荷电状态用安时积分:按额定容量与健康度算出可用容量, 再根据当前荷电状态换算出需要累计的安时上下限,然后对电流做积分。 健康度按万分之一为单位保存。
原因:这条链上有多层换算——荷电状态用的是万分之一, 安时用的是另一种比例,千瓦时又要在安时基础上再乘电压; 每一层的比例系数与零点处理稍有不一致,误差就会逐层放大, 而且因为“值一直在涨”,看起来像是正常的累积,不容易被当成 bug。
办法:把累积相关的换算收敛到一处,统一比例与零点, 并且用“放一段时间已知电流”的方式做一次人工对照验证。
结论:凡是多层换算再加上长期累积的功能,都必须有一个“人工可验证的对照点”。 否则它错了你也看不出来。
另外累计值需要掉电保持(累计充放安时与千瓦时), 这几个量写存储的频度要控制——按另一篇笔记里算过的账, 片上 Flash 的擦写次数是有限资源,频繁写会很快写坏。
九、通信:三套协议在一台设备上共存
主控同时跑三套通信,分工很清楚:
| 通道 | 协议 | 角色 | 用途 |
|---|---|---|---|
| 以太网 | Modbus TCP(服务端) | 被动等待连接 | 上位机读写变量表 |
| 两路 RS485 | Modbus RTU(主机) | 主动轮询 | 采集单元与总压总流单元 |
| 无线模块 | AT 指令 + JSON | 主动上报 | 数据上云与网络对时 |
9.1 以太网只用了 TCP
这个工程里网络部分把 UDP 整个关掉了,只保留 TCP。 原因是数据上云要求可靠传输,而变量表读写是“请求—应答”式的,用 TCP 更省心。 同一个开发者在另一个项目里则相反——板间控制用 UDP、状态上报用 TCP。 这说明“用 TCP 还是 UDP”没有统一答案,取决于这条链路是“要可靠”还是“要快”。
9.2 一个必须显式设置的超时
原因:服务端在等待新连接或收发数据时没有设置接收超时, 对端异常消失(掉电、拔网线)时本端没有任何机制察觉到。
办法:给新连接显式设置一个接收超时, 超时后主动关闭该连接并回到“等待新连接”的状态。
这条经验很通用:凡是被动等待的地方,都要有一个“多久没动静就重来”的机制。
9.3 无线模块:AT 响应和下行命令共用一个串口
无线模块这边,串口既要收模块的 AT 响应,又要收云端下发的命令。 做法是先尝试按“时间响应”解析,解析不成功就把这一帧当作命令帧处理。 这个设计不优雅但很实用——省掉了一个“当前该收什么”的状态机。
模块返回的时间字符串会同时写进实时时钟芯片和系统的 Unix 时间,并做时区偏移。 这里有个值得记的细节:时区偏移不要在多处重复加。 代码注释里明确写着“不主动加偏移,防止手动校时之后服务器再校时一次,导致差出时区那么多”—— 这是一个被真实踩过之后才补上的注释。
十、问题与解决(现象 → 原因 → 办法)
| 现象 | 原因 | 办法 |
|---|---|---|
| 安时积累积值不对 | 多层换算(万分之一荷电状态 → 安时 → 千瓦时)的比例与零点不一致,误差逐层放大 | 把累积换算收敛到一处,统一比例与零点,并用已知电流做人工对照 |
| 整包总电流读数异常 | 总流来自总压总流单元,链路与换算环节多,出错后难以定位是哪一段 | 按链路分段核对(采样 → 换算 → 传输 → 显示),先定位段再定位点 |
| 上位机断开后无法重连 | 服务端接收没有超时,感知不到对端消失 | 给连接设置接收超时,超时后关闭并回到等待状态 |
| 均衡效果不明显 | 同一组内出现互相抵消的选中对象;基准电压选法不适合当时的离散程度 | 加同组唯一性约束;提供众数/中值两种基准并可按现场选择 |
| 均衡与采集互相干扰 | 两者共用总线,同时发起会争用 | 用调度状态机把内阻测量、均衡广播、常规采集串行化并定优先级 |
| 现场适配要重新出固件 | 通信时序参数写死在固件里 | 把时序参数外提成可配置寄存器,现场用上位机就能改 |
| 温补/传感器参数缺失导致读数错 | 参数未写且固件没给默认值,能开机、能通信,只有那一路不对 | 每个参数都要有明确默认值,并在加载后做范围检查 |
十一、局限与还没做完的部分
- 主控里残留了调试代码。当前代码在主任务开头插了一段用于调试的循环, 后面的部分业务逻辑在运行时不会被执行。这是我在整理时才发现的问题—— 也正因为如此,我现在把“检索调试残留”写进了发布前检查表 (见《从样机走到量产》)。
- 健康度的估算还很粗糙。目前主要靠一次标定流程来修正, 没有做长期的内阻趋势跟踪,也没有考虑温度对可用容量的影响。
- 均衡策略只做到“分组去重 + 双阈值”。 没有考虑各节的荷电状态差异、没有做整体能量最优的分配, 本质上还是一个“挑偏离最多的几节”的贪心做法。
- 绝缘检测是定时触发的。 目前按“每分钟里的固定几秒”分三阶段执行,没有和整机运行状态联动 (例如充电中是否应该更频繁地检测)。
- 参数区没有 CRC 之外的保护。 有校验字,但没有做双区备份与版本迁移,结构体一旦变更就需要人工处理老参数。
- 单体报警只做到“上报”。 主控侧的故障汇总做了,但顶层状态机对故障的处理仍然很简单。
- 没有做长期数据记录。 整包的历史曲线依赖上云通道,本地没有留存; 一旦网络中断,那一段的数据就没有了。
参考资料与说明
- Modbus 应用协议规范中关于 RTU 与 TCP 两种传输模式、功能码与异常码的定义。
- 所用的 16 位 ΔΣ 型 ADC、负温度系数热敏电阻与实时时钟芯片的公开数据手册。
- FreeRTOS 与 LwIP 官方文档中关于任务、内存配置与 socket 超时的说明。
- 本文涉及的平台与系统规模均为个人学习项目中的实际配置,其中产品特定的 系数、阈值、时序与地址均未公开;文中示例与表格用于说明方法,不代表任何产品或交付代码。
- 出于对个人项目实现细节的保护,本文只公开方法与思路; 涉及核心实现的部分以步骤与字段说明代替代码。
- 文中芯片与器件型号仅用于说明技术方案,与相关厂商无隶属或授权关系。
- 本文内容为个人技术学习记录,不构成任何电池设计、储能系统或安全相关的建议; 电池系统涉及高压与安全风险,任何实际操作应遵循相关标准与专业指导。