Matlab Simulink can compile and build simulation models into stadalone executables that do not require the presense of any runtime binaries from Matlab. However there are a few limitations, that I have had to overcome this week:
- There can be no algebraic loops in the model.
- Certain Matlab functions are not available.
Eliminating algebraic loops in the model is sometimes possible by reformulating the problem. This is also a good idea in any case, as the convergence of the algebraic loop solver of Simulink is not in any way perfect, possibly resulting in a termination of the simulation.
Below is presented a way to overcome the second restriction in relation to functions that are used for calling external libraries.
Matlab provides a few functions for calling functions in a dynamically loaded library file. (.dll in Windows, .so in Linux):
- loadlibrary(libname, hfile)
Loads a DLL file into Matlab.
- libname is the name of the library, wihtout the file name suffix (.dll/.so)
- hfile is the name of a C-language header file that declares the functions provided by the DLL.
- calllib(libname,funcname,arg1,...,argN)
Calls a function provided by the DLL.
- loadlibrary must be used before calling this function.
- libpointer(datatype, value)
Creates a pointer object that can be used for reading data that is written by a library function into a buffer provided as an argument.
These functions are very convenient, because they can automatically transform the Matlab data types into the correct forms expected by the library function. However, they are not available for use in a compiled Simulink model. When executed within Matlab, the Simulink model is always compiled into an S-function. To be able to access loadlibrary, callib and libpointer, they must be declared using coder.extrinsic(), which provides access to these functions.
However, when a Simulink model is compiled into a completely standalone executable, these functions are not available even with the coder.extrinsic() declaration. If the model contains a user-defined block, such as "MATLAB function", "Level-2 MATLAB S-Function" or "MATLAB System", which contains calls to library functions using the above-mentioned functions, building a standalone executable will fail with an error message: "The extrinsic function 'libpointer' is not available for standalone code generation. It must be eliminated for stand-alone code to be generated."
However, there is an alternative way of calling external library functions from compiled Matlab code:
- coder.ceval('FCN',X1,...XN)
This is a function that can only be used in MATLAB code that is compiled into C code. It is a kind of macro that simply places a call into the generated C code without making any checks on its arguments.
- coder.cinclude(headerfile)
Causes the addition of an #include statement in the generated C code.
Unfortunately the use of calllib and coder.ceval is mutually exclusive; calllib can only be used in non-compiled models and coder.ceval can only be used in compiled models. Fortunately this can be checked at "run-time" in the Matlab code, by checking the value of the variable coder.target. When code is compiled into an S-function within Matlab, this variable has the value 'sfun'. When the code is compiled into a standalone model executable, this variable has the value 'standalone'. This can be checked in the Matlab code using ordinary if statements, so it is possible to write custom blocks that work both in Matlab and as standalone executables.
use_coder = ~strcmp(coder.target, 'sfun');
if use_coder
coder.ceval('myfunction', arg1, arg2, arg3);
else
calllib('library', 'myfunction', arg1, arg2, arg3);
end
Unfortunately it is not so easy as this. To be able to use coder.ceval(), the exectable must be linked against the DLL using a .lib file, forming a fixed dependency between the executable and the DLL(s). This unfortunately prevents calls to identically named functions in several different DLLs.
A bigger difference between coder.ceval() and calllib() is that coder.ceval() makes absolutely not checks or conversions for the datatypes of the arguments. All arguments must already be of a suitable data type, and any variables that are to be passed using a pointer (or reference in C++) must be explicitly declared using coder.ref(). Otherwise the result will most probably be a hard crash of the standalone model.
Lets take as an example the following C language delaration for a function that returns the average of an array of floats and writes the standard deviation into a pointer value.
double stats(float *buf, double *sd_out, int bufsize);
While completely okay when used with callib(), the following call has a number of problems.
n = 10;
values = rand(1, n);
sd = 0.0;
// This will crash!!
mean = coder.ceval('stats', values, sd, n);
The vector 'values' would be passed into the function as double pointer instead of float, as required. 'sd' is passed by value, not as a pointer. 'n' would be passed as a double instead of an int.
Finally, without a prior declaration of 'mean', Matlab Coder has no way to infer the data type of the return value, resulting in an error during build.
Below is a corrected version of the call:
n = int32(10);
values = single(rand(1, n));
sd = 0.0;
mean = 0.0;
mean = coder.ceval('stats', coder.rref(values), coder.ref(sd), n);
To catch more potential typing errors, a call to coder.cinclude('libraryheader.h') should be located at some point, so that a declaration for the library function is added to the compiled C code.
It is quite natural for libraries to require calls to some kind of initialization functions, and a very natural place to locate these would be in an initialization function for the block that uses the library. Unfortunately, when building a standalone executable, these calls will be made before the Simulink Coder starts to build the executable, and the initialization calls will be completely omitted from the standalone model, resulting in possibly mysterious errors.
A possible solution is to move any such calls from the initialization functions into a conditional clause at the dependent code, which is only executed at the first call. An easy way to achieve it is to use a local persistent variable like this:
function y = fcn(xin, par1, par2)
persistent initialized;
if isempty(initialized)
initialized = true;
coder.ceval('myinitializationcall', par1, par2);
end
...
end
To gain access to necessary parameter values for the relocated initialization call, some additional parameters may need to be added to the code block. Below is an example of how to do this using the Model Explorer.
Let's say that we have a Matlab function block like this:
The block has a mask that defines a single integer input (Parameter1) that is used as an argument in an initialization call to a custom library:
The function itself is also implemented as a simple call to the same library:
function y = fcn(u)
coder.extrinsic('callib');
y = callib('mylibrary', 'myfun', u);
end
The coder.extrisic() call is necessary here, because the function is compiled into a MEX function by Simulink, even when the model is simulated in Matlab. The initialization call on the other hand is made by the Matlab interpreter, in which calllib() is always available.
Now if we want to be able to use the block in a standalone Simulink model executable, we need to replace the initialization calls in the mask with calls that are made in the function itself. For this purpose, we can add an additional argument for the function, which provides the value of the mask parameter. For this, we need to open the Model Explorer and select the MATLAB Function block (Right-click/Explore will do). With the "Add Data" command, we can insert the necessary parameter into the function. Note, that the scope of the added data item must be set to "Parameter", instead of the default scope of "Input".
Then we can add the equivalent call to coder.ceval() into the function and also add a conditional code block for the initialization call, like this:
function y = fcn(u, Parameter1)
coder.extrinsic('callib');
persistent init_called;
if strcmp(coder.target, 'sfun')
y = callib('mylibrary', 'myfun', u);
else
coder.cinclude('mylibheader.h');
if isempty(init_called)
init_called = true;
param = int32(Parameter1);
coder.ceval('myinit', param);
end
% make sure of the right types
y = 0.0;
tmp = double(u);
y = coder.ceval('myfun', tmp);
end
end
To be able to build the model, we also need to add 'mylibrary.lib' as an external library in the code generation configuration for the whole model (Code Generation/Custom Code/Libraries). After doing this, we are ready to build the model as blazingly fast and completely standalone executable with no additional runtime library dependencies.