Going further – C MEX S-functions

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:

  • It is the most-used language to develop (hard) real-time systems
  • It is one of the most popular languages, if not the most popular
  • C executables offer unparalleled performance with respect to MATLAB scripts
  • C S-functions can be developed without having MATLAB installed (but you'd still need the external headers and libraries)
  • Legacy C code can be easily ported to S-function blocks and used in Simulink
  • C++ compilers can be used, giving access to some powerful C++ frameworks like Qt

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.

Tip

For a list of supported compilers, go to http://www.mathworks.com/support/compilers/current_release/.

Setting up the mex tool

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.

UNIX-like systems (GNU/Linux in particular)

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/).

Tip

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.

Microsoft Windows systems

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.

How C MEX S-functions work

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):

How C MEX S-functions work

The complete call graph for the simulation and cleanup phase is the following one:

How C MEX S-functions work

Tip

Each routine is extensively documented in the Documentation center: see Simulink | Block Creation | Host-Specific Code | S-Function Basics | Concepts, subsections S-Function Callback Methods and S-Function SimStruct Functions.

The required callbacks

Let's take a closer look at the required routines, and when the Simulink engine calls them.

mdlInitializeSizes

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).

mdlInitializeSampleTimes

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.

mdlOutputs

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.

mdlTerminate

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.

The most useful optional callbacks

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.

mdlStart

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.

mdlInitializeConditions

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.

mdlUpdate

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.

Tip

If you need to initialize persistent variables only once, use mdlStart.

If, on the other hand, you need to initialize persistent variables at each simulation time step, use mdlInitializeConditions.

The DWork vector

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.

Note

Remember that DWork vectors are managed by the Simulink engine: don't attempt to free() their memory inside mdlTerminate.

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 work vectors

The elementary vectors that can store specific data are:

  • The IWork vector can store integer data
  • The RWork vector can store floating-point (real) data
  • The PWork vector can store pointers to persistent data structures (such as file handlers, allocated memory, and so on)
  • The Mode vector can store integer flags modifying the behavior of the S-function

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:

  • The S-function can use only one elementary vector in 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)
  • It's not possible to customize them (for example, assigning them a name)

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.

The filesource S-function

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 beginning – headers and includes

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.

Block properties and memory usage – mdlInitializeSizes

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.

Timings – mdlInitializeSampleTimes

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).

Initial tasks – mdlStart

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.

Core logic – mdlOutputs

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.

Note

Remember that ports (and signals) are arrays, even if they're scalar (their width being 1).

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.

Update memories – mdlUpdate

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.

Cleanup – mdlTerminate

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.

The happy ending

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.

Compiling the S-function

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:

  • On 32-bit Microsoft Windows systems: filesource_sfun.mexw32
  • On 64-bit Microsoft Windows systems: filesource_sfun.mexw64
  • On UNIX-like systems (only 64-bit supported): filesource_sfun.mexa64

That file is the library Simulink will load.

Exercise – the filesink S-function

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.

A quick test

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

Go for another ride

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:

Go for another ride
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.2.225