在大型 Simulink 模型中,最让人头疼的问题之一就是顶层图被各种运算、逻辑、回调代码塞满,连一条信号线都要绕半个屏幕才能连上。这时候如果能像写函数一样把一段算法封装起来,在模型里按名字调用,模型结构会清爽很多。Simulink Function 就是专门解决这个问题的模块。本文从模块库位置、函数调用机制、完整建模案例到代码生成注意事项,把 Simulink Function 的原理和用法完整拆解一遍。
1. 背景与核心概念
首先说清楚 Simulink 常用模块库是怎么组织的。打开 Simulink Library Browser,也就是模块库浏览器,可以看到整个库按照功能分成了很多类别:Sources 是信源,Sinks 是信宿,Continuous 是连续环节,Discrete 是离散环节,Math Operations 是数学运算,Logic and Bit Operations 是逻辑与位运算,User-Defined Functions 是用户自定义函数。我们常说的“搭积木”式仿真建模,就是从这些库里把模块拖到画布上连接起来。
Simulink Function 位于 User-Defined Functions 分类下。它的作用是在模型内部定义一个可复用的函数,这个函数有明确的输入参数和输出参数,可以通过 Function Caller(函数调用方)模块在模型的不同位置调用。也就是说,Simulink Function 允许你像写 C 语言函数一样,把一段算法或逻辑封装成“函数”,然后在模型中按名字调用,而不是把同样的逻辑复制粘贴多个副本。
这里有一个很多人容易混淆的概念:Simulink Function、S-Function、MATLAB Function 到底有什么区别?
我们先从简单的说起。MATLAB Function 是模型里的一个代码块,双击后可以写 MATLAB 代码,它适合实现比较灵活的算法逻辑,比如矩阵运算、循环、条件判断。S-Function 则是一种更底层的接口,允许你用 MATLAB、C、C++ 或 Fortran 编写自定义的 Simulink 模块,适合集成外部已有算法、硬件驱动或者对仿真速度要求极高的场景。
而 Simulink Function 更接近“模型内部的函数封装”。你可以把多个 Simulink 模块组合成一段逻辑,然后给它定义函数名和输入输出接口,再由 Function Caller 通过函数名来调用它。和普通的子系统相比,Simulink Function 最大的不同是调用方式:普通子系统是通过信号直接连接,而 Simulink Function 是通过函数调用语义来触发,这个特性在某些代码生成和架构设计场景下非常有用。
Simulink Function 的典型应用场景包括:
- 在同一个模型中多处调用同一段算法逻辑,避免重复建模。
- 配合 Stateflow 状态机,在状态动作中调用函数。
- 在车辆控制、飞控等分层架构中,把控制器内部算法封装成函数,便于代码生成和集成。
- 与 Function Caller 配合,实现类似“函数指针”的建模思路。
对于需要生成嵌入式 C 代码的人而言,Simulink Function 还有一个重要价值:它可以映射生成独立的 C 函数,函数名、输入输出参数都可以在代码生成映射表中控制。这意味着你的 Simulink 算法模型在生成代码后,依然能保持清晰的函数调用关系。
2. 环境准备与版本说明
本文以常见版本的 MATLAB/Simulink 为例进行演示。不同版本中,Simulink Function 模块的名称、位置和配置界面可能略有差异,但整体流程是一致的。如果你的版本较老或较新,请以实际界面为准。
建议安装以下工具箱:
- Simulink:必选,建模与仿真环境。
- MATLAB Coder:可选,当你需要将模型生成 C/C++ 代码时使用。
- Embedded Coder:可选,用于嵌入式代码生成时配置函数命名、内存映射等细节。
- Stateflow:可选,当你需要在状态机中调用 Simulink Function 时使用。
在 MATLAB 命令窗口中执行ver可以查看当前环境安装了哪些工具箱,例如:
ver输出结果中如果列出了 Simulink、MATLAB Coder 等工具箱名称和版本号,就说明环境已经具备对应的功能。
本文示例以 R2021a 作为参考版本,但核心操作在 R2018a 到 R2024a 之间基本兼容。需要特别说明的是,Simulink Function 的代码生成能力依赖于 MATLAB Coder 和 Embedded Coder,如果只做纯仿真验证,不安装这两个工具箱也能正常使用。
3. Simulink Function 核心概念与参数详解
在动手建模之前,先理解几个关键概念,否则后面操作时容易卡住。
3.1 Simulink Function 模块的组成
从模块库浏览器拖一个 Simulink Function 模块到模型中后,初始状态下它是一个空壳。双击进去,可以看到一个和普通子系统类似的画布,但右上角会显示函数签名信息,例如y = f(u)。这就意味着这个函数有一个输入u,一个输出y。
Simulink Function 模块通过函数签名来确定输入输出端口的数量和类型。在 Function 编辑器中,可以使用专门的函数端口来定义输入输出参数,也可以直接通过模块菜单来修改函数名称、参数名称和数据类型。
具体来说,Simulink Function 支持三种端口类型:
argument input:函数的输入参数,对应函数括号中的输入变量。argument output:函数的输出参数,对应函数括号中的输出变量。argument inout:既是输入又是输出的参数,适合需要修改传入变量的场景,但在代码生成时需要注意可读性。
一个典型的函数签名可以是:
y = limit_signal(x, low, high)其中x、low、high是三个输入,y是一个输出。
3.2 Function Caller 的作用
Function Caller 是调用 Simulink Function 的模块,位置也在 User-Defined Functions 库中。它的作用是:在模型中某个位置通过函数名调用已经定义好的 Simulink Function,并完成参数的传入和输出结果的接收。
Function Caller 模块的配置方式是这样的:双击它,需要填写函数名称、参数列表,点击刷新后会自动识别对应的 Simulink Function 中的端口定义。你也可以手动输入函数原型,例如:
y = limit_signal(x, low, high)然后点击“刷新”或者直接连接信号线,Simulink 会根据函数原型自动推断端口的数量和数据类型。
3.3 Simulink Function 与子系统的区别
这个点值得单独强调。普通子系统 Subsystem 是“信号流”驱动的,信号从输入端流入,经过内部模块计算,再从输出端流出。调用方和子系统之间通过信号线连接,数据是持续流动的。
Simulink Function 则是“函数调用”驱动的。Function Caller 在执行到某个时刻时,会触发函数体的执行,计算完成后把结果返回给调用者。从执行语义上看,它更接近代码中的函数调用。
但是需要说明的是,Simulink Function 内部也可以包含连续模块或离散模块,因此并不是说它只能用于瞬时计算。关键区别在于调用语义:Simulink Function 通常被 Function Caller、Stateflow 的调用语句或者底层的 Function-Call Subsystem 触发,而普通子系统是由输入信号持续驱动的。
3.4 Simulink Function 在模块库中的路径
为了后续操作方便,这里把模块路径写清楚:
Simulink > User-Defined Functions > Simulink Function Simulink > User-Defined Functions > Function Caller在模块库浏览器的左上角搜索框中直接输入“Simulink Function”也可以快速定位。如果你使用的是较新版本,也可以注意到模块图标上通常会有一个函数名占位符显示。
4. 完整实战案例:创建一个限幅函数
接下来我们通过一个完整的案例,演示如何创建 Simulink Function、配置函数签名、编写内部逻辑、用 Function Caller 调用,并最终运行仿真验证结果。
案例需求:在日常信号处理中经常需要做限幅,也就是把信号约束在指定的上下限之间。我们定义一个函数y = limit_signal(x, low, high),输入信号x,下限low和上限high,输出限幅后的信号y。逻辑如下:如果x大于high,则输出high;如果x小于low,则输出low;否则输出x本身。
选择这个案例的原因是逻辑简单、结果容易验证,同时它足够说明 Simulink Function 的完整使用流程。你可以在此基础上替换成 PID 计算、滤波算法、查表逻辑等各种实际工程内容。
4.1 创建模型
在 MATLAB 工具栏或者命令行中新建一个模型:
new_system('simulink_function_demo'); open_system('simulink_function_demo');也可以直接在 Simulink 起始页选择“Blank Model”创建空模型。模型创建好后,先保存为simulink_function_demo.slx,命名越规范越好,方便后续文件管理和代码生成。
4.2 添加 Simulink Function 模块
打开模块库浏览器,进入Simulink > User-Defined Functions,将Simulink Function拖入模型中。此时模型中会出现一个名为Fcn的浅色模块,默认显示函数签名占位符。
双击模块,进入函数内部编辑器。在函数编辑器界面中,可以看到一个和普通子系统类似的画布,画布上方或侧边有端口定义区域。我们需要做两件事:定义函数签名,以及布置内部逻辑。
右键点击函数编辑器中的端口定义区域,选择“Add Argument Input”和“Add Argument Output”。分别添加:
- 输入参数
x - 输入参数
low - 输入参数
high - 输出参数
y
添加完成后,返回模型层,Simulink Function 模块上会显示函数名limit_signal以及对应的参数。如果函数名不对,双击模块并在模块属性中修改为limit_signal。
这里需要注意的是,Simulink Function 的模块名和函数签名中的函数名并不一定完全相同。在使用 Function Caller 时,实际调用的是签名中的函数名。为了减少混淆,建议将模块名和函数名保持一致的命名规范。
4.3 编写函数内部逻辑
在 Simulink Function 内部,你可以像搭建普通子系统一样搭建逻辑。有两种常见方式:
第一种:纯模块搭建。拖入三个输入端口、一个输出端口,然后在中间使用Min和Max模块组合实现限幅逻辑。具体搭法是:y = min(max(x, low), high)。使用Math Operations库中的MinMax模块,一个配置为max,一个配置为min,再把端口对应连好。
第二种:使用 MATLAB Function 写代码。这种方式实现自定义算法更灵活。在函数编辑器内部空白处双击,输入“MATLAB Function”并选择,然后双击该模块,输入以下代码:
function y = limit_signal(x, low, high) if x > high y = high; elseif x < low y = low; else y = x; end end这里需要注意,Simulink Function 本身已经有输入输出参数的定义。如果你使用 MATLAB Function 块,需要让 MATLAB Function 的输入输出参数与函数签名对应起来。更简洁的做法是:在 MATLAB Function 块的端口列表中将输入设为x、low、high,输出设为y,然后在函数代码中实现逻辑。
推荐使用第二种方式,因为代码可读性和可维护性更高,也更像真实项目中的算法实现方式。
4.4 添加 Function Caller 并调用
回到模型层,从Simulink > User-Defined Functions中拖入一个Function Caller模块,放置在 Simulink Function 模块旁边。
双击 Function Caller,在配置界面中填写函数原型:
y = limit_signal(x, low, high)点击“Refresh”按钮后,Simulink 会在模型中搜索匹配的函数定义,并自动更新端口的数量和类型。如果搜索成功,Function Caller 模块上会出现三个输入端口和一个输出端口。
接下来,我们搭建一个简单的测试激励:
- 用
Sources库中的Sine Wave模块产生一个幅值为 2 的正弦信号,作为x输入。 - 用两个
Constant模块分别作为low和high输入,例如low = -0.5,high = 0.5。 - 将 Function Caller 的输出连接到
Sinks库中的Scope模块。
搭建完成后,模型中各个模块的连接关系如下:
Sine Wave输出 -> Function Caller 的x输入端口Constant(-0.5)-> Function Caller 的low输入端口Constant(0.5)-> Function Caller 的high输入端口- Function Caller 的
y输出端口 ->Scope
4.5 运行仿真与验证
将仿真停止时间设置为 10 秒,然后点击运行按钮,或者使用快捷键。仿真结束后,打开 Scope 观察波形。
预期结果:原始正弦波幅值为 2,经过限幅后,波形被限制在 -0.5 到 0.5 之间,超出部分被削平,其余部分保持原样。这个结果直接验证了y = min(max(x, low), high)的逻辑。
如果你使用的是 MATLAB Function 实现方式,也可以在仿真前先编译一下模型:
slbuild('simulink_function_demo');如果模型可以正常构建,说明 Simulink Function 的函数签名、参数类型和内部逻辑没有问题。注意,slbuild需要 MATLAB Coder 支持,如果只做仿真,可以跳过这一步。
5. 常见问题与排查思路
在学习和使用 Simulink Function 的过程中,以下几个问题出现频率很高。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| Function Caller 提示找不到函数定义 | 函数名拼写不一致,或 Function Caller 与 Simulink Function 不在同一个模型命名空间 | 检查函数签名,确认模型中没有重名函数;在两个模块中统一函数名 |
| 输入输出端口数量对不上 | Function Caller 中的函数原型写错,或 Simulink Function 内部端口定义有误 | 重新梳理函数签名,确保y = f(x1, x2)中的参数数量与端口定义一致 |
| 模型编译报错,提示函数参数维度不明确 | 信号源的数据类型或维度没有被正确推断 | 在 Function Caller 和 Simulink Function 中将参数数据类型显式设置为 double,或使用信号属性检查器 |
| Simulink Function 模块显示红色,提示未定义签名 | 没有在函数编辑器中添加输入输出参数 | 双击模块,在编辑器中添加 Argument Input 和 Argument Output |
| 代码生成时函数名被随机命名 | 没有在代码生成映射表中配置函数名称 | 使用 Embedded Coder 的 Code Mapping 设置函数名,或关闭“函数内联”优化 |
| 无法在 Stateflow 中调用 Simulink Function | Stateflow 图表没有放在支持函数调用的上下文中 | 在 Stateflow 的函数调用语句中使用正确的函数名和参数,并在模型配置中启用对外部函数的访问 |
如果你遇到“找不到数据字典”这一类问题,例如提示can.sldd或hwa.sldd找不到,这实际上和数据字典的引用路径有关,不是 Simulink Function 本身的问题。编辑模型中引用了缺失的.sldd文件,导致模型更新或编译失败。解决方法是检查模型的 Data Dictionary 配置,删除失效引用或重新加载正确路径的字典文件。
下面是几个高频报错的根因分析和修复建议:
Error: Function caller 'Function Caller' does not have a matching function definition这个报错的意思是 Function Caller 找不到匹配的 Simulink Function。通常是因为函数名不匹配。检查这两个地方:一是 Function Caller 模块中填写的函数原型,二是 Simulink Function 模块中定义的函数名。如果模型中有多个子系统且使用了数据字典,还要检查函数作用域是否在可见范围内。
Error: Invalid actual argument type for formal argument 'y'出现这个报错通常是因为输入输出的数据类型或维度不匹配。比如 Simulink Function 中定义的是 double,但 Function Caller 的输入信号是 boolean 或者 int8。此时可以使用 Signal Conversion 模块或者 Data Type Conversion 模块统一信号类型。
另外有一个常见误区:初学者容易把 Simulink Function 当成“必须用 MATLAB Function 块才能写逻辑”。实际上,Simulink Function 内部也可以直接放置 Simulink 模块。到底用哪种方式,取决于个人习惯和算法复杂度。如果是矩阵运算、复杂循环,用 MATLAB Function 更方便;如果是纯粹的标准模块连接,直接用模块搭建反而更直观。
6. 最佳实践与工程建议
工程实践中,Simulink Function 的使用不能停留在“会拖模块”的层面,还需要考虑模型的可维护性、代码生成质量和团队协作。这里整理几条实用的建议。
6.1 函数命名与签名设计
命名风格建议参考 C 语言函数命名方式:
- 函数名使用小写字母加下划线,例如
calculate_soc、limit_signal。 - 参数名使用小写字母,尽量语义化,例如
current_value、upper_limit。 - 避免使用无意义的
u1、u2作为参数名,否则调用方很难理解输入含义。
在函数签名上,尽量把可变参数和配置参数分开。配置参数可以理解为标定量或常数,比如限幅上下限;可变参数则是实时变化的信号。这样有助于后续接入标定工具或参数调优。
6.2 函数粒度控制
Simulink Function 不是越细越好,也不是越大越好。如果一个函数内部包含了几十个模块、几百行代码,那它实际上已经变成了一个子系统,失去了函数封装的意义。另一方面,如果函数只有一行加法运算,调用开销和模型复杂度反而不划算。
合理的做法是:将一个相对独立的算法功能封装成一个函数,比如限幅、滤波器、PID 计算、查表、数据处理协议等。每个函数只做一件事,接口控制在 2 到 5 个参数之间。
6.3 使用代码生成映射表
如果你使用 Embedded Coder 生成嵌入式代码,Simulink Function 的一个突出优点是可以直接映射成 C 函数。打开模型配置界面,进入 Code Generation > Interface 或 Code Mapping 设置,可以看到当前模型中的 Simulink Function 列表。在这里可以设置生成的 C 函数名称、参数名称、文件放置位置等。
例如,你希望生成的 C 函数叫LIMIT_SIGNAL,可以在代码映射表中修改。同时,你还可以设置函数是否内联。如果函数被频繁调用且逻辑简单,可以允许内联;如果函数逻辑复杂且希望支持单元测试,建议保留独立函数。
6.4 模型分层与复用
在大型项目中,Simulink Function 的使用应该和模型分层设计结合起来。比如一个车辆控制模型可以分为:
- 软件架构层:负责调度、模式管理、输入输出处理。
- 算法层:使用 Simulink Function 封装各种控制算法,比如挡位计算、扭矩限制、状态估计。
- 器件层:对接底层硬件驱动或 CAN 通信接口。
在这样的分层中,Simulink Function 通常放在算法层,由架构层或 Stateflow 状态机调用。这样做的好处是算法逻辑不依赖具体信号线的连接顺序,代码可读性和复用性都会明显提升。
6.5 单元测试与验证
Simulink Test 工具箱可以创建测试用例,对 Simulink Function 进行单元级验证。将输入参数作为测试用例的输入,检查输出是否符合预期。如果你还没有使用 Simulink Test,也可以用 MATLAB 脚本建立激励和期望值,然后运行仿真并比较结果:
% 简单测试脚本示例 input_data = -2:0.1:2; low = -0.5; high = 0.5; expected_output = min(max(input_data, low), high); % 运行仿真后从 Scope 或 out 变量中获取 Simulink Function 的实际输出 actual_output = simout; assert(max(abs(actual_output - expected_output)) < 1e-6, ... 'Simulink Function output mismatch!');这种自动化验证方式在大规模模型开发中非常实用。
6.6 注意版本兼容性与工具箱依赖
不同 Simulink 版本的模块库路径和界面细节可能不同。在团队协作中,如果不统一版本,同一个模型在不同机器上可能出现模块解析错误或外观不一致。建议在项目根目录使用slprj文件夹管理生成文件,并在README中注明 MATLAB 版本和所需工具箱清单。
另外需要强调,Simulink Function 虽好,但并不是所有场景都适用。如果你的模型中只在一处使用某段逻辑,使用普通子系统或直接连接模块反而更直观。设计原则仍然是:先考虑可读性,再考虑复用性。
7. 总结与学习路线
这篇文章从模块库浏览器的基本结构讲起,重点介绍了 Simulink Function 模块的定位、语法和使用场景。通过一个限幅函数的完整案例,可以看到创建 Simulink Function、配置函数签名、使用 Function Caller 调用、运行仿真验证的完整流程。同时,我们也分析了 S-Function、MATLAB Function 与 Simulink Function 这三类用户自定义模块之间的区别,这有助于在实际建模时选择正确的工具。
如果接下来你想继续深入,可以按下面的路径学习:
- 先掌握 Simulink 常用模块库中 Sources、Sinks、Math Operations、Logic and Bit Operations 等基础模块的使用,这是后续所有建模操作的地基。
- 学习 Stateflow 基础。Simulink Function 经常配合 Stateflow 状态机使用,状态跳转过程中调用函数会带来更灵活的逻辑控制。
- 学习 S-Function 的编写。尤其是需要集成外部 C/C++ 算法时,S-Function 是一个绕不开的知识点。
- 学习 Simulink Coder 和 Embedded Coder。当你需要从模型生成 C 代码时,理解代码映射、数据类型、函数命名规则非常重要。
- 学习如何在项目中使用数据字典或模型引用,把多个负责不同功能的 Simulink Function 组织起来,形成清晰的模型层级。
在实际项目中,建议你先从一个小函数开始改造现有模型。比如把一个你已经写好的 MATLAB Function 块改造成 Simulink Function,再通过 Function Caller 调用,观察两者的区别。只有亲手操作一遍,才能理解函数签名、参数传递和调用时机这些问题。
如果你在使用 Simulink Function 时遇到过其他奇怪的问题,欢迎在评论区交流,也可以把报错信息贴出来一起分析。下一篇内容可以接着聊聊 S-Function 的编写与调试,或者 Stateflow 与 Simulink Function 的配合技巧,看大家需要再决定。