Документация Engee

C Function

Использование Cи-кода в моделях

c function

Описание

Блок C Function позволяет в моделях Engee использовать код на языке Си и разделяемые библиотеки, имеющие Си API.

При подключении динамической библиотеки, использующей внутренние статические переменные, стоит убедиться в том, что она предоставляет API для инициализации/деинициализации этих переменных. Соответствующие функции необходимо вызывать во вкладках StartCode и TerminateCode редактора исходного кода. В противном случае, статические переменные будут сохранять свои последние значения между симуляциями, что может приводить к неправильным результатам при повторных запусках модели.

Использование

Для интеграции исходного Cи кода в модель Engee необходимо:

  • Добавить в модель блок C Function из раздела Базовые/Пользовательские функции библиотеки блоков;

  • Нажать на кнопку Редактировать исходный код, после чего будет открыт редактор исходного кода.

c function settings

Редактор исходного кода

Рабочая область редактора исходного кода состоит из четырех вкладок:

  • Output code — содержит код, выполняющийся на каждом шаге расчета модели;

  • Start code — содержит код, выполняющийся один раз при инициализации модели;

  • Terminate code — содержит код, выполняющийся один раз при остановке модели;

  • SharedCode — содержит код, позволяющий обмениваться глобальными (разделяемыми) переменными и функциями между экземплярами блока C Function.

В каждой из секций выполняется подсветка синтаксиса языка Cи.

В секции Output code расположен блок специальных комментариев, имеющих значение для симулятора и генератора кода.

Комментарии позволяют связать переменные в исходном коде с сигналами (входами и выходами) блока C Function, а также указать дополнительную информацию, необходимую для сборки кода и его использования в модели.

c function code editor

Особенности генерации Си кода из блока C Function

Блок C Function поддерживает генерацию Cи кода. При генерации кода блок C Function превращается в функции, оборачивающие содержимое всех четырех секций исходного кода.

В данный момент генератор Cи-кода не выдает дополнительную информацию для системы сборки, которая бы основывалась на директивах блока C Function. Директивы указываются в разделе Build options настроек блока:

build options c function

Повторное использование блока

Вкладка SharedCode исходного кода C Function позволяет задавать общий код, доступный для обмена данными и функциями между экземплярами блоков C Function с одинаковым Function name.

Для того чтобы использовать функциональность SharedCode, нужно создать блок C Function, указать для него Function name, и затем сохранить этот блок в пользовательскую библиотеку user library 1. После этого его можно добавлять в модель в нескольких местах, где он будет сохранять доступ к общим данным и функциям. В результате при моделировании создается только одна общая разделяемая библиотека и один набор функций, который используется всеми экземплярами этого блока C Function.

При генерации кода для блоков с одинаковым Function name создаются только общие функции init, step и term, а код из вкладки SharedCode добавляется в проект один раз. Это упрощает управление кодом и обеспечивает доступ к общим данным во всех экземплярах блока C Function.

Рабочие переменные

Вкладка WorkVariables блока C Function позволяет задавать статические рабочие переменные, которые остаются специфичными для каждого экземпляра блока, даже если у нескольких блоков одинаковое значение Function name. Эти рабочие переменные, похожи на параметры, но отличаются возможностью изменять их значения из исходного кода блока во время выполнения. Такой подход позволяет хранить и изменять данные, которые используются только в пределах определенного экземпляра блока.

Для каждой рабочей переменной на вкладке WorkVariables указывается имя (Name) и размер в байтах (Size in bytes). Внутри исходного кода эти переменные доступны через указатель на структуру Work, который присутствует в интерфейсах всех трех функций блока: step, init, и term.

Пример:

work variable 1

Здесь в исходном коде используется выравнивание, где макрос is_aligned проверяет корректность выравнивания переменных. Переменные work_variable1 и work_variable3 заранее определены на вкладке WorkVariables и доступны через структуру Work.

Работа с массивами

Когда данные передаются из Julia в код на языке Cи, важно учитывать различия в индексации и структуре массивов. Это актуально для многомерных массивов, которые могут транспонироваться (изменять порядок индексов) при передаче. Ниже приведены основные моменты, которые необходимо учитывать при работе с массивами:

Подробнее о работе с массивами
  • Одномерные массивы — при передаче одномерного массива из Julia в Cи массив сохраняет свою структуру. Индексация в Cи начинается с 0, поэтому при обращении к элементам массива нужно учитывать этот сдвиг. Например, если в параметре блока C-function одномерный массив записан как a = [1, 2, 3], то на языке Cи:

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

    В этом примере для получения значения элемента массива с индексом 2 используется обращение a[2], что вернет значение 3.

  • Многомерные массивы — многомерные массивы могут изменять порядок индексов при передаче из Julia в Cи. В Cи-коде массив будет транспонирован, что означает, что при доступе к элементам массива нужно учитывать изменение порядка индексов. Например, есть массив в Julia:

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

    Тогда в Си:

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

    Массив [1 2 3; 4 5 6] в Julia становится массивом a[2][3] в Cи, где первый индекс указывает на строку, а второй — на столбец. В данном примере на Cи для получения элемента со значением 2 используется индексация a[1][0]. Следовательно, массив в Cи фактически хранит элементы транспонированного массива Julia, что необходимо учитывать при доступе к элементам.

При работе с трехмерными и более сложными массивами индексация также может меняться. Например, если массив в Julia имеет размерность [2, 3, 4], то в Cи он будет представлен как [4][3][2]. Это необходимо учитывать при обращении к элементам массива в Cи-коде.

Пример трехмерного массива в 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

В С:

#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;
}

Общее правило преобразования индексов из Julia в Cи заключается в изменении порядка:

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

Это означает, что индексы в Cи записываются в обратном порядке.

Main

Number of input ports — количество входных портов
1 (по умолчанию)

Определяет количество входных портов блока. Значение параметра Number of input ports будет соответствовать количеству входных портов.

Number of output ports — количество выходных портов
1 (по умолчанию)

Определяет количество выходных портов блока. Значение параметра Number of output ports будет соответствовать количеству выходных портов.

Function name — идентификатор общего кода для нескольких блоков
нет (по умолчанию)

Укажите строковое значение для объединения кода блоков C Function. Блоки с одинаковым Function name будут использовать общий код и обмениваться данными через вкладку исходного кода SharedCode блока C Function.

Если оставить поле пустым, то выбранный блок C Function останется автономным (по умолчанию).

Sample time (-1 for inherited) — интервал между шагами расчета
−1 (по умолчанию)

Укажите интервал между шагами расчета как неотрицательное число. Чтобы наследовать шаг расчета, установите для этого параметра значение −1.

Ports

Вход

Label — имя (текстовая метка) входного порта
нет (по умолчанию)

Имя (текстовая метка) входного порта. По умолчанию ячейка имени не заполнена (имя не задано).

Variable name — имя переменной входного порта в исходном Си-коде
input1 (по умолчанию)

Имя переменной входного порта в коде:

+

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

где input1 — имя переменной входного порта; output1 — имя переменной выходного порта; param1 — имя переменной параметра.

Type — тип данных переменной входного порта в исходном Cи-коде
double (по умолчанию) | float | uint8_t | int16_t | uint16_t | int32_t | uint32_t | int64_t | uint64_t

Тип данных переменной входного порта в исходном Cи-коде. Тип данных переменной должен совпадать с типом данных сигнала.

Поддерживаются базовые типы данных языка Си.

Size — размерность входного сигнала
1 (по умолчанию)

Размерность входного сигнала.

В поле Size используется нотация Julia, где размер записывается в виде кортежа, например, (2, 3, 4). Однако при работе с этим портом в Cи-коде необходимо учитывать изменение порядка индексов. Это значит, что если порт задан с размером (2, 3, 4), то в Cи-коде переменная будет иметь размерность [4][3][2]. Это правило аналогично тому, как работают параметры: порядок индексов в Cи транспонируется по сравнению с Julia.

Выход

Label — имя (текстовая метка) выходного порта
нет (по умолчанию)

Имя (текстовая метка) выходного порта. По умолчанию ячейка имени не заполнена (имя не задано).

Variable name — имя переменной выходного порта в исходном С-коде
output1 (по умолчанию)

