FoxBMS2 源码分析 (2) | 操作系统与任务调度
1. 前言
上一篇我们停在了 OS_StartScheduler() ,从那一刻开始,FoxBMS2 就不再是一个顺序执行的初始化程序,而是一套由多个周期任务、一个最高优先级引擎任务,以及若干监控机制共同驱动的实时系统。
刚开始看源码时我们可能会停留在 1ms、10ms、100ms 三类任务这种层面,但对于 FoxBMS2 来说,真正重要的是下面三个问题:
OS 适配层到底封装了哪些 FreeRTOS 能力。
各个任务不只是周期不同,而是承担了怎样的职责分层。
如果调度失序、任务超时或系统卡死,FoxBMS 靠什么发现并处理。
这一篇就顺着 os.c 、os_freertos.c 、ftask.c 、ftask_cfg.c 、sys_mon.c 、sbc.c 把这条调度骨架讲清楚。
任务层级与模块挂载图
下面这张图只画当前源码里能直接确认的任务、优先级层级和主要挂载点,先把谁在什么位置跑这件事搞明白:
2. OS 适配层
2.1 分阶段建立系统资源
src/app/task/os/os.c 里的 OS_InitializeOperatingSystem() 把启动阶段拆得很明确:
/* Initialize the scheduler */ os_boot = OS_INITIALIZE_SCHEDULER ; OS_InitializeScheduler (); /* operating system configuration (Queues, Tasks) */ os_boot = OS_CREATE_QUEUES ; FTSK_CreateQueues (); os_boot = OS_CREATE_TASKS ; FTSK_CreateTasks (); os_boot = OS_INIT_PRE_OS ;
这里的 os_boot 不是普通状态变量,它是整个启动阶段的同步信号。后面的任务创建函数会反复轮询它,确保高优先级任务、1ms 任务和其余周期任务按既定顺序启动。
换句话说,FoxBMS2 不是任务都创建出来,谁先调度到算谁先跑,而是连任务的启动顺序也用状态机化的方式管理起来了 。
2.2 os_freertos.c
src/app/task/os/freertos/os_freertos.c 至少做了四类事情:
vApplicationGetIdleTaskMemory() 和 vApplicationGetTimerTaskMemory() 为 Idle Task 和 Timer Task 提供静态 TCB 与栈空间。这和车规项目常见的避免动态内存不确定性思路一致。
OS_EnterTaskCritical() / OS_ExitTaskCritical()
上层代码完全不需要知道 FreeRTOS 的类型细节。
OS_WaitForNotification() 、OS_NotifyFromIsr() 、OS_ReceiveFromQueue() 、OS_SendToBackOfQueue() 等都集中在这里,后面数据库引擎和 CAN 队列都会依赖这些接口。
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) { /* After the FreeRTOS stack overflow detection has been triggered, try to instantly send a CAN message, that tells the higher level control unit, that a stack overflow appeared in one of the FreeRTOS tasks. */ CANTX_CrashDump (CANTX_FATAL_ERRORS_ACTIONS_STACK_OVERFLOW ); FAS_ASSERT(FAS_TRAP); }
这说明在 FoxBMS2 的设计里,FreeRTOS 的异常不只是调试信息,而是可以升级为系统级致命故障并对外广播的事件。
2.3 软件时间基准并不只是一串 tick
OS_IncrementTimer() 在 1ms 任务里被调用,用层层进位的方式维护:1ms、10ms、100ms、1s、1min、1h、1d。同时还调用了 RTC_IncrementSystemTime() 。这意味着 FoxBMS 的时间至少有两层用途:
给上层诊断、记录、时间戳相关逻辑提供更长时间尺度的基准。
3. 任务创建顺序:谁先跑,不是偶然
调度器启动后任务进入路径
把 OS_StartScheduler() 之后的主启动链压成一张图,当前源码里能直接确认的顺序是这样的:
3.1 Engine 任务是系统最高优先级中枢
FTSK_CreateTaskEngine() 在 ftask.c 里的顺序是:
OS_MarkTaskAsRequiringFpuContext (); os_boot = OS_SCHEDULER_RUNNING ; FTSK_InitializeUserCodeEngine (); os_boot = OS_ENGINE_RUNNING ; /* AXIVION Next Codeline Style MisraC2012-2.2 FaultDetection-DeadBranches: FreeRTOS task setup requires an infinite * loop for the user code (see www.freertos.org/a00125.html)*/ while (true ) { /* notify system monitoring that task will be called */ SYSM_Notify (SYSM_TASK_ID_ENGINE , SYSM_NOTIFY_ENTER , OS_GetTickCount ()); /* user code implementation */ FTSK_RunUserCodeEngine (); /* notify system monitoring that task has been called */ SYSM_Notify (SYSM_TASK_ID_ENGINE , SYSM_NOTIFY_EXIT , OS_GetTickCount ()); }
Engine 任务并不按固定周期休眠,它的职责是:
初始化系统监控 SYSM_Initialize()
在运行期不断执行 DATA_Task() 和 SYSM_CheckNotifications()
这决定了一个非常重要的架构:数据库处理和任务超时监控都被放在了全系统最高优先级任务里 。
3.2 1ms 任务并不只是快,它还负责拉起其余系统
FTSK_CreateTaskCyclic1ms() 启动前会先等 OS_ENGINE_RUNNING ,然后调用:
FTSK_InitializeUserCodePreCyclicTasks(); os_boot = OS_PRE_CYCLIC_INITIALIZATION_HAS_FINISHED;
而 FTSK_InitializeUserCodePreCyclicTasks() 实际做的事情很多:
SYS_SetStateRequest(SYS_STATE_INITIALIZATION_REQUEST)
也就是说,1ms 任务不仅负责高频周期执行,它还承担了在系统进入调度前做一轮前置初始化 的职责。
3.3 10ms、100ms、算法 100ms 任务
ftask_cfg.c 里最值得看的不是任务定义结构体,而是 FTSK_RunUserCode…() 里到底挂了哪些模块。
1ms 任务
OS_IncrementTimer(); DIAG_UpdateFlags(); MEAS_Control(); CAN_ReadRxBuffer ();
把 CAN 接收队列里的报文尽快搬到上层处理链路里
10ms 任务
SYSM_UpdateFramData(); SYS_Trigger(&sys_state); ILCK_Trigger(); ADC_Control(); SPS_Ctrl(); CAN_MainFunction ();SOF_Calculation(); ALGO_MonitorExecutionTime(); SBC_Trigger(&sbc_stateMcuSupervisor);
MRC_ValidateAfeMeasurement(); MRC_ValidatePackMeasurement();
源码里对这句有明确注释:把 BMS_Trigger() 放在 10ms 任务末尾,是为了让前面已经更新过数据的模块先完成采样和判定,尽量缩短数据更新到状态机响应之间的延迟。
这就是 FoxBMS 的调度风格:不是只给模块分周期,还在同一个周期内部安排执行先后 。
100ms 任务
MINFO_CheckSupplyVoltageClamp30c()
这说明默认配置下的 SOX 估算并不是每 100ms 都更新,而是按 1s 节奏执行。源码注释也写得很清楚:如果不用带积分能力的电流传感器,单纯人工积分就可能需要更高频率。
Algorithm 100ms 任务
它只做一件事:ALGO_MainFunction() 。
这反而说明 FoxBMS 团队是有意把常规 100ms 周期任务和算法任务隔开的,避免所有慢速逻辑混在一个大周期里难以分析执行时间。
3.4 I2C 任务和 AFE 任务是独立执行单元
除了我们经常提的 1/10/100ms 任务,源码里还有:
I2C 任务:PEX_Trigger()、HTSEN_Trigger()、RTC_Trigger(),然后 OS_DelayTaskUntil(¤t_time, 2u)。
AFE 任务:在 FOXBMS_AFE_DRIVER_TYPE_NO_FSM == 1 时才主动执行 MEAS_Control()。
这两个任务的存在说明:FoxBMS 并不是把所有外设都硬塞进统一周期任务里,而是根据驱动模型决定是否给出独立执行上下文 。
4. 相位差与启动门闩:避免任务一窝蜂上线
FTSK_CreateTaskCyclic10ms() 、FTSK_CreateTaskCyclic100ms() 和 FTSK_CreateTaskCyclicAlgorithm100ms() 都会先做两件事:
1. 等待 OS_PRE_CYCLIC_INITIALIZATION_HAS_FINISHED
2. 用 OS_DelayTaskUntil(&os_schedulerStartTime, phase) 做首轮相位偏移
门闩作用 :1ms 任务没完成前置初始化之前,其余周期任务不能抢跑。
错峰作用 :不同周期任务不会在调度器启动瞬间全部在同一 tick 抢占 CPU。
尤其是 FTSK_CreateTaskCyclicAlgorithm100ms() 里还会把 os_boot 设置为 OS_SYSTEM_RUNNING ,说明系统对何时算真正跑起来也有明确定义。
5. 软件监控: sys_mon.c 不是日志模块,而是运行时裁判
5.1 所有关键任务都要显式报到
SYSM_Notify(taskId, SYSM_NOTIFY_ENTER, OS_GetTickCount()); ... SYSM_Notify(taskId, SYSM_NOTIFY_EXIT, OS_GetTickCount());
而且这些更新是在临界区里完成的,避免并发读写把时间戳本身搞坏。
5.2 它检查的不是某次慢了,而是是否持续违背节拍要求
SYSM_CheckNotifications() 的判据是:
time_since_last_call > cycleTime + maxJitter&& duration > cycleTime
DIAG_Handler(DIAG_ID_SYSTEM_MONITORING, …)
SYSM_RecordTimingViolation() 会把具体哪个任务、持续了多久、何时进入任务记录到 FRAM 结构里,不是简单打一条日志。
SYSM_UpdateFramData() 先检查 sysm_flagFramCopyHasChanges ,需要时才真正写 FRAM。这避免了在 10ms 周期里每次都做存储器写操作。
6. 硬件看门狗:SBC 才是最后的物理托底
软件监控只能在软件还活着的时候起作用。FoxBMS 2 的最后一道防线在 sbc.c 。
6.1 SBC 初始化不是一次 SPI 配置就完事
SBC_Trigger() 的初始化阶段有清晰的子流程:
查询需要多少次有效 watchdog refresh 才能清掉 fault error counter
FS85_CheckFaultErrorCounter()
最后进入 SBC_STATEMACHINE_RUNNING
这说明 SBC 并不是外设驱动那么简单,而是直接参与了系统安全链的建立。
SBC 状态推进简表
下表把 sbc.c 里当前实现能直接确认的主状态和初始化子状态整理到一起,方便回源码时快速定位:
状态
进入条件
核心动作
退出条件
SBC_STATEMACHINE_UNINITIALIZED
上电后的初始状态
等待 SBC_STATE_INIT_REQUEST
收到初始化请求后转入 SBC_STATEMACHINE_INITIALIZATION
SBC_STATEMACHINE_INITIALIZATION / SBC_ENTRY
从未初始化状态收到初始化请求
调 FS85_InitializeFsPhase() ;成功后开启周期喂狗并转下一个子状态;失败则最多重试 3 次
成功转入 SBC_INIT_RESET_FAULT_ERROR_COUNTER_PART1 ;超过重试上限转 ERROR
SBC_STATEMACHINE_INITIALIZATION / SBC_INIT_RESET_FAULT_ERROR_COUNTER_PART1
FS85 初始化成功后
调 FS85_InitializeNumberOfRequiredWatchdogRefreshes() ,据此设置等待时间
成功转入 PART2 ;超过重试上限转 ERROR
SBC_STATEMACHINE_INITIALIZATION / SBC_INIT_RESET_FAULT_ERROR_COUNTER_PART2
已等待足够 watchdog refresh 次数
调 FS85_CheckFaultErrorCounter() 检查 fault error counter 是否清零
成功转入 SBC_INITIALIZE_SAFETY_PATH_CHECK ;超过重试上限转 ERROR
SBC_STATEMACHINE_INITIALIZATION / SBC_INITIALIZE_SAFETY_PATH_CHECK
fault error counter 已通过检查
调 FS85_SafetyPathChecks()
成功转入 SBC_STATEMACHINE_RUNNING ;超过重试上限转 ERROR
SBC_STATEMACHINE_RUNNING
初始化链完成
设置长定时;若配置启用 ignition power-down 检查,则调用 SBC_IsIgnitionSignalDetected()
保持运行态,当前代码片段未显示额外自动跳转
SBC_STATEMACHINE_ERROR
初始化阶段超过重试上限或检查失败
记录最后状态并进入长定时循环
保持错误态,当前代码片段未显示自动恢复路径
6.2 周期喂狗逻辑也被纳入 10ms 主循环
一旦 watchdogState == SBC_PERIODIC_WATCHDOG_ACTIVATED ,SBC_TriggerWatchdogIfRequired() 就会在每次 10ms 调用时递减计数器,减到 0 时执行:
FS85_TriggerWatchdog(...)
成功后再把计数器重装成 watchdogPeriod_10ms 。
这意味着喂狗不是一个普通后台线程,而是明确绑定在主系统周期中的安全动作 。从当前实现看,只要软件调度仍能推进,sys_mon 会先对周期任务违例做检测;而 10ms 主循环里的周期喂狗逻辑,则把 SBC 硬件看门狗保留成更靠后的托底层。
软件监控与硬件看门狗关系图
7. 这一套调度设计的真正价值
FoxBMS2 的任务系统最值得学习的地方,不是用了 FreeRTOS,而是它把下面几件事同时做到了:
启动顺序可控 :用 os_boot 明确分隔 scheduler、engine、1ms 前置初始化和完整系统运行阶段。
周期职责明确 :1ms 负责时间基准和快路径输入,10ms 负责系统主控制,100ms 负责慢速控制与估算。
运行期可追责 :sys_mon 记录节拍违例,SBC 负责物理层托底。
时序可解释 :不仅知道什么模块在哪个周期里,还知道为什么要在本周期的这个位置执行。
关键配置项
配置类别
当前源码中的代表项
作用
任务优先级
FTSK_TASK_ENGINE_PRIORITY 、 FTSK_TASK_CYCLIC_1MS_PRIORITY 、 FTSK_TASK_CYCLIC_10MS_PRIORITY 、 FTSK_TASK_CYCLIC_100MS_PRIORITY 、 FTSK_TASK_CYCLIC_ALGORITHM_100MS_PRIORITY 、 FTSK_TASK_AFE_PRIORITY
决定 Engine、各周期任务和 AFE 任务的抢占关系。
任务首轮 phase
FTSK_TASK_CYCLIC_1MS_PHASE 、 FTSK_TASK_CYCLIC_10MS_PHASE 、 FTSK_TASK_CYCLIC_100MS_PHASE 、 FTSK_TASK_CYCLIC_ALGORITHM_100MS_PHASE
决定调度器启动后各周期任务第一次进入执行时的错峰偏移。
周期节拍
FTSK_TASK_CYCLIC_1MS_CYCLE_TIME 、 FTSK_TASK_CYCLIC_10MS_CYCLE_TIME 、 FTSK_TASK_CYCLIC_100MS_CYCLE_TIME 、 FTSK_TASK_CYCLIC_ALGORITHM_100MS_CYCLE_TIME
决定各任务的名义执行周期,也是 sys_mon 配置的基础输入。
最大允许抖动
FTSK_TASK_ENGINE_MAXIMUM_JITTER 、 FTSK_TASK_CYCLIC_1MS_MAXIMUM_JITTER 、 FTSK_TASK_CYCLIC_10MS_MAXIMUM_JITTER 、 FTSK_TASK_CYCLIC_100MS_MAXIMUM_JITTER 、 FTSK_TASK_CYCLIC_ALGORITHM_100MS_MAXIMUM_JITTER
决定 SYSM_CheckNotifications() 里多大调度偏差会被视为违例。
连续任务配置
FTSK_TASK_I2C_PRIORITY / FTSK_TASK_I2C_CYCLE_TIME 、 FTSK_TASK_AFE_PRIORITY / FTSK_TASK_AFE_CYCLE_TIME
说明 I2C 和 AFE 任务不是固定周期任务,而是连续运行任务。
SBC 周期喂狗节拍
SBC_WINDOW_WATCHDOG_PERIOD_MS 、 watchdogPeriod_10ms
决定硬件看门狗的窗口周期以及 10ms 主循环里重装计数的频率。
小结
OS 适配层负责把 FreeRTOS 变成一套受控的、可移植的系统接口。
Engine 任务负责数据库与系统监控,站在任务体系最顶层。
1ms、10ms、100ms、算法 100ms、I2C、AFE 任务共同构成了 FoxBMS 的时间骨架。
sys_mon 和 SBC 分别构成软件和硬件两层看门狗。
下一篇我们就沿着这条骨架往里走,去看最核心的那层中枢:为什么 FoxBMS2 几乎所有模块都不直接互相调用,而是把数据交给数据库引擎来中转。
推荐阅读顺序
先看 OS_InitializeOperatingSystem(),确认启动阶段创建了哪些队列和任务。
再看 FTSK_CreateTaskEngine() 和 FTSK_CreateTaskCyclic1ms(),理解任务启动顺序与 os_boot 门闩。
接着读 FTSK_RunUserCodeCyclic1ms()、FTSK_RunUserCodeCyclic10ms()、FTSK_RunUserCodeCyclic100ms(),把不同周期各自挂载的模块建立起来。
然后回到 OS_IncrementTimer(),理解全局时间基准是如何被维护的。
最后读 SYSM_CheckNotifications() 和 SBC_Trigger(),看软件监控与硬件看门狗如何形成闭环。
sys_mon.c 记录的 duration 是用 timestampExit – timestampEnter 算出来的。如果任务执行中途被更高优先级任务抢占,这段被抢占时间会不会被算进 duration ?如果会,这种设计对任务执行时长和系统调度压力的解释意味着什么?
参考答案
会被算进去。因为 duration 的定义不是这段代码真正占了多少个 CPU 指令周期,而是从任务宣告进入,到任务宣告退出之间,经过了多少系统时间。只要 timestampEnter 和 timestampExit 都是在任务上下文里读取系统 tick,那么中间无论这段任务是在真正执行,还是被更高优先级任务抢占挂起,这段墙上时间都会被包含进 duration 。
这意味着 sys_mon 观测到的并不是纯粹的函数本体执行耗时,而是该任务完成一次调度周期工作所经历的总时延。这个定义在实时系统里其实更有价值,因为系统真正关心的不是某段代码理论上只需跑 2ms,而是它在实际调度压力下是否还能在自己的时间窗内完成。如果一个 10ms 任务本体只需要 2ms,但因为频繁被更高优先级任务抢占,实际从进入到退出花了 11ms,那么对系统来说它就是超时了,后果并不会因为代码本身其实不慢而变得更安全。
这也给超时配置带来一个重要启发:监控阈值不能只按单函数裸跑耗时设,而要按在最坏合理抢占条件下的周期完成时间设。换句话说,sys_mon 更像在度量系统级调度压力下的任务可完成性,而不是在做微观性能剖析。
因此,FoxBMS 这里的设计是有意把执行耗时和调度干扰一起纳入监控模型。它回答的问题不是代码快不快,而是这个任务在真实系统里还能不能按节拍活下去。