- 偏差计算模型的代码生成
- 搭建Simulink模型
- 模型配置参数
- 代码生成
- 管理数据
- 代码生成的存储类介绍
- 函数名称、引用参数
- 代码调用
- 设置可重用函数名、文件名
- 为Step函数设置引用参数
- 复制模块
- 条件代码
- 参考
本文以一个简单模型例子,来说明模型生成代码.
偏差计算模型的代码生成
搭建Simulink模型
模型文件名:my_first_code_xyz.slx
搭建Simlink模型:

其中,k在Matlab工作区间中设置为2.
这个模型计算 输入In1、In2的和,然后乘以k后,求绝对值,作为Out1输出.
运算过程等效于:out1 = |(in1 - in2)*k|,即计算2个输入信号的偏差,并进行比例运算,再求绝对值.
输入模块相当于某些对象的状态,称为信号(Signal). 信号不断变化,所以输出不断变化.
增益为k是人为改变的,不常变化,称为参数(Parameter).
模型配置参数
生成嵌入式C代码,最少需配置3部分:模型的求解器,模型的系统目标文件,硬件实现.
不同配置会对模型或者生成代码产生影响,需要谨慎配置.
- 求解器配置
右键模型空白处> 模型配置参数(Ctrl + E),打开求解器选项卡,配置如下:
求解器设置为 定步长,离散(无连续状态),固定步长设置为 0.0001s

- 系统目标文件配置

因为我们使用EmbeddedCoder生成代码,所以系统目标文件要勾选为 如图所示 ert.tlc

勾选了 “创建代码生成报告”“自动打开报告”“生成模型Web视图”,编译完成后,就会自动生成并打开报告、网页版模型,供用户查看. 非常方便,推荐勾选.
代码生成
点击APP> Embedded Coder,可显示C代码标签:

点击生成代码(或Ctrl + B),开始生成代码. 自动生成的代码都在 filename_ert_rtw 文件夹下.

“filename”是模型文件名,后面系统默认添加. 生成代码同时,会生成报告.
生成的代码所在目录:

生成的报告:

代码生成的内容结构:
| 文件名 | 说明 |
|---|---|
| ert_main.c | 含一个主函数,调用模型的初始化函数和 Step 函数. 正常开发,该文件不会被用到,因为main函数通常在MCU开发环境中编写(如Keil).因此,ert_main.c 可不生成 |
| model.c | 存放 Simulink 模型生成代码的文件,包含数据及代码的入口函数,包含三个重要函数: • model_initialize 初始化函数 • model_step 算法运行函数 • model_terminate 终止函数(一般为空) |
| model.h | 算法函数头文件 |
| model_private.h | 模型和子系统的局部宏与局部数据,被 model.c 引用为头文件 |
| model_types.h | 模型数据结构和参数数据结构预先声明(自定义数据结构) |
| rtwtypes.h | 定义数据类型、结构体和宏,打开 rtwtypes.h 头文件可以看到许多 typedef 的类型定义. 定义和 Simulink 中的 “配置参数→硬件实现→设备详细信息” 一致 |
| model_data.c | 模型中存在非内嵌参数时才产生,主要是模型参数数据结构和常量数据结构,只有在需要时才生成(本书未涉及) |
注:model 就是模型文件名,如my_first_code_xyz.
生成的 Step 函数:
/* Model step function */
void my_first_code_xyz_step(void)
{/* Outport: '<Root>/Out1' incorporates:* Abs: '<Root>/Abs'* Gain: '<Root>/Gain'* Inport: '<Root>/In1'* Inport: '<Root>/In2'* Sum: '<Root>/Sum'*/my_first_code_xyz_Y.Out1 = fabs((my_first_code_xyz_U.In1 -my_first_code_xyz_U.In2) * 2.0);
}
Step 函数 与模型策略完全一致. 如果想验证某个模型生成代码,查看Step 函数即可.
代码中,“_Y” 代表输出,“_U” 代表输入.
自动生成代码的结构体名:
| 结构体名称 | 变量名 |
|---|---|
| 实时模型数据结构体 | model_M |
| 参数结构体 | model_P |
| 外部输入结构体 | model_U |
| 外部输出结构体 | model_Y |
| 模块输入输出结构体 | model_B |
| 离散状态结构体 | model_DW (model_DWork) |
说明:model 指自定义模型文件名.
管理数据
自动生成的代码可读性很低(见上),通过数据管理,为被管理数据起名字,能增加代码可读性.
例如,2个输入分别命名为x,y,输出为z:

