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

Возможности генератора кода

Внутренние функции в сгенерированном коде

Генератор кода Engee поддерживает виртуальные и атомарные подсистемы для генерации кода. Виртуальные подсистемы служат только для визуальной иерархии в модели, в то время как атомарные подсистемы представляют собой единый блок выполнения (или функцию в сгенерированном коде). Настройка в свойствах подсистемы Treat as atomic unit позволяет сделать подсистему виртуальной или атомарной.

Все вычисления внутри виртуальных подсистем встраиваются (инлайнятся) непосредственно в код объемлющей подсистемы или модели. Это может улучшить производительность сгенерированного кода, но ухудшить читаемость и трассируемость к модели.

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

Для примера, превратите RollMode из модели autopilot_roll.engee (см. Пример генерации кода) в атомарную подсистему:

RollMode атомарная подсистема

Сохраните модель и снова сгенерируйте код. В сгенерированном коде теперь появилась функция с именем RollMode (соответствующая имени подсистемы в модели). Эта функция вызывается внутри функции autopilot_roll_step, которая представляет код объемлющей модели. Функция RollMode объявлена как static и, таким образом, является внутренней функцией этой модели.

Генератор кода игнорирует отключенные блоки, и связанный с ними код не создается.

comment out 1

Параметры в сгенерированном коде

Параметры в блоках модели

В блоках модели параметры могут быть заданы:

  • В виде чисел (например, 0.2);

  • В виде выражений (например, 0.2 + 0.7);

  • В виде имен переменных из рабочего пространства Engee (например, Kp).

В сгенерированном коде параметры встраиваются (инлайнятся) с теми значениями, которые заданы в модели:

  • Простые численные значения добавляются в код «как есть», и дополнительных действий не требуется;

  • Выражения могут быть упрощены, и в коде окажется только конечное вычисленное значение;

  • Переменные сохраняются «как есть», но для сборки во внешней среде разработки потребуется подключить заголовочный файл с их определениями и объявлениями.

Настраиваемые параметры

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

  • Если параметр задан через имя переменной (например, Kp), то ее имя сохраняется в структуре:

    struct P_modelname_T_ {
        double Kp;
    };
  • Если параметр задан в виде выражения (например, Kp + 1), то используется название параметра (например, Amplitude):

    struct P_modelname_T_ {
        double Amplitude;
    };

Во всех случаях параметры в структуре modelname_P заполняются вычисленными значениями, доступными из модели, например:

P_modelname_T modelname_P = {
    0.5 // Значение параметра
};

С помощью опции Поведение параметров по умолчанию в окне настроек debug article icon 1 можно управлять тем, какие параметры останутся изменяемыми в сгенерированном коде:

behavior setting 1

  • Встроенные (inlined) — числовые значения напрямую вставляются в сгенерированный код и не добавляются в структуру.

  • Настраиваемые (tunable) — параметры добавляются в структуру даже при числовых значениях.

Интерфейсы сгенерированного кода и интеграция во внешнюю среду разработки

Интерфейсы сгенерированного кода

Внешние интерфейсы сгенерированного кода можно увидеть в сгенерированном файле modelname.h. Внешние интерфейсы включают в себя функции и структуры для работы со сгенерированным кодом:

  • функция modelname_init является функцией инициализации модели — должна вызываться однократно;

  • функция modelname_step является точкой входа в модель и содержит алгоритм модели — должна вызываться периодически в соответствии с шагом расчета модели;

  • структура modelname_U содержит внешние входные порты модели;

  • структура modelname_Y содержит внешние выходные порты модели;

  • структура modelname_S содержит внутренние состояния модели.

Чтобы использовать эти функции и структуры во внешнем коде (написанном вручную), нужно выполнить следующие действия:

  • Подключить сгенерированный заголовочный файл:

    #include "modelname.h"
  • Организовать однократный вызов сгенерированной функции init в своем main:

    modelname_init();
  • Организовать периодический вызов сгенерированной функции step в своем main с нужным шагом:

    modelname_step();
  • Организовать передачу входных и выходных данных в и из функции step:

    /* Устанавливаем значения входов */
    modelname_U.input1 = 42;
    /* Вызываем функцию step */
    modelname_step();
    /* Можно использовать modelname_Y.output1 */

