# 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`.