乐于分享
好东西不私藏

FoxBMS2 源码分析 (4) | 状态机与控制流

FoxBMS2 源码分析 (4) | 状态机与控制流

1. 前言

理解完任务调度和数据库之后,接下来的问题自然就是:
  • 谁在决定系统什么时候算初始化完成?
  • 谁在决定什么时候开始预充?
  • 谁在发现错误后决定延时断开还是立刻断开?
在 FoxBMS2 里,这些职责不是一个状态机全包,而是分成两层:
  1. SYS 状态机:负责 ECU 级别的启动、测量首轮闭环、各功能模块初始化联动。
  2. BMS 状态机:负责电池系统级业务状态,包括待机、预充、运行、开断接触器和错误退出。
这两层分工很清晰:SYS 负责系统能不能开始工作,BMS 负责高压该怎么工作
这张图最值得记住的是下面两点:
  • SYS 和 BMS 都在同一条周期控制路径里,但SYS 负责把 BMS 拉出未初始化态
  • 外部请求和 fatal diagnosis 都不会直接去动接触器,而是分别通过数据库读取和诊断聚合进入 BMS 的决策链。

2. SYS 状态机

2.1 先看状态变量本身

sys.c 中的 sys_state 包含:
  • currentState / currentSubstate
  • nextState / nextSubstate
  • previousState / previousSubstate
  • timer
  • stateRequest
  • illegalRequestsCounter
  • initializationTimeout
  • triggerEntry
其中有两个字段非常关键:
1. initializationTimeout:用来为各个初始化步骤做超时保护。
2. triggerEntry:配合 SYS_CheckMultipleCalls() 做重入保护。也就是说,FoxBMS 明确假设 SYS_Trigger() 不能并发重入。

2.2 顶层状态并不复杂,但每个状态下面的子流程很长

顶层状态主要是:
  • UNINITIALIZED
  • INITIALIZATION
  • PRE_RUNNING
  • RUNNING
  • ERROR
真正复杂的地方不在顶层状态数量,而在 INITIALIZATION 和 PRE_RUNNING 这两段子状态链。

2.3 INITIALIZATION 阶段:先确认系统有没有历史问题

SYS_ProcessInitializationState() 的第一步不是启动外设,而是:
FRAM_ReadData(FRAM_BLOCK_ID_DEEP_DISCHARGE_FLAG);
然后遍历 fram_deepDischargeFlags,如果某个 string 之前已经触发过深度放电标记,就直接通过 DIAG_Handler() 报出来。
这一步很有意思,因为它说明 SYS 初始化并不只关心现在能不能启动,还关心上一次运行有没有留下故障

2.4 然后才是 SBC、CAN、RTC、自检和 Boot Message

INITIALIZATION 阶段的顺序大致是:
  1. 请求 SBC 初始化并等待它进入 SBC_STATEMACHINE_RUNNING
  2. 初始化 CAN
  3. 等 RTC 模块准备好
  4. 执行启动期 BIST:
  • SYS_GeneralMacroBist()
  • DATA_ExecuteDataBist()
5. 发送 Boot Message
6. 转入 PRE_RUNNING
这里最值得注意的是两点:
  • SBC 初始化失败是直接可导致 SYS 进入 ERROR 的。这说明硬件安全基础设施在系统级启动中优先级极高。
  • 数据库自检在这里完成。这进一步印证了前一篇的结论:Database 不只是工具模块,而是系统级依赖。

3.PRE_RUNNING:系统拉起流程的地方

3.1 先初始化 Interlock,再初始化 Balancing

这段顺序来自源码:
  1. ILCK_SetStateRequest(ILCK_STATE_INITIALIZATION_REQUEST)
  2. BAL_SetStateRequest(BAL_STATE_INIT_REQUEST)
  3. 等待 BAL_GetInitializationState() == STD_OK
  4. 根据 BS_BALANCING_DEFAULT_INACTIVE 决定全局使能还是全局禁用均衡

3.2 首轮测量是一个真正的系统门槛

后面的两个子状态很关键:
MEAS_StartMeasurement();...if (MEAS_IsFirstMeasurementCycleFinished() == true) {ALGO_UnlockInitialization();...}
这段逻辑说明:
  • SYS 不会默认测量模块总能很快给出第一批有效数据。
  • 算法模块初始化是被锁住的,必须等第一轮测量完成才解锁。
