7. Integrating external code

7.1. Background

QGen supports interfacing generated code with manually written or code generated from another model by following means:

  • Using custom data types defined in external modules

  • Importing variables and constants defined in external modules

  • Calling C or Ada code using S-Function block

7.2. Using custom data types defined in external modules

Procedures for handling custom datatypes is described in Custom Data Types

7.3. Importing variables and constants defined in external modules

Variable or constant defined in an external module can be imported using Simulink Signal or Simulink Parameter objects. Signal and Parameter objects with StorageClass ‘ImportedExtern’ or StorageClass ‘Custom’ and CustomStorageClass ‘ImportFromFile’ are treated as defined in an external module. In the case of StorageClass ‘Custom’, there is the possibility to indicate a specification file containing the symbol declaration. If a specification file is not defined, QGen generates an import declaration.

7.3.1. Imported variable with no explicit declaration

When importing a variable with StorageClass ‘ImportedExtern’ or in case the StorageClass is ‘Custom’ and the HeaderFile parameter is not defined, QGen assumes that the variable was defined in an external module with C convention. Variable declaration is derived from the type specification in Matlab.

% SIGNAL IntVarDefault %
% External varible, no header specified
% Assumed to be defined in C by default
IntVarDefault = mpt.Signal;
IntVarDefault.DataType = 'int32';
IntVarDefault.Min = [];
IntVarDefault.Max = [];
IntVarDefault.Description = '';
IntVarDefault.RTWInfo.StorageClass = 'Custom';
IntVarDefault.RTWInfo.Alias = '';
IntVarDefault.RTWInfo.Alignment = -1;
IntVarDefault.RTWInfo.CustomStorageClass = 'ImportFromFile';
IntVarDefault.RTWInfo.CustomAttributes.ConcurrentAccess = 0;
% SIGNAL IntVarDefault %

When generating Ada code from the Signal object defined above the corresponding variable is declared in qgen_base_workspace package and imported with explicit “pragma Import” directive:

package qgen_base_workspace is
   IntVarDefault : Integer_16;
   pragma Import (C, IntVarDefault, "IntVarDefault");

end qgen_base_workspace;

When generating C, the variable is declared with “extern” qualifier in qgen_base_workspace.h

#ifndef QGEN_BASE_WORKSPACE_H
#define QGEN_BASE_WORKSPACE_H
#include "qgen_types.h"

extern GAINT16 IntVarDefault;

#endif

7.3.2. Imported variable with explicit declaration

Adding a reference to specification file, where external variable is declared, allows qgen to import that specification directly and declaration in generated code is not required.

% SIGNAL IntVarC %
IntVarC = mpt.Signal;
IntVarC.DataType = 'int16';
IntVarC.Min = [];
IntVarC.Max = [];
IntVarC.Description = '';
IntVarC.RTWInfo.StorageClass = 'Custom';
IntVarC.RTWInfo.Alias = '';
IntVarC.RTWInfo.Alignment = -1;
IntVarC.RTWInfo.CustomStorageClass = 'ImportFromFile';
IntVarC.RTWInfo.CustomAttributes.MemorySection = 'Default';
IntVarC.RTWInfo.CustomAttributes.DataAccess = 'Direct';
IntVarC.RTWInfo.CustomAttributes.HeaderFile = 'ext_signal_c.h';
%IntVarC.RTWInfo.CustomAttributes.ConcurrentAccess = 0;
% SIGNAL IntVarC %

% SIGNAL IntVarAda %
IntVarAda = mpt.Signal;
IntVarAda.DataType = 'int16';
IntVarAda.Min = [];
IntVarAda.Max = [];
IntVarAda.Description = '';
IntVarAda.RTWInfo.StorageClass = 'Custom';
IntVarAda.RTWInfo.Alias = '';
IntVarAda.RTWInfo.Alignment = -1;
IntVarAda.RTWInfo.CustomStorageClass = 'ImportFromFile';
IntVarAda.RTWInfo.CustomAttributes.MemorySection = 'Default';
IntVarAda.RTWInfo.CustomAttributes.DataAccess = 'Direct';
IntVarAda.RTWInfo.CustomAttributes.HeaderFile = 'ext_signal_ada.ads';
IntVarAda.RTWInfo.CustomAttributes.ConcurrentAccess = 0;
% SIGNAL IntVarAda %

