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

Выполнение модели в независимом режиме

Страница в процессе разработки.

В этой статье описан рантайм независимого режима выполнения: правильный вызов Си-функции модели на целевой платформе после генерации, сборки и загрузки. Интерактивное выполнение здесь не рассматривается.

Генерация Си-кода из модели

Перед тем как таргет начнет работу, Engee генерирует Си-код модели. Эти данные являются входными для таргета.

Сгенерированный Си-код модели содержит функции и структуры, которые реализуют вычисление модели:

  • функцию инициализации;

  • одну или несколько step-функций, которые нужно вызывать с заданным периодом;

  • функцию завершения;

  • исходные файлы модели;

  • метаданные о периодах дискретизации, сигналах, параметрах и точках входа модели.

Таргет не генерирует содержательную часть модели заново. Его задача — встроить уже сгенерированный Си-код в рантайм целевой платформы. Рантайм в этом контексте — это обвязка вокруг модели: точка входа, планировщик шагов, работа с таймером, драйверы, файлы сборки и, если нужен интерактивный режим, канал обмена данными.

Например, для Arduino таргет формирует скетч, для STM32 — проект вокруг кода STM32CubeMX и HAL, для x86 — обычное исполняемое приложение.

Где задается выполнение

Python-класс таргета создает проект, но не определяет поведение модели на каждом шаге времени. Это поведение задается рантайм-кодом, который таргет генерирует в generate_executable_code.

Рантайм-код — это обвязка C/C++ вокруг сгенерированной модели. Обычно в него входят:

  • точка входа программы или прошивки;

  • инициализация платформенного таймера;

  • вызов функции инициализации модели;

  • планировщик, который вызывает step-функции модели с нужной периодичностью;

  • проверка конечного времени моделирования;

  • вызов функции завершения модели;

  • обработка нарушения времени выполнения шага.

Чаще всего рантайм-код создается из Jinja2-шаблона. Jinja2 не является обязательным требованием: можно использовать любой другой шаблонизатор или сформировать файлы программно. Но существующие таргеты используют Jinja2, а вспомогательная функция render_template_to_file уже рассчитана именно на Jinja2-шаблоны.

Точки входа модели

После генерации Си-кода Engee передает таргету описание точек входа модели. Точка входа — это Си-функция, которую рантайм должен вызвать в нужный момент.

В статье по возможностям генератора кода это описано со стороны сгенерированного Си-интерфейса: внешний код подключает modelname.h, один раз вызывает функцию инициализации, затем периодически вызывает step-функцию с нужным шагом, а при завершении вызывает функцию завершения. Таргет делает то же самое, но не вручную: он генерирует рантайм-шаблон, который выполняет эти вызовы на целевой платформе.

Основные типы точек входа:

  • init — инициализация состояния модели, вызывается один раз перед первым шагом;

  • steps — список step-функций, выполняющих расчет модели;

  • terminate — завершение модели, вызывается при штатном останове, если оно доступно для платформы.

Таргет получает эти данные через CodeGenInfo, который создается из model_code.c_code.c_code_info.

Пример получения CodeGenInfo

codegen_info = create_codegen_info(
    model_code.c_code.c_code_info,
    model_settings.is_ext_mode,
)

В шаблон рантайма обычно передаются такие поля:

  • model_name — имя модели и главного заголовочного файла;

  • init.cname — имя Си-функции инициализации;

  • steps — список step-функций;

  • step.cname — имя конкретной step-функции;

  • step.base_rate_scale — отношение периода шага к базовому периоду;

  • terminate.cname — имя Си-функции завершения;

  • base_rate — базовый период в секундах;

  • base_rate_us — базовый период в микросекундах;

  • stop_time и stop_time_us — конечное время моделирования, если оно задано.

Базовый планировщик

Планировщик независимого выполнения должен повторять один базовый цикл:

  1. Запомнить время начала текущего базового шага.

  2. Вызвать все step-функции, которые должны выполняться на этом базовом шаге.

  3. Увеличить счетчик базовых шагов.

  4. Проверить, не достигнуто ли конечное время моделирования.

  5. Дождаться следующего базового шага или зафиксировать нарушение времени выполнения.

Главное правило: все step-функции проверяются относительно одного счетчика базового шага. Если у функции base_rate_scale == 1, она вызывается на каждом базовом шаге. Если base_rate_scale == 10, то функция вызывается на каждом десятом базовом шаге.

Пример минимального Jinja2-шаблон планировщика

#include "{{ model_name }}.h"

static uint64_t step_number = 0;
static const uint32_t base_rate_us = {{ base_rate_us }};

int main(void)
{
    platform_timer_init(base_rate_us);
    {{ init.cname }}();

    while (1) {
        uint32_t step_start_us = platform_time_us();

{% for step in steps %}
        if ((step_number % {{ step.base_rate_scale }}ULL) == 0ULL) {
            {{ step.cname }}();
        }
{% endfor %}

        step_number++;

{% if stop_time %}
        if ((step_number * base_rate_us) > {{ stop_time_us }}ULL) {
            {{ terminate.cname }}();
            break;
        }
{% endif %}

        platform_wait_until_next_tick(step_start_us, base_rate_us);
    }

    return 0;
}

Несколько step-функций

Модель может иметь одну или несколько step-функций. Несколько step-функций используется, когда в модели есть несколько значений шага расчета (sample time). Рантайм должен вызывать каждую функцию с ее периодом, но синхронизироваться по самому быстрому, базовому периоду.

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

