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

Выполнение модели в интерактивном режиме

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

Интерактивный режим добавляет к обычному запуску модели двусторонний канал связи между Engee и рантаймом на целевой платформе. Через этот канал Engee получает значения выбранных сигналов и отправляет команды изменения настраиваемых параметров без повторной сборки и загрузки модели.

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

Основные понятия

Логируемая точка — это сигнал или выходной порт модели, который пользователь выбрал для наблюдения во время выполнения. При генерации Си-кода Engee сохраняет информацию о таких точках в метаданных и формирует Си-данные, из которых рантайм может читать значения. В Python-представлении кодогенерации эти данные отражаются в CodeInfo и в упрощенном для шаблонов CodeGenInfo.logging_signals.

Настраиваемый параметр — это параметр модели, значение которого можно изменить после запуска. В сгенерированном Си-коде такие параметры обычно попадают в отдельную структуру параметров модели. В CodeGenInfo эта часть описана через tunable_params. Для интерактивного режима важно, чтобы соответствующая Си-структура была доступна в памяти целевого приложения и не была удалена оптимизатором.

Интерфейс целевой платформы — это физический или программный канал, по которому Engee обменивается данными с моделью. Это может быть UART, TCP, UDP, USB CDC, отладочный канал или другой транспорт. Сам факт наличия логируемых точек в модели не передает данные в Engee: нужен канал связи во время выполнения на целевой платформе.

ELF-файл — это собранный артефакт с символами, по которому Python-код таргета находит адреса сигналов и параметров в памяти целевого приложения. Для интерактивного режима итоговый .elf должен быть доступен на стороне клиента и из него не должна быть удалена информация таким образом, чтобы исчезли нужные символы.

Протокол XCP

XCP (Universal Measurement and Calibration Protocol) — это стандарт ASAM MCD-1 XCP для доступа к внутренним данным приложения во время его работы. Протокол обычно используют для измерения сигналов, калибровки параметров и отладки встраиваемых приложений без повторной сборки и перепрошивки после каждого изменения параметра.

В этом руководстве Engee выступает как XCP master, а рантайм модели на целевой платформе содержит XCP slave. Master управляет сеансом: подключается к slave, настраивает передачу сигналов, отправляет команды чтения и записи. Slave находится внутри целевого приложения, выполняет команды master и читает или изменяет память модели.

Официальное описание стандарта доступно по ссылке ASAM MCD-1 XCP.

Уровни XCP: протокол и транспорт

XCP разделяет логический протокол и транспортный уровень.

Протокольный уровень описывает, какие команды существуют и что они означают: подключение, чтение памяти, запись памяти, настройка DAQ, запуск и остановку передачи. Этот уровень не зависит от того, идет обмен по UART, TCP, UDP, CAN, USB или другому каналу.

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

Это означает, что нужно определить:

  1. Какой XCP slave будет встроен в рантайм модели.

  2. Через какой транспорт Engee будет обмениваться с этим slave.

В составе исполняемого файла протокольная часть XCP slave уже поставляется вместе с инфраструктурой таргета. Для новой платформы нужно выбрать или реализовать только транспорт и платформенный слой.

Master, slave и сеанс связи

XCP-сеанс начинается с подключения master к slave. После подключения master получает базовые возможности slave: поддерживаемый транспорт, размеры пакетов, доступные ресурсы и ограничения. Затем master может выполнять команды.

Для интерактивного режима Engee важны три группы команд:

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

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

  • команды DAQ: настроить списки измеряемых данных и запустить их регулярную передачу.

В классическом XCP slave не знает заранее, какие именно сигналы хочет наблюдать пользователь. Он предоставляет универсальный доступ к памяти, а master настраивает, какие адреса и размеры данных нужно читать. В Engee эти адреса берутся из .elf и метаданных кодогенерации.

Адресация данных

XCP работает с объектами целевого приложения через адреса памяти. Для Engee такими объектами являются:

  • значения логируемых сигналов;

  • настраиваемые параметры модели;

  • служебные структуры, нужные рантайм интерактивного режима выполнения.

