SIMULINK6+SFunction+编程
(M,C/C++语言)与模块封装技术
丁竞渊 编译
上海大学计算机工程与科学学院
1. Simulink S函数概观
S-function(System function)是Simulink模块的计算机语言描述。可以用M、C/C++、Ada、Fortran 语言以MEX文件的形式编写。
S-function以特殊的方式与Simulink方程求解器交互。这种交互和Simulink内建模块的做法非常相似。S-function模块可以是连续、离散或者混合系统。
通过S-function,用户可以将自己的模块加入Simulink模型中。从而可以实现用户自定义的算法或者利用操作系统、硬件设备交互。
2. S函数工作机制
2.1 Simulink模块的数学描述
Simulink模块包括一系列输入、状态和输出。输出是采样时间、输入、模块状态的函数。
图1.
下面的方程描述了输入、输出和状态的数学关系。
图2.
2.2 仿真过程
Simulink模型的执行按下述几个步骤。首先是初始化阶段。在这一阶段Simulink将库模块集合到模型,传播宽度、数据类型和采样时间,评估模块参数,确定模块执行顺序,分配内存。然后是仿真阶段。此时Simulink进入一个仿真循环,循环的每次执行对应一个仿真步。在每个仿真步,Simulink按初始化阶段确定的顺序执行各个模块。对每个模块,Simulink计算模块在当前采样时间的状态、微分和输出。这将持续到仿真结束。图3描述了Simulink的仿真过程。
图3.
2.3 S-function的回调(Callback)方法
S-function包括一系列的回调方法,用以执行每个仿真步骤所需的任务。在一个模型的仿真过程中,每个仿真步骤,Simulink将调用各S-function的适当方法。S-function执行的方法包括:
● 初始化:在首次仿真循环中执行。Simulink初始化S-function。在这一步骤中Simulink将:
- 初始化SimStruct,这是一种Simulink结构,包含了S-function的信
息。
- 设置输入输出端口的个数和纬度。 - 设置模块的采样次数。
- 分配存储区域和数组长度。
● 计算下一采样点:如果定义了一个可变采样步长的模块,这一步将计算下一次采样点,也就是计算下一步长。
● 计算在主要时间步中的输出:这一步结束之后,模块的输出端口在当前时间步是有效的。
● 更新主要时间步中的离散状态:所有的模块在该回调方法中,必须执行一次每次时间步都要执行的活动,比如为下一次仿真循环更新离散状态。
● 积分:这用于具有连续状态的或者(和)具有非采样过零的模型。如果用户的S-function具有连续状态,Simulink在最小采样步长调用S-function的输出和微分部分。这也是Simulink之所以能计算S-function的状态。如果用户S-function(仅针对C MEX)具有非采样过零,Simulink在最小采样步长调用S-function的输出和微分部分,这样可以确定过零点。
3. 编写M语言的S函数
用M语言编写的S-function称为M-file S-functions,根据API版本不同,分为Level-2和Level-1类型。Level-2 的M-file S-function支持采用M语言实现具有全部功能的Simulink模块。Level-2 的M-file S-function APIs 非常接近于C MEX-file S-functions,许多特性可以相同使用。所以本节只叙述适用于M-file S-function的部分。Level-1的M-file S-function APIs是针对老版本的Simulink模块。
3.1 Level-2格式 M-File S-Function的API
Level-2 的M-file S-function APIs定义了,组成Level-2 的M-file S-function所需要的,回调(Callback)方法的信号和基本功能。这些API的实现决定了模块的属性(端口、参数和状态等)和行为(模块作为模块输入、状态和参数的函数的输出)。通过适当实现S-function的一组回调方法,用户可以实现满足用户需求的模块类型。
3.2 S-Function模板
Simulink提供了M语言的模板方便用户编写S-function。模板包含了Level-2 类型的M-file S-function API回调方法的基本实现。这个模板被存放于下面目录:
可以通过复制模板代码,编辑、增加的S-function回调方法,来实现用户需要的S-function模块。 3.3 Main方法 Level-2类型的M-file S-function中的main函数体,执行M-file S-function模块实例的初始化。在概念上Main函数类似于C-MEX S-functions的mdlInitializeSizes回调方法。Main函数执行的Setup任务包括下面步骤: ● 设置模块的输入输出端口个数。 ● 设置端口的维度、数据类型、复杂性和采样时间等属性。 ● 设置参数个数并检验参数的有效性。 ● 通过S-function模块的运行时对象的RegBlockMethod方法,将各个模块方法注册到所用的本地M文件中的函数。 3.4 运行时对象 当调用Level-2类型的M-file S-function回调方法时,Simulink将一个Simulink.MSFcnRunTimeBlock类的对象作为一个参数传递到该回调方法。这个实例被称为S-function模块的运行时对象。对于Level-2类型的M-file S-function,模块运行时对象相当于C MEX-file S-function回调方法中的SimStruct结构。它使回调方法可以获取模块元素,如端口、参数、状态、工作数组等,的信息。这可以通过调用S-function模块运行时对象的方法,或获取/设置模块运行时对象的属性实现。 4. 编写C语言的S函数 4.1 C MEX-file S-function简介 定义了S-function模块的C MEX-file必须在仿真过程中向Simulink提供模型信息。在仿真中Simulink、ODE求解器、MEX-file协作完成指定任务。这些任务包括:定义初始条件和模块特性,计算微分、离散状态和输出。 Simulink与C MEX-file S-function模块的交互仍是通过S-function的回调方法。每个回调方法执行一个预定义的,实现仿真所需功能的任务。S-function可以执行任何其实现的任务。一系列C MEX-file S-function实现的回调方法,都远大于M-file S-function中的。与M-file S-function不同的是,C MEX-file可以访问并修改Simulink内部用来存储S-function信息的数据结构。更多的回调方法和对Simulink内部数据结构的访问能力,使得C MEX-file S-function可以实现更丰富的模块特性,如处理矩阵信号和多种数据类型。 C MEX-file S-function只需实现Simulink定义的回调方法的一个小子集即可。如果不实现某个回调方法,相应的功能将被省略掉。这有利于快速开发简单的模块。 通常C MEX-file S-function的形式如下: #define S_FUNCTION_NAME your_sfunction_name_here #define S_FUNCTION_LEVEL 2 #include \"simstruc.h\" static void mdlInitializeSizes(SimStruct *S) { } static void mdlTerminate(SimStruct *S) { } #ifdef MATLAB_MEX_FILE /* Is this file being compiled as a MEX-file? */ #include \"simulink.c\" /* MEX-file interface mechanism */ #else #include \"cg_sfun.h\" /* Code generation registration function */ #endif mdlInitializeSizes是Simulink与S-function交互时调用的第一个方法。随后Simulink将调用其他S-function方法(都以mdl开头)。仿真结束时,Simulink调用mdlTerminate。 注意:与M-file S-function不同,C MEX-file S-function回调方法不是每个都具有flag参数。这是因为,Simulink仿真时直接在适当的时间调用每个回调方法。 4.2 自动建立S-function模块 S-Function Builder是通过规范定义和用户提供的C代码建立S-function的Simulink模块。S-Function Builder还用作普通的S-function在Simulink模型中的包装。 通过S-Function Builder建立S-function。 1. 将MATLAB当前目录设置到需要建立S-function的目录。 2. 创建新的Simulink模型。 3. 从Simulink User-Defined Functions library中将S-Function Builder拖入新建的ulink模型。 图4. 4. 双击模块打开S-Function Builder对话框。 图5. 5. 输入所需信息和用户代码。(详见下节) 6. 如果还未设置mex编译器,用mex –setup在MATLAB命令行设置。 7. 点Build按钮,启动建立过程。Simulink建立MEX文件实现指定的S-function,并存放在当前目录 8. 保存包含S-Function Builder模块的模型。 部署生成的S-Function 要在其他模型中使用生成的S-Function,首先必须检查生成的S-Function所在的目录是否在MATLAB路径中。然后把S-Function Builder模块从创建它的模型复制到目标模型并设置其参数。 S-Function Builder如何建立S-Function 4.3 S-Function Builder对话框 4.4 基本的C MEX-file S-function实例 本节介绍一个C MEX-file S-function实例:timestwo,实现输出信号放大为输入信号的2倍。以下是模型图: 图6. S-function timestwo的回调方法如图7所示: 图7. 以下是timestwo.c文件的代码: #define S_FUNCTION_NAME timestwo #define S_FUNCTION_LEVEL 2 #include \"simstruc.h\" static void mdlInitializeSizes(SimStruct *S) { ssSetNumSFcnParams(S, 0); if (ssGetNumSFcnParams(S) != ssGetSFcnParamsCount(S)) { return; /* Parameter mismatch will be reported by Simulink */ } if (!ssSetNumInputPorts(S, 1)) return; ssSetInputPortWidth(S, 0, DYNAMICALLY_SIZED); ssSetInputPortDirectFeedThrough(S, 0, 1); if (!ssSetNumOutputPorts(S,1)) return; ssSetOutputPortWidth(S, 0, DYNAMICALLY_SIZED); ssSetNumSampleTimes(S, 1); ssSetOptions(S, SS_OPTION_WORKS_WITH_CODE_REUSE | SS_OPTION_EXCEPTION_FREE_CODE | SS_OPTION_USE_TLC_WITH_ACCELERATOR); } static void mdlInitializeSampleTimes(SimStruct *S) { ssSetSampleTime(S, 0, INHERITED_SAMPLE_TIME); ssSetOffsetTime(S, 0, 0.0); ssSetModelReferenceSampleTimeDefaultInheritance(S); } static void mdlOutputs(SimStruct *S, int_T tid) { int_T i; InputRealPtrsType uPtrs = ssGetInputPortRealSignalPtrs(S,0); real_T *y = ssGetOutputPortRealSignal(S,0); int_T width = ssGetOutputPortWidth(S,0); for (i=0; i static void mdlTerminate(SimStruct *S) { } #ifdef MATLAB_MEX_FILE /* Is this file being compiled as a MEX-file? */ #include \"simulink.c\" /* MEX-file interface mechanism */ #else #include \"cg_sfun.h\" /* Code generation registration function */ #endif 这个实例包括3部分:宏定义和头文件;回调方法的实现;Simulink (或 Real-Time Workshop)接口。 宏定义和头文件 示例程序中包含两个宏定义: #define S_FUNCTION_NAME timestwo #define S_FUNCTION_LEVEL 2 第一个指定S-function的名字,第二个指示该S-function是Level-2格式的。 然后是包含头文件\"simstruc.h\",在其中定义了SimStruct数据结构和MATLAB应用程序接口(API)函数。SimStruct是Simulink用于保持S-function信息的数据结构,\"simstruc.h\"中还有用于MEX文件设置或获取SimStruct属性值的宏定义。 回调方法的实现 mdlInitializeSizes: Simulink调用mdlInitializeSizes方法查询S-function模块的输入输出端口数,端口容量,以及S-function所需的其他对象(如,状态个数等)。 Timestwo实现的mdlInitializeSizes方法指定了下面信息 ● 零参数: 意味着S-function对话框的参数框必须为空。否则,Simulink将报告参数不匹配。 ● 一个输入和一个输出端口: 输入输出端口的宽度可以动态变化。Simulink将把所有输入信号乘以2 作为输出信号的结果。注意,在这种情况下(一个输入一个输出端口),Simulink对S-function宽度的默认处理是输入和输出宽度相等。 ● 一个采样时间 例子timestwo在mdlInitializeSampleTimes中指定实际的采样时间。 ● 代码无异常处理 指定无异常处理的代码可以加速S-function的执行。作这项指定必须谨 慎。通常,如果用户S-function与MATLAB没有交互,指定无异常处理的代码是安全的。 mdlInitializeSampleTimes: Simulink调用mdlInitializeSampleTimes设置S-function的采样时间。每当timestwo模块的前驱模块执行一次,timestwo模块就执行一次,所以timestwo模块采用继承的采样时间。 mdlOutputs Simulink在每个时间步调用mdlOutputs计算模块的输出。Timestwo模块的mdlOutputs方法实现了将输入信号乘以2,将结果写到输出信号。 Timestwo模块的mdlOutputs方法使用了SimStruct中的宏: InputRealPtrsType uPtrs = ssGetInputPortRealSignalPtrs(S,0); 来实现对输入信号的访问。这个宏返回一个指针向量,必须通过*uPtrs[i]来访问。 Timestwo模块的mdlOutputs方法还使用了 real_T *y = ssGetOutputPortRealSignal(S,0); 来访问输出信号。这个宏返回一个包含模块输出的数组。 S-function使用 int_T width = ssGetOutputPortWidth(S,0); 来得到通过该模块的信号宽度。最后S-function循环通过输入得到输出。 mdlTerminate 执行仿真结束最后的任务。这是一个强制的S-function过程。不过由于timestwo模块不需要任何终止操作,所以其函数为空。 Simulink/Real-Time Workshop 接口 在S-function的最后,下面的特定代码将该例子关联到Simulink或者Real-Time Workshop: #ifdef MATLAB_MEX_FILE #include \"simulink.c\" #else #include \"cg_sfun.h\" #endif 生成timestwo实例 在命令行键入: mex timestwo.c mex命令将把timestwo.c编译、链接为一个可由Simulink动态加载执行的模块。这是一个动态链接库文件,在Window中,是个dll文件。 4.5 C MEX S-functions模板 Simulink提供了C MEX S-functions模板。 Simulink模型具有一个与之关联的SimStruct结构,而模型中的每个S-function模块也具有自己的SimStruct结构。这些SimStruct组成一个目录树,模型的SimStruct为根,S-function模块的SimStruct为孩子。 编译C S-function S-function可以以下列三种方式编译: ● MATLAB_MEX_FILE - 编译为用于Simulink的MEX文件。 ● RT - 通过Real-Time Workshop生成代码,成为一个具有固定步长求解器的实时应用程序。 ● NRT -通过Real-Time Workshop生成代码,成为一个具有变步长求解器的非实时应用程序。 4.6 Simulink和C MEX S-functions的互操作 本节从过程和数据两方面的观点来介绍Simulink和C MEX S-functions的交互。 过程观点 下图描述了Simulink调用S-function回调方法的顺序。实线框表示该回调函数在初始化和(或)仿真循环的每个时间步都被调用;虚线框表示该回调函数可能在初始化和(或)仿真循环的每个时间步被调用。 图8. 图9. 数据观点 S-function模块具有输入、输出信号,参数,内部状态,加上其他普通的工作区间。通常模块的输入输出信号都从模块的I/O向量读写。输入还可来自: ● 根输入模块的外部输入。 ● 本地磁盘,如果没有输入信号连接或者是落地的数据。 输出也可以写到根输出模块的外部输出。除了输入、输出信号,S-functions还具有: ● 连续状态。 ● 离散状态。 ● 其他工作区间,如实型、整形或指针工作向量。 利用S-function的对话框还可以实现S-function模块的参数化。 下图显示了这些不同类型数据的相互关系: 图10. S-function的mdlInitializeSizes方法可以设置不同信号和向量占用的空间。S-function访问信号有两种方式:通过指针或者通过毗邻的输入。 通过指针访问信号 在仿真循环中,可通过下面语句访问输入信号: InputRealPtrsType uPtrs = ssGetInputPortRealSignalPtrs(S,portIndex); uPtrs是一个指针数组,其中portIndex从0开始,每个端口都有一个。要访问信号的元素,必须使用*uPtrs[element] 如图所示: 图11. 注意,输入数组的指针可能会指向内存中毗邻的位置。类似的,可以通过下面函数得到输出信号: real_T *y = ssGetOutputPortSignal(S,outputPortIndex); 访问毗邻的输入信号 一个S-function的mdlInitializeSizes方法:ssSetInputPortRequiredContiguous,可用来指定输入信号必须占用毗邻的内存空间。如果输入是连续的,其他方法可以通过ssGetInputPortSignal来访问输入。 访问单个端口的输入信号 图11.显示了输入指针数组可以指向非毗邻的模块I/O向量。特定端口的输出信号形成一个连续的向量。所以,假定输入输出端口的宽度相同,正确的访问输入元素,写输出元素的方法是采用下面这样的代码: int_T element; int_T portWidth = ssGetInputPortWidth(S,inputPortIndex); InputRealPtrsType uPtrs = ssGetInputPortRealSignalPtrs(S,inputPortIndex); real_T *y = ssGetOutputPortSignal(S,outputPortIdx); for (element=0; element } 这里常犯的一个错误是用指针运算来访问输入信号。例如,将 real_T *u = *uPtrs; /* Incorrect */ 正好置于uPtrs的初始化下面,并且在循环内部采用: *y++ = *u++; /* Incorrect */ 这有可能导致非法访问内存,从而MEX文件和Simulink冲突,这取决于用户模型如何建立。 可以通过传递一个复制的信号到S-function的输入端口,来验证S-function是否正确的访问输入信号。如下图: 图12. 4.7 编写回调方法 5. 编写C++语言的S函数 C++ S-functions与创建C S-functions基本类似。本节只讨论不同部分。 5.1 代码形式 C++ S-functions的代码形式与C S-functions基本一致。区别在于要告诉C++编译器,要按照C惯例编译回调方法。这是由于Simulink仿真引擎假定回调方法都遵从C惯例。 为了使编译器按照C惯例编译回调方法,要将实现回调方法的C++代码包装在extern \"C\"声明中。C++版本的S-functions例子:matlabroot/simulink/src/sfun_counter_cpp.cpp,说明了如何利用extern \"C\"声明实现编译器生成回调方法。 /* File : sfun_counter_cpp.cpp * Abstract: * * Example of an C++ S-function which stores an C++ object in * the pointers vector PWork. * * Copyright 1990-2000 The MathWorks, Inc. * */ #include \"iostream.h\" class counter { double x; public: counter() { x = 0.0; } double output(void) { x = x + 1.0; return x; } }; #ifdef __cplusplus extern \"C\" { // use the C fcn-call standard for all functions #endif // defined within this scope #define S_FUNCTION_LEVEL 2 #define S_FUNCTION_NAME sfun_counter_cpp /* * Need to include simstruc.h for the definition of the SimStruct and * its associated macro definitions. */ #include \"simstruc.h\" /*====================* * S-function methods * *====================*/ /* Function: mdlInitializeSizes =============================================== * Abstract: * The sizes information is used by Simulink to determine the S-function * block's characteristics (number of inputs, outputs, states, etc.). */ static void mdlInitializeSizes(SimStruct *S) { /* See sfuntmpl_doc.c for more details on the macros below */ ssSetNumSFcnParams(S, 1); /* Number of expected parameters */ if (ssGetNumSFcnParams(S) != ssGetSFcnParamsCount(S)) { /* Return if number of expected != number of actual parameters */ return; } ssSetNumContStates(S, 0); ssSetNumDiscStates(S, 0); if (!ssSetNumInputPorts(S, 0)) return; if (!ssSetNumOutputPorts(S, 1)) return; ssSetOutputPortWidth(S, 0, 1); ssSetNumSampleTimes(S, 1); ssSetNumRWork(S, 0); ssSetNumIWork(S, 0); ssSetNumPWork(S, 1); // reserve element in the pointers vector ssSetNumModes(S, 0); // to store a C++ object ssSetNumNonsampledZCs(S, 0); ssSetOptions(S, 0); } /* Function: mdlInitializeSampleTimes ========================================= * Abstract: * This function is used to specify the sample time(s) for your * S-function. You must register the same number of sample times as * specified in ssSetNumSampleTimes. */ static void mdlInitializeSampleTimes(SimStruct *S) { ssSetSampleTime(S, 0, mxGetScalar(ssGetSFcnParam(S, 0))); ssSetOffsetTime(S, 0, 0.0); } #define MDL_START /* Change to #undef to remove function */ #if defined(MDL_START) /* Function: mdlStart ======================================================= * Abstract: * This function is called once at start of model execution. If you * have states that should be initialized once, this is the place * to do it. */ static void mdlStart(SimStruct *S) { ssGetPWork(S)[0] = (void *) new counter; // store new C++ object in the } // pointers vector #endif /* MDL_START */ /* Function: mdlOutputs ======================================================= * Abstract: * In this function, you compute the outputs of your S-function * block. Generally outputs are placed in the output vector, ssGetY(S). */ static void mdlOutputs(SimStruct *S, int_T tid) { counter *c = (counter *) ssGetPWork(S)[0]; // retrieve C++ object from real_T *y = ssGetOutputPortRealSignal(S,0); // the pointers vector and use y[0] = c->output(); // member functions of the } // object /* Function: mdlTerminate ===================================================== * Abstract: * In this function, you should perform any actions that are necessary * at the termination of a simulation. For example, if memory was * allocated in mdlStart, this is the place to free it. */ static void mdlTerminate(SimStruct *S) { counter *c = (counter *) ssGetPWork(S)[0]; // retrieve and destroy C++ delete c; // object in the termination } // function /*======================================================* * See sfuntmpl_doc.c for the optional S-function methods * *======================================================*/ /*=============================* * Required S-function trailer * *=============================*/ #ifdef MATLAB_MEX_FILE /* Is this file being compiled as a MEX-file? */ #include \"simulink.c\" /* MEX-file interface mechanism */ #else #include \"cg_sfun.h\" /* Code generation registration function */ #endif #ifdef __cplusplus } // end of extern \"C\" scope #endif 5.2 C++对象的持久化 用户的C++回调方法可能需要创建持久的C++对象。持久指回调方法运行结束后,对象仍然存在下去。例如,一个回调方法可能会用到前次调用所创建的对象,或者一个回调方法会用到其他回调方法创建的对象。按下列步骤可实现C++对象的持久化: 1. 创建一个工作向量,以在各次调用直接保持持久化对象的指针: static void mdlInitializeSizes(SimStruct *S) { ... ssSetNumPWork(S, 1); // reserve element in the pointers vector // to store a C++ object .. } 2. 为每个需要持续化的对象保存一个指针在工作向量里。 static void mdlStart(SimStruct *S) { ssGetPWork(S)[0] = (void *) new counter; // store new C++ object in the } // pointers vector 3. 在后面的调用中找到相应的指针来访问对象。 static void mdlOutputs(SimStruct *S, int_T tid) { counter *c = (counter *) ssGetPWork(S)[0]; // retrieve C++ object from real_T *y = ssGetOutputPortRealSignal(S,0); // the pointers vector and use y[0] = c->output(); // member functions of the } // object 4. 仿真结束时销毁对象 static void mdlTerminate(SimStruct *S) { counter *c = (counter *) ssGetPWork(S)[0]; // retrieve and destroy C++ delete c; // object in the termination } // function 5.3 编译C++ S-function 使用MATLAB的MEX命令建立C++ S-functions与建立C S-functions一致。 6. 封装子系统 7. 在Simulink中使用GUIs 8. 开发自定义库 因篇幅问题不能全部显示,请点此查看更多更全内容