基于Simulink MBD工具箱的二次开发
- 基于 Simulink MBD 工具箱的二次开发(二)
- 修改工具箱
- 1. 编写 S-Function 仿真模块
- 2. 仿真代码不等于目标代码
- 3. 编写 TLC 代码生成规则
- 4. 引入外部头文件
- 5. 获取模块输入和输出
- 6. 生成目标函数调用
- 7. 关于数据类型
- 8. 文件之间的关系
基于 Simulink MBD 工具箱的二次开发(二)
修改工具箱
前面已经介绍了工具箱的基本组成。这一篇通过一个简单的加法模块,说明如何在现有 Simulink MBD 工具箱中增加自定义 S-Function,并让该模块参与目标代码生成。
这个示例包含两个文件:
target_sfun_add.c target_sfun_add.tlc其中:
target_sfun_add.c负责定义模块在 Simulink 中的端口、数据类型、采样时间以及仿真行为;target_sfun_add.tlc负责描述代码生成时应该输出什么目标代码。
整个模块的功能很简单:
输入:a、b 输出:c 计算:c = a + b1. 编写 S-Function 仿真模块
首先编写target_sfun_add.c。
该文件使用 Level-2 C MEX S-Function 接口,定义两个输入端口和一个输出端口。
在mdlInitializeSizes中完成端口配置:
ssSetNumInputPorts(S,2);ssSetNumOutputPorts(S,1);两个输入端口分别对应a和b,输出端口对应c。
为了简化示例,这里暂时将输入和输出都设置为double类型:
ssSetInputPortDataType(S,0,SS_DOUBLE);ssSetInputPortDataType(S,1,SS_DOUBLE);ssSetOutputPortDataType(S,0,SS_DOUBLE);模块采用继承采样时间:
ssSetSampleTime(S,0,INHERITED_SAMPLE_TIME);这样模块不需要单独设置运行周期,而是跟随所在模型或上级系统的采样时间运行。
实际的加法运算放在mdlOutputs中:
c[0]=a[0]+b[0];2. 仿真代码不等于目标代码
到这里需要注意一个问题:
S-Function 能够在 Simulink 中仿真,并不代表它一定能够直接生成目标平台代码。
当前target_sfun_add.c中的mdlOutputs主要用于 MATLAB/Simulink 仿真。代码生成器还不知道,在生成嵌入式 C 代码时,应该如何替换这个 S-Function 模块。
如果没有对应的代码生成支持,在构建目标代码时可能出现以下情况:
- S-Function 无法内联;
- 生成代码中保留对 S-Function 运行环境的依赖;
- 目标工具链无法编译;
- 代码生成过程直接报错。
因此,还需要为该模块编写对应的 TLC 文件。
3. 编写 TLC 代码生成规则
为target_sfun_add.c创建同名的 TLC 文件:
target_sfun_add.tlc首先使用%implements声明,该 TLC 文件用于实现对应的 S-Function:
%implements "target_sfun_add" "C"S-Function 名称必须与 C 文件中的:
#defineS_FUNCTION_NAMEtarget_sfun_add保持一致。
4. 引入外部头文件
这个示例不准备直接在 TLC 中生成:
c=a+b;而是希望调用一个外部函数:
c=my_add(a,b);该函数由my_add.h提供。
因此,在 TLC 的BlockTypeSetup阶段,将头文件加入生成代码的公共头文件列表:
%<LibAddToCommonIncludes("my_add.h")>代码生成后,模型源文件中会出现类似内容:
#include"my_add.h"需要注意,TLC 这里只引入了my_add.h,并没有添加任何.c文件。
也就是说,本示例采用的是:
S-Function + TLC + 头文件而不是:
S-Function + TLC + 外部 C 源文件如果my_add.h中直接提供了static函数实现,就不需要额外参与链接。
如果my_add.h中只有函数声明,那么my_add的实际实现必须已经存在于目标工程链接的静态库中,否则链接阶段会出现未定义符号。
5. 获取模块输入和输出
在 TLC 的Outputs阶段,需要先取得模块的输入和输出信号。
两个输入信号分别对应 S-Function 的输入端口 0 和输入端口 1:
%assign a = LibBlockInputSignal(0, "", "", 0) %assign b = LibBlockInputSignal(1, "", "", 0)输出信号对应输出端口 0:
%assign c = LibBlockOutputSignal(0, "", "", 0)这里获取到的并不一定是固定的变量名,而是代码生成器根据模型结构计算出的合法 C 表达式。
例如,某个输入可能最终对应:
rtU.InputA另一个输入可能对应:
localSignal因此,不应该在 TLC 中直接写死模型信号名称,而应通过LibBlockInputSignal和LibBlockOutputSignal获取。
6. 生成目标函数调用
取得输入和输出表达式后,在 TLC 中生成实际的函数调用:
%<c> = my_add(%<a>, %<b>);代码生成器最终会输出类似代码:
output_c=my_add(input_a,input_b);这样,Simulink 中的 S-Function 模块就被转换成了普通的 C 函数调用。
仿真阶段执行的是target_sfun_add.c中的:
c[0]=a[0]+b[0];目标代码生成阶段执行的则是:
c=my_add(a,b);这也是 S-Function 和 TLC 配合使用的一个典型方式:
- S-Function 描述模块如何仿真;
- TLC 描述模块如何生成目标代码;
- 外部头文件或静态库提供真正运行在 MCU 上的底层实现。
7. 关于数据类型
当前示例为了便于理解,所有端口都使用了double类型。
但在实际的嵌入式项目中,数据类型设计非常重要。
例如 S12 系列 MCU 没有硬件浮点运算单元,float和double运算通常需要通过软件库实现。尤其是double运算,会增加:
- 运算时间;
- 程序空间占用;
- 栈空间占用;
- 运行时库依赖;
- 代码执行时间的不确定性。
因此,正式开发时不建议直接将所有信号都设置为double。
更合理的做法是根据实际信号范围、精度和运算需求选择数据类型,例如:
uint8 uint16 int16 int32 single 定点数据类型例如一个温度信号实际范围为-40~150 ℃,精度要求为0.1 ℃,就可以使用int16保存放大十倍后的物理量,而不一定需要使用浮点数。
在 MBD 开发中,数据类型不仅影响模型仿真结果,还会直接影响生成代码的执行效率、存储空间和目标平台兼容性。
因此,数据类型设计并不是代码生成完成后再考虑的问题,而应该在模型设计初期就确定。
8. 文件之间的关系
这个简单示例中,各文件的作用如下:
target_sfun_add.c └─ 定义 Simulink 模块端口和仿真行为 target_sfun_add.tlc └─ 定义目标代码生成方式 my_add.h └─ 提供目标平台实际调用的加法函数代码生成过程可以理解为:
Simulink 模型 ↓ 发现 target_sfun_add 模块 ↓ 执行 target_sfun_add.tlc ↓ 加入 #include "my_add.h" ↓ 生成 my_add(a, b) 函数调用 ↓ 目标编译器完成编译和链接通过这种方式,可以逐步把现有的底层驱动、协议栈或静态库封装成 Simulink 模块。