Main Content

Generate C++ Code for Export-Function Models That Process Unbounded Variable-Size Data

This example shows how you can generate C++ code for a function in an export-function model that processes unbounded variable-size data. For unbounded variable-size signals transmitting unbounded variable-size data, the code generator generates dynamic arrays to allocate memory dynamically at run time.

In this example, after code generation, you interface the generated code with custom main application code to exchange data between them.

Example Model

This example uses the ex_uvsmdlcomp model. Open the model by using this command in the Command Window:

openExample('ex_uvsmdlcomp')

ex_uvsmdlcomp model

The top model ex_uvsmdlcomp contains four components Data Generator, Event Scheduler, Data Processor and Data Logger. The components Data Generator and Data Processor are referenced models ex_DataGenerator and ex_DataProcessor.

At each time step of model simulation, the referenced model ex_DataGenerator outputs a one-dimensional array that is carried by an unbounded variable-size signal. This array is generated using a MATLAB Function block createArray and the size of the array changes based on input values. The Event Scheduler schedules the data transfer between ex_DataGenerator and ex_DataProcessor.

The referenced model ex_DataProcessor is an export -function model, which is modeled by using a Simulink Function block to process the unbounded variable-size data that passes to it as an input.

ex_DataProcessor

To process the unbounded variable-size data, the Simulink Function block contains:

  • The MATLAB Function block processDataInMATLABFunction, which amplifies the input array values by a factor of 2 and calculates the array length.

  • Output of the MATLAB Function block is passed to the subsystem processDataUsingIteratorSS. The subsystem contains a For Iterator Subsystem block that iterates as the length of the input array.

  • The For Iterator Subsystem block amplifies the input value by a factor of 10 and outputs the unbounded variable-size signal processsedDataOut.

  • A Bus Creator block, which creates a non-virtual bus processsedDataBus that includes the unbounded variable-size signal processsedDataOut and the array size processedDataSize.

The generated data, processed data and their sizes at each time step of model simulation are logged using Outport blocks. The purpose of the Event Scheduler and Data Generator components is to test the behavior of the Data Processor component through simulation and data logging. Once you are satisfied with the simulation results, you can generate code for the function implemented in the Data Processor.

Model Configuration for Simulation and Code Generation

To prepare the top model ex_uvsmdlcomp and its referenced models ex_DataGenerator and ex_DataProcessor for simulation and code generation, in the Configuration Parameters dialog box, these parameters are set.

  • On the Solver pane, Solver is set to discrete (no continuous states).

  • On the Data Import/Export pane, Format is set to Dataset.

  • On the Code Generation pane, Language is set to C++.

  • The Dynamic memory allocation in MATLAB functions selected.

  • On the Interface pane, Array layout is set to Column-major.

For the top model and referenced models, these configuration settings are loaded from dHarness.mat.

Simulate Top Model

Simulate the top model and review the logged data. For more information about simulation and data logging, see Use Unbounded Variable-Size Signals Between Model Components.

Generate C++ Code for ex_DataProcessor Model

Once you are satisfied with the logged data of ex_DataProcessor, you can generate C++ code for the export-function model. To generate code:

  1. Open the ex_DataProcessor model as the top-model.

  2. Open the Embedded Coder app.

  3. On the C++ Code tab, click Build.

The generated code appears in the Code view next to the model.

