1. Simulink can proper work with C or C++
Code generation from Simulink has often the target: Embedded Control written in C or [cpp}.
Often a text line code in a module ("Subsystem") is more well to write, read and understand as a complex wiring of simple FBlocks. For such approaches Simulink offers two languages with three approaches:
-
1. The Mathworks own MATLAB language, using MATLAB functions
-
2. C or C++, using the S-Function User Defined Function.
-
3. C or C++ using the C-Caller User Defined Function.
The point 3. is new since Release 2018-b. It is a simple possibility especially to include a complex C/ solution, calling dedicated Functions from this. It is not able to use for free assembled FBlocks, because the C-Caller FBlock has only one function call, no initialize function and not own local data. The C/ solution can be initialized on model level for the whole solution only. It is a special case.
The point 2. is present since many years with a lot of capability. It may be interesting to run the same code on Simulation on PC and on target. It assumes that the C/++ codes are the same on target and on PC and runs well on both systems. But exactly that is the approach of the emC concept (../../../emc/index.html).
Using point 1., the Mathworks own MATLAB language may be preferred from Mathworks, it is translated well to C by code generation. But some special cases or basic operations may be better mapped immediately to the target language.
2. S-Function writing and generating possibilities
There are three approaches in Simulink to write or generate S-Functions:
-
1. Written manually
-
2. Legacy code tool
-
3. S-Function Builder
Only the point 1. has full capabilities. The legacy code tool may be seen as complex (some MATLAB scripts) and though with not only all necessary features.
Based on the rules to manually written S-Functions a generator was created from Vishia, see GenSfnFromHeader.html. This document explains some capabilities and rules which are used.
3. mex and tlc
The compilation result of the C-Code for S-Functions are stored as Dynamic Linked Libraries called "mex" with the extension .mex
This files are included in the simulation process. They are used by the standard simulation mode. For any S-Function one mex file with the name of the S-Function is necessary.
The compilation to the mex library is done with a MATLAB call:
mex options sourcefiles
Details are explained in the Mathworks/Simulink help.
For the Accelarator and Rapid Accelerator simulation mode as for Code generation tlc-files are used. They contain rules how code should be generated for acceleration modes and for the target. The rules describes how the exisiting operations in C/++ of the S-Functions are called. tlc files have a special syntax, processed from the code generation of Simulink.
For the S-Function compilation the following files are necessary:
-
The original C/++ files to run.
-
A wrapper.c for each S-Function which is input for mex
-
The generatemex.m Matlab script
-
A proper tlc file if accelerator or code generation should be used.
All this files can written manually to use all capabilities, or even generated by the Vishia tool GenSfnFromHeader.html.
For the content of the wrapper.c for S-Functions see next chapter. For the content of tlc files see chapter #tlcContent. For the content of the generatemex MATLAB script see Simulink Help to mex, see examples, and see chapter #mexCallHints.
4. Frame to call S-Functions in Simulink
There are a set of functions, which are included in the mex compilation. Some of this functions are used not only for the simulation itself but also for the building of the project. Some of this functions are not used in the accelerator modes, because the code is called immediately in the pre-compiled model.
4.1. mdlInitializeSizes
static void mdlInitializeSizes(SimStruct *simstruct) {...}
This routine is firstly called if the Model is tested and a SFunction FBlock is found with the given type. The associated mex-File is loaded firstly. It contains the routine.
The mdlInitializeSizes()
writes all necessary information about the FBlock and its ports into the Simstruct
, to inform the simulation engine about the properties of the FBlock. Hence it is necessary already to check the model, number of in/outputs, the types, samling times etc.
For that some Simulink routine should be called, overview:
-
ssSetNumSFcnParams(simstruct, …)
,ssGetNumSFcnParams(simstruct)
: Number of parameter for the FBlock -
ssGetSFcnParamsCount(simstruct)
: check whether number of given parameter matches -
ssSetSFcnParamTunable(…)
: Information whether any parameter is tunable or not -
mdlCheckParameters(…)
: Firstly verifying parameters -
ssGetSFcnParam(simstruct, 0)
: returns the value of the given parameter in the SFunction dialog box of the FBlock. -
ssSetErrorStatus(simstruct,"text")
: Sets info for an error message on check model or run simulation. -
ssGetErrorStatus(simstruct)
: Check whether an error was reported. Note: since R2019a a new operationssGetLocalErrorStatus(simstruct)
is available for thread safety. -
ssSetNumDWork(simstruct, …)
,ssGetNumDWork(simstruct, …)
, : Sets some data pointer as property of this FBlock instance.ssSetDWorkDataType(simstruct, 0, SS_UINT32); ssSetDWorkUsageType(simstruct, 0, SS_DWORK_USED_AS_DWORK); ssSetDWorkName(simstruct, 0, "IDENTIFIER"); ssSetDWorkWidth(simstruct, 0, 1); ssSetDWorkComplexSignal(simstruct, 0, COMPLEX_NO);
-
…:set some properties of the DWork
-
ssSupportsMultipleExecInstances(simstruct, true)
: All FBlocks should be able to use as multiple instances. -
ssSetNumSampleTimes(simstruct, PORT_BASED_SAMPLE_TIMES)
or a given number: Define step times for execution. The port based selection associates step times to ports. It is useable if all step times can be associated to definitive port, independent of usage one or more step times. -
mexCallMATLABWithTrap(…)
: This is a possibility able to use in SFunction wrappers to get information from Matlab data, for example workspace variable. -
mxGetPr(…)
,mxGetDoubles(…)
: gets a value from paramter. Note:mxGetPr(…)
is deprecated since R2018a. -
ssSetNumInputPorts(simstruct,…)
,ssSetNumOutputPorts(simstruct,…)
,ssGetNumInputPorts(simstruct,…)
,ssGetNumOutputPorts(simstruct,…)
: defines the number of ports.ssSetInputPortDataType(simstruct, ixPort, ...); ssSetInputPortWidth(simstruct, ixPort, ...); ssSetInputPortComplexSignal(simstruct, ixPort, ...); ssSetInputPortSampleTime(simstruct, ixPort, ...) ssSetInputPortOffsetTime(simstruct, ixPort, ...); ssSetInputPortAcceptExprInRTW(simstruct, ixPort, ...); ssSetInputPortOverWritable(simstruct, ixPort, 1); ssSetInputPortDirectFeedThrough(simstruct, ixPort, ...); ssSetInputPortOptimOpts(simstruct, ixPort, SS_NOT_REUSABLE_AND_LOCAL); ssSetInputPortRequiredContiguous(simstruct, ixPort, 1);
-
…: Sets properties of the input ports. See Simulink help.
ssSetOutputPortDataType(simstruct, ixPort, ...); ssSetOutputPortWidth(simstruct, ixPort, ...); ssSetOutputPortComplexSignal(simstruct, ixPort, ...); ssSetOutputPortSampleTime(simstruct, ixPort, ...); ssSetOutputPortOffsetTime(simstruct, ixPort, ...);
-
…: adequate for output ports. Note: The SampleTime can be set only if
ssSetNumSampleTimes(simstruct, PORT_BASED_SAMPLE_TIMES)
is determined. -
ssSetModelReferenceNormalModeSupport(simstruct, …)
: Usage for referenced subsystems. -
ssSetSimStateCompliance(simstruct, …)
-
ssSetOptions( simstruct,…)
: Sets some options for the FBlock -
CheckDataTypes(simstruct)
: Checks all.
Using this operations the properties of a FBlock can be defined well for the instance of the FBlock in a model.
4.2. mdlSetIn/OutputPortSampleTime and …DataType
The operations
void mdlSetInputPortSampleTime(SimStruct *simstruct , int_T port, real_T sampleTime, real_T offsetTime) { ...}
void mdlSetOutputPortSampleTime(SimStruct *simstruct , int_T port, real_T sampleTime, real_T offsetTime) { ...}
void mdlSetInputPortDataType(SimStruct *simstruct, int_T port, DTypeId id) { ...}
void mdlSetOutputPortDataType(SimStruct *simstruct, int_T port, DTypeId id) { ...}
void mdlSetInputPortDimensionInfo(SimStruct *simstruct , int_T port, const DimsInfo_T *dimsInfo) { ... }
void mdlSetOutputPortDimensionInfo(SimStruct *simstruct , int_T port, const DimsInfo_T *dimsInfo) { ... }
void mdlSetInputPortComplexSignal(SimStruct *simstruct , int_T port, CSignal_T csig) {...}
void mdlSetOutputPortComplexSignal(SimStruct *simstruct , int_T port, CSignal_T csig) {...}
should be offer for an S-Function. They are called in the initialization phase from the model to set that step times which are determined by the wiring in the model.
4.3. mdlSetDefaultPortDataTypes, … DimensionInfo
void mdlSetDefaultPortDataTypes(SimStruct *simstruct) {...}
void mdlSetDefaultPortDimensionInfo(SimStruct *simstruct) {...}
void mdlSetDefaultPortComplexSignals(SimStruct *simstruct) {...}
This operation should be written to determine that port properties which are neither defined initially nor by wiring. All ports should be well defined. The definition should be done specifically.
4.4. mdlInitializeSampleTimes
static void mdlInitializeSampleTimes(SimStruct *simstruct) { ...}
This routine is called also on compilation or check the model after mdlInitializeSizes(…)
and should set
ssSetSampleTime(simstruct, index, sampleTimeValue) ; ssSetOffsetTime(simstruct, index, 0) ;
if block bases sample times are used, not using ssSetNumSampleTimes(simstruct, PORT_BASED_SAMPLE_TIMES);
in the mdlInitializeSizes(…)
routine.
If PORT_BASED_SAMPLE_TIMES
are used, this routine can be used to determine all INHERITED_SAMPLE_TIME
with a value from other ports to determine it. This may be a necessary approach to better determine the model. Elsewhere the sample times may be backward propagated maybe with differences between ideas and facts to be ascertained.
4.5. mdlStart, mdlInitializeConditions
static void mdlStart(SimStruct *simstruct) { ... }
This routine is called one time on start of the simulation. It can / should allocate data for the instances (multiple used or 'reentrant') and store the address in the prepared DWork pointer. It should do the initialization of the data.
static void mdlInitializeConditions(SimStruct *simstruct) { ... }
This operation is called after mdlStart
, but especially also in enabled subsystems with a restart execution. That is the intrinsic reason of this operation. For ordinary S-Functions the initialization should be done in mdlStart
. This routine should not existing. Whether or not it is existing, a #define MDL_INITIALIZE_CONDITIONS
should be set which is recognized by the mex compiler.
4.6. mdlUpdate, mdlOutputs
static void mdlUpdate(SimStruct *simstruct, int_T tid) { ... }
static void mdlOutputs(SimStruct *simstruct, int_T tid) { ... }
This both routines are the working routines of the S-Function. Both can evaluate input signals from the model. The difference between update and output is:
-
mdlUpdate
is designed for calculation of internal states from inputs. -
mdlOutputs
is designed for output the internal states.
But because output signals may depend immediately form inputs, mdlOutput
can also read the inputs, and mdlOutputs
can also firstly update the internal states, and then calculate the outputs.
5. Hints für mex call
TODO options for compilation etc.
6. Content of tlc files for code generation and accelerator
A tlc file for a SFunction contains the rule how code should be generated. For the own Simulink standard libs the code generation is organized internally, or even with specific tlc-files inside the Simulink tool.
The tlc file for an SFunction contains the code how an SFunction C code is called if it is used in a model.
6.1. General rules
%% This is comment /% This is block comment %/ %implements %%specific keywords starts with %, see next chapters %function NAME(ARG, ARG) void .... %%contains definitions for parts of code, or for general settings %endfunction %assign variableName = CONTENT %%A variable for generation, stores textual content. %<variableName> %%Generate text from content of the variable //This is text immediately generated %assign hthiz = LibBlockDWork(thiz_h, "", "", "0") %%fills a variable with tlc function uint32 hthiz = %<hthiz>; %%writes immediately text and variable content.
It means a tlc file contains free for specific parts of the code. The free text can be prepared using variable, using tlc function call to get content from the Simulink engin.
6.2. Common definitions
The name and type of the SFunction:
%implements step_PIDf_Ctrl_emC_SfH "C"
The next tlc functions defines general settings:
%% Function: BlockTypeSetup =============================================== %% %function BlockTypeSetup(block, system) void %% %% The Target Language must be C %if ::GenCPP==1 %<LibReportFatalError("This S-Function must be only used with the C Target Language")> %endif %%All headers which should be included into the target C file because of this SFunction: %<LibAddToCommonIncludes("possiblePath/myUserSfn.h")> %<LibAddToCommonIncludes("necessarySystemHeader.h")> %%For accelerator: All compilation units (C-files) which should be add to the executable. %% %<SLibAddToStaticSources("path/to/MyUserSfn.c")> %<SLibAddToStaticSources("path/to/AdditionalNecessary.c")> %% %endfunction
The code generator sorts headerfiles and C-files, if there are named more as one only one is included respectively compiled, of course.
%% Function: BlockInstanceSetup =========================================== %% %function BlockInstanceSetup(block, system) void %<LibBlockSetIsExpressionCompliant(block)> %endfunction
See Simulink help.
6.3. Parts of code
%% Function: Start ======================================================== %% %function Start(block, system) Output %%Note: Start and InitializeConditions are invoked one after another. %% %endfunction %% Function: InitializeConditions ======================================== %% %function InitializeConditions (block, system) Output //This code is generated into the
7. Best practice: How to generate and use code for target
The right image is originally from © Mathworks, from a Video. You need of course, a Simulink version which includes the coder capability (with adequate license).
7.1. Some options
You find the options in the "model configuration parameter" menue. There are some settings which needs experience. Recommended: You should always use "Reuseable function" for "Code interface packaging". "Reuseable" means, the data are referenced. Why? Only then extra C-functions with references to data are generated. This functions can be used both in the target and in a possible test environment on a PC C/++ project. Using of references for variable does not need a higher effort for access, because the relevant reference is often hold in a CPU-register for frequently access, and address calculations are executed parallel to machine code execution in modern processors.
"Pass root level IO as" determines how calling arguments are given for the stepping routines. It may be a proper decision to use structures, because only three references are used as arguments:
void Test_PIDctrl_step0( RT_MODEL_Test_PIDctrl_T *const Test_PIDctrl_M, ExtU_Test_PIDctrl_T *Test_PIDctrl_U, ExtY_Test_PIDctrl_T *Test_PIDctrl_Y) /* Sample time: [0.0001s, 0.0s] */ { ... }
The Test_PIDctrl_M
contains all instance data. Test_PIDctrl_U
is a structure reference for input data and …Y
for the outputs. If "Part of model data structure" is used, the stepping operations gets only one argument, but inside this three references are built. It is not optimized for calculation time if the three parts of data are separated anyway.
That are more options. It came from an older Simulink version. Currently options may be simular. Here the option "Single output/update function" is selected. With this choice any step time (sampling time) is presented with one C operation, named MyTargetModule_step0
, MyTargetModule_step1
etc.
The grt.tlc may be sufficient for some approaches. The ert.tlc has some more capabilities. As shown in the first image "MathWorks Code Generation Tool Chain" Mathworks prefers using ert for target microcontroller. Whether or not specific Hardware Support Packages are necessary, it is a decision of development support. For simple or self managed platforms the generated code stand alone may be sufficient. But you should be master the system environment of your controller.
Of course a controller works always with "Fixed-step", not with "Variable-step". The last one is only necessary for simulation models with complicated timing conditions. The Tstep is always the basic step time for controller algorithm in my models. Simulink knows the approach of rated calculation times, the slower step times are executed inside the fast step, but only any nth execution step. But to get free choice for the target, separated tasks should be used which are invoked in control of the target frame programming. But be carefully on concurrent accesses to data. Using rated calculation this problem is avoided.
7.2. Generation from one dedicated FBlock (Subsystem)
Usual the target is a part of the whole model, because Simulink is prior not a tool to writing code, but a tool for development code inclusively simulation.
It is a good praxis to have one Subsystem (sub module) in the top level model which presents the target with all its ports to the environment. It is possible to have more as one module (Subsystem) for components of the target, which are generated independently. It is possible to have more as one simulation model for this parts.
But in all case the relevant code is generated not from the whole model (top level) but from exactly one dedicates Subsystem.
The code generation is complete. It generates also a ert_main.c
routine and offers possible compilation to a ` and MyTargetModule.exe` and using the preferred compiler. But it may be recomended to separate
-
code generation as such
-
build the target
-
build test benches or a PC simulation.
As recommendation of Mathworks any target platform uses its own set of code generation options. In this way the code can be optimized to the target. But in opposite the ../../../emc/index.html approach offers the thinking of "platform independent C/C++ code". This approach is valid both for manual written C/++ modules as also for generated code. The different types which can be fine adjusted on the modelling level can also be used as general common types. The rule is "If you have a floating point processor then you should modelling with float (single) precission, not with double" or adequate using ,,int16,, if the processor is a poor one. The adaption of a specific arithmetic can be done by "INHERIT" data types in some library blocks. So the same generated code can be used for elaborately C/C++ code tests in a test bench as also in the target, as also compiled as specific S-Functions back using for Simulation or compiled as dll or FMI = "Functional Mockup Interface" modules also for another simulation tool. The behavior is tested at target language level in all cases.
If this approach is used, the frame to run the code is in responsibility to the engineering of target building. From the Simulink code generation only the generated code of the module itself should be used without changes.
To check and separate, it is recommended to write the generated code to a temparary location. Then compare and copy the used sources to the target build project and ignore (or zip and store) the other generated stuff. Intrinsically the generated "second sources" of the target module itself are concise and sufficient.
In my opinion the first proper destination for all build processes with some additional stuff is a RAM disk. From this location the necessary files can be firstly compared to check changes, especially for changed options of code generation, and then copy to the destination of using. This is often outside the Simulink environment. The generated code is the output, otherwise used (in the target build or test environment).
For some examples
It may