这很符合真实 BMS 逻辑。因为 SOC/SOH/SOF 一类算法如果在没有一轮有效测量数据的情况下开始工作,结果只会是无意义甚至危险的。

3.3 电流传感器

如果工程配置里存在电流传感器,SYS 会:
  1. CAN_EnablePeriodic(true)
  2. 进入当前传感器存在性检查流程
  3. 对每个 string 检查:
  • CAN_IsCurrentSensorPresent(s)
  • CAN_IsCurrentSensorCcPresent(s)
  • CAN_IsCurrentSensorEcPresent(s)
然后再决定:
  • SE_InitializeSoc(true/false, s)
  • SE_InitializeSoe(true/false, s)
  • SE_InitializeSoh(s)
这个流程很有代表性,因为它说明 FoxBMS2 并不把是否存在带 Coulomb Counting / Energy Counting 能力的传感器写死,而是把它作为启动时可识别条件的一部分。

3.4 最后才请求 BMS 状态机初始化

PRE_RUNNING 的末尾是:
BMS_SetStateRequest(BMS_STATE_INIT_REQUEST);
也就是说,BMS 状态机并不是系统一上电就独立运行,而是被 SYS 明确地放在下面的流程之后,才允许进入初始化。
  • Interlock ready
  • Balancing ready
  • 第一轮测量完成
  • 电流传感器存在性确认
  • SOF 初始化完成
  • IMD 初始化请求成功

3.5 SYS 状态

先看顶层状态:
状态
进入条件
核心动作
退出条件
SYS_FSM_STATE_UNINITIALIZED
上电默认态;SYS_SetStateRequest(SYS_STATE_INITIALIZATION_REQUEST) 合法后由 SYS_Trigger() 消费
等待初始化请求;非法请求计数
收到初始化请求后转入 INITIALIZATION
SYS_FSM_STATE_INITIALIZATIONUNINITIALIZED
 接收初始化请求
读 FRAM 深放标志、启动 SBC、初始化 CAN/RTC、执行启动期 BIST、发送 Boot Message
成功后转入 PRE_RUNNING;SBC 超时等情况转入 ERROR
SYS_FSM_STATE_PRE_RUNNINGINITIALIZATION
 完成后进入
串起 Interlock、Balancing、首轮测量、算法解锁、电流传感器存在性检查、SOF、IMD、BMS 初始化请求
BMS_GetInitializationState() == STD_OK
 后转入 RUNNING;任一初始化超时则转入 ERROR
SYS_FSM_STATE_RUNNINGPRE_RUNNING
 成功完成
不再推进复杂子流程,只保持周期存活
当前源码中默认停留;如被请求错误态则可转入 ERROR
SYS_FSM_STATE_ERROR
初始化或预运行阶段出现错误
停留在错误态
当前源码中默认停留
再看 PRE_RUNNING 里最关键的子状态链:
子状态
进入条件
核心动作
退出条件
SYS_FSM_SUBSTATE_INITIALIZE_INTERLOCKPRE_RUNNING
 入口
发送 ILCK_STATE_INITIALIZATION_REQUEST
立刻转入 Balancing 初始化
SYS_FSM_SUBSTATE_START_INITIALIZATION_BAL
 / WAIT_INITIALIZATION_BAL
Interlock 初始化请求已发出
发送 BAL_STATE_INIT_REQUEST 并轮询 BAL_GetInitializationState()
STD_OK
 后进入全局均衡开关配置;超时则转错误态
SYS_FSM_SUBSTATE_WAIT_INITIALIZATION_BAL_GLOBAL_ENABLE
Balancing 已完成初始化
根据 BS_BALANCING_DEFAULT_INACTIVE 发送全局 disable 或 enable 请求
请求成功后启动首轮测量
SYS_FSM_SUBSTATE_START_FIRST_MEASUREMENT_CYCLE
 / WAIT_FIRST_MEASUREMENT_CYCLE
Balancing 全局策略已确定
MEAS_StartMeasurement()
,等待 MEAS_IsFirstMeasurementCycleFinished()
成功后 ALGO_UnlockInitialization(),再进入电流传感器检查或 misc 初始化
SYS_FSM_SUBSTATE_START_CURRENT_SENSOR_PRESENCE_CHECK
 / WAIT_CURRENT_SENSOR_PRESENCE_CHECK