Поддерживаемые типы данных

Типы данных в сгенерированном коде соответствуют типам данных в модели Engee.

Если пользователь не задал тип данных в модели, то используется тип данных с плавающей точкой двойной точности, соответствующий типу double в Си. Для логических операций или вычислений, формирующих булевые результаты (true (1) или false (0)), используется тип данных boolean, соответствующий восьмибитному беззнаковому типу данных в Си.

Векторизация

Генератор кода поддерживает векторные типы данных в моделях, включая вектора и матрицы. Для векторных операций в коде генерируются стандартные циклы for без вызовов специфических BLAS или LAPACK функций.

Комплексные числа

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

Многочастотные модели

Модели Engee могут быть многочастотными, то есть содержать несколько отличающихся частот дискретизации. Применяя блоки Rate Transition, а также задавая параметр Sample Time блоков и подсистем, пользователь может управлять тем, с каким шагом расчета работают блоки или группы блоков.

Генератор кода Engee поддерживает генерацию кода для многочастотных моделей. Для этого блоки группируются по частотам, и пользователь может управлять тем, в каком виде эти разные частоты представлены в сгенерированном коде.

Настройка Генерация многочастотного кода позволяет управлять режимом работы сгенерированного кода для многочастотной модели. «Однозадачный» код позволяет сгенерировать одну функцию step для модели. «Многозадачный» код позволяет сгенерировать несколько функций step для модели, каждая из которых соответствует определенной частоте дискретизации в модели.

Настройка генерации многочастотного кода

Однозадачный код содержит условия (в виде if) внутри сгенерированной функции step, которые оборачивают вызовы более медленных частот в модели. Функция step содержит блоки, работающие на базовой частоте модели (самой быстрой частоте). Из пользовательской обвязки достаточно обеспечить вызов функции step, при этом планирование вызовов более медленных частот и обмен информацией между частотами осуществляется автоматически в самом сгенерированном коде.

Многозадачный код содержит несколько функций step_N в сгенерированном коде, каждая из которых соответствует своей частоте в модели. Функция step_0 соответствует самой быстрой частоте в модели (базовой частоте), функция step_1 второй более медленной частоте и так далее. Из пользовательской обвязки требуется обеспечить вызов функций step_N с нужными шагами расчета. Взаимодействия по данным между различными частотами обеспечиваются автоматически в самом сгенерированном коде.

Комментарии в сгенерированном коде

По умолчанию в сгенерированном коде содержатся комментарии, позволяющие проследить код до блоков в модели.

Настройка Включить комментарии в панели Генерация кода позволяют управлять комментариями в сгенерированном коде и при необходимости отключить их полностью.

Верификация кода

В Engee можно автоматически создавать блок C Function для верификации сгенерированного кода. Для этого поставьте галочку Генерировать блок C Function в окне настроек lk 5:

c function codegen

При следующей генерации кода (через интерфейс, контекстное меню или команду generate_code) в директории ouput_dir появится файл modelname_verification.jl (если директории не существует — она будет создана автоматически). Этот файл будет содержать скрипт, который можно выполнить в редакторе скриптов (двойным нажатием) interactive scripts icon или командной строке command line icon:

include("path/to/verification.jl")

Скрипт создаст модель Engee с именем modelname_verification.engee и блоком C Function, который использует сгенерированный код:

model generated cfunction

В Engee также доступна генерация кода на языке Verilog. Для этого можно использовать функцию engee.generate_code в командной строке img 41 1 2, указав target="verilog". Если target не указан, то по умолчанию генерируется Си-код.

engee.generate_code("path/to/modelname.engee", "path/to/output_dir"; target = "verilog")

Как и в случае с Си-кодом, будет создан файл с расширением .v, содержащий Verilog-код модели. Здесь "path/to/modelname.engee" — это путь к сохраненной модели, а "path/to/output_dir" — папка, в которую будут сохранены сгенерированные файлы.

Пример генерации Verilog
engee.generate_code("/user/newmodel_1.engee", "/user/newmodel_1_code"; target = "verilog")

codegen verilog

Пример верификации исходной и генерируемой модели
include("path/to/verification.jl")

