简介:面向MATLAB Simulink初学者,这份压缩包收录了两个经典S-Function示例:times2与unit_delay。times2演示自定义模块实现信号乘以2的完整流程,unit_delay则展示单位延迟功能的底层实现,两者均配有可直接运行的Simulink模型文件,便于对照源码与仿真结果学习。资源共4个文件,包含两个MATLAB源文件和两个Simulink模型,压缩包整体仅38KB,轻量易用。已有1276人浏览学习,适合希望掌握S-Function初始化、仿真、输出函数结构,并想进一步理解模块封装、参数配置和Simulink底层交互机制的初学者。通过这两个示例,读者可以快速建立自定义系统模块的开发思路,为复杂建模打下基础。
1. 这个压缩包解决什么问题:S-Function 的适用场景
拿到一个叫“s-function的两个示例.zip”的压缩包,先别急着解压。你要先搞清楚 S-Function 到底解决什么问题,否则就算把两个示例跑通了,换到自己的算法上照样翻车。S-Function 是 Simulink 提供的一类用户自定义模块接口,全称 System-Function,它允许你把手写的 C/C++ 代码或者 MATLAB 脚本封装成 Simulink 里的一个普通模块,和内置模块一样参与仿真、支持参数配置、支持代码生成。这个压缩包的价值,就是用两个最小示例把这条通路完整演示一遍——一个用 C 语言实现,一个用 MATLAB 实现。
这两个示例通常不是花哨的算法,而是最朴素的动态系统,比如积分器或者累加器。它们的作用是让你看清 S-Function 的骨架:初始化时声明状态和输入输出,运行阶段更新状态和计算输出。读完这篇文章,你会知道这两个示例分别适合什么场景、怎么在本地跑起来、参数怎么改、踩坑点在哪,最后能把这套骨架改造成自己的算法模块。适合谁?打算在 Simulink 里集成手写算法的工程师,想把仿真速度提上来的人,以及被 S-Function 报错信息折磨过的新手。
2. 两个示例的本质:C MEX 与 Level-2 MATLAB 的选型
2.1 第一个示例:C MEX S-Function 的骨架与定位
压缩包里第一个常见示例是 C MEX S-Function,也就是用 C 语言编写、通过 mex 命令编译成动态链接库的 S-Function。它适合你手里已经有一份成熟的 C 算法,想原封不动接进 Simulink 仿真,或者你对仿真执行速度有要求,不希望被 MATLAB 解释执行拖慢。C MEX 在仿真时会编译成本地机器码,性能接近手写 C,是工业模型里最常用的方式。
这个示例的骨架是固定的,无论 zip 里原始文件叫什么名字,结构都逃不出下面的样子。我按最常见写法给一个最小可编译版本,方便你对照:文件顶部定义模块名,中间实现各个回调函数,底部通过simulink.c把接口拼接起来。
#define S_FUNCTION_NAME first_c_sfunc #define S_FUNCTION_LEVEL 2 #include "simstruc.h" static void mdlInitializeSizes(SimStruct *S) { ssSetNumSFcnParams(S, 1); // 模块有1个参数 ssSetNumContStates(S, 1); // 1个连续状态 ssSetNumDiscStates(S, 0); // 0个离散状态 ssSetNumInputPorts(S, 1); // 1个输入端口 ssSetInputPortWidth(S, 0, 1); // 输入宽度为1 ssSetInputPortDirectFeedThrough(S, 0, 0); // 输出不直接依赖输入 ssSetNumOutputPorts(S, 1); // 1个输出端口 ssSetOutputPortWidth(S, 0, 1); // 输出宽度为1 ssSetNumSampleTimes(S, 1); // 1个采样时间 } static void mdlInitializeSampleTimes(SimStruct *S) { ssSetSampleTime(S, 0, CONTINUOUS_SAMPLE_TIME); // 连续采样时间 ssSetOffsetTime(S, 0, 0.0); } static void mdlOutputs(SimStruct *S, int_T tid) { real_T *y = ssGetOutputPortRealSignal(S, 0); real_T *x = ssGetContStates(S); y[0] = x[0]; // 输出 = 状态 } static void mdlDerivatives(SimStruct *S) { real_T *dx = ssGetdX(S); real_T *u = ssGetInputPortRealSignal(S, 0); dx[0] = u[0]; // 状态导数 = 输入 } static void mdlTerminate(SimStruct *S) {} #include "simulink.c"这段代码实现的就是一个连续积分器:状态 x 的导数是输入 u,输出 y 等于状态 x。mdlInitializeSizes负责告诉 Simulink 这个模块需要多少状态、多少输入输出、有没有参数。mdlDerivatives是连续状态的核心,simulink.c是 C MEX S-Function 的公共模板,它把上面这些回调函数注册成 Simulink 认识的形式。注意S_FUNCTION_NAME必须和编译产物的名字一致,比如这里定义了first_c_sfunc,最终编译出来的文件就是first_c_sfunc.mexw64。
2.2 第二个示例:Level-2 MATLAB S-Function 的骨架与定位
压缩包里第二个示例通常是 Level-2 MATLAB S-Function,文件名后缀是.m,不需要编译,写完直接在 Simulink 里调用。它适合快速验证控制逻辑、处理比较复杂的模块内部逻辑、以及你不想碰 C 编译器的场景。代价是仿真速度不如 C MEX,在大型模型里如果这类模块数量多,会拖慢整体仿真。但作为一个学习入口,它比 C MEX 直观得多,因为所有回调函数都是 MATLAB 函数,变量名一目了然。
Level-2 MATLAB S-Function 不是脚本,而是一个符合固定格式的函数文件。下面这个最小示例实现的是离散累加器:每个仿真步长把输入累加到状态里,输出当前累加值。
function second_level2_sfunc(block) setup(block); function setup(block) block.NumDialogPrms = 0; % 没有对话框参数 block.NumInputPorts = 1; block.NumOutputPorts = 1; block.SetPreCompInpPortInfoToDynamic; block.SetPreCompOutPortInfoToDynamic; block.InputPort(1).Dimensions = 1; % 输入宽度1 block.InputPort(1).DirectFeedthrough = false; block.OutputPort(1).Dimensions = 1; % 输出宽度1 block.NumContStates = 0; % 没有连续状态 block.NumDiscStates = 1; % 1个离散状态 block.SampleTimes = [-1 0]; % 继承采样时间 block.RegBlockMethod('Outputs', @Outputs); block.RegBlockMethod('Update', @Update); function Outputs(block) block.OutputPort(1).Data = block.DiscStates(1); function Update(block) block.DiscStates(1) = block.DiscStates(1) + block.InputPort(1).Data;和 C MEX 对比,结构非常清晰:setup函数相当于mdlInitializeSizes,Outputs相当于mdlOutputs,Update相当于mdlUpdate。block.DiscStates是离散状态数组,这里的索引从 1 开始,这是和 C 语言从 0 开始最大的思维差异,新手经常在这里写错。SampleTimes设成[-1 0]表示继承上游信号的采样时间,这样模块放到任何采样率的模型里都不会报错。
2.3 选型对比:什么时候用 C MEX,什么时候用 Level-2
两个示例不是二选一的关系,而是同一个接口的两条实现路径。判断依据有三个:性能、开发效率、代码来源。
如果你的算法已经在 C 里跑通了,或者你最终要做代码生成、硬件部署,选 C MEX 是主流方案。它的编译链和 Simulink 代码生成工具链天然兼容。如果你的目标是先验证一个思路,算法可能还会频繁改,或者你更熟悉 MATLAB,选 Level-2 更合适。改一行逻辑立刻生效,不用重新编译。
还有一个容易被忽略的点:Level-2 MATLAB S-Function 支持用 MATLAB 的调试器打断点,这对排查算法逻辑非常有帮助。C MEX 虽然也能调试,但要额外配置外部调试器,门槛高不少。学习阶段建议两个示例都跑通,先理解 Level-2 验证算法,再迁移到 C MEX 提升性能。下表给出快速选型参考:
| 场景 | C MEX | Level-2 MATLAB |
|---|---|---|
| 已有现成C算法 | 首选 | 需要重写为MATLAB |
| 需要高仿真性能 | 首选 | 性能受限 |
| 频繁修改算法逻辑 | 每次要编译 | 改完即用 |
| 需要断点调试 | 配置复杂 | 原生支持 |
| 最终代码生成部署 | 常用 | 需要额外配置 |
3. 把两个示例跑通:从编译到仿真的完整步骤
3.1 环境准备:先让 mex 命令找到可用的编译器
跑 C MEX 示例之前,第一关是编译器。很多人在这一步卡住,报错信息通常是“找不到编译器”或者“不支持当前编译器版本”。因为 MATLAB 的 mex 命令不是拿操作系统的任意编译器都能编译,它需要识别并选择受支持的 C 编译器。在 Windows 上常见的选择是 MinGW-w64 或者 Visual Studio,在 Linux 上通常是系统自带的 GCC。
环境准备的正确顺序是先查看当前系统里有没有可用的编译器,再配置。打开 MATLAB,运行下面这条命令:
mex -setup如果系统里已经安装了受支持的编译器,命令会列出候选列表,按提示输入序号选择就行。如果提示找不到编译器,就需要先安装一个。安装完成之后重新运行mex -setup,直到提示“已选定编译器”再继续。这一步别跳过,否则后面编译示例一定失败。选完编译器可以用mex -setup C单独确认 C 语言的编译器配置。
3.2 编译 C MEX 示例:完整命令与产物检查
编译器配置好之后,把 zip 里那个 C 示例解压到不带中文和空格的路径下。推荐建一个专门的文件夹,比如work\sfunc_demo,把 MATLAB 当前目录切过去,然后运行编译命令:
cd work/sfunc_demo mex first_c_sfunc.c运行完这条命令,当前目录下应该多出一个文件,Windows 上是first_c_sfunc.mexw64,Linux 上是first_c_sfunc.mexa64。这个文件就是编译产物,Simulink 认识的是这个 mex 文件,而不是原始.c文件。
编译成功后用dir检查产物是否存在,这是排除路径问题最快的方式:
dir *.mex*如果这里列不出文件,大概率是编译失败,回头看命令行窗口的红色报错。常见的是S_FUNCTION_NAME和文件名不一致,或者缺少头文件。注意一点:每次改完.c文件,都要重新执行mex编译,否则 Simulink 加载的还是旧产物,仿真结果和代码对不上,这个坑最容易在改代码后出现。
3.3 在 Simulink 里挂载 S-Function 模块并设置参数
编译完成之后,打开 Simulink 新建一个空白模型,从库浏览器左侧搜 “S-Function” 并拖入模型。双击模块,会看到一个对话框,里面最关键的是两个输入框:模块名(Function name)和参数(Parameters)。
在 Function name 里填first_c_sfunc,注意不要填文件名、不要填路径,只填S_FUNCTION_NAME定义的那个名字。Parameters 里填你给模块准备的第 1 个参数,这边示例里声明了 1 个参数但实际上没读取,随便填个数值比如1.0就行,目的是让你理解参数是怎么传递的。
在这个示例的源码里,参数目前并没有被读取。如果要让参数参与计算,需要在回调函数里用ssGetSFcnParam取出来。这一步先不展开,第 4 章专门讲参数。填完之后点 OK,模块图标会变成带输入输出端口的浅蓝色方块。如果模块下面出现红色错误文字,通常是名字和编译产物对不上,或者编译产物不在 MATLAB 路径中。
搭一个最简单的验证模型:从库浏览器拖入 Sine Wave、S-Function、Scope,按顺序连线,把仿真的停止时间设为 10 秒,点运行,在 Scope 里应该看到一条正弦波被积分之后变成余弦曲线。能出这条曲线,C MEX 示例就算真正跑通了。
3.4 验证运行结果:用波形对比确认示例行为
积分器示例的输出是输入信号的积分,所以用正弦波输入,你会得到一条从 0 开始、幅值按 1/频率缩放的余弦曲线。这里教你一个更严密的验证方法:在模型里再拖一个 Simulink 自带的 Integrator 模块,让它和 S-Function 并联,两个输出接同一个 Scope。如果 S-Function 的波形和 Integrator 的波形完全重合,说明模块行为正确;如果幅值或相位对不上,说明回调函数有逻辑错误。
这一步别省,它比盯着代码找问题高效得多。S-Function 是个黑匣子,外部看不出来内部状态,但波形对比能快速暴露哪一步出了问题。积分结果是余弦曲线的话,说明mdlDerivatives正确;如果看到的是直线或者阶跃,说明状态更新逻辑没有执行。顺便说一句,连续时间下这个 C MEX 示例不需要mdlUpdate,因为状态更新完全靠导数积分完成,这一点和离散示例有本质区别。
4. 改参数与回调:把示例变成自己的算法模块
4.1 回调函数的执行顺序:初始化、输出、更新、终止
S-Function 内部不是一个主函数从头跑到尾,而是一系列回调函数,由 Simulink 在仿真不同阶段调用。理解调用顺序,就等于理解了 S-Function 的工作方式。仿真一开始,Simulink 先调用初始化回调,拿到模块的尺寸信息。然后进入仿真循环:在每个采样步长里,先调用输出回调计算当前输出,再调用更新回调或导数回调计算下一个时刻的状态。仿真结束时,调用终止回调做清理。
这解释了一个关键行为:输出计算永远发生在状态更新之前。用离散累加器示例来解释,假设初始状态是 0,第一个仿真步输入是 1,那么这一拍的输出是 0,下一拍的输出才是 1。输出用的是当前状态,更新用的是当前输入,时序上差一个步长。很多新手在这里疑惑“为什么我的输出慢了一拍”,原因就是这个执行顺序。C MEX 里对应的映射关系是:mdlInitializeSizes负责初始化,mdlOutputs负责输出,mdlUpdate负责离散更新,mdlDerivatives负责连续更新,mdlTerminate负责收尾。
4.2 采样时间与输入输出维数:从单输入单输出到多输入多输出
两个示例都是单输入单输出,宽度固定为 1。实际项目里很少这么简单,最常见的改动就是把宽度从 1 改成 n,或者改成动态宽度。C MEX 里把宽度的数字换掉即可:ssSetInputPortWidth(S, 0, 3)表示输入宽度 3,ssSetOutputPortWidth(S, 0, 3)对应输出宽度。Level-2 里改成block.InputPort(1).Dimensions = 3。
如果希望宽度自动跟随上游信号,C MEX 里要写成ssSetInputPortWidth(S, 0, DYNAMIC_SIZING),然后在mdlStart里查询实际宽度并给输出端口设置相同宽度。Level-2 里更简单,Dimensions = -1表示动态继承。我一般建议新手先把宽度固定下来,跑通后再改动态,否则报错信息会同时涉及维数继承和端口匹配,不容易定位。
采样时间也是高频修改项。连续系统用CONTINUOUS_SAMPLE_TIME,离散系统用固定采样时间比如ssSetSampleTime(S, 0, 0.01),表示 10ms 采样。Level-2 里block.SampleTimes = [0.01 0]是固定步长,[-1 0]是继承。固定步长适合模块内部有自己独立采样逻辑的场景,继承则让模块跟随上游信号。两者的选择直接影响仿真速度,固定步长会让 Simulink 在这个模块上强制按步长计算,继承则更灵活。
4.3 参数传入与校验:从硬编码到可配置
示例里虽然声明了参数,但没有真正使用。实际使用中,参数是 S-Function 对外暴露的配置口。C MEX 读取参数的代码是这样写的:
static void mdlStart(SimStruct *S) { real_T *param = mxGetPr(ssGetSFcnParam(S, 0)); // param[0] 就是用户在模块对话框里填的第1个参数 }ssGetSFcnParam(S, 0)拿到的是第 0 个参数的指针,mxGetPr把它解析成double*。如果参数数量比声明少,Simulink 会直接报错,所以ssSetNumSFcnParams(S, 1)声明了 1 个参数,对话框就必须填 1 个。参数数量改多了,对话框里填的项也要对应增加,顺序和索引一一对应。
Level-2 里读参数的方式更直白:block.DialogPrm(1).Data就是第 1 个参数。注意这里的索引从 1 开始,和 C 语言从 0 开始不一样。参数校验建议放在初始化阶段完成,比如判断参数是否为正数、是否在合理范围内,不合规就调用ssSetErrorStatus抛出错误。把校验前置到仿真开始前,比在输出回调里反复判断效率高得多。
4.4 直通标志:S-Function 最容易设错的一个参数
直通标志DirectFeedthrough是 S-Function 里最抽象、也最容易设错的概念。它的含义是:当前时刻的输出是否直接依赖当前时刻的输入。如果输出是上一拍状态的函数,或者完全是状态值,就不算直通,设 0;如果输出计算里用了当前时刻的输入,就必须设 1。
用两个示例对照:C MEX 积分器的输出 y = x,只依赖状态,所以设 0;Level-2 累加器的输出也是状态,同样设 0。如果你的模块输出 y = u + 状态,那么必须设 1。直通设错了后果很严重:该设 1 却设 0,Simulink 在确定模块执行顺序时会认为输出不依赖输入,可能让同一时刻的输出读到旧输入值;不该设 1 却设 1,会在模型里引入一个本不存在的代数环,Simulink 直接报 “Algebraic loop detected” 或者仿真收敛异常。这个参数要靠对算法的分析来定,不是看模块类型,也不是听教程的“一般设 1”。
5. 避坑指南:S-Function 跑不起来时按顺序查这五处
5.1 提示 S-Function 不存在:文件名、路径与大小写
现象:模块显示红色错误,提示The specified S-Function 'first_c_sfunc' does not exist。原因通常是三个:编译产物不在 MATLAB 路径上,模块名和S_FUNCTION_NAME不一致,或者 Linux 下文件名大小写不匹配。解决:先用dir *.mex*确认产物在不在当前目录,再用which first_c_sfunc查看 MATLAB 能否找到它。如果which返回空,就把当前目录加入路径,或者直接addpath('你的目录')。模块名必须和S_FUNCTION_NAME完全一致,这个字符串在编译时固化,不是看文件名。
5.2 编译通过但模型报错:输出类型或维数不匹配
现象:mex 编译成功,波形也能连接,但仿真一开始就报输出不是数值类型,或者输出端口宽度和实际输出不一致。原因多半是输出回调里没有给输出正确赋值,或者赋值的维度比声明的小。C MEX 里如果用ssGetOutputPortRealSignal获取指针,就必须通过这个指针写值,不能直接改输出端口的其他属性。解决:检查输出回调里是否覆盖了全部输出元素,比如声明输出宽度 2,就要给 y[0] 和 y[1] 都赋值。Level-2 里更简单,OutputPort(1).Data的维数必须和Dimensions声明一致。
5.3 仿真结果和预期差一个步长:状态更新时序写反
现象:波形整体向右平移了一个采样周期,或者离散累加结果比预期少一个值。原因:更新回调执行顺序搞反,或者把状态更新写进了输出回调。Simulink 对离散模块先调用mdlOutputs再调用mdlUpdate,输出用的是当前状态。如果你在mdlOutputs里先改了状态再输出,就相当于把更新提前了一拍。解决:把状态修改逻辑放到mdlUpdate,输出回调只读状态。Level-2 里对应的是Update和Outputs两个函数,别把赋值写在Outputs里。
5.4 无缘无故报代数环:直通标志设错
现象:模型结构明明没有反馈,却提示检测到代数环。原因:把模块的直通标志设成了 1,而实际输出不依赖当前输入。Simulink 认为存在直通,就会把模块当成即时依赖输入的元件,插入到反馈路径检查里,从而误报代数环。解决:回到DirectFeedthrough的定义重新分析算法,输出只依赖状态就设 0。改完重新编译再仿真。这个排查方向很多人根本想不到,属于 S-Function 里的玄学问题,但原理其实很朴素。
5.5 仿真卡得无法忍受:回调里打印东西太多
现象:仿真运行速度极慢,控制台被刷屏,甚至像卡死。原因:为了调试在输出回调里加了打印语句。输出回调每个仿真步长会被调用至少一次,连续系统配合变步长求解器时可能被调用成百上千次,打印 IO 完全拖垮仿真实时性。解决:把打印包在条件判断里,只在特定步数打印,比如if (tid == 0)或者用全局计数器控制前几次打印。另一个方案是用mexPrintf而不是printf,前者能正确重定向到 MATLAB 控制台,但同样会拖慢速度。调试完务必删掉这些语句。
6. 一个更高效的用法:把示例骨架封装成自己的模板
6.1 从示例复制一份骨架,只改核心回调
两个示例跑通之后,不建议每次写新算法都从零开始建文件。我平时的工作习惯是维护两个“模板文件”,一个 C MEX 模板、一个 Level-2 模板,内容就是第 2 章的骨架,但把所有状态和参数都改成动态配置。新项目来了,直接复制模板,只改mdlInitializeSizes里的状态数量和端口宽度,再改mdlOutputs、mdlUpdate、mdlDerivatives三个回调。这样每次新写的代码量其实很小,大部分骨架代码是固定的。模板文件里用大写字母注释标注“需要修改的位置”,避免下次打开文件还要从头读一遍。如果是波形对比已经确认过的模板,后续仿真出问题基本可以排除骨架问题,直接查算法逻辑,排查范围大幅缩小。
6.2 验证技巧:和 Simulink 自带模块做同框图对比
最后分享一个我一直在用的验证习惯:不管写什么 S-Function,先找 Simulink 自带的一个等价模块,放到同一个模型里做并联对比。这个习惯帮我省下过很多查找问题的时间,比你盯着回调函数逐行读代码高效得多,算是这个方向最值得投入的排查常年经验。比如你实现了低通滤波,旁边放一个 Transfer Fcn 模块,两个输出接同一个 Scope。如果波形不重合,用一个简单的正弦扫频输入就能快速看出差异发生的频段,从而定位是哪部分算法写错了。对比通过之后,再逐步替换成真实工况的输入信号。这样验证下来的模块,几乎不会在联合仿真阶段出幺蛾子。希望帮到你。
本文还有配套的精品资源,点击获取