# ProtoCAN Boot `protocan-boot` — переносимое C99-ядро адресной прошивки приборов по classic CAN 2.0B и 29-битному ProtoCAN ID. Оно реализует сессию обновления, два логических слота по 512 КиБ, последовательную запись 8-байтовых блоков, CRC32, проверку совместимости, продолжение по номеру следующего блока и безопасный выбор неактивного слота. Это не готовый загрузчик конкретного STM32: внутри нет HAL, регистров Flash, linker script, обработчиков прерываний, криптографии и перехода в приложение. Эти операции предоставляет проект через таблицу callbacks. ## Слои ```text приложение / CAN RX loop | v pcan_boot_process() — протокол и state machine | v pcan_boot_port_t — Flash, политика подписи, commit, reboot | v STM32 HAL / другой MCU / host-тест ``` ## Файлы | Файл | Назначение | Зависимости | |---|---|---| | `include/pcan_boot.h` | публичный API, команды и структуры | C99 stdint/stdbool | | `src/pcan_boot.c` | ProtoCAN ID, state machine и CRC32 | только публичный заголовок | | `tests/test_pcan_boot.c` | хостовые тесты с RAM вместо Flash | стандартная библиотека C | | `CMakeLists.txt` | сборка библиотеки и теста | CMake 3.13+ | Модуль не зависит от `protocan-transport`: разбор пяти полей 29-битного ID сделан переносимыми сдвигами, без непереносимых C-битовых полей. ## Карта `MsgType` | Значение | Имя | Назначение | |---:|---|---| | `0x9` | `BOOT_CONTROL` | команды и метаданные | | `0xA` | `BOOT_DATA_A` | блоки слота A | | `0xB` | `BOOT_DATA_B` | блоки слота B | | `0xC` | `BOOT_STATUS` | ответы и прогресс | | `0xD` | `BOOT_DISCOVERY` | идентификация | Для `BOOT_DATA_A/B` поле `MsgBody` — номер блока `0..65535`, а payload — 8 байт образа: ```text offset = MsgBody * 8 65536 * 8 = 512 КиБ на слот ``` Последний кадр дополняется `0xFF`; ядро передаёт во Flash только оставшиеся байты фактического образа и включает в CRC только их. ## Контракт порта ```c typedef struct { bool (*send)(void *, uint32_t can_id, const uint8_t *, uint8_t dlc); bool (*erase_slot)(void *, uint8_t slot); bool (*write_slot)(void *, uint8_t slot, uint32_t offset, const uint8_t *, uint8_t length); bool (*authorize)(void *, const pcan_boot_manifest_t *); bool (*verify_image)(void *, uint8_t slot, const pcan_boot_manifest_t *); bool (*set_pending_slot)(void *, uint8_t slot, const pcan_boot_manifest_t *); bool (*confirm_running_slot)(void *); void (*reboot)(void *); } pcan_boot_port_t; ``` Обязательны `send`, `erase_slot`, `write_slot` и `set_pending_slot`. `authorize` проверяет политику до стирания (тип, версия, anti-rollback). `verify_image` выполняет платформенную проверку подписанного контейнера после CRC32. Если callbacks отсутствуют, соответствующие дополнительные проверки пропускаются; для серийного изделия их следует реализовать. `set_pending_slot` должен атомарно сохранить boot metadata. До подтверждения нового приложения загрузчик сохраняет возможность отката. `confirm_running_slot` снимает pending-флаг после самопроверки приложения. ## Быстрый старт ```c pcan_boot_t boot; pcan_boot_config_t cfg = { .device_type = 3, .device_id = 5, .product_type = 0x1234, .hardware_revision = 2, .firmware_version = 0x01020000, .active_slot = 0, .ack_window = 16 }; pcan_boot_port_t port = { .send = can_send, .erase_slot = flash_erase_slot, .write_slot = flash_write_slot, .authorize = image_authorize, .verify_image = image_verify_signature, .set_pending_slot = boot_set_pending, .reboot = system_reboot }; pcan_boot_init(&boot, &cfg, &port, &board); /* Из CAN RX-задачи, не из длительного Flash-прерывания: */ pcan_boot_process(&boot, rx.ExtId, rx.Data, rx.DLC); ``` ## Последовательность обновления 1. `IDENTIFY` или discovery получает тип и версии прибора. 2. `ENTER_BOOT` открывает ненулевой `SessionID`. 3. `BEGIN_IMAGE` передаёт размер и CRC32. 4. `BEGIN_COMPAT` передаёт тип изделия, диапазон HW и версию FW. 5. Ядро выбирает неактивный слот и отвечает его номером. 6. `ERASE` вызывает порт стирания. 7. `BOOT_DATA_A` либо `BOOT_DATA_B` передаются строго по порядку. 8. `VERIFY` сравнивает CRC32 и вызывает проверку образа/подписи порта. 9. `COMMIT` атомарно помечает слот как pending. 10. После reboot приложение подтверждает запуск через `CONFIRM`. Все изменяющие Flash команды адресуются конкретному `DeviceType/DeviceID`. Ядро не принимает broadcast-запись и игнорирует кадры с чужим адресом или `Route=FROM_DEVICE`. ## State machine ```text IDLE -> ENTER_BOOT -> METADATA -> BEGIN_IMAGE + BEGIN_COMPAT -> READY_TO_ERASE -> ERASE -> RECEIVING -> VERIFY -> VERIFIED -> COMMIT -> REBOOT -> CONFIRM ``` При ошибке Flash, CRC, совместимости или подписи состояние переходит в `FAILED`. Новая сессия начинается командой `ENTER_BOOT`; `ABORT` очищает текущую сессию без изменения Flash. ## Сборка тестов ```sh cmake -S . -B build cmake --build build ctest --test-dir build --output-on-failure ``` ## Что остаётся проекту устройства - зарезервировать bootloader, slot A/B и metadata в linker script; - реализовать выравнивание и размеры страниц Flash; - не выполнять длительное стирание непосредственно в CAN IRQ; - атомарно хранить active/pending/confirmed и счётчик попыток запуска; - проверить вектор, границы и подпись образа; - включить watchdog и rollback при неподтверждённом запуске; - согласовать фильтры CAN для `MsgType=0x9..0xD`. Проект пока не подключён ни к одной конкретной плате; это самостоятельный шаблон для интеграции в будущие загрузчики приборов SET.