model_main = engee.load("path/to/modelname.engee", force = true)
model_verify = engee.load("path/to/modelname_verification.engee", force = true)

result_main = collect(engee.run(model_main, verbose = true)["Block_name.port_number"])
result_verify = collect(engee.run(model_verify, verbose = true)["Block_name.port_number"])

result_main == result_verify

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

Генерация кода на основе пользовательских шаблонов

Engee позволяет генерировать код из блоков с использованием шаблонов (templates). Шаблоны — это текстовые файлы с расширением .cgt (codegen template), в которых используется специальный синтаксис для автоматического добавления данных и создания кода. Генерация кода с помощью шаблонов поддерживается для языков Cи и Chisel.

Для работы генерации файл должен обязательно иметь окончание .cgt.

Чтобы генератор кода Engee мог использовать шаблоны, их нужно добавить в путь. Для этого выберите папку с шаблонами, щелкните по ней правой кнопкой мыши и добавьте в путь. После добавления папка будет отображаться с синим значком blue directory icon. Это необходимо, чтобы генератор кода мог обойти все указанные пути, найти файлы шаблонов и использовать их для создания кода блоков.

После добавления шаблона в путь нажмите кнопку «Сгенерировать код» codegen icon 1. В папке с результатами вы найдете сгенерированный файл на выбранном языке.

Синтаксис шаблонов основан на комментариях. Они могут быть однострочными, начиная с //!, или многострочными, заключенными в /*! */. Внутри комментариев можно писать любой корректный код на языке Julia. Комментарии используются, чтобы код шаблона выполнялся, но не попадал в итоговый сгенерированный файл.

Незакомментированные части шаблона напрямую записываются в файл. В этих областях должен быть код на языке Cи или Chisel. Для динамической генерации используется синтаксис $(). Внутри скобок можно указать любой рабочий код на Julia, который будет выполнен, а его результат заменит эту конструкцию в итоговом файле.

В областях внутри $() используются специальные поддерживаемые для генерации кода функции:

Список поддерживаемых функций
Категория Функция Описание

Функции для входов

input

Возвращает входной порт блока

input_datatype_name

Возвращает тип данных входного порта

input_size

Возвращает размерность входного порта

input_ndims

Возвращает количество размерностей входного порта

input_iscomplex

Возвращает комплексность входного порта

input_sampletime

Возвращает шаг расчета входного порта

input_ports_num

Возвращает количество входных портов блока

Функции для выходов

output

Возвращает выходной порт блока

output_datatype_name

Возвращает тип данных выходного порта

output_size

Возвращает размерность выходного порта

output_ndims

Возвращает количество размерностей выходного порта

output_iscomplex

Возвращает комплексность выходного порта

output_sampletime

Возвращает шаг расчета выходного порта

output_ports_num

Возвращает количество выходных портов блока

Функции для параметров

parameter

Возвращает параметр блока

parameter_datatype_name

Возвращает тип данных параметра

parameter_size

Возвращает размерность параметра

parameter_ndims

Возвращает количество размерностей параметра

parameter_iscomplex

Возвращает комплексность параметра

parameter_num

Возвращает количество параметров блока

Функции для состояний

state

Возвращает состояние блока

state_addr

Возвращает состояние блока в виде указателя

state_datatype_name

Возвращает тип данных состояния

state_size

Возвращает размерность состояния

state_ndims

Возвращает количество размерностей состояния

state_iscomplex

Возвращает комплексность состояния

state_num

Возвращает количество состояний блока

Функции для работы с путями до блока

get_blockname

Возвращает имя блока

Функции для конфигурации кода

call_model_init

Возвращает код для вызова функции model_init

call_model_step

Возвращает код для вызова функций model_step

call_model_term

Возвращает код для вызова функций model_term

get_model_source

Возвращает название файла model.c

get_model_name

Возвращает имя модели

Функции для работы с выходными буферами генератора кода

buf_include

Добавляет #include в файл model.h

buf_define

Добавляет #define в файл model.c

buf_extern

Добавляет extern объявление в файл model.h

buf_typedef

Добавляет typedef в файл model.h

buf_init

Добавляет код в функцию init блока

buf_step

