eeprom-ft24c256
EEPROM 24Cxx с нарезкой записи по страницам.
Порт: I²C write, write-read и delay.
can-sensor
Однокадровые SETCAN SETTINGS для 64-битных ROM датчиков.
Порт: готовый bxCAN STM32F1 либо callbacks CAN TX/RX.
ds18b20
1-Wire, CRC8, SEARCH ROM и несколько термометров.
Порт: Init, DelayUs, Reset, WriteBit, ReadBit.
SETProtocol
Единое C99-ядро: SET v2, ProtoCAN/GUI v1, телеметрия, firmware flow, GAS и стабильный ABI.
Порт: COM, SLCAN, SocketCAN, JNI или callbacks MCU.
SETProtocol
Единое C99-ядро: SET v2, ProtoCAN/GUI v1, телеметрия, firmware flow, GAS и стабильный ABI.
Порт: COM, SLCAN, SocketCAN, JNI или callbacks MCU.
protocan-boot
Прошивка по ProtoCAN: A/B-слоты, resume, CRC32, verify и rollback.
Порт: CAN TX, Flash, metadata, verify, reboot.
rs485-boot
SETGUI v1 firmware update: stream parser, CRC32 и resume.
Порт: UART TX/RX, DE и Flash.
rtc-service
RTC с резервным backup-томом и единым API времени.
Порт: RTC и backup storage.
Разбор CAN-кадров v1 и v2
Раздел генерируется из doc/CAN_FRAME_PARSE_V1_V2.md. В нём собраны wire-разметка, примеры и готовые Python-парсеры.
Разбор 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.
+
Полный справочник
Этот раздел генерируется из канонического c/set-protocol/docs/SETPROTOCOL.md. Редактировать нужно Markdown, затем запускать doc/build-setprotocol-html.ps1.
2. Состав исходников
-| Модуль | @@ -234,7 +706,7 @@ COM │ SLCAN │ SocketCAN │ USB CDC │ TCP │ STM32 UART/CANABI компилятора C |
|---|
Общая точка включения для C-кода — include/setprotocol.h.
Иностранные runtimes должны использовать include/setprotocol_abi.h, а не
повторять внутреннюю раскладку pcan_parser_t или gui_parser_t.
@@ -253,7 +725,7 @@ ABI v1 пока экспортирует функции pcan_abi_*:
кадра — 19 байт. Формат переносит один classic CAN 2.0 кадр через COM, USB CDC,
RS-232, RS-485 или TCP byte stream.
Флаги:
-| Бит | @@ -283,7 +755,7 @@ RS-232, RS-485 или TCP byte stream.служебный кадр диагностики моста |
|---|
3.2. GUI transport: A5 5A
A5 5A | VER | TYPE | SEQ[2] BE | SIZE[2] BE | PAYLOAD[0..512] | CRC32 LE
@@ -312,7 +784,7 @@ MSVC, GCC и Clang.
pcan_abi.h экспортирует простые числа, указатели и явно ограниченные буферы.
Текущая версия возвращается pcan_abi_version() и равна 1.
CAN bridge API
-| Функция | @@ -353,9 +825,9 @@ MSVC, GCC и Clang.Получить frames/CRC/bad length/stray bytes |
|---|
GUI API
-| Функция | @@ -380,7 +852,7 @@ MSVC, GCC и Clang.Получить frames/CRC/version/length/stray bytes |
|---|
Возврат 0 из encode означает неверные аргументы или недостаточный output
buffer. Parser API возвращает отрицательное значение при неверном context,
0 пока кадр не собран и 1 при готовом кадре.
7. Порты и адаптеры
-| Среда | @@ -449,7 +921,7 @@ pcan_abi_parser_init(storage, size);ABI подходит, Swift wrapper пока не добавлен |
|---|
Linux
Само ядро не содержит WinAPI, поэтому собирается GCC или Clang. Для SETGUI под
Linux остаются две отдельные задачи: упаковать libsetprotocol.so с приложением и
@@ -514,7 +986,7 @@ size_t written = pcan_abi_frame_encode(
Для firmware удобнее полный C API из protocan_transport.h: он даёт link,
callbacks, ring и GAS без FFI-обёртки.
10. Диагностика
-| Симптом | @@ -551,7 +1023,7 @@ callbacks, ring и GAS без FFI-обёртки.COM baud не равен CAN bitrate; проверьте настройку самого адаптера |
|---|
11. Совместимость и ограничения
- ABI v1 изменяется только совместимым добавлением функций. Ломающее изменение