Files
templates/doc/CAN_FRAME_PARSE_V1_V2.md

380 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Разбор CAN-кадров ProtoCAN Boot v1 и SETProtocol v2
Документ описывает wire-форматы двух протоколов обновления прошивки:
- **v1** — `templates/c/protocan-boot`, одна команда или 8 байт образа в одном
Extended CAN-кадре;
- **v2** — `templates/c/set-protocol`, полный кадр SETProtocol разбивается на
несколько Extended CAN-кадров.
Все многобайтные поля payload передаются **little-endian**. CAN ID — 29-битный.
Для рабочего кода нужно использовать канонические реализации из `templates`,
а приведённый ниже Python-парсер удобен для анализатора, логов и отладки.
## 1. ProtoCAN Boot v1
### 1.1. Разметка Extended CAN ID
```text
bits size field
28 1 Priority
27 1 Route: 0 = host -> device, 1 = device -> host
26..24 3 Device Type
23..20 4 Device ID
19..16 4 Message Type
15..0 16 Message Body
```
Формула:
```text
ID = Priority << 28 |
Route << 27 |
DeviceType << 24 |
DeviceID << 20 |
MessageType << 16 |
MessageBody
```
Типы загрузочных сообщений:
| Message Type | Имя | Message Body | CAN payload |
|---:|---|---|---|
| `0x9` | `BOOT_CONTROL` | `SessionID << 8 \| Command` | параметры команды |
| `0xA` | `BOOT_DATA_A` | индекс блока | 8 байт слота A |
| `0xB` | `BOOT_DATA_B` | индекс блока | 8 байт слота B |
| `0xC` | `BOOT_STATUS` | `SessionID << 8 \| Command` | статус и прогресс |
| `0xD` | `BOOT_DISCOVERY` | подтип | информация об устройстве |
Команды `BOOT_CONTROL`:
| Код | Команда | Payload |
|---:|---|---|
| `0x01` | `IDENTIFY` | пустой |
| `0x02` | `ENTER_BOOT` | пустой |
| `0x03` | `BEGIN_IMAGE` | `image_size u32`, `image_crc32 u32` |
| `0x04` | `BEGIN_COMPAT` | `product u16`, `hw_min u8`, `hw_max u8`, `version u32` |
| `0x05` | `ERASE` | пустой |
| `0x06` | `VERIFY` | пустой |
| `0x07` | `COMMIT` | пустой |
| `0x08` | `CONFIRM` | пустой |
| `0x09` | `REBOOT` | пустой |
| `0x0A` | `ABORT` | пустой |
| `0x0B` | `QUERY_PROGRESS` | пустой |
`BOOT_STATUS` всегда содержит 8 байт:
```text
offset size field
0 1 status
1 1 target_slot
2 2 next_block u16 LE
4 4 running_crc32 u32 LE
```
`BOOT_DISCOVERY` с body `1` содержит:
```text
offset size field
0 2 product_type u16 LE
2 1 hardware_revision
3 1 protocol_version = 1
4 4 firmware_version u32 LE
```
Пример запроса `IDENTIFY` для `DeviceType=7`, `DeviceID=13`:
```text
CAN ID: 17D90001
DLC: 0
```
### 1.2. Python-парсер v1
```python
def parse_v1(can_id: int, data: bytes) -> dict:
if not 0 <= can_id <= 0x1FFFFFFF:
raise ValueError("неверный Extended CAN ID")
if len(data) > 8:
raise ValueError("DLC больше 8")
result = {
"version": 1,
"priority": (can_id >> 28) & 0x01,
"route": (can_id >> 27) & 0x01,
"device_type": (can_id >> 24) & 0x07,
"device_id": (can_id >> 20) & 0x0F,
"message_type": (can_id >> 16) & 0x0F,
"message_body": can_id & 0xFFFF,
"data": bytes(data),
}
msg_type = result["message_type"]
body = result["message_body"]
if msg_type in (0x9, 0xC):
result["session_id"] = (body >> 8) & 0xFF
result["command"] = body & 0xFF
elif msg_type in (0xA, 0xB):
result["slot"] = msg_type - 0xA
result["block_index"] = body
if msg_type == 0xC:
if len(data) != 8:
raise ValueError("BOOT_STATUS должен содержать 8 байт")
result.update({
"status": data[0],
"target_slot": data[1],
"next_block": int.from_bytes(data[2:4], "little"),
"running_crc32": int.from_bytes(data[4:8], "little"),
})
elif msg_type == 0xD and body == 1:
if len(data) != 8:
raise ValueError("BOOT_DISCOVERY должен содержать 8 байт")
result.update({
"product_type": int.from_bytes(data[0:2], "little"),
"hardware_revision": data[2],
"protocol_version": data[3],
"firmware_version": int.from_bytes(data[4:8], "little"),
})
return result
```
## 2. SETProtocol v2 поверх classic CAN
В v2 CAN-кадр является только транспортным сегментом. Сначала нужно собрать
полный SETP-пакет, и только затем разбирать его заголовок, payload и CRC32.
### 2.1. Разметка Extended CAN ID
```text
bits size field
28..24 5 Prefix = 0x12
23..16 8 Destination node
15..8 8 Source node
7 1 Priority
6..0 7 Channel
```
Формула:
```text
ID = 0x12 << 24 |
Destination << 16 |
Source << 8 |
Priority << 7 |
Channel
```
### 2.2. CAN-сегменты
Первый байт CAN payload — PCI:
| PCI | Назначение | Формат CAN payload |
|---:|---|---|
| `0x10` | первый сегмент | `10`, `total_length u16 LE`, первые 5 байт SETP |
| `0x20..0x2F` | продолжение | `2N`, следующие 17 байт SETP |
| `0x30..0x32` | flow control | `3S`, `block_size`, `st_min_ms` |
`N` — циклический номер сегмента `1..15,0..`; следующий сегмент обязан иметь
ожидаемый номер, тот же CAN ID и прийти до тайм-аута сборки 500 мс.
### 2.3. Внутренний кадр SETProtocol v2
```text
offset size field
0 2 SOF = A5 5A
2 1 version = 02
3 1 flags
4 2 message_type u16 LE
6 2 source u16 LE
8 2 destination u16 LE
10 2 sequence u16 LE
12 2 payload_length u16 LE
14 N payload
14+N 4 CRC32 IEEE u32 LE
```
CRC32 считается по байтам от `version` на offset 2 до конца payload. Поля
`source`, `destination` и `priority` внутреннего заголовка должны совпадать с
CAN ID.
Флаги:
| Бит | Значение |
|---:|---|
| `0x01` | RESPONSE |
| `0x02` | EVENT |
| `0x04` | ERROR |
| `0x08` | ACK_REQUIRED |
| `0x10` | MORE |
| `0x20` | PRIORITY |
Каждый response начинается с `status u16 LE`. Основные firmware message types:
`FW_BEGIN=0x0100`, `FW_DATA=0x0101`, `FW_END=0x0102`, `FW_ABORT=0x0103`,
`FW_STATUS=0x0104`, `FW_ACTIVATE=0x0105`.
Пример `PING` к BALZAM node `13`, source `0`, sequence `1`, priority `1`,
channel `1`:
```text
Полный SETP:
A5 5A 02 28 01 00 00 00 0D 00 01 00 00 00 E7 29 51 40
CAN ID 120D0081, сегменты:
10 12 00 A5 5A 02 28 01
21 00 00 00 0D 00 01 00
22 00 00 E7 29 51 40
```
### 2.4. Python-парсер и сборщик v2
```python
import binascii
def parse_v2_can_id(can_id: int) -> dict:
if not 0 <= can_id <= 0x1FFFFFFF:
raise ValueError("неверный Extended CAN ID")
if (can_id >> 24) & 0x1F != 0x12:
raise ValueError("не SETProtocol v2 CAN ID")
return {
"destination": (can_id >> 16) & 0xFF,
"source": (can_id >> 8) & 0xFF,
"priority": (can_id >> 7) & 0x01,
"channel": can_id & 0x7F,
}
def parse_setp(packet: bytes, can_id: int) -> dict:
if len(packet) < 18 or packet[:2] != b"\xA5\x5A":
raise ValueError("нет полного SETP-кадра")
if packet[2] != 2:
raise ValueError("неподдерживаемая версия SETP")
flags = packet[3]
if flags & 0xC0:
raise ValueError("установлены зарезервированные флаги")
payload_length = int.from_bytes(packet[12:14], "little")
if len(packet) != 14 + payload_length + 4:
raise ValueError("не совпадает payload_length")
expected_crc = int.from_bytes(packet[-4:], "little")
actual_crc = binascii.crc32(packet[2:-4]) & 0xFFFFFFFF
if actual_crc != expected_crc:
raise ValueError("ошибка CRC32 SETP")
address = parse_v2_can_id(can_id)
source = int.from_bytes(packet[6:8], "little")
destination = int.from_bytes(packet[8:10], "little")
priority = int(bool(flags & 0x20))
if (source, destination, priority) != (
address["source"], address["destination"], address["priority"]
):
raise ValueError("SETP header не совпадает с CAN ID")
payload = packet[14:-4]
result = {
"version": 2,
"flags": flags,
"message_type": int.from_bytes(packet[4:6], "little"),
"source": source,
"destination": destination,
"sequence": int.from_bytes(packet[10:12], "little"),
"payload": payload,
"can": address,
}
if flags & 0x01:
if len(payload) < 2:
raise ValueError("response не содержит status")
result["status"] = int.from_bytes(payload[:2], "little")
result["body"] = payload[2:]
return result
class V2CanReassembler:
def __init__(self, timeout_ms: int = 500):
self.timeout_ms = timeout_ms
self.reset()
def reset(self):
self.can_id = None
self.total = 0
self.data = bytearray()
self.next_sequence = 1
self.deadline_ms = 0
def feed(self, can_id: int, data: bytes, now_ms: int):
parse_v2_can_id(can_id)
if not 1 <= len(data) <= 8:
raise ValueError("DLC вне диапазона 1..8")
if self.can_id is not None and now_ms >= self.deadline_ms:
self.reset()
raise ValueError("тайм-аут сборки SETP")
pci_type = data[0] & 0xF0
if pci_type == 0x10:
if len(data) != 8:
raise ValueError("первый сегмент должен иметь DLC 8")
total = int.from_bytes(data[1:3], "little")
if not 18 <= total <= 530:
raise ValueError("неверный размер SETP")
self.can_id = can_id
self.total = total
self.data = bytearray(data[3:])
self.next_sequence = 1
self.deadline_ms = now_ms + self.timeout_ms
return None
if pci_type == 0x20:
sequence = data[0] & 0x0F
if (
self.can_id is None
or can_id != self.can_id
or sequence != self.next_sequence
or len(data) < 2
):
self.reset()
raise ValueError("ошибка последовательности CAN-сегментов")
if len(data) - 1 > self.total - len(self.data):
self.reset()
raise ValueError("лишние байты CAN-сегмента")
self.data.extend(data[1:])
self.next_sequence = (self.next_sequence + 1) & 0x0F
self.deadline_ms = now_ms + self.timeout_ms
if len(self.data) == self.total:
packet = bytes(self.data)
packet_can_id = self.can_id
self.reset()
return parse_setp(packet, packet_can_id)
return None
if pci_type == 0x30:
return {"flow_control": data[0] & 0x0F, "data": data[1:]}
raise ValueError("неизвестный PCI")
```
В SETGUI эти операции уже реализованы в
`third_party/templates/python/setprotocol/can.py`; собственный parser нужен
только внешнему анализатору или диагностическому скрипту.
## 3. Как отличать v1 от v2
Для используемых сейчас адресов достаточно следующих признаков:
- v2: верхние пять бит CAN ID равны `0x12`, PCI начинается с `0x10`, `0x2N`
или `0x3S`, после reassembly присутствует `A5 5A 02`;
- v1: `MessageType` в битах `19..16` равен `0x9..0xD`, каждый кадр разбирается
самостоятельно.
Однако универсальное автоопределение только по одному CAN ID невозможно:
комбинация `Priority/Route/DeviceType` v1 теоретически тоже может дать верхнее
поле `0x12`, а первый байт firmware data v1 может случайно совпасть с PCI.
Надёжный анализатор должен учитывать настроенный режим узла либо подтвердить v2
только после сборки кадра с корректными `A5 5A 02`, длиной и CRC32.
## 4. Канонические исходники
- v1 ID и state machine: `third_party/templates/c/protocan-boot/src/pcan_boot.c`;
- v2 CAN transport: `third_party/templates/c/set-protocol/src/set_can.c`;
- v2 frame/CRC: `third_party/templates/c/set-protocol/src/set_protocol.c`;
- v2 firmware payload: `third_party/templates/c/set-protocol/src/set_firmware.c`;
- Python v2 CAN: `third_party/templates/python/setprotocol/can.py`.