13 KiB
Разбор 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
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
Формула:
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 байт:
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 содержит:
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:
CAN ID: 17D90001
DLC: 0
1.2. Python-парсер v1
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
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
Формула:
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, следующие 1–7 байт SETP |
0x30..0x32 |
flow control | 3S, block_size, st_min_ms |
N — циклический номер сегмента 1..15,0..; следующий сегмент обязан иметь
ожидаемый номер, тот же CAN ID и прийти до тайм-аута сборки 500 мс.
2.3. Внутренний кадр SETProtocol v2
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:
Полный 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
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.