Inspect C++ Code

  1. In the header file ex_DataProcessor.h, the code generator defines dynamic arrays for the input and output of the MATLAB Function block processDataInMATLABFunction and for the unbounded variable-size signal passing from the For Iterator subsystem as instances of the class template coder::array. To view the dynamic arrays, open the ex_DataProcessor.h file.

    struct B_ex_DataProcessor_T {
        coder::array<real_T,1> u;          // '<S1>/u'
        coder::array<real_T,1> TmpSignalConversionAtuOutport1;// '<S1>/u'
        coder::array<real_T,1> TmpSignalConversionAtxInport1;// '<S1>/u'
        coder::array<real_T,1> TmpSignalConversionAtyInport1;// '<S1>/processDataUsingIteratorSS' 
        coder::array<real_T,1> assign;     // '<S4>/assign'
        coder::array<real_T,1> processedData;// '<S1>/processDataInMATLABFunction'
      };

  2. To view the implementation of the class template, open the header file coder_array.h generated in _Sharedutils. This header file contains details about the coder::array class interface.

  3. In coder_array.h, the code generator also implements the set_size method in the coder namespace. The method uses the CODER_ALLOC and CODER_DEALLOC macros through ensureCapacity() to perform memory allocation and freeing that memory dynamically.

    void set_size(Dims... dims)
        {
          coder::detail::match_dimensions<N == sizeof...(dims)>::check();
          set_size_i<0>(dims...);
          ensureCapacity(numel());
  4. To view the use of the set_size method, open the ex_DataProcessor.cpp file. The code calls the set_size method to allocate and free memory dynamically for the defined dynamic arrays at run time. For example, the following code lines access the size of the first dimension of the input of processDataInMATLABFunction and set the output data size accordingly at run-time.

    // MATLAB Function: '<S1>/processDataInMATLABFunction' incorporates:
      //   SignalConversion generated from: '<S1>/u'
    
      ex_DataProcessor_B.processedData.set_size
        (ex_DataProcessor_B.TmpSignalConversionAtuOutport1.size(0));
      loop_ub = ex_DataProcessor_B.TmpSignalConversionAtuOutport1.size(0);
      for (i = 0; i < loop_ub; i++) {
        ex_DataProcessor_B.processedData[i] =
          ex_DataProcessor_B.TmpSignalConversionAtuOutport1[i] * 2.0;
      }

Integrate Generated Code with Custom Code

If you want to interface the generated code with custom C++ code to call an entry-point function that accepts or returns dynamic arrays, you must define dynamic arrays in the custom code. To define dynamic arrays in the custom code, you need to include the coder_array.h header file in your custom .cpp file. Once you include the coder_array.h file, you can use the templates and methods implemented in coder_array.h in your custom code. You can interact with the dynamic arrays by accessing the size vector of the defined arrays and using the standard C++ array indexing. For more information on the APIs that you can use to interface the generated code containing dynamic arrays with custom code, see Use Dynamically Allocated C++ Arrays in Generated Function Interfaces.

In this example, you customize the generated example main application code ert_main.cpp to call the model-step function process_data.

  1. Open the ert_main.cpp file. Include the coder_array.h header file in ert_main.cpp.

    #include <stdio.h>
    #include "ex_DataProcessor.h"
    #include "coder_array.h" 
  2. The function prototype of the generated process_data function in ex_DataProcessor.cpp is:

    void ex_DataProcessor::process_data(const coder::array<real_T, 1U> &rtu_u, coder::
      array<real_T, 1U> &rty_x, coder::array<real_T, 1U> &rty_y, real_T *rty_z)

    The function accepts a one-dimensional dynamic array of data type real_T as an input and returns three outputs. Two of the outputs are also one-dimensional dynamic arrays of data type real_T.

    To interface process_data with the main application code, in ert_main.cpp, define an input array myArray and two output arrays myResult_x and myResult_y as class templates.

    // Instantiate the input variable by using coder::array template
        coder::array<int32_T, 1> myArray;
    // Instantiate the result variable by using coder::array template
        coder::array<real_T, 1> myResult_x;
        coder::array<real_T, 1> myResult_y;
        real_T myResult_z;
  3. Set the size of the dimension of myArray to 100 by using the set_size method and set the input values in the array elements.

    // Allocate initial memory for the array
        myArray.set_size(100); 
        // Access array with standard C++ indexing
        for (int i = 0; i < myArray.size(0); i++) {
            myArray[i] = i;                   
        }
  4. Use the instance ex_DataProcessor_Obj of the model class ex_DataProcessor to call the process_data function and display the output values of myResult_x through indexing.

    // Pass the input and result arrays to the generated method
        ex_DataProcessor_Obj.process_data(myArray, myResult_x, myResult_y, &myResult_z);
        // Print result
        for (int i = 0; i < myResult_x.size(0); i++) {
            if (i > 0) std::cout << " ";
            std::cout << myResult[i];
            if (((i+1) % 10) == 0) std::cout << std::endl;
        }
        std::cout << std::endl;

    If the dimension of myArray changes later on during execution, the generated code reallocates memory based on the new size.

After interfacing the generated code with the main application code in ert_main.cpp, you can make further changes to the application code to deploy into the target application. The application code and interfaced generated code capable of handling unbounded size data is suitable for deployment to desktop quality targets. For more information, see Deploy Applications to Target Hardware.

Related Topics