当前位置: 首页 > news >正文

MBD笔记:Simulink模型生成代码之基本概念

目录
  • 偏差计算模型的代码生成
    • 搭建Simulink模型
    • 模型配置参数
    • 代码生成
    • 管理数据
    • 代码生成的存储类介绍
  • 函数名称、引用参数
  • 代码调用
    • 设置可重用函数名、文件名
    • 为Step函数设置引用参数
    • 复制模块
  • 条件代码
  • 参考

本文以一个简单模型例子,来说明模型生成代码.

偏差计算模型的代码生成

搭建Simulink模型

模型文件名:my_first_code_xyz.slx

搭建Simlink模型:

img

其中,k在Matlab工作区间中设置为2.

这个模型计算 输入In1、In2的和,然后乘以k后,求绝对值,作为Out1输出.

运算过程等效于:out1 = |(in1 - in2)*k|,即计算2个输入信号的偏差,并进行比例运算,再求绝对值.

输入模块相当于某些对象的状态,称为信号(Signal). 信号不断变化,所以输出不断变化.

增益为k是人为改变的,不常变化,称为参数(Parameter).

模型配置参数

生成嵌入式C代码,最少需配置3部分:模型的求解器,模型的系统目标文件,硬件实现.

不同配置会对模型或者生成代码产生影响,需要谨慎配置.

  • 求解器配置

右键模型空白处> 模型配置参数(Ctrl + E),打开求解器选项卡,配置如下:

求解器设置为 定步长,离散(无连续状态),固定步长设置为 0.0001s

img

  • 系统目标文件配置

img

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

img

勾选了 “创建代码生成报告”“自动打开报告”“生成模型Web视图”,编译完成后,就会自动生成并打开报告、网页版模型,供用户查看. 非常方便,推荐勾选.

代码生成

点击APP> Embedded Coder,可显示C代码标签:
img

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

img

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

生成的代码所在目录:

img

生成的报告:

img

代码生成的内容结构:

文件名 说明
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:

img

这里xyz就是信号线的名字,属于Simulink的Signal信号对象.

如此,计算关系可写成:z = |(x - y) * k|

  • 新建数据字典文件

点击菜单栏 建模> 模型设置> 模型属性,或者 右键模型空白处 > 模型属性,在外部数据标签中新建 数据字典文件. 我们将其命名为 my_xyz_data.sldd(用户自定义).

img

img

  • 查看数据字典

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

img

img

打开 my_xyz_data的Design Data,目前什么都没有.

  • 添加Signal

点击如下工具栏图标,或者 菜单栏 添加> Simulink Signal(Ctrl + S),添加3个Simulink信号(Signal),重命名为x,y,z.

img

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

img

注:上图中 Simulink.Signal:z 表示我们用Simulink 包中的Signal 类创建了对象z.

保存文件,再次代码生成. 查看生成的 my_first_code_xyz_step 函数,发现并没有任何变化,这是因为模型的信号线还没有跟我们创建的Signal对象关联到一起.

  • 关联信号线与Signal对象

右键信号线x> 属性,勾选 “信号名称必须解析为 Simulink 信号对象”,将信号与Signal对象关联. 如此,信号名称旁边会多一个 “简约线条小耙子”.

img

对信号线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)

img
img

保存后重新生成代码:


/* 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

img

生成的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 生成的存储类有十多个类型

img

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代码> 个体因素代码映射,就会显示 代码映射 - 组件接口 页面:

img

img

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

img

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

img

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

所以,我们将In1,In2,Out1对应的信号x,y,z存储类修改为Auto.

img

重新生成代码,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. 生成的代码,形参就会跟我们设置的一致.

img

img

代码调用

如果想反复调用同一段代码,怎么办?

可以将其提炼为一个函数. 对于Simulink模型,可将重复使用的模块配置成原子子系统(atomic subsystem),生成一个单独的文件,供Step函数调用. 原子系统可设置一些生成代码属性,如何重用、不可重用函数、函数名、源文件名设置等.

对于我们的模型,选中Sum、Gain、Abs模块,右键> 基于所选内容创建子系统(Ctrl + G),创建子系统:

img

img

设置可重用函数名、文件名

右键封装的模块> 模块参数(P),

img

勾选视为原子单元,

img

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

img

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

img

img

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

img

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

img

按下图设置 my_first_code_xyz_step 形参名称,
img

然后,将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:
img

生成代码,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.

复制模块

复制所有模型:

img

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

img

此时,如果生成代码,会产生如下代码:

/* 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;
}

说明第二组参数与数据字典中定义的信号名尚未关联. 需要在 代码映射 - 组件接口 中点击函数名设置:

img

生成的代码,信号线名称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:

img

生成代码:


/* 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:

img

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

子系统模块的内容:

img

子系统1模块的内容:

img
注意:第一个模型保持Sum为“|+-”,第二个模型修改了Sum模型为"|++"

因为Variant Subsystem的不同分支内,不允许内部信号线绑定到相同 Signal 对象,所以我们取消分支内的信号线勾选【必须解析到信号对象】绑定 x、y,而采用Variant Subsystem 模块本身端口对应的信号线x、y绑定到对应Signal对象.

img

一级分支:

img

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

img

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

img

默认是 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

img

如此,就脱离手动在工作区手动输入 type = 1 了.

如果我们觉得使用 type == 1type == 2 这样的表达式太麻烦,也可以使用 Variant expression(变体布尔表达式)来替换.

在模型资源管理器中添加2个 Variant expression:top,bottom

top对应条件表达式:type == 1;bottom:type == 2

img

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

img

结果,生成代码:

/* 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入门 无刷电机控制代码生成.

http://www.jsqmd.com/news/1362398/

相关文章:

  • Prometheus-Kubernetes核心组件解析:Prometheus Operator与Grafana如何协作监控K8s
  • 未来优化技术展望:opro项目的路线图与社区贡献指南
  • GIS交通分析实战:从数据到空间智能的技术栈与工程实践
  • 如何在10分钟内使用Startup-Landing搭建专业创业网站:新手入门指南
  • Leshan性能优化:解决物联网设备通信延迟与资源占用问题
  • 大规模数据迁移:选型别只看功能清单
  • GinWAF防火墙系统V1.0.0发布
  • 2026年大连厨房装修施工优选**:专业匠心与实用美学并重,打造理想烹饪空间 - 卓企推荐
  • Multi-Agent Custom Automation Engine Solution Accelerator与Azure Cosmos DB集成指南:数据存储最佳实践
  • PI0Fast (LeRobot) 项目解析:Vision-Language-Action模型的核心原理与应用
  • 分布式事务反直觉坑位与避坑指南:代码评审该盯住哪些细节
  • 交调系统厂家推荐,广州聚杰,为交通调研提供完整数字化方案 - 品牌速递
  • 开发者必看:Multi-Agent Custom Automation Engine Solution Accelerator的架构设计与代码实现原理
  • QTerminal核心功能解析:标签页管理与窗口分割的终极技巧
  • 游戏角色皮肤制作全流程:从3D建模到引擎集成的实战指南
  • FIFA 23实时编辑器完整指南:解锁游戏无限可能
  • AI API调用实战:官方直连与中转服务配置全解析
  • 认知,决定一个人的选择与结局
  • Hunyuan-GameCraft常见问题解答:模型下载、推理报错、视频质量优化全攻略
  • Docker-Security补丁管理:简单高效的容器漏洞修复策略
  • 第13章:JDK Unified Logging 与 GC/运行时日志治理
  • telegram-history-dump开发者指南:自定义格式化器实现教程
  • 2026初创者不熟悉天津本土财税政策怎么办?2026年天津本地方案选择参考 - 各行各业Ethan说
  • 从零部署AI角色生成项目:基于Stable Diffusion的本地化实践指南
  • 如何用AWS Bookstore Demo App构建高效在线书店:5分钟快速启动教程
  • Unity性能优化:DOTween Pro动画GC问题深度解析与实战解决方案
  • ArcGIS Pro与InVEST模型实战:生态安全格局构建全流程解析
  • 交通量调查系统厂家推荐,广州聚杰,支持定制化监测解决方案 - 品牌速递
  • PTA基础编程题目集 7-24约分最简分式(C++语言实现)
  • 2026年北京故意损毁财物罪辩护律师**单:资深刑事律师团队,专业策略与实战经验深度解析 - 优企名品