Compare commits
10 Commits
v0.1.0
...
4b32cda46e
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b32cda46e | |||
| 217073982f | |||
| 94c991380b | |||
| 5a8724da6d | |||
| d507b84a76 | |||
| d3bb634fb1 | |||
| a1d2d05f42 | |||
| 1345766efb | |||
| 085eb3c8bd | |||
| 033c7ab9e8 |
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/`.
|
||||
10
README.md
10
README.md
@@ -15,6 +15,9 @@ templates/
|
||||
python/ модули на чистом Python 3.9+, только stdlib
|
||||
```
|
||||
|
||||
Пошаговая раскладка нового проекта и выбор портов для STM32F103, STM32G431
|
||||
и STM32G474 описаны в [`NEW_PROJECT.md`](NEW_PROJECT.md).
|
||||
|
||||
## Что лежит
|
||||
|
||||
### C
|
||||
@@ -26,8 +29,11 @@ templates/
|
||||
| [`c/menu`](c/menu) | экранное меню: стек экранов, курсор, прокрутка, тема | `stdint.h` | заливка прямоугольника, вывод строки |
|
||||
| [`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/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-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/rtc-service`](c/rtc-service) | RTC с резервированным backup-томом | `stdint.h` | доступ к RTC и backup-памяти — **порт K1921VK028 в комплекте** |
|
||||
|
||||
### Python
|
||||
@@ -72,7 +78,9 @@ git subtree pull --prefix lib/templates https://git.rd12.ru/Andrey/templates.git
|
||||
| `KONOR_ds18b20` | st7789, keypad, menu, eeprom-ft24c256, can-sensor, ds18b20 |
|
||||
| `OpticalTester` | st7789, keypad, menu, eeprom-ft24c256 |
|
||||
| `CAN_to_RS485` | protocan-transport, python/protocan |
|
||||
| `candleLight_fw` | эталон разделения общей логики и G431/G474 FDCAN-порта; общий ProtoCAN Boot переносится по контракту `protocan-boot` |
|
||||
| `SETGUI` | python/protocan |
|
||||
| новые устройства SET и `SETGUI` v2 | set-protocol |
|
||||
| `k1921vk028` | rtc-service |
|
||||
|
||||
## Что сюда не попало и почему
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
# can-sensor
|
||||
|
||||
Передача 64-битных идентификаторов датчиков (ROM 1-Wire) по шине CAN.
|
||||
Однокадровый транспорт SETCAN SETTINGS для привязки 64-битного ROM 1-Wire к
|
||||
локации «номер сборки + позиция» по шине CAN.
|
||||
|
||||
Ядро на C99: не включает заголовки периферии, не обращается к регистрам,
|
||||
не пользуется прерываниями. Обмен идёт через таблицу `CanSensor_Io`,
|
||||
которую заполняет порт платы.
|
||||
|
||||
Сообщение состоит из двух классических CAN-кадров — 64-битный идентификатор
|
||||
и преамбула вместе не помещаются в восемь байтов поля данных:
|
||||
Сообщение помещается в один Extended CAN-кадр: локация находится в Body
|
||||
29-битного ID, а ROM — в восьми байтах payload:
|
||||
|
||||
```
|
||||
кадр 1 — преамбула, DLC = 2 | команда | позиция |
|
||||
кадр 2 — идентификатор, DLC = 8| ROM 8 байт |
|
||||
запрос 0x17FEZZYY: DLC=0 — GET, DLC=8 — WRITE/REPLACE/CLEAR
|
||||
ответ 0x1FFEZZYY: DLC=8 — текущий ROM, DLC=1 — код ошибки
|
||||
```
|
||||
|
||||
## Состав
|
||||
|
||||
| Файл | Что делает | Зависимости |
|
||||
|---|---|---|
|
||||
| `can_sensor.h`, `can_sensor.c` | сборка и разбор пары кадров, повторы передачи, счётчики обмена | `stdint.h` |
|
||||
| `can_sensor.h`, `can_sensor.c` | сборка и разбор SETTINGS, повторы передачи, счётчики обмена | `stdint.h` |
|
||||
|
||||
Приём собирает сообщение сам: кадр ROM без преамбулы и кадр с неверной
|
||||
длиной отбрасываются и учитываются в `dropped_frames`.
|
||||
`ZZ` — номер сборки, `YY` — позиция. Нулевой ROM с DLC=8 очищает локацию.
|
||||
|
||||
## Что нужно от платформы
|
||||
|
||||
@@ -38,10 +38,10 @@ uint8_t receive(void *ctx, CanSensor_Frame *frame); /* 1 — кадр п
|
||||
CanSensor link;
|
||||
CanSensor_Io io = { .send = bxcan_send, .receive = bxcan_receive, .context = &board };
|
||||
CanSensor_Config config;
|
||||
CanSensor_ConfigDefault(&config); /* tx_id 0x200, кадр ROM 0x201, 3 попытки */
|
||||
CanSensor_ConfigDefault(&config); /* TX 0x1FFE0000, RX 0x17FE0000 */
|
||||
|
||||
CanSensor_Init(&link, &io, &config);
|
||||
CanSensor_SendId(&link, position, rom);
|
||||
CanSensor_SendId(&link, position, assembly_serial, rom);
|
||||
|
||||
CanSensor_Message message;
|
||||
if (CanSensor_Poll(&link, &message)) { /* принят идентификатор */ }
|
||||
|
||||
@@ -1,72 +1,35 @@
|
||||
/**
|
||||
* @file can_sensor.c
|
||||
* @brief Сборка, передача и разбор сообщений «преамбула + идентификатор».
|
||||
*
|
||||
* Реализация не хранит очередей и не пользуется временем: состояние приёма
|
||||
* ограничено признаком принятой преамбулы, поэтому библиотека одинаково
|
||||
* работает и в главном цикле, и в обработчике прерывания порта.
|
||||
* @brief Однокадровый транспорт SETCAN SETTINGS для локаций DS18B20.
|
||||
*/
|
||||
|
||||
#include "can_sensor.h"
|
||||
|
||||
/**
|
||||
* @brief Возвращает идентификатор кадра данных для заданных настроек.
|
||||
*
|
||||
* @param config Настройки узла.
|
||||
* @return Идентификатор кадра ROM при передаче.
|
||||
*/
|
||||
static uint32_t can_sensor_tx_data_id(const CanSensor_Config *config)
|
||||
/** Возвращает 1, если все восемь байтов ROM равны нулю. */
|
||||
static uint8_t can_sensor_rom_is_zero(const uint8_t *rom)
|
||||
{
|
||||
if (config->tx_data_id != 0U) {
|
||||
return config->tx_data_id;
|
||||
uint8_t index;
|
||||
|
||||
for (index = 0U; index < CAN_SENSOR_ID_SIZE; index++) {
|
||||
if (rom[index] != 0U) {
|
||||
return 0U;
|
||||
}
|
||||
}
|
||||
return config->tx_id + CAN_SENSOR_DATA_ID_OFFSET;
|
||||
return 1U;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Возвращает ожидаемый идентификатор преамбулы при приёме.
|
||||
*
|
||||
* @param config Настройки узла.
|
||||
* @return Идентификатор кадра преамбулы.
|
||||
*/
|
||||
static uint32_t can_sensor_rx_id(const CanSensor_Config *config)
|
||||
{
|
||||
if (config->rx_id != 0U) {
|
||||
return config->rx_id;
|
||||
}
|
||||
return config->tx_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Возвращает ожидаемый идентификатор кадра ROM при приёме.
|
||||
*
|
||||
* @param config Настройки узла.
|
||||
* @return Идентификатор кадра идентификатора датчика.
|
||||
*/
|
||||
static uint32_t can_sensor_rx_data_id(const CanSensor_Config *config)
|
||||
{
|
||||
if (config->rx_data_id != 0U) {
|
||||
return config->rx_data_id;
|
||||
}
|
||||
return can_sensor_rx_id(config) + CAN_SENSOR_DATA_ID_OFFSET;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Передаёт один кадр с повторами при отказе контроллера.
|
||||
*
|
||||
* @param link Состояние узла.
|
||||
* @param frame Передаваемый кадр.
|
||||
* @return 1, если кадр принят контроллером, иначе 0.
|
||||
*/
|
||||
/** Передаёт уже собранный кадр с ограниченным числом повторов. */
|
||||
static uint8_t can_sensor_send_frame(CanSensor *link, const CanSensor_Frame *frame)
|
||||
{
|
||||
uint8_t attempt;
|
||||
|
||||
for (attempt = 0U; attempt < link->config.retries; attempt++) {
|
||||
if (link->io.send(link->io.context, frame) != 0U) {
|
||||
link->sent_messages++;
|
||||
return 1U;
|
||||
}
|
||||
}
|
||||
link->send_errors++;
|
||||
return 0U;
|
||||
}
|
||||
|
||||
@@ -76,22 +39,17 @@ void CanSensor_ConfigDefault(CanSensor_Config *config)
|
||||
return;
|
||||
}
|
||||
config->tx_id = CAN_SENSOR_DEFAULT_TX_ID;
|
||||
config->tx_data_id = CAN_SENSOR_DEFAULT_TX_ID + CAN_SENSOR_DATA_ID_OFFSET;
|
||||
config->rx_id = CAN_SENSOR_DEFAULT_TX_ID;
|
||||
config->rx_data_id = CAN_SENSOR_DEFAULT_TX_ID + CAN_SENSOR_DATA_ID_OFFSET;
|
||||
config->extended = 0U;
|
||||
config->rx_id = CAN_SENSOR_DEFAULT_RX_ID;
|
||||
config->extended = 1U;
|
||||
config->retries = CAN_SENSOR_DEFAULT_RETRIES;
|
||||
}
|
||||
|
||||
uint8_t CanSensor_Init(CanSensor *link, const CanSensor_Io *io,
|
||||
const CanSensor_Config *config)
|
||||
{
|
||||
uint8_t index;
|
||||
|
||||
if ((link == 0) || (io == 0) || (io->send == 0)) {
|
||||
return 0U;
|
||||
}
|
||||
|
||||
link->io = *io;
|
||||
if (config != 0) {
|
||||
link->config = *config;
|
||||
@@ -101,16 +59,15 @@ uint8_t CanSensor_Init(CanSensor *link, const CanSensor_Io *io,
|
||||
if (link->config.tx_id == 0U) {
|
||||
link->config.tx_id = CAN_SENSOR_DEFAULT_TX_ID;
|
||||
}
|
||||
if (link->config.rx_id == 0U) {
|
||||
link->config.rx_id = CAN_SENSOR_DEFAULT_RX_ID;
|
||||
}
|
||||
link->config.tx_id &= CAN_SENSOR_HEADER_MASK;
|
||||
link->config.rx_id &= CAN_SENSOR_HEADER_MASK;
|
||||
link->config.extended = 1U;
|
||||
if (link->config.retries == 0U) {
|
||||
link->config.retries = CAN_SENSOR_DEFAULT_RETRIES;
|
||||
}
|
||||
|
||||
link->rx.command = 0U;
|
||||
link->rx.position = 0U;
|
||||
for (index = 0U; index < CAN_SENSOR_ID_SIZE; index++) {
|
||||
link->rx.id[index] = 0U;
|
||||
}
|
||||
link->rx_preamble = 0U;
|
||||
link->sent_messages = 0U;
|
||||
link->send_errors = 0U;
|
||||
link->received_messages = 0U;
|
||||
@@ -118,114 +75,114 @@ uint8_t CanSensor_Init(CanSensor *link, const CanSensor_Io *io,
|
||||
return 1U;
|
||||
}
|
||||
|
||||
uint8_t CanSensor_BuildPreamble(const CanSensor_Config *config, CanSensor_Frame *frame,
|
||||
uint8_t command, uint16_t position)
|
||||
uint8_t CanSensor_BuildFrame(const CanSensor_Config *config, CanSensor_Frame *frame,
|
||||
uint8_t response, uint8_t position,
|
||||
uint8_t assembly_serial, const uint8_t *data,
|
||||
uint8_t length)
|
||||
{
|
||||
uint8_t index;
|
||||
uint32_t base;
|
||||
|
||||
if ((config == 0) || (frame == 0)) {
|
||||
if ((config == 0) || (frame == 0) || (length > CAN_SENSOR_MAX_DATA)
|
||||
|| ((length != 0U) && (data == 0))) {
|
||||
return 0U;
|
||||
}
|
||||
frame->id = config->tx_id;
|
||||
frame->extended = config->extended;
|
||||
frame->length = CAN_SENSOR_PREAMBLE_SIZE;
|
||||
frame->data[0] = command;
|
||||
/* Позиция занимает два байта в порядке от старшего к младшему. */
|
||||
frame->data[1] = (uint8_t)((position >> 8U) & 0xFFU);
|
||||
frame->data[2] = (uint8_t)(position & 0xFFU);
|
||||
for (index = CAN_SENSOR_PREAMBLE_SIZE; index < CAN_SENSOR_MAX_DATA; index++) {
|
||||
frame->data[index] = 0U;
|
||||
base = (response != 0U) ? config->tx_id : config->rx_id;
|
||||
frame->id = (base & CAN_SENSOR_HEADER_MASK)
|
||||
| ((uint32_t)assembly_serial << 8U) | (uint32_t)position;
|
||||
frame->extended = 1U;
|
||||
frame->length = length;
|
||||
for (index = 0U; index < CAN_SENSOR_MAX_DATA; index++) {
|
||||
frame->data[index] = (index < length) ? data[index] : 0U;
|
||||
}
|
||||
return 1U;
|
||||
}
|
||||
|
||||
uint8_t CanSensor_BuildIdFrame(const CanSensor_Config *config, CanSensor_Frame *frame,
|
||||
const uint8_t *id)
|
||||
{
|
||||
uint8_t index;
|
||||
|
||||
if ((config == 0) || (frame == 0)) {
|
||||
return 0U;
|
||||
}
|
||||
frame->id = can_sensor_tx_data_id(config);
|
||||
frame->extended = config->extended;
|
||||
frame->length = CAN_SENSOR_ID_SIZE;
|
||||
for (index = 0U; index < CAN_SENSOR_ID_SIZE; index++) {
|
||||
frame->data[index] = (id != 0) ? id[index] : 0U;
|
||||
}
|
||||
return 1U;
|
||||
}
|
||||
|
||||
uint8_t CanSensor_Send(CanSensor *link, uint8_t command, uint16_t position,
|
||||
const uint8_t *id)
|
||||
uint8_t CanSensor_SendId(CanSensor *link, uint8_t position,
|
||||
uint8_t assembly_serial, const uint8_t *id)
|
||||
{
|
||||
CanSensor_Frame frame;
|
||||
|
||||
if ((link == 0) || (link->io.send == 0)) {
|
||||
if ((link == 0) || (id == 0) || (link->io.send == 0)) {
|
||||
return 0U;
|
||||
}
|
||||
(void)CanSensor_BuildPreamble(&link->config, &frame, command, position);
|
||||
if (can_sensor_send_frame(link, &frame) == 0U) {
|
||||
if (CanSensor_BuildFrame(&link->config, &frame, 1U, position,
|
||||
assembly_serial, id, CAN_SENSOR_ID_SIZE) == 0U) {
|
||||
link->send_errors++;
|
||||
return 0U;
|
||||
}
|
||||
(void)CanSensor_BuildIdFrame(&link->config, &frame, id);
|
||||
if (can_sensor_send_frame(link, &frame) == 0U) {
|
||||
/* Одиночная преамбула приёмником отбрасывается, позиция не изменится. */
|
||||
link->send_errors++;
|
||||
return 0U;
|
||||
}
|
||||
link->sent_messages++;
|
||||
return 1U;
|
||||
return can_sensor_send_frame(link, &frame);
|
||||
}
|
||||
|
||||
uint8_t CanSensor_SendId(CanSensor *link, uint16_t position, const uint8_t *id)
|
||||
uint8_t CanSensor_SendError(CanSensor *link, uint8_t position,
|
||||
uint8_t assembly_serial, uint8_t result)
|
||||
{
|
||||
if (id == 0) {
|
||||
CanSensor_Frame frame;
|
||||
|
||||
if ((link == 0) || (link->io.send == 0) || (result == CAN_SENSOR_RESULT_OK)) {
|
||||
return 0U;
|
||||
}
|
||||
return CanSensor_Send(link, CAN_SENSOR_CMD_WRITE_POSITION, position, id);
|
||||
if (CanSensor_BuildFrame(&link->config, &frame, 1U, position,
|
||||
assembly_serial, &result, 1U) == 0U) {
|
||||
link->send_errors++;
|
||||
return 0U;
|
||||
}
|
||||
return can_sensor_send_frame(link, &frame);
|
||||
}
|
||||
|
||||
uint8_t CanSensor_HandleFrame(CanSensor *link, const CanSensor_Frame *frame,
|
||||
CanSensor_Message *out)
|
||||
{
|
||||
CanSensor_Message message;
|
||||
uint32_t header;
|
||||
uint8_t index;
|
||||
|
||||
if ((link == 0) || (frame == 0)) {
|
||||
return 0U;
|
||||
}
|
||||
if (frame->extended == 0U) {
|
||||
return 0U;
|
||||
}
|
||||
header = frame->id & CAN_SENSOR_HEADER_MASK;
|
||||
if (header == (link->config.rx_id & CAN_SENSOR_HEADER_MASK)) {
|
||||
message.request = 1U;
|
||||
} else if (header == (link->config.tx_id & CAN_SENSOR_HEADER_MASK)) {
|
||||
message.request = 0U;
|
||||
} else {
|
||||
return 0U;
|
||||
}
|
||||
|
||||
if ((frame->id == can_sensor_rx_id(&link->config))
|
||||
&& (frame->extended == link->config.extended)) {
|
||||
if (frame->length < CAN_SENSOR_PREAMBLE_SIZE) {
|
||||
link->dropped_frames++;
|
||||
return 0U;
|
||||
message.assembly_serial = (uint8_t)((frame->id >> 8U) & 0xFFU);
|
||||
message.position = (uint8_t)(frame->id & 0xFFU);
|
||||
message.result = CAN_SENSOR_RESULT_OK;
|
||||
for (index = 0U; index < CAN_SENSOR_ID_SIZE; index++) {
|
||||
message.id[index] = (index < frame->length) ? frame->data[index] : 0U;
|
||||
}
|
||||
|
||||
if (message.request != 0U) {
|
||||
if (frame->length == 0U) {
|
||||
message.operation = CAN_SENSOR_OPERATION_GET;
|
||||
} else if (frame->length == CAN_SENSOR_ID_SIZE) {
|
||||
message.operation = (can_sensor_rom_is_zero(message.id) != 0U)
|
||||
? CAN_SENSOR_OPERATION_CLEAR
|
||||
: CAN_SENSOR_OPERATION_WRITE;
|
||||
} else {
|
||||
message.operation = CAN_SENSOR_OPERATION_INVALID;
|
||||
message.result = CAN_SENSOR_RESULT_INVALID_DLC;
|
||||
}
|
||||
link->rx.command = frame->data[0];
|
||||
link->rx.position = (uint16_t)(((uint16_t)frame->data[1] << 8U)
|
||||
| frame->data[2]);
|
||||
link->rx_preamble = 1U;
|
||||
return 0U;
|
||||
}
|
||||
|
||||
if ((frame->id != can_sensor_rx_data_id(&link->config))
|
||||
|| (frame->extended != link->config.extended)) {
|
||||
return 0U;
|
||||
}
|
||||
if ((link->rx_preamble == 0U) || (frame->length < CAN_SENSOR_ID_SIZE)) {
|
||||
/* Идентификатор без преамбулы не говорит, в какую позицию его писать. */
|
||||
link->rx_preamble = 0U;
|
||||
} else if (frame->length == CAN_SENSOR_ID_SIZE) {
|
||||
message.operation = CAN_SENSOR_OPERATION_RESPONSE;
|
||||
} else if (frame->length == 1U) {
|
||||
message.operation = CAN_SENSOR_OPERATION_ERROR;
|
||||
message.result = frame->data[0];
|
||||
} else {
|
||||
link->dropped_frames++;
|
||||
return 0U;
|
||||
}
|
||||
for (index = 0U; index < CAN_SENSOR_ID_SIZE; index++) {
|
||||
link->rx.id[index] = frame->data[index];
|
||||
}
|
||||
link->rx_preamble = 0U;
|
||||
|
||||
link->received_messages++;
|
||||
if (out != 0) {
|
||||
*out = link->rx;
|
||||
*out = message;
|
||||
}
|
||||
return 1U;
|
||||
}
|
||||
|
||||
@@ -1,30 +1,19 @@
|
||||
/**
|
||||
* @file can_sensor.h
|
||||
* @brief Портируемая передача идентификаторов датчиков по шине CAN.
|
||||
* @brief Портируемый транспорт SETTINGS для привязки DS18B20 к локации.
|
||||
*
|
||||
* Библиотека не привязана к микроконтроллеру: она не включает заголовки
|
||||
* периферии, не обращается к регистрам и не пользуется прерываниями. Обмен
|
||||
* идёт через таблицу обратных вызовов CanSensor_Io, которую заполняет порт
|
||||
* платы (для этой сборки — @c src/can.c поверх bxCAN STM32F103C8T6).
|
||||
*
|
||||
* Сообщение состоит из двух кадров классического CAN, потому что 64-битный
|
||||
* идентификатор датчика и преамбула вместе не помещаются в восемь байтов
|
||||
* поля данных:
|
||||
* Один Extended CAN-кадр содержит полную операцию. Поля локации находятся в
|
||||
* Body идентификатора SETCAN, а восьмибайтовый ROM — в payload:
|
||||
*
|
||||
* @code
|
||||
* кадр 1 — преамбула, DLC = 2 +---------+----------+
|
||||
* | команда | позиция |
|
||||
* +---------+----------+
|
||||
* кадр 2 — идентификатор, DLC=8 +----------------------------+
|
||||
* | ROM датчика, байты 0..7 |
|
||||
* +----------------------------+
|
||||
* Priority=1, DeviceType=0x7, DeviceID=0xF, MsgType=0xE (SETTINGS)
|
||||
* Body[15:8] = номер сборки, Body[7:0] = позиция
|
||||
* @endcode
|
||||
*
|
||||
* Преамбула несёт команду записи датчика в позицию: первый байт — код
|
||||
* команды (@ref CAN_SENSOR_CMD_WRITE_POSITION), второй — номер позиции в
|
||||
* таблице узла-приёмника. Кадр идентификатора передаётся сразу за преамбулой
|
||||
* и без неё считается недействительным, поэтому приёмник не запишет ROM в
|
||||
* позицию, которая ему не была назначена.
|
||||
* Запрос ПМ использует Route=0 (0x17FEZZYY), ответ прибора — Route=1
|
||||
* (0x1FFEZZYY). DLC=0 означает GET, DLC=8 с ненулевым ROM — SET/REPLACE,
|
||||
* DLC=8 с нулевым ROM — CLEAR. Успешный ответ возвращает текущий ROM с DLC=8,
|
||||
* ошибка — однобайтовый код с DLC=1.
|
||||
*/
|
||||
|
||||
#ifndef CAN_SENSOR_H
|
||||
@@ -32,186 +21,107 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/** Длина идентификатора датчика (ROM 1-Wire) в байтах. */
|
||||
#define CAN_SENSOR_ID_SIZE 8U
|
||||
/** Длина ROM 1-Wire и максимального payload classic CAN. */
|
||||
#define CAN_SENSOR_ID_SIZE 8U
|
||||
#define CAN_SENSOR_MAX_DATA 8U
|
||||
|
||||
/** Длина преамбулы: код команды (1 байт) и номер позиции (2 байта, u16 BE). */
|
||||
#define CAN_SENSOR_PREAMBLE_SIZE 3U
|
||||
/** Назначенный проекту тип сообщения SETCAN SETTINGS. */
|
||||
#define CAN_SENSOR_MSGTYPE_SETTINGS 0xEU
|
||||
|
||||
/** Предельная длина поля данных классического кадра CAN. */
|
||||
#define CAN_SENSOR_MAX_DATA 8U
|
||||
/** Маска полей SETCAN от Priority до MsgType; Body под маской не находится. */
|
||||
#define CAN_SENSOR_HEADER_MASK 0x1FFF0000UL
|
||||
#define CAN_SENSOR_BODY_MASK 0x0000FFFFUL
|
||||
|
||||
/** Код команды «записать идентификатор датчика в позицию». */
|
||||
#define CAN_SENSOR_CMD_WRITE_POSITION 0xA1U
|
||||
/** Базовые ID SETTINGS для выбранных DeviceType=0x7 и DeviceID=0xF. */
|
||||
#define CAN_SENSOR_DEFAULT_TX_ID 0x1FFE0000UL
|
||||
#define CAN_SENSOR_DEFAULT_RX_ID 0x17FE0000UL
|
||||
|
||||
/** Код команды «очистить позицию»; кадр идентификатора передаётся нулевым. */
|
||||
#define CAN_SENSOR_CMD_CLEAR_POSITION 0xA2U
|
||||
/** Число попыток помещения кадра в почтовый ящик. */
|
||||
#define CAN_SENSOR_DEFAULT_RETRIES 3U
|
||||
|
||||
/** Смещение идентификатора кадра данных относительно кадра преамбулы. */
|
||||
#define CAN_SENSOR_DATA_ID_OFFSET 1U
|
||||
/** Операции, полученные из направления, DLC и payload кадра. */
|
||||
#define CAN_SENSOR_OPERATION_GET 0U
|
||||
#define CAN_SENSOR_OPERATION_WRITE 1U
|
||||
#define CAN_SENSOR_OPERATION_CLEAR 2U
|
||||
#define CAN_SENSOR_OPERATION_RESPONSE 3U
|
||||
#define CAN_SENSOR_OPERATION_ERROR 4U
|
||||
#define CAN_SENSOR_OPERATION_INVALID 5U
|
||||
|
||||
/** Число попыток передачи одного кадра по умолчанию. */
|
||||
#define CAN_SENSOR_DEFAULT_RETRIES 3U
|
||||
/** Коды результата SETTINGS, передаваемые ответом с DLC=1. */
|
||||
#define CAN_SENSOR_RESULT_OK 0x00U
|
||||
#define CAN_SENSOR_RESULT_INVALID_DLC 0x01U
|
||||
#define CAN_SENSOR_RESULT_INVALID_ROM_CRC 0x02U
|
||||
#define CAN_SENSOR_RESULT_NOT_FOUND 0x03U
|
||||
#define CAN_SENSOR_RESULT_ALREADY_ASSIGNED 0x04U
|
||||
#define CAN_SENSOR_RESULT_EEPROM_ERROR 0x05U
|
||||
#define CAN_SENSOR_RESULT_INVALID_LOCATION 0x06U
|
||||
#define CAN_SENSOR_RESULT_BUSY 0x07U
|
||||
|
||||
/** Идентификатор кадра преамбулы по умолчанию (стандартный, 11 бит). */
|
||||
#define CAN_SENSOR_DEFAULT_TX_ID 0x200U
|
||||
|
||||
/**
|
||||
* @brief Кадр шины CAN в форме, не зависящей от контроллера.
|
||||
*/
|
||||
/** Кадр CAN в форме, не зависящей от периферийного контроллера. */
|
||||
typedef struct {
|
||||
uint32_t id; /**< Идентификатор кадра, 11 или 29 бит. */
|
||||
uint8_t extended; /**< 1 — расширенный идентификатор. */
|
||||
uint8_t length; /**< Число значащих байтов поля данных. */
|
||||
uint8_t data[CAN_SENSOR_MAX_DATA]; /**< Поле данных кадра. */
|
||||
uint32_t id; /**< Расширенный идентификатор, 29 бит. */
|
||||
uint8_t extended; /**< Для SETTINGS всегда 1. */
|
||||
uint8_t length; /**< DLC, 0..8. */
|
||||
uint8_t data[CAN_SENSOR_MAX_DATA]; /**< Payload. */
|
||||
} CanSensor_Frame;
|
||||
|
||||
/**
|
||||
* @brief Принятое или собранное сообщение: преамбула и идентификатор датчика.
|
||||
*/
|
||||
/** Разобранная команда или ответ SETTINGS. */
|
||||
typedef struct {
|
||||
uint8_t command; /**< Код команды из преамбулы. */
|
||||
uint16_t position; /**< Позиция записи из преамбулы. */
|
||||
uint8_t id[CAN_SENSOR_ID_SIZE]; /**< Идентификатор датчика. */
|
||||
uint8_t operation; /**< CAN_SENSOR_OPERATION_*. */
|
||||
uint8_t request; /**< 1 — запрос Route=0, 0 — ответ Route=1. */
|
||||
uint8_t assembly_serial; /**< Body[15:8], номер сборки Z. */
|
||||
uint8_t position; /**< Body[7:0], позиция Y. */
|
||||
uint8_t result; /**< Код ошибки для ERROR/INVALID. */
|
||||
uint8_t id[CAN_SENSOR_ID_SIZE]; /**< ROM для WRITE/RESPONSE, нули для CLEAR. */
|
||||
} CanSensor_Message;
|
||||
|
||||
/**
|
||||
* @brief Доступ библиотеки к контроллеру CAN.
|
||||
*
|
||||
* Обязателен только @c send; при нулевом @c receive функция CanSensor_Poll()
|
||||
* ничего не делает и узел работает только на передачу.
|
||||
*/
|
||||
/** Доступ переносимой части к CAN-контроллеру. */
|
||||
typedef struct {
|
||||
/** Ставит кадр в очередь передачи; 1 — кадр принят контроллером. */
|
||||
uint8_t (*send)(void *context, const CanSensor_Frame *frame);
|
||||
/** Забирает принятый кадр; 1 — кадр получен, 0 — очередь пуста. */
|
||||
uint8_t (*receive)(void *context, CanSensor_Frame *frame);
|
||||
void *context; /**< Контекст порта, передаётся вызовам без изменений. */
|
||||
void *context;
|
||||
} CanSensor_Io;
|
||||
|
||||
/**
|
||||
* @brief Идентификаторы кадров и режим передачи.
|
||||
*
|
||||
* Нулевые поля заменяются значениями по умолчанию вызовом
|
||||
* CanSensor_ConfigDefault() или самой CanSensor_Init().
|
||||
*/
|
||||
/** Базовые идентификаторы SETTINGS и режим транспорта. */
|
||||
typedef struct {
|
||||
uint32_t tx_id; /**< Идентификатор кадра преамбулы при передаче. */
|
||||
uint32_t tx_data_id; /**< Идентификатор кадра ROM; 0 — @c tx_id + 1. */
|
||||
uint32_t rx_id; /**< Ожидаемая преамбула при приёме; 0 — @c tx_id. */
|
||||
uint32_t rx_data_id; /**< Ожидаемый кадр ROM; 0 — @c rx_id + 1. */
|
||||
uint8_t extended; /**< 1 — расширенные идентификаторы 29 бит. */
|
||||
uint8_t retries; /**< Попыток передачи кадра; 0 — значение по умолчанию. */
|
||||
uint32_t tx_id; /**< База ответа 0x1FFE0000, Body должен быть нулевым. */
|
||||
uint32_t rx_id; /**< База запроса 0x17FE0000, Body должен быть нулевым. */
|
||||
uint8_t extended; /**< 1 — Extended CAN ID. */
|
||||
uint8_t retries; /**< Число попыток передачи. */
|
||||
} CanSensor_Config;
|
||||
|
||||
/**
|
||||
* @brief Состояние узла: настройки, счётчики и сборка принимаемого сообщения.
|
||||
*/
|
||||
/** Состояние транспорта и диагностические счётчики. */
|
||||
typedef struct {
|
||||
CanSensor_Io io; /**< Обратные вызовы порта. */
|
||||
CanSensor_Config config; /**< Идентификаторы кадров и режим передачи. */
|
||||
CanSensor_Message rx; /**< Собираемое сообщение приёма. */
|
||||
uint8_t rx_preamble; /**< 1 — преамбула принята, ожидается кадр ROM. */
|
||||
uint32_t sent_messages; /**< Полностью переданные сообщения. */
|
||||
uint32_t send_errors; /**< Сообщения, не ушедшие в шину. */
|
||||
uint32_t received_messages; /**< Полностью принятые сообщения. */
|
||||
uint32_t dropped_frames; /**< Кадры без преамбулы или с неверной длиной. */
|
||||
CanSensor_Io io;
|
||||
CanSensor_Config config;
|
||||
uint32_t sent_messages;
|
||||
uint32_t send_errors;
|
||||
uint32_t received_messages;
|
||||
uint32_t dropped_frames;
|
||||
} CanSensor;
|
||||
|
||||
/**
|
||||
* @brief Заполняет настройки значениями по умолчанию.
|
||||
*
|
||||
* Идентификатор преамбулы — @ref CAN_SENSOR_DEFAULT_TX_ID, кадр ROM идёт
|
||||
* следующим идентификатором, приём настроен на те же значения.
|
||||
*
|
||||
* @param config Настройки, принадлежащие вызывающему коду.
|
||||
*/
|
||||
void CanSensor_ConfigDefault(CanSensor_Config *config);
|
||||
|
||||
/**
|
||||
* @brief Готовит узел к работе.
|
||||
*
|
||||
* @param link Состояние узла, принадлежащее вызывающему коду.
|
||||
* @param io Обратные вызовы порта; копируются внутрь состояния.
|
||||
* @param config Настройки либо 0 для значений по умолчанию.
|
||||
* @return 1 при успешной настройке, 0 при неполных аргументах.
|
||||
*/
|
||||
uint8_t CanSensor_Init(CanSensor *link, const CanSensor_Io *io,
|
||||
const CanSensor_Config *config);
|
||||
|
||||
/**
|
||||
* @brief Собирает кадр преамбулы.
|
||||
*
|
||||
* @param config Настройки узла.
|
||||
* @param frame Кадр приёмника.
|
||||
* @param command Код команды, например @ref CAN_SENSOR_CMD_WRITE_POSITION.
|
||||
* @param position Номер позиции записи датчика.
|
||||
* @return 1 при успешной сборке, иначе 0.
|
||||
*/
|
||||
uint8_t CanSensor_BuildPreamble(const CanSensor_Config *config, CanSensor_Frame *frame,
|
||||
uint8_t command, uint16_t position);
|
||||
/** Собирает SETTINGS-кадр на базе tx_id или rx_id. */
|
||||
uint8_t CanSensor_BuildFrame(const CanSensor_Config *config, CanSensor_Frame *frame,
|
||||
uint8_t response, uint8_t position,
|
||||
uint8_t assembly_serial, const uint8_t *data,
|
||||
uint8_t length);
|
||||
|
||||
/**
|
||||
* @brief Собирает кадр идентификатора датчика.
|
||||
*
|
||||
* @param config Настройки узла.
|
||||
* @param frame Кадр приёмника.
|
||||
* @param id Идентификатор датчика длиной @ref CAN_SENSOR_ID_SIZE либо 0 для
|
||||
* нулевого кадра команды очистки.
|
||||
* @return 1 при успешной сборке, иначе 0.
|
||||
*/
|
||||
uint8_t CanSensor_BuildIdFrame(const CanSensor_Config *config, CanSensor_Frame *frame,
|
||||
const uint8_t *id);
|
||||
/** Передаёт успешный ответ с текущим ROM (DLC=8). */
|
||||
uint8_t CanSensor_SendId(CanSensor *link, uint8_t position,
|
||||
uint8_t assembly_serial, const uint8_t *id);
|
||||
|
||||
/**
|
||||
* @brief Передаёт преамбулу и идентификатор датчика.
|
||||
*
|
||||
* Кадры уходят подряд; при отказе контроллера передача повторяется
|
||||
* @c retries раз. Если преамбула ушла, а кадр ROM — нет, сообщение считается
|
||||
* несостоявшимся: приёмник отбросит одиночную преамбулу.
|
||||
*
|
||||
* @param link Состояние узла.
|
||||
* @param command Код команды преамбулы.
|
||||
* @param position Номер позиции записи датчика.
|
||||
* @param id Идентификатор датчика либо 0 для команды очистки позиции.
|
||||
* @return 1, если оба кадра приняты контроллером, иначе 0.
|
||||
*/
|
||||
uint8_t CanSensor_Send(CanSensor *link, uint8_t command, uint16_t position,
|
||||
const uint8_t *id);
|
||||
/** Передаёт ошибку SETTINGS (DLC=1). */
|
||||
uint8_t CanSensor_SendError(CanSensor *link, uint8_t position,
|
||||
uint8_t assembly_serial, uint8_t result);
|
||||
|
||||
/**
|
||||
* @brief Передаёт команду записи датчика в позицию.
|
||||
*
|
||||
* Краткая форма CanSensor_Send() с кодом @ref CAN_SENSOR_CMD_WRITE_POSITION.
|
||||
*
|
||||
* @param link Состояние узла.
|
||||
* @param position Номер позиции записи датчика.
|
||||
* @param id Идентификатор датчика длиной @ref CAN_SENSOR_ID_SIZE.
|
||||
* @return 1, если оба кадра приняты контроллером, иначе 0.
|
||||
*/
|
||||
uint8_t CanSensor_SendId(CanSensor *link, uint16_t position, const uint8_t *id);
|
||||
|
||||
/**
|
||||
* @brief Разбирает принятый кадр и собирает из пары кадров сообщение.
|
||||
*
|
||||
* Кадр идентификатора без предшествующей преамбулы отбрасывается, а новая
|
||||
* преамбула заменяет незавершённую: сборка не требует таймера.
|
||||
*
|
||||
* @param link Состояние узла.
|
||||
* @param frame Принятый кадр.
|
||||
* @param out Приёмник готового сообщения либо 0.
|
||||
* @return 1, если сообщение собрано полностью, иначе 0.
|
||||
*/
|
||||
uint8_t CanSensor_HandleFrame(CanSensor *link, const CanSensor_Frame *frame,
|
||||
CanSensor_Message *out);
|
||||
|
||||
/**
|
||||
* @brief Забирает кадры у порта и возвращает первое собранное сообщение.
|
||||
*
|
||||
* @param link Состояние узла.
|
||||
* @param out Приёмник сообщения либо 0.
|
||||
* @return 1, если сообщение собрано, иначе 0.
|
||||
*/
|
||||
uint8_t CanSensor_Poll(CanSensor *link, CanSensor_Message *out);
|
||||
|
||||
#endif /* CAN_SENSOR_H */
|
||||
|
||||
@@ -23,7 +23,9 @@
|
||||
|---|---|---|
|
||||
| `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` |
|
||||
| `ports/stm32f1/` | тайминги Maxim, вывод с открытым стоком, задержки по DWT | CMSIS `stm32f10x.h` |
|
||||
| `ports/stm32f1/` | STM32F103: тайминги Maxim, открытый сток, задержки по DWT | CMSIS `stm32f10x.h` |
|
||||
| `ports/stm32g4/` | то же для STM32G431 и STM32G474 | CMSIS `stm32g4xx.h` |
|
||||
| `ports/stm32f4/` | то же для STM32F407VET6 и родственных F4 | CMSIS `stm32f4xx.h` |
|
||||
| `tests/` | симулятор шины с виртуальными датчиками и host-тесты | — |
|
||||
|
||||
## Порт: что нужно от платформы
|
||||
@@ -48,7 +50,7 @@ uint8_t OneWire_ReadBit(void);
|
||||
микросекундных задержек — счётчик тактов ядра `DWT->CYCCNT`.
|
||||
|
||||
Привязку вывода порт берёт из `onewire_config.h`, который предоставляет
|
||||
приложение — шаблон лежит рядом:
|
||||
приложение — шаблон `onewire_config.f103.template.h` лежит рядом:
|
||||
|
||||
```c
|
||||
#include "stm32f10x.h"
|
||||
@@ -60,6 +62,33 @@ uint8_t OneWire_ReadBit(void);
|
||||
|
||||
Требуется внешняя подтяжка 4.7 кОм к питанию.
|
||||
|
||||
### Готовые порты STM32G4 и STM32F4
|
||||
|
||||
Логика та же, что и в порте STM32F1: открытый сток, чтение через `IDR`,
|
||||
задержки по `DWT->CYCCNT`, запрет прерываний только на один тайм-слот.
|
||||
Отличается лишь настройка вывода — у G4 и F4 это `MODER`/`OTYPER`/
|
||||
`OSPEEDR`/`PUPDR` вместо `CRL`/`CRH`, а низкий уровень выставляется
|
||||
через верхнюю половину `BSRR`.
|
||||
|
||||
STM32G431 и STM32G474 обслуживает один порт `ports/stm32g4/`: блок GPIO и
|
||||
шина тактирования у них совпадают, различается только CMSIS-заголовок в
|
||||
`onewire_config.h`. Шаблоны лежат рядом — по одному на каждый камень:
|
||||
|
||||
```c
|
||||
/* ports/stm32g4/onewire_config.g474.template.h */
|
||||
#include "stm32g4xx.h"
|
||||
|
||||
#define ONEWIRE_GPIO GPIOA
|
||||
#define ONEWIRE_PIN 1U
|
||||
#define ONEWIRE_GPIO_CLOCK RCC_AHB2ENR_GPIOAEN /* у F4 — RCC_AHB1ENR_GPIOAEN */
|
||||
```
|
||||
|
||||
Порт `ports/stm32f4/` написан под STM32F407VET6 и подходит любому F4 с тем
|
||||
же блоком GPIO; тактирование включается в `RCC->AHB1ENR`.
|
||||
|
||||
Подтяжка 4.7 кОм — внешняя и здесь: внутренняя `PUPDR` намеренно
|
||||
оставлена выключенной, её сопротивления для шины не хватает.
|
||||
|
||||
## Использование
|
||||
|
||||
```c
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* и OneWire_ReadBit.
|
||||
*
|
||||
* Чтобы перенести библиотеку на другой микроконтроллер, достаточно написать
|
||||
* эти пять функций - остальное собирается как есть.
|
||||
* эти пять функций - остальное собирается как есть. ТЕСТ
|
||||
*/
|
||||
|
||||
#ifndef ONEWIRE_H
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @file onewire_config.template.h
|
||||
* @brief Шаблон привязки шины 1-Wire для порта STM32F1.
|
||||
* @file onewire_config.f103.template.h
|
||||
* @brief Шаблон привязки шины 1-Wire для STM32F103 (порт stm32f1).
|
||||
*
|
||||
* Скопируйте в своё приложение под именем onewire_config.h и положите на
|
||||
* путь поиска заголовков. Библиотека сама этот файл не подставляет:
|
||||
@@ -8,7 +8,8 @@
|
||||
* терять байты USART во время обмена.
|
||||
*
|
||||
* Привязка вывода приходит из onewire_config.h, который предоставляет
|
||||
* приложение. Шаблон - onewire_config.template.h рядом с этим файлом.
|
||||
* приложение. Шаблон для STM32F103 —
|
||||
* onewire_config.f103.template.h рядом с этим файлом.
|
||||
*/
|
||||
|
||||
#include "onewire.h"
|
||||
|
||||
26
c/ds18b20/ports/stm32f4/onewire_config.f407.template.h
Normal file
26
c/ds18b20/ports/stm32f4/onewire_config.f407.template.h
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @file onewire_config.f407.template.h
|
||||
* @brief Шаблон привязки шины 1-Wire для STM32F407VET6 (порт stm32f4).
|
||||
*
|
||||
* Скопируйте в своё приложение под именем onewire_config.h и положите на
|
||||
* путь поиска заголовков. Библиотека сама этот файл не подставляет:
|
||||
* привязка выводов принадлежит плате, а не драйверу.
|
||||
*
|
||||
* Требуется внешняя подтяжка 4.7 кОм к питанию.
|
||||
*/
|
||||
|
||||
#ifndef ONEWIRE_CONFIG_H
|
||||
#define ONEWIRE_CONFIG_H
|
||||
|
||||
#include "stm32f4xx.h"
|
||||
|
||||
/** Порт вывода шины. */
|
||||
#define ONEWIRE_GPIO GPIOA
|
||||
|
||||
/** Номер вывода в порту, 0..15. */
|
||||
#define ONEWIRE_PIN 1U
|
||||
|
||||
/** Бит включения тактирования порта в RCC->AHB1ENR. */
|
||||
#define ONEWIRE_GPIO_CLOCK RCC_AHB1ENR_GPIOAEN
|
||||
|
||||
#endif /* ONEWIRE_CONFIG_H */
|
||||
170
c/ds18b20/ports/stm32f4/onewire_stm32f4.c
Normal file
170
c/ds18b20/ports/stm32f4/onewire_stm32f4.c
Normal file
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* @file onewire_stm32f4.c
|
||||
* @brief Порт шины 1-Wire на STM32F4: тайминги, вывод и задержки.
|
||||
*
|
||||
* Написан под STM32F407VET6 и подходит любому STM32F4 с тем же блоком GPIO:
|
||||
* различается только CMSIS-заголовок, который подставляет onewire_config.h.
|
||||
*
|
||||
* Вывод постоянно настроен как выход с открытым стоком: чтение уровня
|
||||
* выполняется через регистр IDR, поэтому переключать режим не требуется.
|
||||
* Прерывания запрещаются только на длительность одного тайм-слота, чтобы не
|
||||
* терять байты USART во время обмена.
|
||||
*
|
||||
* Привязка вывода приходит из onewire_config.h, который предоставляет
|
||||
* приложение. Шаблон — onewire_config.f407.template.h рядом с этим файлом.
|
||||
*/
|
||||
|
||||
#include "onewire.h"
|
||||
|
||||
#include "onewire_config.h"
|
||||
|
||||
#if !defined(ONEWIRE_GPIO) || !defined(ONEWIRE_PIN) || !defined(ONEWIRE_GPIO_CLOCK)
|
||||
#error "onewire_config.h должен задать ONEWIRE_GPIO, ONEWIRE_PIN и ONEWIRE_GPIO_CLOCK"
|
||||
#endif
|
||||
|
||||
/** Длительности тайм-слотов в микросекундах согласно рекомендациям Maxim. */
|
||||
#define OW_RESET_LOW_US 480U
|
||||
#define OW_PRESENCE_WAIT_US 70U
|
||||
#define OW_RESET_TAIL_US 410U
|
||||
#define OW_WRITE1_LOW_US 6U
|
||||
#define OW_WRITE1_TAIL_US 64U
|
||||
#define OW_WRITE0_LOW_US 60U
|
||||
#define OW_WRITE0_TAIL_US 10U
|
||||
#define OW_READ_LOW_US 6U
|
||||
#define OW_READ_SAMPLE_US 9U
|
||||
#define OW_READ_TAIL_US 55U
|
||||
|
||||
/** Число тактов ядра на микросекунду; заполняется в OneWire_Init. */
|
||||
static uint32_t g_cycles_per_us = 168U;
|
||||
|
||||
/**
|
||||
* @brief Переводит вывод шины в активный низкий уровень.
|
||||
*/
|
||||
static void ow_drive_low(void)
|
||||
{
|
||||
ONEWIRE_GPIO->BSRR = (uint32_t)(1UL << (ONEWIRE_PIN + 16U));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Отпускает шину; уровень поднимает внешний резистор подтяжки.
|
||||
*/
|
||||
static void ow_release(void)
|
||||
{
|
||||
ONEWIRE_GPIO->BSRR = (uint32_t)(1UL << ONEWIRE_PIN);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Читает текущий уровень на выводе шины.
|
||||
*
|
||||
* @return 1 для высокого уровня, 0 для низкого.
|
||||
*/
|
||||
static uint8_t ow_read_pin(void)
|
||||
{
|
||||
return (uint8_t)((ONEWIRE_GPIO->IDR >> ONEWIRE_PIN) & 1U);
|
||||
}
|
||||
|
||||
void OneWire_DelayUs(uint32_t microseconds)
|
||||
{
|
||||
const uint32_t start = DWT->CYCCNT;
|
||||
const uint32_t ticks = microseconds * g_cycles_per_us;
|
||||
|
||||
while ((DWT->CYCCNT - start) < ticks) {
|
||||
__NOP();
|
||||
}
|
||||
}
|
||||
|
||||
void OneWire_Init(void)
|
||||
{
|
||||
uint32_t config;
|
||||
|
||||
g_cycles_per_us = SystemCoreClock / 1000000U;
|
||||
if (g_cycles_per_us == 0U) {
|
||||
g_cycles_per_us = 1U;
|
||||
}
|
||||
|
||||
/* Счётчик тактов ядра — единственный источник микросекундных задержек. */
|
||||
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
|
||||
DWT->CYCCNT = 0U;
|
||||
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
|
||||
|
||||
RCC->AHB1ENR |= ONEWIRE_GPIO_CLOCK;
|
||||
|
||||
/* Открытый сток: OTYPER = 1. */
|
||||
ONEWIRE_GPIO->OTYPER |= (uint32_t)(1UL << ONEWIRE_PIN);
|
||||
|
||||
/* Подтяжка внешняя, внутренняя не нужна: PUPDR = 00. */
|
||||
config = ONEWIRE_GPIO->PUPDR;
|
||||
config &= ~(0x3UL << (ONEWIRE_PIN * 2U));
|
||||
ONEWIRE_GPIO->PUPDR = config;
|
||||
|
||||
/* Максимальная скорость фронта: OSPEEDR = 11. */
|
||||
config = ONEWIRE_GPIO->OSPEEDR;
|
||||
config |= (0x3UL << (ONEWIRE_PIN * 2U));
|
||||
ONEWIRE_GPIO->OSPEEDR = config;
|
||||
|
||||
/* Уровень отпускаем до перевода в выход, чтобы не дёрнуть шину вниз. */
|
||||
ow_release();
|
||||
|
||||
/* Обычный выход: MODER = 01. */
|
||||
config = ONEWIRE_GPIO->MODER;
|
||||
config &= ~(0x3UL << (ONEWIRE_PIN * 2U));
|
||||
config |= (0x1UL << (ONEWIRE_PIN * 2U));
|
||||
ONEWIRE_GPIO->MODER = config;
|
||||
|
||||
OneWire_DelayUs(OW_RESET_LOW_US);
|
||||
}
|
||||
|
||||
uint8_t OneWire_Reset(void)
|
||||
{
|
||||
uint32_t primask;
|
||||
uint8_t presence;
|
||||
|
||||
/* Удлинение импульса сброса безопасно, поэтому прерывания не запрещаем. */
|
||||
ow_drive_low();
|
||||
OneWire_DelayUs(OW_RESET_LOW_US);
|
||||
|
||||
primask = __get_PRIMASK();
|
||||
__disable_irq();
|
||||
ow_release();
|
||||
OneWire_DelayUs(OW_PRESENCE_WAIT_US);
|
||||
presence = (uint8_t)(ow_read_pin() == 0U);
|
||||
__set_PRIMASK(primask);
|
||||
|
||||
OneWire_DelayUs(OW_RESET_TAIL_US);
|
||||
return (uint8_t)(presence && (ow_read_pin() != 0U));
|
||||
}
|
||||
|
||||
void OneWire_WriteBit(uint8_t bit)
|
||||
{
|
||||
const uint32_t primask = __get_PRIMASK();
|
||||
|
||||
__disable_irq();
|
||||
ow_drive_low();
|
||||
if (bit != 0U) {
|
||||
OneWire_DelayUs(OW_WRITE1_LOW_US);
|
||||
ow_release();
|
||||
OneWire_DelayUs(OW_WRITE1_TAIL_US);
|
||||
} else {
|
||||
OneWire_DelayUs(OW_WRITE0_LOW_US);
|
||||
ow_release();
|
||||
OneWire_DelayUs(OW_WRITE0_TAIL_US);
|
||||
}
|
||||
__set_PRIMASK(primask);
|
||||
}
|
||||
|
||||
uint8_t OneWire_ReadBit(void)
|
||||
{
|
||||
const uint32_t primask = __get_PRIMASK();
|
||||
uint8_t bit;
|
||||
|
||||
__disable_irq();
|
||||
ow_drive_low();
|
||||
OneWire_DelayUs(OW_READ_LOW_US);
|
||||
ow_release();
|
||||
OneWire_DelayUs(OW_READ_SAMPLE_US);
|
||||
bit = ow_read_pin();
|
||||
__set_PRIMASK(primask);
|
||||
|
||||
OneWire_DelayUs(OW_READ_TAIL_US);
|
||||
return bit;
|
||||
}
|
||||
26
c/ds18b20/ports/stm32g4/onewire_config.g431.template.h
Normal file
26
c/ds18b20/ports/stm32g4/onewire_config.g431.template.h
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @file onewire_config.g431.template.h
|
||||
* @brief Шаблон привязки шины 1-Wire для STM32G431 (порт stm32g4).
|
||||
*
|
||||
* Скопируйте в своё приложение под именем onewire_config.h и положите на
|
||||
* путь поиска заголовков. Библиотека сама этот файл не подставляет:
|
||||
* привязка выводов принадлежит плате, а не драйверу.
|
||||
*
|
||||
* Требуется внешняя подтяжка 4.7 кОм к питанию.
|
||||
*/
|
||||
|
||||
#ifndef ONEWIRE_CONFIG_H
|
||||
#define ONEWIRE_CONFIG_H
|
||||
|
||||
#include "stm32g4xx.h"
|
||||
|
||||
/** Порт вывода шины. */
|
||||
#define ONEWIRE_GPIO GPIOA
|
||||
|
||||
/** Номер вывода в порту, 0..15. */
|
||||
#define ONEWIRE_PIN 1U
|
||||
|
||||
/** Бит включения тактирования порта в RCC->AHB2ENR. */
|
||||
#define ONEWIRE_GPIO_CLOCK RCC_AHB2ENR_GPIOAEN
|
||||
|
||||
#endif /* ONEWIRE_CONFIG_H */
|
||||
26
c/ds18b20/ports/stm32g4/onewire_config.g474.template.h
Normal file
26
c/ds18b20/ports/stm32g4/onewire_config.g474.template.h
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @file onewire_config.g474.template.h
|
||||
* @brief Шаблон привязки шины 1-Wire для STM32G474 (порт stm32g4).
|
||||
*
|
||||
* Скопируйте в своё приложение под именем onewire_config.h и положите на
|
||||
* путь поиска заголовков. Библиотека сама этот файл не подставляет:
|
||||
* привязка выводов принадлежит плате, а не драйверу.
|
||||
*
|
||||
* Требуется внешняя подтяжка 4.7 кОм к питанию.
|
||||
*/
|
||||
|
||||
#ifndef ONEWIRE_CONFIG_H
|
||||
#define ONEWIRE_CONFIG_H
|
||||
|
||||
#include "stm32g4xx.h"
|
||||
|
||||
/** Порт вывода шины. */
|
||||
#define ONEWIRE_GPIO GPIOA
|
||||
|
||||
/** Номер вывода в порту, 0..15. */
|
||||
#define ONEWIRE_PIN 1U
|
||||
|
||||
/** Бит включения тактирования порта в RCC->AHB2ENR. */
|
||||
#define ONEWIRE_GPIO_CLOCK RCC_AHB2ENR_GPIOAEN
|
||||
|
||||
#endif /* ONEWIRE_CONFIG_H */
|
||||
172
c/ds18b20/ports/stm32g4/onewire_stm32g4.c
Normal file
172
c/ds18b20/ports/stm32g4/onewire_stm32g4.c
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* @file onewire_stm32g4.c
|
||||
* @brief Порт шины 1-Wire на STM32G4: тайминги, вывод и задержки.
|
||||
*
|
||||
* Подходит и STM32G431, и STM32G474 — периферия GPIO и шина тактирования
|
||||
* у них одинаковы, различается только CMSIS-заголовок, который подставляет
|
||||
* onewire_config.h.
|
||||
*
|
||||
* Вывод постоянно настроен как выход с открытым стоком: чтение уровня
|
||||
* выполняется через регистр IDR, поэтому переключать режим не требуется.
|
||||
* Прерывания запрещаются только на длительность одного тайм-слота, чтобы не
|
||||
* терять байты USART во время обмена.
|
||||
*
|
||||
* Привязка вывода приходит из onewire_config.h, который предоставляет
|
||||
* приложение. Шаблоны — onewire_config.g431.template.h и
|
||||
* onewire_config.g474.template.h рядом с этим файлом.
|
||||
*/
|
||||
|
||||
#include "onewire.h"
|
||||
|
||||
#include "onewire_config.h"
|
||||
|
||||
#if !defined(ONEWIRE_GPIO) || !defined(ONEWIRE_PIN) || !defined(ONEWIRE_GPIO_CLOCK)
|
||||
#error "onewire_config.h должен задать ONEWIRE_GPIO, ONEWIRE_PIN и ONEWIRE_GPIO_CLOCK"
|
||||
#endif
|
||||
|
||||
/** Длительности тайм-слотов в микросекундах согласно рекомендациям Maxim. */
|
||||
#define OW_RESET_LOW_US 480U
|
||||
#define OW_PRESENCE_WAIT_US 70U
|
||||
#define OW_RESET_TAIL_US 410U
|
||||
#define OW_WRITE1_LOW_US 6U
|
||||
#define OW_WRITE1_TAIL_US 64U
|
||||
#define OW_WRITE0_LOW_US 60U
|
||||
#define OW_WRITE0_TAIL_US 10U
|
||||
#define OW_READ_LOW_US 6U
|
||||
#define OW_READ_SAMPLE_US 9U
|
||||
#define OW_READ_TAIL_US 55U
|
||||
|
||||
/** Число тактов ядра на микросекунду; заполняется в OneWire_Init. */
|
||||
static uint32_t g_cycles_per_us = 170U;
|
||||
|
||||
/**
|
||||
* @brief Переводит вывод шины в активный низкий уровень.
|
||||
*/
|
||||
static void ow_drive_low(void)
|
||||
{
|
||||
ONEWIRE_GPIO->BSRR = (uint32_t)(1UL << (ONEWIRE_PIN + 16U));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Отпускает шину; уровень поднимает внешний резистор подтяжки.
|
||||
*/
|
||||
static void ow_release(void)
|
||||
{
|
||||
ONEWIRE_GPIO->BSRR = (uint32_t)(1UL << ONEWIRE_PIN);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Читает текущий уровень на выводе шины.
|
||||
*
|
||||
* @return 1 для высокого уровня, 0 для низкого.
|
||||
*/
|
||||
static uint8_t ow_read_pin(void)
|
||||
{
|
||||
return (uint8_t)((ONEWIRE_GPIO->IDR >> ONEWIRE_PIN) & 1U);
|
||||
}
|
||||
|
||||
void OneWire_DelayUs(uint32_t microseconds)
|
||||
{
|
||||
const uint32_t start = DWT->CYCCNT;
|
||||
const uint32_t ticks = microseconds * g_cycles_per_us;
|
||||
|
||||
while ((DWT->CYCCNT - start) < ticks) {
|
||||
__NOP();
|
||||
}
|
||||
}
|
||||
|
||||
void OneWire_Init(void)
|
||||
{
|
||||
uint32_t config;
|
||||
|
||||
g_cycles_per_us = SystemCoreClock / 1000000U;
|
||||
if (g_cycles_per_us == 0U) {
|
||||
g_cycles_per_us = 1U;
|
||||
}
|
||||
|
||||
/* Счётчик тактов ядра — единственный источник микросекундных задержек. */
|
||||
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
|
||||
DWT->CYCCNT = 0U;
|
||||
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
|
||||
|
||||
RCC->AHB2ENR |= ONEWIRE_GPIO_CLOCK;
|
||||
|
||||
/* Открытый сток: OTYPER = 1. */
|
||||
ONEWIRE_GPIO->OTYPER |= (uint32_t)(1UL << ONEWIRE_PIN);
|
||||
|
||||
/* Подтяжка внешняя, внутренняя не нужна: PUPDR = 00. */
|
||||
config = ONEWIRE_GPIO->PUPDR;
|
||||
config &= ~(0x3UL << (ONEWIRE_PIN * 2U));
|
||||
ONEWIRE_GPIO->PUPDR = config;
|
||||
|
||||
/* Максимальная скорость фронта: OSPEEDR = 11. */
|
||||
config = ONEWIRE_GPIO->OSPEEDR;
|
||||
config |= (0x3UL << (ONEWIRE_PIN * 2U));
|
||||
ONEWIRE_GPIO->OSPEEDR = config;
|
||||
|
||||
/* Уровень отпускаем до перевода в выход, чтобы не дёрнуть шину вниз. */
|
||||
ow_release();
|
||||
|
||||
/* Обычный выход: MODER = 01. */
|
||||
config = ONEWIRE_GPIO->MODER;
|
||||
config &= ~(0x3UL << (ONEWIRE_PIN * 2U));
|
||||
config |= (0x1UL << (ONEWIRE_PIN * 2U));
|
||||
ONEWIRE_GPIO->MODER = config;
|
||||
|
||||
OneWire_DelayUs(OW_RESET_LOW_US);
|
||||
}
|
||||
|
||||
uint8_t OneWire_Reset(void)
|
||||
{
|
||||
uint32_t primask;
|
||||
uint8_t presence;
|
||||
|
||||
/* Удлинение импульса сброса безопасно, поэтому прерывания не запрещаем. */
|
||||
ow_drive_low();
|
||||
OneWire_DelayUs(OW_RESET_LOW_US);
|
||||
|
||||
primask = __get_PRIMASK();
|
||||
__disable_irq();
|
||||
ow_release();
|
||||
OneWire_DelayUs(OW_PRESENCE_WAIT_US);
|
||||
presence = (uint8_t)(ow_read_pin() == 0U);
|
||||
__set_PRIMASK(primask);
|
||||
|
||||
OneWire_DelayUs(OW_RESET_TAIL_US);
|
||||
return (uint8_t)(presence && (ow_read_pin() != 0U));
|
||||
}
|
||||
|
||||
void OneWire_WriteBit(uint8_t bit)
|
||||
{
|
||||
const uint32_t primask = __get_PRIMASK();
|
||||
|
||||
__disable_irq();
|
||||
ow_drive_low();
|
||||
if (bit != 0U) {
|
||||
OneWire_DelayUs(OW_WRITE1_LOW_US);
|
||||
ow_release();
|
||||
OneWire_DelayUs(OW_WRITE1_TAIL_US);
|
||||
} else {
|
||||
OneWire_DelayUs(OW_WRITE0_LOW_US);
|
||||
ow_release();
|
||||
OneWire_DelayUs(OW_WRITE0_TAIL_US);
|
||||
}
|
||||
__set_PRIMASK(primask);
|
||||
}
|
||||
|
||||
uint8_t OneWire_ReadBit(void)
|
||||
{
|
||||
const uint32_t primask = __get_PRIMASK();
|
||||
uint8_t bit;
|
||||
|
||||
__disable_irq();
|
||||
ow_drive_low();
|
||||
OneWire_DelayUs(OW_READ_LOW_US);
|
||||
ow_release();
|
||||
OneWire_DelayUs(OW_READ_SAMPLE_US);
|
||||
bit = ow_read_pin();
|
||||
__set_PRIMASK(primask);
|
||||
|
||||
OneWire_DelayUs(OW_READ_TAIL_US);
|
||||
return bit;
|
||||
}
|
||||
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
28
c/set-protocol/CMakeLists.txt
Normal file
28
c/set-protocol/CMakeLists.txt
Normal file
@@ -0,0 +1,28 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
project(set_protocol C)
|
||||
|
||||
set(CMAKE_C_STANDARD 99)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
|
||||
add_library(set_protocol STATIC
|
||||
src/set_protocol.c
|
||||
src/set_can.c
|
||||
src/set_firmware.c
|
||||
src/set_telemetry.c
|
||||
)
|
||||
|
||||
target_include_directories(set_protocol PUBLIC include)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(set_protocol PRIVATE /W4 /WX)
|
||||
else()
|
||||
target_compile_options(set_protocol PRIVATE -Wall -Wextra -Wpedantic -Werror)
|
||||
endif()
|
||||
|
||||
option(SETP_BUILD_TESTS "Build host tests" ON)
|
||||
if(SETP_BUILD_TESTS)
|
||||
enable_testing()
|
||||
add_executable(test_set_protocol tests/test_set_protocol.c)
|
||||
target_link_libraries(test_set_protocol PRIVATE set_protocol)
|
||||
add_test(NAME set_protocol COMMAND test_set_protocol)
|
||||
endif()
|
||||
63
c/set-protocol/MIGRATION.md
Normal file
63
c/set-protocol/MIGRATION.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Переход на SET protocol v2
|
||||
|
||||
Цель перехода — не поддерживать два развивающихся протокола. GUI protocol v1
|
||||
и ProtoCAN после начала миграции получают только исправления критических ошибок;
|
||||
новые команды добавляются исключительно в SETP v2.
|
||||
|
||||
## Этап 1. Транспорт и обнаружение
|
||||
|
||||
1. Подключить `set_protocol.c` к прошивке.
|
||||
2. Реализовать v2 `PING`, `DEVICE_INFO` и `CAPABILITIES`.
|
||||
3. В SETGUI при подключении отправлять v2 PING.
|
||||
4. Только после полного тайм-аута пробовать v1 PING для старого оборудования.
|
||||
5. После первого ответа фиксировать версию до закрытия соединения.
|
||||
|
||||
Нельзя разбирать v1 и v2 одним header после поля version: их размеры различны.
|
||||
|
||||
## Этап 2. Одна карта данных
|
||||
|
||||
Все измерения, уставки и состояния получают стабильный 32-битный адрес и
|
||||
описание в `CATALOG`. Обычные операции изделия переводятся на:
|
||||
|
||||
- `READ` — разовое получение;
|
||||
- `WRITE` — изменение конфигурации;
|
||||
- `SUBSCRIBE/PUBLISH` — значения для экранов и графиков.
|
||||
|
||||
Команды изделия из диапазона `0x1000..0x7FFF` остаются только для действий,
|
||||
которые не являются чтением или записью данных: поиск 1-Wire, нажатие экранной
|
||||
кнопки, запуск самотеста и аналогичные операции.
|
||||
|
||||
## Этап 3. Real-time
|
||||
|
||||
SETGUI получает каталог, строит подписку из реально видимых сигналов и обновляет
|
||||
графики по `timestamp_ms` устройства. Полные снимки всего прибора по таймеру не
|
||||
передаются. При перегрузке устройство отбрасывает старую телеметрию, сохраняя
|
||||
ответы на команды.
|
||||
|
||||
## Этап 4. Загрузчик
|
||||
|
||||
1. Реализовать staging/A-B backend и `FW_STATUS`.
|
||||
2. Проверить resume после обрыва на каждом допустимом offset.
|
||||
3. Проверить битый CRC блока и битый SHA-256 образа.
|
||||
4. Для серийной прошивки включить обязательную подпись и закрепить public key в
|
||||
bootloader/защищённой области.
|
||||
5. Проверить rollback при reset до подтверждения успешного старта приложения.
|
||||
|
||||
До выполнения этих пунктов кнопка удалённой активации не считается безопасной.
|
||||
|
||||
## Этап 5. Носители
|
||||
|
||||
- UART/RS-232/RS-485 и USB CDC используют один stream parser.
|
||||
- Ethernet TCP подключается к тому же parser без новой логики команд.
|
||||
- Ethernet UDP использует `setp_frame_decode_datagram()`.
|
||||
- CAN использует `set_can.c`; после reassembly кадр передаётся тому же router.
|
||||
|
||||
## Этап 6. Завершение
|
||||
|
||||
Когда все поддерживаемые изделия отвечают на v2:
|
||||
|
||||
1. убрать fallback v1 из штатного режима SETGUI;
|
||||
2. оставить отдельную legacy-утилиту только для обслуживания старых приборов;
|
||||
3. удалить дублирующиеся карты регистров и device-specific сообщения, которые
|
||||
заменены общей картой;
|
||||
4. зафиксировать v2 test vectors в CI прошивок и SETGUI.
|
||||
57
c/set-protocol/PORTING.md
Normal file
57
c/set-protocol/PORTING.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Перенос set-protocol
|
||||
|
||||
## Общая граница
|
||||
|
||||
Ядро кодирует и разбирает кадры, но не владеет UART, CAN-контроллером, сокетом,
|
||||
Flash или таймером. Порт приложения должен предоставить:
|
||||
|
||||
- неблокирующую очередь передачи целого кадра;
|
||||
- передачу принятых chunks в `setp_parser_feed()`;
|
||||
- монотонное время для телеметрии;
|
||||
- callback-и общей карты и загрузчика прошивки.
|
||||
|
||||
## UART / RS-232 / RS-485
|
||||
|
||||
DMA/ISR складывает байты в кольцевой буфер. Главный цикл отдаёт непрерывные
|
||||
участки parser-у. Для RS-485 DE остаётся включённым до физического окончания
|
||||
последнего stop-бита. Кадр нельзя считать отправленным по событию «DMA закончил
|
||||
читать память».
|
||||
|
||||
## USB CDC
|
||||
|
||||
USB packet boundaries не являются границами SETP-кадра. Каждый принятый USB
|
||||
chunk передаётся обычному stream-parser.
|
||||
|
||||
## Ethernet TCP
|
||||
|
||||
Каждый результат `recv()` передаётся stream-parser. `send()` может принять не
|
||||
весь буфер, поэтому порт хранит offset до полной отправки кадра. Закрытие TCP
|
||||
соединения удаляет его подписки на телеметрию.
|
||||
|
||||
## Ethernet UDP
|
||||
|
||||
UDP payload сначала проверяется parser-ом. Принимается ровно один кадр и ноль
|
||||
остаточных байт. Адрес peer хранится портом до отправки ответа. Для broadcast и
|
||||
multicast запрещены WRITE, FW_BEGIN и FW_ACTIVATE.
|
||||
|
||||
## CAN
|
||||
|
||||
Не вызывайте прикладной router из ISR. ISR принимает CAN-кадры в очередь,
|
||||
сегментация собирается в главном цикле, и только полный SETP-кадр передаётся
|
||||
parser/router. Сборка должна иметь timeout, контроль номера сегмента и одного
|
||||
активного отправителя на канал.
|
||||
|
||||
## Firmware backend
|
||||
|
||||
Протокол не заменяет безопасный загрузчик. Backend обязан:
|
||||
|
||||
1. писать только в неактивный слот или staging-область;
|
||||
2. проверять границы до erase/program;
|
||||
3. принимать повтор подтверждённого блока идемпотентно;
|
||||
4. хранить `next_offset` так, чтобы пережить reset;
|
||||
5. проверить CRC32, SHA-256 и, если требуется, подпись;
|
||||
6. атомарно пометить слот READY;
|
||||
7. иметь rollback, если новая прошивка не подтвердила успешный старт.
|
||||
|
||||
Во время обновления ответы и FW_* имеют приоритет над PUBLISH. Подписки можно
|
||||
временно приостановить, но соединение и `FW_STATUS` должны продолжать работать.
|
||||
238
c/set-protocol/PROTOCOL.md
Normal file
238
c/set-protocol/PROTOCOL.md
Normal file
@@ -0,0 +1,238 @@
|
||||
# SET protocol v2 — wire contract
|
||||
|
||||
## 1. Назначение
|
||||
|
||||
SETP v2 — единственный прикладной протокол новых устройств SET. Он решает три
|
||||
задачи одним контрактом:
|
||||
|
||||
1. запросы и ответы: конфигурация, диагностика, журналы и общая карта данных;
|
||||
2. события: значения для отрисовки в реальном времени;
|
||||
3. обновление прошивки с продолжением после разрыва соединения.
|
||||
|
||||
Носитель не меняет типы сообщений или payload. Меняется только способ доставки
|
||||
целого SETP-кадра.
|
||||
|
||||
## 2. Общий кадр
|
||||
|
||||
```text
|
||||
offset size field
|
||||
0 2 SOF = A5 5A
|
||||
2 1 version = 02
|
||||
3 1 flags
|
||||
4 2 message_type u16 LE
|
||||
6 2 source u16 LE
|
||||
8 2 destination u16 LE
|
||||
10 2 sequence u16 LE
|
||||
12 2 payload_length u16 LE
|
||||
14 N payload
|
||||
14+N 4 CRC32 IEEE u32 LE
|
||||
```
|
||||
|
||||
CRC32 считается от `version` (offset 2) до последнего байта payload. Полином
|
||||
`0xEDB88320`, init/final XOR `0xFFFFFFFF`; проверочное значение строки
|
||||
`123456789` — `0xCBF43926`.
|
||||
|
||||
Максимальный payload базового профиля — 512 байт. Реализация может объявить
|
||||
меньший предел через `CAPABILITIES`, но не может молча принять начало большого
|
||||
кадра и отбросить его конец.
|
||||
|
||||
Эталонный PING к узлу `0x002A`, sequence `0x1234`, с `ACK_REQUIRED`:
|
||||
|
||||
```text
|
||||
A5 5A 02 08 01 00 00 00 2A 00 34 12 00 00 33 EC 33 04
|
||||
```
|
||||
|
||||
## 3. Флаги и транзакции
|
||||
|
||||
| Бит | Имя | Смысл |
|
||||
|---:|---|---|
|
||||
| 0 | `RESPONSE` | ответ; тип и sequence повторяют запрос |
|
||||
| 1 | `EVENT` | самостоятельная публикация, не ответ |
|
||||
| 2 | `ERROR` | status ответа не равен `OK` |
|
||||
| 3 | `ACK_REQUIRED` | отправитель требует явный ответ |
|
||||
| 4 | `MORE` | за этим логическим куском последуют другие |
|
||||
| 5 | `PRIORITY` | приоритет над обычной телеметрией |
|
||||
| 7..6 | — | передавать нулями |
|
||||
|
||||
Запрос содержит `RESPONSE=0`, `EVENT=0`. Ответ содержит `RESPONSE=1`, тот же
|
||||
`message_type` и `sequence`, а первые два байта payload всегда являются
|
||||
`status u16`. Push-телеметрия содержит `EVENT=1`; её `sequence` — счётчик
|
||||
кадров источника и позволяет заметить потерю.
|
||||
|
||||
`source/destination = 0` означает локальный узел в точке-точке. `0xFFFF` —
|
||||
broadcast; на broadcast-запрос отвечать нельзя, если прикладная команда явно
|
||||
не задаёт безопасное окно ответа.
|
||||
|
||||
## 4. Стабильные типы сообщений
|
||||
|
||||
| Диапазон | Назначение |
|
||||
|---|---|
|
||||
| `0x0000..0x00FF` | системные команды и карта данных |
|
||||
| `0x0100..0x01FF` | обновление прошивки |
|
||||
| `0x0200..0x0FFF` | зарезервировано общей спецификацией |
|
||||
| `0x1000..0x7FFF` | команды конкретного изделия |
|
||||
| `0x8000..0xFFFF` | зарезервировано |
|
||||
|
||||
Общие команды:
|
||||
|
||||
| Код | Имя | Назначение |
|
||||
|---:|---|---|
|
||||
| `0x0001` | `PING` | доступность и uptime |
|
||||
| `0x0002` | `DEVICE_INFO` | модель, версии, серийный номер |
|
||||
| `0x0003` | `CAPABILITIES` | интерфейсы, MTU, функции, лимиты |
|
||||
| `0x0008` | `DIAGNOSTICS` | счётчики транспорта и приложения |
|
||||
| `0x0009` | `READ` | чтение 32-битно адресуемой карты |
|
||||
| `0x000A` | `WRITE` | транзакционная запись карты |
|
||||
| `0x0010` | `LOG_READ` | чтение журналов блоками |
|
||||
| `0x0011` | `CATALOG` | метаданные общей карты |
|
||||
| `0x0012` | `SUBSCRIBE` | создать/изменить поток данных |
|
||||
| `0x0013` | `PUBLISH` | пакет значений для отрисовки |
|
||||
| `0x0014` | `UNSUBSCRIBE` | удалить подписку |
|
||||
| `0x0100..0105` | `FW_*` | обновление и активация прошивки |
|
||||
|
||||
Неизвестный тип не является ошибкой кадрирования. Устройство отвечает
|
||||
`UNSUPPORTED`, если запрос был адресован ему и требовал ответа.
|
||||
|
||||
## 5. Телеметрия реального времени
|
||||
|
||||
### SUBSCRIBE
|
||||
|
||||
```text
|
||||
subscription_id u16
|
||||
period_ms u32 (0 = по изменению)
|
||||
address_count u16
|
||||
addresses u32[address_count]
|
||||
```
|
||||
|
||||
Ответ сообщает status и фактически принятый период. Устройство вправе увеличить
|
||||
слишком короткий период. Подписка принадлежит соединению/источнику и удаляется
|
||||
при его закрытии либо командой `UNSUBSCRIBE`.
|
||||
|
||||
### PUBLISH
|
||||
|
||||
```text
|
||||
subscription_id u16
|
||||
sample_sequence u16
|
||||
timestamp_ms u32
|
||||
item_count u16
|
||||
|
||||
repeat item_count times:
|
||||
address u32
|
||||
encoding u8 (U16/I16/U32/I32/F32/BYTES)
|
||||
element_count u8
|
||||
data_length u16
|
||||
data u8[data_length]
|
||||
```
|
||||
|
||||
`timestamp_ms` — монотонное время устройства. GUI строит графики по нему, а не
|
||||
по моменту прихода в Windows. Большой массив, например спектр, разбивается на
|
||||
несколько `PUBLISH` с `MORE`; адрес и `sample_sequence` остаются теми же.
|
||||
|
||||
Телеметрия имеет меньший приоритет, чем ответы и прошивка. При переполнении
|
||||
очереди разрешено отбросить старый `PUBLISH`, но нельзя частично передать кадр.
|
||||
|
||||
## 6. Прошивка
|
||||
|
||||
Типы:
|
||||
|
||||
| Код | Команда |
|
||||
|---:|---|
|
||||
| `0x0100` | `FW_BEGIN` |
|
||||
| `0x0101` | `FW_DATA` |
|
||||
| `0x0102` | `FW_END` |
|
||||
| `0x0103` | `FW_ABORT` |
|
||||
| `0x0104` | `FW_STATUS` |
|
||||
| `0x0105` | `FW_ACTIVATE` |
|
||||
|
||||
`FW_BEGIN` содержит размер, CRC32 и SHA-256 образа, версию, базовый адрес,
|
||||
целевой слот, желаемый размер блока, ID ключа и необязательную подпись. Подпись
|
||||
проверяется над каноническим manifest, а не над полученными по частям данными:
|
||||
|
||||
```text
|
||||
ASCII "SETPFW2\0" || image_size || image_crc32 || image_version ||
|
||||
base_address || slot || sha256
|
||||
```
|
||||
|
||||
Числа manifest также little-endian. Рекомендуемая подпись — Ed25519 (64 байта).
|
||||
Конкретный загрузчик может потребовать подписанный образ и вернуть `AUTH_FAILED`
|
||||
для неподписанного.
|
||||
|
||||
`FW_DATA`:
|
||||
|
||||
```text
|
||||
offset u32 | data_length u16 | flags u16 | data_crc32 u32 | data[]
|
||||
```
|
||||
|
||||
Ответ возвращает status и `next_offset u32`. Повтор уже записанного блока с теми
|
||||
же данными обязан быть идемпотентным. После потери связи GUI запрашивает
|
||||
`FW_STATUS` и продолжает с `next_offset`.
|
||||
|
||||
`FW_END` повторяет размер, CRC32 и SHA-256. Устройство проверяет весь образ и
|
||||
только затем переводит слот в `READY`. `FW_ACTIVATE` меняет загрузочный слот;
|
||||
операция обновления не должна перезаписывать единственный рабочий образ. Для
|
||||
серийных устройств требуется A/B или эквивалентный механизм rollback.
|
||||
|
||||
CRC32 защищает линию, SHA-256 — целостность образа, подпись — происхождение.
|
||||
Один CRC не является защитой от подмены прошивки.
|
||||
|
||||
## 7. Привязки к физическим интерфейсам
|
||||
|
||||
### RS-232, RS-485, USB CDC
|
||||
|
||||
Кадры передаются подряд как поток байтов. Parser обязан восстанавливаться после
|
||||
мусора и битого CRC. На RS-485 используются `source/destination`; передача
|
||||
broadcast не должна запускать прошивку или запись конфигурации.
|
||||
|
||||
### Ethernet TCP
|
||||
|
||||
TCP несёт тот же поток кадров без дополнительной длины: она уже есть в header.
|
||||
Один `recv()` может вернуть часть кадра или несколько кадров. Порт по умолчанию
|
||||
задаётся приложением; рекомендуемое значение проекта — `25060`, оно не считается
|
||||
зарегистрированным IANA. Для внешних сетей используется TLS, SETP внутри TLS не
|
||||
меняется.
|
||||
|
||||
### Ethernet UDP
|
||||
|
||||
Одна UDP-датаграмма содержит ровно один полный SETP-кадр. Датаграммы с хвостом,
|
||||
двумя кадрами или несовпадающей длиной отбрасываются. Базовый payload 512 байт
|
||||
не превышает безопасный IPv4 MTU. Прошивка по UDP допустима только в режиме
|
||||
stop-and-wait с `ACK_REQUIRED`; предпочтителен TCP.
|
||||
|
||||
### CAN
|
||||
|
||||
Через классический CAN передаются байты того же полного SETP-кадра. Повторно
|
||||
кодировать команды в поля CAN ID нельзя.
|
||||
|
||||
Extended CAN ID:
|
||||
|
||||
```text
|
||||
28..24 prefix = 0x12
|
||||
23..16 destination (младшие 8 бит SETP destination)
|
||||
15..8 source (младшие 8 бит SETP source)
|
||||
7 priority
|
||||
6..0 channel
|
||||
```
|
||||
|
||||
CAN-профиль использует node `1..254`; `0` и `255` сохраняют смысл local и
|
||||
broadcast. Поля полного SETP-заголовка остаются обязательными и должны совпасть
|
||||
с CAN ID после reassembly.
|
||||
|
||||
PCI классического CAN:
|
||||
|
||||
```text
|
||||
FIRST: data[0]=0x10, data[1..2]=total_length u16 LE, data[3..7]=5 байт
|
||||
CONSECUTIVE: data[0]=0x20|SN, data[1..7]=до 7 байт, SN начинается с 1
|
||||
FLOW_CONTROL:data[0]=0x30|status, data[1]=block_size, data[2]=st_min_ms
|
||||
```
|
||||
|
||||
`status`: 0 continue, 1 wait, 2 overflow. Номер сегмента идёт по модулю 16.
|
||||
Timeout сборки по умолчанию 500 мс. Новый FIRST заменяет незавершённую сборку
|
||||
того же канала. CAN-FD может увеличить данные сегмента в следующей версии
|
||||
binding, не меняя SETP frame/message/payload.
|
||||
|
||||
## 8. Совместимость
|
||||
|
||||
GUI protocol v1 и SETP v2 несовместимы. Автоопределение допускается только во
|
||||
время миграции: клиент посылает v2 PING, затем при полном тайм-ауте пробует v1.
|
||||
После первого корректного ответа формат соединения фиксируется до отключения.
|
||||
Новое устройство не должно одновременно публиковать v1 и v2 в одном потоке.
|
||||
75
c/set-protocol/README.md
Normal file
75
c/set-protocol/README.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# set-protocol
|
||||
|
||||
Единый переносимый протокол SET для новых устройств и SETGUI. Один и тот же
|
||||
прикладной кадр используется для управления, чтения общей карты, потоковой
|
||||
телеметрии и безопасной передачи прошивки через:
|
||||
|
||||
- RS-232 и RS-485;
|
||||
- USB CDC;
|
||||
- CAN с сегментацией;
|
||||
- Ethernet TCP;
|
||||
- Ethernet UDP, когда нужен обмен без соединения.
|
||||
|
||||
Протокол не привязан к HAL, ОС или микроконтроллеру. Реализация на C99 не
|
||||
использует динамическую память. Все многобайтные значения little-endian.
|
||||
|
||||
## Что уже реализовано
|
||||
|
||||
| Файл | Назначение |
|
||||
|---|---|
|
||||
| `set_protocol.*` | кадр v2, CRC32, потоковый parser, статусы ответов |
|
||||
| `set_can.*` | extended CAN ID, сегментация, flow-control и reassembly timeout |
|
||||
| `set_firmware.*` | BEGIN/DATA/END/STATUS, resume, CRC блока, SHA-256 и подпись |
|
||||
| `set_telemetry.*` | подписки и типизированные push-пакеты с timestamp |
|
||||
| `PROTOCOL.md` | обязательный wire-контракт и привязки к носителям |
|
||||
| `MIGRATION.md` | порядок перехода SETGUI и существующих прошивок с v1 |
|
||||
| `PORTING.md` | подключение UART, CAN, USB и Ethernet |
|
||||
| `tests/` | host-тесты и фиксированный эталонный кадр |
|
||||
|
||||
Файл [`PROTOCOL.md`](PROTOCOL.md) является источником истины. Числа из него
|
||||
нельзя менять без выпуска следующей версии протокола.
|
||||
|
||||
## Минимальный stream-приёмник
|
||||
|
||||
```c
|
||||
#include "set_protocol.h"
|
||||
|
||||
static setp_parser_t parser;
|
||||
|
||||
static void on_frame(const setp_frame_t *frame, void *user)
|
||||
{
|
||||
(void)user;
|
||||
/* frame->payload действует только до возврата из callback. */
|
||||
}
|
||||
|
||||
void protocol_init(void)
|
||||
{
|
||||
setp_parser_init(&parser);
|
||||
}
|
||||
|
||||
void protocol_feed(const uint8_t *data, uint16_t length)
|
||||
{
|
||||
(void)setp_parser_feed(&parser, data, length, on_frame, NULL);
|
||||
}
|
||||
```
|
||||
|
||||
Для RS-232, RS-485, USB CDC и TCP в parser передаются любые принятые chunks.
|
||||
Для UDP один UDP payload должен содержать ровно один полный SETP-кадр.
|
||||
|
||||
## Сборка
|
||||
|
||||
```text
|
||||
cmake -B build
|
||||
cmake --build build
|
||||
ctest --test-dir build
|
||||
```
|
||||
|
||||
Либо добавьте три файла из `src/` и каталог `include/` непосредственно в
|
||||
проект прошивки.
|
||||
|
||||
## Версии
|
||||
|
||||
- GUI protocol v1 остаётся только переходным форматом старых устройств.
|
||||
- Все новые устройства используют SET protocol v2 (`SETP_VERSION = 2`).
|
||||
- v1 и v2 имеют одинаковый SOF `A5 5A`, поэтому номер версии проверяется до
|
||||
разбора остальных полей.
|
||||
89
c/set-protocol/include/set_can.h
Normal file
89
c/set-protocol/include/set_can.h
Normal file
@@ -0,0 +1,89 @@
|
||||
#ifndef SET_CAN_H
|
||||
#define SET_CAN_H
|
||||
|
||||
#include "set_protocol.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define SETP_CAN_ID_PREFIX 0x12U
|
||||
#define SETP_CAN_ID_PREFIX_MASK 0x1F000000UL
|
||||
#define SETP_CAN_ID_MASK 0x1FFFFFFFUL
|
||||
|
||||
#define SETP_CAN_PCI_FIRST 0x10U
|
||||
#define SETP_CAN_PCI_CONSECUTIVE 0x20U
|
||||
#define SETP_CAN_PCI_FLOW_CONTROL 0x30U
|
||||
#define SETP_CAN_PCI_TYPE_MASK 0xF0U
|
||||
#define SETP_CAN_PCI_VALUE_MASK 0x0FU
|
||||
|
||||
#define SETP_CAN_FC_CONTINUE 0x00U
|
||||
#define SETP_CAN_FC_WAIT 0x01U
|
||||
#define SETP_CAN_FC_OVERFLOW 0x02U
|
||||
|
||||
#define SETP_CAN_FIRST_DATA 5U
|
||||
#define SETP_CAN_CONSECUTIVE_DATA 7U
|
||||
|
||||
#ifndef SETP_CAN_REASSEMBLY_TIMEOUT_MS
|
||||
#define SETP_CAN_REASSEMBLY_TIMEOUT_MS 500U
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
uint32_t id;
|
||||
uint8_t length;
|
||||
uint8_t data[8];
|
||||
} setp_can_frame_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t source;
|
||||
uint8_t destination;
|
||||
uint8_t priority;
|
||||
uint8_t channel;
|
||||
} setp_can_id_t;
|
||||
|
||||
typedef enum {
|
||||
SETP_CAN_RX_NONE = 0,
|
||||
SETP_CAN_RX_COMPLETE = 1,
|
||||
SETP_CAN_RX_FORMAT_ERROR = 2,
|
||||
SETP_CAN_RX_SEQUENCE_ERROR = 3,
|
||||
SETP_CAN_RX_TIMEOUT = 4
|
||||
} setp_can_rx_result_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t buffer[SETP_FRAME_MAX];
|
||||
uint16_t expected_length;
|
||||
uint16_t received_length;
|
||||
uint32_t can_id;
|
||||
uint32_t deadline_ms;
|
||||
uint8_t next_sequence;
|
||||
uint8_t active;
|
||||
} setp_can_rx_t;
|
||||
|
||||
typedef struct {
|
||||
const uint8_t *data;
|
||||
uint16_t length;
|
||||
uint32_t can_id;
|
||||
} setp_can_packet_t;
|
||||
|
||||
typedef bool (*setp_can_send_fn)(const setp_can_frame_t *frame, void *user);
|
||||
|
||||
uint32_t setp_can_id_pack(const setp_can_id_t *value);
|
||||
bool setp_can_id_unpack(uint32_t raw, setp_can_id_t *out);
|
||||
|
||||
/** Segment one complete SETP frame. Flow-control pacing belongs to the port. */
|
||||
bool setp_can_segment(const uint8_t *packet, uint16_t length, uint32_t can_id,
|
||||
setp_can_send_fn send, void *user);
|
||||
|
||||
void setp_can_rx_init(setp_can_rx_t *state);
|
||||
setp_can_rx_result_t setp_can_rx_feed(setp_can_rx_t *state,
|
||||
const setp_can_frame_t *frame,
|
||||
uint32_t now_ms, setp_can_packet_t *out);
|
||||
|
||||
bool setp_can_flow_control(setp_can_frame_t *out, uint32_t can_id,
|
||||
uint8_t status, uint8_t block_size, uint8_t st_min_ms);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* SET_CAN_H */
|
||||
91
c/set-protocol/include/set_firmware.h
Normal file
91
c/set-protocol/include/set_firmware.h
Normal file
@@ -0,0 +1,91 @@
|
||||
#ifndef SET_FIRMWARE_H
|
||||
#define SET_FIRMWARE_H
|
||||
|
||||
#include "set_protocol.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define SETP_SHA256_SIZE 32U
|
||||
#define SETP_FW_BEGIN_FIXED_SIZE 58U
|
||||
#define SETP_FW_DATA_HEADER_SIZE 12U
|
||||
#define SETP_FW_END_SIZE 40U
|
||||
#define SETP_FW_STATUS_SIZE 16U
|
||||
#define SETP_FW_MANIFEST_SIZE 57U
|
||||
|
||||
#define SETP_FW_FLAG_RESUME 0x01U
|
||||
#define SETP_FW_FLAG_SIGNED 0x02U
|
||||
#define SETP_FW_FLAG_ERASE_SLOT 0x04U
|
||||
#define SETP_FW_FLAG_ACTIVATE 0x08U
|
||||
|
||||
typedef enum {
|
||||
SETP_FW_IDLE = 0,
|
||||
SETP_FW_RECEIVING = 1,
|
||||
SETP_FW_VERIFYING = 2,
|
||||
SETP_FW_READY = 3,
|
||||
SETP_FW_ACTIVE = 4,
|
||||
SETP_FW_FAILED = 5
|
||||
} setp_fw_state_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t image_size;
|
||||
uint32_t image_crc32;
|
||||
uint32_t image_version;
|
||||
uint32_t base_address;
|
||||
uint8_t slot;
|
||||
uint8_t flags;
|
||||
uint16_t block_size;
|
||||
uint8_t sha256[SETP_SHA256_SIZE];
|
||||
uint32_t signing_key_id;
|
||||
uint16_t signature_length;
|
||||
const uint8_t *signature;
|
||||
} setp_fw_begin_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t offset;
|
||||
uint16_t data_length;
|
||||
uint16_t flags;
|
||||
uint32_t data_crc32;
|
||||
const uint8_t *data;
|
||||
} setp_fw_data_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t image_size;
|
||||
uint32_t image_crc32;
|
||||
uint8_t sha256[SETP_SHA256_SIZE];
|
||||
} setp_fw_end_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t state;
|
||||
uint8_t active_slot;
|
||||
uint16_t max_block_size;
|
||||
uint32_t next_offset;
|
||||
uint32_t image_size;
|
||||
uint16_t last_error;
|
||||
uint16_t flags;
|
||||
} setp_fw_status_t;
|
||||
|
||||
size_t setp_fw_begin_encode(const setp_fw_begin_t *value, uint8_t *out, size_t capacity);
|
||||
bool setp_fw_begin_decode(const uint8_t *payload, uint16_t length, setp_fw_begin_t *out);
|
||||
|
||||
size_t setp_fw_data_encode(uint32_t offset, uint16_t flags,
|
||||
const uint8_t *data, uint16_t data_length,
|
||||
uint8_t *out, size_t capacity);
|
||||
bool setp_fw_data_decode(const uint8_t *payload, uint16_t length, setp_fw_data_t *out);
|
||||
|
||||
size_t setp_fw_end_encode(const setp_fw_end_t *value, uint8_t *out, size_t capacity);
|
||||
bool setp_fw_end_decode(const uint8_t *payload, uint16_t length, setp_fw_end_t *out);
|
||||
|
||||
size_t setp_fw_status_encode(const setp_fw_status_t *value, uint8_t *out, size_t capacity);
|
||||
bool setp_fw_status_decode(const uint8_t *payload, uint16_t length, setp_fw_status_t *out);
|
||||
|
||||
/** Canonical bytes verified by a firmware-signature backend. */
|
||||
size_t setp_fw_manifest_encode(const setp_fw_begin_t *value,
|
||||
uint8_t *out, size_t capacity);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* SET_FIRMWARE_H */
|
||||
148
c/set-protocol/include/set_protocol.h
Normal file
148
c/set-protocol/include/set_protocol.h
Normal file
@@ -0,0 +1,148 @@
|
||||
#ifndef SET_PROTOCOL_H
|
||||
#define SET_PROTOCOL_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define SETP_VERSION 0x02U
|
||||
#define SETP_SOF0 0xA5U
|
||||
#define SETP_SOF1 0x5AU
|
||||
#define SETP_HEADER_SIZE 14U
|
||||
#define SETP_CRC_SIZE 4U
|
||||
|
||||
#ifndef SETP_MAX_PAYLOAD
|
||||
#define SETP_MAX_PAYLOAD 512U
|
||||
#endif
|
||||
|
||||
#define SETP_FRAME_MAX (SETP_HEADER_SIZE + SETP_MAX_PAYLOAD + SETP_CRC_SIZE)
|
||||
#define SETP_NODE_LOCAL 0x0000U
|
||||
#define SETP_NODE_BROADCAST 0xFFFFU
|
||||
|
||||
/* Frame flags. Reserved bits must be transmitted as zero. */
|
||||
#define SETP_FLAG_RESPONSE 0x01U
|
||||
#define SETP_FLAG_EVENT 0x02U
|
||||
#define SETP_FLAG_ERROR 0x04U
|
||||
#define SETP_FLAG_ACK_REQUIRED 0x08U
|
||||
#define SETP_FLAG_MORE 0x10U
|
||||
#define SETP_FLAG_PRIORITY 0x20U
|
||||
#define SETP_FLAG_KNOWN_MASK 0x3FU
|
||||
#define SETP_FLAG_RESERVED_MASK 0xC0U
|
||||
|
||||
typedef enum {
|
||||
SETP_IFACE_NONE = 0,
|
||||
SETP_IFACE_RS232 = 1,
|
||||
SETP_IFACE_RS485 = 2,
|
||||
SETP_IFACE_CAN = 3,
|
||||
SETP_IFACE_USB_CDC = 4,
|
||||
SETP_IFACE_ETHERNET_TCP = 5,
|
||||
SETP_IFACE_ETHERNET_UDP = 6
|
||||
} setp_iface_t;
|
||||
|
||||
typedef enum {
|
||||
SETP_MSG_PING = 0x0001,
|
||||
SETP_MSG_DEVICE_INFO = 0x0002,
|
||||
SETP_MSG_CAPABILITIES = 0x0003,
|
||||
SETP_MSG_DIAGNOSTICS = 0x0008,
|
||||
SETP_MSG_READ = 0x0009,
|
||||
SETP_MSG_WRITE = 0x000A,
|
||||
SETP_MSG_LOG_READ = 0x0010,
|
||||
SETP_MSG_CATALOG = 0x0011,
|
||||
SETP_MSG_SUBSCRIBE = 0x0012,
|
||||
SETP_MSG_PUBLISH = 0x0013,
|
||||
SETP_MSG_UNSUBSCRIBE = 0x0014,
|
||||
|
||||
SETP_MSG_FW_BEGIN = 0x0100,
|
||||
SETP_MSG_FW_DATA = 0x0101,
|
||||
SETP_MSG_FW_END = 0x0102,
|
||||
SETP_MSG_FW_ABORT = 0x0103,
|
||||
SETP_MSG_FW_STATUS = 0x0104,
|
||||
SETP_MSG_FW_ACTIVATE = 0x0105,
|
||||
|
||||
SETP_MSG_DEVICE_BASE = 0x1000,
|
||||
SETP_MSG_DEVICE_LAST = 0x7FFF
|
||||
} setp_message_type_t;
|
||||
|
||||
typedef enum {
|
||||
SETP_STATUS_OK = 0,
|
||||
SETP_STATUS_INVALID_ARGUMENT = 1,
|
||||
SETP_STATUS_INVALID_LENGTH = 2,
|
||||
SETP_STATUS_NOT_FOUND = 3,
|
||||
SETP_STATUS_ACCESS_DENIED = 4,
|
||||
SETP_STATUS_BUSY = 5,
|
||||
SETP_STATUS_NO_PROVIDER = 6,
|
||||
SETP_STATUS_INTERNAL = 7,
|
||||
SETP_STATUS_CRC = 8,
|
||||
SETP_STATUS_SEQUENCE = 9,
|
||||
SETP_STATUS_NO_SPACE = 10,
|
||||
SETP_STATUS_UNSUPPORTED = 11,
|
||||
SETP_STATUS_VERIFY_FAILED = 12,
|
||||
SETP_STATUS_AUTH_FAILED = 13,
|
||||
SETP_STATUS_TIMEOUT = 14,
|
||||
SETP_STATUS_WRONG_STATE = 15
|
||||
} setp_status_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t flags;
|
||||
uint16_t message_type;
|
||||
uint16_t source;
|
||||
uint16_t destination;
|
||||
uint16_t sequence;
|
||||
uint16_t payload_length;
|
||||
const uint8_t *payload;
|
||||
} setp_frame_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t frames;
|
||||
uint32_t crc_errors;
|
||||
uint32_t version_errors;
|
||||
uint32_t length_errors;
|
||||
uint32_t flag_errors;
|
||||
uint32_t stray_bytes;
|
||||
uint32_t overflows;
|
||||
} setp_parser_stats_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t buffer[SETP_FRAME_MAX];
|
||||
uint16_t length;
|
||||
setp_parser_stats_t stats;
|
||||
} setp_parser_t;
|
||||
|
||||
typedef void (*setp_frame_fn)(const setp_frame_t *frame, void *user);
|
||||
|
||||
uint16_t setp_get_u16(const uint8_t *data);
|
||||
uint32_t setp_get_u32(const uint8_t *data);
|
||||
void setp_put_u16(uint8_t *data, uint16_t value);
|
||||
void setp_put_u32(uint8_t *data, uint32_t value);
|
||||
|
||||
uint32_t setp_crc32(const uint8_t *data, size_t length);
|
||||
|
||||
/** Encode one complete stream/UDP frame. Returns zero on invalid arguments. */
|
||||
size_t setp_frame_encode(const setp_frame_t *frame, uint8_t *out, size_t capacity);
|
||||
|
||||
/** Decode exactly one complete UDP/datagram frame; trailing bytes are rejected. */
|
||||
bool setp_frame_decode_datagram(const uint8_t *data, size_t length, setp_frame_t *out);
|
||||
|
||||
void setp_parser_init(setp_parser_t *parser);
|
||||
void setp_parser_reset(setp_parser_t *parser);
|
||||
|
||||
/** Feed an arbitrary stream chunk. Returns the number of delivered frames. */
|
||||
size_t setp_parser_feed(setp_parser_t *parser, const uint8_t *data, size_t length,
|
||||
setp_frame_fn callback, void *user);
|
||||
|
||||
/** Encode the common two-byte status prefix used by every response. */
|
||||
size_t setp_status_encode(uint16_t status, uint8_t *out, size_t capacity);
|
||||
|
||||
/** Split a response payload into status and optional response body. */
|
||||
bool setp_status_decode(const setp_frame_t *frame, uint16_t *status,
|
||||
const uint8_t **body, uint16_t *body_length);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* SET_PROTOCOL_H */
|
||||
73
c/set-protocol/include/set_telemetry.h
Normal file
73
c/set-protocol/include/set_telemetry.h
Normal file
@@ -0,0 +1,73 @@
|
||||
#ifndef SET_TELEMETRY_H
|
||||
#define SET_TELEMETRY_H
|
||||
|
||||
#include "set_protocol.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define SETP_SUBSCRIBE_HEADER_SIZE 8U
|
||||
#define SETP_PUBLISH_HEADER_SIZE 10U
|
||||
#define SETP_PUBLISH_ITEM_HEADER_SIZE 8U
|
||||
|
||||
typedef enum {
|
||||
SETP_VALUE_U16 = 1,
|
||||
SETP_VALUE_I16 = 2,
|
||||
SETP_VALUE_U32 = 3,
|
||||
SETP_VALUE_I32 = 4,
|
||||
SETP_VALUE_F32 = 5,
|
||||
SETP_VALUE_BYTES = 6
|
||||
} setp_value_encoding_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t *output;
|
||||
uint16_t capacity;
|
||||
uint16_t length;
|
||||
uint16_t item_count;
|
||||
} setp_publish_builder_t;
|
||||
|
||||
typedef struct {
|
||||
const uint8_t *payload;
|
||||
uint16_t length;
|
||||
uint16_t offset;
|
||||
uint16_t remaining;
|
||||
uint16_t subscription_id;
|
||||
uint16_t sample_sequence;
|
||||
uint32_t timestamp_ms;
|
||||
} setp_publish_view_t;
|
||||
|
||||
typedef struct {
|
||||
uint32_t address;
|
||||
uint8_t encoding;
|
||||
uint8_t element_count;
|
||||
uint16_t data_length;
|
||||
const uint8_t *data;
|
||||
} setp_publish_item_t;
|
||||
|
||||
size_t setp_subscribe_encode(uint16_t subscription_id, uint32_t period_ms,
|
||||
const uint32_t *addresses, uint16_t address_count,
|
||||
uint8_t *out, size_t capacity);
|
||||
bool setp_subscribe_decode_header(const uint8_t *payload, uint16_t length,
|
||||
uint16_t *subscription_id, uint32_t *period_ms,
|
||||
uint16_t *address_count);
|
||||
bool setp_subscribe_address(const uint8_t *payload, uint16_t length,
|
||||
uint16_t index, uint32_t *address);
|
||||
|
||||
bool setp_publish_begin(setp_publish_builder_t *builder,
|
||||
uint16_t subscription_id, uint16_t sample_sequence,
|
||||
uint32_t timestamp_ms, uint8_t *out, uint16_t capacity);
|
||||
bool setp_publish_add(setp_publish_builder_t *builder, uint32_t address,
|
||||
uint8_t encoding, uint8_t element_count,
|
||||
const uint8_t *data, uint16_t data_length);
|
||||
size_t setp_publish_finish(setp_publish_builder_t *builder);
|
||||
|
||||
bool setp_publish_view_init(setp_publish_view_t *view,
|
||||
const uint8_t *payload, uint16_t length);
|
||||
bool setp_publish_next(setp_publish_view_t *view, setp_publish_item_t *item);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* SET_TELEMETRY_H */
|
||||
185
c/set-protocol/src/set_can.c
Normal file
185
c/set-protocol/src/set_can.c
Normal file
@@ -0,0 +1,185 @@
|
||||
#include "set_can.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
uint32_t setp_can_id_pack(const setp_can_id_t *value)
|
||||
{
|
||||
if (value == NULL) {
|
||||
return 0U;
|
||||
}
|
||||
return ((uint32_t)SETP_CAN_ID_PREFIX << 24)
|
||||
| ((uint32_t)value->destination << 16)
|
||||
| ((uint32_t)value->source << 8)
|
||||
| ((uint32_t)(value->priority & 1U) << 7)
|
||||
| (uint32_t)(value->channel & 0x7FU);
|
||||
}
|
||||
|
||||
bool setp_can_id_unpack(uint32_t raw, setp_can_id_t *out)
|
||||
{
|
||||
if ((out == NULL) || ((raw & ~SETP_CAN_ID_MASK) != 0U)
|
||||
|| ((raw & SETP_CAN_ID_PREFIX_MASK)
|
||||
!= ((uint32_t)SETP_CAN_ID_PREFIX << 24))) {
|
||||
return false;
|
||||
}
|
||||
out->destination = (uint8_t)(raw >> 16);
|
||||
out->source = (uint8_t)(raw >> 8);
|
||||
out->priority = (uint8_t)((raw >> 7) & 1U);
|
||||
out->channel = (uint8_t)(raw & 0x7FU);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool setp_can_segment(const uint8_t *packet, uint16_t length, uint32_t can_id,
|
||||
setp_can_send_fn send, void *user)
|
||||
{
|
||||
setp_can_frame_t frame;
|
||||
uint16_t offset;
|
||||
uint8_t sequence = 1U;
|
||||
|
||||
if ((packet == NULL) || (send == NULL)
|
||||
|| (length < SETP_HEADER_SIZE + SETP_CRC_SIZE)
|
||||
|| (length > SETP_FRAME_MAX)
|
||||
|| ((can_id & ~SETP_CAN_ID_MASK) != 0U)) {
|
||||
return false;
|
||||
}
|
||||
(void)memset(&frame, 0, sizeof(frame));
|
||||
frame.id = can_id;
|
||||
frame.length = 8U;
|
||||
frame.data[0] = SETP_CAN_PCI_FIRST;
|
||||
setp_put_u16(&frame.data[1], length);
|
||||
(void)memcpy(&frame.data[3], packet, SETP_CAN_FIRST_DATA);
|
||||
if (!send(&frame, user)) {
|
||||
return false;
|
||||
}
|
||||
offset = SETP_CAN_FIRST_DATA;
|
||||
|
||||
while (offset < length) {
|
||||
uint16_t remaining = (uint16_t)(length - offset);
|
||||
uint8_t chunk = remaining > SETP_CAN_CONSECUTIVE_DATA
|
||||
? SETP_CAN_CONSECUTIVE_DATA : (uint8_t)remaining;
|
||||
(void)memset(frame.data, 0, sizeof(frame.data));
|
||||
frame.data[0] = (uint8_t)(SETP_CAN_PCI_CONSECUTIVE
|
||||
| (sequence & SETP_CAN_PCI_VALUE_MASK));
|
||||
(void)memcpy(&frame.data[1], &packet[offset], chunk);
|
||||
frame.length = (uint8_t)(1U + chunk);
|
||||
if (!send(&frame, user)) {
|
||||
return false;
|
||||
}
|
||||
offset = (uint16_t)(offset + chunk);
|
||||
sequence = (uint8_t)((sequence + 1U) & SETP_CAN_PCI_VALUE_MASK);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void setp_can_rx_init(setp_can_rx_t *state)
|
||||
{
|
||||
if (state != NULL) {
|
||||
(void)memset(state, 0, sizeof(*state));
|
||||
}
|
||||
}
|
||||
|
||||
static bool setp_can_expired(const setp_can_rx_t *state, uint32_t now_ms)
|
||||
{
|
||||
return state->active && ((int32_t)(now_ms - state->deadline_ms) >= 0);
|
||||
}
|
||||
|
||||
setp_can_rx_result_t setp_can_rx_feed(setp_can_rx_t *state,
|
||||
const setp_can_frame_t *frame,
|
||||
uint32_t now_ms, setp_can_packet_t *out)
|
||||
{
|
||||
uint8_t pci;
|
||||
if ((state == NULL) || (frame == NULL) || (out == NULL)
|
||||
|| (frame->length == 0U) || (frame->length > 8U)) {
|
||||
return SETP_CAN_RX_FORMAT_ERROR;
|
||||
}
|
||||
if (setp_can_expired(state, now_ms)) {
|
||||
state->active = 0U;
|
||||
return SETP_CAN_RX_TIMEOUT;
|
||||
}
|
||||
pci = (uint8_t)(frame->data[0] & SETP_CAN_PCI_TYPE_MASK);
|
||||
if (pci == SETP_CAN_PCI_FIRST) {
|
||||
uint16_t total;
|
||||
if (frame->length != 8U) {
|
||||
return SETP_CAN_RX_FORMAT_ERROR;
|
||||
}
|
||||
total = setp_get_u16(&frame->data[1]);
|
||||
if ((total < SETP_HEADER_SIZE + SETP_CRC_SIZE) || (total > SETP_FRAME_MAX)) {
|
||||
return SETP_CAN_RX_FORMAT_ERROR;
|
||||
}
|
||||
(void)memcpy(state->buffer, &frame->data[3], SETP_CAN_FIRST_DATA);
|
||||
state->expected_length = total;
|
||||
state->received_length = SETP_CAN_FIRST_DATA;
|
||||
state->can_id = frame->id;
|
||||
state->deadline_ms = now_ms + SETP_CAN_REASSEMBLY_TIMEOUT_MS;
|
||||
state->next_sequence = 1U;
|
||||
state->active = 1U;
|
||||
return SETP_CAN_RX_NONE;
|
||||
}
|
||||
if (pci == SETP_CAN_PCI_CONSECUTIVE) {
|
||||
uint8_t sequence = (uint8_t)(frame->data[0] & SETP_CAN_PCI_VALUE_MASK);
|
||||
uint16_t remaining;
|
||||
uint8_t chunk;
|
||||
if (!state->active || (frame->id != state->can_id)
|
||||
|| (sequence != state->next_sequence) || (frame->length < 2U)) {
|
||||
state->active = 0U;
|
||||
return SETP_CAN_RX_SEQUENCE_ERROR;
|
||||
}
|
||||
remaining = (uint16_t)(state->expected_length - state->received_length);
|
||||
chunk = (uint8_t)(frame->length - 1U);
|
||||
if (chunk > remaining) {
|
||||
state->active = 0U;
|
||||
return SETP_CAN_RX_FORMAT_ERROR;
|
||||
}
|
||||
(void)memcpy(&state->buffer[state->received_length], &frame->data[1], chunk);
|
||||
state->received_length = (uint16_t)(state->received_length + chunk);
|
||||
state->next_sequence = (uint8_t)((state->next_sequence + 1U)
|
||||
& SETP_CAN_PCI_VALUE_MASK);
|
||||
state->deadline_ms = now_ms + SETP_CAN_REASSEMBLY_TIMEOUT_MS;
|
||||
if (state->received_length == state->expected_length) {
|
||||
setp_can_id_t id;
|
||||
uint16_t source;
|
||||
uint16_t destination;
|
||||
uint8_t priority;
|
||||
if (!setp_can_id_unpack(state->can_id, &id)
|
||||
|| (state->expected_length < SETP_HEADER_SIZE + SETP_CRC_SIZE)
|
||||
|| (state->buffer[0] != SETP_SOF0)
|
||||
|| (state->buffer[1] != SETP_SOF1)) {
|
||||
state->active = 0U;
|
||||
return SETP_CAN_RX_FORMAT_ERROR;
|
||||
}
|
||||
source = setp_get_u16(&state->buffer[6]);
|
||||
destination = setp_get_u16(&state->buffer[8]);
|
||||
priority = (state->buffer[3] & SETP_FLAG_PRIORITY) != 0U ? 1U : 0U;
|
||||
if ((source != id.source) || (destination != id.destination)
|
||||
|| (priority != id.priority)) {
|
||||
state->active = 0U;
|
||||
return SETP_CAN_RX_FORMAT_ERROR;
|
||||
}
|
||||
out->data = state->buffer;
|
||||
out->length = state->expected_length;
|
||||
out->can_id = state->can_id;
|
||||
state->active = 0U;
|
||||
return SETP_CAN_RX_COMPLETE;
|
||||
}
|
||||
return SETP_CAN_RX_NONE;
|
||||
}
|
||||
if (pci == SETP_CAN_PCI_FLOW_CONTROL) {
|
||||
return SETP_CAN_RX_NONE;
|
||||
}
|
||||
return SETP_CAN_RX_FORMAT_ERROR;
|
||||
}
|
||||
|
||||
bool setp_can_flow_control(setp_can_frame_t *out, uint32_t can_id,
|
||||
uint8_t status, uint8_t block_size, uint8_t st_min_ms)
|
||||
{
|
||||
if ((out == NULL) || (status > SETP_CAN_FC_OVERFLOW)
|
||||
|| (st_min_ms > 0x7FU) || ((can_id & ~SETP_CAN_ID_MASK) != 0U)) {
|
||||
return false;
|
||||
}
|
||||
(void)memset(out, 0, sizeof(*out));
|
||||
out->id = can_id;
|
||||
out->length = 3U;
|
||||
out->data[0] = (uint8_t)(SETP_CAN_PCI_FLOW_CONTROL | status);
|
||||
out->data[1] = block_size;
|
||||
out->data[2] = st_min_ms;
|
||||
return true;
|
||||
}
|
||||
167
c/set-protocol/src/set_firmware.c
Normal file
167
c/set-protocol/src/set_firmware.c
Normal file
@@ -0,0 +1,167 @@
|
||||
#include "set_firmware.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
size_t setp_fw_begin_encode(const setp_fw_begin_t *value, uint8_t *out, size_t capacity)
|
||||
{
|
||||
size_t total;
|
||||
if ((value == NULL) || (out == NULL)
|
||||
|| (value->block_size == 0U)
|
||||
|| (value->block_size > SETP_MAX_PAYLOAD - SETP_FW_DATA_HEADER_SIZE)
|
||||
|| ((value->signature_length != 0U) && (value->signature == NULL))) {
|
||||
return 0U;
|
||||
}
|
||||
total = (size_t)SETP_FW_BEGIN_FIXED_SIZE + value->signature_length;
|
||||
if ((total > SETP_MAX_PAYLOAD) || (capacity < total)
|
||||
|| (((value->flags & SETP_FW_FLAG_SIGNED) != 0U)
|
||||
&& (value->signature_length == 0U))) {
|
||||
return 0U;
|
||||
}
|
||||
setp_put_u32(&out[0], value->image_size);
|
||||
setp_put_u32(&out[4], value->image_crc32);
|
||||
setp_put_u32(&out[8], value->image_version);
|
||||
setp_put_u32(&out[12], value->base_address);
|
||||
out[16] = value->slot;
|
||||
out[17] = value->flags;
|
||||
setp_put_u16(&out[18], value->block_size);
|
||||
(void)memcpy(&out[20], value->sha256, SETP_SHA256_SIZE);
|
||||
setp_put_u32(&out[52], value->signing_key_id);
|
||||
setp_put_u16(&out[56], value->signature_length);
|
||||
if (value->signature_length != 0U) {
|
||||
(void)memcpy(&out[SETP_FW_BEGIN_FIXED_SIZE], value->signature,
|
||||
value->signature_length);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
bool setp_fw_begin_decode(const uint8_t *payload, uint16_t length, setp_fw_begin_t *out)
|
||||
{
|
||||
uint16_t signature_length;
|
||||
if ((payload == NULL) || (out == NULL) || (length < SETP_FW_BEGIN_FIXED_SIZE)) {
|
||||
return false;
|
||||
}
|
||||
out->image_size = setp_get_u32(&payload[0]);
|
||||
out->image_crc32 = setp_get_u32(&payload[4]);
|
||||
out->image_version = setp_get_u32(&payload[8]);
|
||||
out->base_address = setp_get_u32(&payload[12]);
|
||||
out->slot = payload[16];
|
||||
out->flags = payload[17];
|
||||
out->block_size = setp_get_u16(&payload[18]);
|
||||
(void)memcpy(out->sha256, &payload[20], SETP_SHA256_SIZE);
|
||||
out->signing_key_id = setp_get_u32(&payload[52]);
|
||||
signature_length = setp_get_u16(&payload[56]);
|
||||
if (length != SETP_FW_BEGIN_FIXED_SIZE + signature_length) {
|
||||
return false;
|
||||
}
|
||||
out->signature_length = signature_length;
|
||||
out->signature = signature_length != 0U ? &payload[SETP_FW_BEGIN_FIXED_SIZE] : NULL;
|
||||
return (out->block_size != 0U)
|
||||
&& (out->block_size <= SETP_MAX_PAYLOAD - SETP_FW_DATA_HEADER_SIZE)
|
||||
&& (((out->flags & SETP_FW_FLAG_SIGNED) == 0U) || (signature_length != 0U));
|
||||
}
|
||||
|
||||
size_t setp_fw_data_encode(uint32_t offset, uint16_t flags,
|
||||
const uint8_t *data, uint16_t data_length,
|
||||
uint8_t *out, size_t capacity)
|
||||
{
|
||||
size_t total = (size_t)SETP_FW_DATA_HEADER_SIZE + data_length;
|
||||
if ((data == NULL) || (out == NULL) || (data_length == 0U)
|
||||
|| (total > SETP_MAX_PAYLOAD) || (capacity < total)) {
|
||||
return 0U;
|
||||
}
|
||||
setp_put_u32(&out[0], offset);
|
||||
setp_put_u16(&out[4], data_length);
|
||||
setp_put_u16(&out[6], flags);
|
||||
setp_put_u32(&out[8], setp_crc32(data, data_length));
|
||||
(void)memcpy(&out[SETP_FW_DATA_HEADER_SIZE], data, data_length);
|
||||
return total;
|
||||
}
|
||||
|
||||
bool setp_fw_data_decode(const uint8_t *payload, uint16_t length, setp_fw_data_t *out)
|
||||
{
|
||||
uint16_t data_length;
|
||||
if ((payload == NULL) || (out == NULL) || (length < SETP_FW_DATA_HEADER_SIZE)) {
|
||||
return false;
|
||||
}
|
||||
data_length = setp_get_u16(&payload[4]);
|
||||
if ((data_length == 0U) || (length != SETP_FW_DATA_HEADER_SIZE + data_length)) {
|
||||
return false;
|
||||
}
|
||||
out->offset = setp_get_u32(&payload[0]);
|
||||
out->data_length = data_length;
|
||||
out->flags = setp_get_u16(&payload[6]);
|
||||
out->data_crc32 = setp_get_u32(&payload[8]);
|
||||
out->data = &payload[SETP_FW_DATA_HEADER_SIZE];
|
||||
return setp_crc32(out->data, out->data_length) == out->data_crc32;
|
||||
}
|
||||
|
||||
size_t setp_fw_end_encode(const setp_fw_end_t *value, uint8_t *out, size_t capacity)
|
||||
{
|
||||
if ((value == NULL) || (out == NULL) || (capacity < SETP_FW_END_SIZE)) {
|
||||
return 0U;
|
||||
}
|
||||
setp_put_u32(&out[0], value->image_size);
|
||||
setp_put_u32(&out[4], value->image_crc32);
|
||||
(void)memcpy(&out[8], value->sha256, SETP_SHA256_SIZE);
|
||||
return SETP_FW_END_SIZE;
|
||||
}
|
||||
|
||||
bool setp_fw_end_decode(const uint8_t *payload, uint16_t length, setp_fw_end_t *out)
|
||||
{
|
||||
if ((payload == NULL) || (out == NULL) || (length != SETP_FW_END_SIZE)) {
|
||||
return false;
|
||||
}
|
||||
out->image_size = setp_get_u32(&payload[0]);
|
||||
out->image_crc32 = setp_get_u32(&payload[4]);
|
||||
(void)memcpy(out->sha256, &payload[8], SETP_SHA256_SIZE);
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t setp_fw_status_encode(const setp_fw_status_t *value, uint8_t *out, size_t capacity)
|
||||
{
|
||||
if ((value == NULL) || (out == NULL) || (capacity < SETP_FW_STATUS_SIZE)) {
|
||||
return 0U;
|
||||
}
|
||||
out[0] = value->state;
|
||||
out[1] = value->active_slot;
|
||||
setp_put_u16(&out[2], value->max_block_size);
|
||||
setp_put_u32(&out[4], value->next_offset);
|
||||
setp_put_u32(&out[8], value->image_size);
|
||||
setp_put_u16(&out[12], value->last_error);
|
||||
setp_put_u16(&out[14], value->flags);
|
||||
return SETP_FW_STATUS_SIZE;
|
||||
}
|
||||
|
||||
bool setp_fw_status_decode(const uint8_t *payload, uint16_t length, setp_fw_status_t *out)
|
||||
{
|
||||
if ((payload == NULL) || (out == NULL) || (length != SETP_FW_STATUS_SIZE)) {
|
||||
return false;
|
||||
}
|
||||
out->state = payload[0];
|
||||
out->active_slot = payload[1];
|
||||
out->max_block_size = setp_get_u16(&payload[2]);
|
||||
out->next_offset = setp_get_u32(&payload[4]);
|
||||
out->image_size = setp_get_u32(&payload[8]);
|
||||
out->last_error = setp_get_u16(&payload[12]);
|
||||
out->flags = setp_get_u16(&payload[14]);
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t setp_fw_manifest_encode(const setp_fw_begin_t *value,
|
||||
uint8_t *out, size_t capacity)
|
||||
{
|
||||
static const uint8_t prefix[8] = {
|
||||
'S', 'E', 'T', 'P', 'F', 'W', '2', 0U
|
||||
};
|
||||
if ((value == NULL) || (out == NULL) || (capacity < SETP_FW_MANIFEST_SIZE)) {
|
||||
return 0U;
|
||||
}
|
||||
(void)memcpy(out, prefix, sizeof(prefix));
|
||||
setp_put_u32(&out[8], value->image_size);
|
||||
setp_put_u32(&out[12], value->image_crc32);
|
||||
setp_put_u32(&out[16], value->image_version);
|
||||
setp_put_u32(&out[20], value->base_address);
|
||||
out[24] = value->slot;
|
||||
(void)memcpy(&out[25], value->sha256, SETP_SHA256_SIZE);
|
||||
return SETP_FW_MANIFEST_SIZE;
|
||||
}
|
||||
280
c/set-protocol/src/set_protocol.c
Normal file
280
c/set-protocol/src/set_protocol.c
Normal file
@@ -0,0 +1,280 @@
|
||||
#include "set_protocol.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
uint16_t setp_get_u16(const uint8_t *data)
|
||||
{
|
||||
return (uint16_t)((uint16_t)data[0] | ((uint16_t)data[1] << 8));
|
||||
}
|
||||
|
||||
uint32_t setp_get_u32(const uint8_t *data)
|
||||
{
|
||||
return (uint32_t)data[0]
|
||||
| ((uint32_t)data[1] << 8)
|
||||
| ((uint32_t)data[2] << 16)
|
||||
| ((uint32_t)data[3] << 24);
|
||||
}
|
||||
|
||||
void setp_put_u16(uint8_t *data, uint16_t value)
|
||||
{
|
||||
data[0] = (uint8_t)(value & 0xFFU);
|
||||
data[1] = (uint8_t)(value >> 8);
|
||||
}
|
||||
|
||||
void setp_put_u32(uint8_t *data, uint32_t value)
|
||||
{
|
||||
data[0] = (uint8_t)(value & 0xFFU);
|
||||
data[1] = (uint8_t)((value >> 8) & 0xFFU);
|
||||
data[2] = (uint8_t)((value >> 16) & 0xFFU);
|
||||
data[3] = (uint8_t)(value >> 24);
|
||||
}
|
||||
|
||||
uint32_t setp_crc32(const uint8_t *data, size_t length)
|
||||
{
|
||||
uint32_t crc = 0xFFFFFFFFUL;
|
||||
size_t index;
|
||||
uint8_t bit;
|
||||
|
||||
if ((data == NULL) && (length != 0U)) {
|
||||
return 0U;
|
||||
}
|
||||
for (index = 0U; index < length; ++index) {
|
||||
crc ^= data[index];
|
||||
for (bit = 0U; bit < 8U; ++bit) {
|
||||
crc = (crc >> 1) ^ ((crc & 1U) ? 0xEDB88320UL : 0U);
|
||||
}
|
||||
}
|
||||
return crc ^ 0xFFFFFFFFUL;
|
||||
}
|
||||
|
||||
size_t setp_frame_encode(const setp_frame_t *frame, uint8_t *out, size_t capacity)
|
||||
{
|
||||
size_t total;
|
||||
uint32_t crc;
|
||||
|
||||
if ((frame == NULL) || (out == NULL)) {
|
||||
return 0U;
|
||||
}
|
||||
if ((frame->flags & SETP_FLAG_RESERVED_MASK) != 0U) {
|
||||
return 0U;
|
||||
}
|
||||
if ((frame->payload_length > SETP_MAX_PAYLOAD)
|
||||
|| ((frame->payload_length != 0U) && (frame->payload == NULL))) {
|
||||
return 0U;
|
||||
}
|
||||
total = (size_t)SETP_HEADER_SIZE + frame->payload_length + SETP_CRC_SIZE;
|
||||
if (capacity < total) {
|
||||
return 0U;
|
||||
}
|
||||
|
||||
out[0] = SETP_SOF0;
|
||||
out[1] = SETP_SOF1;
|
||||
out[2] = SETP_VERSION;
|
||||
out[3] = frame->flags;
|
||||
setp_put_u16(&out[4], frame->message_type);
|
||||
setp_put_u16(&out[6], frame->source);
|
||||
setp_put_u16(&out[8], frame->destination);
|
||||
setp_put_u16(&out[10], frame->sequence);
|
||||
setp_put_u16(&out[12], frame->payload_length);
|
||||
if (frame->payload_length != 0U) {
|
||||
(void)memcpy(&out[SETP_HEADER_SIZE], frame->payload, frame->payload_length);
|
||||
}
|
||||
crc = setp_crc32(&out[2], (size_t)(SETP_HEADER_SIZE - 2U) + frame->payload_length);
|
||||
setp_put_u32(&out[SETP_HEADER_SIZE + frame->payload_length], crc);
|
||||
return total;
|
||||
}
|
||||
|
||||
bool setp_frame_decode_datagram(const uint8_t *data, size_t length, setp_frame_t *out)
|
||||
{
|
||||
uint16_t payload_length;
|
||||
size_t expected_length;
|
||||
uint32_t expected_crc;
|
||||
if ((data == NULL) || (out == NULL) || (length < SETP_HEADER_SIZE + SETP_CRC_SIZE)
|
||||
|| (data[0] != SETP_SOF0) || (data[1] != SETP_SOF1)
|
||||
|| (data[2] != SETP_VERSION)
|
||||
|| ((data[3] & SETP_FLAG_RESERVED_MASK) != 0U)) {
|
||||
return false;
|
||||
}
|
||||
payload_length = setp_get_u16(&data[12]);
|
||||
expected_length = (size_t)SETP_HEADER_SIZE + payload_length + SETP_CRC_SIZE;
|
||||
if ((payload_length > SETP_MAX_PAYLOAD) || (length != expected_length)) {
|
||||
return false;
|
||||
}
|
||||
expected_crc = setp_get_u32(&data[SETP_HEADER_SIZE + payload_length]);
|
||||
if (setp_crc32(&data[2], (size_t)(SETP_HEADER_SIZE - 2U) + payload_length)
|
||||
!= expected_crc) {
|
||||
return false;
|
||||
}
|
||||
out->flags = data[3];
|
||||
out->message_type = setp_get_u16(&data[4]);
|
||||
out->source = setp_get_u16(&data[6]);
|
||||
out->destination = setp_get_u16(&data[8]);
|
||||
out->sequence = setp_get_u16(&data[10]);
|
||||
out->payload_length = payload_length;
|
||||
out->payload = &data[SETP_HEADER_SIZE];
|
||||
return true;
|
||||
}
|
||||
|
||||
void setp_parser_init(setp_parser_t *parser)
|
||||
{
|
||||
if (parser != NULL) {
|
||||
(void)memset(parser, 0, sizeof(*parser));
|
||||
}
|
||||
}
|
||||
|
||||
void setp_parser_reset(setp_parser_t *parser)
|
||||
{
|
||||
if (parser != NULL) {
|
||||
parser->length = 0U;
|
||||
}
|
||||
}
|
||||
|
||||
static void setp_drop(setp_parser_t *parser, uint16_t count)
|
||||
{
|
||||
if (count >= parser->length) {
|
||||
parser->length = 0U;
|
||||
return;
|
||||
}
|
||||
(void)memmove(parser->buffer, &parser->buffer[count], parser->length - count);
|
||||
parser->length = (uint16_t)(parser->length - count);
|
||||
}
|
||||
|
||||
static uint16_t setp_find_sof(const setp_parser_t *parser)
|
||||
{
|
||||
uint16_t index;
|
||||
for (index = 0U; index + 1U < parser->length; ++index) {
|
||||
if ((parser->buffer[index] == SETP_SOF0)
|
||||
&& (parser->buffer[index + 1U] == SETP_SOF1)) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return UINT16_MAX;
|
||||
}
|
||||
|
||||
static size_t setp_parser_process(setp_parser_t *parser,
|
||||
setp_frame_fn callback, void *user)
|
||||
{
|
||||
size_t delivered = 0U;
|
||||
|
||||
for (;;) {
|
||||
uint16_t sof;
|
||||
uint16_t payload_length;
|
||||
uint16_t total;
|
||||
uint32_t expected_crc;
|
||||
uint32_t actual_crc;
|
||||
setp_frame_t frame;
|
||||
|
||||
if (parser->length < 2U) {
|
||||
break;
|
||||
}
|
||||
sof = setp_find_sof(parser);
|
||||
if (sof == UINT16_MAX) {
|
||||
if (parser->buffer[parser->length - 1U] == SETP_SOF0) {
|
||||
parser->stats.stray_bytes += parser->length - 1U;
|
||||
parser->buffer[0] = SETP_SOF0;
|
||||
parser->length = 1U;
|
||||
} else {
|
||||
parser->stats.stray_bytes += parser->length;
|
||||
parser->length = 0U;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (sof != 0U) {
|
||||
parser->stats.stray_bytes += sof;
|
||||
setp_drop(parser, sof);
|
||||
}
|
||||
if (parser->length < SETP_HEADER_SIZE) {
|
||||
break;
|
||||
}
|
||||
if (parser->buffer[2] != SETP_VERSION) {
|
||||
++parser->stats.version_errors;
|
||||
setp_drop(parser, 1U);
|
||||
continue;
|
||||
}
|
||||
if ((parser->buffer[3] & SETP_FLAG_RESERVED_MASK) != 0U) {
|
||||
++parser->stats.flag_errors;
|
||||
setp_drop(parser, 1U);
|
||||
continue;
|
||||
}
|
||||
payload_length = setp_get_u16(&parser->buffer[12]);
|
||||
if (payload_length > SETP_MAX_PAYLOAD) {
|
||||
++parser->stats.length_errors;
|
||||
setp_drop(parser, 1U);
|
||||
continue;
|
||||
}
|
||||
total = (uint16_t)(SETP_HEADER_SIZE + payload_length + SETP_CRC_SIZE);
|
||||
if (parser->length < total) {
|
||||
break;
|
||||
}
|
||||
expected_crc = setp_get_u32(&parser->buffer[SETP_HEADER_SIZE + payload_length]);
|
||||
actual_crc = setp_crc32(&parser->buffer[2],
|
||||
(size_t)(SETP_HEADER_SIZE - 2U) + payload_length);
|
||||
if (expected_crc != actual_crc) {
|
||||
++parser->stats.crc_errors;
|
||||
setp_drop(parser, 1U);
|
||||
continue;
|
||||
}
|
||||
|
||||
frame.flags = parser->buffer[3];
|
||||
frame.message_type = setp_get_u16(&parser->buffer[4]);
|
||||
frame.source = setp_get_u16(&parser->buffer[6]);
|
||||
frame.destination = setp_get_u16(&parser->buffer[8]);
|
||||
frame.sequence = setp_get_u16(&parser->buffer[10]);
|
||||
frame.payload_length = payload_length;
|
||||
frame.payload = &parser->buffer[SETP_HEADER_SIZE];
|
||||
if (callback != NULL) {
|
||||
callback(&frame, user);
|
||||
}
|
||||
++parser->stats.frames;
|
||||
++delivered;
|
||||
setp_drop(parser, total);
|
||||
}
|
||||
return delivered;
|
||||
}
|
||||
|
||||
size_t setp_parser_feed(setp_parser_t *parser, const uint8_t *data, size_t length,
|
||||
setp_frame_fn callback, void *user)
|
||||
{
|
||||
size_t index;
|
||||
size_t delivered = 0U;
|
||||
|
||||
if ((parser == NULL) || ((data == NULL) && (length != 0U))) {
|
||||
return 0U;
|
||||
}
|
||||
for (index = 0U; index < length; ++index) {
|
||||
if (parser->length >= SETP_FRAME_MAX) {
|
||||
++parser->stats.overflows;
|
||||
parser->length = 0U;
|
||||
}
|
||||
parser->buffer[parser->length++] = data[index];
|
||||
delivered += setp_parser_process(parser, callback, user);
|
||||
}
|
||||
return delivered;
|
||||
}
|
||||
|
||||
size_t setp_status_encode(uint16_t status, uint8_t *out, size_t capacity)
|
||||
{
|
||||
if ((out == NULL) || (capacity < 2U)) {
|
||||
return 0U;
|
||||
}
|
||||
setp_put_u16(out, status);
|
||||
return 2U;
|
||||
}
|
||||
|
||||
bool setp_status_decode(const setp_frame_t *frame, uint16_t *status,
|
||||
const uint8_t **body, uint16_t *body_length)
|
||||
{
|
||||
if ((frame == NULL) || (status == NULL)
|
||||
|| ((frame->flags & SETP_FLAG_RESPONSE) == 0U)
|
||||
|| (frame->payload_length < 2U) || (frame->payload == NULL)) {
|
||||
return false;
|
||||
}
|
||||
*status = setp_get_u16(frame->payload);
|
||||
if (body != NULL) {
|
||||
*body = &frame->payload[2];
|
||||
}
|
||||
if (body_length != NULL) {
|
||||
*body_length = (uint16_t)(frame->payload_length - 2U);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
155
c/set-protocol/src/set_telemetry.c
Normal file
155
c/set-protocol/src/set_telemetry.c
Normal file
@@ -0,0 +1,155 @@
|
||||
#include "set_telemetry.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
size_t setp_subscribe_encode(uint16_t subscription_id, uint32_t period_ms,
|
||||
const uint32_t *addresses, uint16_t address_count,
|
||||
uint8_t *out, size_t capacity)
|
||||
{
|
||||
size_t total = (size_t)SETP_SUBSCRIBE_HEADER_SIZE + ((size_t)address_count * 4U);
|
||||
uint16_t index;
|
||||
if ((out == NULL) || ((address_count != 0U) && (addresses == NULL))
|
||||
|| (total > SETP_MAX_PAYLOAD) || (capacity < total)) {
|
||||
return 0U;
|
||||
}
|
||||
setp_put_u16(&out[0], subscription_id);
|
||||
setp_put_u32(&out[2], period_ms);
|
||||
setp_put_u16(&out[6], address_count);
|
||||
for (index = 0U; index < address_count; ++index) {
|
||||
setp_put_u32(&out[SETP_SUBSCRIBE_HEADER_SIZE + (index * 4U)], addresses[index]);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
bool setp_subscribe_decode_header(const uint8_t *payload, uint16_t length,
|
||||
uint16_t *subscription_id, uint32_t *period_ms,
|
||||
uint16_t *address_count)
|
||||
{
|
||||
uint16_t count;
|
||||
if ((payload == NULL) || (subscription_id == NULL)
|
||||
|| (period_ms == NULL) || (address_count == NULL)
|
||||
|| (length < SETP_SUBSCRIBE_HEADER_SIZE)) {
|
||||
return false;
|
||||
}
|
||||
count = setp_get_u16(&payload[6]);
|
||||
if (length != SETP_SUBSCRIBE_HEADER_SIZE + (count * 4U)) {
|
||||
return false;
|
||||
}
|
||||
*subscription_id = setp_get_u16(&payload[0]);
|
||||
*period_ms = setp_get_u32(&payload[2]);
|
||||
*address_count = count;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool setp_subscribe_address(const uint8_t *payload, uint16_t length,
|
||||
uint16_t index, uint32_t *address)
|
||||
{
|
||||
uint16_t subscription_id;
|
||||
uint32_t period_ms;
|
||||
uint16_t count;
|
||||
if ((address == NULL)
|
||||
|| !setp_subscribe_decode_header(payload, length, &subscription_id,
|
||||
&period_ms, &count)
|
||||
|| (index >= count)) {
|
||||
return false;
|
||||
}
|
||||
(void)subscription_id;
|
||||
(void)period_ms;
|
||||
*address = setp_get_u32(&payload[SETP_SUBSCRIBE_HEADER_SIZE + (index * 4U)]);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool setp_publish_begin(setp_publish_builder_t *builder,
|
||||
uint16_t subscription_id, uint16_t sample_sequence,
|
||||
uint32_t timestamp_ms, uint8_t *out, uint16_t capacity)
|
||||
{
|
||||
if ((builder == NULL) || (out == NULL) || (capacity < SETP_PUBLISH_HEADER_SIZE)) {
|
||||
return false;
|
||||
}
|
||||
builder->output = out;
|
||||
builder->capacity = capacity;
|
||||
builder->length = SETP_PUBLISH_HEADER_SIZE;
|
||||
builder->item_count = 0U;
|
||||
setp_put_u16(&out[0], subscription_id);
|
||||
setp_put_u16(&out[2], sample_sequence);
|
||||
setp_put_u32(&out[4], timestamp_ms);
|
||||
setp_put_u16(&out[8], 0U);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool setp_publish_add(setp_publish_builder_t *builder, uint32_t address,
|
||||
uint8_t encoding, uint8_t element_count,
|
||||
const uint8_t *data, uint16_t data_length)
|
||||
{
|
||||
uint16_t offset;
|
||||
size_t next;
|
||||
if ((builder == NULL) || (builder->output == NULL)
|
||||
|| (element_count == 0U) || (data_length == 0U) || (data == NULL)
|
||||
|| (encoding < SETP_VALUE_U16) || (encoding > SETP_VALUE_BYTES)) {
|
||||
return false;
|
||||
}
|
||||
next = (size_t)builder->length + SETP_PUBLISH_ITEM_HEADER_SIZE + data_length;
|
||||
if ((next > builder->capacity) || (next > SETP_MAX_PAYLOAD)) {
|
||||
return false;
|
||||
}
|
||||
offset = builder->length;
|
||||
setp_put_u32(&builder->output[offset], address);
|
||||
builder->output[offset + 4U] = encoding;
|
||||
builder->output[offset + 5U] = element_count;
|
||||
setp_put_u16(&builder->output[offset + 6U], data_length);
|
||||
(void)memcpy(&builder->output[offset + SETP_PUBLISH_ITEM_HEADER_SIZE], data, data_length);
|
||||
builder->length = (uint16_t)next;
|
||||
++builder->item_count;
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t setp_publish_finish(setp_publish_builder_t *builder)
|
||||
{
|
||||
if ((builder == NULL) || (builder->output == NULL)) {
|
||||
return 0U;
|
||||
}
|
||||
setp_put_u16(&builder->output[8], builder->item_count);
|
||||
return builder->length;
|
||||
}
|
||||
|
||||
bool setp_publish_view_init(setp_publish_view_t *view,
|
||||
const uint8_t *payload, uint16_t length)
|
||||
{
|
||||
if ((view == NULL) || (payload == NULL) || (length < SETP_PUBLISH_HEADER_SIZE)) {
|
||||
return false;
|
||||
}
|
||||
view->payload = payload;
|
||||
view->length = length;
|
||||
view->offset = SETP_PUBLISH_HEADER_SIZE;
|
||||
view->subscription_id = setp_get_u16(&payload[0]);
|
||||
view->sample_sequence = setp_get_u16(&payload[2]);
|
||||
view->timestamp_ms = setp_get_u32(&payload[4]);
|
||||
view->remaining = setp_get_u16(&payload[8]);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool setp_publish_next(setp_publish_view_t *view, setp_publish_item_t *item)
|
||||
{
|
||||
uint16_t data_length;
|
||||
size_t end;
|
||||
if ((view == NULL) || (item == NULL) || (view->remaining == 0U)
|
||||
|| (view->offset + SETP_PUBLISH_ITEM_HEADER_SIZE > view->length)) {
|
||||
return false;
|
||||
}
|
||||
data_length = setp_get_u16(&view->payload[view->offset + 6U]);
|
||||
end = (size_t)view->offset + SETP_PUBLISH_ITEM_HEADER_SIZE + data_length;
|
||||
if ((data_length == 0U) || (end > view->length)) {
|
||||
return false;
|
||||
}
|
||||
item->address = setp_get_u32(&view->payload[view->offset]);
|
||||
item->encoding = view->payload[view->offset + 4U];
|
||||
item->element_count = view->payload[view->offset + 5U];
|
||||
item->data_length = data_length;
|
||||
item->data = &view->payload[view->offset + SETP_PUBLISH_ITEM_HEADER_SIZE];
|
||||
view->offset = (uint16_t)end;
|
||||
--view->remaining;
|
||||
if ((view->remaining == 0U) && (view->offset != view->length)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
302
c/set-protocol/tests/test_set_protocol.c
Normal file
302
c/set-protocol/tests/test_set_protocol.c
Normal file
@@ -0,0 +1,302 @@
|
||||
#include "set_firmware.h"
|
||||
#include "set_can.h"
|
||||
#include "set_protocol.h"
|
||||
#include "set_telemetry.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static int failures;
|
||||
static int checks;
|
||||
|
||||
static void check_impl(int condition, const char *text, int line)
|
||||
{
|
||||
++checks;
|
||||
if (!condition) {
|
||||
++failures;
|
||||
(void)printf("FAIL line %d: %s\n", line, text);
|
||||
}
|
||||
}
|
||||
|
||||
#define CHECK(condition) check_impl((condition) ? 1 : 0, #condition, __LINE__)
|
||||
|
||||
typedef struct {
|
||||
setp_frame_t frame;
|
||||
uint8_t payload[SETP_MAX_PAYLOAD];
|
||||
int count;
|
||||
} capture_t;
|
||||
|
||||
static void capture_frame(const setp_frame_t *frame, void *user)
|
||||
{
|
||||
capture_t *capture = (capture_t *)user;
|
||||
capture->frame = *frame;
|
||||
if (frame->payload_length != 0U) {
|
||||
(void)memcpy(capture->payload, frame->payload, frame->payload_length);
|
||||
}
|
||||
capture->frame.payload = capture->payload;
|
||||
++capture->count;
|
||||
}
|
||||
|
||||
static void test_crc_and_reference_frame(void)
|
||||
{
|
||||
static const uint8_t expected[] = {
|
||||
0xA5U, 0x5AU, 0x02U, 0x08U, 0x01U, 0x00U, 0x00U, 0x00U,
|
||||
0x2AU, 0x00U, 0x34U, 0x12U, 0x00U, 0x00U, 0x33U, 0xECU,
|
||||
0x33U, 0x04U
|
||||
};
|
||||
setp_frame_t frame;
|
||||
uint8_t output[SETP_FRAME_MAX];
|
||||
size_t length;
|
||||
setp_frame_t decoded;
|
||||
|
||||
CHECK(setp_crc32((const uint8_t *)"123456789", 9U) == 0xCBF43926UL);
|
||||
(void)memset(&frame, 0, sizeof(frame));
|
||||
frame.flags = SETP_FLAG_ACK_REQUIRED;
|
||||
frame.message_type = SETP_MSG_PING;
|
||||
frame.destination = 0x002AU;
|
||||
frame.sequence = 0x1234U;
|
||||
length = setp_frame_encode(&frame, output, sizeof(output));
|
||||
CHECK(length == sizeof(expected));
|
||||
CHECK(memcmp(output, expected, sizeof(expected)) == 0);
|
||||
CHECK(setp_frame_decode_datagram(output, length, &decoded));
|
||||
CHECK(decoded.message_type == SETP_MSG_PING);
|
||||
CHECK(decoded.destination == 0x002AU);
|
||||
CHECK(!setp_frame_decode_datagram(output, length - 1U, &decoded));
|
||||
CHECK(setp_frame_encode(&frame, output, 4U) == 0U);
|
||||
}
|
||||
|
||||
static void test_parser(void)
|
||||
{
|
||||
setp_frame_t frame;
|
||||
uint8_t raw[SETP_FRAME_MAX];
|
||||
uint8_t payload[] = { 0x11U, 0x22U, 0x33U };
|
||||
size_t length;
|
||||
setp_parser_t parser;
|
||||
capture_t capture;
|
||||
|
||||
(void)memset(&frame, 0, sizeof(frame));
|
||||
frame.message_type = 0x3456U; /* Unknown extensions are valid frames. */
|
||||
frame.source = 7U;
|
||||
frame.destination = 9U;
|
||||
frame.sequence = 11U;
|
||||
frame.payload_length = sizeof(payload);
|
||||
frame.payload = payload;
|
||||
length = setp_frame_encode(&frame, raw, sizeof(raw));
|
||||
|
||||
(void)memset(&capture, 0, sizeof(capture));
|
||||
setp_parser_init(&parser);
|
||||
CHECK(setp_parser_feed(&parser, (const uint8_t *)"noise", 5U,
|
||||
capture_frame, &capture) == 0U);
|
||||
CHECK(setp_parser_feed(&parser, raw, 3U, capture_frame, &capture) == 0U);
|
||||
CHECK(setp_parser_feed(&parser, &raw[3], length - 3U,
|
||||
capture_frame, &capture) == 1U);
|
||||
CHECK(capture.count == 1);
|
||||
CHECK(capture.frame.message_type == 0x3456U);
|
||||
CHECK(capture.frame.source == 7U);
|
||||
CHECK(capture.frame.destination == 9U);
|
||||
CHECK(capture.frame.sequence == 11U);
|
||||
CHECK(capture.frame.payload_length == sizeof(payload));
|
||||
CHECK(memcmp(capture.payload, payload, sizeof(payload)) == 0);
|
||||
CHECK(parser.stats.stray_bytes == 5U);
|
||||
|
||||
raw[length - 1U] ^= 1U;
|
||||
CHECK(setp_parser_feed(&parser, raw, length, capture_frame, &capture) == 0U);
|
||||
CHECK(parser.stats.crc_errors == 1U);
|
||||
}
|
||||
|
||||
static void test_status(void)
|
||||
{
|
||||
uint8_t payload[4] = { 0U };
|
||||
setp_frame_t frame;
|
||||
uint16_t status;
|
||||
uint16_t body_length;
|
||||
const uint8_t *body;
|
||||
|
||||
CHECK(setp_status_encode(SETP_STATUS_BUSY, payload, sizeof(payload)) == 2U);
|
||||
payload[2] = 0xAAU;
|
||||
payload[3] = 0x55U;
|
||||
(void)memset(&frame, 0, sizeof(frame));
|
||||
frame.flags = SETP_FLAG_RESPONSE | SETP_FLAG_ERROR;
|
||||
frame.payload = payload;
|
||||
frame.payload_length = sizeof(payload);
|
||||
CHECK(setp_status_decode(&frame, &status, &body, &body_length));
|
||||
CHECK(status == SETP_STATUS_BUSY);
|
||||
CHECK(body_length == 2U);
|
||||
CHECK(body[0] == 0xAAU && body[1] == 0x55U);
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
setp_can_frame_t frames[96];
|
||||
uint16_t count;
|
||||
} can_capture_t;
|
||||
|
||||
static bool capture_can(const setp_can_frame_t *frame, void *user)
|
||||
{
|
||||
can_capture_t *capture = (can_capture_t *)user;
|
||||
if (capture->count >= 96U) {
|
||||
return false;
|
||||
}
|
||||
capture->frames[capture->count++] = *frame;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void test_can_binding(void)
|
||||
{
|
||||
setp_can_id_t id = { 7U, 9U, 1U, 3U };
|
||||
setp_can_id_t decoded_id;
|
||||
uint32_t raw_id = setp_can_id_pack(&id);
|
||||
setp_frame_t frame;
|
||||
uint8_t payload[40];
|
||||
uint8_t packet[SETP_FRAME_MAX];
|
||||
size_t packet_length;
|
||||
can_capture_t capture;
|
||||
setp_can_rx_t rx;
|
||||
setp_can_packet_t complete;
|
||||
setp_can_rx_result_t result = SETP_CAN_RX_NONE;
|
||||
uint16_t index;
|
||||
|
||||
CHECK(setp_can_id_unpack(raw_id, &decoded_id));
|
||||
CHECK(decoded_id.source == 7U);
|
||||
CHECK(decoded_id.destination == 9U);
|
||||
CHECK(decoded_id.priority == 1U);
|
||||
CHECK(decoded_id.channel == 3U);
|
||||
for (index = 0U; index < sizeof(payload); ++index) {
|
||||
payload[index] = (uint8_t)index;
|
||||
}
|
||||
(void)memset(&frame, 0, sizeof(frame));
|
||||
frame.flags = SETP_FLAG_PRIORITY;
|
||||
frame.message_type = SETP_MSG_PUBLISH;
|
||||
frame.source = 7U;
|
||||
frame.destination = 9U;
|
||||
frame.sequence = 3U;
|
||||
frame.payload = payload;
|
||||
frame.payload_length = sizeof(payload);
|
||||
packet_length = setp_frame_encode(&frame, packet, sizeof(packet));
|
||||
(void)memset(&capture, 0, sizeof(capture));
|
||||
(void)memset(&complete, 0, sizeof(complete));
|
||||
CHECK(setp_can_segment(packet, (uint16_t)packet_length, raw_id,
|
||||
capture_can, &capture));
|
||||
CHECK(capture.count > 1U);
|
||||
CHECK(capture.frames[0].data[0] == SETP_CAN_PCI_FIRST);
|
||||
setp_can_rx_init(&rx);
|
||||
for (index = 0U; index < capture.count; ++index) {
|
||||
result = setp_can_rx_feed(&rx, &capture.frames[index], index, &complete);
|
||||
}
|
||||
CHECK(result == SETP_CAN_RX_COMPLETE);
|
||||
CHECK(complete.length == packet_length);
|
||||
CHECK(complete.can_id == raw_id);
|
||||
CHECK(memcmp(complete.data, packet, packet_length) == 0);
|
||||
}
|
||||
|
||||
static void test_firmware(void)
|
||||
{
|
||||
setp_fw_begin_t begin;
|
||||
setp_fw_begin_t decoded_begin;
|
||||
setp_fw_data_t decoded_data;
|
||||
uint8_t signature[64];
|
||||
uint8_t image[32];
|
||||
uint8_t manifest[SETP_FW_MANIFEST_SIZE];
|
||||
uint8_t payload[SETP_MAX_PAYLOAD];
|
||||
size_t length;
|
||||
uint16_t index;
|
||||
|
||||
(void)memset(&begin, 0, sizeof(begin));
|
||||
(void)memset(signature, 0xA5, sizeof(signature));
|
||||
for (index = 0U; index < sizeof(image); ++index) {
|
||||
image[index] = (uint8_t)index;
|
||||
}
|
||||
begin.image_size = 4096U;
|
||||
begin.image_crc32 = 0x12345678UL;
|
||||
begin.image_version = 0x00010203UL;
|
||||
begin.base_address = 0x08010000UL;
|
||||
begin.slot = 1U;
|
||||
begin.flags = SETP_FW_FLAG_RESUME | SETP_FW_FLAG_SIGNED;
|
||||
begin.block_size = 480U;
|
||||
(void)memset(begin.sha256, 0x5A, sizeof(begin.sha256));
|
||||
begin.signing_key_id = 3U;
|
||||
begin.signature_length = sizeof(signature);
|
||||
begin.signature = signature;
|
||||
length = setp_fw_begin_encode(&begin, payload, sizeof(payload));
|
||||
CHECK(length == SETP_FW_BEGIN_FIXED_SIZE + sizeof(signature));
|
||||
CHECK(setp_fw_begin_decode(payload, (uint16_t)length, &decoded_begin));
|
||||
CHECK(decoded_begin.image_size == begin.image_size);
|
||||
CHECK(decoded_begin.block_size == 480U);
|
||||
CHECK(decoded_begin.signing_key_id == 3U);
|
||||
CHECK(decoded_begin.signature_length == sizeof(signature));
|
||||
CHECK(memcmp(decoded_begin.signature, signature, sizeof(signature)) == 0);
|
||||
CHECK(setp_fw_manifest_encode(&decoded_begin, manifest, sizeof(manifest))
|
||||
== SETP_FW_MANIFEST_SIZE);
|
||||
CHECK(memcmp(manifest, "SETPFW2", 7U) == 0);
|
||||
CHECK(setp_get_u32(&manifest[8]) == begin.image_size);
|
||||
|
||||
length = setp_fw_data_encode(1024U, 0U, image, sizeof(image),
|
||||
payload, sizeof(payload));
|
||||
CHECK(length == SETP_FW_DATA_HEADER_SIZE + sizeof(image));
|
||||
CHECK(setp_fw_data_decode(payload, (uint16_t)length, &decoded_data));
|
||||
CHECK(decoded_data.offset == 1024U);
|
||||
CHECK(decoded_data.data_length == sizeof(image));
|
||||
CHECK(memcmp(decoded_data.data, image, sizeof(image)) == 0);
|
||||
payload[length - 1U] ^= 1U;
|
||||
CHECK(!setp_fw_data_decode(payload, (uint16_t)length, &decoded_data));
|
||||
}
|
||||
|
||||
static void test_telemetry(void)
|
||||
{
|
||||
uint32_t addresses[] = { 0x10U, 0x12345678UL };
|
||||
uint8_t payload[SETP_MAX_PAYLOAD];
|
||||
uint16_t subscription_id;
|
||||
uint16_t count;
|
||||
uint32_t period;
|
||||
uint32_t address;
|
||||
size_t length;
|
||||
setp_publish_builder_t builder;
|
||||
setp_publish_view_t view;
|
||||
setp_publish_item_t item;
|
||||
uint8_t value1[] = { 0x34U, 0x12U };
|
||||
uint8_t value2[] = { 1U, 2U, 3U, 4U };
|
||||
|
||||
length = setp_subscribe_encode(7U, 50U, addresses, 2U, payload, sizeof(payload));
|
||||
CHECK(length == 16U);
|
||||
CHECK(setp_subscribe_decode_header(payload, (uint16_t)length,
|
||||
&subscription_id, &period, &count));
|
||||
CHECK(subscription_id == 7U);
|
||||
CHECK(period == 50U);
|
||||
CHECK(count == 2U);
|
||||
CHECK(setp_subscribe_address(payload, (uint16_t)length, 1U, &address));
|
||||
CHECK(address == 0x12345678UL);
|
||||
|
||||
CHECK(setp_publish_begin(&builder, 7U, 9U, 123456U,
|
||||
payload, sizeof(payload)));
|
||||
CHECK(setp_publish_add(&builder, 0x10U, SETP_VALUE_U16, 1U,
|
||||
value1, sizeof(value1)));
|
||||
CHECK(setp_publish_add(&builder, 0x20U, SETP_VALUE_BYTES, 4U,
|
||||
value2, sizeof(value2)));
|
||||
length = setp_publish_finish(&builder);
|
||||
CHECK(length == SETP_PUBLISH_HEADER_SIZE
|
||||
+ (2U * SETP_PUBLISH_ITEM_HEADER_SIZE) + sizeof(value1) + sizeof(value2));
|
||||
CHECK(setp_publish_view_init(&view, payload, (uint16_t)length));
|
||||
CHECK(view.subscription_id == 7U);
|
||||
CHECK(view.sample_sequence == 9U);
|
||||
CHECK(view.timestamp_ms == 123456U);
|
||||
CHECK(setp_publish_next(&view, &item));
|
||||
CHECK(item.address == 0x10U);
|
||||
CHECK(item.encoding == SETP_VALUE_U16);
|
||||
CHECK(item.data_length == sizeof(value1));
|
||||
CHECK(setp_publish_next(&view, &item));
|
||||
CHECK(item.address == 0x20U);
|
||||
CHECK(item.element_count == 4U);
|
||||
CHECK(view.remaining == 0U);
|
||||
CHECK(!setp_publish_next(&view, &item));
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_crc_and_reference_frame();
|
||||
test_parser();
|
||||
test_status();
|
||||
test_can_binding();
|
||||
test_firmware();
|
||||
test_telemetry();
|
||||
(void)printf("checks: %d, failures: %d\n", checks, failures);
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
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_INPUT = 0b0111
|
||||
ERROR = 0b1000
|
||||
BOOT_CONTROL = 0b1001
|
||||
BOOT_DATA_A = 0b1010
|
||||
BOOT_DATA_B = 0b1011
|
||||
BOOT_STATUS = 0b1100
|
||||
BOOT_DISCOVERY = 0b1101
|
||||
PULSE = 0b1111
|
||||
|
||||
|
||||
@@ -90,6 +95,11 @@ MSGTYPE_RU = {
|
||||
MsgType.MODBUS_HOLDING: "Modbus Holding",
|
||||
MsgType.MODBUS_INPUT: "Modbus Input",
|
||||
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",
|
||||
}
|
||||
|
||||
@@ -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_INPUT: _decode_modbus_reg,
|
||||
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,
|
||||
}
|
||||
try:
|
||||
@@ -312,6 +327,28 @@ def decode(raw_id: int, data: bytes, ide: bool = True, rtr: bool = False) -> Dec
|
||||
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:
|
||||
body, btype = split_broadcast(res.id.msg_body)
|
||||
res.fields.append((" Broadcast.Type", "0x%03X — %s" % (
|
||||
|
||||
Reference in New Issue
Block a user