Адрес, который видит master, должен быть корректно преобразован в указатель внутри slave. За это отвечает платформенный макрос XCP_ADDRESS_GET. На микроконтроллере без виртуальной памяти адрес часто можно использовать напрямую. На хост-платформах с PIE/ASLR адреса из .elf могут быть смещениями, поэтому платформенный слой должен учитывать базовый адрес загруженного образа.

Из-за этого для интерактивного режима выполнения важен .elf с символами: Python-код таргета должен найти адреса данных, а XCP slave должен уметь по этим адресам обратиться к памяти целевого приложения.

DAQ, ODT и события

DAQ (data acquisition) — это механизм XCP для потоковой передачи данных от slave к master. Master заранее настраивает состав отправляемых данных, а slave передает их при наступлении события.

DAQ-настройка строится из нескольких уровней:

  • DAQ list — список данных, который активируется по событию;

  • ODT (object descriptor table) — группа элементов, которые помещаются в один DTO-пакет;

  • ODT entry — один измеряемый объект: адрес, размер и дополнительные атрибуты;

  • event channel — событие рантайма, по которому slave должен собрать и отправить данные.

В модели Engee таким событием обычно является завершение шага модели. Рантайм вызывает rtExtModeUpload, и XCP slave отправляет данные.

При конфигурации XCP важно учитывать размеры:

  • сколько сигналов пользователь может записывать одновременно;

  • сколько байт помещается в один DTO-пакет;

  • сколько ODT-записей разрешено в одном ODT;

  • сколько DAQ-списков доступно на платформе;

  • нужен ли timestamp в DAQ-пакете и сколько байт он занимает.

Если лимиты слишком маленькие, часть сигналов не удастся включить в поток. Если лимиты слишком большие, XCP slave будет занимать больше RAM, что критично для маленьких MCU.

Запись параметров

Изменение параметра в интерактивном режиме — это запись нового значения в память целевого приложения. Master выбирает адрес параметра и отправляет данные записи. Slave проверяет команду и записывает байты в память.

Для корректной работы нужно, чтобы:

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

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

  • запись не происходила одновременно с небезопасным чтением из другого потока или обработчика прерываний (ISR);

  • рантайм модели был готов использовать новое значение на следующих шагах.

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

Конфигурация таргета

Для настройки XCP нужно определить:

  1. Транспорт: UART, TCP, UDP или другой канал.

  2. Лимиты DAQ: количество списков, ODT и ODT entries.

  3. Способ преобразования XCP-адреса в указатель.

  4. Модель синхронизации: нужны ли мьютексы, где возможны потоки или обработчики прерываний (thread/ISR).

Общий поток данных

Интерактивный режим выполнения работает как цепочка из нескольких частей:

  1. Engee генерирует Си-код модели и метаданные о сигналах, параметрах и точках входа.

  2. Таргет генерирует рантайм-шаблон интерактивного режима.

  3. В проект добавляются XCP slave и платформенный транспорт.

  4. Тулчейн собирает артефакт и .elf с символами модели.

  5. Python-код таргета разбирает .elf и получает адреса логируемых точек и параметров.

  6. Engee подключается к XCP slave через выбранный интерфейс.

  7. Планировщик модели выполняет step-функции и вызывает DAQ-события.

  8. XCP slave читает значения из памяти и передает их в Engee.

  9. При изменении параметра Engee отправляет команду записи, а XCP slave записывает новое значение в память модели.

Python-код таргета

Таргет с интерактивным режимом обычно наследуется от BaseTarget и XCPTarget. BaseTarget задает методы для управления работой модели, а XCPTarget предоставляет Python-код обмена: настройку конечных точек, создание потоковых очередей, запуск чтения данных и изменение параметров.

Пример базового наследования

from targets.base_target import BaseTarget
from targets.xcp_target.xcp_target import XCPTarget


class MyTarget(BaseTarget, XCPTarget):
    def __init__(self) -> None:
        XCPTarget.__init__(self)
        ...