Имя переменной выходного порта в коде:

+

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

где input1 — имя переменной входного порта; output1 — имя переменной выходного порта; param1 — имя переменной параметра.

Type — тип данных выходной переменной в исходном C-коде
double (по умолчанию) | float | uint8_t | int16_t | uint16_t | int32_t | uint32_t | int64_t | uint64_t

Тип данных выходной переменной в исходном Cи-коде. Тип данных переменной должен совпадать с типом данных сигнала.

Поддерживаются базовые типы данных языка Си.

Size — размерность выходного сигнала
1 (по умолчанию)

Размерность выходного сигнала.

В поле Size используется нотация Julia, где размер записывается в виде кортежа, например, (2, 3, 4). Однако при работе с этим портом в Cи-коде необходимо учитывать изменение порядка индексов. Это значит, что если порт задан с размером (2, 3, 4), то в Cи-коде переменная будет иметь размерность [4][3][2]. Это правило аналогично тому, как работают параметры: порядок индексов в Cи транспонируется по сравнению с Julia.

Parameters

Number of parameters — укажите число параметров
1 (по умолчанию)

Число параметров, используемых в блоке.

Parameter — определяет параметр как переменную
1 (по умолчанию)

Определяет параметр как переменную для использования в исходном коде:

  • Name — имя параметра, по умолчанию param1. Может быть изменено. Имена новых Parameter называются param2 и далее по возрастанию.

  • Value — значение параметра, по умолчанию 1. Может быть изменено. Значение новых Parameter равно нулю.

Для работы параметров необходимо зайти в редактор кода блока С Function (Редактировать исходный код) во вкладку OutputCode и внести имя нужного параметра. По умолчанию:

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

где input1 — переменная входного сигнала; output1 — переменная выходного сигнала.

WorkVariables

Number of work variables — число рабочих переменных
0 (по умолчанию)

Число рабочих переменных, используемых в блоке для хранения данных, специфичных для каждого экземпляра. Рабочие переменные доступны через структуру Work и могут использоваться в функциях init, step и term для записи и чтения данных.

Каждая переменная задается с указанием имени (Name) и размера (Size), что позволяет хранить данные различных типов. Если переменной присвоен больший размер, то ее можно использовать как массив.

WorkVariable — определяет рабочую переменную
1 (по умолчанию)

Определяет рабочую переменную для использования в исходном коде:

  • Name — имя параметра, по умолчанию work_variable1. Может быть изменено. Имена новых WorkVariable называются work_variable2 и далее по возрастанию.

  • Size in bytes — объем памяти, выделяемый для переменной, в байтах. Этот параметр позволяет указать размер переменной, будь то скаляр или массив.

Для работы переменных необходимо зайти в редактор кода блока С Function (Редактировать исходный код) во вкладку OutputCode и внести имя переменной.

Build options

Source files — подключение файлов исходного кода
нет (по умолчанию)

Используется для подключения дополнительных файлов исходного кода. Должен включать путь и имя файла вместе с расширением.

Например:

/user/project/src/example.c
Include directories — определение пути к каталогу заголовочных файлов
нет (по умолчанию)

Используется для определения пути к каталогу, содержащему заголовочные файлы.

Например:

/user/project/include
Library directories — определение пути к каталогу библиотек
нет (по умолчанию)

Используется для определения пути к каталогу, содержащему разделяемые библиотеки.

Например:

/user/project/third_party
Headers — подключение заголовочных файлов
stdint.h math.h (по умолчанию)

Используется для подключения заголовочных файлов. Должен включать имя заголовочного файла вместе с расширением.

Например:

example.h
Defines — определение дополнительных директив #define
нет (по умолчанию)

Используется для определения дополнительных директив #define.

Например:

defines LOWER=0 UPPER=300 STEP=20
Libraries — подключение библиотек
нет (по умолчанию)

Используется для подключения библиотек. Должен включать имя библиотеки вместе с расширением.

Например:

libexample.so
Фактический тип данных, как и поддержка возможных типов данных, зависит от пользовательского кода внутри блока.

Дополнительные возможности

Генерация Си кода: Да