Engee documentation

C Function

Usage of C-code in models

c function

Description

Block C Function allows Engee models to use C code and shared libraries with C API.

When connecting a dynamic library that uses internal static variables, make sure that it provides an API for initialising/deinitialising these variables. The corresponding functions should be called in the StartCode and TerminateCode tabs of the source code editor. Otherwise, static variables will retain their last values between simulations, which may lead to incorrect results when the model is rerun.

Usage

To integrate source C code into an Engee model, you need to:

  • Add a block to the model C Function from the Basic/User Functions section of the block library;

  • Click on the Edit Source Code button and the source code editor will be opened.

c function settings

Source code editor

The source code editor workspace consists of four tabs:

  • Output code - contains code executed at each step of model calculation;

  • Start code - contains code that is executed once when the model is initialised;

  • Terminate code - contains code that is executed once when the model is stopped;

  • SharedCode - contains code allowing to exchange global (shared) variables and functions between instances of the block C Function.

C syntax highlighting is performed in each of the sections.

The Output code section contains a block of special comments that are important for the simulator and the code generator.

Comments allow you to associate variables in the source code with signals (inputs and outputs) of the block, as well as to specify additional information necessary for the simulator and the code generator. *C Function*and also to specify additional information necessary for code assembly and its usage in the model.

c function code editor

Peculiarities of C code generation from C Function block

Block C Function supports C code generation. When generating code, the block C Function is turned into functions that wrap the contents of all four sections of the source code.

At this time, the C code generator does not provide additional information to the build system based on the block’s directives C Function. The directives are specified in the Build options section of the block settings:

build options c function

Block usage reuse

The SharedCode tab of the source code C Function allows you to specify the common code available for data and function exchange between block instances with the same Function name. C Function with the same Function name.

In order to use the SharedCode functionality, you need to create a block C Function, specify a Function name for it, and then save that block to user library user library 1. It can then be added to the model in several places, where it will retain access to common data and functions. As a result, modelling creates only one common shared library and one set of functions that is used by all instances of this block C Function.

When generating code for blocks with the same Function name, only the shared functions init, step and term are created, and the code from the SharedCode tab is added to the project once. This simplifies code management and ensures that common data is available in all instances of the block C Function.

Working variables

The WorkVariables tab of a block C Function allows you to set static work variables that remain specific to each block instance, even if several blocks have the same Function name value. These work variables are similar to parameters, but differ in their ability to change their values from the block’s source code at runtime. This approach allows you to store and modify data that is only used within a particular block instance.

For each work variable on the WorkVariables tab, a name (Name) and size in bytes (Size in bytes) are specified. Inside the source code, these variables are accessible via a pointer to the Work structure, which is present in the interfaces of all three block functions: step, init, and term.

Example:

work variable 1

Alignment is used here in the source code, where the is_aligned macro checks that the variables are aligned correctly. The variables work_variable1 and work_variable3 are predefined on the WorkVariables tab and are available through the Work structure.

Working with arrays

When data is transferred from Julia to C code, it is important to consider differences in indexing and array structure. This is relevant for multidimensional arrays, which can be transposed (change index order) during transfer. Below are the main points to consider when working with arrays:

For more information about working with arrays.
  • One-dimensional arrays - when passing a one-dimensional array from Julia to C, the array retains its structure. Indexing in C starts from 0, so when referring to array elements you need to take this shift into account. For example, if a one-dimensional array is written as a = [1, 2, 3] in the parameters of a C-function block, then in C:

    #include <stdio.h>
    
    int main() {
        int a[3] = {1, 2, 3};
        printf("%d", a[2]); // Выведет 3
        return 0;
    }

    In this example, a[2] is invoked to get the value of the array element with index 2, which will return the value 3.

  • Multidimensional Arrays - Multidimensional arrays can change index order when passed from Julia to C. In C code, the array will be transposed, which means that when accessing elements of the array, the change in index order must be accounted for. For example, there is an array in Julia:

    a = [1 2 3; 4 5 6]
    
    #Julia интерпретирует это как матрицу 2x3
    2x3 Matrix{Int64}
     1 2 3
     4 5 6
    
    #доступ к элементу массива
    a [1,2]
    2

    Then in C:

    #include <stdio.h>
    
    int main() {
        int a[3][2] = {
            {1, 4},
            {2, 5},
            {3, 6}
        };
        printf("%d", a[1][0]); // Выведет 2
        return 0;
    }

    The array [1 2 3; 4 5 6] in Julia becomes the array a[2][3] in C, where the first index points to a row and the second index points to a column. In this C example, the indexing a[1][0] is used to get an element with value 2. Consequently, the array in C actually stores the elements of the Julia transposed array, which must be taken into account when accessing the elements.

