乐于分享
好东西不私藏

FoxBMS2 源码分析 (7) | 安全与诊断:电池管理系统的安全保障

FoxBMS2 源码分析 (7) | 安全与诊断:电池管理系统的安全保障
项目地址:https://www.github.com/foxBMS/foxbms-2
1. 前言:安全系统最重要的能力是不轻信
前面几篇我们已经看到:
  • 测量链会把电压、温度、反馈写进数据库。
  • 状态机会根据请求和诊断结果决定是否预充、闭合接触器或退出运行。
  • 算法模块会持续给出 SOC 与均衡控制建议。
但一个成熟的 BMS 不能默认这些结果永远可信,FoxBMS2 在这方面做得很彻底,它专门用几层不同性质的检查,把异常输入逐步转成系统级故障:
  1. Plausibility:同一类测量值之间是否自洽。
  2. Redundancy:不同测量链路是否一致。
  3. SOA:即便没有越绝对阈值,当前工作点是否仍在安全工作区。
  4. DIAG:统一管理阈值、防抖、回调、fatal error 和对外广播。

1.1 先用一张图看清谁发现、谁定性、谁执行退出

2. Plausibility

2.1 它不是独立调度的大模块,而是一组被调用的检查函数

如果只看文件名,很容易以为 plausibility.c 自己就是一条独立安全链,但当前仓库里更准确的理解是:
  • plausibility.c 提供 PL_CheckStringVoltage()、PL_CheckCellVoltage()、PL_CheckCellTemperature()、PL_CheckVoltageSpread()、PL_CheckTemperatureSpread() 这些检查函数。
  • 它们主要在 redundancy.c 的校验路径里被调用。
  • 检查结果会立刻通过 DIAG_CheckEvent() 进入 DIAG,而不是停留在本地模块内部。
所以从执行关系看,Plausibility 更像被 Redundancy 和测量校验链调用的一组判断器,而不是一个单独周期任务。

2.2 当前实现里它主要做 4 类事

从源码能直接确认的 Plausibility 角色,主要有下面 4 类:
  1. 比较 AFE 簇电压和电流传感器高压测量是否接近。
  2. 比较 base / redundancy0 两份 cell 电压是否一致;如果一致,输出两者平均值。
  3. 比较 base / redundancy0 两份 cell 温度是否一致;如果一致,输出两者平均值。
  4. 在已生成的有效 cell 表上,检查单体值与簇平均值的偏差是否超过 spread 容差。
这四类检查的配置分别来自 plausibility_cfg.h,例如:
  • PL_STRING_VOLTAGE_TOLERANCE_mV = 3000
  • PL_CELL_VOLTAGE_TOLERANCE_mV = 10
  • PL_CELL_TEMPERATURE_TOLERANCE_dK = 50
  • PL_CELL_VOLTAGE_SPREAD_TOLERANCE_mV = 300
  • PL_CELL_TEMPERATURE_SPREAD_TOLERANCE_dK = 100

2.3 Spread 检查不只是报错,还会把条目直接判成无效

这一点很值得单独强调。PL_CheckVoltageSpread() 和 PL_CheckTemperatureSpread() 不只是报个 warning 然后继续用原值,而是会:
  • 把偏离平均值过大的 cell 项直接标成 invalid;
  • 然后由 Redundancy 重新计算 min / max / average;
  • 同时通过 DIAG_CheckEvent() 更新对应的 Plausibility 诊断项。
所以这里的 Plausibility 会直接影响后续哪些测量值还能继续参与系统判断。

3. Redundancy

3.1 Cell 冗余校验先查新鲜度,再决定是否比较

MRC_ValidateCellVoltageMeasurement() 和 MRC_ValidateCellTemperatureMeasurement() 的入口逻辑很清楚:
  • 先用 DATA_DatabaseEntryUpdatedAtLeastOnce() 判断 redundancy0 这条链是不是启用过;
  • 再用 DATA_EntryUpdatedWithinInterval() 判断 base / redundancy0 是否超时;
  • 再比较当前时间戳和上次处理时间戳,确认这次是否真的拿到了新样本。
只有当 base 和 redundancy0 两边都更新过时,才会进入真正的双路比对。

3.2 如果只有一边更新,且另一边已超时,模块会降级使用单路值

这也是这套实现最像工程系统、而不是教科书伪代码的地方。
当前代码不是只要做不了双路比较就直接失败,而是:
  • 如果只有 base 更新,而 redundancy0 已超时,就用 base 更新 validated 表;
  • 如果只有 redundancy0 更新,而 base 已超时,就用 redundancy0 更新 validated 表;
  • 同时把对应 timeout 诊断项送进 DIAG。
这说明 Redundancy 模块真正维护的是当前哪些值还能可信地继续维持系统运行,而不是执着于两路必须永远同时存在。

