Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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_MainFunctionWdgM_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“。而是给了系统多次自救机会,只有确定无法恢复后才执行最终复位。

本篇小结

  1. WdgM 的 1450 行代码分为三层架构:配置层(const 结构体定义规则)→ 执行层(WdgM_MainFunction 120 行完成三重监督计算)→ 响应层(全局状态 STOPPED 后硬件看门狗饿死复位),遵循“按配置规则→周期检查→超时自杀“的 fail-safe 闭环。
  2. Init 阶段采用“先全部关停再只开初始模式“的双循环策略,且 isInitiated 标志只在 WdgIf 激活成功后设为 TRUE,杜绝“僵尸模块“隐患。
  3. Alive 监控以 实际计数 - 期望值 ∈ [-MinMargin, +MaxMargin] 的带符号整数比较实现容忍度,容忍度不是性能优化而是安全设计——应对 OS 调度抖动和中断延迟。
  4. Deadline 和 Logical 监控分别借助 OS 计数器和有向图转换表,检测时间越界和程序流篡改;全局状态聚合利用 Substate 的枚举值(0x01)做加法求和后 switch 分发,0~1 个正确→直接 EXPIRED,2 个→进入容忍计算,3 个→OK。
  5. 冗余取反存储(firstExpiredSEID + ~firstExpiredSEIDInverse)仅 6 行代码,实现了 NASA 航天器级别的单比特翻转检测,是微观层面的 fail-safe 设计典范。

【下集预告】: WdgM 保护了“程序有没有在正确的时间沿着正确的路径执行“,但数据本身的完整性呢?下一节进入 E2E_P01.c 的 680 行纯函数代码,看 CRC8+4-bit 计数器+DataID 如何组合成一道无法绕过的数学防线。一个让人不安的问题:如果 CRC 校验通过了,但 DataID 因为配置错误指向了另一个传感器,你还能信任这条消息吗?E2E 的答案是“不能“,并在 Check 函数中把这种情况和比特翻转一视同仁地拦截。