When working with three-dimensional and more complex arrays, the indexing can also change. For example, if an array in Julia has dimension [2, 3, 4], in C it will be represented as [4][3][2]. This must be taken into account when referring to array elements in C code.

Example of a three-dimensional array in Julia:

a = reshape(1:24, 2, 3, 4)

[:, :, 1] =
 1  3  5
 2  4  6

[:, :, 2] =
 7   9  11
 8  10  12

[:, :, 3] =
13  15  17
14  16  18

[:, :, 4] =
19  21  23
20  22  24


println(a[1, 2, 3]) # Выведет 13

IN S:

#include <stdio.h>

int main() {
    int a[4][3][2] = {
        {
            {1, 4},
            {2, 5},
            {3, 6}
        },
        {
            {7, 10},
            {8, 11},
            {9, 12}
        },
        {
            {13, 16},
            {14, 17},
            {15, 18}
        },
        {
            {19, 22},
            {20, 23},
            {21, 24}
        }
    };
    printf("%d", a[2][1][0]); // Выведет 13
    return 0;
}

The general rule for converting indexes from Julia to C is to change the order:

(D1, D2, …​, Dn) -> [Dn][Dn-1]…​[D1].

This means that the indices in C are written in reverse order.

Tyre handling

In the block C Function bus operation is supported both on the input/output ports and in the block parameters. The specifics of bus configuration and usage are described below:

Bus on ports