3.3 Pack 级校验把 Plausibility、降级和重建混在了一起

MRC_ValidatePackMeasurement() 之后的 pack 校验更能体现这点:
  • MRC_ValidateCurrentMeasurement() 先检查电流测量是否及时、是否有效;
  • MRC_ValidateStringVoltageMeasurement() 再把 AFE 簇电压与 current sensor 高压测量做 Plausibility 检查;
  • 如果二者都不够完整,代码还会基于有效 cell 数和平均 cell 电压重建近似簇电压;
  • 只有当无效单体数超过 MRC_ALLOWED_NUMBER_OF_INVALID_CELL_VOLTAGES 时,才把簇电压标成 invalid。
所以这里的 Redundancy 不是比较两个值然后二选一,而是一条包含 freshness、Plausibility、降级与估算重建的完整链路。

3.4 这条链的真实任务上下文在 10ms 线程里,每 50ms 触发一次

ftask_cfg.c 里可以直接看到:
  • Redundancy 在初始化阶段先执行 MRC_Initialize();
  • 运行期由 10ms 周期任务驱动;
  • 其中 MRC_ValidateAfeMeasurement() 和 MRC_ValidatePackMeasurement() 每经过 5 个 10ms 周期执行一次,也就是 50ms 一次。

4. SOA:把当前运行点翻译成诊断事件

4.1 当前实现里,SOA 是 BMS 10ms 的一部分

BMS_Trigger() 在运行态每次都会按顺序做下面几件事:
  1. BMS_GetMeasurementValues()
  2. BMS_UpdateBatterySystemState()
  3. SOA_CheckVoltages()
  4. SOA_CheckTemperatures()
  5. SOA_CheckCurrent()
  6. SOA_CheckSlaveTemperatures()
也就是说,SOA 当前不是一个独立线程,而是直接嵌在 BMS 主状态机 10ms 路径里的系统约束检查层。

4.2 电压和温度检查是分层阈值链,不是单一阈值 if

SOA_CheckVoltages() 和 SOA_CheckTemperatures() 都是典型的分层告警结构:
  • 先看 MOL
  • 再看 RSL
  • 最后看 MSL
  • 恢复时也按相反方向逐级清除
这些阈值来自 battery_cell_cfg.h。例如当前配置里:
  • BC_VOLTAGE_MAX_MOL_mV = 2720
  • BC_VOLTAGE_MIN_MSL_mV = 1500
  • BC_VOLTAGE_DEEP_DISCHARGE_mV = BC_VOLTAGE_MIN_MSL_mV
  • BC_TEMPERATURE_MAX_DISCHARGE_MSL_ddegC = 550
所以 SOA 的工作不是帮你算一个安全分数,而是把当前最值和工作方向翻译成一组层次化的 DIAG 事件。

4.3 Deep Discharge 是带持久化语义的闭环

这一点在当前版本里是能完全闭环到源码的:
  • 当欠压已经进入 DIAG_ID_CELL_VOLTAGE_UNDERVOLTAGE_MSL,且最低电压继续低于 BC_VOLTAGE_DEEP_DISCHARGE_mV 时,SOA_CheckVoltages() 会触发 DIAG_ID_DEEP_DISCHARGE_DETECTED;
  • DIAG_ErrorDeepDischarge() 收到 DIAG_EVENT_NOT_OK 后,会把 fram_deepDischargeFlags.deepDischargeFlag[stringNumber] 置位并写入 FRAM_BLOCK_ID_DEEP_DISCHARGE_FLAG;
  • 下次上电时,SYS_FSM_CHECK_DEEP_DISCHARGE 会读回这个 FRAM 块,并重新把该诊断项送进 DIAG。

4.4 current on open string 体现的是状态与物理路径的一致性检查

SOA_IsCurrentOnOpenString() 的判断依据也很直接:
  • 只要检测到存在充电或放电方向;
  • 且当前 string 既不是 closed,也不是 precharging;
  • 那么就认定这是 open string 上出现了不应有的电流。
这类问题很难只靠单一测量值表达,但放在 SOA 层就很自然,因为它本质上是在比对系统状态和物理电流是否一致。

5.diag.c:前面三层只是发现问题,真正组织故障语义的是它

5.1 所有异常最终都要归一到DIAG_Handler()

FoxBMS 中各种模块不会自己去随意改全局错误位,而是统一通过:
DIAG_Handler(diagId, DIAG_EVENT_NOT_OK, data)
或:
DIAG_Handler(diagId, DIAG_EVENT_OK, data)
进入诊断框架,这让检测故障和决定故障是否真正激活被严格分开。

5.2 DIAG_EVENT_NOT_OK的阈值语义比表面看更细

源码里,收到 DIAG_EVENT_NOT_OK 后的处理不是第一次异常就 active。逻辑是:
1. occurrenceCounter 先递增
2. 只有当计数走到阈值时,才首次:
  • 置 active flag
  • 记录 error
  • 触发 callback
  • 必要时上报 fatal error
