第十章 用 S-函数扩展 Simulink
通过第八、第九章的学习,用户对用 Simulink 建模的基本思想已经有了清晰的认识,Simulink 为用
户提供了许许多多的内置库模块,用户只需使用这些库模块构建系统即可。但在实际应用中,用户通常
会发现有些过程用 Simulink 的库模块不容易建模。这时,可以使用 S-函数来扩展 Simulink。S-函数
结合了 Simulink 框图图形化的特点和 MATLAB 编程灵活方便的优点,从而给用户提供增强和扩展 Simulink
的强大机制,同时它也是使 RTW(Real Time Workshop)实现实时仿真的关键。
10.1 S-函数概述
10.1.1 S-函数的基本概念
S-函数是 System function 系统函数的简称,是指采用非图形化(即计算机语言,而非 Simulink
系统模块)的方式描述的功能模块。在 MATLAB 中,用户除了可以使用 MATLAB 代码编写 S-函数以外,还
可以使用 C、C++、FORTRAN 或 Ada 语言编写 S-函数,只不过用这些语言编写程序时需要用编译器生成
动态连接库(DLL)文件,然后在 Simulink 中直接调用。
S-函数是由一种特殊的语法构成的,用来描述并实现动态系统的。它采用一种特殊的调用语法,使
函数和 Simulink 求解器进行交互。这种交互与求解器和 Simulink 仿真模型间的交互相类似:S-函数接
受来自 Simulink 求解器的相关信息,并对求解器发出的命令做出适当的响应。
S-函数作为与其它语言结合的接口,可以使用这个语言所提供的强大功能。例如,使用 MATLAB 语
言编写的 S-函数称为 M 文件 S-函数,它可以充分利用 MATLAB 所提供的丰富资源,方便地调用各种工
具箱函数和图形函数;而使用 C 语言编写的 S-函数被称为 C-MEX 文件 S-函数,则可以实现对操作系
统和外部设备等的访问,也可以提供与操作系统的接口。另外,S-函数可以使用其他多种语言编写,因
此可以实现代码的移植,即将已有的代码结合进来,而不需在 Simulink 中重新实现算法。
S-函数中采用非图形化的方式描述系统,其内部采用文本方式输入描述系统的公式、方程,这种方
式非常适合复杂动态系统的数学描述,且可以在仿真过程中对仿真进行精确的控制。
10.1.2 如何使用 S-函数
在动态系统仿真中,要想将 S-函数加入 Simulink 仿真模型中,用户需要将 User Defined Functions
模型库中的 S-Function 模块拖进该模型中。S-Function 模块是一个单输入单输出模块,如果有多个输
入与输出信号,用户需要使用 Mux 模块和 Demux 模块对信号进行组合或分离。S-Function 模块仅仅是以
图形的方式提供给用户一个 S-函数的使用接口,在它的参数设置对话框中仅包含 S-函数的名称及函数
所需的参数列表(见图 10.1),而 S-函数所实现的功能则由 S-函数源文件描述,S-函数源文件必须由
用户自行编写。
使用 S-函数的步骤如下:
一、在系统的 Simulink 仿真框图中添加 S-function 模块,并进行正确的设置;
二、创建 S-函数源文件。创建 S-函数源文件的方法有多种。用户可以按照 S-函数的语法格式自行编
写代码,但是这样做很麻烦,且容易出错。Simulink 在 S-function Examples 模型库中为用户提供了针
对不同语言的很多 S-函数模板和例子,用户可以根据自己的需要修改相应的模板或例子即可完成 S-函
数源文件的编写工作;
三、在系统的 Simulink 仿真框图中按照定义好的功能连接输入输出端口。
这里需要说明的是,S-function 模块中 S-函数名称必须和用户建立的 S-函数源文件的名称完全
相同,S-function 模块中的 S-函数参数列表必须按照 S-函数源文件中的参数顺序赋值,且参数之间
需要用逗号隔开。另外,用户也可以使用子系统封装技术对 S-函数进行封装,这样做的好处是可以增强
系统模型的可读性。
122
S-函数块
S-函数参数设置对话框
S-函数源文件
图 10.1 S-函数模块、参数设置对话框及其源文件的关系
为了方便用户编写 C MEX S 函数,Simulink 的 User Defined Functions 模型库中为用户编写 C MEX
S 函数提供了一个图形化的集成的图形开发环境 S-function builder。用户只需在 S-function builder
中相应的位置写入相应的名称和代码即可编译成相应的 MEX 文件。
为了使读者尽快掌握 S-函数的使用步骤,先举一个简单的例子。
例 10.1 使用 S-函数实现系统: 2*
=
y
u
。
解:一、在 Simulink 模型框图中添加 S-function 模块,打开 S-funfction 模块的参数设置对话框,
参数 S-function name 需设置为 timestwo。
二、创建 S-函数源文件。
1、打开 M 文件 S-函数模板文件 Sfuntmpl.m,并在指定目录下另存为 timestwo.m。打开 M 文件 S-函数
模板文件 Sfuntmpl.m 的方法由两种:
方法一,在 MATLAB 命令窗口键入 edit Sfuntmpl 即可;
方法二,在 Simulink 浏览器中寻找 S-function demos 模型库,其中包含各种语言编写的 S-函数
例子和模板。对于本例,用户只需点击 M-file S-functions 就可以看到 M 文件 S-函数模板文件
Sfuntmpl.m 和几个编程例子,双击模板文件 Sfuntmpl.m 即可。
2、修改模板。找到函数 mdlInitializeSizes,修改以下代码:
size.NumOutputs=1;
size.NumIutputs=1;
找到函数 mdOutputs,将代码 sys=[ ]改为 sys=2*u
至此,已经写好了该 S-函数的源文件,保存修改即可。
三、在 Simulink 模型框图中按要求添加并连接各个模块。系统 Simulink 仿真模型如图 10.2 所示。
四、运行仿真。仿真结果可以从 Scope 模块中观察。结果如图 10.2 所示。
10.1.3 与 S-函数相关的术语
用户对 S-函数相关的术语的理解对于了解 S-函数的工作原理、编写 S-函数源文件是非常有用的。
一、仿真例程(Routines)
123
图 10.2 例 10.1 系统 Simulink 仿真模型及其结果
Simulink 在仿真的不同阶段调用 S-函数的不同的功能函数以完成不同的任务。对于 M 文件 S-函数,
Simulink 通过传递一个 flag 参量给 S-函数,通知 S-函数当前所处的仿真阶段,以便执行相应的功能
函数。S-函数的功能函数包括初始化、计算输出、更新离散状态、计算导数、结束仿真等。这些功能函
数称为仿真例程或回调函数(call back function)。在写 M 文件 S-函数时,用户只需用 MATLAB 语言为
每个 flag 对应的功能函数编写代码即可。表 10.1 列出了各仿真阶段的功能函数及其对应的 flag 值。
表 10.1 各仿真阶段的仿真例程及其 flag 值
仿真阶段
S-函数仿真例程(回调函数)
Flag 值(M 文件 S-函数)
初始化
mdlInitializeSizes
计算下一个采样点
mdlGetTimeofNextVarHit
计算输出值
更新离散状态
计算导数
结束仿真
mdlOutputs
mdlUpdate
mdlDerivatives
mdlTerminate
二、直接馈入(Direct Feedthrough)
0
4
3
2
1
9
直接馈入是指模块的输出或采样时间(变速率模型)直接由其某个输入端口控制。判断一个 S-函数
是否具有直接馈入的标准是:
某时刻系统的输出 y 包含该时刻系统的输入 u,即计算系统输出的方程中包含输入变量 u;
若系统是一个变采样时间系统,且下一个采样点的计算与输入 u 有关。
馈入标志的设置不仅关系到系统模型中的系统模块的执行顺序,而且关系到对代数环的检测和处理。
因此正确设置馈入标志是非常重要的。
三、采样时间和偏移量(Sample time & offsets)
M 文件和 C MEX 文件 S-函数都允许用户十分方便地设定 S-函数被调用的时间。
采样时间在离散系统中控制采样点的间隔,偏移量则用于延迟采样点。一个采样点对应的时间值由
下列公式计算:
其中,n 表示当前仿真步,是整数。
TimeHit=n×period+offset
如 果 用 户 定 义 了 一 个 离 散 采 样 时 间 , Simulink 就 会 在 所 定 义 的 每 个 采 样 点 调 用 S - 函 数 的
mdlOutputs 和 mdlUpdate 例程。
对于连续时间系统,采样时间和偏移量的值均应设置为零。采样时间还可以继承来自驱动模块、目
124
标模块或系统最小的采样时间,这种情况下,采样
时间值设置为-1。
四、动态输入(Dynamically sized inputs)
S-函数支持动态可变维数的输入。S-函数的
输入变量的维数取决于驱动 S-函数模块的输入信
号维数。仿真开始时,通过 size 或 length 函数确
定输入信号的维数。然后就可以利用这个维数来估
计连续状态数目、离散状态数目和输出向量的维数。
在 M 文件 S-函数中动态设置输入维数时,应该
把 sizes 数据结构的对应成员设置为-1。
比 如 在 例 10.1 中 , 如 果 将 函 数
mdlInitializeSizes 中 的 代 码 设 置 成
size.NumOutputs=-1; size.NumIutputs=-1; 则
当输入是两维信号,分别为幅值为 1 和 3 的正弦信
号,系统的输出也是两维信号,结果如图 10.3 所示。
图 10.3 例 10.1 进行动态输入设置后的仿真结果
10.2 S-函数工作原理
了解 S-函数的工作原理对于用户掌握 S-函数的编写方法是非常有用的,对用户对于 Simulink 的
仿真原理的理解也是很有帮助的。本节介绍 S-函数的工作原理。
在具体介绍 S-函数的工作原理之前,首先需要回顾一下 Simulink 模块的工作原理。
Simulink 中的每个模块都有三个基本元素:输入向量、状
态向量和输出向量,分别表示为u , x 和 y 。图 10.4 反映了它
们之间的关系。在 Simulink 模块的三个元素中,状态向量是最
重要的,也是最灵活的概念。在 Simulink 中状态向量可以分为
连续状态、离散状态或两者的结合。输入、输出及状态的关系
可以用状态方程描述:
图 10.4 Simulink 模块的基本模型
输出方程:
y
=
f
o
t
( ,
x u
,
)
连续状态方程:
d
=x
f
d
t
( ,
x u
,
)
离散状态方程: 1
k
x
+ =
uf
t
( ,
x,u
)
其中 [
=x
d
x
]1k
x 。
+
Simulink 在仿真时将上述方程对应不同的仿真阶段,它们分别是计算模块的输出、更新离散状态、
计算连续状态的微分。在仿真开始和结束,还包括初始化和结束仿真两个阶段。在每个阶段,Simulink
都反复地调用模块。
至此,读者已经接触到了几个关于仿真的概念:仿真步长(Simulation step)、仿真阶段(Simulation
stage)。为了深入了解 S-函数的工作原理,还需了解一个概念:仿真循环(Simulation loop)。一个仿
真循环就是由仿真阶段按一定顺序组成的执行序列。对于每个模块,经过一次仿真循环就是一个仿真步
长,而在同一个仿真步长中,模型中各模块的仿真按照事先排好的顺序依次执行。这个过程可以用图 10.5
表示。
从图中可以看出,在仿真开始时,Simulink 首先对模型进行初始化,此阶段不属于仿真循环。在所
有模块都初始化后,模块进入仿真循环,在仿真循环的每个阶段,Simulink 都要调用模块或者 S-函数。
125
由于在积分时,对仿真步长有要求,所以此时需要
将仿真步长细化。完成一个仿真循环就进入下一个
仿真步长,如此循环直至仿真结束。
在调用模型中的 S-函数时,Simulink 会调用
用户定义的 S-函数的例程来实现每个仿真阶段要
完成的任务。这些任务包括:
一、初始化:仿真开始前,Simulink 在这个阶段初
始化 S-函数,完成的主要工作包括:
1 、 初 始 化 包 含 S - 函 数 所 有 信 息 的 结 构 体
SimStruct;
2、确定输入输出端口的数目和大小;
3、确定模块的采样时间;
4、分配内存和 Sizes 数组。
二、计算下一个采样时刻。如果模型使用变步长求
解器,那么就需要在当前仿真步长内确定下一个采
样点的时间,也即下一个仿真步长的大小;
三、计算输出:计算所有输出端口的输出值。
四、更新离散状态:此例程在每个仿真步长处都要
执行一次,为当前时间的仿真循环更新离散状态;
五、数值积分:这个阶段只有模块具有连续状态和
非采样过零点时才会存在。如果 S-函数存在连续
状态,Simulink 就在细化的小时间步长中调用 S-
函数的输出(mdlOutputs)和微分(mdlDerivatives)
例程。如果存在非采样过零点,Simulink 将调用 S
- 函 数 中 的 输 出 ( mdlOutputs ) 和 过 零 检 测
(mdlZeroCrossngs)例程,以定位过零点。
初始化模型
计算下一个采样时间点
(仅适用变采样速率模块)
环
循
真
仿
计算输出
更新离散状态
数值积分
计算导数
计算输出
计算导数
检测过零事件
p
e
t
s
e
m
i
t
r
o
n
i
M
p
e
t
s
e
m
i
t
r
o
j
a
M
仿真结束
图 10.5 S-函数仿真流程
10.3 编写 M 文件 S-函数
由前面的介绍可以知道,S-函数是由一系列仿真例程组成的。这些仿真例程就是 S-函数特有的语
法结构,用户编写 S-函数的任务就是在相应的例程中填写适当的代码,供 Simulink 及 MATLAB 求解器调
用。M 文件 S-函数结构明晰,易于理解、书写方便,可以调用丰富 MATLAB 函数,所以在实际工作中得
到了广泛的应用。
M 文件 S-函数利用 Flag 标志控制调用例程函数的顺序。各仿真阶段的仿真例程及对应的标志值如
表 10.1 所示。
M 文件 S-函数的仿真流程同图 10.5 介绍的 S-函数的仿真流程。在初始化阶段,通过标志 0 调用 S
-函数,并请求提供输入输出个数、初始状态和采样时间等信息。然后,仿真开始。下一个标志为 4,请
求 S-函数提供下一步的采样时间(这个例程在单采样速率系统下不被调用)。接着 flag=3 计算模块的
输出,flag=2 更新离散状态,当需要计算连续状态导数时 flag=1。然后求解器使用积分例程计算状态
的值。计算状态导数和更新离散状态之后通过标志 3 计算模块的输出。这样就完成了一个仿真步长的工
作。当到达结束时间时,采用标志 9 完成结束前的处理工作。
10.3.1 M 文件 S-函数模板
126
Simulink 为用户提供了各种语言编写 S-函数的模板文件。这些 S-函数的模板文件中定义了 S-函
数的框架结构,用户可以根据自己的需要修改。
编写 M 文件 S-函数时,需要使用 M 文件 S-函数模板文件 sfuntmpl.m 文件。该文件包含了所有的 S
-函数的例程,及包含 1 个主函数和 6 个子函数。在主函数中,程序使用一个多分支语句(Switch-case)
根据标志将执行流程转移到相应的例程函数。主函数的参数 Flag 标志值是由系统(Simulink 引擎)调用
时给出的。读者可以打开并阅读该 M 文件 S-函数模板文件。
一、打开模板文件的方法由两种,用户可以在 MATLAB 命令窗口中键入:
>> edit sfuntmpl
或者双击 User-defined Function \S-function Examples\M-file S-functions\Leveal-1 M-file
S-functions1\ Leveal-1 M-file template 模块。
二、M 文件 S-函数模板文件代码
M 文件 S-函数模板文件的代码如下:
%主函数
function [sys,x0,str,ts] = sfuntmpl(t,x,u,flag)
switch flag,
case 0,
[sys,x0,str,ts]=mdlInitializeSizes;
case 1,
sys=mdlDerivatives(t,x,u);
case 2,
sys=mdlUpdate(t,x,u);
case 3,
sys=mdlOutputs(t,x,u);
case 4,
sys=mdlGetTimeOfNextVarHit(t,x,u);
case 9,
sys=mdlTerminate(t,x,u);
otherwise
error(['Unhandled flag = ',num2str(flag)]);
end % 主函数结束,下面是各个子函数,即各个仿真例程
% 初始化例程子函数:提供状态、输入、输出、采样时间数目和初始状态的值。
function [sys,x0,str,ts]=mdlInitializeSizes
sizes = simsizes; % 生成 sizes 数据结构
sizes.NumContStates = 0; % 连续状态数,缺省为 0
sizes.NumDiscStates = 0; % 离散状态数,缺省为 0
sizes.NumOutputs = 0; % 输出量个数,缺省为 0
sizes.NumInputs = 0; % 输入量个数,缺省为 0
sizes.DirFeedthrough = 1; % 有无直接馈入,有取 1,无取 0,缺省为 1
sizes.NumSampleTimes = 1; % 采样时间个数,至少取 1
sys = simsizes(sizes); % 返回 sizes 数据结构所包含的信息
127
x0 = []; % 设置初始状态
str = []; % 保留变量,置为空矩阵
ts = [0 0]; % 采样时间:[采样周期 偏移量],采样时间取 0 表示为连续系统
% 计算导数例程子函数:计算连续状态的导数,用户需在此例程输入连续状态方程。
% 该子函数可以不存在。
function sys=mdlDerivatives(t,x,u)
sys = []; % sys 表示连续状态导数
% 状态更新例程子函数:计算离散状态的更新。
% 用户除了需在此输入离散状态方程外,还可以输入其它每个仿真步长都有必要执行的代码。
% 该子函数可以不存在。
function sys=mdlUpdate(t,x,u)
sys = []; % sys 表示下一个离散状态,即 x(k+1)
% 计算输出例程子函数: 计算模块输出。该子函数必须存在,用户在此输入系统的输出方程。
function sys=mdlOutputs(t,x,u)
sys = []; % sys 表示系统输出 y
% 计算下一个采样时间, 只有变采样时间系统才调用此仿真例程。
function sys=mdlGetTimeOfNextVarHit(t,x,u)
sampleTime = 1; % 设置下一次的采样时间是 1s 以后
sys = t + sampleTime; % sys 表示下一个采样时间点
% 仿真结束调用的例程函数:用户需在此输入结束仿真所需要的必要工作。
function sys=mdlTerminate(t,x,u)
sys = [];
三、M 文件 S-函数模板文件的几点说明
主函数包含四个输出参数:sys 数组返回某个子函数,它的含义随着调用子函数的不同而不同;x0
为所有状态的初始化向量;str 是保留参数,总是一个空矩阵;Ts 返回系统采样时间。
主函数的四个输入参数分别是采样时间 t,状态 x,输入 u 和仿真流程控制标志变量 flag。
输入参数后面还可以附加一系列用户仿真需要的参数。
编写用户自己的 S-函数时,应将函数名改为 sfuntmpl 改为 S-function 模块中设置的函数名。
读者可能已经发现一个令人困惑的问题:不论在哪个仿真阶段,例程子函数的返回变量都是 sys。要
128
搞清楚这个问题,还要回到 Simulink 如何调用 S-函数上来。前面讲过,Simulink 在每个仿真步长的仿
真循环中的每个仿真阶段都要调用 S-函数。在调用时,Simulink 不但根据所处的仿真阶段为 flag 传入
不同的值,还会为返回变量 sys 指定不同的角色。即是说尽管是相同的 sys 变量,但在不同的仿真阶段
其意义是不相同的,这种变化由 Simulink 自动完成。
10.3.2 M 文件 S-函数的应用举例
了解了 M 文件 S-函数模板文件的代码、代码中各个部分完成的功能及各参数的含义后,用户可以着
手利用 S-函数进行系统仿真了。下面我们使用 M 文件 S-函数实现几种不同的系统。
一、含用户参数的简单系统
M 文件 S 函数除了模板文件中要求的几个必需的参数,还可以加入用户自定义的参数,自定义参数需
要在 S-函数的输入参数中列出。在含用户自定义参数的 S 函数中,主函数要做适当的修改以便将自定义
参数传递到子函数中,子函数也需要相应的修改以便接受自定义参数。在编写 S-函数时,应能区分哪些
参数会影响哪一个子函数的执行,要针对这些参数做相应的修改。还需注意的一点是,S-function 模块
中的参数设置对话框中的参数输入顺序应与 S-函数中自定义参数的顺序相同。
例 10.2 用 S-函数实现 gain 模块:增益值作为 S-函数用户自定义参数输入。
解:(1)编写 S-函数的源文件
修改 M 文件 S-函数的主函数:增加自定义参数,采用新的函数名:
function [sys,x0,str,ts] = sfun_var_gain(t,x,u,flag,gain)
由于增益参数只是用来计算输出值的,因而对初始化例程和计算输出例程子函数做修改,其他例程
均不需调用,不用做修改
case 0,
[sys,x0,str,ts]=mdlInitializeSizes(gain);
case 3,
sys=mdlOutputs(t,x,u,gain);
修改初始化例程子函数:
function [sys,x0,str,ts]=mdlInitializeSizes(gain)
sizes.NumContStates = 0;
sizes.NumDiscStates = 0;
sizes.NumOutputs = 1;
sizes.NumInputs = 1;
sizes.DirFeedthrough = 1;
定义计算输出例程子函数
function sys=mdlOutputs(t,x,u,gain)
sys = gain*u; % 输出=增益×输入
(2)建立如图 10.6 所示的系统仿真模型,将自定义参数设置为 3,运行仿真,仿真结果如图 10.6 所示,
验证了 S-函数的正确性。
129