В generate_executable_code нужно подготовить проект интерактивного режима:

  • проверить model_settings.is_ext_mode;

  • выбрать рантайм-шаблон с вызовами rtExtMode*;

  • сохранить исходники модели;

  • добавить общие исходники XCP slave через get_xcp_slave_src_list;

  • добавить платформенные файлы транспорта;

  • добавить нужные include paths и compile definitions;

  • обеспечить сборку .elf с символами.

В start_model нужно подготовить связь Engee с уже собранной и запущенной моделью:

  1. Настроить endpoint через XCPTarget.set_endpoint.

  2. Найти итоговый .elf.

  3. Получить данные о сигналах и параметрах через get_engee_elf_info.

  4. Вызвать XCPTarget.setup_streaming.

  5. Вернуть TargetResponse с data_stream_q, controll_stream_q и discovered_signals.

start_stream обычно вызывает XCPTarget._start_stream, change_param вызывает set_param, а stop_model останавливает поток через XCPTarget.stop_stream.

Пример схемы методов Python-кода таргета

from targets.xcp_target.engee_elf_info import get_engee_elf_info
from targets.xcp_target.xcp_target import get_xcp_slave_src_list


async def start_model(self, model: EngeeModel) -> TargetResponse:
    XCPTarget.set_endpoint(self, interface="uart", url=self.target_block.com_port)

    elf_data = get_engee_elf_info(
        elf_path=self.elf_path,
        code_info=self.code_info,
    )

    data_q, control_q, discovered = await XCPTarget.setup_streaming(
        self,
        elf_data,
        self.model_settings,
    )

    return TargetResponse(
        detail="start_model",
        data_stream_q=data_q,
        controll_stream_q=control_q,
        discovered_signals=discovered,
    )


async def start_stream(self, simulation_uuid: str) -> Result:
    await XCPTarget._start_stream(self, simulation_uuid)
    return Result.success()


async def change_param(self, block_key, param, data) -> TargetResponse:
    await self.set_param(block_key, param, data)
    return TargetResponse(detail="Param is changed")


async def stop_model(self) -> TargetResponse:
    await XCPTarget.stop_stream(self)
    return TargetResponse(detail="stop_model")

Рантайм-шаблон интерактивного режима выполнения

Рантайм интерактивного режима выполнения строится поверх планировщика независимого режима, но добавляет вызовы API адаптера интерактивного режима (external-mode adapter) из ext_work.h.

Минимальный порядок:

  1. Инициализировать модель.

  2. Вызвать rtExtModeCheckInit.

  3. Дождаться команды старта через rtExtModeWaitForStartPkt.

  4. В цикле выполнять step-функции модели.

  5. После шага вызвать rtExtModeUpload, чтобы опубликовать DAQ-событие.

  6. На каждом шаге вызвать rtExtModeOneStep, чтобы обработать команды master.

  7. При запросе остановки вызвать terminate.

  8. При штатном завершении вызвать rtExtModeShutdown, если платформа поддерживает корректное завершение (clean shutdown).

Важно, чтобы rtExtModeOneStep вызывался регулярно. Если планировщик надолго блокируется в драйвере, ожидании периферии или sleep-функции, Engee будет хуже получать данные и команды изменения параметров.

Пример схемы рантайм-цикла

#include "ext_work.h"

RTWExtModeInfo ext_mode_info;
boolean_T stop_requested = false;

model_initialize();

rtExtModeCheckInit(1);
rtExtModeWaitForStartPkt(&ext_mode_info, 1, &stop_requested);
rtERTExtModeStartMsg();

while (!stop_requested) {
    run_due_model_steps();
    rtExtModeUpload(0, getCurrentTimestamp());
    rtExtModeOneStep(&ext_mode_info, 1, &stop_requested);
    wait_until_next_tick();
}

model_terminate();
rtExtModeShutdown(1);

XCP slave в Engee.Интеграции

Общий XCP slave находится в составе поставляемого исполняемого файла engee-device-manager. Python-код таргета добавляет его в проект через get_xcp_slave_src_list, а платформенный слой добавляет отдельно.

