基于Simulink MBD工具箱的二次开发(二)
2026/8/3 14:28:08 网站建设 项目流程

基于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 + b

1. 编写 S-Function 仿真模块

首先编写target_sfun_add.c

该文件使用 Level-2 C MEX S-Function 接口,定义两个输入端口和一个输出端口。

mdlInitializeSizes中完成端口配置:

ssSetNumInputPorts(S,2);ssSetNumOutputPorts(S,1);

两个输入端口分别对应ab,输出端口对应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 中直接写死模型信号名称,而应通过LibBlockInputSignalLibBlockOutputSignal获取。


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 没有硬件浮点运算单元,floatdouble运算通常需要通过软件库实现。尤其是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 模块。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询