Now we'll learn how to develop a custom S-function block using the C language. A basic knowledge of the C syntax is required.
The C language offers some advantages over MATLAB's scripting language:
C MEX S-functions are programmed in C/C++ and built with the mex
tool, which comes with MATLAB.
They are compiled as dynamically linked libraries on Windows platform, and as shared objects on UNIX/Linux platform, using the available compiler.
For a list of supported compilers, go to http://www.mathworks.com/support/compilers/current_release/.
Since mex
is only a frontend to a compiler and linker, we need to locate the available compilers in the system, and tell MATLAB which one to use.
This is easily done; just enter this command in the MATLAB main window:
mex -setup
The response is different based on the operating system in use.
MATLAB will list the available compiler configurations, asking you to choose one: the default compiler is gcc
, and will appear in the list if it is installed.
As soon as an option is selected, the file mexopts.sh
containing the compiler options will be written in your home folder (usually ~/.matlab/R2013a/
).
If you want to use C++ comments (beginning with //
) in your C code, you can replace the string -ansi
with -std=c99
in your ~/.matlab/R2013a/mexopts.sh
file. Keep in mind that the next time you run mex -setup
, your changes will be overwritten.
If MATLAB complains about your gcc version being unsupported, either install the package gcc-4.4
(or similar) if your distribution ships it, or run MATLAB inside a chroot
with a distribution using gcc 4.4 by default, like Debian Squeeze.
On Microsoft Windows systems, MATLAB will ask you for permission to automatically detect the compilers, and then it will list the available compilers, allowing you to select one.
Even if the lcc
compiler is shipped bundled with 32-bit MATLAB for Windows, the best choice is to install the SDK provided free of charge by Microsoft, containing the msvc
compiler. 64-bit MATLAB ships without a compiler, making it mandatory to install the SDK.
Like we already learned with MATLAB S-functions, the Simulink engine requires some callbacks to be defined, and will optionally look for other callbacks.
There are four mandatory callbacks: mdlInitializeSizes
, mdlInitializeSampleTimes
, mdlOutputs
, and mdlTerminate
.
The first two belong to the initialization phase; mdlOutputs
is called during the simulation loop and mdlTerminate
in the cleanup phase.
The complete call graph for the initialization phase is the following one (required routines are the darker ones):
The complete call graph for the simulation and cleanup phase is the following one:
Let's take a closer look at the required routines, and when the Simulink engine calls them.
This function is used to describe the inports, outports, and characteristics of their parameters, together with the number of continuous states (integrators/derivatives), discrete states (memories), and work vectors.
It is called during the initialization phase and during a model update (it is called as soon as you configure the S-function block parameters too).
This function must define the sampling time period at which the S-function block operates, together with the offset time (mostly useful if the output has to be calculated after the sampling done by other blocks).
Like mdlInitializeSizes
, it is called during the initialization phase and during a model update.
This function must implement the block's core logic, using saved states, work vectors, and input signals to calculate the block outputs.
It is called during the simulation phase, at each time step.
This function must have the cleanup logic to be executed in order to free the system resources that were allocated when the simulation started. A common usage is to close file descriptors and free the memory manually allocated with malloc()
.
This function is called once at the end of the simulation, in the cleanup phase.
These routines allow the developer to have a more fine-grained control on the S-function behavior.
Each optional routine is ignored by the Simulink engine, unless the corresponding preprocessor identifier is properly defined.
The most useful optional routines are mdlStart
, mdlInitializeConditions
, and mdlUpdate
. Their main purpose, like the optional callbacks we saw while developing MATLAB S-functions, is to initialize and update block states and work vectors.
This function is called once, right when the simulation starts allowing you to allocate the necessary system resources. The corresponding preprocessor identifier is MDL_START
.
This function is called during the simulation phase, at the beginning of each time step, allowing you to reinitialize the variables stored in block states and work vectors before mdlOutputs
is executed. If the S-function block is placed into an enabled subsystem, this function will be called each time the subsystem is re-enabled. The corresponding preprocessor identifier is MDL_INITIALIZE_CONDITIONS
.
This function, called during the simulation phase right after the execution of mdlOutputs
, serves the purpose of updating block states and work vectors.
It is called only if the block has discrete states or doesn't have direct feedthrough (otherwise there is obviously no state to update, and the Simulink engine avoids making this extra call). The corresponding preprocessor identifier is MDL_UPDATE
.
Like its MATLAB S-function counterpart, this is the main work vector. It is able to store every datatype supported by Simulink.
Its usage is similar: the work vector characteristics, which were defined in the MATLAB S-function callback PostPropagationSetup
, now have to be defined in mdlInitializeSizes
.
Then, using mdlStart
, initialize the values of every DWork
vector and allocate the needed system resources.
Optionally, we can use mdlInitializeConditions
to reinitialize the DWork
vector values at the beginning of each time step.
The DWork
vectors are then used by mdlOutputs
to calculate the S-function block outputs and updated by mdlUpdate
.
Finally, in mdlTerminate
, we get the allocated system resources from the DWork
vector(s) and free them up.
There is more though. C MEX S-functions can use the so-called elementary work vectors built for the most common use cases.
The elementary vectors that can store specific data are:
Their usage is almost identical to the usage of DWork
vectors described in the previous paragraphs, but with a big difference, they must be unique. This means that:
mdlInitializeSizes
(for example, it's not possible to have two RWork
vectors in the same code, while it's possible to use the RWork
vector together with the IWork
vector)Now that we've learned the theory behind C MEX S-functions, let's implement the same file-based source and sink blocks in C.
As we saw earlier, we need one DWork
vector to store the previous block output.
Open a new file in the C editor of your preference, and call it filesource_sfun.c
.
The first thing to do is to define the S-function name and level, which will be used by the Simulink engine:
#define S_FUNCTION_NAME filesource_sfun /* mandatory */ #define S_FUNCTION_LEVEL 2 /* mandatory */
The S-function name is the filename without the extension. The S-function level, since Simulink release 2.2, must be always set to 2.
We may now include the required headers:
#include <stdio.h> /* file manipulation functions */ #include "simstruc.h" /* mandatory */ #include "matrix.h" /* helper functions */
The first header contains the system's standard I/O functions (including the file operations we'll need).
The simstruc.h
header contains the C equivalent of MATLAB's Simulink.MSFcnRunTimeBlock
object: a structure (SimStruct
) representing the whole block; therefore, it's mandatory to include it.
Finally, the matrix.h
header contains some convenient functions to operate with strings.
We must define the block characteristics: parameters, states, ports, sample times, and work vectors.
The function begins with the following snippet of code:
static void mdlInitializeSizes(SimStruct *S) {
Every S-function routine requires SimStruct *S
as the first argument. This is the pointer to the Simulink block structure, maintained by the Simulink engine.
Let's define the parameters. We're going to have two parameters passed to the S-function: the first parameter is the file path, and the second is the initial output. This is accomplished by the following code:
/* set number of S-function parameters */ ssSetNumSFcnParams(S, 2); if (ssGetNumSFcnParams(S) != ssGetSFcnParamsCount(S)) return; /* Parameter mismatch will be reported by Simulink */ /* set parameters as non-tunable during simulation */ ssSetSFcnParamTunable(S, 0, SS_PRM_NOT_TUNABLE); ssSetSFcnParamTunable(S, 1, SS_PRM_NOT_TUNABLE);
The function ssSetNumSFcnParams
allows us to declare that the block accepts two parameters. The function ssGetNumSFcnParams
will return 2, and the function ssGetSFcnParamsCount
will return the actual number of defined parameters. If the latter isn't 2, Simulink will report an error while editing the S-function block.
Each parameter is set as SS_PRM_NOT_TUNABLE
with the ssSetSFcnParamTunable
function, where the second argument is the parameter index. Since we have two parameters, the function is called twice using the indexes 0 and 1. This disables the possibility of changing the parameters while the simulation is running.
Regarding the states, we don't need continuous or discrete states (we're using the DWork
vector instead to save the previous output value). This code instructs Simulink about it:
ssSetNumContStates(S, 0); /* no continuous states (integrator/derivator) */ ssSetNumDiscStates(S, 0); /* no discrete states (unit delay/memory) */
Ports are easy: we only need one output port, and no input port.
if (!ssSetNumInputPorts(S, 0)) /* set inports number to 0 */ return; if (!ssSetNumOutputPorts(S, 1)) /* set outports number to 1 */ return; ssSetOutputPortWidth(S, 0, 1); /* set outport width (scalar) */ ssSetOutputPortDataType(S, 0, SS_DOUBLE); /* set outport type */
The functions ssSetOutputPortWidth
and ssSetOutputPortDataType
need the zero-based port index as second parameter. The former declares that the output signal is a scalar (width equal to 1), the latter that it is a real number (of datatype double
).
We don't need a different sample time than the one used in the simulation, so we use the following code line:
ssSetNumSampleTimes(S, 1); /* one block-based sample time */
Setting the number of sample times to 1 with ssSetNumSampleTimes
causes the block to use the global sample time only.
Finally, we need to declare a work vector that will hold the initial value and previous output. We could use the easier elementary RWork
vector, but we're interested in learning the more generic DWork
vector. The following snippet demonstrates its usage:
ssSetNumDWork(S, 1); /* needed vectors: 1 */ ssSetDWorkWidth(S, 0, 1); /* the vector 0 has only 1 element */ ssSetDWorkDataType(S, 0, SS_DOUBLE); /* and will hold a real signal */
The three functions ssSetNumDWork
, ssSetDWorkWidth
, and ssSetDWorkDataType
allow the Simulink engine to know how much memory should be reserved. S-functions can contain more than one DWork
vector; their number is set with ssSetNumDWork
. The zero-based index of the vector is then used by every other function accessing the vector itself as the second argument.
One last thing: let's put a debug statement, before the closing bracket, to ensure everything went well:
#ifndef NDEBUG printf("mdlInitializeSizes called "); #endif }
That's it; this routine now contains everything the Simulink engine needs to know in order to reserve the right amount of memory, and the debug statement will allow us to see when it is called by the Simulink engine.
We want to use the simulation sample time without any offset. In other words, we want this S-function block to be executed at the beginning of each time step.
The C implementation is very simple:
static void mdlInitializeSampleTimes(SimStruct *S) { ssSetSampleTime(S, 0, CONTINUOUS_SAMPLE_TIME); /* sample always */ ssSetOffsetTime(S, 0, 0.0); /* apply no offset */ #ifndef NDEBUG printf("mdlInitializeSampleTimes called "); #endif }
The ssSetSampleTime
and ssSetOffsetTime
functions use the zero-based sample time index as second argument. Having declared (in mdlInitializeSizes
) that we're using only one sample time, the index is 0
.
Since this is a source block, we define a continuous sample time. Other options are INHERITED_SAMPLE_TIME
, VARIABLE_SAMPLE_TIME
, and discrete sample time (any real number greater than 0).
We'll use this optional routine to retrieve the default output parameter and store it into the DWork
vector. Remember that we must define MDL_START
, or Simulink will ignore the function.
The implementation is:
#define MDL_START /* Change to #undef to remove function. */ #ifdef MDL_START static void mdlStart(SimStruct *S) { real_T *yPrev = NULL; /* pointer to DWork element */real_T yInit = 0.0; /* tmp variable to read parameter */ yPrev = (real_T*) ssGetDWork(S, 0); yInit = mxGetScalar(ssGetSFcnParam(S, 1)); yPrev[0] = yInit; #ifndef NDEBUGprintf("mdlStart: got initial output %f ", yInit);#endif } #endif /* MDL_START */
The function ssGetDWork
is used to retrieve the first DWork
vector pointer. The index is zero based.
The function ssGetSFcnParam
retrieves a pointer to the second parameter (the initial output), converted to a number with mxGetScalar
. That number is saved in the first (and only) element of the DWork
vector, accessed as an array. Remember that the number of elements was declared in mdlInitializeSizes
.
This routine is where the magic happens. We have to get the last line of the source file, parse the number, and output the result. If any error occurs, the recovery strategy is to output the last valid value taken from the DWork
vector (used as a memory).
The routine definition is:
static void mdlOutputs(SimStruct *S, int_T tid) { FILE *fd = NULL; char_T path[FILEPATH_LEN]; char_T line[FILELINE_LEN]; char_T *lineEnd; real_T *y = NULL; real_T *yPrev = NULL; real_T yOut; UNUSED_ARG(tid); /* get the output pointer */ y = ssGetOutputPortRealSignal(S,0); /* get the previous output pointer from DWork */ yPrev = (real_T*) ssGetDWork(S,0); /* get the filename for reading */ memset(path, 0, FILEPATH_LEN); mxGetString(ssGetSFcnParam(S,0), path, FILEPATH_LEN); /* open file for reading */ fd = fopen(path, "r"); if(fd == NULL) { printf("Error: source file %s not readable ", path); y[0] = yPrev[0]; return; } /* get one line */ memset(line, 0, FILELINE_LEN); if (fgets(line, FILELINE_LEN, fd) == NULL) /* error or empty file */ { printf("Error: source file %s empty ", path); y[0] = yPrev[0]; fclose(fd); return; } /* convert from string to double */ lineEnd = NULL; yOut = strtod(line, &lineEnd); if(lineEnd == line) { printf("Error: string to double conversion failed"); y[0] = yPrev[0]; fclose(fd); return; } /* if we got here everything went good */ y[0] = yOut; /* set the output */ fclose(fd); /* close the file */ #ifndef NDEBUG printf("%s value: %f ", path, yOut); #endif }
We now have a second argument: tid
. Blocks with more than one sample time (called multirate blocks) are run in different tasks by the Simulink engine, and tid
identifies the current task being run.
Since this is not our case (we have only one sample time), we're using the UNUSED_ARG
macro to avoid compiler warnings. The macro just casts the variable to void
.
We've already learned how to get the DWork
vectors with ssGetDWork
. The ssGetOutputPortRealSignal
function (accepting the zero-based port index as second argument) returns the pointer to the output port.
The first block parameter (the file name) is retrieved with ssGetSFcnParam
, and converted to a string by mxGetString
.
The following logic attempts to open the file, read a line from it, parse a number from the line, and output the number. If the file doesn't exist, or is empty, or there is a parsing error, the previous output is used.
We need to save the new output into the work vector DWork
:
#define MDL_UPDATE #ifdef MDL_UPDATE static void mdlUpdate(SimStruct *S, int_T tid){ real_T *y = NULL; real_T *yPrev = NULL; UNUSED_ARG(tid); /* get the output pointer */ y = ssGetOutputPortRealSignal(S,0); /* get the previous output pointer from DWork */ yPrev = (real_T*) ssGetDWork(S,0); /* update the previous output */ yPrev[0] = y[0]; #ifndef NDEBUG printf("mdlUpdate: saved output %f ", yPrev[0]); #endif } #endif /* MDL_UPDATE */
Being an optional function, it's necessary to define MDL_UPDATE
. The rest of the code is nothing we haven't seen before; both the output signal and the work vector are mono-dimensional arrays, with only one element as specified in mdlInitializeSizes
.
This is where we perform the cleanup, closing every system resource that has been allocated. But we don't have any.
So the C implementation is the following:
static void mdlTerminate(SimStruct *S) { UNUSED_ARG(S); #ifndef NDEBUG printf("mdlTerminate successfully called "); #endif }
This code is just an empty function that will let us know when it has been called while we're debugging. Avoid compiler warnings about unused parameters, of course.
One little final step has to be performed, since each S-function must have this trailer:
/*=============================* * Required S-function trailer * *=============================*/ #ifdef MATLAB_MEX_FILE /* Is this file being compiled as aMEX-file?*/ #include "simulink.c" /* MEX-file interface mechanism */ #else #include "cg_sfun.h" /* Code generation registration function */ #endif
We don't need to define MATLAB_MEX_FILE
, since it is already defined by the mex
tool, in order to include the necessary functions to build a MEX executable loadable by the Simulink engine.
Since automatic code generation is outside the scope of this book (and requires Simulink Coder, formerly Real-Time Workshop, which isn't cheap either), we'll ignore the cg_sfun.h
header meaning.
We're done now, our first S-function is ready to be compiled.
It's sufficient to type the following command into MATLAB's Command Window:
mex -v -g filesource_sfun.c
This will invoke the mex
utility in verbose (-v
) mode and with debugging (-g
) enabled. This way we'll be able to read compiler messages and the NDEBUG
preprocessor flag will be undefined.
If the compilation is successful, your folder will contain a new file:
filesource_sfun.mexw32
filesource_sfun.mexw64
filesource_sfun.mexa64
That file is the library Simulink will load.
This S-function will get a real, scalar signal from its input port and write it to a file. The file name is the only S-function parameter, and the file format is the same as described earlier.
Being very similar to the filesource S-function, the source code will not be commented in detail. It's available in the code bundle provided with this book.
The reader is strongly encouraged to write and compile this S-function as an exercise following the guidelines provided below, instead of using the provided code.
We need to make a copy of the filesource_sfun.c
file and rename it to filesink_sfun.c
. Remember to change the mandatory definitions accordingly.
To read the input signal value, the best method is to use the ssGetInputPortRealSignal
function. However, this requires the input signal to be set as contiguous with ssSetInputPortRequiredContiguous
.
The sample time can be inherited from the driving block now.
An important thing that needs special attention is that by default the file writes are buffered by the operating system. You should turn off buffering by using setbuf(fd, NULL);
right after opening the file.
The command to compile the S-function is the same as we've seen before:
mex -v -g filesink_sfun.c
The compilation will produce the filesink_sfun
MEX file for your platform.
Let's open the model we used to do a preliminary test on MATLAB S-functions again and save it as sfun_test.slx
.
We only need to replace Level 2 MATLAB S-function blocks with S-function blocks. Those blocks will use the newly developed filesource_sfun
and filesink_sfun
as S-function names. Their parameters, as well as the model configuration, will be the same as before.
As soon as we're done editing and running a model update, a quick look at the MATLAB's Command Window will reveal, thanks to the debug statements we put in the code, that the mdlInitializeSizes
routine has been called by Simulink in order to draw the new blocks' input and output ports.
Running the simulation and opening the Scope block should show the same results we had while testing MATLAB S-functions: both sine waves should coincide, and the MATLAB's Command Window will show the debug messages. Notice that mdlStart
is called only once.
We can recompile them without the debugging macros:
mex -v filesource_sfun.c mex -v filesink_sfun.c
Let's run the application together with the cruise controller again, this time using the new S-functions.
Open the cruise_control_external_msfun.slx
model we developed earlier, save a copy as cruise_control_external_sfun.slx
, and replace every Level 2 MATLAB S-function blocks with S-function blocks, but keep the same parameters (except the S-function name).
Having the cruise_control_external.mat
workspace loaded, the model configuration parameters, and the application configured as before, press the application's Run button, and then run the simulation.
Everything should behave as it did before:
18.191.237.201