4.6 集成——WdgM_ConfigTypes.h 与静态配置的艺术
场景:在配置数据的“基因序列“中穿行
你已经看完了头五节的全部运行时代码,WdgM 的 Init/MainFunction、E2E 的 Protect/Check、Safety_Queue 的双 CRC、RamTst 的 March X。这些代码有一个共同特征:它们从不做运行时决策。没有 malloc,没有 if (runtime_flag) create_table(),没有动态创建的对象。
打开 WdgM_ConfigTypes.h。159 行。它包含了整个 WdgM 世界的静态蓝图,从最顶层的 WdgM_ConfigType 逐步展开到每一个监督实体、每一个 alive checkpoint、每一个 deadline 时间窗口、每一个逻辑转换边。这不仅是配置,这是系统行为在编译前被完整确定的“基因序列“。
一、从叶子到根:反序遍历配置树
从最底层的数据结构开始,逐层向上构建。
第一层:监督项
每个配置中的最小单位:
typedef struct
{
const uint16 TriggerConditionValue;
const WdgIf_ModeType WatchdogMode;
const uint8 WatchdogId;
} WdgM_Trigger;
一个 Trigger 定义了:给哪个看门狗(WatchdogId→映射到 WdgIf 设备 ID)、设什么模式(FAST/SLOW/OFF)、喂什么值。三行 struct,决定了 MainFunction 中 WdgIf_SetTriggerCondition 的全部参数。
typedef struct
{
const uint16 CheckpointId;
const uint16 ExpectedAliveIndications;
const uint8 MinMargin;
const uint8 MaxMargin;
const uint16 SupervisionReferenceCycle;
} WdgM_AliveSupervision;
一个 AliveSupervision 绑定一个 checkpointId,期望 ExpectedAliveIndications 次调用,容忍 +/-MinMargin/+MaxMargin,每 SupervisionReferenceCycle 个 MainFunction 周期评估一次。所有参数都是 <inttypes.h> 中的固定宽度类型:16 位无符号整数用于 ID 和计数,8 位无符号用于容忍度。没有任何类型是不精确的。这就是为嵌入式安全定制的数据模型。
typedef struct
{
const uint16 CheckpointIdStart;
const uint16 CheckpointIdFinish;
const uint32 DeadlineMin;
const uint32 DeadlineMax;
} WdgM_DeadlineSupervision;
DeadlineMin/Max 是 uint32,提供秒级精度,因为 Deadline 监控涉及 OS tick 的乘法和比较,需要宽位来防止溢出。
typedef struct
{
const uint16 CheckpointIdSource;
const uint16 CheckpointIdDestination;
} WdgM_InternalTransition;
逻辑监控的转换边,一对有序 checkpoint ID,定义了程序执行图中哪些路径是合法的。
第二层:监督实体定义
typedef struct
{
const uint16 Id;
const uint16 *CheckpointIds;
const uint16 Length_CheckpointIds;
const WdgM_InternalTransition *Transitions;
const uint16 Length_Transitions;
const uint16 *StartCheckpointIds;
const uint16 Length_StartCheckpointIds;
const uint16 *FinalCheckpointIds;
const uint16 Length_FinalCheckpointIds;
const boolean isOsApplicationRefSet;
const ApplicationType OsApplicationRef;
} WdgM_SupervisedEntity;
WdgM_SupervisedEntity 描述一个受监控的软件实体:它有一个唯一的 Id、一组 checkpoint、一张转换表、一组起始和终止 checkpoint。注意所有数组都是指针+长度组合,没有柔性数组成员,没有任何运行时长度推断。Length_xxx 字段的存在使得每个数组的边界在编译时已知,从而消除了所有越界访问的可能性。
isOsApplicationRefSet 是一个布尔标记,如果设为 TRUE,则 OsApplicationRef 有效,WdgM 可以关联到 OS Application 级别。
第三层:模式内的实体配置
typedef struct
{
const uint16 SupervisedEntityId;
const boolean CheckInternalLogic;
const WdgM_AliveSupervision *AliveSupervisions;
const uint16 Length_AliveSupervisions;
const WdgM_DeadlineSupervision *DeadlineSupervisions;
const uint16 Length_DeadlineSupervisions;
const uint8 FailedAliveSupervisionReferenceCycleTol;
const uint8 OSCounter;
} WdgM_SupervisedEntityConfiguration;
这才是模式上下文中对实体的“运行时配置“:指定该模式下哪些 alive/deadline 监控要生效、内部逻辑监控要不要开(CheckInternalLogic)、失败容忍多少个周期、使用哪个 OS Counter。注意这里没有 LogicSupervision 数组,逻辑监控的转换表在 WdgM_SupervisedEntity 中定义,是一个全局常量,不随模式切换而变。
FailedAliveSupervisionReferenceCycleTol 决定了“连续多少次 Alive 失败后才进入 EXPIRED“。这是局部状态从 FAILED 到 EXPIRED 的容忍度。
第四层:模式
typedef struct
{
const uint8 Id;
const uint16 ExpiredSupervisionCycleTol;
const uint32 SupervisionCycle;
const WdgM_SupervisedEntityConfiguration *SEConfigurations;
const uint16 Length_SEConfigurations;
const WdgM_Trigger *Triggers;
const uint8 Length_Triggers;
} WdgM_Mode;
一个 Mode 定义了:全局标识 ID、全局 EXPIRED→STOPPED 容忍周期数、哪些 SE 在当前模式下活跃、喂哪些看门狗。
ExpiredSupervisionCycleTol 和 SupervisionCycle 是模式级别而不是实体级别的参数,意味着整个模式共享同样的“宽限期“。这是 AUTOSAR 规范的设计选择:模式切换本身是全局事件,所以容忍度也应该全局统一。
第五层:General 和 ConfigSet
typedef struct
{
const WdgM_SupervisedEntity *SupervisedEntities;
const uint16 Length_SupervisedEntities;
const WdgM_Watchdog *Watchdogs;
const uint8 Length_Watchdogs;
} WdgM_General;
typedef struct
{
#if defined(USE_DEM)
const WdgM_DEMEventIdRefs DemEventIdRefs;
#endif
const uint8 initialModeId;
const WdgM_Mode *Modes;
const uint8 Length_Modes;
} WdgM_ConfigSet;
General 包含两个全局数组:
SupervisedEntities[]:所有受监控实体的定义(它们的转换表、checkpoint 清单)Watchdogs[]:所有硬件看门狗的映射(每个 WatchdogId 对应一个 WdgIf 设备 ID)
ConfigSet 包含:
DemEventIdRefs:两个 DEM 事件 ID,发送端(Supervision 失败事件)和模式设置失败事件(SetMode 失败事件)initialModeId:启动后进入的模式Modes[]:所有模式的定义
顶层:封装
typedef struct
{
const WdgM_General General;
const WdgM_ConfigSet ConfigSet;
} WdgM_ConfigType;
extern const WdgM_ConfigType WdgMConfig;
到了顶层,一切变成了一个全局 const 符号 WdgMConfig,链接器把它放在 .rodata 段(或 Flash 映射的地址空间)。WdgM_Init(&WdgMConfig) 一个指针就启动了整个监控世界。
二、“全 const“的安全论证
回头看整个配置树,从叶子节点到根节点,每一个字段都是 const。这意味着:
-
物理只读:如果
.rodata映射到 Flash 的只读区域(这是 Cortex-R5 的默认行为),任何试图修改配置的代码都会触发 MPU fault,硬件层面阻止配置篡改。 -
编译时决策:所有数组的长度都是显式的
Length_xxx字段。编译器知道每个数组的大小,静态分析工具(如 Polyspace、Astree)可以证明没有越界访问。 -
无间接跳转:没有函数指针在配置中(注意
WdgM_Trigger只有值和模式,没有回调)。所有执行路径在编译时确定。 -
可证明性:因为配置是静态常量,可以编写形式化的配置验证工具:“这个模式中的 SE 的 AliveSupervision 的 CheckpointId 是否在 SE 定义的 CheckpointIds 数组中?”,答案是纯逻辑推理,不需要执行。
这是一个有力的安全论证:如果配置有错误,它会在编译/链接阶段暴露(通过 #error 和版本检查宏),不会在运行时默默失效。如果配置编译通过且所有静态分析通过,那么配置的结构完整性是证明的,不需要运行时验证。
三、EOL(End-of-List)终止模式
看代码中没有显式的 EOL 标记,没有 {0, 0, 0, ...} 哨兵值。那怎么知道数组在哪结束?
答案就是 Length_xxx 字段。WdgM_Mode 迭代时遍历的边界是 Length_SEConfigurations:
for (i = 0u; i < WdgM_instance.CurrentMode->Length_SEConfigurations; i++)
这种模式有两个优势:
- 不需要哨兵值:哨兵值可能与有效数据冲突(比如 checkpointId=0 可能合法)
- 边界在 O(1) 内获取:不需要线性扫描找 EOL,
Length_xxx直接给出
AUTOSAR 规范强制要求这种模式。对比其他嵌入式平台的常见做法(用 {NULL, 0, 0, 0} 终止数组),WdgM 的显式长度是更安全的 O(1) 方法。
四、WdgIf 映射链
从 WdgM 的配置走到硬件看门狗的信号链:
WdgM_Mode.Triggers[i].WatchdogId
→ WdgM_General.Watchdogs[WatchdogId].WatchdogDeviceId
→ WdgIf_SetMode(DeviceIndex, mode)
→ WdgIf_SetTriggerCondition(DeviceIndex, value)
这个映射链的每一环都是编译时确定的索引:配置生成工具(如 Vector DaVinci)确保 WatchdogId 永远不会越界。源代码如下:
uint8 DeviceIndex = WdgM_instance.ConfigPtr->General.Watchdogs[
Mode->Triggers[i].WatchdogId].WatchdogDeviceId;
(void)DeviceIndex; /* Remove compile warning when using
only one watch dog driver */
Std_ReturnType potError = WdgIf_SetMode(
DeviceIndex, Mode->Triggers[i].WatchdogMode);
(void)DeviceIndex 这个惯用法值得玩味:当配置中只有一个看门狗时,DeviceIndex 被优化器发现只使用一次(在 WdgIf 调用中),导致“变量未使用“警告。(void) 显式压制了警告。这是静态配置代码中常见的代码气味处理。
五、DEM 事件的可选编译
#if defined(USE_DEM)
typedef struct
{
const Dem_EventIdType Supervision;
const Dem_EventIdType SetMode;
} WdgM_DEMEventIdRefs;
#endif
如果 USE_DEM 未定义,WdgM_DEMEventIdRefs 根本不存在于结构中。ConfigSet 变为:
typedef struct
{
#if defined(USE_DEM)
const WdgM_DEMEventIdRefs DemEventIdRefs;
#endif
const uint8 initialModeId;
const WdgM_Mode *Modes;
const uint8 Length_Modes;
} WdgM_ConfigSet;
#if 在有 DEM 时插入、无 DEM 时消失,结构体的内存布局在两种配置下不同。这是 AUTOSAR 中常见的“编译时多态“,不是通过虚函数表,而是通过预处理器。优点是无 DEM 时节省了 4 字节(或 8 字节,取决于 Dem_EventIdType 的大小),并且代码中调用 DEM_EVENT_ID_NULL 的地方被宏替换为 0(在 WdgM.c 开头定义)。
六、运行时数据结构:配置的影子
配置是只读的,但 WdgM 还有一套对应的运行时数据结构,在 WdgM 的私有头文件中定义。你已经在代码中见过:
WdgM_runtime_SupervisedEntity:存储 LocalState、SubstateAlive/Deadline/Logical、FailedAliveCyclesCounter、PreviousCheckpointId_internalLogic、IsInternalGraphActiveWdgM_runtime_SupervisedEntityConfig:存储每个 AliveSupervision 的计数器、每个 DeadlineSupervision 的 LastTickValueWdgM_runtime_Mode,存储 ExpiredSupervisionCycleCounter
运行时结构体镜像配置结构体的树形关系,但字段是 uint16 而不是 const uint16,可变但类型相同。
实例化:
WdgM_debuggable_runtimeData WdgM_instance = {
.GlobalState = WDGM_GLOBAL_STATUS_DEACTIVATED,
.isInitiated = (boolean)FALSE
};
WdgM_instance 是全局变量,这是 WdgM 模块中唯一的全局 mutable 状态。所有运行时状态的根都在这个结构体中,没有其他分散的全局变量(除了 firstExpiredSEID 和 firstExpiredSEIDInverse 这两个冗余存储变量)。这种“单一全局状态根“(Single Global State Root)是安全编码的最佳实践,状态完整性的断言只需要检查一个结构体的 CRC(如果实现了的话)。
七、版本检查的最后一环
别忽视 WdgM_ConfigTypes.h 开头的包括和版本依赖性:
#include "WdgM_Cfg.h"
#include "WdgIf.h"
#if defined(USE_DEM)
#include "Dem.h"
#endif
#include "Os.h"
WdgM_Cfg.h 是配置生成工具的输出:它定义了 WDGM_DEV_ERROR_DETECT、USE_DEM、USE_RTE 等编译开关。这些开关决定了哪些结构体、哪些字段、哪些代码路径会被编译。可以说 WdgM_Cfg.h 是配置树的“根部“,它连接了 WdgM_ConfigTypes.h 的类型定义和 WdgM.c 的行为实现。
八、全栈对接:配置如何驱动行为
总结整个配置→运行时的映射:
| 配置结构体 | 驱动哪些运行时行为 |
|---|---|
WdgM_ConfigType | WdgM_Init 的唯一参数 |
WdgM_Mode.Id | 模式切换的索引 |
WdgM_Mode.Triggers[] | MainFunction 喂狗的参数 |
WdgM_Mode.SEConfigurations[] | 当前活跃的 SE 列表 |
WdgM_Mode.ExpiredSupervisionCycleTol | GlobalState EXPIRED→STOPPED 的时机 |
WdgM_SEConfiguration.AliveSupervisions[] | Alive 期望和容忍度计算 |
WdgM_SEConfiguration.DeadlineSupervisions[] | Deadline 起止和时间窗口 |
WdgM_SupervisedEntity.Transitions[] | Logical 监控的转换表验证 |
WdgM_ConfigSet.DemEventIdRefs | DEM 事件报告 |
WdgM_ConfigSet.initialModeId | 启动后首个模式 |
每一个运行时决策都可以从配置结构体中逐字段追溯到其在代码中的引用位置。这不仅是代码理解的需要,在 ISO 26262 的安全审计中,配置参数到执行路径的完整可追溯性是强制性要求。
“静态配置“的真正力量不在编译时,而在 Safety Case(安全性论证)。当你说“这个系统在运行时不会发生配置错误”,你的证据不是写了多少 if-else 来兜底,而是“配置是 const 的、放在只读内存的、在编译前由工具链验证的“。静态配置把“可能出错“的代码变成了“不可能编译通过“的代码,你不需要证明软件能处理所有配置错误,你只需要证明错误的配置无法被编译和链接。
本篇小结
WdgM_ConfigTypes.h的 159 行定义了从WdgM_Trigger到WdgM_ConfigType的完整配置树,所有字段均为const,链接时放入.rodata只读段,任何运行时篡改尝试都会触发 MPU fault。- 配置树采用“叶子→根“的反向构建:
WdgM_AliveSupervision/WdgM_DeadlineSupervision/WdgM_InternalTransition→WdgM_SupervisedEntity→WdgM_SupervisedEntityConfiguration→WdgM_Mode→WdgM_ConfigSet+WdgM_General→WdgM_ConfigType,每层均通过Length_xxx显式数组长度消除越界风险。 - 运行时数据结构(
WdgM_runtime_*)镜像配置树的树形结构,但字段为可变uint16;唯一的全局 mutable 状态根是WdgM_instance,符合“单一全局状态根“的安全编码最佳实践。 - DEM 事件 ID 通过
#if defined(USE_DEM)条件编译控制结构体内存布局,无 DEM 时节省 4~8 字节——这是 AUTOSAR 的“编译时多态“,非虚函数表,纯预处理器实现。 - 静态配置的真正价值在 Safety Case:不是运行时写一堆 if-else 兜底,而是让错误配置无法通过编译和链接——“可能出错“的代码变成了“不可能编译通过“的代码。
【下集预告】: 这一章的源码走读让你在 Arctic Core 的六千多行安全代码中完成了一次深度潜泳。但只知道“别人怎么写“还不够,第五章你将亲手构建一个名为 safe-lite 的教学级安全框架,从零编写看门狗、E2E、安全队列和故障注入器。四章的理论积累将在下一节转化为你的肌肉记忆:看懂一个安全系统和能写出来一个,是完全不同的两件事。