Importing C/C++ into Model Composer - 2022.1 English

Vitis Model Composer User Guide (UG1483)

Document ID
UG1483
Release Date
2022-05-26
Version
2022.1 English

While Model Composer lets you import C or C++ functions to create a library of blocks, it does have specific requirements for the code to be properly recognized and processed. The function source can be defined in either a header file (.h), or in a C or C++ source file (.c, .cpp), but the header file must include the function signature.

You can import functions with function arguments that are real or complex types of scalar, vectors, or matrices, as well as using all the data types supported by Model Composer HLS Library, including fixed-point data types. Model Composer also lets you define functions as templates, with template variables defined by input signals, or as customization parameters to be specified when the block is added into the model or prior to simulating the model. Using function templates in your code lets you create a Model Composer block that supports different applications, and can increase the re-usability of your block library. Refer to Defining Blocks Using Function Templates for more information.

Important: The function signature must be defined in the header file, and any Model Composer (XMC) pragmas must be specified as part of the function signature in the header file.

The xmcImportFunction command supports C++ functions using std::complex<T> or hls::x_complex<T> types. For more details, see the explanation in Using Complex Types.

If the input of an imported function is a 1-D array, the tool can perform some automatic mappings between the input signal and the function argument. For example, if the function argument is a[10], then the connected signal in Model Composer can be either a vector of size 10, or a row or column matrix of size 1x10 or 10x1.

However, if all of the inputs and outputs of an imported function are scalar arguments, you can connect a vector signal, or a matrix signal to the input. In this case, the imported function processes each value of the vector, or matrix on the input signal as a separate value, and will combine those values into the vector, or matrix on the output signal. For example, a vector of size 10 connected to a scalar input, will have each element of the vector processed, and then returned to a vector of size 10 on the output signal.

You can import functions that do not have any inputs, and instead only generate outputs. This is known as a source block, and can have an output type of scalar, vector, complex, or matrix. You can also import source blocks with multiple outputs. The following example function has no input port, and y is the output:
#include <stdint.h>
#include <ap_fixed.h>

#pragma XMC OUTPORT y
#pragma XMC PARAMETER Limit
template <typename T>
void counter(T &y, int16_t Limit)
{
    static T count = 0;
    
    count++;
    
    if (count > Limit)
            count =0;    
    y = count;
}
Tip: Because source blocks have no inputs, the SampleTime parameter is automatically added when the block is created with xmcImportFunction command, as shown in the Function declaration in the following image. The default value is -1 which means the sample time is inherited from the model. You can also explicitly specify the sample time by customizing the block when it is added to a model, as shown below.
Figure 1. Setting Sample Time for a Source Block Generated by Your Tool
The direction of ports for the function arguments can be determined automatically by the xmcImportFunction command, or manually specified by pragma with the function signature in the header file.
  • Automatically determining input and output ports:
    • The return value of the function is always defined as an output, unless the return value is void.
    • A formal function argument declared with the const qualifier is defined as an input.
    • An argument declared with a reference, a pointer type, or an array type without a const qualifier is defined as an output.
    • Other arguments are defined as inputs by default (e.g., scalar read-by-value).
  • Manually defining input and output ports:
    • You can specify which function arguments are defined as inputs and outputs by adding the INPORT and OUTPORT pragmas into the header file immediately before the function declaration.
    • #pragma XMC INPORT <parameter_name> [, <parameter_name>...]
    • #pragma XMC OUTPORT <parameter_name> [, <parameter_name>...]

In the following example in is automatically defined as an input due to the presence of the const qualifier, and out is defined as an output. The imported block will also have a second output due to the integer return value of the function.

int func(const int in, int &out);

In the following function in is automatically defined as an input, and out as an output, however, there is no return value.

void func(const in[512], int out[512]);

In the following example the ports are manually identified using pragmas that have been added to the source code right before the function declaration. This is the only modification to the original C++ code needed to import the function into Model Composer. In this example the pragmas specify which parameter is the input to the block and which parameter is the output of the block.

#pragma XMC INPORT din
#pragma XMC OUTPORT dout
void fir_sym (ap_fixed<17,3,AP_TRN,AP_WRAP> din[100],
              ap_fixed<17,3,AP_TRN,AP_WRAP> dout[100]);
Tip: ap_fixed specifies a fixed-point number compatible with Vitis HLS.

Manually adding pragmas to the function signature in the header file to define the input and output parameters of the function is useful when your code does not use the const qualifier, and adding the const qualifier can require extensive editing of the source code when there is a hierarchy of functions. It also makes the designation of the inputs and outputs explicit in the code, which can make the relationship to the imported block more clear.

Some final things to consider when writing C or C++ code for importing into Model Composer:

  • You should develop your source code to be portable between 32 bit and 64 bit architectures.
  • Your source code can use Vitis HLS pragmas for resource and performance optimization, and Model Composer uses those pragmas but does not modify or add to them.
  • If your code has static variables, the static variable will be shared across all instances of the blocks that import that function. If you do not want to share that variable across all instances you should copy and rename the function with the static variable and import a new library block using the xmcImportFunction command.
  • If you use C (.c) source files to model the library function (as opposed to C++ (.cpp) source files), the .h header file must include an extern "C" declaration for the downstream tools (such as Vitis HLS) to work properly. An example of how to declare the extern "C" in the header files is as follows:

    // c_function.h:
    #ifdef __cplusplus
    extern 'C' {
    #endif
    void c_function(int in, int &out);
    #ifdef __cplusplus
    }
    #endif