BS_CURRENT_SENSOR_PRESENT == true
 且首轮测量完成
CAN_EnablePeriodic(true)
,检查电流传感器、CC、EC 能力,并据此初始化 SE_InitializeSoc/Soe/Soh
所有 string 满足条件后 SOF_Init() 并进入 misc 初始化;超时转错误态
SYS_FSM_SUBSTATE_INITIALIZATION_MISC
无电流传感器分支,或存在性检查完成
在无电流传感器配置下走 SE_InitializeSoc(false) / SE_InitializeSoe(false)
进入 IMD 初始化
SYS_FSM_SUBSTATE_INITIALIZATION_IMD
misc 初始化已完成
请求 IMD_RequestInitialization()
成功后进入 BMS 初始化请求;重试超限则转错误态
SYS_FSM_SUBSTATE_START_INITIALIZATION_BMS
 / WAIT_INITIALIZATION_BMS
IMD 初始化请求成功
发送 BMS_STATE_INIT_REQUEST 并轮询 BMS_GetInitializationState()
STD_OK
 后 SYS 才进入 RUNNING

4. BMS 状态机

4.1 BMS 状态变量里最值得看的辅助状态

bms_state 里除了 state/substate 外,还有很多直接决定行为的字段:
  • prechargeTryCounter
  • powerPath
  • closedStrings[]
  • closedPrechargeContactors[]
  • numberOfClosedStrings
  • firstClosedString
  • stringOpenTimeout
  • stringCloseTimeout
  • nextState
  • restTimer_10ms
  • currentFlowState
  • remainingDelay_ms
  • minimumActiveDelay_ms
  • transitionToErrorState
这说明 BMS 状态机不是一个简单的几态切换图,而是把接触器控制、错误延时、预充重试、静置判定等都加进了状态变量里。

4.2 外部请求是走数据库

BMS_CheckCanRequests() 的实现:
  1. 从数据库读取 DATA_BLOCK_STATE_REQUEST
  2. 查看 stateRequestViaCan
  3. 映射为 BMS_REQ_ID_STANDBY / NORMAL / CHARGE / NOREQ
这意味着 CAN 接收回调并不会直接改 BMS 状态,而是先把请求写进数据库,再由 BMS 状态机在自己的线程里统一处理。
这和前面讲的架构完全一致:请求也是数据,不是跨模块直接函数调用

4.3 从 CAN 请求到预充执行,是这样串起来的

这里有一个非常容易忽略的实现细节:BMS_STATEMACH_CHARGE 和 BMS_STATEMACH_DISCHARGE 在当前版本里更多是预充目标模式和 CAN 对外状态,真正承接预充后的主运行态仍然是 BMS_STATEMACH_NORMAL

5. 预充流程:源码比先负后正复杂得多

5.1 进入预充前,先决定要从哪一串开始

在 BMS_STATEMACH_PRECHARGE 的 BMS_ENTRY 分支里,FoxBMS 会先根据目标模式选择电池串:
  • 充电:BMS_GetLowestString(BMS_TAKE_PRECHARGE_INTO_ACCOUNT, &bms_tablePackValues)
  • 放电:BMS_GetHighestString(BMS_TAKE_PRECHARGE_INTO_ACCOUNT, &bms_tablePackValues)
为什么?
  • 充电时先从电压最低的 string 开始更合理。
  • 放电时先从电压最高的 string 开始更合理。
而且函数名里明确写了 TAKE_PRECHARGE_INTO_ACCOUNT,说明不是所有 string 都默认有资格被选中,只有具备预充能力的 string 才会被纳入候选。

5.2 第一步先合负极

预充入口阶段的第一步是:
CONT_CloseContactor(firstClosedString, CONT_MINUS)
成功后设置:
  • stringCloseTimeout = BMS_STRING_CLOSE_TIMEOUT
  • timer = BMS_WAIT_TIME_AFTER_CLOSING_STRING_CONTACTOR
  • substate = BMS_PRECHARGE_CLOSE_PRECHARGE
这说明接触器闭合也不是发个命令就算成功,而是要进入等待窗口,再回读状态确认。

5.3 第二步才是闭合预充接触器