这里xyz就是信号线的名字,属于Simulink的Signal信号对象.
如此,计算关系可写成:z = |(x - y) * k|
- 新建数据字典文件
点击菜单栏 建模> 模型设置> 模型属性,或者 右键模型空白处 > 模型属性,在外部数据标签中新建 数据字典文件. 我们将其命名为 my_xyz_data.sldd(用户自定义).


- 查看数据字典
点击菜单栏 建模> 模型资源管理器,或者 快捷键 Ctrl+H,就能看到刚刚新建的数据字典 my_xyz_data


打开 my_xyz_data的Design Data,目前什么都没有.
- 添加Signal
点击如下工具栏图标,或者 菜单栏 添加> Simulink Signal(Ctrl + S),添加3个Simulink信号(Signal),重命名为x,y,z.

列视图切换到 Data Objects,z的存储类选择 ExportedGlobal、x和y的存储类都选择ImportedExtern.

注:上图中 Simulink.Signal:z 表示我们用Simulink 包中的Signal 类创建了对象z.
保存文件,再次代码生成. 查看生成的 my_first_code_xyz_step 函数,发现并没有任何变化,这是因为模型的信号线还没有跟我们创建的Signal对象关联到一起.
- 关联信号线与Signal对象
右键信号线x> 属性,勾选 “信号名称必须解析为 Simulink 信号对象”,将信号与Signal对象关联. 如此,信号名称旁边会多一个 “简约线条小耙子”.

对信号线y、z做同样操作,然后重新生成代码. 发现 Step函数变成了x、y、z描述的形式:
/* Model step function */
void my_first_code_xyz_step(void)
{/* Abs: '<Root>/Abs' incorporates:* Gain: '<Root>/Gain'* Inport: '<Root>/In1'* Inport: '<Root>/In2'* Sum: '<Root>/Sum'*/z = fabs((x - y) * 2.0);
}
- 添加 Simulink 参数k
之前,参数k是在Matlab工作区间设置值,实际应用很不方便. 我们在数据字典中添加参数k,初值设为2,存储类选择 ExportedGlobal.
注意:为避免重复定义,需要删除Matlab工作区变量k.
点击 如图所示,工具栏 添加 Simulink Parameter按钮(或者菜单栏 添加> Simulink Parameter,Ctrl + P)


保存后重新生成代码:
/* Exported block parameters */
real_T k = 2.0; /* Variable: k* Referenced by: '<Root>/Gain'*//* Model step function */
void my_first_code_xyz_step(void)
{/* Abs: '<Root>/Abs' incorporates:* Gain: '<Root>/Gain'* Inport: '<Root>/In1'* Inport: '<Root>/In2'* Sum: '<Root>/Sum'*/z = fabs((x - y) * k);
}
我们发现参数k是一个内存变量,容易被其他模块修改. 如果我们希望参数k 在标定中不会改变,那么可以将k的存储类改为 ConstVolatile

生成的k定义代码:
/* ConstVolatile memory section */
/* Definition for custom storage class: ConstVolatile */
const volatile real_T k = 2.0; /* Referenced by: '<Root>/Gain' */
这样,k就会放到 ConstVolatile 存储区域. 我们可以通过 链接该区域到指定Flash/Ram区域,从而控制其属性.
如此,一个清晰的 Step算法函数就完成了,我们可以在MCU中断处理函数中调用 my_first_code_xyz_step 函数,例如:
ISR_time_10ms() {X=read_AD0();Y=read_AD1();my_first_code_xyz_step();PWM_output();
}
代码生成的存储类介绍
Matlab2023b 生成的存储类有十多个类型

ExportedGlobal: 表示将其输出为全局变量,在Simulink 代码生成中进行全局变量的定义. 在.c 文件内定义,.h 中用 extern 关键字声明.
ExportedGlobal 适用于Outport的信号线,因为Inport的输入量通常是别的模型产生,Outport通常是自己的模型产生.
ImportedExtern: 表示外部引入的变量,在Simulink 代码生成中只声明,不定义. Step函数用到了改变量,但没有在本文件中定义它. 在private.h文件中,可以看到该头文件中外部声明了这个变量.
ImportedExtern 适用于 Inport端口的信号线.
Volatile: 表示可变内存变量,选它会多出3个用户参数
- HeaderFile 指头文件,变量外部声明就在这里
- DefinitionFile 指c文件,变量定义就在这里
- Owner 指定义变量的人员名称,可不填
FileScope: 表示静态全局变量,只在文件内调用,不能在模型Root层级使用.
Matlab 函数代码生成限制很多,例如不支持动态内存分配,不支持元胞数组. 很多Matlab函数、工具包不能生成代码,具体可查阅Matlab Function帮助文件.
注意:元胞数组(Cell Array)是 MATLAB 独有的复合容器数组,每个元素(元胞 cell)可以存放不同类型、不同尺寸的数据,普通数组只能存同类型同维度数值.
函数名称、引用参数
到目前为止,自动生成的代码包括 Initialize函数,Step函数,Terminate函数,这些函数名称我们能否自定义?
答案是可以. 菜单栏 C代码> 个体因素代码映射,就会显示 代码映射 - 组件接口 页面:


如下图,将C Step函数命名为 error_xyz,勾选 “为单步函数原型配置参数”,点击 获取默认值 按钮,修改参数标识符名称为 in_x, in_y, out_z.

修改成功后,Step 函数原型变成:

如果直接生成代码,会报错:无法使用函数原型控制,因为连接到端口In1的信号的存储类不是Auto. 这是因为 Simunlink根层级的 In/Out 模块存储类必须为 Auto. 如果给顶层 In 设 ConstVolatile,相当于要求函数形参是 const volatile real_T In1,而嵌入式代码生成器默认标准接口不允许形参带 const/volatile 修饰,语法冲突,直接编译报错. Auto 存储类代表 “由编译器自动分配栈空间,作为函数局部参数”,完全匹配 C 函数形参逻辑.
所以,我们将In1,In2,Out1对应的信号x,y,z存储类修改为Auto.

重新生成代码,Step函数:
/* Model step function */
void error_xyz(real_T in_x, real_T in_y, real_T *out_z)
{/* Outport: '<Root>/Out1' incorporates:* Abs: '<Root>/Abs'* Gain: '<Root>/Gain'* Inport: '<Root>/In1'* Inport: '<Root>/In2'* Sum: '<Root>/Sum'*/*out_z = fabs((in_x - in_y) * k);
}
不过,函数形成名称 in_x, in_y, out_z 还是很奇怪,我们可以将其修改为x, y, z. 生成的代码,形参就会跟我们设置的一致.


代码调用
如果想反复调用同一段代码,怎么办?
可以将其提炼为一个函数. 对于Simulink模型,可将重复使用的模块配置成原子子系统(atomic subsystem),生成一个单独的文件,供Step函数调用. 原子系统可设置一些生成代码属性,如何重用、不可重用函数、函数名、源文件名设置等.
对于我们的模型,选中Sum、Gain、Abs模块,右键> 基于所选内容创建子系统(Ctrl + G),创建子系统:


设置可重用函数名、文件名
右键封装的模块> 模块参数(P),

勾选视为原子单元,

可根据自己的喜好和需要,设置函数名称、文件名称:

因为用子系统将模块封装起来了,所以x,y,z不再是顶层,将x,y,z存储类还原成ImportedExtern,ImportedExtern,ExportedExtern,且Step函数还原成默认的 my_first_code_xyz_step(设置为空即可):


然后,重新生成代码,发现Step函数中多了一个 我们命名的 my_error_xyz 函数,同时多了子系统文件 my_error_xyz.h/.c

Step 函数:
/* Model step function */
void error_xyz(real_T x, real_T y, real_T *z)
{/* Outputs for Atomic SubSystem: '<Root>/Subsystem' *//* Outport: '<Root>/Out1' incorporates:* Inport: '<Root>/In1'* Inport: '<Root>/In2'*/*z = my_error_xyz(x, y);/* End of Outputs for SubSystem: '<Root>/Subsystem' */
}
可重用代码:
/* Output and update for atomic system: '<Root>/Subsystem' */
real_T my_error_xyz(real_T rtu_x, real_T rtu_y)
{/* Abs: '<S1>/Abs' incorporates:* Gain: '<S1>/Gain'* Sum: '<S1>/Sum'*/return fabs((rtu_x - rtu_y) * k);
}
为Step函数设置引用参数
点击 代码映射 - 组件接口 > 函数名 my_first_code_xyz_step,

按下图设置 my_first_code_xyz_step 形参名称,

然后,将C返回参数类型 由void 改为 Out1.
重新生成代码,会发现报错:
错误:潜在的标识符 'x' 用法冲突: the signal connected to output port 1 of the block
'my_first_code_xyz/In1', and the name of an argument of a top level function for the model
'my_first_code_xyz'
(按Ctrl + H)我们将 x、y、z的存储类修改为Auto:

生成代码,Step 函数:
/* Model step function */
real_T my_first_code_xyz_step(real_T x, real_T y)
{/* specified return value */real_T z;/* Outputs for Atomic SubSystem: '<Root>/Subsystem' *//* Outport: '<Root>/Out1' incorporates:* Inport: '<Root>/In1'* Inport: '<Root>/In2'*/z = my_error_xyz(x, y);/* End of Outputs for SubSystem: '<Root>/Subsystem' */return z;
}
这里的x,y,z就是我们上面配置C Step函数接口时指定的形参名、返回值名. 也可以改成其他名称,如a,b,c.
复制模块
复制所有模型:

新建与信号线名称一致的信号x1, y1:

此时,如果生成代码,会产生如下代码:
/* Model step function */
real_T my_first_code_xyz_step(real_T x, real_T y, real_T *arg_Out2, real_T*arg_In3, real_T *arg_In4)
{real_T z;z = my_error_xyz(x, y);*arg_Out2 = my_error_xyz(*arg_In4, *arg_In3);return z;
}
说明第二组参数与数据字典中定义的信号名尚未关联. 需要在 代码映射 - 组件接口 中点击函数名设置:

生成的代码,信号线名称x1, y1将与数据字典名称保持一致:
/* Model step function */
real_T my_first_code_xyz_step(real_T x, real_T y, real_T *z1, real_T *x1, real_T*y1)
{/* specified return value */real_T z;z = my_error_xyz(x, y);*z1 = my_error_xyz(*y1, *x1);return z;
}
输出端口可以有多个,但返回参数只能有一个,可以根据需要选择. 下图将返回参数修改为z1:

生成代码:
/* Model step function */
real_T my_first_code_xyz_step(real_T x, real_T y, real_T *x1, real_T *y1, real_T*z)
{/* specified return value */real_T z1;*z = my_error_xyz(x, y);z1 = my_error_xyz(*y1, *x1);return z1;
}
注意:同样的原子子系统的输入输出类型应该保持一致,以有效降低代码量. 如果接口类型不同,强制指定原子子系统对应函数的名称,会导致代码生成出错.
条件代码
我们的ECU项目,可能包含上百甚至上千个组件. 不同的车型,尽管配置在某些方面相似,但可能有不同的属性,如不同的标定数据,不同的通信协议,而用 Variant Subystem(变体模块),可以对具有多种配置的汽车进行模型设计,能有效帮助管理模型和标定数据.
变体模块可以通过判断条件,内部执行不同逻辑,也能以宏的形式生成代码. 类似于 C条件编译.
继续前面的例子,用Variant Subystem 来管理我们项目. 为模型添加Variant Subystem,该模型位于 Simulink > Port & Subsysystems:

我们将模型复制到 变体模块的子系统和子系统1.
子系统模块的内容:

子系统1模块的内容:

注意:第一个模型保持Sum为“|+-”,第二个模型修改了Sum模型为"|++"
因为Variant Subsystem的不同分支内,不允许内部信号线绑定到相同 Signal 对象,所以我们取消分支内的信号线勾选【必须解析到信号对象】绑定 x、y,而采用Variant Subsystem 模块本身端口对应的信号线x、y绑定到对应Signal对象.

一级分支:

数据字典(Ctrl+H)中x、y、z对象:

为了生成条件编译代码,我们还需要为2个分支添加不同的编译条件. 右键点击 Variant Subsystem> 模块参数(P),两个子系统的控制表达式如下图:

默认是 true,false. 我们分别手动改为 type == 1,type == 2.
然后,在Matlab工作区输入 > type = 1.
于是,生成代码:
/* Model step function */
void my_first_code_xyz_step(void)
{
#if type == 1z = fabs((x - y) * k);
#elif type == 2z = fabs((x + y) * k);
#endif
}
当然,我们可以在模型文件同一目录下新建头文件 "my_macro.h",写入宏定义代码:
#define type 1
然后,我们还需要在数据字典中新建参数 type,初值1,类型为int32,存储类ImportedDefine,头文件为my_macro.h

如此,就脱离手动在工作区手动输入 type = 1 了.
如果我们觉得使用 type == 1,type == 2 这样的表达式太麻烦,也可以使用 Variant expression(变体布尔表达式)来替换.
在模型资源管理器中添加2个 Variant expression:top,bottom
top对应条件表达式:type == 1;bottom:type == 2

然后右键Variant Subsystem> 模块参数(P),修改变体控制项表达式分别为top、bottom:

结果,生成代码:
/* Model step function */
void my_first_code_xyz_step(void)
{
#if topz = fabs((x - y) * k);
#elif bottomz = fabs((x + y) * k);
#endif
}
不难发现,原来的宏定义 type == 1, type == 2,现在变成了 top, bottom.
参考
武 超 编著. 科学出版社. 2025. MBD入门 无刷电机控制代码生成.
