4.2 WdgM源码走读——三重监督的C语言状态机
场景:当 MainFunction 被调度器唤醒
上午九点多,ECU 的 OS 计数器滴答不停。在 10ms 周期任务队列中,WdgM_MainFunction 被唤醒。它只有一百二十行代码,但将在接下来的 700 微秒内遍历所有受监控实体,执行存活、时限、逻辑三重检查,聚合局部状态,最终决定要不要让整个 ECU 进入 STOPPED 状态,然后触发硬件看门狗复位。
你打开 WdgM.c,一行一行读下去。/** @req SWS_WdgM_00011 */ 这样的注释密密麻麻,每一段代码都能追溯到 AUTOSAR 规范的具体需求。这不是“我觉得应该这样写“的代码,这是“规范第 4.3.0 版第 327 条要求这样实现“的代码。
在逐行读它之前,先看整体骨架。WdgM 的 1450 行代码分三层:
最底层(配置层):WdgM_ConfigTypes.h 用 const 结构体定义了所有受监控实体(Supervised Entity,SE)的参数——每个 SE 的 deadline、容忍度、初始状态、触发的故障响应。这一层不执行任何逻辑,只存放“规则“。
中间层(执行层):WdgM_MainFunction() 是核心入口,在每个调度周期被调用。它在 120 行代码内完成三重监督的计算:Alive 看是否喂狗→Deadline 看是否准时→Logical 看是否走对了路径。计算结果是每个 SE 的局部状态(OK/FAILED/EXPIRED/STOPPED)。
最顶层(响应层):当任意 SE 持续超时达到容忍度上限,WdgM_MainFunction() 将全局状态置为 STOPPED,硬件看门狗不再被喂,倒计数到零触发复位。这是最后一击——软件层已经不信任自己了,把决策权交给硬件。
三层串起来就是:按配置规则→周期检查→超时自杀。下面从 Init 开始,按执行顺序走完每一层。
一、开局:实例化与宏武装
文件一开头,你会看到一段零运行时开销的守卫代码:
#if !(((WDGM_SW_MAJOR_VERSION == 3u) && (WDGM_SW_MINOR_VERSION == 0u)) )
#error WdgM: Expected BSW module version to be 3.0.*
#endif
#if !(((WDGM_AR_RELEASE_MAJOR_VERSION == 4u) && (WDGM_AR_RELEASE_MINOR_VERSION == 3u)) )
#error WdgM: Expected AUTOSAR version to be 4.3.*
#endif
这是 AUTOSAR 的“编译时契约“,如果集成者把 WdgM 3.0 的头文件与不一致的 AUTOSAR 版本混用,编译器直接罢工。生产环境没有“运行时断言“,只有 #error。
然后是第一组宏,参数检查的双重编译:
#if (WDGM_DEV_ERROR_DETECT == STD_ON)
#define WDGM_CHECK_INIT_NO_RV(functionID) \
{ \
if (WdgM_instance.isInitiated == (boolean)FALSE) \
{ \
WDGM_REPORT_DET_ERROR(functionID, WDGM_E_NO_INIT); \
return; \
} \
}
// ... 10+ 种检查宏 ...
#else
#define WDGM_CHECK_INIT_NO_RV(functionID) \
{ \
if (WdgM_instance.isInitiated == (boolean)FALSE) \
{ \
return; \
} \
}
当 WDGM_DEV_ERROR_DETECT == STD_ON 时,宏展开为带 DET 报错的完整版本。当关闭时,宏只保留安全检查逻辑,零 DET 开销。但关键不在性能,在开发阶段,每一个参数越界都会立刻被 DET 捕获;而在生产阶段,安全检查仍然执行(只是不报 DET)。这在 ISO 26262 中称为“防御性编程的编译时可变性“。
MISRA C:2012 Rule 15.5 标注的 /*lint -e904 */ 你在全文件中反复看到。这是针对“提前 return 破坏了单一函数出口“规则的正式豁免。Arctic Core 选择“先检查错误、立即返回“而不是嵌套 if,可读性优先于教条。
二、Init:从配置到运行时世界的跃迁
WdgM_Init() 函数约 80 行,是理解整个模块的入口:
void WdgM_Init(const WdgM_ConfigType *ConfigPtr)
{
const WdgM_Mode *initialMode;
uint16 i;
boolean status = (boolean)TRUE;
Std_ReturnType ret = E_OK;
WDGM_CHECK_NULL_POINTER_NO_RV(ConfigPtr, WDGM_SID_INIT);
WDGM_CHECK_DEINIT_NO_RV(WDGM_SID_INIT);
WdgM_instance.ConfigPtr = ConfigPtr;
WdgM_instance.runtimeDataPtr = &WdgM_runtimeData;
initialMode = &WdgM_instance.ConfigPtr->ConfigSet.Modes[
WdgM_instance.ConfigPtr->ConfigSet.initialModeId];
前三步揭示了核心设计:配置(ConfigPtr)是 const 指针,永远不变;运行时数据(runtimeDataPtr)是可变的,每轮 MainFunction 更新。这种“只读配置 + 可变状态“的分离是功能安全代码的基石,配置可以放在 Flash 中(物理只读),状态在 RAM 中。
接下来是“初始化保险“,检查初始模式是否真有一个看门狗在运行:
#if (WDGM_OFF_MODE_ENABLED != STD_ON)
status = WdgM_internal_isAtLeastOneWdogEnabled(
initialMode->Id, WDGM_SID_INIT, WDGM_E_DISABLE_NOT_ALLOWED);
#endif
如果配置错误,初始模式里所有看门狗都被设为 OFF,Init 直接报 DET 并退出。ECU 不会带着“看门狗没开“的状态启动。
然后是两轮循环,先全部关掉,再只开初始模式的:
for (i = 0u; i < WdgM_instance.ConfigPtr->General.Length_SupervisedEntities; i++)
{
WdgM_internal_DeactivateAndResetSE(i);
}
for (i = 0u; i < initialMode->Length_SEConfigurations; i++)
{
uint16 SEId = initialMode->SEConfigurations[i].SupervisedEntityId;
WdgM_runtime_SupervisedEntity *runtime_se =
&WdgM_instance.runtimeDataPtr->SEs[SEId];
runtime_se->LocalState = WDGM_LOCAL_STATUS_OK;
WdgM_Internal_ReportLocalModeChange(runtime_se, SEId);
}
最后激活初始模式的硬件看门狗触发器:
ret = WdgM_internal_activateTriggers(
initialMode,
WdgM_instance.ConfigPtr->ConfigSet.DemEventIdRefs.SetMode);
if (E_OK == ret)
{
WdgM_instance.isInitiated = (boolean)TRUE;
}
注意一个细节:isInitiated 只在触发 WdgIf 成功后设为 TRUE。如果硬件看门狗无法启动,WdgM 的内部状态就是一个永不启动的“僵尸模块“,任何后续 API 调用都会被 WDGM_CHECK_INIT_* 宏拦截。不完整的初始化不会留下一个“假装在工作“的状态机。
三、MainFunction:120行代码的调度奇迹
这是你接下来需要逐行精读的部分:
void WdgM_MainFunction( void )
{
WDGM_CHECK_INIT_NO_RV(WDGM_SID_MAINFUNCTION);
uint8 i;
/* now do alive monitoring */
WdgM_internal_CheckAlives();
/* now do the other stuff */
WdgM_internal_CalculateGlobalState();
if (WdgM_instance.GlobalState == WDGM_GLOBAL_STATUS_STOPPED)
{
#if (WDGM_DEM_ALIVE_SUPERVISION_REPORT == STD_ON)
WDGM_REPORT_DEM_ERROR(
WdgM_instance.ConfigPtr->ConfigSet.DemEventIdRefs.Supervision);
#endif
#if (WDGM_IMMEDIATE_RESET == STD_ON)
Mcu_PerformReset();
#endif
}
/* set the triggers for all wdgdevices != WDGIF_OFF_MODE */
for(i = 0u; i < WdgM_instance.CurrentMode->Length_Triggers; i++)
{
if (WdgM_instance.CurrentMode->Triggers[i].WatchdogMode
!= WDGIF_OFF_MODE)
{
uint16 triggerValue = (WdgM_instance.GlobalState
== WDGM_GLOBAL_STATUS_STOPPED) ? 0u :
WdgM_instance.CurrentMode->Triggers[i].TriggerConditionValue;
if (WdgM_instance.ResetInitiated == (boolean)FALSE)
{
WdgIf_SetTriggerCondition(
WdgM_instance.ConfigPtr->General.Watchdogs[
WdgM_instance.CurrentMode->Triggers[i].WatchdogId
].WatchdogDeviceId, triggerValue);
}
}
}
}
三步走:CheckAlives → CalculateGlobalState → 喂狗或复仇。
最后一步中那个三元运算符是关键:GlobalState == STOPPED ? 0u : TriggerConditionValue。当系统进入 STOPPED 状态后,不是立马调用 Mcu_PerformReset()(那是 WDGM_IMMEDIATE_RESET 的可选配置),而是把触发值设为零。硬件看门狗不喂了会自动复位。这是“失败安全“(fail-safe)原则的最后防线:即使软件在 STOPPED 之后崩溃了,硬件定时器到期也会复位。
四、Alive Check:容忍度里的生存密码
WdgM_internal_CheckAlives() 遍历当前模式下的所有 SE 配置,每个 alive checkpoint 都会经过两阶段判断:
static WdgM_Substate WdgM_internal_SpecAliveAlgo(
const WdgM_AliveSupervision *aliveCP,
WdgM_runtime_AliveSupervision *runtimeAliveCP)
{
WdgM_Substate retVal = WDGM_SUBSTATE_INCORRECT;
sint16 temp = (sint16)runtimeAliveCP->AliveCounter
- (sint16)aliveCP->ExpectedAliveIndications;
if ((temp <= (sint16)aliveCP->MaxMargin)
&& (temp >= (0 - (sint16)aliveCP->MinMargin)))
{
retVal = WDGM_SUBSTATE_CORRECT;
}
runtimeAliveCP->AliveCounter = 0u;
runtimeAliveCP->SupervisionCycleCounter = 0u;
return retVal;
}
核心算法:用一个带符号整数计算 实际 Alive 计数 - 期望值,然后验证是否在 [-MinMargin, +MaxMargin] 范围内。例如配置期望 5 次 checkpoint、MinMargin=1、MaxMargin=1,则 4 次或 6 次都是 OK 的。容忍度不是性能优化,是安全性设计,OS 调度抖动、中断延迟都可能导致一次额外的 MainFunction 循环。
触发条件是 SupervisionReferenceCycle:
static WdgM_Substate WdgM_internal_IsAliveHold(
const WdgM_AliveSupervision *aliveCP,
WdgM_runtime_AliveSupervision *runtime_aliveCP)
{
if (aliveCP->SupervisionReferenceCycle == 1u)
{
/* each cycle */
retVal = WdgM_internal_SpecAliveAlgo(aliveCP, runtime_aliveCP);
}
else
{
if (runtime_aliveCP->SupervisionCycleCounter
== aliveCP->SupervisionReferenceCycle)
{
retVal = WdgM_internal_SpecAliveAlgo(aliveCP, runtime_aliveCP);
}
}
}
你可以把 SupervisionReferenceCycle 设为 1(每个 MainFunction 周期都检查),也可以设置为更大值(比如 5,每 5 个周期检查一次)。这是给慢速 SE 的“宽限期“,不是所有任务都以 10ms 周期运行。
五、Deadline Check:OS 计数器的时间法官
Deadline 监控使用了 AUTOSAR OS 的 GetElapsedValue() 标准接口:
static WdgM_Substate WdgM_internal_DeadlineMonitoring(
const WdgM_SupervisedEntityConfiguration *seConf,
const WdgM_runtime_SupervisedEntityConfig *runtime_seConf,
WdgM_CheckpointIdType CPId)
{
WdgM_Substate result = WDGM_SUBSTATE_CORRECT;
for(i = 0u; i < seConf->Length_DeadlineSupervisions; i++)
{
if (seConf->DeadlineSupervisions[i].CheckpointIdStart == CPId)
{
TickType elapsedTicks = 0u;
TickRefType elapsedTicksPointer;
elapsedTicksPointer = &elapsedTicks;
TickRefType LastTickPointer = (TickRefType)
&(runtime_seConf->DeadlineSupervisions[i].LastTickValue);
(void)GetElapsedValue(seConf->OSCounter,
LastTickPointer, elapsedTicksPointer);
}
if ((seConf->DeadlineSupervisions[i].CheckpointIdFinish == CPId)
&& (runtime_seConf->DeadlineSupervisions[i].LastTickValue != 0u))
{
if (WdgM_internalIsDeadlineHold(seConf->OSCounter,
&seConf->DeadlineSupervisions[i],
&runtime_seConf->DeadlineSupervisions[i])
== WDGM_SUBSTATE_INCORRECT)
{
result = WDGM_SUBSTATE_INCORRECT;
break;
}
runtime_seConf->DeadlineSupervisions[i].LastTickValue = 0u;
}
}
return result;
}
Start checkpoint 到达时记录 OS 时刻(通过 GetElapsedValue 获取当前计数值),Finish checkpoint 到达时再次读取并计算差值:
#define WDGM_OSTICKSPERUS (OSTICKDURATION / 1000UL) /* ticks per microsecond */
static WdgM_Substate WdgM_internalIsDeadlineHold(
const CounterType counter_id,
const WdgM_DeadlineSupervision *deadlineCP,
WdgM_runtime_DeadlineSupervision *runtime_deadlineCP)
{
TickType elapsedTicks = 0u;
// ... GetElapsedValue ...
if ((elapsedTicks >=
(deadlineCP->DeadlineMin * WDGM_OSTICKSPERUS))
&& (elapsedTicks <=
(deadlineCP->DeadlineMax * WDGM_OSTICKSPERUS)))
{
ret = WDGM_SUBSTATE_CORRECT;
}
return ret;
}
配置中 DeadlineMin/Max 的单位是微秒(μs),乘以 WDGM_OSTICKSPERUS(刻/μs)得到 OS tick 数。如果 OS 的 tick 周期是 1000ns(1μs),则 WDGM_OSTICKSPERUS = 1,微秒值直接等于 tick 数。
六、Logical Check:转换表验证
逻辑监控的核心是一个有向图,WdgM_SupervisedEntity 中定义的 Transitions[] 数组:
static WdgM_Substate WdgM_internal_LogicalMonitoring_GraphActive(
const WdgM_SupervisedEntity *se,
WdgM_runtime_SupervisedEntity *runtime_se,
WdgM_CheckpointIdType CPId)
{
WdgM_Substate retVal = WDGM_SUBSTATE_INCORRECT;
for(i = 0u; i < se->Length_Transitions; i++)
{
if(se->Transitions[i].CheckpointIdSource
== runtime_se->PreviousCheckpointId_internalLogic)
{
if(se->Transitions[i].CheckpointIdDestination == CPId)
{
retVal = WDGM_SUBSTATE_CORRECT;
runtime_se->PreviousCheckpointId_internalLogic = CPId;
break;
}
}
}
if (retVal == WDGM_SUBSTATE_CORRECT)
{
for(i = 0u; i < se->Length_FinalCheckpointIds; i++)
{
if (se->FinalCheckpointIds[i] == CPId)
{
runtime_se->IsInternalGraphActive = (boolean)FALSE;
break;
}
}
}
return retVal;
}
逻辑是两层的:如果图处于激活状态(IsInternalGraphActive == TRUE),每个 checkpoint 必须能与前一个 checkpoint 通过一条已定义的转换边相连。当到达 FinalCheckpoint 时,图进入非激活状态,等待下一个 StartCheckpoint 重新激活。
这种机制可以检测程序流篡改:如果某个 SE 按 CP1→CP2→CP3 正常执行,但某次走到了 CP1→CP4(CP4 不在合法转换表中),逻辑监控立刻捕获。原则上就是一个小型的程序流监控(CFC, Control Flow Checking),用纯 C 数据结构实现。
七、全局状态聚合:从 Local 到 Global
WdgM_internal_CalculateGlobalState() 遍历所有 SE,汇总状态:
static void WdgM_internal_CalculateLocalState(
WdgM_runtime_SupervisedEntity *runtime_se,
const WdgM_SupervisedEntityConfiguration *seConf,
const WdgM_runtime_SupervisedEntityConfig *runtime_seConf)
{
uint8 nrOfCorrectSubstates = runtime_se->SubstateAlive
+ runtime_se->SubstateDeadline + runtime_se->SubstateLogical;
switch (nrOfCorrectSubstates) {
case WDGM_ALLSUBSTATECORRECT: // = 3
break;
case WDGM_ONESUBSTATEINCORRECT: // = 2
if (runtime_se->LocalState == WDGM_LOCAL_STATUS_OK)
{
WdgM_internal_CalculateLocalState_FromOKOneError(...);
}
if (runtime_se->LocalState == WDGM_LOCAL_STATUS_FAILED)
{
WdgM_internal_CalculateLocalState_FromFailedOneError(...);
}
break;
default: // 0 or 1 substates correct
runtime_se->LocalState = WDGM_LOCAL_STATUS_EXPIRED;
break;
}
}
一个精妙的设计:Substate 定义为 #define WDGM_SUBSTATE_CORRECT (WdgM_Substate)0x01 。 那么 SubstateAlive + SubstateDeadline + SubstateLogical 的和就是正确的子状态个数!加起来直接做 switch-case 分发。0-1 个正确→直接 EXPIRED,2 个正确→进入宽容度计算(FailedAliveCyclesCounter),3 个正确→一切 OK。
然后是全局状态的转换逻辑,从 Global OK 出发:
static void WdgM_internal_updateFromGlobalOK(uint16 expired, uint16 failed)
{
if (failed > 0u)
{
WdgM_instance.GlobalState = WDGM_GLOBAL_STATUS_FAILED;
}
if (expired > 0u) {
if (WdgM_instance.runtimeDataPtr->Modes[
WdgM_instance.CurrentMode->Id].ExpiredSupervisionCycleCounter
< WdgM_instance.CurrentMode->ExpiredSupervisionCycleTol) {
WdgM_instance.GlobalState = WDGM_GLOBAL_STATUS_EXPIRED;
} else {
WdgM_instance.GlobalState = WDGM_GLOBAL_STATUS_STOPPED;
}
}
}
有任何一个 EXPIRED 的 SE?先进入 EXPIRED 状态,给系统最后 ExpiredSupervisionCycleTol 个 MainFunction 周期的时间自救。超时后→STOPPED→看门狗饿死→复位。
八、冗余取反存储:一个6行的安全亮点
在 WdgM_MainFunction → WdgM_internal_CalculateGlobalState 中,你会看到这一段:
if (WdgM_instance.isFirstExpiredSet == (boolean)FALSE)
{
firstExpiredSEID = i;
firstExpiredSEIDInverse = ~i;
WdgM_instance.isFirstExpiredSet = (boolean)TRUE;
}
而在 WdgM_GetFirstExpiredSEID 中:
uint16 invertedSEID = (~firstExpiredSEID);
if (invertedSEID == firstExpiredSEIDInverse) {
*SEID = firstExpiredSEID;
retVal = E_OK;
} else {
*SEID = 0u;
retVal = E_NOT_OK;
}
这是带冗余存储的故障检测:把同一个值 i 存为 firstExpiredSEID 和它的按位取反 firstExpiredSEIDInverse。读出时重新计算取反并比对。如果 RAM 中任意一个 bit 静默翻转了,取反后的值就不一致,函数返回 E_NOT_OK。这和 NASA 航天器代码中的“关键变量三重冗余“是同一种思想。
WdgM 的 1450 行 C 代码实现了一个完整的故障检测→状态转换→触发响应的闭环。从微观的冗余存储(取反校验)到中观的容忍度计算(-MinMargin/+MaxMargin)到宏观的状态机(OK→FAILED→EXPIRED→STOPPED),每一层都遵循“检测→容忍→升级→复位“的失败安全原则。它不是“检测到异常就 panic“。而是给了系统多次自救机会,只有确定无法恢复后才执行最终复位。
本篇小结
- WdgM 的 1450 行代码分为三层架构:配置层(const 结构体定义规则)→ 执行层(
WdgM_MainFunction120 行完成三重监督计算)→ 响应层(全局状态 STOPPED 后硬件看门狗饿死复位),遵循“按配置规则→周期检查→超时自杀“的 fail-safe 闭环。 - Init 阶段采用“先全部关停再只开初始模式“的双循环策略,且
isInitiated标志只在 WdgIf 激活成功后设为 TRUE,杜绝“僵尸模块“隐患。 - Alive 监控以
实际计数 - 期望值 ∈ [-MinMargin, +MaxMargin]的带符号整数比较实现容忍度,容忍度不是性能优化而是安全设计——应对 OS 调度抖动和中断延迟。 - Deadline 和 Logical 监控分别借助 OS 计数器和有向图转换表,检测时间越界和程序流篡改;全局状态聚合利用
Substate的枚举值(0x01)做加法求和后 switch 分发,0~1 个正确→直接 EXPIRED,2 个→进入容忍计算,3 个→OK。 - 冗余取反存储(
firstExpiredSEID+~firstExpiredSEIDInverse)仅 6 行代码,实现了 NASA 航天器级别的单比特翻转检测,是微观层面的 fail-safe 设计典范。
【下集预告】: WdgM 保护了“程序有没有在正确的时间沿着正确的路径执行“,但数据本身的完整性呢?下一节进入
E2E_P01.c的 680 行纯函数代码,看 CRC8+4-bit 计数器+DataID 如何组合成一道无法绕过的数学防线。一个让人不安的问题:如果 CRC 校验通过了,但 DataID 因为配置错误指向了另一个传感器,你还能信任这条消息吗?E2E 的答案是“不能“,并在 Check 函数中把这种情况和比特翻转一视同仁地拦截。