Files
templates/doc/CAN_FRAME_PARSE_V1_V2.md

13 KiB
Raw Blame History

Разбор CAN-кадров ProtoCAN Boot v1 и SETProtocol v2

Документ описывает wire-форматы двух протоколов обновления прошивки:

  • v1templates/c/protocan-boot, одна команда или 8 байт образа в одном Extended CAN-кадре;
  • v2templates/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, следующие 17 байт 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.