Для таргета должно выполняться правило: вместо предположения о единственном шаге модели следует использовать список CodeGenInfo.steps. При наличии одного элемента выполняется один шаг; при наличии нескольких — рантайм последовательно обрабатывает все элементы, применяя к каждому его base_rate_scale.

Например:

  • базовый период равен 1 мс;

  • шаг A имеет base_rate_scale = 1 и вызывается каждую 1 мс;

  • шаг B имеет base_rate_scale = 10 и вызывается каждые 10 мс;

  • шаг C имеет base_rate_scale = 100 и вызывается каждые 100 мс.

Порядок вызова step-функций берите из CodeGenInfo.steps. В другом порядке функции вызывать нельзя.

Нельзя вызывать только первую step-функцию. Такой рантайм будет корректен только для самых простых моделей и приведет к ошибкам в моделях с несколькими шагами дискретизации.

Пример вызова нескольких шагов

for (;;) {
    if ((step_number % 1ULL) == 0ULL) {
        model_step_1ms();
    }

    if ((step_number % 10ULL) == 0ULL) {
        model_step_10ms();
    }

    if ((step_number % 100ULL) == 0ULL) {
        model_step_100ms();
    }

    step_number++;
    wait_until_next_1ms_tick();
}

Расширение словаря шаблона

CodeGenInfo содержит общие данные, которые нужны большинству рантайм-шаблонов. Но конкретной платформе часто нужны дополнительные параметры: режим таймера, имя платы, путь к файлу компоновки, частота CPU, размер стека, настройки RTOS task.

В этом случае можно расширить словарь, который передается в Jinja2-шаблон. Обычно берут codegen_info.model_dump(), добавляют платформенные поля и передают результат в render_template_to_file.

Пример добавления параметров блока EDM-Target в контекст шаблона

codegen_info = create_codegen_info(
    model_code.c_code.c_code_info,
    model_settings.is_ext_mode,
)

context = codegen_info.model_dump()
context.update(
    {
        "cpu_frequency_hz": self.target_block.cpu_frequency_hz,
        "timer_prescaler": self.target_block.timer_prescaler,
        "linker_script": self.target_block.linker_script,
    }
)

render_template_to_file(
    self.main_template,
    self.project_path / "src" / "main.c",
    context,
)

Пример использования дополнительных полей в шаблоне

#define CPU_FREQUENCY_HZ {{ cpu_frequency_hz }}UL
#define TIMER_PRESCALER  {{ timer_prescaler }}UL

extern const char linker_script_name[] = "{{ linker_script }}";

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

TET и нарушение времени шага

TET (task execution time) — это время выполнения одного базового шага рантайма. Обычно TET измеряют от начала базового шага до момента, когда все нужные step-функции и служебная обработка завершились.

Для независимого режима выполнения важно, чтобы TET был меньше или равен базовому периоду модели:

TET <= base_rate_us

Если TET будет больше базового периода, рантайм не успеет выполнить модель в заданном темпе. Такое состояние обычно называют overrun.

Нарушение TET делает работу модели некорректной:

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

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

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

  • внешние устройства получают команды позже, чем предполагает модель;

  • накопленная задержка может сделать результаты моделирования непредсказуемыми.

Для предотвращения нарушения TET рекомендуется:

  1. Измерять TET на каждом базовом шаге.

  2. Устанавливать флаг overrun_flag при TET > base_rate_us.

  3. Не выполнять пропущенные шаги пачкой, если платформа явно не рассчитана на catch-up.

  4. Документировать выбранную стратегию обработки: продолжение выполнения, остановка модели, пропуск шага или только диагностическая индикация.

Пример измерения TET

uint32_t step_start_us = platform_time_us();

run_due_model_steps();

uint32_t step_end_us = platform_time_us();
uint32_t tet_us = step_end_us - step_start_us;

if (tet_us > base_rate_us) {
    overrun_flag = true;
} else {
    overrun_flag = false;
    platform_sleep_us(base_rate_us - tet_us);
}

На bare-metal и RTOS-платформах измерение TET лучше делать с помощью таймера с достаточным разрешением. Если базовый период модели 1 мс, таймер с разрешением 1 мс уже слишком грубый для диагностики: он не покажет, насколько близко рантайм подходит к нарушению периода.

Время модели и timestamp

Рантайм должен различать физическое время платформы и модельное время.

Физическое время платформы берется из аппаратного таймера (hardware timer), системного тика RTOS (RTOS tick) или системных часов и нужно для ожидания следующего шага и измерения TET.

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

model_time = step_number * base_rate

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

Пример timestamp по модельному времени

uint32_t getCurrentTimestampUs(void)
{
    return step_number * base_rate_us;
}

double getCurrentTimestamp(void)
{
    return step_number * base_rate;
}

Что выбрать для новой платформы

Перед реализацией рантайм-шаблона зафиксируйте решения:

  • какой источник времени задает базовый период;

  • где выполняются step-функции: в основном цикле, в прерывании, в задаче RTOS или в отдельном процессе;

  • каким таймером измеряется TET;

  • что происходит при overrun;

  • как реализована остановка;

  • вызывается ли terminate при штатном завершении;

  • какие платформенные драйверы доступны из step-функций;

  • нужны ли дополнительные поля в контексте шаблона.

Для первой версии таргета обычно достаточно простого основного цикла или периодической задачи. Главное — корректно вызвать все step-функции с их base_rate_scale, выдержать базовый период и явно обработать нарушение TET.