feat(protocan-boot): добавь прошивку приборов по CAN

This commit is contained in:
2026-08-29 17:11:53 +03:00
parent a1d2d05f42
commit d3bb634fb1
6 changed files with 927 additions and 0 deletions

View 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
View 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.

View 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 */

View 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;
}

View 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;
}