docs: document CAN bootloader protocol
This commit is contained in:
198
README.md
198
README.md
@@ -16,6 +16,20 @@ SETCAN/
|
||||
|
||||
Модуль зависит от файлов `main.h` и `can.h`, сгенерированных STM32CubeMX, а также от HAL-драйверов CAN, RTC и TIM.
|
||||
|
||||
## Документация протокола
|
||||
|
||||
Нормативное описание разделено по назначению:
|
||||
|
||||
- [`docs/protocan/PROTOCOL.md`](docs/protocan/PROTOCOL.md) — структура ProtoCAN;
|
||||
- [`docs/protocan/BOOTLOADER.md`](docs/protocan/BOOTLOADER.md) — прошивка по CAN;
|
||||
- [`docs/protocan/OAP.md`](docs/protocan/OAP.md) — правила ведения ОАП;
|
||||
- [`docs/protocan/examples/test-vectors.json`](docs/protocan/examples/test-vectors.json) — машинные эталоны;
|
||||
- [`docs/protocan/build/protocol.html`](docs/protocan/build/protocol.html) — собранная HTML-версия.
|
||||
|
||||
Большой файл `Протокол CAN и ОАП.md` и исходный XLSX сохранены как табличное
|
||||
представление реестра. Правила приоритета источников описаны в
|
||||
[`docs/protocan/README.md`](docs/protocan/README.md).
|
||||
|
||||
## Формат расширенного CAN ID
|
||||
|
||||
Протокол размещает служебные поля в 29-битном Extended ID:
|
||||
@@ -42,6 +56,11 @@ SETCAN/
|
||||
| `0x6` | Modbus Holding |
|
||||
| `0x7` | Modbus Input |
|
||||
| `0x8` | ошибка |
|
||||
| `0x9` | управление загрузчиком (`BOOT_CONTROL`, зарезервировано) |
|
||||
| `0xA` | данные прошивки, слот A (`BOOT_DATA_A`, зарезервировано) |
|
||||
| `0xB` | данные прошивки, слот B (`BOOT_DATA_B`, зарезервировано) |
|
||||
| `0xC` | состояние загрузчика (`BOOT_STATUS`, зарезервировано) |
|
||||
| `0xD` | обнаружение загрузчиков (`BOOT_DISCOVERY`, зарезервировано) |
|
||||
| `0xE` | настройка привязки датчика (`SETTINGS`) |
|
||||
| `0xF` | пульс присутствия устройства |
|
||||
|
||||
@@ -56,6 +75,11 @@ SETCAN/
|
||||
|
||||
Для регистров `uint16_t` в CAN payload используется порядок байтов little-endian: сначала младший байт, затем старший.
|
||||
|
||||
Пара `DeviceType/DeviceID` образует уникальный адрес прибора. Три бита
|
||||
`DeviceType` задают 8 типов, четыре бита `DeviceID` — 16 экземпляров каждого
|
||||
типа; всего на одной шине можно адресовать до 128 приборов. Загрузочные кадры
|
||||
так же адресуются этой парой и не требуют изменения 29-битного CAN ID.
|
||||
|
||||
> Разметка ID реализована C-битовыми полями. Она соответствует используемому STM32 GCC ABI, но не является переносимой между произвольными компиляторами без проверки фактического расположения битов.
|
||||
|
||||
## Подключение к STM32-проекту
|
||||
@@ -348,6 +372,180 @@ Broadcast-команда `PROTOCAN_BROADCAST_RTCSETUP` ожидает ровно
|
||||
|
||||
Перед записью проверяются диапазоны и количество дней с учётом високосного года.
|
||||
|
||||
## Прошивка приборов по 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` адресует 65536 блоков:
|
||||
|
||||
```text
|
||||
65536 блоков * 8 байт = 524288 байт = 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`.
|
||||
|
||||
## Важные ограничения текущей реализации
|
||||
|
||||
- Модуль рассчитан на classic CAN с payload до 8 байт, не на CAN FD.
|
||||
|
||||
183
docs/protocan/BOOTLOADER.md
Normal file
183
docs/protocan/BOOTLOADER.md
Normal file
@@ -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`.
|
||||
21
docs/protocan/CHANGELOG.md
Normal file
21
docs/protocan/CHANGELOG.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# История изменений ProtoCAN
|
||||
|
||||
Формат основан на Keep a Changelog. Версия относится к спецификации, а не к
|
||||
версии прошивки отдельного прибора.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Структурированный комплект документации `docs/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` сохранены.
|
||||
56
docs/protocan/OAP.md
Normal file
56
docs/protocan/OAP.md
Normal file
@@ -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 считаются представлением.
|
||||
114
docs/protocan/PROTOCOL.md
Normal file
114
docs/protocan/PROTOCOL.md
Normal file
@@ -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).
|
||||
42
docs/protocan/README.md
Normal file
42
docs/protocan/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Документация ProtoCAN
|
||||
|
||||
Статус комплекта: **Draft**
|
||||
Версия комплекта: **1.0**
|
||||
Дата редакции: **2026-08-29**
|
||||
Транспорт: **Classic CAN 2.0B, Extended ID, DLC 0…8**
|
||||
|
||||
Этот каталог разделяет нормативное описание протокола, загрузчик и реестр
|
||||
общего адресного пространства. Большой исходный документ
|
||||
[`Протокол CAN и ОАП.md`](../../Протокол%20CAN%20и%20ОАП.md) сохранён как
|
||||
совместимое представление таблиц из Excel.
|
||||
|
||||
## Документы
|
||||
|
||||
| Документ | Назначение | Статус источника |
|
||||
|---|---|---|
|
||||
| [PROTOCOL.md](PROTOCOL.md) | 29-битный CAN ID, адресация, реестр `MsgType`, порядок байтов | нормативный |
|
||||
| [BOOTLOADER.md](BOOTLOADER.md) | обновление прошивки, кадры, состояния, ошибки и A/B-слоты | нормативный draft |
|
||||
| [OAP.md](OAP.md) | правила ведения общего адресного пространства | нормативный индекс |
|
||||
| [../../Протокол CAN и ОАП.xlsx](../../Протокол%20CAN%20и%20ОАП.xlsx) | редактируемый реестр ОАП | источник таблиц |
|
||||
| [examples/test-vectors.json](examples/test-vectors.json) | машинные эталоны CAN ID и payload | нормативные примеры |
|
||||
| [CHANGELOG.md](CHANGELOG.md) | история версий документа | нормативный |
|
||||
|
||||
## Приоритет источников
|
||||
|
||||
При расхождении данных действует следующий порядок:
|
||||
|
||||
1. `PROTOCOL.md` — структура ProtoCAN и реестр типов сообщений.
|
||||
2. `BOOTLOADER.md` — загрузочный сервис `0x9…0xD`.
|
||||
3. XLSX — адреса и свойства регистров ОАП.
|
||||
4. Сгенерированный HTML — только представление, не самостоятельный источник.
|
||||
|
||||
## Сборка HTML
|
||||
|
||||
В PowerShell 7:
|
||||
|
||||
```powershell
|
||||
./build-html.ps1
|
||||
```
|
||||
|
||||
Результат создаётся в `build/protocol.html`. Скрипт не изменяет исходные
|
||||
Markdown/XLSX и пригоден для запуска в CI.
|
||||
19
docs/protocan/build-html.bat
Normal file
19
docs/protocan/build-html.bat
Normal file
@@ -0,0 +1,19 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
|
||||
where pwsh.exe >nul 2>nul
|
||||
if %ERRORLEVEL% EQU 0 (
|
||||
pwsh.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%build-html.ps1" %*
|
||||
) else (
|
||||
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%build-html.ps1" %*
|
||||
)
|
||||
|
||||
set "EXIT_CODE=%ERRORLEVEL%"
|
||||
if not "%EXIT_CODE%"=="0" (
|
||||
echo.
|
||||
echo Ошибка сборки HTML. Код: %EXIT_CODE%
|
||||
)
|
||||
|
||||
exit /b %EXIT_CODE%
|
||||
57
docs/protocan/build-html.ps1
Normal file
57
docs/protocan/build-html.ps1
Normal file
@@ -0,0 +1,57 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$OutputPath = (Join-Path $PSScriptRoot 'build\protocol.html')
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$documents = @(
|
||||
'README.md',
|
||||
'PROTOCOL.md',
|
||||
'BOOTLOADER.md',
|
||||
'OAP.md',
|
||||
'CHANGELOG.md'
|
||||
)
|
||||
|
||||
$sections = foreach ($document in $documents) {
|
||||
$path = Join-Path $PSScriptRoot $document
|
||||
$markdown = Get-Content -Raw -LiteralPath $path
|
||||
(ConvertFrom-Markdown -InputObject $markdown).Html
|
||||
}
|
||||
|
||||
$style = @'
|
||||
body { max-width: 1180px; margin: 32px auto; padding: 0 24px;
|
||||
color: #111827; background: #fff; font: 15px/1.55 Arial, sans-serif; }
|
||||
h1, h2, h3 { line-height: 1.25; }
|
||||
h1 { margin-top: 48px; border-bottom: 2px solid #334155; padding-bottom: 8px; }
|
||||
h2 { margin-top: 32px; }
|
||||
a { color: #1155cc; }
|
||||
code { font-family: Consolas, "Courier New", monospace; }
|
||||
pre { overflow: auto; padding: 12px; border: 1px solid #cbd5e1; background: #f8fafc; }
|
||||
table { width: 100%; border-collapse: collapse; margin: 12px 0 24px; }
|
||||
th, td { border: 1px solid #94a3b8; padding: 6px 9px; vertical-align: top; }
|
||||
th { background: #e2e8f0; }
|
||||
tr:nth-child(even) td { background: #f8fafc; }
|
||||
'@
|
||||
|
||||
$outputDirectory = Split-Path -Parent $OutputPath
|
||||
if (-not (Test-Path -LiteralPath $outputDirectory)) {
|
||||
New-Item -ItemType Directory -Path $outputDirectory | Out-Null
|
||||
}
|
||||
|
||||
$html = @"
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>ProtoCAN — спецификация</title>
|
||||
<style>$style</style>
|
||||
</head>
|
||||
<body>
|
||||
$($sections -join "`n<hr>`n")
|
||||
</body>
|
||||
</html>
|
||||
"@
|
||||
|
||||
Set-Content -LiteralPath $OutputPath -Value $html -Encoding utf8
|
||||
Write-Host "Создан $OutputPath"
|
||||
833
docs/protocan/build/protocol.html
Normal file
833
docs/protocan/build/protocol.html
Normal file
@@ -0,0 +1,833 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>ProtoCAN — спецификация</title>
|
||||
<style>body { max-width: 1180px; margin: 32px auto; padding: 0 24px;
|
||||
color: #111827; background: #fff; font: 15px/1.55 Arial, sans-serif; }
|
||||
h1, h2, h3 { line-height: 1.25; }
|
||||
h1 { margin-top: 48px; border-bottom: 2px solid #334155; padding-bottom: 8px; }
|
||||
h2 { margin-top: 32px; }
|
||||
a { color: #1155cc; }
|
||||
code { font-family: Consolas, "Courier New", monospace; }
|
||||
pre { overflow: auto; padding: 12px; border: 1px solid #cbd5e1; background: #f8fafc; }
|
||||
table { width: 100%; border-collapse: collapse; margin: 12px 0 24px; }
|
||||
th, td { border: 1px solid #94a3b8; padding: 6px 9px; vertical-align: top; }
|
||||
th { background: #e2e8f0; }
|
||||
tr:nth-child(even) td { background: #f8fafc; }</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="protocan">Документация ProtoCAN</h1>
|
||||
<p>Статус комплекта: <strong>Draft</strong><br />
|
||||
Версия комплекта: <strong>1.0</strong><br />
|
||||
Дата редакции: <strong>2026-08-29</strong><br />
|
||||
Транспорт: <strong>Classic CAN 2.0B, Extended ID, DLC 0…8</strong></p>
|
||||
<p>Этот каталог разделяет нормативное описание протокола, загрузчик и реестр
|
||||
общего адресного пространства. Большой исходный документ
|
||||
<a href="../../%D0%9F%D1%80%D0%BE%D1%82%D0%BE%D0%BA%D0%BE%D0%BB%20CAN%20%D0%B8%20%D0%9E%D0%90%D0%9F.md"><code>Протокол CAN и ОАП.md</code></a> сохранён как
|
||||
совместимое представление таблиц из Excel.</p>
|
||||
<h2 id="section">Документы</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Документ</th>
|
||||
<th>Назначение</th>
|
||||
<th>Статус источника</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><a href="PROTOCOL.md">PROTOCOL.md</a></td>
|
||||
<td>29-битный CAN ID, адресация, реестр <code>MsgType</code>, порядок байтов</td>
|
||||
<td>нормативный</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="BOOTLOADER.md">BOOTLOADER.md</a></td>
|
||||
<td>обновление прошивки, кадры, состояния, ошибки и A/B-слоты</td>
|
||||
<td>нормативный draft</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="OAP.md">OAP.md</a></td>
|
||||
<td>правила ведения общего адресного пространства</td>
|
||||
<td>нормативный индекс</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="../../%D0%9F%D1%80%D0%BE%D1%82%D0%BE%D0%BA%D0%BE%D0%BB%20CAN%20%D0%B8%20%D0%9E%D0%90%D0%9F.xlsx">../../Протокол CAN и ОАП.xlsx</a></td>
|
||||
<td>редактируемый реестр ОАП</td>
|
||||
<td>источник таблиц</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="examples/test-vectors.json">examples/test-vectors.json</a></td>
|
||||
<td>машинные эталоны CAN ID и payload</td>
|
||||
<td>нормативные примеры</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="CHANGELOG.md">CHANGELOG.md</a></td>
|
||||
<td>история версий документа</td>
|
||||
<td>нормативный</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2 id="section-1">Приоритет источников</h2>
|
||||
<p>При расхождении данных действует следующий порядок:</p>
|
||||
<ol>
|
||||
<li><code>PROTOCOL.md</code> — структура ProtoCAN и реестр типов сообщений.</li>
|
||||
<li><code>BOOTLOADER.md</code> — загрузочный сервис <code>0x9…0xD</code>.</li>
|
||||
<li>XLSX — адреса и свойства регистров ОАП.</li>
|
||||
<li>Сгенерированный HTML — только представление, не самостоятельный источник.</li>
|
||||
</ol>
|
||||
<h2 id="html">Сборка HTML</h2>
|
||||
<p>В PowerShell 7:</p>
|
||||
<pre><code class="language-powershell">./build-html.ps1
|
||||
</code></pre>
|
||||
<p>Результат создаётся в <code>build/protocol.html</code>. Скрипт не изменяет исходные
|
||||
Markdown/XLSX и пригоден для запуска в CI.</p>
|
||||
|
||||
<hr>
|
||||
<h1 id="protocan">ProtoCAN — базовый протокол</h1>
|
||||
<p>Статус: <strong>Stable с зарезервированным загрузочным расширением</strong><br />
|
||||
Версия: <strong>1.0</strong><br />
|
||||
Порядок байтов payload: <strong>little-endian</strong>, если явно не указано иное</p>
|
||||
<h2 id="section">Назначение</h2>
|
||||
<p>ProtoCAN — прикладной протокол поверх classic CAN 2.0B. Используются только
|
||||
расширенные 29-битные идентификаторы (<code>IDE=1</code>) и payload длиной 0…8 байт.</p>
|
||||
<h2 id="section-1">Термины</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Термин</th>
|
||||
<th>Значение</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>ПМ</td>
|
||||
<td>управляющий модуль</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>прибор</td>
|
||||
<td>адресуемый узел на шине</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>DeviceType</code></td>
|
||||
<td>тип прибора, 0…7</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>DeviceID</code></td>
|
||||
<td>экземпляр прибора данного типа, 0…15</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>MsgType</code></td>
|
||||
<td>класс сообщения или сервис</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>MsgBody</code></td>
|
||||
<td>16-битное поле, формат которого зависит от <code>MsgType</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Пара <code>DeviceType/DeviceID</code> задаёт до <code>8 × 16 = 128</code> уникальных адресов.</p>
|
||||
<h2 id="can-id">Расширенный CAN ID</h2>
|
||||
<pre><code class="language-text">28 27 26...24 23...20 19...16 15........0
|
||||
Priority Route DeviceType DeviceID MsgType MsgBody
|
||||
1 бит 1 бит 3 бита 4 бита 4 бита 16 бит
|
||||
</code></pre>
|
||||
<pre><code class="language-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;
|
||||
</code></pre>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Поле</th>
|
||||
<th>Значения</th>
|
||||
<th>Назначение</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>Priority</code></td>
|
||||
<td><code>0</code> critical, <code>1</code> standard</td>
|
||||
<td>CAN-арбитраж</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Route</code></td>
|
||||
<td><code>0</code> от ПМ, <code>1</code> от прибора</td>
|
||||
<td>логическое направление</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>DeviceType</code></td>
|
||||
<td><code>0…7</code></td>
|
||||
<td>тип прибора</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>DeviceID</code></td>
|
||||
<td><code>0…15</code></td>
|
||||
<td>номер экземпляра</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>MsgType</code></td>
|
||||
<td><code>0…15</code></td>
|
||||
<td>тип сообщения</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>MsgBody</code></td>
|
||||
<td><code>0…65535</code></td>
|
||||
<td>команда, адрес или номер блока</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><code>Route</code> не является направлением физического трансивера. Ответ прибора
|
||||
сохраняет адрес <code>DeviceType/DeviceID</code> и устанавливает <code>Route=1</code>.</p>
|
||||
<h2 id="msgtype">Реестр <code>MsgType</code></h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align: right;">Код</th>
|
||||
<th>Имя</th>
|
||||
<th>Основное направление</th>
|
||||
<th style="text-align: right;">DLC</th>
|
||||
<th>Статус</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x0</code></td>
|
||||
<td><code>BROADCAST</code></td>
|
||||
<td>ПМ → все</td>
|
||||
<td style="text-align: right;">зависит от команды</td>
|
||||
<td>stable</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x1</code></td>
|
||||
<td><code>DISCRETE</code></td>
|
||||
<td>оба</td>
|
||||
<td style="text-align: right;">0…8</td>
|
||||
<td>stable</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x2</code></td>
|
||||
<td><code>ANALOG</code></td>
|
||||
<td>оба</td>
|
||||
<td style="text-align: right;">0…8</td>
|
||||
<td>stable</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x3</code></td>
|
||||
<td><code>GAS</code></td>
|
||||
<td>оба</td>
|
||||
<td style="text-align: right;">0/2/4/6/8</td>
|
||||
<td>stable</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x4</code></td>
|
||||
<td><code>MODBUS_COIL</code></td>
|
||||
<td>оба</td>
|
||||
<td style="text-align: right;">0…8</td>
|
||||
<td>stable</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x5</code></td>
|
||||
<td><code>MODBUS_DISCRETE</code></td>
|
||||
<td>оба</td>
|
||||
<td style="text-align: right;">0…8</td>
|
||||
<td>stable</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x6</code></td>
|
||||
<td><code>MODBUS_HOLDING</code></td>
|
||||
<td>оба</td>
|
||||
<td style="text-align: right;">0…8</td>
|
||||
<td>stable</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x7</code></td>
|
||||
<td><code>MODBUS_INPUT</code></td>
|
||||
<td>оба</td>
|
||||
<td style="text-align: right;">0…8</td>
|
||||
<td>stable</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x8</code></td>
|
||||
<td><code>ERROR</code></td>
|
||||
<td>прибор → ПМ</td>
|
||||
<td style="text-align: right;">0</td>
|
||||
<td>stable</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x9</code></td>
|
||||
<td><code>BOOT_CONTROL</code></td>
|
||||
<td>ПМ → прибор</td>
|
||||
<td style="text-align: right;">0/8</td>
|
||||
<td>draft</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0xA</code></td>
|
||||
<td><code>BOOT_DATA_A</code></td>
|
||||
<td>ПМ → прибор</td>
|
||||
<td style="text-align: right;">8</td>
|
||||
<td>draft</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0xB</code></td>
|
||||
<td><code>BOOT_DATA_B</code></td>
|
||||
<td>ПМ → прибор</td>
|
||||
<td style="text-align: right;">8</td>
|
||||
<td>draft</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0xC</code></td>
|
||||
<td><code>BOOT_STATUS</code></td>
|
||||
<td>прибор → ПМ</td>
|
||||
<td style="text-align: right;">8</td>
|
||||
<td>draft</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0xD</code></td>
|
||||
<td><code>BOOT_DISCOVERY</code></td>
|
||||
<td>прибор → ПМ</td>
|
||||
<td style="text-align: right;">8</td>
|
||||
<td>draft</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0xE</code></td>
|
||||
<td><code>SETTINGS</code></td>
|
||||
<td>оба</td>
|
||||
<td style="text-align: right;">0/1/8</td>
|
||||
<td>stable</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0xF</code></td>
|
||||
<td><code>PULSE</code></td>
|
||||
<td>прибор → сеть</td>
|
||||
<td style="text-align: right;">1</td>
|
||||
<td>stable</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Подробный формат <code>0x9…0xD</code> находится в <a href="BOOTLOADER.md">BOOTLOADER.md</a>.</p>
|
||||
<h2 id="msgbody">Разметки <code>MsgBody</code></h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><code>MsgType</code></th>
|
||||
<th>Биты <code>MsgBody</code></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>broadcast</td>
|
||||
<td>команда <code>[15:4]</code>, параметр <code>[3:0]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>discrete/analog</td>
|
||||
<td>подтип <code>[15:12]</code>, значение/адрес <code>[11:0]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Modbus</td>
|
||||
<td>начальный адрес <code>[15:4]</code>, количество <code>[3:0]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>GAS</td>
|
||||
<td>адрес первого 16-битного регистра <code>[15:0]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>error</td>
|
||||
<td>дополнительная информация <code>[15:8]</code>, код <code>[7:0]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>settings</td>
|
||||
<td>номер сборки <code>[15:8]</code>, позиция <code>[7:0]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>boot control/status</td>
|
||||
<td><code>SessionID[15:8]</code>, команда <code>[7:0]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>boot data</td>
|
||||
<td><code>BlockIndex[15:0]</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2 id="section-2">Общие правила обмена</h2>
|
||||
<ul>
|
||||
<li>Многобайтовые значения в <code>DATA</code> передаются little-endian.</li>
|
||||
<li>Узел игнорирует адресованные кадры с чужим <code>DeviceType/DeviceID</code>.</li>
|
||||
<li>Прибор принимает команды ПМ с <code>Route=0</code>; ПМ принимает ответы с <code>Route=1</code>.</li>
|
||||
<li>Стандартные 11-битные CAN ID не являются кадрами ProtoCAN.</li>
|
||||
<li>RTR для загрузочного сервиса запрещён.</li>
|
||||
<li>Неописанные комбинации <code>MsgType/MsgBody/DLC</code> должны отвергаться.</li>
|
||||
</ul>
|
||||
<h2 id="id">Эталон упаковки ID</h2>
|
||||
<pre><code class="language-text">Priority = 1
|
||||
Route = 0
|
||||
DeviceType = 3
|
||||
DeviceID = 5
|
||||
MsgType = 0x9
|
||||
MsgBody = 0x0702
|
||||
|
||||
CAN ID = 0x13590702
|
||||
</code></pre>
|
||||
<p>Этот пример соответствует <code>ENTER_BOOT</code>, <code>SessionID=7</code>. Машинные варианты
|
||||
находятся в <a href="examples/test-vectors.json">examples/test-vectors.json</a>.</p>
|
||||
|
||||
<hr>
|
||||
<h1 id="protocan-boot-protocol">ProtoCAN Boot Protocol</h1>
|
||||
<p>Статус: <strong>Draft</strong><br />
|
||||
Версия протокола: <strong>1.0</strong><br />
|
||||
Совместимость: <strong>classic CAN 2.0B, Extended ID, DLC 0…8</strong><br />
|
||||
Реализация: <a href="../../../templates/c/protocan-boot"><code>templates/c/protocan-boot</code></a></p>
|
||||
<h2 id="section">Назначение</h2>
|
||||
<p>Сервис обновляет адресованный прибор по CAN и поддерживает два логических
|
||||
слота A/B. Активный слот не стирается: новый образ записывается в неактивный,
|
||||
проверяется и атомарно назначается кандидатом на запуск.</p>
|
||||
<h2 id="section-1">Карта сообщений</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align: right;"><code>MsgType</code></th>
|
||||
<th>Имя</th>
|
||||
<th><code>MsgBody</code></th>
|
||||
<th>Payload</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x9</code></td>
|
||||
<td><code>BOOT_CONTROL</code></td>
|
||||
<td><code>SessionID[15:8] \| Command[7:0]</code></td>
|
||||
<td>параметры команды</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0xA</code></td>
|
||||
<td><code>BOOT_DATA_A</code></td>
|
||||
<td><code>BlockIndex[15:0]</code></td>
|
||||
<td>8 байт слота A</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0xB</code></td>
|
||||
<td><code>BOOT_DATA_B</code></td>
|
||||
<td><code>BlockIndex[15:0]</code></td>
|
||||
<td>8 байт слота B</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0xC</code></td>
|
||||
<td><code>BOOT_STATUS</code></td>
|
||||
<td><code>SessionID[15:8] \| Command[7:0]</code></td>
|
||||
<td>статус и прогресс</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0xD</code></td>
|
||||
<td><code>BOOT_DISCOVERY</code></td>
|
||||
<td>подтип ответа</td>
|
||||
<td>идентификация</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Все команды записи адресуются конкретному <code>DeviceType/DeviceID</code> и имеют
|
||||
<code>Route=0</code>. Ответы сохраняют адрес прибора и имеют <code>Route=1</code>.</p>
|
||||
<h2 id="section-2">Адресация образа</h2>
|
||||
<p><code>MsgBody</code> кадра данных — номер 8-байтового блока:</p>
|
||||
<pre><code class="language-c">offset = (uint32_t)BlockIndex * 8U;
|
||||
address = SLOT_X_BASE + offset;
|
||||
</code></pre>
|
||||
<pre><code class="language-text">512 КиБ = 524 288 байт
|
||||
524 288 / 8 = 65 536 блоков
|
||||
BlockIndex = 0x0000…0xFFFF
|
||||
</code></pre>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align: right;"><code>BlockIndex</code></th>
|
||||
<th style="text-align: right;">Смещение</th>
|
||||
<th style="text-align: right;">Диапазон байтов</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x0000</code></td>
|
||||
<td style="text-align: right;"><code>0x00000</code></td>
|
||||
<td style="text-align: right;"><code>0x00000…0x00007</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x0001</code></td>
|
||||
<td style="text-align: right;"><code>0x00008</code></td>
|
||||
<td style="text-align: right;"><code>0x00008…0x0000F</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0xFFFF</code></td>
|
||||
<td style="text-align: right;"><code>0x7FFF8</code></td>
|
||||
<td style="text-align: right;"><code>0x7FFF8…0x7FFFF</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><code>0x80000</code> является первой позицией за границей слота. Последний кадр
|
||||
дополняется <code>0xFF</code>, но CRC32 вычисляется только по <code>ImageSize</code> байтам.</p>
|
||||
<h2 id="boot_control">Команды <code>BOOT_CONTROL</code></h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align: right;">Код</th>
|
||||
<th>Команда</th>
|
||||
<th style="text-align: right;">DLC</th>
|
||||
<th>Payload</th>
|
||||
<th>Допустимое состояние</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x01</code></td>
|
||||
<td><code>IDENTIFY</code></td>
|
||||
<td style="text-align: right;">0</td>
|
||||
<td>отсутствует</td>
|
||||
<td>любое</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x02</code></td>
|
||||
<td><code>ENTER_BOOT</code></td>
|
||||
<td style="text-align: right;">0</td>
|
||||
<td>отсутствует</td>
|
||||
<td>любое; <code>SessionID != 0</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x03</code></td>
|
||||
<td><code>BEGIN_IMAGE</code></td>
|
||||
<td style="text-align: right;">8</td>
|
||||
<td>размер и CRC32</td>
|
||||
<td>metadata</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x04</code></td>
|
||||
<td><code>BEGIN_COMPAT</code></td>
|
||||
<td style="text-align: right;">8</td>
|
||||
<td>совместимость и версия</td>
|
||||
<td>metadata</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x05</code></td>
|
||||
<td><code>ERASE</code></td>
|
||||
<td style="text-align: right;">0</td>
|
||||
<td>отсутствует</td>
|
||||
<td>ready-to-erase</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x06</code></td>
|
||||
<td><code>VERIFY</code></td>
|
||||
<td style="text-align: right;">0</td>
|
||||
<td>отсутствует</td>
|
||||
<td>образ получен</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x07</code></td>
|
||||
<td><code>COMMIT</code></td>
|
||||
<td style="text-align: right;">0</td>
|
||||
<td>отсутствует</td>
|
||||
<td>verified</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x08</code></td>
|
||||
<td><code>CONFIRM</code></td>
|
||||
<td style="text-align: right;">0</td>
|
||||
<td>отсутствует</td>
|
||||
<td>запущенное приложение</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x09</code></td>
|
||||
<td><code>REBOOT</code></td>
|
||||
<td style="text-align: right;">0</td>
|
||||
<td>отсутствует</td>
|
||||
<td>активная сессия</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x0A</code></td>
|
||||
<td><code>ABORT</code></td>
|
||||
<td style="text-align: right;">0</td>
|
||||
<td>отсутствует</td>
|
||||
<td>активная сессия</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x0B</code></td>
|
||||
<td><code>QUERY_PROGRESS</code></td>
|
||||
<td style="text-align: right;">0</td>
|
||||
<td>отсутствует</td>
|
||||
<td>активная сессия</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="begin_image"><code>BEGIN_IMAGE</code></h3>
|
||||
<pre><code class="language-text">DATA[0..3] ImageSize, uint32 little-endian
|
||||
DATA[4..7] ImageCRC32, uint32 little-endian
|
||||
</code></pre>
|
||||
<h3 id="begin_compat"><code>BEGIN_COMPAT</code></h3>
|
||||
<pre><code class="language-text">DATA[0..1] ProductType, uint16 little-endian
|
||||
DATA[2] HardwareRevisionMin
|
||||
DATA[3] HardwareRevisionMax
|
||||
DATA[4..7] FirmwareVersion, uint32 little-endian
|
||||
</code></pre>
|
||||
<p>До <code>ERASE</code> прибор обязан получить обе части метаданных и проверить размер,
|
||||
тип изделия, аппаратную ревизию, версию и политику anti-rollback.</p>
|
||||
<h2 id="boot_status"><code>BOOT_STATUS</code></h2>
|
||||
<pre><code class="language-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
|
||||
</code></pre>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align: right;">Код</th>
|
||||
<th>Статус</th>
|
||||
<th>Повтор допустим</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x00</code></td>
|
||||
<td><code>OK</code></td>
|
||||
<td>—</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x01</code></td>
|
||||
<td><code>BUSY</code></td>
|
||||
<td>да, после задержки</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x02</code></td>
|
||||
<td><code>INVALID_COMMAND</code></td>
|
||||
<td>после исправления</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x03</code></td>
|
||||
<td><code>WRONG_DEVICE</code></td>
|
||||
<td>нет для этого образа</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x04</code></td>
|
||||
<td><code>WRONG_HARDWARE</code></td>
|
||||
<td>нет для этого образа</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x05</code></td>
|
||||
<td><code>INVALID_SIZE</code></td>
|
||||
<td>нет для этого образа</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x06</code></td>
|
||||
<td><code>CRC_ERROR</code></td>
|
||||
<td>новая передача</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x07</code></td>
|
||||
<td><code>FLASH_ERROR</code></td>
|
||||
<td>зависит от платформы</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x08</code></td>
|
||||
<td><code>SEQUENCE_ERROR</code></td>
|
||||
<td>да, с <code>NextBlock</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x09</code></td>
|
||||
<td><code>SIGNATURE_ERROR</code></td>
|
||||
<td>нет</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x0A</code></td>
|
||||
<td><code>SESSION_ERROR</code></td>
|
||||
<td>открыть новую сессию</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x0B</code></td>
|
||||
<td><code>VOLTAGE_ERROR</code></td>
|
||||
<td>да после нормализации питания</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x0C</code></td>
|
||||
<td><code>INVALID_STATE</code></td>
|
||||
<td>выполнить правильный переход</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2 id="state-machine">State machine</h2>
|
||||
<pre><code class="language-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
|
||||
</code></pre>
|
||||
<p>Ошибка Flash, CRC, совместимости или подписи переводит сессию в <code>FAILED</code>.
|
||||
Новая <code>ENTER_BOOT</code> создаёт чистую сессию. <code>ABORT</code> прекращает текущую передачу,
|
||||
не активируя частично записанный слот.</p>
|
||||
<h2 id="section-3">Надёжность и повторы</h2>
|
||||
<ul>
|
||||
<li>Блоки передаются строго по возрастанию <code>BlockIndex</code>.</li>
|
||||
<li>Дубликат или пропуск возвращает <code>SEQUENCE_ERROR</code> и ожидаемый <code>NextBlock</code>.</li>
|
||||
<li>Базовый режим подтверждает каждый блок.</li>
|
||||
<li>Рабочий режим может подтверждать окно из 16 блоков.</li>
|
||||
<li>После потери связи <code>QUERY_PROGRESS</code> возвращает следующий ожидаемый блок,
|
||||
пока состояние загрузчика сохранено.</li>
|
||||
<li>Для продолжения после перезагрузки порт должен сохранять session metadata
|
||||
и восстановить её при инициализации; ядро версии 1.0 само это не делает.</li>
|
||||
</ul>
|
||||
<h2 id="ab">Безопасность и A/B-обновление</h2>
|
||||
<pre><code class="language-text">active=A -> target=B -> verify -> pending=B
|
||||
active=B -> target=A -> verify -> pending=A
|
||||
</code></pre>
|
||||
<p>CRC32 защищает только от случайного повреждения. Серийный загрузчик должен
|
||||
дополнительно проверить подпись контейнера, границы вектора, совместимость и
|
||||
anti-rollback. Bootloader не обновляется командами <code>BOOT_DATA_A/B</code>.</p>
|
||||
<p>Boot metadata должна атомарно хранить:</p>
|
||||
<ul>
|
||||
<li>активный слот;</li>
|
||||
<li>pending-слот;</li>
|
||||
<li>подтверждение запуска;</li>
|
||||
<li>число неудачных попыток;</li>
|
||||
<li>версию и CRC32 образа.</li>
|
||||
</ul>
|
||||
<p>Если приложение не выполняет <code>CONFIRM</code> за установленное число запусков,
|
||||
загрузчик возвращается к предыдущему подтверждённому слоту.</p>
|
||||
<h2 id="section-4">Эталонный сценарий</h2>
|
||||
<ol>
|
||||
<li>ПМ адресно отправляет <code>IDENTIFY</code>.</li>
|
||||
<li>ПМ открывает ненулевой <code>SessionID</code> командой <code>ENTER_BOOT</code>.</li>
|
||||
<li>ПМ отправляет <code>BEGIN_IMAGE</code> и <code>BEGIN_COMPAT</code>.</li>
|
||||
<li>Прибор сообщает выбранный неактивный слот.</li>
|
||||
<li>ПМ выполняет <code>ERASE</code> и передаёт <code>BOOT_DATA_A</code> либо <code>BOOT_DATA_B</code>.</li>
|
||||
<li>ПМ выполняет <code>VERIFY</code>, затем <code>COMMIT</code> и <code>REBOOT</code>.</li>
|
||||
<li>Новое приложение после самопроверки выполняет <code>CONFIRM</code>.</li>
|
||||
</ol>
|
||||
|
||||
<hr>
|
||||
<h1 id="section">Общее адресное пространство</h1>
|
||||
<p>Статус: <strong>Stable, данные ведутся в XLSX</strong><br />
|
||||
Порядок значений: <strong>16-битные регистры, little-endian в CAN payload</strong></p>
|
||||
<p>Редактируемый источник реестра:
|
||||
<a href="../../%D0%9F%D1%80%D0%BE%D1%82%D0%BE%D0%BA%D0%BE%D0%BB%20CAN%20%D0%B8%20%D0%9E%D0%90%D0%9F.xlsx"><code>Протокол CAN и ОАП.xlsx</code></a>.</p>
|
||||
<p>Просматриваемая большая таблица находится в
|
||||
<a href="../../%D0%9F%D1%80%D0%BE%D1%82%D0%BE%D0%BA%D0%BE%D0%BB%20CAN%20%D0%B8%20%D0%9E%D0%90%D0%9F.html"><code>Протокол CAN и ОАП.html</code></a> и
|
||||
<a href="../../%D0%9F%D1%80%D0%BE%D1%82%D0%BE%D0%BA%D0%BE%D0%BB%20CAN%20%D0%B8%20%D0%9E%D0%90%D0%9F.md"><code>Протокол CAN и ОАП.md</code></a>.</p>
|
||||
<h2 id="section-1">Назначение</h2>
|
||||
<p>ОАП связывает 16-битный адрес регистра с его типом, назначением, доступом и
|
||||
масштабом. В ProtoCAN используется <code>MsgType=0x3</code>, а <code>MsgBody</code> содержит адрес
|
||||
первого регистра.</p>
|
||||
<h2 id="section-2">Обязательные поля реестра</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Поле</th>
|
||||
<th>Требование</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>AddressHex</td>
|
||||
<td><code>0x0000…0xFFFF</code>, уникальное значение</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>AddressDec</td>
|
||||
<td>десятичный эквивалент AddressHex</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Group</td>
|
||||
<td>функциональная группа</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>однозначное имя параметра</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td><code>u16</code>, <code>i16</code>, <code>u32</code>, <code>i32</code>, <code>float32</code>, bitmap или массив</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Registers</td>
|
||||
<td>число занятых 16-битных регистров</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Access</td>
|
||||
<td><code>R</code>, <code>W</code> или <code>RW</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Unit</td>
|
||||
<td>физическая единица либо <code>—</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Scale</td>
|
||||
<td>множитель/делитель представления</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default</td>
|
||||
<td>значение после сброса, если применимо</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>семантика, диапазон и особые значения</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2 id="section-3">Правила ведения</h2>
|
||||
<ul>
|
||||
<li>Адрес не переиспользуется с другим смыслом после выпуска стабильной версии.</li>
|
||||
<li>Многорегистровое значение занимает непрерывный диапазон.</li>
|
||||
<li>Порядок 16-битных слов для 32-битного значения фиксируется в строке типа.</li>
|
||||
<li>Резервные диапазоны явно отмечаются и не используются без изменения версии.</li>
|
||||
<li>Удалённый параметр помечается deprecated, а не исчезает молча.</li>
|
||||
<li>Изменение адреса, типа или масштаба отражается в <code>CHANGELOG.md</code>.</li>
|
||||
</ul>
|
||||
<h2 id="section-4">Экспорт</h2>
|
||||
<p>Для программной генерации каталог следует экспортировать из XLSX в CSV с
|
||||
UTF-8 и фиксированными английскими именами колонок. CSV должен проверяться на:</p>
|
||||
<ul>
|
||||
<li>уникальность адресов;</li>
|
||||
<li>пересечение многорегистровых значений;</li>
|
||||
<li>допустимые типы и права доступа;</li>
|
||||
<li>равенство шестнадцатеричного и десятичного адреса;</li>
|
||||
<li>попадание адреса в диапазон <code>0x0000…0xFFFF</code>.</li>
|
||||
</ul>
|
||||
<p>До появления автоматического экспортёра нормативным источником адресов
|
||||
остаётся XLSX, а HTML/Markdown считаются представлением.</p>
|
||||
|
||||
<hr>
|
||||
<h1 id="protocan">История изменений ProtoCAN</h1>
|
||||
<p>Формат основан на Keep a Changelog. Версия относится к спецификации, а не к
|
||||
версии прошивки отдельного прибора.</p>
|
||||
<h2 id="unreleased">[Unreleased]</h2>
|
||||
<h3 id="added">Added</h3>
|
||||
<ul>
|
||||
<li>Структурированный комплект документации <code>docs/protocan</code>.</li>
|
||||
<li>Загрузочный сервис <code>MsgType=0x9…0xD</code>.</li>
|
||||
<li>Два логических слота по 512 КиБ, 65 536 блоков по 8 байт каждый.</li>
|
||||
<li>Машинные эталоны CAN ID в <code>examples/test-vectors.json</code>.</li>
|
||||
</ul>
|
||||
<h2 id="section">[1.0] — 2026-08-29</h2>
|
||||
<h3 id="added-1">Added</h3>
|
||||
<ul>
|
||||
<li>Зафиксирована 29-битная структура ProtoCAN ID.</li>
|
||||
<li>Зафиксирована адресация 8 типов по 16 экземпляров.</li>
|
||||
<li>Существующие сообщения <code>0x0…0x8</code>, <code>0xE</code>, <code>0xF</code> сохранены.</li>
|
||||
</ul>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
45
docs/protocan/examples/test-vectors.json
Normal file
45
docs/protocan/examples/test-vectors.json
Normal file
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
833
docs/protocan/protocol.html
Normal file
833
docs/protocan/protocol.html
Normal file
@@ -0,0 +1,833 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>ProtoCAN — спецификация</title>
|
||||
<style>body { max-width: 1180px; margin: 32px auto; padding: 0 24px;
|
||||
color: #111827; background: #fff; font: 15px/1.55 Arial, sans-serif; }
|
||||
h1, h2, h3 { line-height: 1.25; }
|
||||
h1 { margin-top: 48px; border-bottom: 2px solid #334155; padding-bottom: 8px; }
|
||||
h2 { margin-top: 32px; }
|
||||
a { color: #1155cc; }
|
||||
code { font-family: Consolas, "Courier New", monospace; }
|
||||
pre { overflow: auto; padding: 12px; border: 1px solid #cbd5e1; background: #f8fafc; }
|
||||
table { width: 100%; border-collapse: collapse; margin: 12px 0 24px; }
|
||||
th, td { border: 1px solid #94a3b8; padding: 6px 9px; vertical-align: top; }
|
||||
th { background: #e2e8f0; }
|
||||
tr:nth-child(even) td { background: #f8fafc; }</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="protocan">Документация ProtoCAN</h1>
|
||||
<p>Статус комплекта: <strong>Draft</strong><br />
|
||||
Версия комплекта: <strong>1.0</strong><br />
|
||||
Дата редакции: <strong>2026-08-29</strong><br />
|
||||
Транспорт: <strong>Classic CAN 2.0B, Extended ID, DLC 0…8</strong></p>
|
||||
<p>Этот каталог разделяет нормативное описание протокола, загрузчик и реестр
|
||||
общего адресного пространства. Большой исходный документ
|
||||
<a href="../../%D0%9F%D1%80%D0%BE%D1%82%D0%BE%D0%BA%D0%BE%D0%BB%20CAN%20%D0%B8%20%D0%9E%D0%90%D0%9F.md"><code>Протокол CAN и ОАП.md</code></a> сохранён как
|
||||
совместимое представление таблиц из Excel.</p>
|
||||
<h2 id="section">Документы</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Документ</th>
|
||||
<th>Назначение</th>
|
||||
<th>Статус источника</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><a href="PROTOCOL.md">PROTOCOL.md</a></td>
|
||||
<td>29-битный CAN ID, адресация, реестр <code>MsgType</code>, порядок байтов</td>
|
||||
<td>нормативный</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="BOOTLOADER.md">BOOTLOADER.md</a></td>
|
||||
<td>обновление прошивки, кадры, состояния, ошибки и A/B-слоты</td>
|
||||
<td>нормативный draft</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="OAP.md">OAP.md</a></td>
|
||||
<td>правила ведения общего адресного пространства</td>
|
||||
<td>нормативный индекс</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="../../%D0%9F%D1%80%D0%BE%D1%82%D0%BE%D0%BA%D0%BE%D0%BB%20CAN%20%D0%B8%20%D0%9E%D0%90%D0%9F.xlsx">../../Протокол CAN и ОАП.xlsx</a></td>
|
||||
<td>редактируемый реестр ОАП</td>
|
||||
<td>источник таблиц</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="examples/test-vectors.json">examples/test-vectors.json</a></td>
|
||||
<td>машинные эталоны CAN ID и payload</td>
|
||||
<td>нормативные примеры</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="CHANGELOG.md">CHANGELOG.md</a></td>
|
||||
<td>история версий документа</td>
|
||||
<td>нормативный</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2 id="section-1">Приоритет источников</h2>
|
||||
<p>При расхождении данных действует следующий порядок:</p>
|
||||
<ol>
|
||||
<li><code>PROTOCOL.md</code> — структура ProtoCAN и реестр типов сообщений.</li>
|
||||
<li><code>BOOTLOADER.md</code> — загрузочный сервис <code>0x9…0xD</code>.</li>
|
||||
<li>XLSX — адреса и свойства регистров ОАП.</li>
|
||||
<li>Сгенерированный HTML — только представление, не самостоятельный источник.</li>
|
||||
</ol>
|
||||
<h2 id="html">Сборка HTML</h2>
|
||||
<p>В PowerShell 7:</p>
|
||||
<pre><code class="language-powershell">./build-html.ps1
|
||||
</code></pre>
|
||||
<p>Результат создаётся в <code>build/protocol.html</code>. Скрипт не изменяет исходные
|
||||
Markdown/XLSX и пригоден для запуска в CI.</p>
|
||||
|
||||
<hr>
|
||||
<h1 id="protocan">ProtoCAN — базовый протокол</h1>
|
||||
<p>Статус: <strong>Stable с зарезервированным загрузочным расширением</strong><br />
|
||||
Версия: <strong>1.0</strong><br />
|
||||
Порядок байтов payload: <strong>little-endian</strong>, если явно не указано иное</p>
|
||||
<h2 id="section">Назначение</h2>
|
||||
<p>ProtoCAN — прикладной протокол поверх classic CAN 2.0B. Используются только
|
||||
расширенные 29-битные идентификаторы (<code>IDE=1</code>) и payload длиной 0…8 байт.</p>
|
||||
<h2 id="section-1">Термины</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Термин</th>
|
||||
<th>Значение</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>ПМ</td>
|
||||
<td>управляющий модуль</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>прибор</td>
|
||||
<td>адресуемый узел на шине</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>DeviceType</code></td>
|
||||
<td>тип прибора, 0…7</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>DeviceID</code></td>
|
||||
<td>экземпляр прибора данного типа, 0…15</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>MsgType</code></td>
|
||||
<td>класс сообщения или сервис</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>MsgBody</code></td>
|
||||
<td>16-битное поле, формат которого зависит от <code>MsgType</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Пара <code>DeviceType/DeviceID</code> задаёт до <code>8 × 16 = 128</code> уникальных адресов.</p>
|
||||
<h2 id="can-id">Расширенный CAN ID</h2>
|
||||
<pre><code class="language-text">28 27 26...24 23...20 19...16 15........0
|
||||
Priority Route DeviceType DeviceID MsgType MsgBody
|
||||
1 бит 1 бит 3 бита 4 бита 4 бита 16 бит
|
||||
</code></pre>
|
||||
<pre><code class="language-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;
|
||||
</code></pre>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Поле</th>
|
||||
<th>Значения</th>
|
||||
<th>Назначение</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>Priority</code></td>
|
||||
<td><code>0</code> critical, <code>1</code> standard</td>
|
||||
<td>CAN-арбитраж</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>Route</code></td>
|
||||
<td><code>0</code> от ПМ, <code>1</code> от прибора</td>
|
||||
<td>логическое направление</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>DeviceType</code></td>
|
||||
<td><code>0…7</code></td>
|
||||
<td>тип прибора</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>DeviceID</code></td>
|
||||
<td><code>0…15</code></td>
|
||||
<td>номер экземпляра</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>MsgType</code></td>
|
||||
<td><code>0…15</code></td>
|
||||
<td>тип сообщения</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>MsgBody</code></td>
|
||||
<td><code>0…65535</code></td>
|
||||
<td>команда, адрес или номер блока</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><code>Route</code> не является направлением физического трансивера. Ответ прибора
|
||||
сохраняет адрес <code>DeviceType/DeviceID</code> и устанавливает <code>Route=1</code>.</p>
|
||||
<h2 id="msgtype">Реестр <code>MsgType</code></h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align: right;">Код</th>
|
||||
<th>Имя</th>
|
||||
<th>Основное направление</th>
|
||||
<th style="text-align: right;">DLC</th>
|
||||
<th>Статус</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x0</code></td>
|
||||
<td><code>BROADCAST</code></td>
|
||||
<td>ПМ → все</td>
|
||||
<td style="text-align: right;">зависит от команды</td>
|
||||
<td>stable</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x1</code></td>
|
||||
<td><code>DISCRETE</code></td>
|
||||
<td>оба</td>
|
||||
<td style="text-align: right;">0…8</td>
|
||||
<td>stable</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x2</code></td>
|
||||
<td><code>ANALOG</code></td>
|
||||
<td>оба</td>
|
||||
<td style="text-align: right;">0…8</td>
|
||||
<td>stable</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x3</code></td>
|
||||
<td><code>GAS</code></td>
|
||||
<td>оба</td>
|
||||
<td style="text-align: right;">0/2/4/6/8</td>
|
||||
<td>stable</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x4</code></td>
|
||||
<td><code>MODBUS_COIL</code></td>
|
||||
<td>оба</td>
|
||||
<td style="text-align: right;">0…8</td>
|
||||
<td>stable</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x5</code></td>
|
||||
<td><code>MODBUS_DISCRETE</code></td>
|
||||
<td>оба</td>
|
||||
<td style="text-align: right;">0…8</td>
|
||||
<td>stable</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x6</code></td>
|
||||
<td><code>MODBUS_HOLDING</code></td>
|
||||
<td>оба</td>
|
||||
<td style="text-align: right;">0…8</td>
|
||||
<td>stable</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x7</code></td>
|
||||
<td><code>MODBUS_INPUT</code></td>
|
||||
<td>оба</td>
|
||||
<td style="text-align: right;">0…8</td>
|
||||
<td>stable</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x8</code></td>
|
||||
<td><code>ERROR</code></td>
|
||||
<td>прибор → ПМ</td>
|
||||
<td style="text-align: right;">0</td>
|
||||
<td>stable</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x9</code></td>
|
||||
<td><code>BOOT_CONTROL</code></td>
|
||||
<td>ПМ → прибор</td>
|
||||
<td style="text-align: right;">0/8</td>
|
||||
<td>draft</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0xA</code></td>
|
||||
<td><code>BOOT_DATA_A</code></td>
|
||||
<td>ПМ → прибор</td>
|
||||
<td style="text-align: right;">8</td>
|
||||
<td>draft</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0xB</code></td>
|
||||
<td><code>BOOT_DATA_B</code></td>
|
||||
<td>ПМ → прибор</td>
|
||||
<td style="text-align: right;">8</td>
|
||||
<td>draft</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0xC</code></td>
|
||||
<td><code>BOOT_STATUS</code></td>
|
||||
<td>прибор → ПМ</td>
|
||||
<td style="text-align: right;">8</td>
|
||||
<td>draft</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0xD</code></td>
|
||||
<td><code>BOOT_DISCOVERY</code></td>
|
||||
<td>прибор → ПМ</td>
|
||||
<td style="text-align: right;">8</td>
|
||||
<td>draft</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0xE</code></td>
|
||||
<td><code>SETTINGS</code></td>
|
||||
<td>оба</td>
|
||||
<td style="text-align: right;">0/1/8</td>
|
||||
<td>stable</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0xF</code></td>
|
||||
<td><code>PULSE</code></td>
|
||||
<td>прибор → сеть</td>
|
||||
<td style="text-align: right;">1</td>
|
||||
<td>stable</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Подробный формат <code>0x9…0xD</code> находится в <a href="BOOTLOADER.md">BOOTLOADER.md</a>.</p>
|
||||
<h2 id="msgbody">Разметки <code>MsgBody</code></h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><code>MsgType</code></th>
|
||||
<th>Биты <code>MsgBody</code></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>broadcast</td>
|
||||
<td>команда <code>[15:4]</code>, параметр <code>[3:0]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>discrete/analog</td>
|
||||
<td>подтип <code>[15:12]</code>, значение/адрес <code>[11:0]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Modbus</td>
|
||||
<td>начальный адрес <code>[15:4]</code>, количество <code>[3:0]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>GAS</td>
|
||||
<td>адрес первого 16-битного регистра <code>[15:0]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>error</td>
|
||||
<td>дополнительная информация <code>[15:8]</code>, код <code>[7:0]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>settings</td>
|
||||
<td>номер сборки <code>[15:8]</code>, позиция <code>[7:0]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>boot control/status</td>
|
||||
<td><code>SessionID[15:8]</code>, команда <code>[7:0]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>boot data</td>
|
||||
<td><code>BlockIndex[15:0]</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2 id="section-2">Общие правила обмена</h2>
|
||||
<ul>
|
||||
<li>Многобайтовые значения в <code>DATA</code> передаются little-endian.</li>
|
||||
<li>Узел игнорирует адресованные кадры с чужим <code>DeviceType/DeviceID</code>.</li>
|
||||
<li>Прибор принимает команды ПМ с <code>Route=0</code>; ПМ принимает ответы с <code>Route=1</code>.</li>
|
||||
<li>Стандартные 11-битные CAN ID не являются кадрами ProtoCAN.</li>
|
||||
<li>RTR для загрузочного сервиса запрещён.</li>
|
||||
<li>Неописанные комбинации <code>MsgType/MsgBody/DLC</code> должны отвергаться.</li>
|
||||
</ul>
|
||||
<h2 id="id">Эталон упаковки ID</h2>
|
||||
<pre><code class="language-text">Priority = 1
|
||||
Route = 0
|
||||
DeviceType = 3
|
||||
DeviceID = 5
|
||||
MsgType = 0x9
|
||||
MsgBody = 0x0702
|
||||
|
||||
CAN ID = 0x13590702
|
||||
</code></pre>
|
||||
<p>Этот пример соответствует <code>ENTER_BOOT</code>, <code>SessionID=7</code>. Машинные варианты
|
||||
находятся в <a href="examples/test-vectors.json">examples/test-vectors.json</a>.</p>
|
||||
|
||||
<hr>
|
||||
<h1 id="protocan-boot-protocol">ProtoCAN Boot Protocol</h1>
|
||||
<p>Статус: <strong>Draft</strong><br />
|
||||
Версия протокола: <strong>1.0</strong><br />
|
||||
Совместимость: <strong>classic CAN 2.0B, Extended ID, DLC 0…8</strong><br />
|
||||
Реализация: <a href="../../../templates/c/protocan-boot"><code>templates/c/protocan-boot</code></a></p>
|
||||
<h2 id="section">Назначение</h2>
|
||||
<p>Сервис обновляет адресованный прибор по CAN и поддерживает два логических
|
||||
слота A/B. Активный слот не стирается: новый образ записывается в неактивный,
|
||||
проверяется и атомарно назначается кандидатом на запуск.</p>
|
||||
<h2 id="section-1">Карта сообщений</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align: right;"><code>MsgType</code></th>
|
||||
<th>Имя</th>
|
||||
<th><code>MsgBody</code></th>
|
||||
<th>Payload</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x9</code></td>
|
||||
<td><code>BOOT_CONTROL</code></td>
|
||||
<td><code>SessionID[15:8] \| Command[7:0]</code></td>
|
||||
<td>параметры команды</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0xA</code></td>
|
||||
<td><code>BOOT_DATA_A</code></td>
|
||||
<td><code>BlockIndex[15:0]</code></td>
|
||||
<td>8 байт слота A</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0xB</code></td>
|
||||
<td><code>BOOT_DATA_B</code></td>
|
||||
<td><code>BlockIndex[15:0]</code></td>
|
||||
<td>8 байт слота B</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0xC</code></td>
|
||||
<td><code>BOOT_STATUS</code></td>
|
||||
<td><code>SessionID[15:8] \| Command[7:0]</code></td>
|
||||
<td>статус и прогресс</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0xD</code></td>
|
||||
<td><code>BOOT_DISCOVERY</code></td>
|
||||
<td>подтип ответа</td>
|
||||
<td>идентификация</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Все команды записи адресуются конкретному <code>DeviceType/DeviceID</code> и имеют
|
||||
<code>Route=0</code>. Ответы сохраняют адрес прибора и имеют <code>Route=1</code>.</p>
|
||||
<h2 id="section-2">Адресация образа</h2>
|
||||
<p><code>MsgBody</code> кадра данных — номер 8-байтового блока:</p>
|
||||
<pre><code class="language-c">offset = (uint32_t)BlockIndex * 8U;
|
||||
address = SLOT_X_BASE + offset;
|
||||
</code></pre>
|
||||
<pre><code class="language-text">512 КиБ = 524 288 байт
|
||||
524 288 / 8 = 65 536 блоков
|
||||
BlockIndex = 0x0000…0xFFFF
|
||||
</code></pre>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align: right;"><code>BlockIndex</code></th>
|
||||
<th style="text-align: right;">Смещение</th>
|
||||
<th style="text-align: right;">Диапазон байтов</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x0000</code></td>
|
||||
<td style="text-align: right;"><code>0x00000</code></td>
|
||||
<td style="text-align: right;"><code>0x00000…0x00007</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x0001</code></td>
|
||||
<td style="text-align: right;"><code>0x00008</code></td>
|
||||
<td style="text-align: right;"><code>0x00008…0x0000F</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0xFFFF</code></td>
|
||||
<td style="text-align: right;"><code>0x7FFF8</code></td>
|
||||
<td style="text-align: right;"><code>0x7FFF8…0x7FFFF</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><code>0x80000</code> является первой позицией за границей слота. Последний кадр
|
||||
дополняется <code>0xFF</code>, но CRC32 вычисляется только по <code>ImageSize</code> байтам.</p>
|
||||
<h2 id="boot_control">Команды <code>BOOT_CONTROL</code></h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align: right;">Код</th>
|
||||
<th>Команда</th>
|
||||
<th style="text-align: right;">DLC</th>
|
||||
<th>Payload</th>
|
||||
<th>Допустимое состояние</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x01</code></td>
|
||||
<td><code>IDENTIFY</code></td>
|
||||
<td style="text-align: right;">0</td>
|
||||
<td>отсутствует</td>
|
||||
<td>любое</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x02</code></td>
|
||||
<td><code>ENTER_BOOT</code></td>
|
||||
<td style="text-align: right;">0</td>
|
||||
<td>отсутствует</td>
|
||||
<td>любое; <code>SessionID != 0</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x03</code></td>
|
||||
<td><code>BEGIN_IMAGE</code></td>
|
||||
<td style="text-align: right;">8</td>
|
||||
<td>размер и CRC32</td>
|
||||
<td>metadata</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x04</code></td>
|
||||
<td><code>BEGIN_COMPAT</code></td>
|
||||
<td style="text-align: right;">8</td>
|
||||
<td>совместимость и версия</td>
|
||||
<td>metadata</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x05</code></td>
|
||||
<td><code>ERASE</code></td>
|
||||
<td style="text-align: right;">0</td>
|
||||
<td>отсутствует</td>
|
||||
<td>ready-to-erase</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x06</code></td>
|
||||
<td><code>VERIFY</code></td>
|
||||
<td style="text-align: right;">0</td>
|
||||
<td>отсутствует</td>
|
||||
<td>образ получен</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x07</code></td>
|
||||
<td><code>COMMIT</code></td>
|
||||
<td style="text-align: right;">0</td>
|
||||
<td>отсутствует</td>
|
||||
<td>verified</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x08</code></td>
|
||||
<td><code>CONFIRM</code></td>
|
||||
<td style="text-align: right;">0</td>
|
||||
<td>отсутствует</td>
|
||||
<td>запущенное приложение</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x09</code></td>
|
||||
<td><code>REBOOT</code></td>
|
||||
<td style="text-align: right;">0</td>
|
||||
<td>отсутствует</td>
|
||||
<td>активная сессия</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x0A</code></td>
|
||||
<td><code>ABORT</code></td>
|
||||
<td style="text-align: right;">0</td>
|
||||
<td>отсутствует</td>
|
||||
<td>активная сессия</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x0B</code></td>
|
||||
<td><code>QUERY_PROGRESS</code></td>
|
||||
<td style="text-align: right;">0</td>
|
||||
<td>отсутствует</td>
|
||||
<td>активная сессия</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3 id="begin_image"><code>BEGIN_IMAGE</code></h3>
|
||||
<pre><code class="language-text">DATA[0..3] ImageSize, uint32 little-endian
|
||||
DATA[4..7] ImageCRC32, uint32 little-endian
|
||||
</code></pre>
|
||||
<h3 id="begin_compat"><code>BEGIN_COMPAT</code></h3>
|
||||
<pre><code class="language-text">DATA[0..1] ProductType, uint16 little-endian
|
||||
DATA[2] HardwareRevisionMin
|
||||
DATA[3] HardwareRevisionMax
|
||||
DATA[4..7] FirmwareVersion, uint32 little-endian
|
||||
</code></pre>
|
||||
<p>До <code>ERASE</code> прибор обязан получить обе части метаданных и проверить размер,
|
||||
тип изделия, аппаратную ревизию, версию и политику anti-rollback.</p>
|
||||
<h2 id="boot_status"><code>BOOT_STATUS</code></h2>
|
||||
<pre><code class="language-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
|
||||
</code></pre>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align: right;">Код</th>
|
||||
<th>Статус</th>
|
||||
<th>Повтор допустим</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x00</code></td>
|
||||
<td><code>OK</code></td>
|
||||
<td>—</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x01</code></td>
|
||||
<td><code>BUSY</code></td>
|
||||
<td>да, после задержки</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x02</code></td>
|
||||
<td><code>INVALID_COMMAND</code></td>
|
||||
<td>после исправления</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x03</code></td>
|
||||
<td><code>WRONG_DEVICE</code></td>
|
||||
<td>нет для этого образа</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x04</code></td>
|
||||
<td><code>WRONG_HARDWARE</code></td>
|
||||
<td>нет для этого образа</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x05</code></td>
|
||||
<td><code>INVALID_SIZE</code></td>
|
||||
<td>нет для этого образа</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x06</code></td>
|
||||
<td><code>CRC_ERROR</code></td>
|
||||
<td>новая передача</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x07</code></td>
|
||||
<td><code>FLASH_ERROR</code></td>
|
||||
<td>зависит от платформы</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x08</code></td>
|
||||
<td><code>SEQUENCE_ERROR</code></td>
|
||||
<td>да, с <code>NextBlock</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x09</code></td>
|
||||
<td><code>SIGNATURE_ERROR</code></td>
|
||||
<td>нет</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x0A</code></td>
|
||||
<td><code>SESSION_ERROR</code></td>
|
||||
<td>открыть новую сессию</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x0B</code></td>
|
||||
<td><code>VOLTAGE_ERROR</code></td>
|
||||
<td>да после нормализации питания</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: right;"><code>0x0C</code></td>
|
||||
<td><code>INVALID_STATE</code></td>
|
||||
<td>выполнить правильный переход</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2 id="state-machine">State machine</h2>
|
||||
<pre><code class="language-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
|
||||
</code></pre>
|
||||
<p>Ошибка Flash, CRC, совместимости или подписи переводит сессию в <code>FAILED</code>.
|
||||
Новая <code>ENTER_BOOT</code> создаёт чистую сессию. <code>ABORT</code> прекращает текущую передачу,
|
||||
не активируя частично записанный слот.</p>
|
||||
<h2 id="section-3">Надёжность и повторы</h2>
|
||||
<ul>
|
||||
<li>Блоки передаются строго по возрастанию <code>BlockIndex</code>.</li>
|
||||
<li>Дубликат или пропуск возвращает <code>SEQUENCE_ERROR</code> и ожидаемый <code>NextBlock</code>.</li>
|
||||
<li>Базовый режим подтверждает каждый блок.</li>
|
||||
<li>Рабочий режим может подтверждать окно из 16 блоков.</li>
|
||||
<li>После потери связи <code>QUERY_PROGRESS</code> возвращает следующий ожидаемый блок,
|
||||
пока состояние загрузчика сохранено.</li>
|
||||
<li>Для продолжения после перезагрузки порт должен сохранять session metadata
|
||||
и восстановить её при инициализации; ядро версии 1.0 само это не делает.</li>
|
||||
</ul>
|
||||
<h2 id="ab">Безопасность и A/B-обновление</h2>
|
||||
<pre><code class="language-text">active=A -> target=B -> verify -> pending=B
|
||||
active=B -> target=A -> verify -> pending=A
|
||||
</code></pre>
|
||||
<p>CRC32 защищает только от случайного повреждения. Серийный загрузчик должен
|
||||
дополнительно проверить подпись контейнера, границы вектора, совместимость и
|
||||
anti-rollback. Bootloader не обновляется командами <code>BOOT_DATA_A/B</code>.</p>
|
||||
<p>Boot metadata должна атомарно хранить:</p>
|
||||
<ul>
|
||||
<li>активный слот;</li>
|
||||
<li>pending-слот;</li>
|
||||
<li>подтверждение запуска;</li>
|
||||
<li>число неудачных попыток;</li>
|
||||
<li>версию и CRC32 образа.</li>
|
||||
</ul>
|
||||
<p>Если приложение не выполняет <code>CONFIRM</code> за установленное число запусков,
|
||||
загрузчик возвращается к предыдущему подтверждённому слоту.</p>
|
||||
<h2 id="section-4">Эталонный сценарий</h2>
|
||||
<ol>
|
||||
<li>ПМ адресно отправляет <code>IDENTIFY</code>.</li>
|
||||
<li>ПМ открывает ненулевой <code>SessionID</code> командой <code>ENTER_BOOT</code>.</li>
|
||||
<li>ПМ отправляет <code>BEGIN_IMAGE</code> и <code>BEGIN_COMPAT</code>.</li>
|
||||
<li>Прибор сообщает выбранный неактивный слот.</li>
|
||||
<li>ПМ выполняет <code>ERASE</code> и передаёт <code>BOOT_DATA_A</code> либо <code>BOOT_DATA_B</code>.</li>
|
||||
<li>ПМ выполняет <code>VERIFY</code>, затем <code>COMMIT</code> и <code>REBOOT</code>.</li>
|
||||
<li>Новое приложение после самопроверки выполняет <code>CONFIRM</code>.</li>
|
||||
</ol>
|
||||
|
||||
<hr>
|
||||
<h1 id="section">Общее адресное пространство</h1>
|
||||
<p>Статус: <strong>Stable, данные ведутся в XLSX</strong><br />
|
||||
Порядок значений: <strong>16-битные регистры, little-endian в CAN payload</strong></p>
|
||||
<p>Редактируемый источник реестра:
|
||||
<a href="../../%D0%9F%D1%80%D0%BE%D1%82%D0%BE%D0%BA%D0%BE%D0%BB%20CAN%20%D0%B8%20%D0%9E%D0%90%D0%9F.xlsx"><code>Протокол CAN и ОАП.xlsx</code></a>.</p>
|
||||
<p>Просматриваемая большая таблица находится в
|
||||
<a href="../../%D0%9F%D1%80%D0%BE%D1%82%D0%BE%D0%BA%D0%BE%D0%BB%20CAN%20%D0%B8%20%D0%9E%D0%90%D0%9F.html"><code>Протокол CAN и ОАП.html</code></a> и
|
||||
<a href="../../%D0%9F%D1%80%D0%BE%D1%82%D0%BE%D0%BA%D0%BE%D0%BB%20CAN%20%D0%B8%20%D0%9E%D0%90%D0%9F.md"><code>Протокол CAN и ОАП.md</code></a>.</p>
|
||||
<h2 id="section-1">Назначение</h2>
|
||||
<p>ОАП связывает 16-битный адрес регистра с его типом, назначением, доступом и
|
||||
масштабом. В ProtoCAN используется <code>MsgType=0x3</code>, а <code>MsgBody</code> содержит адрес
|
||||
первого регистра.</p>
|
||||
<h2 id="section-2">Обязательные поля реестра</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Поле</th>
|
||||
<th>Требование</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>AddressHex</td>
|
||||
<td><code>0x0000…0xFFFF</code>, уникальное значение</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>AddressDec</td>
|
||||
<td>десятичный эквивалент AddressHex</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Group</td>
|
||||
<td>функциональная группа</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>однозначное имя параметра</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td><code>u16</code>, <code>i16</code>, <code>u32</code>, <code>i32</code>, <code>float32</code>, bitmap или массив</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Registers</td>
|
||||
<td>число занятых 16-битных регистров</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Access</td>
|
||||
<td><code>R</code>, <code>W</code> или <code>RW</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Unit</td>
|
||||
<td>физическая единица либо <code>—</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Scale</td>
|
||||
<td>множитель/делитель представления</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Default</td>
|
||||
<td>значение после сброса, если применимо</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>семантика, диапазон и особые значения</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2 id="section-3">Правила ведения</h2>
|
||||
<ul>
|
||||
<li>Адрес не переиспользуется с другим смыслом после выпуска стабильной версии.</li>
|
||||
<li>Многорегистровое значение занимает непрерывный диапазон.</li>
|
||||
<li>Порядок 16-битных слов для 32-битного значения фиксируется в строке типа.</li>
|
||||
<li>Резервные диапазоны явно отмечаются и не используются без изменения версии.</li>
|
||||
<li>Удалённый параметр помечается deprecated, а не исчезает молча.</li>
|
||||
<li>Изменение адреса, типа или масштаба отражается в <code>CHANGELOG.md</code>.</li>
|
||||
</ul>
|
||||
<h2 id="section-4">Экспорт</h2>
|
||||
<p>Для программной генерации каталог следует экспортировать из XLSX в CSV с
|
||||
UTF-8 и фиксированными английскими именами колонок. CSV должен проверяться на:</p>
|
||||
<ul>
|
||||
<li>уникальность адресов;</li>
|
||||
<li>пересечение многорегистровых значений;</li>
|
||||
<li>допустимые типы и права доступа;</li>
|
||||
<li>равенство шестнадцатеричного и десятичного адреса;</li>
|
||||
<li>попадание адреса в диапазон <code>0x0000…0xFFFF</code>.</li>
|
||||
</ul>
|
||||
<p>До появления автоматического экспортёра нормативным источником адресов
|
||||
остаётся XLSX, а HTML/Markdown считаются представлением.</p>
|
||||
|
||||
<hr>
|
||||
<h1 id="protocan">История изменений ProtoCAN</h1>
|
||||
<p>Формат основан на Keep a Changelog. Версия относится к спецификации, а не к
|
||||
версии прошивки отдельного прибора.</p>
|
||||
<h2 id="unreleased">[Unreleased]</h2>
|
||||
<h3 id="added">Added</h3>
|
||||
<ul>
|
||||
<li>Структурированный комплект документации <code>docs/protocan</code>.</li>
|
||||
<li>Загрузочный сервис <code>MsgType=0x9…0xD</code>.</li>
|
||||
<li>Два логических слота по 512 КиБ, 65 536 блоков по 8 байт каждый.</li>
|
||||
<li>Машинные эталоны CAN ID в <code>examples/test-vectors.json</code>.</li>
|
||||
</ul>
|
||||
<h2 id="section">[1.0] — 2026-08-29</h2>
|
||||
<h3 id="added-1">Added</h3>
|
||||
<ul>
|
||||
<li>Зафиксирована 29-битная структура ProtoCAN ID.</li>
|
||||
<li>Зафиксирована адресация 8 типов по 16 экземпляров.</li>
|
||||
<li>Существующие сообщения <code>0x0…0x8</code>, <code>0xE</code>, <code>0xF</code> сохранены.</li>
|
||||
</ul>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
13405
Протокол CAN и ОАП.html
Normal file
13405
Протокол CAN и ОАП.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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"> </td>
|
||||
<td class="x50"> </td>
|
||||
<td colspan="4" class="x156"> </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"> </td>
|
||||
<td colspan="4" class="x101">NULL</td>
|
||||
<td colspan="4" class="x101">0/8</td>
|
||||
<td class="x14"> </td>
|
||||
<td colspan="8" class="x101">NULL</td>
|
||||
<td colspan="8" class="x101">Параметры команды</td>
|
||||
</tr>
|
||||
<tr style="height:17px">
|
||||
<td class="x26"> </td>
|
||||
<td class="x27"> </td>
|
||||
<td class="x50"> </td>
|
||||
<td class="x50"> </td>
|
||||
<td class="x50"> </td>
|
||||
<td colspan="4" class="x156"> </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"> </td>
|
||||
<td colspan="4" class="x101">8</td>
|
||||
<td class="x14"> </td>
|
||||
<td colspan="8" class="x101">8 байт прошивки слота A</td>
|
||||
</tr>
|
||||
<tr style="height:17px">
|
||||
<td class="x26"> </td>
|
||||
<td class="x27"> </td>
|
||||
<td class="x50"> </td>
|
||||
<td class="x50"> </td>
|
||||
<td class="x50"> </td>
|
||||
<td colspan="4" class="x156"> </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"> </td>
|
||||
<td colspan="4" class="x101">8</td>
|
||||
<td class="x14"> </td>
|
||||
<td colspan="8" class="x101">8 байт прошивки слота B</td>
|
||||
</tr>
|
||||
<tr style="height:17px">
|
||||
<td class="x26"> </td>
|
||||
<td class="x27"> </td>
|
||||
<td class="x50"> </td>
|
||||
<td class="x50"> </td>
|
||||
<td class="x50"> </td>
|
||||
<td colspan="4" class="x156"> </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"> </td>
|
||||
<td colspan="4" class="x101">8</td>
|
||||
<td class="x14"> </td>
|
||||
<td colspan="8" class="x101">Status, Slot, NextBlock, RunningCRC32</td>
|
||||
</tr>
|
||||
<tr style="height:17px">
|
||||
<td class="x26"> </td>
|
||||
<td class="x27"> </td>
|
||||
<td class="x50"> </td>
|
||||
<td class="x50"> </td>
|
||||
<td class="x50"> </td>
|
||||
<td colspan="4" class="x156"> </td>
|
||||
<td colspan="4" class="x149">0xD. BOOT_DISCOVERY</td>
|
||||
<td colspan="16" class="x162">Подтип запроса/ответа идентификации загрузчика</td>
|
||||
<td class="x14"> </td>
|
||||
<td colspan="4" class="x101">0/8</td>
|
||||
<td class="x14"> </td>
|
||||
<td colspan="8" class="x101">ProductType, HardwareRevision, BootVersion, FirmwareVersion</td>
|
||||
</tr>
|
||||
<tr style="height:17px">
|
||||
<td class="x26"> </td>
|
||||
|
||||
Reference in New Issue
Block a user