Структура XCP slave:

  • include/ext_work.h — API, который вызывает рантайм-шаблон модели;

  • src/ext_work.c — адаптер между rtExtMode* и XCP slave;

  • src/xcp_daq.c, src/xcp_daq.h — обработка XCP-команд, DAQ-списков, чтения сигналов и записи параметров;

  • src/xcp_frame/xcp_frame_uart.c — кадрирование для UART;

  • src/xcp_frame/xcp_frame_tcp.c — кадрирование для TCP;

  • src/xcp_frame/xcp_frame_udp.c — кадрирование для UDP;

  • include/xcp_config.h — compile-time конфигурация XCP slave;

  • include/rtiostream.h — интерфейс транспортного ввода-вывода.

Основные функции rtExtMode*:

  • rtExtModeCheckInit — инициализирует XCP slave и открывает транспорт;

  • rtExtModeWaitForStartPkt — ждет команду старта от master;

  • rtExtModeUpload — публикует DAQ-событие;

  • rtExtModeOneStep — выполняет один неблокирующий проход обработки команд;

  • rtExtModePauseIfNeeded — обслуживает команды в состоянии паузы;

  • rtExtModeShutdown — сбрасывает состояние slave и закрывает рантайм;

  • rtExtModeParseArgs и rtERTExtModeStartMsg оставлены для совместимости с генерируемым API интерактивного режима (external-mode).

Определения компиляции для XCP

В проекте должен быть выбран транспорт:

-DXCP_UART_TRANSPORT

или:

-DXCP_TCP_TRANSPORT

или:

-DXCP_UDP_TRANSPORT

Для встроенной платформы обычно добавляют:

-DXCP_CUSTOM_PLATFORM

Для хост-платформы x86 можно использовать готовый платформенный слой:

-DXCP_PLATFORM_X86

xcp_config.h также содержит настраиваемые размеры:

  • XCP_MAX_ODT_ENTRIES_COUNT — максимум элементов в одном ODT;

  • XCP_MAX_DAQ_LISTS — максимум DAQ-списков;

  • XCP_DAQ_TIMESTAMP_SIZE — размер timestamp в DAQ-пакете;

  • XCP_DAQ_DATA_SIZE — максимум полезных DAQ-данных в одном DTO;

  • XCP_MAX_SHORT_DOWNLOAD_DATA_LEN — максимум данных для записи параметра одной командой;

  • XCP_MAX_CTO_LEN — размер пакета команд/ответов CTO;

  • XCP_MAX_DTO_LEN — размер пакета передачи данных DTO.

Для маленьких MCU эти значения можно уменьшать, но после этого нужно проверить, что выбранные логируемые точки помещаются в DAQ-лимиты.

Пользовательская платформа

Если сборка XCP slave выполняется не на готовом x86-слое, нужно включить -DXCP_CUSTOM_PLATFORM и предоставить xcp_platform_custom.h.

Минимальный набор требований к платформе включает реализацию следующих макросов и директив:

  • XCP_PRINTF — вывод диагностики или пустой макрос;

  • XCP_MUTEX_DEFINE;

  • XCP_MUTEX_INIT;

  • XCP_MUTEX_LOCK;

  • XCP_MUTEX_UNLOCK;

  • XCP_ADDRESS_GET;

  • XCP_SLEEP;

  • макросы упаковки/выравнивания: XCP_PRAGMA_PACK_BEGIN, XCP_PRAGMA_PACK_END, XCP_ATTRIBUTE_ALIGNED, XCP_ATTRIBUTE_PACKED.

Пример базового xcp_platform_custom.h

#ifndef XCP_PLATFORM_CUSTOM_H
#define XCP_PLATFORM_CUSTOM_H

#include <stdint.h>
#include "rtwtypes.h"

#define XCP_PRINTF(...)

#define XCP_MUTEX_DEFINE(lock)
#define XCP_MUTEX_INIT(lock)
#define XCP_MUTEX_LOCK(lock)
#define XCP_MUTEX_UNLOCK(lock)

#define XCP_ADDRESS_GET(addressExtension, address) \
    ((uint8_T *)((uintptr_t)(address)))

#define XCP_SLEEP(seconds, microseconds)  platform_sleep(seconds, microseconds)

