docs: document CAN bootloader protocol

This commit is contained in:
2026-08-29 21:45:30 +03:00
parent 01de7651e8
commit bd8d22312e
13 changed files with 16060 additions and 4 deletions

View File

@@ -4,6 +4,200 @@
- [Интерфейс протокола](#sheet-protocol)
- [Общее адресное пространство](#sheet-oap)
- [Прошивка приборов по CAN](#can-firmware)
<a id="can-firmware"></a>
## Прошивка приборов по CAN
Ниже зафиксирован формат планируемого загрузочного сервиса. Значения
`MsgType=0x9..0xD` зарезервированы в протоколе, но обработчики загрузчика в
текущих `protocan.c/.h` ещё не реализованы.
Загрузчик работает поверх classic CAN 2.0B с Extended ID. Управляющий модуль
использует `Route=0`, прибор отвечает с `Route=1`. Команды стирания и записи
всегда должны быть адресованы конкретной паре `DeviceType/DeviceID`;
широковещательный режим допустим только для обнаружения.
### Карта загрузочных сообщений
| `MsgType` | Имя | Назначение `MsgBody` | CAN payload |
|---:|---|---|---|
| `0x9` | `BOOT_CONTROL` | `SessionID:8 \| Command:8` | параметры команды |
| `0xA` | `BOOT_DATA_A` | номер 8-байтового блока слота A | 8 байт образа |
| `0xB` | `BOOT_DATA_B` | номер 8-байтового блока слота B | 8 байт образа |
| `0xC` | `BOOT_STATUS` | `SessionID:8 \| Command:8` | статус и прогресс |
| `0xD` | `BOOT_DISCOVERY` | подтип запроса/ответа | идентификация прибора |
`MsgBody` в кадрах данных является не байтовым адресом, а номером блока:
```c
block_offset = (uint32_t)MsgBody * 8U;
if (MsgType == PROTOCAN_MSGTYPE_BOOT_DATA_A) {
address = SLOT_A_BASE + block_offset;
} else if (MsgType == PROTOCAN_MSGTYPE_BOOT_DATA_B) {
address = SLOT_B_BASE + block_offset;
}
```
Диапазон `MsgBody=0x0000..0xFFFF` адресует ровно 65 536 блоков:
```text
512 КиБ = 512 * 1024 = 524 288 байт
524 288 / 8 = 65 536 блоков
```
| `MsgBody` | Номер блока | Смещение от начала слота | Диапазон байтов |
|---:|---:|---:|---:|
| `0x0000` | 0 | `0x00000` | `0x00000..0x00007` |
| `0x0001` | 1 | `0x00008` | `0x00008..0x0000F` |
| `0x0002` | 2 | `0x00010` | `0x00010..0x00017` |
| `0xFFFF` | 65 535 | `0x7FFF8` | `0x7FFF8..0x7FFFF` |
Формула физического адреса блока:
```c
address = SLOT_X_BASE + ((uint32_t)MsgBody * 8U);
```
Последний допустимый блок начинается со смещения `0x7FFF8` и заканчивается
на `0x7FFFF` включительно. Следующее смещение `0x80000` уже находится за
границей 512-КиБ слота.
Таким образом, `BOOT_DATA_A` и `BOOT_DATA_B` адресуют два логических слота
по 512 КиБ, всего 1 МиБ пространства образов. Физические `SLOT_A_BASE` и
`SLOT_B_BASE` задаёт конкретный загрузчик. Если внутренняя Flash имеет ровно
1 МиБ, два полных слота в ней не поместятся вместе с загрузчиком и метаданными:
нужно уменьшить слоты, выбрать MCU с большей Flash или хранить staging-образ
во внешней памяти.
### Кадр данных
```text
Extended CAN ID
Priority = STANDARD
Route = FROM_PM
DeviceType = тип целевого прибора
DeviceID = экземпляр целевого прибора
MsgType = 0xA (слот A) или 0xB (слот B)
MsgBody = BlockIndex, 0x0000..0xFFFF
DATA[0..7] = очередные 8 байт образа
```
Например, `MsgBody=0x0123` задаёт смещение `0x0123 * 8 = 0x0918` от
начала выбранного слота. Последний неполный блок дополняется значениями
`0xFF`; фактический размер передаётся командой `BEGIN_UPDATE`, поэтому CRC32
считается только по байтам образа.
### Управляющие команды
В `BOOT_CONTROL` поле `MsgBody` имеет формат:
```text
15........8 7.........0
SessionID Command
```
Рекомендуемые команды:
| Код | Команда | Назначение |
|---:|---|---|
| `0x01` | `IDENTIFY` | прочитать тип, аппаратную и программную версии |
| `0x02` | `ENTER_BOOT` | перейти из приложения в загрузчик |
| `0x03` | `BEGIN_IMAGE` | передать размер и CRC32 образа |
| `0x04` | `BEGIN_COMPAT` | передать тип, аппаратную и программную версии |
| `0x05` | `ERASE` | подготовить неактивный слот |
| `0x06` | `VERIFY` | проверить размер, CRC32 и подпись |
| `0x07` | `COMMIT` | назначить проверенный слот кандидатом на запуск |
| `0x08` | `CONFIRM` | подтвердить успешный запуск новой программы |
| `0x09` | `REBOOT` | перезагрузить прибор |
| `0x0A` | `ABORT` | отменить текущую сессию |
| `0x0B` | `QUERY_PROGRESS` | запросить слот и следующий ожидаемый блок |
`BEGIN_IMAGE` содержит размер и CRC образа:
```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] минимальная HardwareRevision
DATA[3] максимальная HardwareRevision
DATA[4..7] FirmwareVersion, uint32 little-endian
```
Обе команды передаются с одним `SessionID`. До стирания Flash загрузчик обязан
получить обе части метаданных и проверить `ProductType`, аппаратную ревизию,
размер образа, границы выбранного слота и допустимость версии.
### Ответ состояния
`BOOT_STATUS` возвращает результат команды и точку продолжения:
```text
MsgBody[15..8] = SessionID
MsgBody[7..0] = команда, на которую дан ответ
DATA[0] Status
DATA[1] TargetSlot: 0 = A, 1 = B
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` |
| `0x09` | `SIGNATURE_ERROR` |
| `0x0A` | `SESSION_ERROR` |
| `0x0B` | `VOLTAGE_ERROR` |
`NextBlock` позволяет возобновить загрузку после разрыва связи. Для первой
реализации допустимо подтверждать каждый блок. Для рабочей скорости лучше
передавать окна по 16 кадров и подтверждать окно одним `BOOT_STATUS`; при
необходимости протокол статуса можно расширить битовой картой потерянных
блоков.
### Выбор слота и безопасное обновление
GUI не должен самостоятельно перезаписывать активный слот. После получения
`BEGIN_IMAGE` и `BEGIN_COMPAT` загрузчик выбирает неактивный слот и сообщает его в
`BOOT_STATUS`:
```text
активен A -> принимать BOOT_DATA_B
активен B -> принимать BOOT_DATA_A
```
Рекомендуемый цикл обновления:
1. Обнаружить прибор и сверить `DeviceType/DeviceID`, `ProductType`, UID и версии.
2. Выполнить адресную команду `ENTER_BOOT` и получить новый `SessionID`.
3. Передать `BEGIN_IMAGE` и `BEGIN_COMPAT`; загрузчик выберет неактивный слот.
4. Стереть выбранный слот и передать блоки `BOOT_DATA_A` или `BOOT_DATA_B`.
5. Выполнить `VERIFY`: проверить размер, CRC32 и цифровую подпись образа.
6. Выполнить `COMMIT` и перезагрузить устройство.
7. Новое приложение вызывает `CONFIRM` после успешной самопроверки.
8. При отсутствии подтверждения загрузчик возвращается к предыдущему слоту.
CRC32 обнаруживает случайное повреждение, но не защищает от подмены. Для
серийных изделий образ следует подписывать, а открытый ключ проверки хранить
в неизменяемой части загрузчика. Сам загрузчик не должен обновляться обычными
командами `BOOT_DATA_A/B`.
<style>
@@ -724,12 +918,68 @@
<td class="x50">&nbsp;</td>
<td class="x50">&nbsp;</td>
<td colspan="4" class="x156">&nbsp;</td>
<td colspan="4" class="x149">0x9 – 0xE. Резерв</td>
<td colspan="16" class="x162">0x0 – 0xFFFF. Резерв</td>
<td colspan="4" class="x149">0x9. BOOT_CONTROL</td>
<td colspan="16" class="x162">SessionID[15:8] | Command[7:0]. Управление загрузчиком</td>
<td class="x14">&nbsp;</td>
<td colspan="4" class="x101">NULL</td>
<td colspan="4" class="x101">0/8</td>
<td class="x14">&nbsp;</td>
<td colspan="8" class="x101">NULL</td>
<td colspan="8" class="x101">Параметры команды</td>
</tr>
<tr style="height:17px">
<td class="x26">&nbsp;</td>
<td class="x27">&nbsp;</td>
<td class="x50">&nbsp;</td>
<td class="x50">&nbsp;</td>
<td class="x50">&nbsp;</td>
<td colspan="4" class="x156">&nbsp;</td>
<td colspan="4" class="x149">0xA. BOOT_DATA_A</td>
<td colspan="16" class="x162">MsgBody = BlockIndex 0x0000–0xFFFF: 65 536 блоков по 8 байт, слот A 512 КиБ</td>
<td class="x14">&nbsp;</td>
<td colspan="4" class="x101">8</td>
<td class="x14">&nbsp;</td>
<td colspan="8" class="x101">8 байт прошивки слота A</td>
</tr>
<tr style="height:17px">
<td class="x26">&nbsp;</td>
<td class="x27">&nbsp;</td>
<td class="x50">&nbsp;</td>
<td class="x50">&nbsp;</td>
<td class="x50">&nbsp;</td>
<td colspan="4" class="x156">&nbsp;</td>
<td colspan="4" class="x149">0xB. BOOT_DATA_B</td>
<td colspan="16" class="x162">MsgBody = BlockIndex 0x0000–0xFFFF: 65 536 блоков по 8 байт, слот B 512 КиБ</td>
<td class="x14">&nbsp;</td>
<td colspan="4" class="x101">8</td>
<td class="x14">&nbsp;</td>
<td colspan="8" class="x101">8 байт прошивки слота B</td>
</tr>
<tr style="height:17px">
<td class="x26">&nbsp;</td>
<td class="x27">&nbsp;</td>
<td class="x50">&nbsp;</td>
<td class="x50">&nbsp;</td>
<td class="x50">&nbsp;</td>
<td colspan="4" class="x156">&nbsp;</td>
<td colspan="4" class="x149">0xC. BOOT_STATUS</td>
<td colspan="16" class="x162">SessionID[15:8] | Command[7:0]. Ответ и точка продолжения</td>
<td class="x14">&nbsp;</td>
<td colspan="4" class="x101">8</td>
<td class="x14">&nbsp;</td>
<td colspan="8" class="x101">Status, Slot, NextBlock, RunningCRC32</td>
</tr>
<tr style="height:17px">
<td class="x26">&nbsp;</td>
<td class="x27">&nbsp;</td>
<td class="x50">&nbsp;</td>
<td class="x50">&nbsp;</td>
<td class="x50">&nbsp;</td>
<td colspan="4" class="x156">&nbsp;</td>
<td colspan="4" class="x149">0xD. BOOT_DISCOVERY</td>
<td colspan="16" class="x162">Подтип запроса/ответа идентификации загрузчика</td>
<td class="x14">&nbsp;</td>
<td colspan="4" class="x101">0/8</td>
<td class="x14">&nbsp;</td>
<td colspan="8" class="x101">ProductType, HardwareRevision, BootVersion, FirmwareVersion</td>
</tr>
<tr style="height:17px">
<td class="x26">&nbsp;</td>