From d3bb634fb13e19dadaeb6e1f90084c1833035388 Mon Sep 17 00:00:00 2001 From: Andrey Kruchinkin Date: Sat, 29 Aug 2026 17:11:53 +0300 Subject: [PATCH] =?UTF-8?q?feat(protocan-boot):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D1=8C=20=D0=BF=D1=80=D0=BE=D1=88=D0=B8=D0=B2=D0=BA=D1=83?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B8=D0=B1=D0=BE=D1=80=D0=BE=D0=B2=20=D0=BF?= =?UTF-8?q?=D0=BE=20CAN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + c/protocan-boot/CMakeLists.txt | 22 ++ c/protocan-boot/README.md | 158 ++++++++++++ c/protocan-boot/include/pcan_boot.h | 143 +++++++++++ c/protocan-boot/src/pcan_boot.c | 336 +++++++++++++++++++++++++ c/protocan-boot/tests/test_pcan_boot.c | 267 ++++++++++++++++++++ 6 files changed, 927 insertions(+) create mode 100644 c/protocan-boot/CMakeLists.txt create mode 100644 c/protocan-boot/README.md create mode 100644 c/protocan-boot/include/pcan_boot.h create mode 100644 c/protocan-boot/src/pcan_boot.c create mode 100644 c/protocan-boot/tests/test_pcan_boot.c diff --git a/README.md b/README.md index 96baac4..e4003dc 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ templates/ | [`c/can-sensor`](c/can-sensor) | передача 64-битных ROM датчиков парой CAN-кадров | `stdint.h` | отправка и приём CAN-кадра | | [`c/ds18b20`](c/ds18b20) | термометры DS18B20 поверх программной 1-Wire | `stdint.h` | Init, DelayUs, Reset, WriteBit, ReadBit — **порт STM32F1 в комплекте** | | [`c/protocan-transport`](c/protocan-transport) | транспорт ProtoCAN: кадр, канал, CRC, общее адресное пространство, каталог GUI | `stdint.h` | запись в поток и запрос свободного места — **порт STM32F4 в комплекте** | +| [`c/protocan-boot`](c/protocan-boot) | адресная прошивка по ProtoCAN: A/B-слоты, сессия, CRC32, verify и rollback-контракт | C99 | CAN TX, erase/write Flash, boot metadata, проверка образа и reboot | | [`c/set-protocol`](c/set-protocol) | единый SET protocol v2: управление, real-time телеметрия и обновление прошивки через UART/CAN/USB/Ethernet | C99 | доставка целого stream/datagram-кадра, часы, backend карты и загрузчика | | [`c/rtc-service`](c/rtc-service) | RTC с резервированным backup-томом | `stdint.h` | доступ к RTC и backup-памяти — **порт K1921VK028 в комплекте** | diff --git a/c/protocan-boot/CMakeLists.txt b/c/protocan-boot/CMakeLists.txt new file mode 100644 index 0000000..d984ca5 --- /dev/null +++ b/c/protocan-boot/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.13) +project(protocan_boot C) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED ON) + +add_library(protocan_boot STATIC src/pcan_boot.c) +target_include_directories(protocan_boot PUBLIC include) + +if(MSVC) + target_compile_options(protocan_boot PRIVATE /W4) +else() + target_compile_options(protocan_boot PRIVATE -Wall -Wextra -Wpedantic) +endif() + +option(PCAN_BOOT_BUILD_TESTS "Собирать тесты ProtoCAN boot" ON) +if(PCAN_BOOT_BUILD_TESTS) + enable_testing() + add_executable(test_pcan_boot tests/test_pcan_boot.c) + target_link_libraries(test_pcan_boot PRIVATE protocan_boot) + add_test(NAME pcan_boot COMMAND test_pcan_boot) +endif() diff --git a/c/protocan-boot/README.md b/c/protocan-boot/README.md new file mode 100644 index 0000000..ab6c0d7 --- /dev/null +++ b/c/protocan-boot/README.md @@ -0,0 +1,158 @@ +# 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. diff --git a/c/protocan-boot/include/pcan_boot.h b/c/protocan-boot/include/pcan_boot.h new file mode 100644 index 0000000..7868ef0 --- /dev/null +++ b/c/protocan-boot/include/pcan_boot.h @@ -0,0 +1,143 @@ +#ifndef PCAN_BOOT_H +#define PCAN_BOOT_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define PCAN_BOOT_VERSION 1U +#define PCAN_BOOT_BLOCK_SIZE 8U +#define PCAN_BOOT_BLOCK_COUNT 65536UL +#define PCAN_BOOT_SLOT_SIZE (PCAN_BOOT_BLOCK_COUNT * PCAN_BOOT_BLOCK_SIZE) +#define PCAN_BOOT_SLOT_NONE 0xFFU + +#define PCAN_BOOT_MSG_CONTROL 0x9U +#define PCAN_BOOT_MSG_DATA_A 0xAU +#define PCAN_BOOT_MSG_DATA_B 0xBU +#define PCAN_BOOT_MSG_STATUS 0xCU +#define PCAN_BOOT_MSG_DISCOVERY 0xDU + +#define PCAN_BOOT_ROUTE_FROM_PM 0U +#define PCAN_BOOT_ROUTE_FROM_DEVICE 1U + +typedef enum { + PCAN_BOOT_CMD_IDENTIFY = 0x01U, + PCAN_BOOT_CMD_ENTER_BOOT = 0x02U, + PCAN_BOOT_CMD_BEGIN_IMAGE = 0x03U, + PCAN_BOOT_CMD_BEGIN_COMPAT = 0x04U, + PCAN_BOOT_CMD_ERASE = 0x05U, + PCAN_BOOT_CMD_VERIFY = 0x06U, + PCAN_BOOT_CMD_COMMIT = 0x07U, + PCAN_BOOT_CMD_CONFIRM = 0x08U, + PCAN_BOOT_CMD_REBOOT = 0x09U, + PCAN_BOOT_CMD_ABORT = 0x0AU, + PCAN_BOOT_CMD_QUERY_PROGRESS = 0x0BU +} pcan_boot_command_t; + +typedef enum { + PCAN_BOOT_STATUS_OK = 0x00U, + PCAN_BOOT_STATUS_BUSY = 0x01U, + PCAN_BOOT_STATUS_INVALID_COMMAND = 0x02U, + PCAN_BOOT_STATUS_WRONG_DEVICE = 0x03U, + PCAN_BOOT_STATUS_WRONG_HARDWARE = 0x04U, + PCAN_BOOT_STATUS_INVALID_SIZE = 0x05U, + PCAN_BOOT_STATUS_CRC_ERROR = 0x06U, + PCAN_BOOT_STATUS_FLASH_ERROR = 0x07U, + PCAN_BOOT_STATUS_SEQUENCE_ERROR = 0x08U, + PCAN_BOOT_STATUS_SIGNATURE_ERROR = 0x09U, + PCAN_BOOT_STATUS_SESSION_ERROR = 0x0AU, + PCAN_BOOT_STATUS_VOLTAGE_ERROR = 0x0BU, + PCAN_BOOT_STATUS_INVALID_STATE = 0x0CU +} pcan_boot_status_t; + +typedef enum { + PCAN_BOOT_STATE_IDLE = 0, + PCAN_BOOT_STATE_METADATA, + PCAN_BOOT_STATE_READY_TO_ERASE, + PCAN_BOOT_STATE_RECEIVING, + PCAN_BOOT_STATE_VERIFIED, + PCAN_BOOT_STATE_FAILED +} pcan_boot_state_t; + +typedef struct { + uint32_t image_size; + uint32_t image_crc32; + uint32_t firmware_version; + uint16_t product_type; + uint8_t hardware_revision_min; + uint8_t hardware_revision_max; +} pcan_boot_manifest_t; + +typedef struct { + uint8_t device_type; + uint8_t device_id; + uint16_t product_type; + uint8_t hardware_revision; + uint32_t firmware_version; + uint8_t active_slot; + uint8_t ack_window; /**< 1 — ACK каждого блока, 16 — ACK каждых 16 блоков. */ +} pcan_boot_config_t; + +typedef struct { + bool (*send)(void *user, uint32_t can_id, const uint8_t *data, uint8_t dlc); + bool (*erase_slot)(void *user, uint8_t slot); + bool (*write_slot)(void *user, uint8_t slot, uint32_t offset, + const uint8_t *data, uint8_t length); + bool (*authorize)(void *user, const pcan_boot_manifest_t *manifest); + bool (*verify_image)(void *user, uint8_t slot, + const pcan_boot_manifest_t *manifest); + bool (*set_pending_slot)(void *user, uint8_t slot, + const pcan_boot_manifest_t *manifest); + bool (*confirm_running_slot)(void *user); + void (*reboot)(void *user); +} pcan_boot_port_t; + +typedef struct { + pcan_boot_config_t config; + pcan_boot_port_t port; + void *port_user; + pcan_boot_manifest_t manifest; + pcan_boot_state_t state; + uint32_t running_crc32; + uint32_t bytes_received; + uint16_t next_block; + uint8_t session_id; + uint8_t target_slot; + uint8_t last_status; + bool have_image; + bool have_compat; +} pcan_boot_t; + +/** Инициализирует экземпляр. Все обязательные callbacks должны быть заданы. */ +bool pcan_boot_init(pcan_boot_t *boot, const pcan_boot_config_t *config, + const pcan_boot_port_t *port, void *port_user); + +/** Сбрасывает незавершённую сессию, не меняя конфигурацию и callbacks. */ +void pcan_boot_abort(pcan_boot_t *boot); + +/** + * Обрабатывает один Extended CAN-кадр. + * Возвращает true, если кадр принадлежал загрузочному сервису этого прибора. + */ +bool pcan_boot_process(pcan_boot_t *boot, uint32_t can_id, + const uint8_t *data, uint8_t dlc); + +/** Переносимые операции с 29-битным ProtoCAN ID. */ +uint32_t pcan_boot_make_id(uint8_t priority, uint8_t route, + uint8_t device_type, uint8_t device_id, + uint8_t msg_type, uint16_t msg_body); +uint8_t pcan_boot_id_route(uint32_t can_id); +uint8_t pcan_boot_id_device_type(uint32_t can_id); +uint8_t pcan_boot_id_device_id(uint32_t can_id); +uint8_t pcan_boot_id_msg_type(uint32_t can_id); +uint16_t pcan_boot_id_msg_body(uint32_t can_id); + +#ifdef __cplusplus +} +#endif + +#endif /* PCAN_BOOT_H */ diff --git a/c/protocan-boot/src/pcan_boot.c b/c/protocan-boot/src/pcan_boot.c new file mode 100644 index 0000000..9cefb52 --- /dev/null +++ b/c/protocan-boot/src/pcan_boot.c @@ -0,0 +1,336 @@ +#include "pcan_boot.h" + +#include + +#define PCAN_BOOT_ID_MASK 0x1FFFFFFFUL + +static uint16_t get_u16(const uint8_t *p) +{ + return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8)); +} + +static uint32_t get_u32(const uint8_t *p) +{ + return (uint32_t)p[0] + | ((uint32_t)p[1] << 8) + | ((uint32_t)p[2] << 16) + | ((uint32_t)p[3] << 24); +} + +static void put_u16(uint8_t *p, uint16_t value) +{ + p[0] = (uint8_t)value; + p[1] = (uint8_t)(value >> 8); +} + +static void put_u32(uint8_t *p, uint32_t value) +{ + p[0] = (uint8_t)value; + p[1] = (uint8_t)(value >> 8); + p[2] = (uint8_t)(value >> 16); + p[3] = (uint8_t)(value >> 24); +} + +static uint32_t crc32_update(uint32_t crc, const uint8_t *data, size_t length) +{ + size_t i; + crc = ~crc; + for (i = 0U; i < length; ++i) { + uint8_t bit; + crc ^= data[i]; + for (bit = 0U; bit < 8U; ++bit) { + crc = (crc >> 1) ^ ((crc & 1U) != 0U ? 0xEDB88320UL : 0U); + } + } + return ~crc; +} + +uint32_t pcan_boot_make_id(uint8_t priority, uint8_t route, + uint8_t device_type, uint8_t device_id, + uint8_t msg_type, uint16_t msg_body) +{ + return ((((uint32_t)priority & 1U) << 28) + | (((uint32_t)route & 1U) << 27) + | (((uint32_t)device_type & 7U) << 24) + | (((uint32_t)device_id & 15U) << 20) + | (((uint32_t)msg_type & 15U) << 16) + | msg_body) & PCAN_BOOT_ID_MASK; +} + +uint8_t pcan_boot_id_route(uint32_t can_id) { return (uint8_t)((can_id >> 27) & 1U); } +uint8_t pcan_boot_id_device_type(uint32_t can_id) { return (uint8_t)((can_id >> 24) & 7U); } +uint8_t pcan_boot_id_device_id(uint32_t can_id) { return (uint8_t)((can_id >> 20) & 15U); } +uint8_t pcan_boot_id_msg_type(uint32_t can_id) { return (uint8_t)((can_id >> 16) & 15U); } +uint16_t pcan_boot_id_msg_body(uint32_t can_id) { return (uint16_t)can_id; } + +static bool send_frame(pcan_boot_t *boot, uint8_t msg_type, uint16_t body, + const uint8_t *data, uint8_t dlc) +{ + uint32_t id = pcan_boot_make_id(1U, PCAN_BOOT_ROUTE_FROM_DEVICE, + boot->config.device_type, + boot->config.device_id, + msg_type, body); + return boot->port.send(boot->port_user, id, data, dlc); +} + +static bool send_status(pcan_boot_t *boot, uint8_t command, uint8_t status) +{ + uint8_t data[8]; + uint16_t body = (uint16_t)(((uint16_t)boot->session_id << 8) | command); + data[0] = status; + data[1] = boot->target_slot; + put_u16(&data[2], boot->next_block); + put_u32(&data[4], boot->running_crc32); + boot->last_status = status; + return send_frame(boot, PCAN_BOOT_MSG_STATUS, body, data, 8U); +} + +static bool send_identity(pcan_boot_t *boot) +{ + uint8_t data[8]; + put_u16(&data[0], boot->config.product_type); + data[2] = boot->config.hardware_revision; + data[3] = PCAN_BOOT_VERSION; + put_u32(&data[4], boot->config.firmware_version); + return send_frame(boot, PCAN_BOOT_MSG_DISCOVERY, 1U, data, 8U); +} + +void pcan_boot_abort(pcan_boot_t *boot) +{ + if (boot == NULL) { + return; + } + (void)memset(&boot->manifest, 0, sizeof(boot->manifest)); + boot->state = PCAN_BOOT_STATE_IDLE; + boot->running_crc32 = 0U; + boot->bytes_received = 0U; + boot->next_block = 0U; + boot->session_id = 0U; + boot->target_slot = PCAN_BOOT_SLOT_NONE; + boot->last_status = PCAN_BOOT_STATUS_OK; + boot->have_image = false; + boot->have_compat = false; +} + +bool pcan_boot_init(pcan_boot_t *boot, const pcan_boot_config_t *config, + const pcan_boot_port_t *port, void *port_user) +{ + if ((boot == NULL) || (config == NULL) || (port == NULL) + || (config->device_type > 7U) || (config->device_id > 15U) + || (config->active_slot > 1U) + || (port->send == NULL) || (port->erase_slot == NULL) + || (port->write_slot == NULL) || (port->set_pending_slot == NULL)) { + return false; + } + (void)memset(boot, 0, sizeof(*boot)); + boot->config = *config; + boot->port = *port; + boot->port_user = port_user; + if (boot->config.ack_window == 0U) { + boot->config.ack_window = 1U; + } + pcan_boot_abort(boot); + return true; +} + +static bool metadata_complete(pcan_boot_t *boot, uint8_t command) +{ + if (!boot->have_image || !boot->have_compat) { + boot->state = PCAN_BOOT_STATE_METADATA; + return send_status(boot, command, PCAN_BOOT_STATUS_OK); + } + if ((boot->manifest.image_size == 0U) + || (boot->manifest.image_size > PCAN_BOOT_SLOT_SIZE)) { + boot->state = PCAN_BOOT_STATE_FAILED; + return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_SIZE); + } + if ((boot->manifest.product_type != boot->config.product_type) + || (boot->config.hardware_revision < boot->manifest.hardware_revision_min) + || (boot->config.hardware_revision > boot->manifest.hardware_revision_max)) { + boot->state = PCAN_BOOT_STATE_FAILED; + return send_status(boot, command, PCAN_BOOT_STATUS_WRONG_HARDWARE); + } + if ((boot->port.authorize != NULL) + && !boot->port.authorize(boot->port_user, &boot->manifest)) { + boot->state = PCAN_BOOT_STATE_FAILED; + return send_status(boot, command, PCAN_BOOT_STATUS_SIGNATURE_ERROR); + } + boot->target_slot = (uint8_t)(boot->config.active_slot ^ 1U); + boot->state = PCAN_BOOT_STATE_READY_TO_ERASE; + return send_status(boot, command, PCAN_BOOT_STATUS_OK); +} + +static bool process_control(pcan_boot_t *boot, uint16_t body, + const uint8_t *data, uint8_t dlc) +{ + uint8_t session = (uint8_t)(body >> 8); + uint8_t command = (uint8_t)body; + + if (command == PCAN_BOOT_CMD_IDENTIFY) { + return send_identity(boot); + } + if (command == PCAN_BOOT_CMD_ENTER_BOOT) { + if (session == 0U) { + return send_status(boot, command, PCAN_BOOT_STATUS_SESSION_ERROR); + } + pcan_boot_abort(boot); + boot->session_id = session; + boot->state = PCAN_BOOT_STATE_METADATA; + return send_status(boot, command, PCAN_BOOT_STATUS_OK); + } + if ((session == 0U) || (session != boot->session_id)) { + return send_status(boot, command, PCAN_BOOT_STATUS_SESSION_ERROR); + } + + switch (command) { + case PCAN_BOOT_CMD_BEGIN_IMAGE: + if ((dlc != 8U) || (boot->state != PCAN_BOOT_STATE_METADATA)) { + return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE); + } + boot->manifest.image_size = get_u32(&data[0]); + boot->manifest.image_crc32 = get_u32(&data[4]); + boot->have_image = true; + return metadata_complete(boot, command); + + case PCAN_BOOT_CMD_BEGIN_COMPAT: + if ((dlc != 8U) || (boot->state != PCAN_BOOT_STATE_METADATA)) { + return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE); + } + boot->manifest.product_type = get_u16(&data[0]); + boot->manifest.hardware_revision_min = data[2]; + boot->manifest.hardware_revision_max = data[3]; + boot->manifest.firmware_version = get_u32(&data[4]); + boot->have_compat = true; + return metadata_complete(boot, command); + + case PCAN_BOOT_CMD_ERASE: + if (boot->state != PCAN_BOOT_STATE_READY_TO_ERASE) { + return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE); + } + if (!boot->port.erase_slot(boot->port_user, boot->target_slot)) { + boot->state = PCAN_BOOT_STATE_FAILED; + return send_status(boot, command, PCAN_BOOT_STATUS_FLASH_ERROR); + } + boot->running_crc32 = 0U; + boot->bytes_received = 0U; + boot->next_block = 0U; + boot->state = PCAN_BOOT_STATE_RECEIVING; + return send_status(boot, command, PCAN_BOOT_STATUS_OK); + + case PCAN_BOOT_CMD_VERIFY: + if ((boot->state != PCAN_BOOT_STATE_RECEIVING) + || (boot->bytes_received != boot->manifest.image_size)) { + return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE); + } + if (boot->running_crc32 != boot->manifest.image_crc32) { + boot->state = PCAN_BOOT_STATE_FAILED; + return send_status(boot, command, PCAN_BOOT_STATUS_CRC_ERROR); + } + if ((boot->port.verify_image != NULL) + && !boot->port.verify_image(boot->port_user, boot->target_slot, + &boot->manifest)) { + boot->state = PCAN_BOOT_STATE_FAILED; + return send_status(boot, command, PCAN_BOOT_STATUS_SIGNATURE_ERROR); + } + boot->state = PCAN_BOOT_STATE_VERIFIED; + return send_status(boot, command, PCAN_BOOT_STATUS_OK); + + case PCAN_BOOT_CMD_COMMIT: + if (boot->state != PCAN_BOOT_STATE_VERIFIED) { + return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE); + } + if (!boot->port.set_pending_slot(boot->port_user, boot->target_slot, + &boot->manifest)) { + return send_status(boot, command, PCAN_BOOT_STATUS_FLASH_ERROR); + } + return send_status(boot, command, PCAN_BOOT_STATUS_OK); + + case PCAN_BOOT_CMD_CONFIRM: + if ((boot->port.confirm_running_slot == NULL) + || !boot->port.confirm_running_slot(boot->port_user)) { + return send_status(boot, command, PCAN_BOOT_STATUS_FLASH_ERROR); + } + return send_status(boot, command, PCAN_BOOT_STATUS_OK); + + case PCAN_BOOT_CMD_QUERY_PROGRESS: + return send_status(boot, command, boot->last_status); + + case PCAN_BOOT_CMD_ABORT: + pcan_boot_abort(boot); + boot->session_id = session; + return send_status(boot, command, PCAN_BOOT_STATUS_OK); + + case PCAN_BOOT_CMD_REBOOT: + (void)send_status(boot, command, PCAN_BOOT_STATUS_OK); + if (boot->port.reboot != NULL) { + boot->port.reboot(boot->port_user); + } + return true; + + default: + return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_COMMAND); + } +} + +static bool process_data(pcan_boot_t *boot, uint8_t msg_type, uint16_t block, + const uint8_t *data, uint8_t dlc) +{ + uint8_t expected_type; + uint8_t write_length; + uint32_t remaining; + + if (boot->state != PCAN_BOOT_STATE_RECEIVING) { + return send_status(boot, 0U, PCAN_BOOT_STATUS_INVALID_STATE); + } + expected_type = boot->target_slot == 0U ? PCAN_BOOT_MSG_DATA_A + : PCAN_BOOT_MSG_DATA_B; + if (msg_type != expected_type) { + return send_status(boot, 0U, PCAN_BOOT_STATUS_INVALID_STATE); + } + if ((block != boot->next_block) || (dlc != PCAN_BOOT_BLOCK_SIZE)) { + return send_status(boot, 0U, PCAN_BOOT_STATUS_SEQUENCE_ERROR); + } + remaining = boot->manifest.image_size - boot->bytes_received; + write_length = remaining < PCAN_BOOT_BLOCK_SIZE ? (uint8_t)remaining + : PCAN_BOOT_BLOCK_SIZE; + if ((write_length == 0U) + || !boot->port.write_slot(boot->port_user, boot->target_slot, + boot->bytes_received, data, write_length)) { + boot->state = PCAN_BOOT_STATE_FAILED; + return send_status(boot, 0U, PCAN_BOOT_STATUS_FLASH_ERROR); + } + boot->running_crc32 = crc32_update(boot->running_crc32, data, write_length); + boot->bytes_received += write_length; + boot->next_block++; + + if ((boot->bytes_received == boot->manifest.image_size) + || ((boot->next_block % boot->config.ack_window) == 0U)) { + return send_status(boot, 0U, PCAN_BOOT_STATUS_OK); + } + return true; +} + +bool pcan_boot_process(pcan_boot_t *boot, uint32_t can_id, + const uint8_t *data, uint8_t dlc) +{ + uint8_t msg_type; + if ((boot == NULL) || (dlc > 8U) || ((dlc != 0U) && (data == NULL))) { + return false; + } + msg_type = pcan_boot_id_msg_type(can_id); + if ((msg_type < PCAN_BOOT_MSG_CONTROL) || (msg_type > PCAN_BOOT_MSG_DISCOVERY)) { + return false; + } + if ((pcan_boot_id_route(can_id) != PCAN_BOOT_ROUTE_FROM_PM) + || (pcan_boot_id_device_type(can_id) != boot->config.device_type) + || (pcan_boot_id_device_id(can_id) != boot->config.device_id)) { + return false; + } + if (msg_type == PCAN_BOOT_MSG_CONTROL) { + return process_control(boot, pcan_boot_id_msg_body(can_id), data, dlc); + } + if ((msg_type == PCAN_BOOT_MSG_DATA_A) || (msg_type == PCAN_BOOT_MSG_DATA_B)) { + return process_data(boot, msg_type, pcan_boot_id_msg_body(can_id), data, dlc); + } + return false; +} diff --git a/c/protocan-boot/tests/test_pcan_boot.c b/c/protocan-boot/tests/test_pcan_boot.c new file mode 100644 index 0000000..fc0aba7 --- /dev/null +++ b/c/protocan-boot/tests/test_pcan_boot.c @@ -0,0 +1,267 @@ +#include "pcan_boot.h" + +#include +#include +#include + +typedef struct { + uint8_t flash[2][64]; + uint32_t last_id; + uint8_t last_data[8]; + uint8_t last_dlc; + unsigned sends; + unsigned erases; + unsigned writes; + uint8_t pending_slot; + bool authorized; + bool verified; + bool rebooted; +} fake_t; + +static bool fake_send(void *user, uint32_t id, const uint8_t *data, uint8_t dlc) +{ + fake_t *f = (fake_t *)user; + f->last_id = id; + f->last_dlc = dlc; + (void)memset(f->last_data, 0, sizeof(f->last_data)); + if (dlc != 0U) { + (void)memcpy(f->last_data, data, dlc); + } + f->sends++; + return true; +} + +static bool fake_erase(void *user, uint8_t slot) +{ + fake_t *f = (fake_t *)user; + (void)memset(f->flash[slot], 0xFF, sizeof(f->flash[slot])); + f->erases++; + return true; +} + +static bool fake_write(void *user, uint8_t slot, uint32_t offset, + const uint8_t *data, uint8_t length) +{ + fake_t *f = (fake_t *)user; + if ((slot > 1U) || (offset + length > sizeof(f->flash[slot]))) { + return false; + } + (void)memcpy(&f->flash[slot][offset], data, length); + f->writes++; + return true; +} + +static bool fake_authorize(void *user, const pcan_boot_manifest_t *manifest) +{ + fake_t *f = (fake_t *)user; + (void)manifest; + return f->authorized; +} + +static bool fake_verify(void *user, uint8_t slot, + const pcan_boot_manifest_t *manifest) +{ + fake_t *f = (fake_t *)user; + (void)slot; + (void)manifest; + return f->verified; +} + +static bool fake_pending(void *user, uint8_t slot, + const pcan_boot_manifest_t *manifest) +{ + fake_t *f = (fake_t *)user; + (void)manifest; + f->pending_slot = slot; + return true; +} + +static bool fake_confirm(void *user) +{ + (void)user; + return true; +} + +static void fake_reboot(void *user) +{ + ((fake_t *)user)->rebooted = true; +} + +static uint32_t crc32(const uint8_t *data, size_t length) +{ + uint32_t crc = 0xFFFFFFFFUL; + size_t i; + for (i = 0U; i < length; ++i) { + uint8_t bit; + crc ^= data[i]; + for (bit = 0U; bit < 8U; ++bit) { + crc = (crc >> 1) ^ ((crc & 1U) != 0U ? 0xEDB88320UL : 0U); + } + } + return ~crc; +} + +static void put_u16(uint8_t *p, uint16_t v) +{ + p[0] = (uint8_t)v; + p[1] = (uint8_t)(v >> 8); +} + +static void put_u32(uint8_t *p, uint32_t v) +{ + p[0] = (uint8_t)v; + p[1] = (uint8_t)(v >> 8); + p[2] = (uint8_t)(v >> 16); + p[3] = (uint8_t)(v >> 24); +} + +static uint32_t request_id(uint8_t type, uint16_t body) +{ + return pcan_boot_make_id(1U, PCAN_BOOT_ROUTE_FROM_PM, 3U, 5U, type, body); +} + +static bool command(pcan_boot_t *boot, uint8_t session, uint8_t cmd, + const uint8_t *data, uint8_t dlc) +{ + uint16_t body = (uint16_t)(((uint16_t)session << 8) | cmd); + return pcan_boot_process(boot, request_id(PCAN_BOOT_MSG_CONTROL, body), data, dlc); +} + +static void setup(pcan_boot_t *boot, fake_t *fake) +{ + pcan_boot_config_t config; + pcan_boot_port_t port; + (void)memset(fake, 0, sizeof(*fake)); + (void)memset(&config, 0, sizeof(config)); + (void)memset(&port, 0, sizeof(port)); + fake->authorized = true; + fake->verified = true; + fake->pending_slot = PCAN_BOOT_SLOT_NONE; + config.device_type = 3U; + config.device_id = 5U; + config.product_type = 0x1234U; + config.hardware_revision = 2U; + config.firmware_version = 0x01020304UL; + config.active_slot = 0U; + config.ack_window = 2U; + port.send = fake_send; + port.erase_slot = fake_erase; + port.write_slot = fake_write; + port.authorize = fake_authorize; + port.verify_image = fake_verify; + port.set_pending_slot = fake_pending; + port.confirm_running_slot = fake_confirm; + port.reboot = fake_reboot; + assert(pcan_boot_init(boot, &config, &port, fake)); +} + +static void begin_update(pcan_boot_t *boot, const uint8_t *image, uint32_t size) +{ + uint8_t data[8]; + assert(command(boot, 7U, PCAN_BOOT_CMD_ENTER_BOOT, NULL, 0U)); + put_u32(&data[0], size); + put_u32(&data[4], crc32(image, size)); + assert(command(boot, 7U, PCAN_BOOT_CMD_BEGIN_IMAGE, data, 8U)); + put_u16(&data[0], 0x1234U); + data[2] = 1U; + data[3] = 3U; + put_u32(&data[4], 0x02000000UL); + assert(command(boot, 7U, PCAN_BOOT_CMD_BEGIN_COMPAT, data, 8U)); +} + +static void test_id_layout(void) +{ + uint32_t id = pcan_boot_make_id(1U, 0U, 7U, 15U, 0xAU, 0x1234U); + assert(id == 0x17FA1234UL); + assert(pcan_boot_id_device_type(id) == 7U); + assert(pcan_boot_id_device_id(id) == 15U); + assert(pcan_boot_id_msg_type(id) == 0xAU); + assert(pcan_boot_id_msg_body(id) == 0x1234U); +} + +static void test_crc32_reference(void) +{ + static const uint8_t reference[] = "123456789"; + assert(crc32(reference, sizeof(reference) - 1U) == 0xCBF43926UL); +} + +static void test_complete_update(void) +{ + static const uint8_t image[13] = { + 0x10U, 0x11U, 0x12U, 0x13U, 0x14U, 0x15U, 0x16U, 0x17U, + 0x20U, 0x21U, 0x22U, 0x23U, 0x24U + }; + uint8_t block[8]; + pcan_boot_t boot; + fake_t fake; + setup(&boot, &fake); + begin_update(&boot, image, sizeof(image)); + assert(boot.state == PCAN_BOOT_STATE_READY_TO_ERASE); + assert(boot.target_slot == 1U); + assert(command(&boot, 7U, PCAN_BOOT_CMD_ERASE, NULL, 0U)); + assert(fake.erases == 1U); + + assert(pcan_boot_process(&boot, request_id(PCAN_BOOT_MSG_DATA_B, 0U), image, 8U)); + (void)memset(block, 0xFF, sizeof(block)); + (void)memcpy(block, &image[8], 5U); + assert(pcan_boot_process(&boot, request_id(PCAN_BOOT_MSG_DATA_B, 1U), block, 8U)); + assert(boot.bytes_received == sizeof(image)); + assert(boot.next_block == 2U); + assert(memcmp(fake.flash[1], image, sizeof(image)) == 0); + + assert(command(&boot, 7U, PCAN_BOOT_CMD_VERIFY, NULL, 0U)); + assert(boot.state == PCAN_BOOT_STATE_VERIFIED); + assert(command(&boot, 7U, PCAN_BOOT_CMD_COMMIT, NULL, 0U)); + assert(fake.pending_slot == 1U); + assert(command(&boot, 7U, PCAN_BOOT_CMD_REBOOT, NULL, 0U)); + assert(fake.rebooted); +} + +static void test_rejects_wrong_address_and_sequence(void) +{ + uint8_t image[8] = {0U}; + pcan_boot_t boot; + fake_t fake; + uint32_t other = pcan_boot_make_id(1U, 0U, 3U, 6U, + PCAN_BOOT_MSG_CONTROL, + (uint16_t)((7U << 8) | PCAN_BOOT_CMD_ENTER_BOOT)); + setup(&boot, &fake); + assert(!pcan_boot_process(&boot, other, NULL, 0U)); + begin_update(&boot, image, sizeof(image)); + assert(command(&boot, 7U, PCAN_BOOT_CMD_ERASE, NULL, 0U)); + assert(pcan_boot_process(&boot, request_id(PCAN_BOOT_MSG_DATA_B, 1U), image, 8U)); + assert(fake.last_data[0] == PCAN_BOOT_STATUS_SEQUENCE_ERROR); + assert(fake.writes == 0U); +} + +static void test_rejects_wrong_session_and_compatibility(void) +{ + uint8_t data[8] = {0U}; + pcan_boot_t boot; + fake_t fake; + setup(&boot, &fake); + assert(command(&boot, 7U, PCAN_BOOT_CMD_ENTER_BOOT, NULL, 0U)); + assert(command(&boot, 8U, PCAN_BOOT_CMD_ERASE, NULL, 0U)); + assert(fake.last_data[0] == PCAN_BOOT_STATUS_SESSION_ERROR); + put_u32(&data[0], 8U); + put_u32(&data[4], 0U); + assert(command(&boot, 7U, PCAN_BOOT_CMD_BEGIN_IMAGE, data, 8U)); + put_u16(&data[0], 0x9999U); + data[2] = 1U; + data[3] = 3U; + put_u32(&data[4], 1U); + assert(command(&boot, 7U, PCAN_BOOT_CMD_BEGIN_COMPAT, data, 8U)); + assert(fake.last_data[0] == PCAN_BOOT_STATUS_WRONG_HARDWARE); + assert(boot.state == PCAN_BOOT_STATE_FAILED); +} + +int main(void) +{ + test_id_layout(); + test_crc32_reference(); + test_complete_update(); + test_rejects_wrong_address_and_sequence(); + test_rejects_wrong_session_and_compatibility(); + puts("pcan_boot tests: OK"); + return 0; +}