When generating Ada code, ext_signal_ada.ads` is included using with statement, an import pragma is defined in package ext_signal_c for variable declared in ext_signal_c.h. Note, that the initialization statement is generated only for variable IntVarAda2 defined in the model. All imported variables are expected to be initialized in their originating modules.

package ext_signal_c is
   IntVarC : Integer_16;
   pragma Import (C, IntVarC, "IntVarC");

end ext_signal_c;
with ext_signal_c; use ext_signal_c;
with ext_signal_ada; use ext_signal_ada;
with qgen_base_workspace; use qgen_base_workspace;

package body ext_signal_c_ada is

   procedure initStates (State : in out ext_signal_c_ada_State) is
   begin
      --  Block 'ext_signal_c_ada/IntVarAda2Mem'
      State.IntVarAda2 := Integer_16 (0.0);
      --  End Block 'ext_signal_c_ada/IntVarAda2Mem'
   end initStates;

   procedure comp
      (Out1 : out Integer_16;
       Out2 : out Integer_16;
       Out3 : out Integer_16;
       Out4 : out Integer_16;
       State : in out ext_signal_c_ada_State)
   is
   begin
      Out1 := qgen_base_workspace.IntVarDefault;

      Out2 := ext_signal_c.IntVarC;

      Out3 := ext_signal_ada.IntVarAda;

      Out4 := State.IntVarAda2;
   end comp;
end ext_signal_c_ada;

In case of C as the output language the pattern is the same, except that now we generate “extern” declaration for originally declared in ext_signal_ada.ads and insert include statement for the one declared in ext_signal_c.h.

#ifndef EXT_SIGNAL_ADA_H
#define EXT_SIGNAL_ADA_H
#include "qgen_types.h"

extern GAINT16 IntVarAda;

#endif
#ifndef EXT_SIGNAL_C_ADA_H
#define EXT_SIGNAL_C_ADA_H
#include "ext_signal_c_ada_states.h"
#include "qgen_types.h"
#include "qgen_base_workspace.h"
#include "ext_signal_c.h"
#include "ext_signal_ada.h"

extern void ext_signal_c_ada_initStates
  (ext_signal_c_ada_State* const State);
extern void ext_signal_c_ada_comp
  (GAINT16* const Out1,
   GAINT16* const Out2,
   GAINT16* const Out3,
   GAINT16* const Out4,
   ext_signal_c_ada_State* const State);

#endif
#include "ext_signal_c_ada.h"

void ext_signal_c_ada_initStates
  (ext_signal_c_ada_State* const State)
{
   /* Block 'ext_signal_c_ada/IntVarAda2Mem' */
   State->IntVarAda2 = (GAINT16) 0.0;
   /* End Block 'ext_signal_c_ada/IntVarAda2Mem' */

}
void ext_signal_c_ada_comp
  (GAINT16* const Out1,
   GAINT16* const Out2,
   GAINT16* const Out3,
   GAINT16* const Out4,
   ext_signal_c_ada_State* const State)
{
   *Out1 = IntVarDefault;

   *Out2 = IntVarC;

   *Out3 = IntVarAda;

   *Out4 = State->IntVarAda2;
}

7.3.3. Imported variables in subystem interface

QGen supports several modes of interfacing with generated code (see full list of modes in section Calling the generated code). By default each top level port is converted to an argument of corresponding compute function. The --global-io switch changes this behavior and generates global variables instead.

If any of the input and output ports is connected to a signal with imported variable, the QGen-generated global variable is omitted and the imported one is used for IO.

_images/simulink_signals_extern_io.png
% SIGNAL ImportedSigIn %
ImportedSigIn = Simulink.Signal;
ImportedSigIn.DataType = 'int16';
ImportedSigIn.Dimensions = [2 2];
ImportedSigIn.Complexity = 'real';
ImportedSigIn.Min = -100;
ImportedSigIn.Max = 100;
ImportedSigIn.SamplingMode = 'Sample based';
ImportedSigIn.SampleTime = 5;
ImportedSigIn.Description = 'Global input';
ImportedSigIn.RTWInfo.StorageClass = 'ImportedExtern';
% SIGNAL ImportedSigIn %

% SIGNAL ImportedSigOut %
ImportedSigOut = Simulink.Signal;
ImportedSigOut.DataType = 'int16';
ImportedSigOut.Dimensions = [2 2];
ImportedSigOut.Complexity = 'real';
ImportedSigOut.Min = -100;
ImportedSigOut.Max = 100;
ImportedSigOut.SamplingMode = 'Sample based';
ImportedSigOut.SampleTime = 5;
ImportedSigOut.Description = 'Global output';
ImportedSigOut.RTWInfo.StorageClass = 'ImportedExtern';
% SIGNAL ImportedSigOut %

By default all ports have a corresponding function argument that, in case of an imported Signal object, is assigned to/from global variable

void simulink_signals_extern_io_comp
  (GAINT16 const In1[2][2],
   GAINT16 const In2[2][2],
   GAINT16 Out1[2][2],
   GAINT16 Out2[2][2])
{
   GAUINT8 i;
   GAUINT8 j;

   /* Block 'simulink_signals_extern_io/In1' */
   for (i = 0; i <= 1; i++) {
      for (j = 0; j <= 1; j++) {
         ImportedSigIn[i][j] = In1[i][j];
      }
   }
   /* End Block 'simulink_signals_extern_io/In1' */

   /* Block 'simulink_signals_extern_io/Gain' */
   for (i = 0; i <= 1; i++) {
      for (j = 0; j <= 1; j++) {
         ImportedSigOut[i][j] = 2 * ImportedSigIn[i][j];
      }
   }
   /* End Block 'simulink_signals_extern_io/Gain' */

   /* Block 'simulink_signals_extern_io/Out1' */
   for (i = 0; i <= 1; i++) {
      for (j = 0; j <= 1; j++) {
         Out1[i][j] = ImportedSigOut[i][j];
      }
   }
   /* End Block 'simulink_signals_extern_io/Out1' */

   /* Block 'simulink_signals_extern_io/Sum' */
   /* Block 'simulink_signals_extern_io/In2' */
   /* Block 'simulink_signals_extern_io/Out2' */
   for (i = 0; i <= 1; i++) {
      for (j = 0; j <= 1; j++) {
         Out2[i][j] = ImportedSigOut[i][j] + In2[i][j];
      }
   }
   /* End Block 'simulink_signals_extern_io/Out2' */
   /* End Block 'simulink_signals_extern_io/In2' */
   /* End Block 'simulink_signals_extern_io/Sum' */
}

Running qgenc with --global-io switch removes the function arguments and generates global variables for In2 and Out2. External variables ImportedSignIn and ImportedSignOut are used for ports In1 and In2.

void simulink_signals_extern_io_comp (void) {
   GAUINT8 i;
   GAUINT8 j;

   /* Block 'simulink_signals_extern_io/Gain' */
   for (i = 0; i <= 1; i++) {
      for (j = 0; j <= 1; j++) {
         ImportedSigOut[i][j] = 2 * ImportedSigIn[i][j];
      }
   }
   /* End Block 'simulink_signals_extern_io/Gain' */

   /* Block 'simulink_signals_extern_io/Sum' */
   /* Block 'simulink_signals_extern_io/In2' */
   /* Block 'simulink_signals_extern_io/Out2' */
   for (i = 0; i <= 1; i++) {
      for (j = 0; j <= 1; j++) {
         simulink_signals_extern_io_comp_Out2[i][j] = ImportedSigOut[i][j] + simulink_signals_extern_io_comp_In2[i][j];
      }
   }
   /* End Block 'simulink_signals_extern_io/Out2' */
   /* End Block 'simulink_signals_extern_io/In2' */
   /* End Block 'simulink_signals_extern_io/Sum' */
}

7.4. Calling C or Ada code using S-Function block

Launching legacy code in Simulink is achieved using S-Function blocks and mex files. Mex file (MATLAB EXecutable, http://www.mathworks.se/help/matlab/matlab_external/introducing-mex-files.html) is a compiled binary format, proprietary to Mathworks. The legacy code written in C, C++, Fortran or Ada can be compiled to this format and then linked to a model through S-Function blocks (called “Legacy Function” blocks).

For executing simulation no information on original sources is required. In case a model containing Legacy S-Function blocks is used for code generation in Real Time Workshop one needs to provide a special template file for each mex function containing the function prototype, reference to file containing function prototype etc. qgenc does not support this template format and relies only on the information contained in mdl and slx files.

The only type of supported S-Function is “Legacy function”. There are two options to generate code from this block:

  • Encode all information about the external library in S-Function parameters using the legacy_code tool and let qgenc generate the calls or

  • Generate a wrapper function using qgenc and write the actual call to the legacy function manually in this wrapper.

7.6. Option B: generate S-Function wrappers

This option applies when the S-Function block in not generated using the legacy code tool (or one did not provide the full prototype specification). When function prototype specification is not found in the block specification then qgenc will generate a module named <modelname>_wrappers which contains a function for each generated block. The workflow is as follows:

  • Run qgenc on a Simulink model to generate code

  • qgenc will produce a module for each atomic subsystem + a special module containing function wrappers for each S-Function with no prototype.

  • two files are generated for this special wrappers module:

    • <modelname>_wrappers.[h|ads] – the prototypes of wrapper functions

    • <modelname>_wrappers.[c|adb].template – empty function stubs for each wrapper function

  • the wrappers module will contain a function for each S-Function block in form

    <S-Function_name> (<inport 1>, <inport 2>, ..., <inport N>, <outport1>, <outport 2>, ..., <outport M>)

  • if this was the first time to generate code for this model then rename the file “<modelname>_wrappers.[c|adb].template” to “<modelname>_wrappers.[c|adb]” and write an appropriate function call in each wrapper function

  • in case of repeated code generation simply check that the function calls in the wrapper module generated earlier are not changed (the h/ads file is always overwritten, c/adb file remains inteact as the new file created by qgenc has “.template” suffix)

  • while compiling the code provide the legacy code module in include and lib paths

7.6.1. Example

Let us consider a model mutlvect.mdl that calls a function mult_vect from library my_vect_ops multiplying a vector with constant.

_images/my_vect_ops_1.png

7.6.1.1. Library my_vect_ops

my_vect_ops.h

#ifndef MY_VECT_OPS_H
#define MY_VECT_OPS_H

/**
 * Copies contents of vector to vector2 and multiplies each element
 * with multiplier.
**/
void mult_vect (int *vector, int vector_len, int multiplier, int *vector2);

#endif
_images/my_vect_ops_3.png

7.6.1.2. Generated wrapper functions

The model contains a block named “sfun_mult_vect” with one input port of type int8[6], one input of type int8 and one output of type int8[6].

The wrappers generated by qgenc are as follows:

no_prototype_demo_wrappers.h

/* Copyright (C) Project P Consortium */
/*
 * @generated with GNAT Model Compiler 1.0w
 * Command line arguments:
 *   -l c no_prototype_demo.xmi
 *   --clean --pre-process-xmi
*/

#ifndef NO_PROTOTYPE_DEMO_WRAPPERS_H
#define NO_PROTOTYPE_DEMO_WRAPPERS_H
#include "qgen_types.h"

extern void no_prototype_demo_wrappers_sfun_mult_vect
  (GAINT8 In1_out1[6],
   GAINT8 In2_out1,
   GAINT8 sfun_mult_vect_out1[6]);

#endif
/*  @EOF  */

no_prototype_demo_wrappers.c.template

/* Copyright (C) Project P Consortium */
/*
 * @generated with GNAT Model Compiler 1.0w
 * Command line arguments:
 *   -l c no_prototype_demo.xmi
 *   --clean --pre-process-xmi
*/

#include "no_prototype_demo_wrappers.h"

void no_prototype_demo_wrappers_sfun_mult_vect
  (GAINT8 In1_out1[6],
   GAINT8 In2_out1,
   GAINT8 sfun_mult_vect_out1[6])
{
}
/*  @EOF  */

7.6.1.3. Call to S-Function

no_prototype_demo.c

/* Copyright (C) Project P Consortium */
/*
 * @generated with GNAT Model Compiler 1.0w
 * Command line arguments:
 *   -l c no_prototype_demo.xmi
 *   --clean --pre-process-xmi
*/

#include "no_prototype_demo.h"

void no_prototype_demo_no_prototype_demo_init (void) {
}
void no_prototype_demo_no_prototype_demo_comp
  (GAINT8 In1[6],
   GAINT8 In2,
   GAINT8 Out1[6])
{
   GAINT8 In1_out1[6];
   GAINT8 In2_out1;
   GAINT8 sfun_mult_vect_out1[6];

   /* Block no_prototype_demo/In1 */
   for (GAUINT8 i = 0; i <= 5; i++) {
      In1_out1[i] = In1[i];
   }
   /* End Block no_prototype_demo/In1 */

   /* Block no_prototype_demo/In2 */
   In2_out1 = In2;
   /* End Block no_prototype_demo/In2 */

   /* Block no_prototype_demo/sfun_mult_vect */
   (void) no_prototype_demo_wrappers_sfun_mult_vect (In1_out1, In2_out1, sfun_mult_vect_out1);
   /* End Block no_prototype_demo/sfun_mult_vect */

   /* Block no_prototype_demo/Out1 */
   for (GAUINT8 i_1 = 0; i_1 <= 5; i_1++) {
      Out1[i_1] = sfun_mult_vect_out1[i_1];
   }
   /* End Block no_prototype_demo/Out1 */

}
/*  @EOF  */

7.6.1.4. Manual code

The code written manually inside of the generated multvect_template stubs:

no_prototype_demo_wrappers.c

/* Copyright (C) Project P Consortium */
/*
 * @generated with GNAT Model Compiler 1.0w
 * Command line arguments:
 *   -l c no_prototype_demo.xmi
 *   --clean --pre-process-xmi
*/

#include "no_prototype_demo_wrappers.h"
/* the legacy code specs */
#include "my_vect_ops.h"

void no_prototype_demo_wrappers_sfun_mult_vect
  (GAINT8 In1_out1[6],
   GAINT8 In2_out1,
   GAINT8 sfun_mult_vect_out1[6])
{
   /* call external function. At this point the vector length is
      statically known, so we pass the length argument as hard-coded
      constant
    */
    mult_vect (In1_out1, 6, In2_out1, sfun_mult_vect_out1);
}
/*  @EOF  */

From this point on, the no_prototype_demo_wrappers.c shall be made available at link time and it remains intact when new code is generated (unless the block interface changes).

7.7. Compiling and linking the generated code

  • The header file referenced in the HeaderFiles parameter shall be available at compile time.

  • The source file referenced in SourceFiles shall be available to the linker

7.8. Calling C or Ada codebases using QGen-SFun block

The QGen-SFun block is available in the Simulink Library Browser under the QGen Toolset Library.

To use it simply drag it to the model from which you want to call your existing C or Ada code.

To configure the code, double click on the block. The following dialog will appear:

_images/QGen-Sfun-mask.png

The first step is to choose the language that you are calling from the Language dropdown list.

Then, click on Setup S-Function and the S-Function builder panel documented here https://www.mathworks.com/help/simulink/sfg/s-function-builder-dialog-box.html will pop up.

_images/S-Function-builder.png

Type the name of your S-Function binary within the S-Function name field.

Keep the language within that panel set to C, even if you chose Ada in the first screen. This language corresponds to the language called by Simulink and we need it to be C.

In the Initialization screen, set the number of discrete states and an array initializing them, note that each variable will be of type Float in Ada or single in C in the function prototypes.

_images/QGen-Sfun-States.png

In the Data properties screen, set the Input ports, output ports and data type attributes as needed. Changes to the parameters tab will not be taken into account.

In the Libraries screen, add the relative path to the sources directories of your code within the Library/Object/Source files (one per line) panel. Only paths to directories are supported by the QGen-SFun block.

The External function declarations only has to be filled when linking Ada code. When calling Ada you first have to make sure that you exported your Ada function(s) symbol(s) to C, as written in the example below:

package Ada_Sfun is
   procedure do_Compute(u0 : Long_Float;
                        u1 : Long_Float;
                        u2 : Long_Float;
                        u3 : Long_Float;
                        u4 : Long_Float;
                        y0 : out Long_Float);

   pragma Export (C, do_Compute,  "do_Compute");
end Ada_Sfun;

Warning

You have to Export your function with a symbol equal to its name.

Then you have to add in External function declarations the equivalent C prototypes for the exported symbol. Which, for the example above, would be :

extern void do_Compute (const real_T u0,
                        const real_T u1,
                        const real_T u2,
                        const real_T u3,
                        const real_T u4,
                        const real_T * y0);

Be sure to realize this step for each function that you want to call directly from Simulink. In effect you will only have to do this for at most 3 functions, one for the Outputs call (mandatory) and two for Start and Update in the case where you have discrete states.

Finally the last step is to call the C symbol of your function(s) in the Outputs tab. If you have states you also should call the necessary functions in the Start and Update tab.

Warning

Do not type any extra C code in these panels. The only code that you should add is the call to your function. If you wish to realize additional processing of the inputs, you should do so within the called code, or call a wrapper that does the desired processing of the inputs.

In our example we would write the following call, using the names of the ports defined in Data properties. The states are passed as an array of single named xD.

_images/QGen-Sfun-Outputs.png

Warning

Pass the arguments to your function in the same order they were defined in Data Properties. Changing the order, omitting a parameter or combining parameters would cause the S-Function to be built correctly but the QGen code generation would not be correct. All ports and states are passed as pointers, if you are treating your input ports as scalar, make sure to dereference them, like in the example above.

Once the calls are complete click Save then Close. Some of your changes will appear in the Setup summary panel.

_images/QGen-Sfun-complete.png

In C make sure to fill the header file field next to the functions that are called. This is mandatory for code generation.

_images/QGen-Sfun-cheader.png

After reviewing or completing the setup, click on Build S-Function. Your S-Function will be automatically built based on the provided setup. In the case of compilation errors, they will be detailed in the MATLAB command window.

Fix them by going back to Setup S-Function and editing the necessary fields. You can find the generated files within [libname]_wrappers.

When the build is sucessful you are ready to connect your block. You can also generate code for your model and QGen will call the correct functions.

Note

If your block is already connected and your reopen the Setup S-Function panel, connecting signals will be deleted. Simply reconnect them.

7.8.1. Debugging the QGen S-Function during simulation

After building your QGen S-Function, you can simply debug your code during the Simulink Simulation by clicking on Debug S-Function in Simulation.

This will spawn a GNAT Studio or GPS instance that will automatically attach to Simulink. When the Debugger Console is available, you can start or step within the simulation and the {GNAT Studio|GPS} Debugger will automatically break when reaching your S-Function call.

Note

As soon as the S-Function code breakpoint is reached, MATLAB will hang. This is normal, just continue the code execution after debugging this step to unblock MATLAB. Once MATLAB is unblocked, you can step, continue or stop the simulation.

All standard debugging capabilities are available after your stepped within the S-Function code.