基于Simulink MBD工具箱的二次开发(二)
基于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 CS-Function 名称必须与 C 文件中的#defineS_FUNCTION_NAMEtarget_sfun_add保持一致。4. 引入外部头文件这个示例不准备直接在 TLC 中生成cab;而是希望调用一个外部函数cmy_add(a,b);该函数由my_add.h提供。因此在 TLC 的BlockTypeSetup阶段将头文件加入生成代码的公共头文件列表%LibAddToCommonIncludes(my_add.h)代码生成后模型源文件中会出现类似内容#includemy_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_cmy_add(input_a,input_b);这样Simulink 中的 S-Function 模块就被转换成了普通的 C 函数调用。仿真阶段执行的是target_sfun_add.c中的c[0]a[0]b[0];目标代码生成阶段执行的则是cmy_add(a,b);这也是 S-Function 和 TLC 配合使用的一个典型方式S-Function 描述模块如何仿真TLC 描述模块如何生成目标代码外部头文件或静态库提供真正运行在 MCU 上的底层实现。7. 关于数据类型当前示例为了便于理解所有端口都使用了double类型。但在实际的嵌入式项目中数据类型设计非常重要。例如 S12 系列 MCU 没有硬件浮点运算单元float和double运算通常需要通过软件库实现。尤其是double运算会增加运算时间程序空间占用栈空间占用运行时库依赖代码执行时间的不确定性。因此正式开发时不建议直接将所有信号都设置为double。更合理的做法是根据实际信号范围、精度和运算需求选择数据类型例如uint8 uint16 int16 int32 single 定点数据类型例如一个温度信号实际范围为-40150 ℃精度要求为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 模块。