C-Mex S-Function编写入门
# C-Mex S-Function的调用顺序
参考官网文档Simulink引擎如何与C-S函数交互 (opens new window),文中介绍了C-Mex S-Function的调用顺序如下图:
大致可总结为:
- 设置输入、输出、状态和参数个数(
mdlInitializeSizes
); - 初始化采样时间(
mdlInitializeSampleTimes
); - 仿真开始前调用一次,在此可以填入相关准备工作(
mdlStart
); - 初始化状态变量(
mdlInitializeConditions
); - 进入仿真循环
- 当参数改变时则调用
mdlProcessParameters
函数; - 当采样时间可变时,调用
mdlGetTimeOfNextVarHit
; - 计算输出(
mdlOutputs
); - 更新离散状态(
mdlUpdate
); - 当系统含有连续状态时调用该子循环,计算连续状态的导数和输出
mdlDerivatives
;mdlOutputs
;mdlDerivatives
;
- 当检测到过零时调用该子循环
mdlOutputs
;mdlZeroCrossings
;
- 当参数改变时则调用
- 当仿真结束时调用
mdlTerminate
函数做必要的清理工作。
# C-Mex S-Function文件的主要组成
# 文件表头说明
根据C-Mex S-Function的编写规则,在每个C-Mex S-Function文件头部包含如下宏定义和头文件引用声明:
// 说明函数的等级
#define S_FUNCTION_LEVEL 2
// 定义S-Function的名称
#define S_FUNCTION_NAME sfun_name
// 包含 SimStruct 类型
#include "simstruc.h"
在此之后可根据需求引用其他头文件,推荐对固定参数如输入输出个数进行常量定义,便于后续对文件的修改和管理,提高可读性与可维护性。
在完成文件的声明后,便可以开始对相关函数进行编写,参考文档可移步至配置C/C++ S-Function功能 (opens new window),下面按照仿真流程顺序对常见的函数进行简要说明。
# 初始化函数 mdlInitializeSizes
该函数是Simulink调用的第一个函数,此函数执行以下任务:
- 使用
ssSetNumSFcnParams
函数指定此S-Function支持的参数个数,当参数在仿真期间无法更改时,使用ssSetSFcnParamTunable(S, paramIdx, 0)
; - 使用
ssSetNumContStates
和ssSetNumDiscStates
指定函数具有的状态个数; - 配置输入端口,包括:
- 使用
ssSetNumInputPorts
指定S-Function具有的输入端口数; - 使用
ssSetInputPortDimensionInfo
指定输入端口的维度; - 使用
ssSetInputPortDirectFeedThrough
指定是否具有直接馈通;
- 使用
- 配置输出端口,包括:
- 使用
ssSetNumOutputPorts
指定S-Function具有的输出端口数; - 使用
ssSetOutputPortWidth
指定输出端口的维度;
- 使用
- 使用
ssSetNumSampleTimes
配置采样次数,(采样时间将会在mdlInitializeSampleTimes
函数中进行配置); - 设置模块工作向量大小,包括:
- ssSetNumRWork (opens new window),设置浮点指针型向量个数;
- ssSetNumIWork (opens new window),设置整型向量个数;
- ssSetNumPWork (opens new window),设置模块的指针型向量个数;
- ssSetNumModes (opens new window),设置mode vector(?不太懂这个)的个数;
- ssSetNumNonsampledZCs (opens new window),设置模块检测采样点之间发生过零的状态数;
- 使用
ssSetOptions
设置此模块的选项。所有选项都形如SS_OPTION_<NAME>
,使用按位运算符进行多个选项的设置,有关每个功能的信息,请参阅配置C/C++ S-Function功能 (opens new window)。
给出初始化函数的示例代码:
static void mdlInitializeSizes(SimStruct *S)
{
int_T nInputPorts = 1; /* 输入端口数 */
int_T nOutputPorts = 1; /* 输出端口数 */
// 设置输入端口维度信息
DECL_AND_INIT_DIMSINFO(nInputDims);
int_T indims[2];
nInputDims.numDims = 2;
indims[0] = MEASURE_DIM;
indims[1] = 1;
nInputDims.dims = indims;
nInputDims.width = MEASURE_DIM;
// 设置输出端口维度信息
DECL_AND_INIT_DIMSINFO(nOutputDims);
int_T outdims[2];
nOutputDims.numDims = 2;
outdims[0] = STATE_DIM;
outdims[1] = 1;
nOutputDims.dims = outdims;
nOutputDims.width = STATE_DIM;
int_T needsInput = 1; /* 是否具有直接馈通 */
int_T inputPortIdx = 0;
int_T outputPortIdx = 0;
ssSetNumSFcnParams(S, 0); /* 期望的额外参数个数 */
if (ssGetNumSFcnParams(S) != ssGetSFcnParamsCount(S)) {
/* 获取的可选参数与期望的个数不匹配则退出 */
return;
}
ssSetNumContStates(S, 0); /* 连续状态个数 */
ssSetNumDiscStates(S, 0); /* 离散状态个数 */
// 配置 I/O
if (!ssSetNumInputPorts(S, nInputPorts)) return;
if(!ssSetInputPortDimensionInfo(S, inputPortIdx, &nInputDims)) return;
ssSetInputPortDirectFeedThrough(S, inputPortIdx, needsInput);
if (!ssSetNumOutputPorts(S, nOutputPorts)) return;
if(!ssSetOutputPortDimensionInfo(S,outputPortIdx, &nOutputDims)) return;
ssSetNumSampleTimes(S, 1); /* 配置采样次数 */
// 设置工作向量
ssSetNumRWork(S, 0); /* real vector */
ssSetNumIWork(S, 0); /* integer vector */
ssSetNumPWork(S, 1); /* pointer vector */
ssSetNumModes(S, 0); /* mode vector */
ssSetNumNonsampledZCs(S, 0); /* zero crossings */
// ssSetOperatingPointCompliance(S, USE_CUSTOM_OPERATING_POINT);
/* 将此S-Function设置为运行时线程安全,以便多核执行 */
ssSetRuntimeThreadSafetyCompliance(S, RUNTIME_THREAD_SAFETY_COMPLIANCE_TRUE);
ssSetOptions(S,
SS_OPTION_WORKS_WITH_CODE_REUSE |
SS_OPTION_EXCEPTION_FREE_CODE |
SS_OPTION_DISALLOW_CONSTANT_SAMPLE_TIME);
}
# 初始化采样时间 mdlInitializeSampleTimes
static void mdlInitializeSampleTimes(SimStruct *S)
{
ssSetSampleTime(S, 0, 2);
ssSetOffsetTime(S, 0, 0.0);
ssSetModelReferenceSampleTimeDefaultInheritance(S);
}
通常采样时间可以通过设置额外参数作为输入进行设置,如果在 mdlInitializeSizes
中设置了额外参数,则可以获取额外参数进行相关设置:
// ssSetSampleTime(S, 0, 2);
ssSetSampleTime(S, 0, mxGetPr(ssGetSFcnParam(S,0))[0]);
# 开始函数 mdlStart
此函数在模型开始阶段调用一次,如果有应该只初始化一次的状态,应当在这里进行。
#define MDL_START
static void mdlStart(SimStruct *S)
{
// 在PWork中储存指针对象
MyEKFilter *filter = new MyEKFilter(STATE_DIM, U_DIM, MEASURE_DIM);
ssGetPWork(S)[0] = filter;
}
注意,此处初始化时申请了内存,请务必在模型仿真结束后,使用
mdlTerminate
函数进行内存释放。
# 初始化状态变量 mdlInitializeConditions
#define MDL_INITIALIZE_CONDITIONS /*Change to #undef to remove */
#if defined(MDL_INITIALIZE_CONDITIONS)
static void mdlInitializeConditions(SimStruct *S)
{
// 从PWork中拿到储存的指针变量
MyEKFilter *filter = static_cast<MyEKFilter *>(ssGetPWork(S)[0]);
auto P0 = kalmans::Matrix<double>::Diag({
1, 0.1, 0.01,
1, 0.1, 0.01,
1, 0.1, 0.01
});
kalmans::Matrix<double> x(STATE_DIM, 1);
x(0) = 1500e3;
x(3) = 10e3;
x(7) = -250;
filter->init(x, P0);
}
#endif /* MDL_INITIALIZE_CONDITIONS */
# 输出函数 mdlOutputs
通过 ssGetInputPortRealSignalPtrs
和 ssGetOutputPortRealSignal
获取I/O的数据地址,设置输出的值实现模块输出。需要注意的是,这里用到了输入数据u时应当在初始化时设置存在直接馈通。
static void mdlOutputs(SimStruct *S, int_T tid)
{
MyEKFilter *filter = static_cast<MyEKFilter *>(ssGetPWork(S)[0]);
// 获取 I/O 的数据地址
InputRealPtrsType u = ssGetInputPortRealSignalPtrs(S,0);
real_T *y = ssGetOutputPortRealSignal(S, 0);
kalmans::Matrix<double> z(MEASURE_DIM, 1);
kalmans::Matrix<double> u_(STATE_DIM, 1);
kalmans::Matrix<double> filter_value(1, STATE_DIM);
for (int j = 0; j < MEASURE_DIM; ++j)
z(j) = *u[j];
filter->step(u_, z);
filter_value.assign(filter->get_state(), 0);
for (int j = 0; j < STATE_DIM; ++j)
y[j] = filter_value(j);
}
# 更新函数 mdlUpdate
在此进行更新离散状态量的操作,使用 ssGetRealDiscStates
获取离散状态变量。
#define MDL_UPDATE
static void mdlUpdate(SimStruct *S, int_T tid)
{
real_T *x = ssGetRealDiscStates(S);
/* 在此添加更新相关代码 */
}
# 仿真结束函数 mdlTerminate
执行任何结束仿真后需要处理的任务。
static void mdlTerminate(SimStruct *S)
{
// 获取指针变量并进行释放
MyEKFilter *filter = static_cast<MyEKFilter *>(ssGetPWork(S)[0]);
delete filter;
}
# 文件结尾说明
在函数文件的结尾,必须添加如下代码,通常不需要对其进行修改。
// 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
MATLAB_MEX_FILE
这个预处理宏会在编译期进行定义,表示当前S-Function是否正在被编译为mex文件。
# 模块化编程与链接静态库
笔者通常习惯于使用C/C++语言编写S-Function时将核心代码打包为一个静态库,然后根据C-Mex S-Function的编写方式链接库函数并实现具体模块。这样做便于程序的调试与管理,便于已有代码的移植,在项目结构方面也更加清楚。
在MATLAB中编译并链接一个库比较简单,通常将源文件与库文件放在同一目录下,使用命令即可:
mex sourcecode.cpp library1.lib library2.lib
当需要设置C++语言标准,需要根据编译器类型设置相应的编译器选项,具体编译器参数可查阅相关说明文档,使用 mex
追加编译器选项需要进行如下设置:
mex COMPFLAGS='$COMPFLAGS /std:c++17' sourcecode.cpp library1.lib library2.lib
其中需要确定变量名称:
- 要使用 MinGW®、macOS 和 Linux® 编译器编译 C++ 代码,请使用 CXXFLAGS。
- 要使用 MinGW、macOS 和 Linux 编译器编译 C 代码,请使用 CFLAGS。
- 对于 Microsoft® Visual Studio® 编译器,请使用 COMPFLAGS。
使用 Visual Studio 编译 MEX 文件时,请指定 C++17 标准。