在 BMS_PRECHARGE_CLOSE_PRECHARGE 中,源码先检查负极接触器反馈是否已经到 CONT_SWITCH_ON,只有确认成功后才执行:
CONT_ClosePrecharge(firstClosedString)
同时:
  • OscillationTimeout = BMS_OSCILLATION_TIMEOUT
  • closedPrechargeContactors[string] = 1
OscillationTimeout 很值得注意。它说明设计者考虑到了预充失败后不断来回试图重入预充的振荡问题,不希望系统在故障边界附近无限反复尝试。

5.4 真正的预充判据不是等一会儿,而是电压差和电流双判据

BMS_CheckPrecharge() 并不是一个抽象函数,源码里把判据写得很清楚:
cont_prechargeVoltDiff_mV = abs(stringVoltage - highVoltageBusVoltage)current_mA = abs(stringCurrent)
只有同时满足:
  • cont_prechargeVoltDiff_mV < BMS_PRECHARGE_VOLTAGE_THRESHOLD_mV
  • current_mA < BMS_PRECHARGE_CURRENT_THRESHOLD_mA
才认为预充成功。
这两个判据对应了两个不同风险:
  • 压差太大,说明母线电容还没充起来。
  • 电流太大,说明存在异常浪涌或负载问题。
而且函数里会分别对电压原因失败和电流原因失败打不同的诊断标志:
  • DIAG_ID_PRECHARGE_ABORT_REASON_VOLTAGE
  • DIAG_ID_PRECHARGE_ABORT_REASON_CURRENT
这比预充失败一个笼统标志要细得多。

5.5 预充失败允许有限重试

如果预充失败,源码不会立刻认定系统完蛋,而是:
  1. 先尝试打开预充接触器
  2. 检查 prechargeTryCounter < (BMS_PRECHARGE_TRIES - 1u)
  3. 若还没到上限,则:
  • 清除closedPrechargeContactors[string]
  • 等待BMS_TIME_WAIT_AFTER_PRECHARGE_FAIL
  • 再回到BMS_PRECHARGE_CLOSE_PRECHARGE
只有超过重试上限,才转入断开接触器并进入错误路径。
也就是说,FoxBMS 的策略不是碰到一次异常就立刻停机,而是在安全边界内允许有限恢复尝试

5.6 成功预充后,还要确认正极接触器闭合

当 BMS_CheckPrecharge() 返回成功且预充接触器状态正常后,才会执行:
CONT_CloseContactor(firstClosedString, CONT_PLUS)
然后进入 BMS_CHECK_CLOSE_SECOND_STRING_CONTACTOR_PRECHARGE_STATE 等待反馈确认。只有确认正极接触器真的闭合,才会把该 string 标记为已闭合并继续后续流程。
这再次说明:FoxBMS 的接触器控制模型是命令 + 等待 + 反馈确认,而不是单纯的发控制命令。

5.7 预充关键子状态

下面这张表只列 bms.c 当前主分支里真正走到的预充子状态。
子状态
进入条件
核心动作
退出条件
BMS_ENTRYSTANDBY
 接收到 NORMAL 或 CHARGE 请求后进入 BMS_STATEMACH_PRECHARGE
选首个 string;记录 firstClosedString;先闭合负极接触器
负极闭合请求成功后转 BMS_PRECHARGE_CLOSE_PRECHARGE;无可用 string 或命令非法则走开断错误路径
BMS_PRECHARGE_CLOSE_PRECHARGE
负极闭合请求已发出
轮询负极反馈;成功后设置 OscillationTimeout 并闭合预充接触器
预充闭合成功后转 BMS_CHECK_ERROR_FLAGS_CLOSING_PRECHARGE;超时或命令失败则走错误路径
BMS_CHECK_ERROR_FLAGS_CLOSING_PRECHARGE
预充接触器闭合后
在真正判断预充成功前先过一轮 fatal error 检查
无错误则转 BMS_CHECK_STATE_REQUESTS;有错误则开断进入错误路径
BMS_CHECK_STATE_REQUESTS
预充过程中的统一请求检查点
检查是否收到 STANDBY 退回请求
若收到 STANDBY 则转 OPEN_CONTACTORS;否则转 BMS_PRECHARGE_CHECK_VOLTAGES
BMS_PRECHARGE_CHECK_VOLTAGES
预充接触器已闭合,开始判断是否预充成功
检查预充接触器反馈;调用 BMS_CheckPrecharge() 做压差+电流双判据;成功后闭合正极
成功则转 BMS_CHECK_CLOSE_SECOND_STRING_CONTACTOR_PRECHARGE_STATE;失败则打开预充接触器,并在未超重试上限时等待后重试,否则进错误路径
BMS_CHECK_CLOSE_SECOND_STRING_CONTACTOR_PRECHARGE_STATE
正极闭合请求已发出
轮询正极反馈,确认第一串主回路真正闭合
成功则把该 string 记为 closed,进入 BMS_CHECK_ERROR_FLAGS_PRECHARGE_CLOSING_STRINGS;超时则走错误路径
BMS_CHECK_ERROR_FLAGS_PRECHARGE_CLOSING_STRINGS
第一串已闭合
再做一次 fatal error 检查,确认不是“带着错误继续留在高压闭合态”
无错误则转 BMS_PRECHARGE_OPEN_PRECHARGE
BMS_PRECHARGE_OPEN_PRECHARGE
 / BMS_PRECHARGE_CHECK_OPEN_PRECHARGE
