feat(protocan-boot): добавь прошивку приборов по CAN
This commit is contained in:
@@ -28,6 +28,7 @@ templates/
|
|||||||
| [`c/can-sensor`](c/can-sensor) | передача 64-битных ROM датчиков парой CAN-кадров | `stdint.h` | отправка и приём CAN-кадра |
|
| [`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/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-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/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 в комплекте** |
|
| [`c/rtc-service`](c/rtc-service) | RTC с резервированным backup-томом | `stdint.h` | доступ к RTC и backup-памяти — **порт K1921VK028 в комплекте** |
|
||||||
|
|
||||||
|
|||||||
22
c/protocan-boot/CMakeLists.txt
Normal file
22
c/protocan-boot/CMakeLists.txt
Normal file
@@ -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()
|
||||||
158
c/protocan-boot/README.md
Normal file
158
c/protocan-boot/README.md
Normal file
@@ -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.
|
||||||
143
c/protocan-boot/include/pcan_boot.h
Normal file
143
c/protocan-boot/include/pcan_boot.h
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
#ifndef PCAN_BOOT_H
|
||||||
|
#define PCAN_BOOT_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#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 */
|
||||||
336
c/protocan-boot/src/pcan_boot.c
Normal file
336
c/protocan-boot/src/pcan_boot.c
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
#include "pcan_boot.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
267
c/protocan-boot/tests/test_pcan_boot.c
Normal file
267
c/protocan-boot/tests/test_pcan_boot.c
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
#include "pcan_boot.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user