Добавляет код в функцию step блока

buf_term

Добавляет код в функцию term блока

Функции для работы с планировщиком

block_sampletime

Возвращает шаг расчета блока (можно использовать output_sampletime(1), если у блока есть выход)

get_rates_num

Возвращает количество частот дискретизации в модели

get_baserate

Возвращает базовую частоту дискретизации модели

get_model_rates

Возвращает массив соотношений базовой и других частот дискретизации, например, [1 3 5]

is_singlerate_model

Возвращает true, если модель одночастотная

is_singletask_model

Возвращает true, если модель однозадачная

В библиотеке также доступны семейства функций вида xxx_len, где xxx может быть input, output или parameter. Эти функции, в отличие от xxx_size, возвращают длину элемента аналогично функции length в Julia.

Кроме того, предусмотрено семейство функций xxx_len_decl, которое преобразует результат xxx_len в строку, используемую как декларация размера массива. Например, если xxx_len возвращает значение 20, то xxx_len_decl вернет строку "[20]".

Рассмотрим пример шаблона для следующей модели:

codegen model 1

//! BlockType = :Product
//! TargetLang = :C

//! @Step
//! if contains(param.Inputs,"/")
    /* Выполняется деление, если в параметрах блока указан символ "/" */
    /* Чтобы избежать ошибки деления на ноль, добавлена проверка входного значения */
    $(output_datatype_name(1)) $(output(1)) = $(input(2)) == 0 ? $(input(1)) : $(input(1)) / $(input(2));
//! else
    /* Выполняется умножение, если деление не задано в параметрах */
    $(output_datatype_name(1)) $(output(1)) = $(input(1)) * $(input(2));
//! end

В приведенном примере показан обновленный шаблон для генерации кода блока Product на языке Си. В этом коде:

  • BlockType = :Product: Указывает тип блока, для которого генерируется код;

  • TargetLang = :C: Указывает целевой язык для генерации, в данном случае — Си.

Код шаблона использует секцию @Step, которая определяет вычисления, выполняемые на каждом шаге расчета:

#! @Step
if contains(param.Inputs,"/")
    /* Кастомное деление с проверкой деления на ноль */
    $(output_datatype_name(1)) $(output(1)) = $(input(2)) == 0 ? $(input(1)) : $(input(1)) / $(input(2));
else
    /* Ветка для случая, если это блок умножения */
    $(output_datatype_name(1)) $(output(1)) = $(input(1)) * $(input(2));
end

В этом коде:

  • Условие if contains(param.Inputs,"/") проверяет, содержит ли параметр Inputs строку "/". Если это так, то выполняется операция деления:

    • $(input(2)) == 0 — проверка деления на ноль. Если знаменатель равен нулю, то результат берется равным $(input(1)).

    • Иначе выполняется деление $(input(1)) / $(input(2)).

  • В случае, если деление не используется, выполняется умножение $(input(1)) * $(input(2)).

Принцип работы:

  • Код секции @Step исполняется на каждом шаге расчета, определяя поведение блока в зависимости от конфигурации параметров.

  • Использование $(input(n)) и $(output(n)) позволяет динамически обращаться к входным и выходным портам блока.

  • Переменные типа данных выходного порта определяются через $(output_datatype_name(1)), что делает код универсальным для разных типов данных.

Макрос @Step относится к предопределенным макросам метакода. Полный список доступных макросов, используемых в шаблонах, представлен в таблице ниже.

Список предопределенных макросов метакода
Категория Макрос Описание

Буферы шаблона

@Definitions

Используется для определения глобальных переменных, типов данных и макросов. Содержимое буфера эмитируется в начале файла после всех #include.

@Epilogue

Добавляет код в конец сгенерированного файла. Обычно используется для завершения работы или добавления комментариев.

@Init

Содержит код, который выполняется при инициализации модели. Буфер эмитируется в функции model_init.

@Inports

Используется для генерации описания входных портов блока. Буфер включается в соответствующую секцию кода модели.

@Outports

Содержит код для описания выходных портов блока. Генерируется в секции, связанной с обработкой выходов.

@Prologue

Добавляет код в начало файла, до секции @Definitions. Может содержать предварительные определения или настройки.

