2. MBD模型与生成代码基础

在基于模型的设计(Model-Based Design,MBD)流程中,Simulink/Stateflow 模型是设计意图的“唯一可信源”,而嵌入式 C 代码则是最终烧录到控制器中的执行载体。要验证两者的一致性,首先必须理解模型语义如何被代码生成器翻译成 C 代码,以及哪些环节天然存在差异。本节从模型语义、代码生成器工作原理、转换规则和差异来源四个层面展开。

一、Simulink/Stateflow 模型语义

Simulink 模型在语义上是一个带方向的信号流图,由模块(Block)、信号线(Signal Line)和求解器(Solver)共同定义。每个模块在数学上对应一个输入输出映射,例如 Gain 模块实现 y = K * u,Sum 模块实现 y = u1 + u2。信号线不仅传递数值,还携带数据类型、维度、采样时间等属性,这些属性直接决定生成代码中的变量类型和运算方式。

Stateflow 则描述事件驱动的状态机语义,包含状态(State)、转移(Transition)、条件(Condition)和动作(Action)。其执行语义遵循“事件触发—条件判断—状态迁移—动作执行”的顺序,且区分进入动作(entry)、退出动作(exit)、转移动作(during)等不同执行时机。这些语义在代码生成时会被翻译为 switch-case 结构或状态变量更新逻辑。

需要特别注意的是,Simulink 的求解器分为固定步长和变步长两类。在嵌入式代码生成中,通常只使用固定步长离散求解器,因为生成代码必须具有确定性的执行时序。模型中的采样时间(Sample Time)决定了代码中每个任务的调用周期,这是模型与代码一致性的重要锚点。

二、代码生成器工作原理

以 Embedded Coder 为例,代码生成过程大致分为四个阶段:模型编译与语义分析、中间表示(IR)生成、优化与代码映射、目标代码输出。

第一阶段,代码生成器读取模型,解析模块参数、信号属性、采样时间,构建出完整的计算图。此时会进行类型检查、维度检查和采样时间一致性检查,任何不满足条件的模型都会在编译阶段报错。

第二阶段,生成中间表示。这一层将 Simulink 模块映射为更接近 C 语言的计算节点,例如将 Gain 模块映射为乘法表达式,将 Unit Delay 映射为带状态变量的赋值语句。中间表示是模型语义与目标代码之间的桥梁。

第三阶段,进行优化和代码映射。优化包括常量折叠、死代码消除、表达式简化等;代码映射则决定哪些信号生成全局变量、哪些生成局部变量、哪些直接内联。这一阶段直接影响最终代码的结构和可读性。

第四阶段,输出目标代码。生成的文件通常包括模型主文件(.c)、头文件(.h)、数据类型定义文件、以及可能的标定文件(.a2l)和 ASAP2 描述文件。代码中会包含模型初始化函数、步进函数和终止函数,分别对应模型的初始化、每个采样周期的计算和终止清理。

Simulink/Stateflow 模型到 C 代码的转换链路与差异来源 Simulink 模型 模块参数 / 信号属性 采样时间 / 数据类型 求解器配置 Stateflow 状态机 状态 / 转移 / 条件 entry / during / exit 事件驱动语义 代码生成器 ① 模型编译与语义分析 ② 中间表示(IR)生成 ③ 优化与代码映射 ④ 目标代码输出 常量折叠 / 死代码消除 变量分类 / 内联决策 生成 C 代码 模型主文件 .c 头文件 .h 数据类型定义 标定文件 .a2l 初始化 / 步进 / 终止函数 全局变量 / 局部变量 常见差异来源 数据类型差异 饱和 / 舍入 / 溢出 运算顺序差异 浮点结合律 / 优化重排 采样时序差异 多速率 / 任务调度 状态初始化差异 初值 / 复位行为

三、模型到代码的转换规则

理解转换规则是定位差异的前提。以下列出几类核心映射关系。

模型元素 生成代码形式 关键注意点
Gain 模块 y = K * u; K 的数据类型决定是否发生隐式转换
Unit Delay y = state; state = u; 状态变量初值来自模块参数
Sum 模块 y = u1 + u2; 累加顺序可能被优化重排
Switch 模块 y = (cond) ? u1 : u2; 条件表达式可能被简化
Stateflow 状态 switch(state_var) { case ... } 状态编码方式影响可读性
Lookup Table 查表函数或内联插值 插值算法和边界处理需一致

以 Unit Delay 为例,模型中的初值参数为 InitialCondition = 0.5,生成代码中会体现为状态变量初始化:

/* 模型初始化函数 */
void Model_Init(void) {
    Model_DW.UnitDelay_DSTATE = 0.5;  /* 对应 InitialCondition */
}

/* 模型步进函数 */
void Model_Step(void) {
    real_T u = Model_U.Inport1;
    Model_Y.Outport1 = Model_DW.UnitDelay_DSTATE;
    Model_DW.UnitDelay_DSTATE = u;
}

可以看到,模型中的“初值”被翻译为初始化函数中的赋值,而“延迟一个采样周期”被翻译为“先读状态、后写状态”的顺序。如果验证时只比较步进函数的输出,而忽略初始化函数,就可能漏掉初值不一致的问题。

四、常见差异来源

即使模型和代码来自同一套工具链,差异仍然可能出现在以下环节。

  • 数据类型与量化:模型中使用 double 类型,但目标硬件只支持 single 或定点。代码生成器会插入类型转换,转换时的饱和、舍入方式若与模型仿真设置不一致,就会产生数值偏差。
  • 运算顺序与浮点结合律:编译器优化可能重排浮点加法顺序,而浮点加法不满足结合律,导致结果在最低有效位不同。这类差异通常很小,但在积分器、滤波器等累积运算中会被放大。
  • 采样时序与任务调度:多速率模型中,不同采样时间的模块在代码中可能被分配到不同任务。如果任务调度顺序与模型求解顺序不一致,跨速率信号的值就会不同。
  • 状态初始化与复位:模型中的状态初值、使能复位行为、Stateflow 的默认转移等,在代码中可能因初始化顺序或复位逻辑的实现方式而产生差异。
  • 查表与插值:Lookup Table 的插值算法、边界外推方式、索引计算在模型和代码中若配置不同,输出会有明显偏差。
在一致性验证中,建议优先锁定采样时间和数据类型这两项“硬约束”。它们一旦不一致,后续所有信号比较都失去意义。确认这两项一致后,再逐步排查运算顺序、状态初值和查表实现等“软差异”。

理解上述转换规则和差异来源后,就可以有针对性地设计验证用例:对纯组合逻辑,重点比较单步输出;对含状态的模块,重点比较多周期累积输出和初始化后的首周期输出;对多速率模型,重点比较跨速率边界的信号值。这样才能在模型与代码之间建立可追溯、可解释的一致性证据链。