To set a bus on an input or output port:

  1. In the Type parameters of the corresponding port (Ports tab in the block settings), select the BusSignal type.

  2. The Input bus type (or Output bus type) parameters will then automatically take the form:

    BusSignal{(), Tuple(), ()}

    This is where it should be stated:

    • Bus type explicitly, e.g. BusSignal{(:s1, :s2), Tuple(Float64, Int54), ), (}. :s1, :s2 - names of signals in the bus; Tuple(Float64, Int64) - their types; ), ( - their dimensions;

    • Or the name of a variable containing a description of the bus type defined in the Engee workspace (in the variables window) or in C code.

All bus types must be specified explicitly — this is a feature of the block C Function.

Bus names should not contain spaces because they are used in C code, where spaces are not allowed in variable names.

Tyres in parameters

If the bus is used as a parameter:

  • In the Value field, the tyre must be specified as a named tuple, e.g.:

    (s1 = 4.4, s2 = (b1 = 5, b))
  • If a parameter is named, for example, p, then in C code (block source code C Function) bus elements can be treated as structures:

    output1 = input1 * (p.s1 + p.s2.b1);

Main

Number of input ports - number of input ports
1 (By default)

Defines the number of input ports of the block. The value of the Number of input ports parameters will correspond to the number of input ports.

Number of output ports will be the number of output ports
1 (By default).

Defines the number of output ports of the block. The value of the Number of output ports parameters will correspond to the number of output ports.

Function name - common code identifier for multiple blocks
none (By default)

Specify a string value for combining block code C Function. Blocks with the same Function name will use common code and share data via the source code tab SharedCode of the block C Function.

If the field is left empty, the selected block will remain autonomous (by default). C Function will remain autonomous (by default).

Sample time (-1 for inherited) - interval between calculation steps
-1 (by default).

Specify the interval between calculation steps as a non-negative number. To inherit a calculation step, set this parameters to -1.

Ports

Input

Label - name (text label) of the input port
none (By default)

The name (text label) of the input port. By default, the name cell is not filled in (no name is set).

Variable name - the name of the input port variable in the source C code
input1 (by default).

Name of the input port variable in the code:

+

/* Этот код вызывается на каждом шаге расчета модели */
output1 = input1 * param1;

where input1 is the name of the input port variable; output1 is the name of the output port variable; param1 is the name of the parameters variable.

Type - data type of the input port variable in the source C code
double (by default) | float | uint8_t | int16_t | uint16_t | int32_t | uint32_t | int64_t | uint64_t

Data type of the input port variable in the source C code. The data type of the variable must match the data type of the signal.

Basic C language data types are supported.

Size - dimension of the input signal
1 (By default)

Dimensionality of the input signal.

The Size field uses Julia notation, where the size is written as a tuple, for example, (2, 3, 4). However, when working with this port in C code, you must take into account the change in index order. This means that if a port is specified with the dimension (2, 3, 4), then in C code the variable will have the dimension [4][3][2]. This rule is similar to the way parameters work: index order is transposed in C compared to Julia.

Output

Label - name (text label) of the output port
none (by default)

The name (text label) of the output port. By default, the name cell is not filled in (no name is set).

Variable name - name of the output port variable in the source C code
output1 (by default)

The name of the output port variable in the code:

+

/* Этот код вызывается на каждом шаге расчета модели */
output1 = input1 * param1;

where input1 is the name of the input port variable; output1 is the name of the output port variable; param1 is the name of the parameters variable.

Type - data type of the output variable in the source C code
double (by default) | float | uint8_t | int16_t | uint16_t | int32_t | uint32_t | int64_t | uint64_t

Data type of the output variable in the source C code. The data type of the variable must match the data type of the signal.

Basic C language data types are supported.

Size - dimension of the output signal
1 (By default)

Output signal dimensionality.

The Size field uses Julia notation, where the size is written as a tuple, for example, (2, 3, 4). However, when working with this port in C code, you must take into account the change in index order. This means that if a port is specified with the dimension (2, 3, 4), then in C code the variable will have the dimension [4][3][2]. This rule is similar to the way parameters work: index order is transposed in C compared to Julia.

Parameters

Number of parameters - specify the number of parameters
1 (By default)

Number of parameters used in the block.

Parameter - defines parameters as a variable
`1 (By default)

Defines parameter as a variable for usage in source code:

  • Name is the name of the parameters, by default param1. Can be changed. New Parameter names are called param2 and then in ascending order.

  • Value - the value of the parameters, by default 1. Can be changed. The value of new Parameter is zero.

For parameters to work it is necessary to go to the block code editor C Function (Edit Source Code) to the OutputCode tab and enter the name of the required parameter. By default:

/* Этот код вызывается на каждом шаге расчета модели */
output1 = input1 * param1;

where input1 is the input signal variable; output1 is the output signal variable.

WorkVariables

Number of work variables - number of work variables
0 (By default)

Number of work variables used in the block to store data specific to each instance. Work variables are accessible through the Work structure and can be used in init, step and term functions to write and read data.

Each variable is specified with a name (Name) and a size (Size), which allows storing data of different types. If a variable is assigned a larger size, it can be used as an array.

WorkVariable - defines a working variable
`1 (By default)

Defines a working variable for usage in source code:

  • Name is the name of the parameters, by default work_variable1. Can be changed. New WorkVariable names are called work_variable2 and then in ascending order.

  • Size in bytes - the amount of memory allocated for the variable, in bytes. This parameter allows you to specify the size of the variable, whether it is a scalar or an array.

For variables to work it is necessary to go to the code editor of the C Function block (Edit Source Code) to the OutputCode tab and enter the variable name.

Build options

Source files - connect source code files
no (By default)

Used to connect additional source code files. Must include path and filename along with extension.

For example:

/user/project/src/example.c
Include directories - defining the path to the directory of header files
none (By default)

Used to define the path to the directory containing header files.

For example:

/user/project/include
Library directories - defining the path to the library catalogue
none (By default)

Used to define a path to a directory containing shared libraries.

For example:

/user/project/third_party
Headers - connection of header files
stdint.h math.h (by default)

Used to connect header files. Must include the name of the header file along with the extension.

For example:

example.h
Defines - definition of additional directives #define
none (By default)

Used to define additional #define directives.

For example:

defines LOWER=0 UPPER=300 STEP=20
Libraries - connecting libraries
none (By default)

Used to connect libraries. Must include the library name along with the extension.

For example:

libexample.so
The actual data type, as well as the support for possible data types, depends on the custom code within the block.

Additional options

C code generation: Yes