184 lines
8.2 KiB
Markdown
184 lines
8.2 KiB
Markdown
# 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`.
|