第一串已闭合并准备退出预充支路
打开预充接触器并确认反馈关闭
预充接触器确认断开后转 BMS_STATEMACH_NORMAL;否则错误退出

6. 错误处理:BMS 并不是看到故障就立刻跳ERROR

6.1 BMS 会先扫描所有 fatal error,并选最短延时

BMS_IsAnyFatalErrorFlagSet() 会遍历所有 fatal diagnosis entry:
DIAG_GetDiagnosisEntryState(...)DIAG_GetDelay(...)
只要有任何 fatal error 激活,就记录对应 delay,并且总是取最短的那个作为 minimumActiveDelay_ms
这非常关键,因为它意味着不同故障可以配置不同的允许延时断开时间,而 BMS 会自动收敛到最严苛的那一个。

6.2 remainingDelay_ms 让错误切断变成了可控策略

BMS_IsBatterySystemStateOkay() 并不是简单返回有错/没错。它还有一套延时衰减逻辑:
  • 如果已经进入错误过渡态,就根据距离上次调用过去的时间递减remainingDelay_ms
  • 如果新发现了更短延时的 fatal error,就把剩余延时压缩到更短值
  • 只有当remainingDelay_ms == 0 时,才真正返回STD_NOT_OK
这说明 BMS 的错误响应不是单一策略,而是允许:
  • 有些错误立刻切断
  • 有些错误允许一个很短的受控延时
这种设计在动力系统里很常见,因为立刻切断和受控退出往往各有风险。

7. SYS 和 BMS 之间真正的关系

如果把两层状态机放在一起看,FoxBMS 的控制流其实非常清楚:
  1. main() 只负责把硬件和调度器拉起来。
  2. SYS 负责把 Interlock、Balancing、首轮测量、传感器存在性检查、IMD、BMS 初始化全部串起来。
  3. BMS 在自己的 10ms 节拍里消费外部请求和诊断结果,决定接触器如何动作。
所以,这不是两个平级状态机,而是:
  • SYS 是系统级编排者
  • BMS 是高压业务执行者

小结

这一篇真正应该带走的认识有三点:
  • SYS 的重点是把系统按正确顺序拉到可运行状态。
  • BMS 的重点是把接触器控制、预充重试、反馈确认、错误延时这些细节都纳入了状态机。
  • 外部请求和错误都是通过数据库和诊断模块进入状态机。
下一篇开始,我们从大脑往下走,看这些状态机依赖的数据到底是如何从物理芯片里被采上来、校验过、再写进数据库的。

推荐阅读顺序

  • 先读SYS_Trigger(),建立 SYS 状态机的顶层入口。
  • 再看SYS_ProcessInitializationState() 和SYS_ProcessPreRunningState(),把系统拉起顺序理清楚。
  • 接着跳到BMS_Trigger(),并补读CANRX_BmsStateRequest() 与BMS_CheckCanRequests(),把外部请求如何进入 BMS 决策链串起来。
  • 然后读BMS_CheckPrecharge() 与BMS_STATEMACH_PRECHARGE 分支,把预充执行主链串起来。
  • 最后读BMS_IsBatterySystemStateOkay()、接触器动作函数和配置头文件,看错误延时、执行动作与阈值定义如何一起收口。