Выполнение модели в независимом режиме
|
Страница в процессе разработки. |
В этой статье описан рантайм независимого режима выполнения: правильный вызов Си-функции модели на целевой платформе после генерации, сборки и загрузки. Интерактивное выполнение здесь не рассматривается.
Генерация Си-кода из модели
Перед тем как таргет начнет работу, Engee генерирует Си-код модели. Эти данные являются входными для таргета.
Сгенерированный Си-код модели содержит функции и структуры, которые реализуют вычисление модели:
-
функцию инициализации;
-
одну или несколько step-функций, которые нужно вызывать с заданным периодом;
-
функцию завершения;
-
исходные файлы модели;
-
метаданные о периодах дискретизации, сигналах, параметрах и точках входа модели.
Таргет не генерирует содержательную часть модели заново. Его задача — встроить уже сгенерированный Си-код в рантайм целевой платформы. Рантайм в этом контексте — это обвязка вокруг модели: точка входа, планировщик шагов, работа с таймером, драйверы, файлы сборки и, если нужен интерактивный режим, канал обмена данными.
Где задается выполнение
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— конечное время моделирования, если оно задано.
Базовый планировщик
Планировщик независимого выполнения должен повторять один базовый цикл:
-
Запомнить время начала текущего базового шага.
-
Вызвать все step-функции, которые должны выполняться на этом базовом шаге.
-
Увеличить счетчик базовых шагов.
-
Проверить, не достигнуто ли конечное время моделирования.
-
Дождаться следующего базового шага или зафиксировать нарушение времени выполнения.
Главное правило: все 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 рекомендуется:
-
Измерять TET на каждом базовом шаге.
-
Устанавливать флаг
overrun_flagприTET > base_rate_us. -
Не выполнять пропущенные шаги пачкой, если платформа явно не рассчитана на catch-up.
-
Документировать выбранную стратегию обработки: продолжение выполнения, остановка модели, пропуск шага или только диагностическая индикация.
Пример измерения 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.