Разделы про роли и идентификацию устройства, прошивку STM (сборка/заливка через ST-Link по SWD, подключения по ролям, проверка роли), кадр USB CDC, сетевой слой (UDP, seq/новая сессия, HMAC, WireGuard), многоуровневый failsafe и запуск моста. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
13 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) различает экземпляры.
Прошивка STM
Плата — WeAct STM32G431CB. Нужен PlatformIO Core (pio); если не установлен:
pip install platformio (в репозитории он лежит в ~/.platformio/penv/bin/pio).
Все команды выполняются из каталога прошивки device_firmware/cart_firmware.
Две роли — две отдельные прошивки на две платы. По умолчанию загрузчик — ST-Link
по SWD (upload_protocol = stlink в platformio.ini).
1. Подключить ST-Link к плате по SWD (4 провода):
| ST-Link | WeAct G431CB |
|---|---|
| SWDIO | PA13 (SWDIO) |
| SWCLK | PA14 (SWCLK) |
| GND | GND |
| 3V3 | 3V3 |
2. Собрать и прошить роль (прошивать по одной плате за раз, один ST-Link):
# плата-передатчик (к ней подключён пульт)
pio run -e cart_tx -t upload
# плата-приёмник (к ней подключён провод драйвера колёс)
pio run -e cart_rx -t upload
- Только собрать без заливки:
pio run -e cart_tx. Артефакты —.pio/build/<env>/firmware.{elf,bin}. - Если подключено несколько ST-Link, указать конкретный:
pio run -e cart_tx -t upload --upload-port <serial-ST-Link>. - Сменить роль платы — просто залить другой env, флеш перезаписывается целиком.
3. Физические подключения по ролям (распиновка общая, см. выше по документу):
| CART-TX | CART-RX | |
|---|---|---|
| Стик X / Y | PA0 / PA1 (АЦП) | — не используется |
| Выход SBUS | PA9 → локальный драйвер (как раньше) | PA9 → провод драйвера колёс |
| USB | → ПК (bridge --role tx) | → ПК (bridge --role rx) |
4. Проверить роль после прошивки. Подключить плату по USB к ПК и прочитать product string (роль зашита в неё):
python3 -c "from serial.tools import list_ports; \
[print(p.device, p.product, p.serial_number) for p in list_ports.comports()]"
# ожидаемо: /dev/ttyACM0 CART-TX <UID> либо CART-RX
Либо просто запустить tools/bridge.py соответствующей роли — на старте он
делает handshake и логирует подтверждённые роль, версию прошивки и UID.
USB CDC теперь несёт бинарный протокол, а не текстовый лог.
pio device monitorпокажет «мусор» — это нормально. Отладочный текст идёт кадрамиLOG, которые печатаетbridge.py.
Кадр поверх 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.