Compare commits
6 Commits
a1d2d05f42
...
4b32cda46e
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b32cda46e | |||
| 217073982f | |||
| 94c991380b | |||
| 5a8724da6d | |||
| d507b84a76 | |||
| d3bb634fb1 |
100
NEW_PROJECT.md
Normal file
100
NEW_PROJECT.md
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# Новый проект на переносимых библиотеках
|
||||||
|
|
||||||
|
Этот репозиторий содержит два разных типа кода:
|
||||||
|
|
||||||
|
1. `c/<library>/include` и `c/<library>/src` — переносимое ядро;
|
||||||
|
2. `c/<library>/ports/<family>` — небольшой аппаратный порт конкретного
|
||||||
|
семейства микроконтроллеров.
|
||||||
|
|
||||||
|
Привязка выводов, частоты шин и выбранной периферии принадлежит плате и всегда
|
||||||
|
остаётся в проекте в `app_config.h` либо в узком конфигурационном заголовке
|
||||||
|
библиотеки, например `onewire_config.h`.
|
||||||
|
|
||||||
|
## Рекомендуемая структура прошивки
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-device/
|
||||||
|
app/ логика изделия
|
||||||
|
include/
|
||||||
|
src/
|
||||||
|
board/ одна конкретная плата
|
||||||
|
app_config.h выводы, частоты, адреса, возможности платы
|
||||||
|
port/ связь библиотек с SDK выбранного МК
|
||||||
|
display_port.c
|
||||||
|
keypad_port.c
|
||||||
|
eeprom_port.c
|
||||||
|
can_port.c
|
||||||
|
vendor/ CMSIS, startup, linker script или Cube-generated код
|
||||||
|
lib/templates/ git submodule этого репозитория
|
||||||
|
```
|
||||||
|
|
||||||
|
Не следует помещать `stm32xxxx.h`, `HAL_*`, регистры, IRQ handlers и номера
|
||||||
|
выводов в `app/` или в ядро библиотеки. Тогда при переходе на другой МК
|
||||||
|
заменяются только `board/`, `port/`, startup и linker script.
|
||||||
|
|
||||||
|
## Выбор готового порта DS18B20
|
||||||
|
|
||||||
|
| Целевой МК | Файл порта | Шаблон конфигурации |
|
||||||
|
|---|---|---|
|
||||||
|
| STM32F103 | `c/ds18b20/ports/stm32f1/onewire_stm32f1.c` | `onewire_config.f103.template.h` |
|
||||||
|
| STM32G431 | `c/ds18b20/ports/stm32g4/onewire_stm32g4.c` | `onewire_config.g431.template.h` |
|
||||||
|
| STM32G474 | `c/ds18b20/ports/stm32g4/onewire_stm32g4.c` | `onewire_config.g474.template.h` |
|
||||||
|
|
||||||
|
В сборку добавляются ровно три файла:
|
||||||
|
|
||||||
|
```text
|
||||||
|
c/ds18b20/src/onewire.c
|
||||||
|
c/ds18b20/src/ds18b20.c
|
||||||
|
c/ds18b20/ports/<family>/onewire_<family>.c
|
||||||
|
```
|
||||||
|
|
||||||
|
Пути включения:
|
||||||
|
|
||||||
|
```text
|
||||||
|
c/ds18b20/include
|
||||||
|
board
|
||||||
|
```
|
||||||
|
|
||||||
|
Нужный шаблон копируется в `board/onewire_config.h`; после этого в нём
|
||||||
|
меняются только порт, номер вывода и бит тактирования GPIO.
|
||||||
|
|
||||||
|
G431 и G474 намеренно используют один исходник порта: GPIO, RCC и DWT для
|
||||||
|
этой задачи совместимы. Раздельными остаются конфигурация платы, startup,
|
||||||
|
linker script и выбранный CMSIS device define.
|
||||||
|
|
||||||
|
Рабочий пример полноценного G474-порта находится в соседнем проекте
|
||||||
|
`candleLight_fw`: `src/device/device_g4.c` отвечает за clock/GPIO,
|
||||||
|
`src/can/m_can.c` — за FDCAN, а `include/config.h` — за привязку платы
|
||||||
|
STM32G474VET6. Эти файлы остаются в прошивке, поскольку зависят от STM32 HAL,
|
||||||
|
USB gs_usb и конкретной разводки адаптера; переносимая логика протоколов
|
||||||
|
остаётся в `templates`.
|
||||||
|
|
||||||
|
## Какие части KONOR уже общие
|
||||||
|
|
||||||
|
| Возможность | Переносимое ядро | Что остаётся в `port/` проекта |
|
||||||
|
|---|---|---|
|
||||||
|
| DS18B20/1-Wire | `c/ds18b20` | готовый порт выбранного семейства и вывод |
|
||||||
|
| дисплей ST7789 | `c/st7789` | SPI, DC, CS, RESET, подсветка и задержка |
|
||||||
|
| кнопки | `c/keypad` | настройка GPIO и чтение уровней |
|
||||||
|
| меню | `c/menu` | только привязка painter к дисплею |
|
||||||
|
| EEPROM FT24C256 | `c/eeprom-ft24c256` | I2C write/write-read и задержка |
|
||||||
|
| CAN SETTINGS | `c/can-sensor` | bxCAN для F103 либо FDCAN для G431/G474 |
|
||||||
|
|
||||||
|
Порт дисплея, EEPROM и CAN нельзя честно сделать только по названию МК:
|
||||||
|
нужно знать экземпляр периферии, альтернативную функцию и выводы конкретной
|
||||||
|
платы. Поэтому библиотеки принимают таблицы обратных вызовов, а короткие
|
||||||
|
адаптеры лежат в проекте рядом с `app_config.h`.
|
||||||
|
|
||||||
|
## Порядок создания проекта
|
||||||
|
|
||||||
|
1. Создать проект в CubeMX/Keil либо добавить CMSIS, startup и linker script.
|
||||||
|
2. Подключить `templates` сабмодулем.
|
||||||
|
3. Добавить только ядра нужных библиотек.
|
||||||
|
4. Выбрать готовый 1-Wire порт по таблице выше.
|
||||||
|
5. Создать `board/app_config.h` с выводами и частотами.
|
||||||
|
6. Реализовать короткие адаптеры SPI/GPIO/I2C/CAN в `port/`.
|
||||||
|
7. Сначала прогнать host-тесты библиотек, затем проверить аппаратные порты
|
||||||
|
логическим анализатором или осциллографом.
|
||||||
|
|
||||||
|
Так новый проект не является копией старого: он использует общие ядра одной
|
||||||
|
версии, а аппаратные отличия видны в одном небольшом каталоге `port/`.
|
||||||
@@ -15,6 +15,9 @@ templates/
|
|||||||
python/ модули на чистом Python 3.9+, только stdlib
|
python/ модули на чистом Python 3.9+, только stdlib
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Пошаговая раскладка нового проекта и выбор портов для STM32F103, STM32G431
|
||||||
|
и STM32G474 описаны в [`NEW_PROJECT.md`](NEW_PROJECT.md).
|
||||||
|
|
||||||
## Что лежит
|
## Что лежит
|
||||||
|
|
||||||
### C
|
### C
|
||||||
@@ -26,8 +29,10 @@ templates/
|
|||||||
| [`c/menu`](c/menu) | экранное меню: стек экранов, курсор, прокрутка, тема | `stdint.h` | заливка прямоугольника, вывод строки |
|
| [`c/menu`](c/menu) | экранное меню: стек экранов, курсор, прокрутка, тема | `stdint.h` | заливка прямоугольника, вывод строки |
|
||||||
| [`c/eeprom-ft24c256`](c/eeprom-ft24c256) | EEPROM 24Cxx по I²C с нарезкой записи по страницам | `stdint.h` | две I²C-транзакции, задержка |
|
| [`c/eeprom-ft24c256`](c/eeprom-ft24c256) | EEPROM 24Cxx по I²C с нарезкой записи по страницам | `stdint.h` | две I²C-транзакции, задержка |
|
||||||
| [`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 — **порты STM32F103, STM32G431 и STM32G474 в комплекте** |
|
||||||
| [`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/rs485-boot`](c/rs485-boot) | прошивка по RS-485 в формате SETGUI v1: потоковый parser, CRC32 и resume | C99 | UART TX/RX, DE, Flash — **порты STM32F103 и STM32G474VET в комплекте** |
|
||||||
| [`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 в комплекте** |
|
||||||
|
|
||||||
@@ -73,6 +78,7 @@ git subtree pull --prefix lib/templates https://git.rd12.ru/Andrey/templates.git
|
|||||||
| `KONOR_ds18b20` | st7789, keypad, menu, eeprom-ft24c256, can-sensor, ds18b20 |
|
| `KONOR_ds18b20` | st7789, keypad, menu, eeprom-ft24c256, can-sensor, ds18b20 |
|
||||||
| `OpticalTester` | st7789, keypad, menu, eeprom-ft24c256 |
|
| `OpticalTester` | st7789, keypad, menu, eeprom-ft24c256 |
|
||||||
| `CAN_to_RS485` | protocan-transport, python/protocan |
|
| `CAN_to_RS485` | protocan-transport, python/protocan |
|
||||||
|
| `candleLight_fw` | эталон разделения общей логики и G431/G474 FDCAN-порта; общий ProtoCAN Boot переносится по контракту `protocan-boot` |
|
||||||
| `SETGUI` | python/protocan |
|
| `SETGUI` | python/protocan |
|
||||||
| новые устройства SET и `SETGUI` v2 | set-protocol |
|
| новые устройства SET и `SETGUI` v2 | set-protocol |
|
||||||
| `k1921vk028` | rtc-service |
|
| `k1921vk028` | rtc-service |
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `include/ds18b20.h`, `src/ds18b20.c` | перечень датчиков, измерения, запись TH/TL/разрешения | `onewire.h`, `stdint.h` |
|
| `include/ds18b20.h`, `src/ds18b20.c` | перечень датчиков, измерения, запись TH/TL/разрешения | `onewire.h`, `stdint.h` |
|
||||||
| `include/onewire.h`, `src/onewire.c` | байты и блоки, CRC8 Dallas/Maxim, обход SEARCH ROM | `stdint.h` |
|
| `include/onewire.h`, `src/onewire.c` | байты и блоки, CRC8 Dallas/Maxim, обход SEARCH ROM | `stdint.h` |
|
||||||
| `ports/stm32f1/` | тайминги Maxim, вывод с открытым стоком, задержки по DWT | CMSIS `stm32f10x.h` |
|
| `ports/stm32f1/` | STM32F103: тайминги Maxim, открытый сток, задержки по DWT | CMSIS `stm32f10x.h` |
|
||||||
| `ports/stm32g4/` | то же для STM32G431 и STM32G474 | CMSIS `stm32g4xx.h` |
|
| `ports/stm32g4/` | то же для STM32G431 и STM32G474 | CMSIS `stm32g4xx.h` |
|
||||||
| `ports/stm32f4/` | то же для STM32F407VET6 и родственных F4 | CMSIS `stm32f4xx.h` |
|
| `ports/stm32f4/` | то же для STM32F407VET6 и родственных F4 | CMSIS `stm32f4xx.h` |
|
||||||
| `tests/` | симулятор шины с виртуальными датчиками и host-тесты | — |
|
| `tests/` | симулятор шины с виртуальными датчиками и host-тесты | — |
|
||||||
@@ -50,7 +50,7 @@ uint8_t OneWire_ReadBit(void);
|
|||||||
микросекундных задержек — счётчик тактов ядра `DWT->CYCCNT`.
|
микросекундных задержек — счётчик тактов ядра `DWT->CYCCNT`.
|
||||||
|
|
||||||
Привязку вывода порт берёт из `onewire_config.h`, который предоставляет
|
Привязку вывода порт берёт из `onewire_config.h`, который предоставляет
|
||||||
приложение — шаблон лежит рядом:
|
приложение — шаблон `onewire_config.f103.template.h` лежит рядом:
|
||||||
|
|
||||||
```c
|
```c
|
||||||
#include "stm32f10x.h"
|
#include "stm32f10x.h"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* @file onewire_config.template.h
|
* @file onewire_config.f103.template.h
|
||||||
* @brief Шаблон привязки шины 1-Wire для порта STM32F1.
|
* @brief Шаблон привязки шины 1-Wire для STM32F103 (порт stm32f1).
|
||||||
*
|
*
|
||||||
* Скопируйте в своё приложение под именем onewire_config.h и положите на
|
* Скопируйте в своё приложение под именем onewire_config.h и положите на
|
||||||
* путь поиска заголовков. Библиотека сама этот файл не подставляет:
|
* путь поиска заголовков. Библиотека сама этот файл не подставляет:
|
||||||
@@ -8,7 +8,8 @@
|
|||||||
* терять байты USART во время обмена.
|
* терять байты USART во время обмена.
|
||||||
*
|
*
|
||||||
* Привязка вывода приходит из onewire_config.h, который предоставляет
|
* Привязка вывода приходит из onewire_config.h, который предоставляет
|
||||||
* приложение. Шаблон - onewire_config.template.h рядом с этим файлом.
|
* приложение. Шаблон для STM32F103 —
|
||||||
|
* onewire_config.f103.template.h рядом с этим файлом.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "onewire.h"
|
#include "onewire.h"
|
||||||
|
|||||||
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()
|
||||||
170
c/protocan-boot/README.md
Normal file
170
c/protocan-boot/README.md
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
# 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
|
||||||
|
512 КиБ = 512 * 1024 = 524 288 байт
|
||||||
|
524 288 / 8 = 65 536 блоков на слот
|
||||||
|
```
|
||||||
|
|
||||||
|
| `MsgBody` | Номер блока | Смещение | Байты слота |
|
||||||
|
|---:|---:|---:|---:|
|
||||||
|
| `0x0000` | 0 | `0x00000` | `0x00000..0x00007` |
|
||||||
|
| `0x0001` | 1 | `0x00008` | `0x00008..0x0000F` |
|
||||||
|
| `0xFFFF` | 65 535 | `0x7FFF8` | `0x7FFF8..0x7FFFF` |
|
||||||
|
|
||||||
|
Последний блок заканчивается на смещении `0x7FFFF`. Значение `0x80000`
|
||||||
|
равно размеру слота и является первой позицией за его границей. В коде эти
|
||||||
|
границы заданы константами `PCAN_BOOT_BLOCK_COUNT=65536` и
|
||||||
|
`PCAN_BOOT_SLOT_SIZE=524288`.
|
||||||
|
|
||||||
|
Последний кадр дополняется `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;
|
||||||
|
}
|
||||||
9
c/rs485-boot/CMakeLists.txt
Normal file
9
c/rs485-boot/CMakeLists.txt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
project(rs485_boot C)
|
||||||
|
add_library(rs485_boot src/rs485_boot.c)
|
||||||
|
target_include_directories(rs485_boot PUBLIC include)
|
||||||
|
if(BUILD_TESTING)
|
||||||
|
add_executable(test_rs485_boot tests/test_rs485_boot.c)
|
||||||
|
target_link_libraries(test_rs485_boot PRIVATE rs485_boot)
|
||||||
|
add_test(NAME rs485_boot COMMAND test_rs485_boot)
|
||||||
|
endif()
|
||||||
24
c/rs485-boot/README.md
Normal file
24
c/rs485-boot/README.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# RS-485 firmware boot
|
||||||
|
|
||||||
|
Переносимое ядро обновления прошивки по полудуплексному RS-485. Wire-протокол
|
||||||
|
совместим с SETGUI protocol v1 проекта `KONOR_ds18b20`: кадры `A5 5A`, CRC32,
|
||||||
|
команды `FIRMWARE_BEGIN/DATA/END/ABORT/STATUS` (`0x0B..0x0F`).
|
||||||
|
|
||||||
|
Ядро не обращается к регистрам МК. Проект передаёт ему принятые байты через
|
||||||
|
`rs485_boot_feed()` и реализует callbacks Flash/передачи из `rs485_boot_port_t`.
|
||||||
|
Готовые bare-metal порты находятся в `ports/stm32f103` и `ports/stm32g474vet`.
|
||||||
|
|
||||||
|
Важно: линии A/B должны иметь терминацию и fail-safe bias. Порт держит DE
|
||||||
|
активным до флага физического завершения передачи TC, затем возвращается в RX.
|
||||||
|
|
||||||
|
## Подключение
|
||||||
|
|
||||||
|
1. Добавить `src/rs485_boot.c` и порт нужного МК в проект.
|
||||||
|
2. Скопировать `rs485_boot_port_config.template.h` в include-каталог проекта как
|
||||||
|
`rs485_boot_port_config.h` и проверить UART, DE, адрес приложения и Flash.
|
||||||
|
3. Вызвать `rs485_boot_hw_init()`, затем `rs485_boot_init()` с callbacks из
|
||||||
|
`rs485_boot_hw_make_port()`.
|
||||||
|
4. В основном цикле читать `rs485_boot_hw_get_byte()` и передавать байты ядру.
|
||||||
|
|
||||||
|
Порт STM32F103 рассчитан на стандартную периферию STM32F10x. Порт G474VET
|
||||||
|
использует CMSIS STM32G4 (`stm32g474xx.h`) и 64-битное программирование Flash.
|
||||||
73
c/rs485-boot/include/rs485_boot.h
Normal file
73
c/rs485-boot/include/rs485_boot.h
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
#ifndef RS485_BOOT_H
|
||||||
|
#define RS485_BOOT_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define RS485_BOOT_VERSION 1U
|
||||||
|
#define RS485_BOOT_MAX_PAYLOAD 512U
|
||||||
|
#define RS485_BOOT_MAX_FRAME (8U + RS485_BOOT_MAX_PAYLOAD + 4U)
|
||||||
|
#define RS485_BOOT_DATA_HEADER_SIZE 8U
|
||||||
|
#define RS485_BOOT_MAX_BLOCK (RS485_BOOT_MAX_PAYLOAD - RS485_BOOT_DATA_HEADER_SIZE)
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
RS485_BOOT_IDLE = 0U,
|
||||||
|
RS485_BOOT_RECEIVING = 1U,
|
||||||
|
RS485_BOOT_READY = 2U,
|
||||||
|
RS485_BOOT_FAILED = 3U
|
||||||
|
} rs485_boot_state_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
RS485_BOOT_OK = 0U,
|
||||||
|
RS485_BOOT_INVALID_ARGUMENT = 1U,
|
||||||
|
RS485_BOOT_INVALID_LENGTH = 2U,
|
||||||
|
RS485_BOOT_BUSY = 5U,
|
||||||
|
RS485_BOOT_INTERNAL = 7U,
|
||||||
|
RS485_BOOT_UNSUPPORTED = 0x11U
|
||||||
|
} rs485_boot_result_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint32_t app_address;
|
||||||
|
uint32_t app_size;
|
||||||
|
uint16_t max_block_size;
|
||||||
|
} rs485_boot_config_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
bool (*transmit)(void *user, const uint8_t *data, uint16_t length);
|
||||||
|
bool (*erase)(void *user, uint32_t image_size);
|
||||||
|
bool (*write)(void *user, uint32_t offset, const uint8_t *data, uint16_t length);
|
||||||
|
bool (*read)(void *user, uint32_t offset, uint8_t *data, uint16_t length);
|
||||||
|
bool (*image_valid)(void *user, uint32_t image_size, uint32_t image_crc32);
|
||||||
|
void (*reboot)(void *user);
|
||||||
|
} rs485_boot_port_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
rs485_boot_config_t config;
|
||||||
|
rs485_boot_port_t port;
|
||||||
|
void *port_user;
|
||||||
|
uint8_t parser[RS485_BOOT_MAX_FRAME];
|
||||||
|
uint16_t parser_length;
|
||||||
|
uint8_t tx[RS485_BOOT_MAX_FRAME];
|
||||||
|
uint8_t scratch[32];
|
||||||
|
uint32_t image_size;
|
||||||
|
uint32_t image_crc32;
|
||||||
|
uint32_t next_offset;
|
||||||
|
uint32_t crc_errors;
|
||||||
|
rs485_boot_state_t state;
|
||||||
|
} rs485_boot_t;
|
||||||
|
|
||||||
|
bool rs485_boot_init(rs485_boot_t *boot, const rs485_boot_config_t *config,
|
||||||
|
const rs485_boot_port_t *port, void *port_user);
|
||||||
|
void rs485_boot_feed(rs485_boot_t *boot, const uint8_t *data, size_t length);
|
||||||
|
void rs485_boot_abort(rs485_boot_t *boot);
|
||||||
|
uint32_t rs485_boot_crc32(const uint8_t *data, size_t length);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#ifndef RS485_BOOT_PORT_CONFIG_H
|
||||||
|
#define RS485_BOOT_PORT_CONFIG_H
|
||||||
|
#define RS485_BOOT_BAUDRATE 512000UL
|
||||||
|
#define RS485_BOOT_APP_ADDRESS 0x08003000UL
|
||||||
|
#define RS485_BOOT_FLASH_END 0x08010000UL
|
||||||
|
#define RS485_BOOT_FLASH_PAGE 1024UL
|
||||||
|
#define RS485_BOOT_DE_PIN 8U /* USART1 PA9/PA10; DE PA8, active high. */
|
||||||
|
#endif
|
||||||
17
c/rs485-boot/ports/stm32f103/rs485_boot_stm32f103.c
Normal file
17
c/rs485-boot/ports/stm32f103/rs485_boot_stm32f103.c
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#include "rs485_boot_stm32f103.h"
|
||||||
|
#include "rs485_boot_port_config.h"
|
||||||
|
#include "stm32f10x.h"
|
||||||
|
#include <string.h>
|
||||||
|
static bool fw(void){while(FLASH->SR&FLASH_SR_BSY){}if(FLASH->SR&(FLASH_SR_PGERR|FLASH_SR_WRPRTERR)){FLASH->SR=FLASH_SR_PGERR|FLASH_SR_WRPRTERR|FLASH_SR_EOP;return false;}FLASH->SR=FLASH_SR_EOP;return true;}
|
||||||
|
static void ul(void){if(FLASH->CR&FLASH_CR_LOCK){FLASH->KEYR=0x45670123UL;FLASH->KEYR=0xCDEF89ABUL;}}
|
||||||
|
static void lk(void){FLASH->CR|=FLASH_CR_LOCK;}
|
||||||
|
void rs485_boot_hw_init(void){uint32_t c;RCC->APB2ENR|=RCC_APB2ENR_IOPAEN|RCC_APB2ENR_USART1EN;c=GPIOA->CRH;c&=~((0xFUL<<0)|(0xFUL<<4)|(0xFUL<<8));c|=(0x3UL<<0)|(0xBUL<<4)|(0x4UL<<8);GPIOA->CRH=c;GPIOA->BRR=1UL<<RS485_BOOT_DE_PIN;USART1->BRR=(SystemCoreClock+RS485_BOOT_BAUDRATE/2U)/RS485_BOOT_BAUDRATE;USART1->CR1=USART_CR1_TE|USART_CR1_RE|USART_CR1_UE;}
|
||||||
|
bool rs485_boot_hw_get_byte(uint8_t*b){uint32_t s=USART1->SR;if(s&(USART_SR_ORE|USART_SR_FE|USART_SR_NE)){(void)USART1->DR;return false;}if(!(s&USART_SR_RXNE))return false;*b=(uint8_t)USART1->DR;return true;}
|
||||||
|
static bool tx(void*u,const uint8_t*d,uint16_t n){uint16_t i;(void)u;GPIOA->BSRR=1UL<<RS485_BOOT_DE_PIN;for(i=0;i<n;i++){while(!(USART1->SR&USART_SR_TXE)){}USART1->DR=d[i];}while(!(USART1->SR&USART_SR_TC)){}GPIOA->BRR=1UL<<RS485_BOOT_DE_PIN;return true;}
|
||||||
|
static bool er(void*u,uint32_t n){uint32_t a,e;(void)u;if(!n||n>RS485_BOOT_FLASH_END-RS485_BOOT_APP_ADDRESS)return false;e=RS485_BOOT_APP_ADDRESS+((n+RS485_BOOT_FLASH_PAGE-1U)&~(RS485_BOOT_FLASH_PAGE-1U));ul();for(a=RS485_BOOT_APP_ADDRESS;a<e;a+=RS485_BOOT_FLASH_PAGE){FLASH->CR=FLASH_CR_PER;FLASH->AR=a;FLASH->CR|=FLASH_CR_STRT;if(!fw()){FLASH->CR=0;lk();return false;}}FLASH->CR=0;lk();return true;}
|
||||||
|
static bool wr(void*u,uint32_t o,const uint8_t*d,uint16_t n){uint16_t i;(void)u;if((o&1U)||o+n>RS485_BOOT_FLASH_END-RS485_BOOT_APP_ADDRESS)return false;ul();for(i=0;i<n;i+=2){uint16_t v=(uint16_t)d[i]|(uint16_t)(i+1<n?((uint16_t)d[i+1]<<8):0xFF00U);volatile uint16_t*q=(volatile uint16_t*)(RS485_BOOT_APP_ADDRESS+o+i);FLASH->CR=FLASH_CR_PG;*q=v;if(!fw()||*q!=v){FLASH->CR=0;lk();return false;}}FLASH->CR=0;lk();return true;}
|
||||||
|
static bool rd(void*u,uint32_t o,uint8_t*d,uint16_t n){(void)u;if(o+n>RS485_BOOT_FLASH_END-RS485_BOOT_APP_ADDRESS)return false;memcpy(d,(const void*)(RS485_BOOT_APP_ADDRESS+o),n);return true;}
|
||||||
|
static bool ok(void*u,uint32_t n,uint32_t c){uint32_t sp=*(const uint32_t*)RS485_BOOT_APP_ADDRESS,rv=*(const uint32_t*)(RS485_BOOT_APP_ADDRESS+4);(void)u;return sp>=0x20000000UL&&sp<=0x20005000UL&&(rv&1U)&&(rv&~1UL)>=RS485_BOOT_APP_ADDRESS&&(rv&~1UL)<RS485_BOOT_FLASH_END&&rs485_boot_crc32((const uint8_t*)RS485_BOOT_APP_ADDRESS,n)==c;}
|
||||||
|
static void reset(void*u){(void)u;NVIC_SystemReset();}
|
||||||
|
rs485_boot_config_t rs485_boot_hw_config(void){rs485_boot_config_t c={RS485_BOOT_APP_ADDRESS,RS485_BOOT_FLASH_END-RS485_BOOT_APP_ADDRESS,RS485_BOOT_MAX_BLOCK};return c;}
|
||||||
|
rs485_boot_port_t rs485_boot_hw_make_port(void){rs485_boot_port_t p={tx,er,wr,rd,ok,reset};return p;}
|
||||||
8
c/rs485-boot/ports/stm32f103/rs485_boot_stm32f103.h
Normal file
8
c/rs485-boot/ports/stm32f103/rs485_boot_stm32f103.h
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#ifndef RS485_BOOT_STM32F103_H
|
||||||
|
#define RS485_BOOT_STM32F103_H
|
||||||
|
#include "rs485_boot.h"
|
||||||
|
void rs485_boot_hw_init(void);
|
||||||
|
bool rs485_boot_hw_get_byte(uint8_t *byte);
|
||||||
|
rs485_boot_config_t rs485_boot_hw_config(void);
|
||||||
|
rs485_boot_port_t rs485_boot_hw_make_port(void);
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#ifndef RS485_BOOT_PORT_CONFIG_H
|
||||||
|
#define RS485_BOOT_PORT_CONFIG_H
|
||||||
|
#define RS485_BOOT_BAUDRATE 512000UL
|
||||||
|
#define RS485_BOOT_UART_CLOCK_HZ SystemCoreClock
|
||||||
|
#define RS485_BOOT_APP_ADDRESS 0x08008000UL
|
||||||
|
#define RS485_BOOT_FLASH_END 0x08080000UL
|
||||||
|
#define RS485_BOOT_FLASH_PAGE 2048UL
|
||||||
|
#define RS485_BOOT_DE_PIN 8U /* USART1 PA9/PA10 AF7; DE PA8. */
|
||||||
|
#endif
|
||||||
17
c/rs485-boot/ports/stm32g474vet/rs485_boot_stm32g474vet.c
Normal file
17
c/rs485-boot/ports/stm32g474vet/rs485_boot_stm32g474vet.c
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
|
|||||||
|
#ifndef RS485_BOOT_STM32G474VET_H
|
||||||
|
#define RS485_BOOT_STM32G474VET_H
|
||||||
|
#include "rs485_boot.h"
|
||||||
|
void rs485_boot_hw_init(void);
|
||||||
|
bool rs485_boot_hw_get_byte(uint8_t *byte);
|
||||||
|
rs485_boot_config_t rs485_boot_hw_config(void);
|
||||||
|
rs485_boot_port_t rs485_boot_hw_make_port(void);
|
||||||
|
#endif
|
||||||
147
c/rs485-boot/src/rs485_boot.c
Normal file
147
c/rs485-boot/src/rs485_boot.c
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
#include "rs485_boot.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define SOF0 0xA5U
|
||||||
|
#define SOF1 0x5AU
|
||||||
|
#define HEADER_SIZE 8U
|
||||||
|
#define CRC_SIZE 4U
|
||||||
|
#define MSG_BEGIN 0x0BU
|
||||||
|
#define MSG_DATA 0x0CU
|
||||||
|
#define MSG_END 0x0DU
|
||||||
|
#define MSG_ABORT 0x0EU
|
||||||
|
#define MSG_STATUS 0x0FU
|
||||||
|
#define MSG_NACK 0x80U
|
||||||
|
|
||||||
|
static uint16_t get_u16(const uint8_t *p) { return (uint16_t)(p[0] | ((uint16_t)p[1] << 8U)); }
|
||||||
|
static uint32_t get_u32(const uint8_t *p) { return (uint32_t)p[0] | ((uint32_t)p[1] << 8U) | ((uint32_t)p[2] << 16U) | ((uint32_t)p[3] << 24U); }
|
||||||
|
static void put_u32(uint8_t *p, uint32_t v) { p[0]=(uint8_t)v; p[1]=(uint8_t)(v>>8U); p[2]=(uint8_t)(v>>16U); p[3]=(uint8_t)(v>>24U); }
|
||||||
|
|
||||||
|
uint32_t rs485_boot_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 >> 1U) ^ ((crc & 1U) ? 0xEDB88320UL : 0U);
|
||||||
|
}
|
||||||
|
return crc ^ 0xFFFFFFFFUL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void rs485_boot_abort(rs485_boot_t *boot)
|
||||||
|
{
|
||||||
|
if (boot == NULL) return;
|
||||||
|
boot->state = RS485_BOOT_IDLE;
|
||||||
|
boot->image_size = 0U;
|
||||||
|
boot->image_crc32 = 0U;
|
||||||
|
boot->next_offset = 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool rs485_boot_init(rs485_boot_t *boot, const rs485_boot_config_t *config,
|
||||||
|
const rs485_boot_port_t *port, void *port_user)
|
||||||
|
{
|
||||||
|
if ((boot == NULL) || (config == NULL) || (port == NULL) ||
|
||||||
|
(config->app_size == 0U) || (port->transmit == NULL) ||
|
||||||
|
(port->erase == NULL) || (port->write == NULL) || (port->read == NULL)) return false;
|
||||||
|
(void)memset(boot, 0, sizeof(*boot));
|
||||||
|
boot->config = *config;
|
||||||
|
boot->port = *port;
|
||||||
|
boot->port_user = port_user;
|
||||||
|
if ((boot->config.max_block_size == 0U) || (boot->config.max_block_size > RS485_BOOT_MAX_BLOCK))
|
||||||
|
boot->config.max_block_size = RS485_BOOT_MAX_BLOCK;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool send_frame(rs485_boot_t *b, uint8_t type, uint16_t seq, const uint8_t *payload, uint16_t size)
|
||||||
|
{
|
||||||
|
uint32_t crc;
|
||||||
|
uint16_t i;
|
||||||
|
const uint16_t total = (uint16_t)(HEADER_SIZE + size + CRC_SIZE);
|
||||||
|
if (size > RS485_BOOT_MAX_PAYLOAD) return false;
|
||||||
|
b->tx[0]=SOF0; b->tx[1]=SOF1; b->tx[2]=RS485_BOOT_VERSION; b->tx[3]=type;
|
||||||
|
b->tx[4]=(uint8_t)(seq>>8U); b->tx[5]=(uint8_t)seq; b->tx[6]=(uint8_t)(size>>8U); b->tx[7]=(uint8_t)size;
|
||||||
|
for (i=0U; i<size; ++i) b->tx[HEADER_SIZE+i]=payload[i];
|
||||||
|
crc=rs485_boot_crc32(&b->tx[2], (size_t)6U+size);
|
||||||
|
put_u32(&b->tx[HEADER_SIZE+size], crc);
|
||||||
|
return b->port.transmit(b->port_user, b->tx, total);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void nack(rs485_boot_t *b, uint8_t request, uint16_t seq, uint16_t result)
|
||||||
|
{
|
||||||
|
uint8_t p[3]={(uint8_t)result,(uint8_t)(result>>8U),request};
|
||||||
|
(void)send_frame(b, MSG_NACK, seq, p, 3U);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool matches(rs485_boot_t *b, uint32_t offset, const uint8_t *data, uint16_t size)
|
||||||
|
{
|
||||||
|
uint16_t done=0U;
|
||||||
|
while (done<size) {
|
||||||
|
uint16_t n=(uint16_t)(size-done); uint16_t i;
|
||||||
|
if (n>sizeof(b->scratch)) n=sizeof(b->scratch);
|
||||||
|
if (!b->port.read(b->port_user, offset+done, b->scratch, n)) return false;
|
||||||
|
for(i=0U;i<n;++i) if(b->scratch[i]!=data[done+i]) return false;
|
||||||
|
done=(uint16_t)(done+n);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void process(rs485_boot_t *b, uint8_t type, uint16_t seq, const uint8_t *p, uint16_t size)
|
||||||
|
{
|
||||||
|
if (type==MSG_BEGIN) {
|
||||||
|
uint32_t base;
|
||||||
|
if(size!=16U){nack(b,type,seq,RS485_BOOT_INVALID_LENGTH);return;}
|
||||||
|
b->image_size=get_u32(p); b->image_crc32=get_u32(&p[4]); base=get_u32(&p[12]);
|
||||||
|
if((b->image_size==0U)||(b->image_size>b->config.app_size)||((base!=0U)&&(base!=b->config.app_address))){nack(b,type,seq,RS485_BOOT_INVALID_ARGUMENT);return;}
|
||||||
|
b->next_offset=0U; b->state=RS485_BOOT_RECEIVING;
|
||||||
|
if(!b->port.erase(b->port_user,b->image_size)){b->state=RS485_BOOT_FAILED;nack(b,type,seq,RS485_BOOT_INTERNAL);return;}
|
||||||
|
put_u32(b->scratch,0U); (void)send_frame(b,type,seq,b->scratch,4U); return;
|
||||||
|
}
|
||||||
|
if(type==MSG_DATA){
|
||||||
|
uint32_t offset; uint16_t n,expected=0U,i;
|
||||||
|
if((b->state!=RS485_BOOT_RECEIVING)||(size<RS485_BOOT_DATA_HEADER_SIZE)){nack(b,type,seq,RS485_BOOT_BUSY);return;}
|
||||||
|
offset=get_u32(p); n=get_u16(&p[4]); for(i=0U;i<n && (uint16_t)(RS485_BOOT_DATA_HEADER_SIZE+i)<size;++i) expected=(uint16_t)(expected+p[RS485_BOOT_DATA_HEADER_SIZE+i]);
|
||||||
|
if((n==0U)||(n>b->config.max_block_size)||(size!=(uint16_t)(RS485_BOOT_DATA_HEADER_SIZE+n))||((offset&1U)!=0U)||(offset>b->image_size)||(n>b->image_size-offset)){nack(b,type,seq,RS485_BOOT_INVALID_LENGTH);return;}
|
||||||
|
if(expected!=get_u16(&p[6])){nack(b,type,seq,RS485_BOOT_INVALID_ARGUMENT);return;}
|
||||||
|
if(offset<b->next_offset){if((offset+n>b->next_offset)||!matches(b,offset,&p[8],n)){nack(b,type,seq,RS485_BOOT_INVALID_ARGUMENT);return;}}
|
||||||
|
else {if(offset!=b->next_offset){nack(b,type,seq,RS485_BOOT_INVALID_ARGUMENT);return;} if(!b->port.write(b->port_user,offset,&p[8],n)){b->state=RS485_BOOT_FAILED;nack(b,type,seq,RS485_BOOT_INTERNAL);return;} b->next_offset+=n;}
|
||||||
|
put_u32(b->scratch,b->next_offset); (void)send_frame(b,type,seq,b->scratch,4U); return;
|
||||||
|
}
|
||||||
|
if(type==MSG_END){
|
||||||
|
if((b->state!=RS485_BOOT_RECEIVING)||(size!=8U)||(get_u32(p)!=b->image_size)||(get_u32(&p[4])!=b->image_crc32)||(b->next_offset!=b->image_size)||((b->port.image_valid!=NULL)&&!b->port.image_valid(b->port_user,b->image_size,b->image_crc32))){b->state=RS485_BOOT_FAILED;nack(b,type,seq,RS485_BOOT_INVALID_ARGUMENT);return;}
|
||||||
|
b->state=RS485_BOOT_READY; (void)send_frame(b,type,seq,NULL,0U); if(b->port.reboot!=NULL)b->port.reboot(b->port_user); return;
|
||||||
|
}
|
||||||
|
if(type==MSG_ABORT){rs485_boot_abort(b);(void)send_frame(b,type,seq,NULL,0U);return;}
|
||||||
|
if(type==MSG_STATUS){b->scratch[0]=(uint8_t)b->state;put_u32(&b->scratch[1],b->next_offset);(void)send_frame(b,type,seq,b->scratch,5U);return;}
|
||||||
|
nack(b,type,seq,RS485_BOOT_UNSUPPORTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void discard(rs485_boot_t *b, uint16_t n)
|
||||||
|
{
|
||||||
|
if(n>=b->parser_length){b->parser_length=0U;return;}
|
||||||
|
b->parser_length=(uint16_t)(b->parser_length-n);
|
||||||
|
(void)memmove(b->parser,&b->parser[n],b->parser_length);
|
||||||
|
}
|
||||||
|
|
||||||
|
void rs485_boot_feed(rs485_boot_t *b, const uint8_t *data, size_t length)
|
||||||
|
{
|
||||||
|
size_t input;
|
||||||
|
if((b==NULL)||((data==NULL)&&(length!=0U)))return;
|
||||||
|
for(input=0U;input<length;++input){
|
||||||
|
if(b->parser_length==RS485_BOOT_MAX_FRAME)b->parser_length=0U;
|
||||||
|
b->parser[b->parser_length++]=data[input];
|
||||||
|
for(;;){
|
||||||
|
uint16_t payload,total,i; uint32_t got;
|
||||||
|
while((b->parser_length>=2U)&&((b->parser[0]!=SOF0)||(b->parser[1]!=SOF1)))discard(b,1U);
|
||||||
|
if(b->parser_length<HEADER_SIZE)break;
|
||||||
|
if(b->parser[2]!=RS485_BOOT_VERSION){discard(b,1U);continue;}
|
||||||
|
payload=(uint16_t)(((uint16_t)b->parser[6]<<8U)|b->parser[7]);
|
||||||
|
if(payload>RS485_BOOT_MAX_PAYLOAD){discard(b,1U);continue;}
|
||||||
|
total=(uint16_t)(HEADER_SIZE+payload+CRC_SIZE); if(b->parser_length<total)break;
|
||||||
|
got=get_u32(&b->parser[HEADER_SIZE+payload]);
|
||||||
|
if(got!=rs485_boot_crc32(&b->parser[2],(size_t)6U+payload)){++b->crc_errors;discard(b,1U);continue;}
|
||||||
|
i=(uint16_t)(((uint16_t)b->parser[4]<<8U)|b->parser[5]);
|
||||||
|
process(b,b->parser[3],i,&b->parser[HEADER_SIZE],payload); discard(b,total);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
c/rs485-boot/tests/test_rs485_boot.c
Normal file
13
c/rs485-boot/tests/test_rs485_boot.c
Normal file
File diff suppressed because one or more lines are too long
45
doc/index.html
Normal file
45
doc/index.html
Normal file
File diff suppressed because one or more lines are too long
@@ -39,6 +39,11 @@ class MsgType(IntEnum):
|
|||||||
MODBUS_HOLDING = 0b0110
|
MODBUS_HOLDING = 0b0110
|
||||||
MODBUS_INPUT = 0b0111
|
MODBUS_INPUT = 0b0111
|
||||||
ERROR = 0b1000
|
ERROR = 0b1000
|
||||||
|
BOOT_CONTROL = 0b1001
|
||||||
|
BOOT_DATA_A = 0b1010
|
||||||
|
BOOT_DATA_B = 0b1011
|
||||||
|
BOOT_STATUS = 0b1100
|
||||||
|
BOOT_DISCOVERY = 0b1101
|
||||||
PULSE = 0b1111
|
PULSE = 0b1111
|
||||||
|
|
||||||
|
|
||||||
@@ -90,6 +95,11 @@ MSGTYPE_RU = {
|
|||||||
MsgType.MODBUS_HOLDING: "Modbus Holding",
|
MsgType.MODBUS_HOLDING: "Modbus Holding",
|
||||||
MsgType.MODBUS_INPUT: "Modbus Input",
|
MsgType.MODBUS_INPUT: "Modbus Input",
|
||||||
MsgType.ERROR: "Error",
|
MsgType.ERROR: "Error",
|
||||||
|
MsgType.BOOT_CONTROL: "Boot Control",
|
||||||
|
MsgType.BOOT_DATA_A: "Boot Data A",
|
||||||
|
MsgType.BOOT_DATA_B: "Boot Data B",
|
||||||
|
MsgType.BOOT_STATUS: "Boot Status",
|
||||||
|
MsgType.BOOT_DISCOVERY: "Boot Discovery",
|
||||||
MsgType.PULSE: "Pulse",
|
MsgType.PULSE: "Pulse",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,6 +308,11 @@ def decode(raw_id: int, data: bytes, ide: bool = True, rtr: bool = False) -> Dec
|
|||||||
MsgType.MODBUS_HOLDING: _decode_modbus_reg,
|
MsgType.MODBUS_HOLDING: _decode_modbus_reg,
|
||||||
MsgType.MODBUS_INPUT: _decode_modbus_reg,
|
MsgType.MODBUS_INPUT: _decode_modbus_reg,
|
||||||
MsgType.ERROR: _decode_error,
|
MsgType.ERROR: _decode_error,
|
||||||
|
MsgType.BOOT_CONTROL: _decode_boot,
|
||||||
|
MsgType.BOOT_DATA_A: _decode_boot,
|
||||||
|
MsgType.BOOT_DATA_B: _decode_boot,
|
||||||
|
MsgType.BOOT_STATUS: _decode_boot,
|
||||||
|
MsgType.BOOT_DISCOVERY: _decode_boot,
|
||||||
MsgType.PULSE: _decode_pulse,
|
MsgType.PULSE: _decode_pulse,
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
@@ -312,6 +327,28 @@ def decode(raw_id: int, data: bytes, ide: bool = True, rtr: bool = False) -> Dec
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_boot(res: Decoded) -> None:
|
||||||
|
kind = MsgType(res.id.msg_type)
|
||||||
|
if kind == MsgType.BOOT_CONTROL:
|
||||||
|
command = res.id.msg_body & 0xFF
|
||||||
|
session = res.id.msg_body >> 8
|
||||||
|
names = {1: "IDENTIFY", 2: "ENTER_BOOT", 3: "BEGIN_IMAGE",
|
||||||
|
4: "BEGIN_COMPAT", 5: "ERASE", 6: "VERIFY", 7: "COMMIT",
|
||||||
|
8: "CONFIRM", 9: "REBOOT", 10: "ABORT", 11: "QUERY_PROGRESS"}
|
||||||
|
res.summary = "BOOT CONTROL %s, session=%d" % (
|
||||||
|
names.get(command, "0x%02X" % command), session)
|
||||||
|
elif kind in (MsgType.BOOT_DATA_A, MsgType.BOOT_DATA_B):
|
||||||
|
res.summary = "BOOT DATA slot %s, block=%d" % (
|
||||||
|
"A" if kind == MsgType.BOOT_DATA_A else "B", res.id.msg_body)
|
||||||
|
elif kind == MsgType.BOOT_STATUS:
|
||||||
|
res.summary = "BOOT STATUS command=0x%02X, session=%d" % (
|
||||||
|
res.id.msg_body & 0xFF, res.id.msg_body >> 8)
|
||||||
|
else:
|
||||||
|
res.summary = "BOOT DISCOVERY"
|
||||||
|
if res.data[:4] == b"PONG" and len(res.data) == 8:
|
||||||
|
res.summary = "PONG, FW %d.%d, protocol %d" % tuple(res.data[4:7])
|
||||||
|
|
||||||
|
|
||||||
def _decode_broadcast(res: Decoded) -> None:
|
def _decode_broadcast(res: Decoded) -> None:
|
||||||
body, btype = split_broadcast(res.id.msg_body)
|
body, btype = split_broadcast(res.id.msg_body)
|
||||||
res.fields.append((" Broadcast.Type", "0x%03X — %s" % (
|
res.fields.append((" Broadcast.Type", "0x%03X — %s" % (
|
||||||
|
|||||||
Reference in New Issue
Block a user