#define PRAGMA(n) _Pragma(#n)
#define XCP_PRAGMA_PACK_BEGIN(n) PRAGMA(pack(push, n))
#define XCP_PRAGMA_PACK_END()    PRAGMA(pack(pop))
#define XCP_ATTRIBUTE_ALIGNED(n)
#define XCP_ATTRIBUTE_PACKED

#endif

XCP_ADDRESS_GET — критически важный макрос. Он преобразует адрес из XCP-команды в реальный указатель в адресном пространстве целевого приложения.

Для микроконтроллеров без виртуальной памяти обычно достаточно прямого преобразования адреса в указатель. Для хост-приложений с PIE/ASLR адрес из ELF-файла может быть задан как смещение (offset), и к нему нужно прибавлять базовый адрес загруженного образа. Это уже реализовано в ritm-xcpslave/platform_x86.

Мьютексы зависят от модели выполнения:

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

  • если ввод-вывод, DAQ или планировщик работают из разных задач, потоков или обработчиков прерываний, нужна реальная блокировка;

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

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

Транспорт должен реализовать rtIOStream API:

int rtIOStreamOpen(int argc, void *argv[]);
int rtIOStreamSend(int streamID, const void *src, size_t size, size_t *sizeSent);
int rtIOStreamRecv(int streamID, void *dst, size_t size, size_t *sizeRecvd);
int rtIOStreamClose(int streamID);

Правила для rtIOStreamSend:

  • возвращайте RTIOSTREAM_NO_ERROR, если транспорт исправен;

  • если сейчас нельзя отправить данные, установите *sizeSent = 0;

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

  • при реальной ошибке транспорта возвращайте RTIOSTREAM_ERROR.

Правила для rtIOStreamRecv:

  • функция должна быть неблокирующей или иметь короткий временной лимит;

  • если данных нет, установите *sizeRecvd = 0 и верните RTIOSTREAM_NO_ERROR;

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

  • при ошибке транспорта верните RTIOSTREAM_ERROR.

Для UART текущий формат кадра использует стартовый байт 0xFF, длину данных (payload), данные и XOR CRC. UART-транспорт должен передавать байты без эха, текстовой обработки и преобразования символов конца строки.

Что скопировать в проект

Для проекта интерактивного выполнения нужны:

  • ritm-xcpslave/include/*.h;

  • ritm-xcpslave/src/ext_work.c;

  • ritm-xcpslave/src/xcp_daq.c;

  • ritm-xcpslave/src/xcp_daq.h;

  • ritm-xcpslave/src/xcp_frame/xcp_frame.h;

  • один файл обработчика кадров: xcp_frame_uart.c, xcp_frame_tcp.c или xcp_frame_udp.c;

  • платформенный xcp_platform_custom.h или готовый platform_x86;

  • реализация rtIOStreamOpen, rtIOStreamSend, rtIOStreamRecv, rtIOStreamClose;

  • при необходимости свои rtwtypes.h и ext_mode_types.h.

ELF и символы

Для интерактивного выполнения Engee должен сопоставить логируемые точки и параметры модели с адресами в памяти целевого приложения. Для этого Python-код таргета использует .elf и get_engee_elf_info.

На практике необходимо обеспечить следующее:

  • итоговый .elf должен быть доступен на стороне клиента;

  • символы сигналов и параметров не должны быть удалены посредством strip;

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

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

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

Проверочный список

  1. В модели есть логируемая точка и хотя бы один проверочный сигнал.

  2. В проект добавлен XCP slave.

  3. Выбран один транспорт: UART, TCP или UDP.

  4. Определена компиляция транспорта и платформы.

  5. Реализован или выбран xcp_platform_custom.h.

  6. Реализован rtIOStream для выбранного интерфейса.

  7. Рантайм-шаблон вызывает rtExtModeCheckInit, rtExtModeWaitForStartPkt, rtExtModeUpload, rtExtModeOneStep и rtExtModeShutdown.

  8. .elf содержит нужные символы и доступен Python-таргету.

  9. XCPTarget.setup_streaming возвращает очереди и список найденных сигналов.

  10. На демо-модели проверены старт, чтение сигнала, изменение параметра и останов.