3. Embedded Coder快速上手
前两节我们已经理解了Embedded Coder在Simulink代码生成工具链中的定位。本节不再停留在概念层面,而是完整走通一次“从模型到可部署C代码”的最小闭环:创建一个足够简单但结构完整的模型,配置代码生成目标,生成C代码,并检查生成的文件结构、入口函数与数据接口。走完这一遍,你会对“模型里的什么设置,最终变成了C代码里的什么”建立起直观对应。
一、先明确本节要交付的“可部署模型”长什么样
所谓“可部署”,并不是指模型有多复杂,而是指它生成的代码具备三个特征:有明确的周期入口函数、有清晰的数据接口、不依赖Simulink运行时环境。为了聚焦这条主线,我们构造一个教学示例:一个带增益和饱和限幅的信号处理模块,输入为 In1,输出为 Out1,内部对输入乘以增益 K = 2.5,再限制在 [-100, 100] 区间内。这个模型小到可以一眼看懂,又包含了输入、参数、运算、限幅和输出,足以体现代码生成的关键环节。
在Simulink中新建模型后,放入以下模块并连线:Inport(命名为 In1)→ Gain(增益设为 2.5)→ Saturation(上限 100,下限 -100)→ Outport(命名为 Out1)。将求解器设置为固定步长,步长设为 0.01 秒,因为嵌入式目标通常运行在确定的周期节拍上,变步长求解器不适合直接部署。这一步看似简单,却决定了后续代码是否具备周期可调度性。
二、配置代码生成目标:从“仿真模型”切换到“嵌入式目标”
模型搭好后,按 Ctrl+E 打开配置参数对话框,重点修改以下几项。第一,在 Solver 页把 Type 设为 Fixed-step,Solver 选 discrete (no continuous states),Fixed-step size 填 0.01。第二,在 Code Generation 页把 System target file 选为 ert.tlc,这是Embedded Coder提供的嵌入式目标,生成的代码精简、无操作系统依赖。第三,在 Code Generation → Interface 中把 Code interface packaging 设为 Nonreusable function,这样每个模型生成独立的入口函数,便于第一次上手时观察结构。第四,在 Code Generation → Report 中勾选 Create code generation report,生成后可以直接在浏览器里查看文件与函数索引。
这里有一个容易忽略但很关键的设置:Code Generation → Optimization 中的 Default parameter behavior。若设为 Inlined,增益 2.5 会直接以字面量形式写进C代码;若设为 Tunable,则会生成可调参数变量。第一次上手建议先用 Inlined,让代码更直观;等理解了参数接口后再切换为 Tunable,体会标定接口的生成方式。
三、生成C代码并检查文件结构
配置完成后,按 Ctrl+B 启动代码生成。若模型没有错误,Simulink会在当前工作目录下创建一个以模型名命名的文件夹,例如模型名为 first_deploy,则生成 first_deploy_ert_rtw 目录。打开该目录,你会看到一组分工明确的文件:first_deploy.c 和 first_deploy.h 是核心算法与接口文件;first_deploy_data.c 存放参数与常量;first_deploy_types.h 定义数据类型;ert_main.c 是一个示例主程序,展示如何以固定周期调用入口函数;此外还有 rtwtypes.h、rtmodel.h 等支撑文件。如果勾选了代码生成报告,还会生成 html 子目录,里面是可点击索引的网页报告。
检查文件结构时,重点看三件事。第一,first_deploy.h 中是否声明了 extern void first_deploy_step(void);,这是模型的周期入口函数。第二,first_deploy.h 中是否定义了输入输出结构体,例如 ExtU_first_deploy_T 和 ExtY_first_deploy_T,它们分别承载 In1 和 Out1。第三,first_deploy_data.c 中是否出现了增益参数。由于我们选择了 Inlined,增益 2.5 不会作为独立变量出现,而是直接写在 first_deploy.c 的运算语句里。如果你把参数行为改为 Tunable 再生成一次,就会在 _data.c 中看到对应的可调参数定义,这正是参数接口随配置变化的直接体现。
四、读懂入口函数与数据接口
打开 first_deploy.c,找到 first_deploy_step 函数。它的典型结构如下(示例代码,实际名称随模型名变化):
void first_deploy_step(void)
{
real_T rtb_Saturation;
rtb_Saturation = 2.5 * first_deploy_U.In1;
if (rtb_Saturation > 100.0) {
rtb_Saturation = 100.0;
} else if (rtb_Saturation < -100.0) {
rtb_Saturation = -100.0;
}
first_deploy_Y.Out1 = rtb_Saturation;
}
这段代码把模型里的三个环节一一对应了出来:2.5 * first_deploy_U.In1 对应增益,两个 if 分支对应饱和限幅,最后的赋值对应输出端口。输入通过 first_deploy_U.In1 读取,输出通过 first_deploy_Y.Out1 写出,二者都是结构体成员,这就是Embedded Coder默认的数据接口形式。理解这一点后,你就能明白:外部系统只需要在调用 first_deploy_step 之前把输入写入 first_deploy_U,调用之后从 first_deploy_Y 读取输出,整个模型就完成了与外部代码的对接。
再看 ert_main.c,它给出了一个最小可运行框架:初始化模型、进入 while 循环、按固定周期调用 first_deploy_step。在真实嵌入式工程中,这个循环通常由定时器中断或RTOS任务替代,但调用关系完全一致。至此,你已经完成了从模型到可部署代码的第一次完整穿越。