@States

Используется для описания состояний блока, включая их размеры, типы данных и инициализацию. Генерируется в секции состояния модели.

@Step

Основной буфер, в который по умолчанию записывается код, если не указан другой макрос. Этот код выполняется на каждом шаге моделирования.

@Term

Содержит код, выполняемый при завершении работы модели. Буфер эмитируется в функции model_term.

@Toplevel

Добавляет код, который должен быть размещен в самом начале модели, сразу после #include.

Для блоков C Function и Engee Function синтаксис шаблона отличается. Вместо стандартного указания типа блока через BlockType, используется формат //!BlockType = :EngeeFunction!EngeeFunction, где после ! указывается имя блока из модели, преобразованное для целевого языка. Если указанный BlockType некорректен, то система выдаст ошибку генерации.

В результате использования шаблона для модели newmodel_1 с блоком Product будет сгенерирован файл newmodel_1.c со следующим содержимым:

#include "newmodel_1.h"

/* External inputs */
Ext_newmodel_1_U newmodel_1_U;

/* External outputs */
Ext_newmodel_1_Y newmodel_1_Y;

/* Model initialize function */
void newmodel_1_init() {
	/* (no initialize code required) */
}

/* Model terminate function */
void newmodel_1_term() {
	/* (no terminate code required) */
}

/* Model step function */
void newmodel_1_step() {

	/* Product: /Product incorporates:
	 *  Inport: /In2
	 *  Inport: /In1
	 */
	const double Product = newmodel_1_U.In2 * newmodel_1_U.In1;
	/* Product: /Divide incorporates:
	 *  Inport: /In1
	 *  Inport: /In1
	 */
	const double Divide = newmodel_1_U.In1 / newmodel_1_U.In1;
	/* Outport: /Out1 incorporates:
	 *  Product: /Divide
	 */
	newmodel_1_Y.Out1 = Divide;
	/* Outport: /Out2 incorporates:
	 *  Product: /Product
	 */
	newmodel_1_Y.Out2 = Product;

}

В сгенерированном коде реализуются основные функции для работы модели: инициализация (newmodel_1_init), выполнение шага расчета (newmodel_1_step) и завершение работы (newmodel_1_term).

Функция newmodel_1_step выполняет ключевые операции модели. В этом примере реализованы два вычисления: умножение (Product) и деление (Divide). Результаты этих операций записываются в соответствующие выходные параметры модели Out1 и Out2. Для расчета значений используются данные, поступающие через входы In1 и In2, определенные в структуре Ext_newmodel_1_U.

В функциях инициализации (newmodel_1_init) и завершения работы (newmodel_1_term) указано /* (no initialize code required) / и / (no terminate code required) */. Это означает, что для данной модели на этапе генерации кода не потребовалось добавлять дополнительный пользовательский код для выполнения операций инициализации или завершения работы. Эти функции остаются пустыми, но все равно включаются в код для поддержки синтаксиса шаблона.

В сгенерированном коде функции init и term создаются всегда, даже если в шаблоне их секции не добавлены явно (@Init или @Term). Это сделано для того, чтобы структура сгенерированного кода оставалась единой и готовой к добавлению новой логики в будущем.

Например, в приведённом шаблоне для блока Product есть только секция @Step. В результате:

Функции init и term в сгенерированном коде также будут присутствовать, но останутся пустыми, с комментариями /* (no initialize code required) / и / (no terminate code required) */. При этом, содержимое секции @Step будет вставлено в функцию step, так как это единственная секция, заданная в шаблоне.

Таким образом, под каждую секцию всегда резервируется место в генерируемом коде, но их использование полностью зависит от содержания шаблона.

Управление именами сигналов

Генератор кода использует названия сигналов, заданные пользователем в модели, чтобы сделать сгенерированный код более понятным и легким для сопоставления с моделью. Например:

image14

Если сигнал (выход блока) имеет имя, то в сгенерированном коде для этого сигнала будет использоваться то же имя переменной. Это возможно, только если сигнал не был оптимизирован и не содержит символов, которые не поддерживаются в Си-коде.

Поддерживаемые блоки

Генератором кода Engee поддерживаются следующие библиотечные блоки: