Разделы про роли и идентификацию устройства, кадр USB CDC, сетевой слой (UDP, seq/новая сессия, HMAC, WireGuard), многоуровневый failsafe и запуск моста. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9.9 KiB
Протокол старого пульта тележки (реверс-инжиниринг)
Снято 2026-08-20 осциллографом Hantek DPO7204C с сигнального провода пульта
(канал CH3). Инструменты: tools/capture.py (захват), tools/decode.py
(декодер), сырые данные и картинки — в captures/.
Физический уровень
- Один сигнальный провод, логика 3.3 В.
- Инвертированный UART (стандартная полярность SBUS): в покое линия низкая (~0 В), импульсы вверх до ~3.3 В.
- Скорость 100 000 бод, формат 8E2 (8 бит данных, чётность even, 2 стоп-бита), биты LSB-first. Длительность бита 10 мкс.
Кадровый уровень — SBUS
Стандартный кадр Futaba SBUS, 25 байт (3 мс на линии):
| Смещение | Размер | Содержимое |
|---|---|---|
| 0 | 1 | Заголовок 0x0F |
| 1 | 22 | 16 каналов × 11 бит, упакованы подряд LSB-first |
| 23 | 1 | Флаги: bit0=CH17, bit1=CH18, bit2=frame_lost, bit3=failsafe |
| 24 | 1 | Футер 0x00 |
Распаковка каналов: 22 байта складываются в 176-битное число LSB-first, канал N (N=0..15) = биты [11·N .. 11·N+10], диапазон значений 0–2047.
- Кадры отправляются каждые 50 мс (20 Гц). Это медленнее стандартного SBUS (7/14 мс) — ответная часть с этим темпом работает.
- Флаги во всех наблюдениях =
0x00.
Карта каналов (нумерация с 1)
| Управление | Канал | Мин | Нейтраль | Макс |
|---|---|---|---|---|
| Стик вперёд/назад | 2 | 433 (назад) | 1024 | 1643 (вперёд) |
| Стик влево/вправо | 4 | 446 (влево) | 1024 | 1654 (вправо) |
| Остальные 14 | — | всегда 1024 |
Диапазон осей ~±600 от нейтрали (не полная шкала SBUS). Других органов управления на пульте нет.
Эталонный кадр нейтрали (hex):
0F 00 04 20 00 01 08 40 00 02 10 80 00 04 20 00 01 08 40 00 02 10 80 00 00
Воспроизведение на STM32G431
- USART: 100000 бод, 8 бит + чётность even (в терминах STM32: M=1, 9-bit с PCE=1), 2 стоп-бита, TXINV=1 (аппаратная инверсия TX) — бит-бэнг не нужен.
- Отправлять 25-байтовый кадр по таймеру каждые 50 мс.
- Нейтраль: все каналы 1024; управление — каналы 2 и 4 в измеренных диапазонах.
Сетевой мост «пульт → тележка»
Та же команда управления может идти не по проводу рядом, а через локальную сеть/интернет, не ломая прежний проводной режим. Цепочка:
[пульт] ADC→ STM(CART-TX) ─USB─► bridge --role tx ─UDP─► bridge --role rx ─USB─► STM(CART-RX) ─SBUS─► драйвер
STM-TX продолжает гнать SBUS на свой локальный провод как раньше и дополнительно отдаёт те же значения каналов по USB. STM-RX принимает их по USB и выдаёт на свой провод так, будто сигнал пришёл с пульта.
Роли и идентификация устройства
Роль выбирается на сборке (platformio.ini): pio run -e cart_tx или
-e cart_rx. Общий код (sbus, usb_link) собирается в оба env, специфичный —
через build_src_filter, мёртвого кода неактивной роли нет.
Каждая прошивка публикует USB product string: CART-TX / CART-RX. По ней
tools/bridge.py находит нужный порт (serial.tools.list_ports). После открытия
порта ПК шлёт HELLO и ждёт IDENT — подтверждение роли, версии протокола и
96-битного UID чипа (на случай неверно прошитого устройства, рассинхрона версий
или чужого CDC с тем же VID/PID). Serial-дескриптор (UID) различает экземпляры.
Кадр поверх USB CDC
Единственный источник истины — src/link_protocol.h, зеркало на Python —
tools/link_proto.py (сверка: python3 tools/link_proto.py --selftest).
[0xA5][0x5A][VER=1][TYPE][LEN][PAYLOAD..LEN][CRC16_LO][CRC16_HI]
- CRC16-CCITT (poly
0x1021, init0xFFFF) по байтам[VER..конец PAYLOAD], LE. Контрольный вектор:crc16("123456789") = 0x29B1. - Приёмник (STM и Python) — стейт-машина: ищет синхрослово, читает по
LEN, проверяет CRC; при сбое сдвиг на байт и ресинхронизация (устойчивость к мусору и потере байта). - Типы:
0x01 CONTROL,0x02 IDENT,0x03 HELLO,0x04 LOG. Отладочный текст идёт только кадромLOG— «голых»Serial.print, ломающих бинарный поток, нет.
Payload CONTROL (LE): seq:u32, t_ms:u32, flags:u8, ch[16]:u16 = 41 байт.
ch[] — значения SBUS-каналов 0..2047, ровно те, что TX кладёт на свой провод.
Это общая «валюта» всей цепочки: ПК перекладывает payload в UDP и обратно, RX
распаковывает и пакует в SBUS. flags bit0 = failsafe.
Сетевой слой (между экземплярами bridge)
- Транспорт — UDP: для управления 20 Гц свежий кадр ценнее доставленного; TCP при потере встал бы в head-of-line blocking и копил лаг.
- Полезная нагрузка UDP — тот же
CONTROLpayload.seqдаёт отбрасывание устаревших/повторных пакетов (защита от переупорядочивания и дубликатов UDP) с корректной арифметикой переполнения uint32. Перезапуск источника (seq снова с 0) распознаётся по пачке подряд «несвежих» пакетов и трактуется как новая сессия — иначе поток завис бы в failsafe навсегда. Это best-effort защита от повтора старых команд, не криптографическая: аутентичность даёт HMAC, против целевого replay на недоверенной сети используй WireGuard. - Приём из сети и выдача SBUS на провод — разные таймеры: джиттер сети не попадает на провод, STM-RX гонит SBUS строго каждые 50 мс из последнего состояния.
- Безопасность. Для интернета обязателен общий ключ (
--key): каждый UDP- пакет аутентифицируется усечённым HMAC-SHA256 (8 байт), RX отбрасывает неаутентичные и принимает только с ожидаемого адреса (--peer). Рекомендуемый способ наружу — WireGuard-туннель, внутри которого идёт простой UDP. «Голый» неаутентифицированный UDP допустим только в доверенной локальной сети.
FAILSAFE
Потеря связи никогда не оставляет тележку на последней команде — выход всегда нейтраль (все каналы 1024), а не удержание.
- Главный предохранитель — на самом STM-RX. Если валидный
CONTROLпо USB не приходил дольшеRX_USB_TIMEOUT_MS = 150мс (3 кадра), STM-RX сам выдаёт на провод нейтраль с флагом failsafe (SBUS bit3). Срабатывает при обрыве USB, зависании или падении ПК-процесса. SBUS на провод идёт непрерывно 20 Гц в любом случае — драйвер не видит пропажи сигнала. - Сетевой failsafe на ПК-RX. При тишине из сети дольше 100 мс bridge активно
шлёт на STM-RX нейтральные
CONTROLс флагомCTRL_FLAG_FAILSAFEв темпе 20 Гц, чтобы причина была явной. STM-RX транслирует этот флаг в бит failsafe SBUS-кадра (byte 23 bit3). Восстановление — по первому свежему валидному пакету.
Запуск
# сторона пульта
.venv/bin/python tools/bridge.py --role tx --peer 10.0.0.2:5005 --key SECRET
# сторона тележки
.venv/bin/python tools/bridge.py --role rx --bind 0.0.0.0:5005 --peer 10.0.0.1 --key SECRET
Локальная проверка без сети: оба экземпляра на одной машине с --peer 127.0.0.1
и двумя STM по USB; SBUS с RX снять tools/decode.py и сверить с осями стика TX.