而 DIAG_GetDiagnosisEntryState() 的 active 判定又是基于 occurrenceCounter > threshold
这个细节很重要,因为它决定了:
  • 诊断项并不是一出现一次异常就对系统生效。
  • 要理解某个故障何时真正进入 active,不能只看调用次数,还要看 threshold 语义。
这里还有一个容易忽略的配置细节:diag_cfg.h 里 DIAG_SEN_EVENT_1 = 0DIAG_SEN_EVENT_10 = 9DIAG_SEN_EVENT_20 = 19。也就是说,配置表里看见第 10 次生效,在实现层其实是counter 先累到 9,再在下一次 NOT_OK 时越过阈值并置 active。

5.3 DIAG_EVENT_OK也采用了对称的渐退逻辑

恢复路径同样不是收到一次 OK 就立刻清故障。源码会逐步递减 occurrenceCounter,只有减回 0,才会:
  • 清除 error flag
  • 清除 fatal error
  • 调用 reset callback
这让故障恢复也具备防抖能力。

5.4 关键链路其实是检测模块 -> DIAG -> callback / fatal -> BMS

6. Fatal Error 不是一次性事件,而是周期性维持的系统状态

6.1 diag.c 会创建 fatal error resend timer

初始化阶段,诊断模块会建立一个周期性重发机制,周期是 100ms。这意味着 fatal error 一旦激活后,不是发一次 CAN 就结束,而是会持续重发相关信息。
这背后是很现实的通信考虑:
  • 故障发生瞬间,上位机可能还没准备好接收。
  • 总线可能有丢帧。
  • 后接入的节点也需要感知当前处于 fatal error。

6.2 BMS 的错误延时就是建立在这层诊断上的

上一篇提到 BMS 会遍历所有 fatal diagnosis entry,并根据 DIAG_GetDelay() 选择最短延时。也就是说:
  • 前面 Plausibility / Redundancy / SOA 发现异常
  • diag.c 决定异常何时真正 active、是否 fatal
  • BMS 再根据 fatal error 的延时策略决定何时切断高压
这是一条很完整的故障处理链。
更具体一点说,当前实现里:
  • DIAG_SetFatalErrorById() 首次发现 fatal 后会立即发一次 fatal CAN,并在第一个 fatal 激活时启动 100ms 重发 timer;
  • DIAG_ResendFatalErrors() 在 timer 回调里遍历所有 active fatal 项,逐个重发;
  • BMS_IsAnyFatalErrorFlagSet() 会遍历 fatal 链接表,取所有 active fatal 项中最短的 delay;
  • BMS_IsBatterySystemStateOkay() 再根据这个最短 delay 倒计时,直到真正允许状态机进入错误态。

7. 这一套设计真正解决了什么问题

把四层放在一起看,FoxBMS 的安全链条其实非常清楚:
  1. Plausibility 负责发现数据不像真的。
  2. Redundancy 负责发现另一条链路对不上。
  3. SOA 负责发现当前运行点本身不应该继续维持。
  4. DIAG 负责把这些离散异常整理成带阈值、带回调、带 fatal 语义的系统故障。
这让 FoxBMS 不是简单地看到异常就停机,而是能区分:
  • 毛刺
  • 持续异常
  • 可恢复异常
  • 需要延时受控退出的 fatal error
  • 需要跨上电周期保留的安全事实

小结

这一篇真正的核心不是列举了多少个诊断模块,而是建立了一条完整认识:FoxBMS 把检测异常和决定异常何时真正影响系统行为分成了不同层次。
这也是它相比很多简单 BMS 项目更成熟的地方。后者往往是某个 if 条件一满足就直接断开,而 FoxBMS 明显更强调故障语义的整理和时序控制。
下一篇收尾,我们把这些状态、请求和错误放回到 CAN 和工具链上下文里,看系统如何与外部世界交互,以及工程团队如何维护这些通信定义。

推荐阅读顺序

  1. 先看 ftask_cfg.c 里的 10ms 路径,确认 MRC_ValidateAfeMeasurement()、MRC_ValidatePackMeasurement() 和 BMS_Trigger() 分别挂在哪个节拍上。
  2. 再读 redundancy.c,把 freshness 判断、单路降级和 pack 级 Plausibility 路径串起来。
  3. 然后回到 plausibility.c,看每个 PL_Check... 函数到底在比什么、改什么。
  4. 接着看 bms.c 里的 BMS_Trigger() 和 soa.c,理解 SOA 是怎样嵌入 BMS 主链的。
  5. 最后读 diag.c、diag_cfg.c、diag_cbs_deep-discharge.c 和 sys.c,把 threshold、生效、持久化和延时退出闭环起来。