4. 数据对象与存储类设计

在Embedded Coder的代码生成流程中,模型里的信号和参数最终都要落到C代码里的变量、常量、结构体或宏上。决定“落成什么形态”的关键,不是模块本身,而是数据对象及其存储类(Storage Class)。如果只把模型搭好就直接生成代码,往往会得到一堆自动命名的局部变量,既无法标定,也无法与底层驱动、AUTOSAR接口或既有工程代码对接。因此,本节的核心任务是:用Simulink.Parameter、Simulink.Signal和总线对象把数据“显式化”,再通过存储类控制它们生成成什么样的C对象。

三类数据对象的分工

Simulink默认的数据管理是“隐式”的:参数值写在模块对话框里,信号线只有名字没有属性。工程化管理的做法是把它们提升为工作区中的对象,让代码生成器能读到类型、初值、存储类等元信息。

  • Simulink.Parameter:用于标定量和可调参数。它承载数值、数据类型、最小值/最大值、单位,以及最关键的StorageClass。例如把增益模块的Gain从字面量2.5改为参数对象Kp,后续就能生成可标定的全局变量。
  • Simulink.Signal:用于信号、状态和测量量。它描述信号的数据类型、维度、采样时间、初始值以及存储类。把关键信号解析为Simulink.Signal后,才能让它生成具名全局变量,供测量或跨模块访问。
  • 总线对象(Simulink.Bus / BusElement):用于把一组相关信号组织成结构体。总线对象定义元素名、数据类型、维度、采样时间,代码生成时对应C结构体类型,使接口清晰、可复用。

三者的关系可以这样理解:总线对象定义“结构体类型”,Simulink.Signal定义“结构体变量或普通信号变量”,Simulink.Parameter定义“标定/参数变量”。它们共同把模型数据从“图形连线”翻译成“工程可管理的C数据”。

存储类:控制变量生成方式的总开关

存储类决定一个数据对象在生成代码中的存在形式。常用的几类及其典型用途如下:

存储类生成形态典型用途
Auto由代码生成器决定(常为局部变量或内联)纯内部中间量,无需外部访问
ExportedGlobal全局变量,带extern声明需要被其他C文件或测量工具访问的信号
ImportedExtern引用外部已定义的全局变量与手写代码、驱动共享的变量
Const / Volatileconst或volatile修饰的变量只读标定表、被中断或硬件修改的变量
Define宏定义(#define)编译期常量,如数组长度、固定阈值
Struct结构体成员把多个标定量打包成标定结构体

选择存储类时先问三个问题:这个数据要不要在模型外部被读写?要不要在运行时被标定?要不要被测量或观测?答案决定了它是全局、结构体成员还是宏。需要特别注意的是,ExportedGlobal会引入全局命名空间,命名必须遵循工程规范,避免与手写代码冲突。

配置存储类的实际操作

以把增益参数Kp做成可标定全局变量为例,操作路径是:在模型资源管理器中新建Simulink.Parameter对象Kp,设置Value=2.5、DataType='single'、Min=0、Max=10,然后把StorageClass设为ExportedGlobal。将增益模块的增益值改为Kp后,生成代码中会出现类似:

/* Exported global parameter */
single Kp = 2.5F;

/* 在Step函数中使用 */
Y = Kp * U;

若把StorageClass改为Define,则生成#define Kp (2.5F),此时它不再是可标定量,而是编译期常量。可见存储类直接改变了变量的工程语义。

对于信号,把关键输出信号y_out解析为Simulink.Signal,设置StorageClass='ExportedGlobal'、DataType='single',生成代码中就会出现全局变量y_out,可在测量工具或其它模块中按名访问。若希望它只在本文件可见,可选用FileScope类存储类。

总线对象与结构体接口

当接口信号较多时,逐个定义全局变量会让接口散乱。此时用总线对象把一组信号打包。例如定义总线CtrlBus,包含元素ref、fb、err,类型均为single。代码生成后得到:

typedef struct {
  real32_T ref;
  real32_T fb;
  real32_T err;
} CtrlBus;

CtrlBus CtrlBus_DWork;   /* 若信号存储类为ExportedGlobal */

总线对象的价值在于:类型定义集中、接口稳定、便于跨模型复用。修改总线元素时,所有引用该总线的模型和代码会同步更新,减少手工维护结构体的错误。

标定与测量数据的工程化管理

标定量和测量量的管理目标不同。标定量要求“可改、可追溯、有范围保护”,因此通常使用Simulink.Parameter配合ExportedGlobal或Struct存储类,并设置Min/Max,让代码生成器在需要时插入范围检查。测量量要求“可观测、命名稳定”,因此使用Simulink.Signal配合ExportedGlobal或Volatile,保证变量不会被优化掉。

建议把标定量按功能分组,用Struct存储类打包成标定结构体,例如Cal_Param,成员为Kp、Ki、Kd。这样在标定工具中只需导入一个结构体,接口也更整洁。

下面这张图展示了从模型数据对象到生成C代码的映射关系,以及存储类在其中的控制作用。

数据对象 → 存储类 → 生成C代码 映射关系 模型数据对象 Simulink.Parameter Kp / Ki / Kd 标定量 Simulink.Signal y_out / err 测量量 Simulink.Bus CtrlBus 结构体类型 属性:类型/初值 Min/Max/单位 存储类 StorageClass ExportedGlobal ImportedExtern Const / Volatile Define Struct Auto / FileScope 生成的C代码 全局变量 single Kp = 2.5F; extern single y_out; 结构体类型 typedef struct { real32_T ref; ... 宏定义 #define BUF_LEN 16 #define Kp (2.5F) 标定/测量接口 A2L / 观测变量 绑定 绑定 绑定 决定 决定 决定

工程化管理的检查要点

完成数据对象和存储类配置后,建议按以下要点自查,避免生成代码与预期不符:

  1. 所有需要外部访问的标定量和测量量,是否都已解析为Simulink.Parameter或Simulink.Signal,而不是停留在字面量或自动命名信号?
  2. 存储类是否与用途匹配?可标定量不要误设为Define,否则无法在线修改;纯内部量不要滥用ExportedGlobal,否则污染全局命名空间。
  3. 标定量的Min/Max是否设置?这关系到代码生成器能否插入范围保护,也影响标定工具的安全边界。
  4. 总线对象的元素类型、维度、采样时间是否与模型实际信号一致?类型不匹配会导致生成代码编译错误或隐式转换。
  5. 命名是否符合工程规范?全局变量建议加模块前缀,如Ctrl_Kp,降低与手写代码冲突的概率。
存储类一旦确定,生成代码的接口形态就固定下来。若后期修改存储类,可能导致变量从全局变为局部、从变量变为宏,进而影响其它模块的链接和标定脚本。因此建议在模型架构阶段就规划好数据对象清单和存储类策略,而不是等到生成代码后再逐个修补。

把数据对象和存储类管理好,代码生成就从“能跑通”升级为“可标定、可测量、可集成”。这也是Embedded Coder工程化落地中最基础、却最容易被忽视的一环。