diff --git a/c/protocan-boot/README.md b/c/protocan-boot/README.md index 589f243..8eb00da 100644 --- a/c/protocan-boot/README.md +++ b/c/protocan-boot/README.md @@ -1,5 +1,9 @@ # ProtoCAN Boot +Каноническое описание обмена загрузчика перенесено из SETCAN в +[`docs/BOOTLOADER.md`](docs/BOOTLOADER.md). Реализация и документ меняются +вместе в этом переносимом модуле. + `protocan-boot` — переносимое C99-ядро адресной прошивки приборов по classic CAN 2.0B и 29-битному ProtoCAN ID. Оно реализует сессию обновления, два логических слота по 512 КиБ либо single-slot обновление, последовательную запись 8-байтовых блоков, diff --git a/c/protocan-boot/docs/BOOTLOADER.md b/c/protocan-boot/docs/BOOTLOADER.md new file mode 100644 index 0000000..f4ccf78 --- /dev/null +++ b/c/protocan-boot/docs/BOOTLOADER.md @@ -0,0 +1,183 @@ +# ProtoCAN Boot Protocol + +Статус: **Draft** +Версия протокола: **1.0** +Совместимость: **classic CAN 2.0B, Extended ID, DLC 0…8** +Реализация: [`templates/c/protocan-boot`](../../../templates/c/protocan-boot) + +## Назначение + +Сервис обновляет адресованный прибор по CAN и поддерживает два логических +слота A/B. Активный слот не стирается: новый образ записывается в неактивный, +проверяется и атомарно назначается кандидатом на запуск. + +## Карта сообщений + +| `MsgType` | Имя | `MsgBody` | Payload | +|---:|---|---|---| +| `0x9` | `BOOT_CONTROL` | `SessionID[15:8] \| Command[7:0]` | параметры команды | +| `0xA` | `BOOT_DATA_A` | `BlockIndex[15:0]` | 8 байт слота A | +| `0xB` | `BOOT_DATA_B` | `BlockIndex[15:0]` | 8 байт слота B | +| `0xC` | `BOOT_STATUS` | `SessionID[15:8] \| Command[7:0]` | статус и прогресс | +| `0xD` | `BOOT_DISCOVERY` | подтип ответа | идентификация | + +Все команды записи адресуются конкретному `DeviceType/DeviceID` и имеют +`Route=0`. Ответы сохраняют адрес прибора и имеют `Route=1`. + +## Адресация образа + +`MsgBody` кадра данных — номер 8-байтового блока: + +```c +offset = (uint32_t)BlockIndex * 8U; +address = SLOT_X_BASE + offset; +``` + +```text +512 КиБ = 524 288 байт +524 288 / 8 = 65 536 блоков +BlockIndex = 0x0000…0xFFFF +``` + +| `BlockIndex` | Смещение | Диапазон байтов | +|---:|---:|---:| +| `0x0000` | `0x00000` | `0x00000…0x00007` | +| `0x0001` | `0x00008` | `0x00008…0x0000F` | +| `0xFFFF` | `0x7FFF8` | `0x7FFF8…0x7FFFF` | + +`0x80000` является первой позицией за границей слота. Последний кадр +дополняется `0xFF`, но CRC32 вычисляется только по `ImageSize` байтам. + +## Команды `BOOT_CONTROL` + +| Код | Команда | DLC | Payload | Допустимое состояние | +|---:|---|---:|---|---| +| `0x01` | `IDENTIFY` | 0 | отсутствует | любое | +| `0x02` | `ENTER_BOOT` | 0 | отсутствует | любое; `SessionID != 0` | +| `0x03` | `BEGIN_IMAGE` | 8 | размер и CRC32 | metadata | +| `0x04` | `BEGIN_COMPAT` | 8 | совместимость и версия | metadata | +| `0x05` | `ERASE` | 0 | отсутствует | ready-to-erase | +| `0x06` | `VERIFY` | 0 | отсутствует | образ получен | +| `0x07` | `COMMIT` | 0 | отсутствует | verified | +| `0x08` | `CONFIRM` | 0 | отсутствует | запущенное приложение | +| `0x09` | `REBOOT` | 0 | отсутствует | активная сессия | +| `0x0A` | `ABORT` | 0 | отсутствует | активная сессия | +| `0x0B` | `QUERY_PROGRESS` | 0 | отсутствует | активная сессия | + +### `BEGIN_IMAGE` + +```text +DATA[0..3] ImageSize, uint32 little-endian +DATA[4..7] ImageCRC32, uint32 little-endian +``` + +### `BEGIN_COMPAT` + +```text +DATA[0..1] ProductType, uint16 little-endian +DATA[2] HardwareRevisionMin +DATA[3] HardwareRevisionMax +DATA[4..7] FirmwareVersion, uint32 little-endian +``` + +До `ERASE` прибор обязан получить обе части метаданных и проверить размер, +тип изделия, аппаратную ревизию, версию и политику anti-rollback. + +## `BOOT_STATUS` + +```text +MsgBody[15..8] SessionID +MsgBody[7..0] команда, на которую дан ответ + +DATA[0] Status +DATA[1] TargetSlot: 0=A, 1=B, 0xFF=не выбран +DATA[2..3] NextBlock, uint16 little-endian +DATA[4..7] RunningCRC32, uint32 little-endian +``` + +| Код | Статус | Повтор допустим | +|---:|---|---| +| `0x00` | `OK` | — | +| `0x01` | `BUSY` | да, после задержки | +| `0x02` | `INVALID_COMMAND` | после исправления | +| `0x03` | `WRONG_DEVICE` | нет для этого образа | +| `0x04` | `WRONG_HARDWARE` | нет для этого образа | +| `0x05` | `INVALID_SIZE` | нет для этого образа | +| `0x06` | `CRC_ERROR` | новая передача | +| `0x07` | `FLASH_ERROR` | зависит от платформы | +| `0x08` | `SEQUENCE_ERROR` | да, с `NextBlock` | +| `0x09` | `SIGNATURE_ERROR` | нет | +| `0x0A` | `SESSION_ERROR` | открыть новую сессию | +| `0x0B` | `VOLTAGE_ERROR` | да после нормализации питания | +| `0x0C` | `INVALID_STATE` | выполнить правильный переход | + +## State machine + +```text +IDLE + └─ ENTER_BOOT ─> METADATA + ├─ BEGIN_IMAGE + └─ BEGIN_COMPAT + │ + v + READY_TO_ERASE + │ ERASE + v + RECEIVING + │ VERIFY + v + VERIFIED + │ COMMIT + v + PENDING + REBOOT + │ CONFIRM + v + CONFIRMED +``` + +Ошибка Flash, CRC, совместимости или подписи переводит сессию в `FAILED`. +Новая `ENTER_BOOT` создаёт чистую сессию. `ABORT` прекращает текущую передачу, +не активируя частично записанный слот. + +## Надёжность и повторы + +- Блоки передаются строго по возрастанию `BlockIndex`. +- Дубликат или пропуск возвращает `SEQUENCE_ERROR` и ожидаемый `NextBlock`. +- Базовый режим подтверждает каждый блок. +- Рабочий режим может подтверждать окно из 16 блоков. +- После потери связи `QUERY_PROGRESS` возвращает следующий ожидаемый блок, + пока состояние загрузчика сохранено. +- Для продолжения после перезагрузки порт должен сохранять session metadata + и восстановить её при инициализации; ядро версии 1.0 само это не делает. + +## Безопасность и A/B-обновление + +```text +active=A -> target=B -> verify -> pending=B +active=B -> target=A -> verify -> pending=A +``` + +CRC32 защищает только от случайного повреждения. Серийный загрузчик должен +дополнительно проверить подпись контейнера, границы вектора, совместимость и +anti-rollback. Bootloader не обновляется командами `BOOT_DATA_A/B`. + +Boot metadata должна атомарно хранить: + +- активный слот; +- pending-слот; +- подтверждение запуска; +- число неудачных попыток; +- версию и CRC32 образа. + +Если приложение не выполняет `CONFIRM` за установленное число запусков, +загрузчик возвращается к предыдущему подтверждённому слоту. + +## Эталонный сценарий + +1. ПМ адресно отправляет `IDENTIFY`. +2. ПМ открывает ненулевой `SessionID` командой `ENTER_BOOT`. +3. ПМ отправляет `BEGIN_IMAGE` и `BEGIN_COMPAT`. +4. Прибор сообщает выбранный неактивный слот. +5. ПМ выполняет `ERASE` и передаёт `BOOT_DATA_A` либо `BOOT_DATA_B`. +6. ПМ выполняет `VERIFY`, затем `COMMIT` и `REBOOT`. +7. Новое приложение после самопроверки выполняет `CONFIRM`. diff --git a/c/protocan-transport/README.md b/c/protocan-transport/README.md index 4261d0d..f674e11 100644 --- a/c/protocan-transport/README.md +++ b/c/protocan-transport/README.md @@ -40,6 +40,12 @@ AA 55 | LEN | SEQ | FLAGS | ID0 ID1 ID2 ID3 | DATA[0..8] | CRC_L CRC_H `LEN = 6 + DLC` (6..14), CRC-16/CCITT-FALSE по байтам `LEN..DATA`, little-endian. Подробности — [docs/FRAME.md](docs/FRAME.md). +Полное описание прикладного ProtoCAN и общего адресного пространства теперь +также хранится здесь: [docs/PROTOCOL.md](docs/PROTOCOL.md) и +[docs/OAP.md](docs/OAP.md). Эталонные данные находятся в +`tests/vectors/test-vectors.json`. Это канонические документы; копии в SETCAN +считаются историческим снимком legacy HAL-адаптера. + ## Общее адресное пространство Плоское пространство 16-битных регистров `0x0000..0xFFFF`, собранное из diff --git a/c/protocan-transport/docs/CHANGELOG.md b/c/protocan-transport/docs/CHANGELOG.md new file mode 100644 index 0000000..edbd92e --- /dev/null +++ b/c/protocan-transport/docs/CHANGELOG.md @@ -0,0 +1,21 @@ +# История изменений ProtoCAN + +Формат основан на Keep a Changelog. Версия относится к спецификации, а не к +версии прошивки отдельного прибора. + +## [Unreleased] + +### Added + +- Структурированный комплект документации `doc/protocan`. +- Загрузочный сервис `MsgType=0x9…0xD`. +- Два логических слота по 512 КиБ, 65 536 блоков по 8 байт каждый. +- Машинные эталоны CAN ID в `examples/test-vectors.json`. + +## [1.0] — 2026-08-29 + +### Added + +- Зафиксирована 29-битная структура ProtoCAN ID. +- Зафиксирована адресация 8 типов по 16 экземпляров. +- Существующие сообщения `0x0…0x8`, `0xE`, `0xF` сохранены. diff --git a/c/protocan-transport/docs/OAP.md b/c/protocan-transport/docs/OAP.md new file mode 100644 index 0000000..b33a350 --- /dev/null +++ b/c/protocan-transport/docs/OAP.md @@ -0,0 +1,56 @@ +# Общее адресное пространство + +Статус: **Stable, данные ведутся в XLSX** +Порядок значений: **16-битные регистры, little-endian в CAN payload** + +Редактируемый источник реестра: +[`Протокол CAN и ОАП.xlsx`](../../Протокол%20CAN%20и%20ОАП.xlsx). + +Просматриваемая большая таблица находится в +[`Протокол CAN и ОАП.html`](../../Протокол%20CAN%20и%20ОАП.html) и +[`Протокол CAN и ОАП.md`](../../Протокол%20CAN%20и%20ОАП.md). + +## Назначение + +ОАП связывает 16-битный адрес регистра с его типом, назначением, доступом и +масштабом. В ProtoCAN используется `MsgType=0x3`, а `MsgBody` содержит адрес +первого регистра. + +## Обязательные поля реестра + +| Поле | Требование | +|---|---| +| AddressHex | `0x0000…0xFFFF`, уникальное значение | +| AddressDec | десятичный эквивалент AddressHex | +| Group | функциональная группа | +| Name | однозначное имя параметра | +| Type | `u16`, `i16`, `u32`, `i32`, `float32`, bitmap или массив | +| Registers | число занятых 16-битных регистров | +| Access | `R`, `W` или `RW` | +| Unit | физическая единица либо `—` | +| Scale | множитель/делитель представления | +| Default | значение после сброса, если применимо | +| Description | семантика, диапазон и особые значения | + +## Правила ведения + +- Адрес не переиспользуется с другим смыслом после выпуска стабильной версии. +- Многорегистровое значение занимает непрерывный диапазон. +- Порядок 16-битных слов для 32-битного значения фиксируется в строке типа. +- Резервные диапазоны явно отмечаются и не используются без изменения версии. +- Удалённый параметр помечается deprecated, а не исчезает молча. +- Изменение адреса, типа или масштаба отражается в `CHANGELOG.md`. + +## Экспорт + +Для программной генерации каталог следует экспортировать из XLSX в CSV с +UTF-8 и фиксированными английскими именами колонок. CSV должен проверяться на: + +- уникальность адресов; +- пересечение многорегистровых значений; +- допустимые типы и права доступа; +- равенство шестнадцатеричного и десятичного адреса; +- попадание адреса в диапазон `0x0000…0xFFFF`. + +До появления автоматического экспортёра нормативным источником адресов +остаётся XLSX, а HTML/Markdown считаются представлением. diff --git a/c/protocan-transport/docs/PROTOCOL.md b/c/protocan-transport/docs/PROTOCOL.md new file mode 100644 index 0000000..5f76f3c --- /dev/null +++ b/c/protocan-transport/docs/PROTOCOL.md @@ -0,0 +1,114 @@ +# ProtoCAN — базовый протокол + +Статус: **Stable с зарезервированным загрузочным расширением** +Версия: **1.0** +Порядок байтов payload: **little-endian**, если явно не указано иное + +## Назначение + +ProtoCAN — прикладной протокол поверх classic CAN 2.0B. Используются только +расширенные 29-битные идентификаторы (`IDE=1`) и payload длиной 0…8 байт. + +## Термины + +| Термин | Значение | +|---|---| +| ПМ | управляющий модуль | +| прибор | адресуемый узел на шине | +| `DeviceType` | тип прибора, 0…7 | +| `DeviceID` | экземпляр прибора данного типа, 0…15 | +| `MsgType` | класс сообщения или сервис | +| `MsgBody` | 16-битное поле, формат которого зависит от `MsgType` | + +Пара `DeviceType/DeviceID` задаёт до `8 × 16 = 128` уникальных адресов. + +## Расширенный CAN ID + +```text +28 27 26...24 23...20 19...16 15........0 +Priority Route DeviceType DeviceID MsgType MsgBody + 1 бит 1 бит 3 бита 4 бита 4 бита 16 бит +``` + +```c +can_id = + ((uint32_t)priority << 28) | + ((uint32_t)route << 27) | + ((uint32_t)device_type << 24) | + ((uint32_t)device_id << 20) | + ((uint32_t)msg_type << 16) | + msg_body; +``` + +| Поле | Значения | Назначение | +|---|---|---| +| `Priority` | `0` critical, `1` standard | CAN-арбитраж | +| `Route` | `0` от ПМ, `1` от прибора | логическое направление | +| `DeviceType` | `0…7` | тип прибора | +| `DeviceID` | `0…15` | номер экземпляра | +| `MsgType` | `0…15` | тип сообщения | +| `MsgBody` | `0…65535` | команда, адрес или номер блока | + +`Route` не является направлением физического трансивера. Ответ прибора +сохраняет адрес `DeviceType/DeviceID` и устанавливает `Route=1`. + +## Реестр `MsgType` + +| Код | Имя | Основное направление | DLC | Статус | +|---:|---|---|---:|---| +| `0x0` | `BROADCAST` | ПМ → все | зависит от команды | stable | +| `0x1` | `DISCRETE` | оба | 0…8 | stable | +| `0x2` | `ANALOG` | оба | 0…8 | stable | +| `0x3` | `GAS` | оба | 0/2/4/6/8 | stable | +| `0x4` | `MODBUS_COIL` | оба | 0…8 | stable | +| `0x5` | `MODBUS_DISCRETE` | оба | 0…8 | stable | +| `0x6` | `MODBUS_HOLDING` | оба | 0…8 | stable | +| `0x7` | `MODBUS_INPUT` | оба | 0…8 | stable | +| `0x8` | `ERROR` | прибор → ПМ | 0 | stable | +| `0x9` | `BOOT_CONTROL` | ПМ → прибор | 0/8 | draft | +| `0xA` | `BOOT_DATA_A` | ПМ → прибор | 8 | draft | +| `0xB` | `BOOT_DATA_B` | ПМ → прибор | 8 | draft | +| `0xC` | `BOOT_STATUS` | прибор → ПМ | 8 | draft | +| `0xD` | `BOOT_DISCOVERY` | прибор → ПМ | 8 | draft | +| `0xE` | `SETTINGS` | оба | 0/1/8 | stable | +| `0xF` | `PULSE` | прибор → сеть | 1 | stable | + +Подробный формат `0x9…0xD` находится в [BOOTLOADER.md](BOOTLOADER.md). + +## Разметки `MsgBody` + +| `MsgType` | Биты `MsgBody` | +|---|---| +| broadcast | команда `[15:4]`, параметр `[3:0]` | +| discrete/analog | подтип `[15:12]`, значение/адрес `[11:0]` | +| Modbus | начальный адрес `[15:4]`, количество `[3:0]` | +| GAS | адрес первого 16-битного регистра `[15:0]` | +| error | дополнительная информация `[15:8]`, код `[7:0]` | +| settings | номер сборки `[15:8]`, позиция `[7:0]` | +| boot control/status | `SessionID[15:8]`, команда `[7:0]` | +| boot data | `BlockIndex[15:0]` | + +## Общие правила обмена + +- Многобайтовые значения в `DATA` передаются little-endian. +- Узел игнорирует адресованные кадры с чужим `DeviceType/DeviceID`. +- Прибор принимает команды ПМ с `Route=0`; ПМ принимает ответы с `Route=1`. +- Стандартные 11-битные CAN ID не являются кадрами ProtoCAN. +- RTR для загрузочного сервиса запрещён. +- Неописанные комбинации `MsgType/MsgBody/DLC` должны отвергаться. + +## Эталон упаковки ID + +```text +Priority = 1 +Route = 0 +DeviceType = 3 +DeviceID = 5 +MsgType = 0x9 +MsgBody = 0x0702 + +CAN ID = 0x13590702 +``` + +Этот пример соответствует `ENTER_BOOT`, `SessionID=7`. Машинные варианты +находятся в [examples/test-vectors.json](examples/test-vectors.json). diff --git a/c/protocan-transport/tests/vectors/test-vectors.json b/c/protocan-transport/tests/vectors/test-vectors.json new file mode 100644 index 0000000..72d6eb7 --- /dev/null +++ b/c/protocan-transport/tests/vectors/test-vectors.json @@ -0,0 +1,45 @@ +{ + "schema_version": 1, + "byte_order": "little-endian", + "frames": [ + { + "name": "enter_boot_session_7", + "direction": "pm_to_device", + "priority": 1, + "route": 0, + "device_type": 3, + "device_id": 5, + "msg_type": 9, + "msg_body": 1794, + "can_id_hex": "0x13590702", + "dlc": 0, + "data_hex": "" + }, + { + "name": "slot_b_block_1", + "direction": "pm_to_device", + "priority": 1, + "route": 0, + "device_type": 3, + "device_id": 5, + "msg_type": 11, + "msg_body": 1, + "can_id_hex": "0x135B0001", + "dlc": 8, + "data_hex": "1011121314151617" + }, + { + "name": "enter_boot_ok", + "direction": "device_to_pm", + "priority": 1, + "route": 1, + "device_type": 3, + "device_id": 5, + "msg_type": 12, + "msg_body": 1794, + "can_id_hex": "0x1B5C0702", + "dlc": 8, + "data_hex": "00FF000000000000" + } + ] +}