cart: общий бинарный протокол связи поверх USB CDC
Единый слой связи пульт<->ПК<->тележка, одинаковый на обеих ролях и
зеркалимый на Python.
- link_protocol.h: единственный источник истины — кадрирование
(SYNC/VER/TYPE/LEN/PAYLOAD/CRC16-CCITT), типы CONTROL/IDENT/HELLO/LOG,
layout полезных нагрузок, роли, тайминги failsafe.
- usb_link.{h,cpp}: CRC16, сериализация CONTROL/IDENT, отправка кадров и
стейт-машина приёма с ресинхронизацией по синхрослову и проверкой CRC.
- device_id.h: чтение 96-битного UID чипа для IDENT.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
// 96-битный уникальный идентификатор чипа STM32G431 (UID base 0x1FFF7590).
|
||||
// Используется в IDENT, чтобы ПК отличал конкретные экземпляры устройств.
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
static inline void cartReadUid(uint8_t uid[12]) {
|
||||
memcpy(uid, (const void *)0x1FFF7590UL, 12);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Единый источник истины для бинарного протокола связи пульт<->ПК<->тележка.
|
||||
// Эти же числа зеркалятся в tools/link_proto.py — при любой правке меняй ОБА
|
||||
// файла и сверяйся тест-векторами (tools/link_proto.py --selftest).
|
||||
//
|
||||
// Кадр поверх потока (USB CDC и полезная нагрузка UDP используют общий payload):
|
||||
// [SYNC0][SYNC1][VER][TYPE][LEN][PAYLOAD..LEN][CRC16_LO][CRC16_HI]
|
||||
// CRC16-CCITT (poly 0x1021, init 0xFFFF) по байтам [VER..конец PAYLOAD], LE.
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
// --- кадрирование ---
|
||||
static const uint8_t LINK_SYNC0 = 0xA5;
|
||||
static const uint8_t LINK_SYNC1 = 0x5A;
|
||||
static const uint8_t LINK_VERSION = 1; // версия протокола, проверяется в handshake
|
||||
static const uint8_t LINK_MAX_PAYLOAD = 64;
|
||||
|
||||
// --- типы кадров ---
|
||||
enum LinkType : uint8_t {
|
||||
LINK_HELLO = 0x03, // ПК -> STM: запрос идентификации (payload пуст)
|
||||
LINK_IDENT = 0x02, // STM -> ПК: ответ на HELLO (LinkIdent)
|
||||
LINK_CONTROL = 0x01, // поток управления в обе стороны (LinkControl)
|
||||
LINK_LOG = 0x04, // STM -> ПК: отладочный ASCII-текст (вместо голого Serial)
|
||||
};
|
||||
|
||||
// --- роли устройства (совпадают с продуктовой строкой USB) ---
|
||||
enum LinkRole : uint8_t {
|
||||
ROLE_TX = 'T', // к STM подключён пульт
|
||||
ROLE_RX = 'R', // STM гонит SBUS на драйвер колёс
|
||||
};
|
||||
|
||||
// --- флаги в LinkControl.flags ---
|
||||
static const uint8_t CTRL_FLAG_FAILSAFE = 0x01; // источник объявил безопасный стоп
|
||||
|
||||
// Полезная нагрузка CONTROL (LE на проводе). Это общая «валюта» всей цепочки:
|
||||
// TX кладёт сюда ровно те ch[], что уходят на его локальный провод, ПК
|
||||
// перекладывает payload в UDP и обратно, RX распаковывает и пакует в SBUS.
|
||||
// Пакуется/распаковывается вручную (без struct-паддинга) в link_pack.*.
|
||||
struct LinkControl {
|
||||
uint32_t seq; // монотонный счётчик кадров источника
|
||||
uint32_t t_ms; // millis() на источнике (оценка задержки/джиттера)
|
||||
uint8_t flags; // CTRL_FLAG_*
|
||||
uint16_t ch[16]; // значения SBUS-каналов 0..2047
|
||||
};
|
||||
static const uint8_t LINK_CONTROL_LEN = 4 + 4 + 1 + 2 * 16; // 41 байт
|
||||
|
||||
// Полезная нагрузка IDENT.
|
||||
struct LinkIdent {
|
||||
uint8_t role; // LinkRole
|
||||
uint8_t proto_ver; // LINK_VERSION на устройстве
|
||||
uint16_t fw_ver; // версия прошивки
|
||||
uint8_t uid[12]; // 96-битный UID чипа
|
||||
};
|
||||
static const uint8_t LINK_IDENT_LEN = 1 + 1 + 2 + 12; // 16 байт
|
||||
|
||||
static const uint16_t CART_FW_VERSION = 0x0100; // 1.00
|
||||
|
||||
// --- тайминги (мс) ---
|
||||
static const uint32_t FRAME_PERIOD_MS = 50; // 20 Гц, темп старого пульта
|
||||
static const uint32_t RX_USB_TIMEOUT_MS = 150; // нет CONTROL дольше -> нейтраль на проводе
|
||||
@@ -0,0 +1,148 @@
|
||||
#include "usb_link.h"
|
||||
#include <Arduino.h>
|
||||
#include <string.h>
|
||||
|
||||
uint16_t linkCrc16(const uint8_t *data, uint16_t len) {
|
||||
uint16_t crc = 0xFFFF;
|
||||
for (uint16_t i = 0; i < len; i++) {
|
||||
crc ^= (uint16_t)data[i] << 8;
|
||||
for (int b = 0; b < 8; b++)
|
||||
crc = (crc & 0x8000) ? (uint16_t)((crc << 1) ^ 0x1021) : (uint16_t)(crc << 1);
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
// ---- сериализация payload (little-endian, без struct-паддинга) ----
|
||||
static void put_u16(uint8_t *p, uint16_t v) { p[0] = v & 0xFF; p[1] = v >> 8; }
|
||||
static void put_u32(uint8_t *p, uint32_t v) {
|
||||
p[0] = v & 0xFF; p[1] = (v >> 8) & 0xFF; p[2] = (v >> 16) & 0xFF; p[3] = (v >> 24) & 0xFF;
|
||||
}
|
||||
static uint16_t get_u16(const uint8_t *p) { return (uint16_t)p[0] | ((uint16_t)p[1] << 8); }
|
||||
static uint32_t get_u32(const uint8_t *p) {
|
||||
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
|
||||
}
|
||||
|
||||
static uint8_t packControl(const LinkControl *c, uint8_t *out) {
|
||||
put_u32(out + 0, c->seq);
|
||||
put_u32(out + 4, c->t_ms);
|
||||
out[8] = c->flags;
|
||||
for (int i = 0; i < 16; i++)
|
||||
put_u16(out + 9 + 2 * i, c->ch[i]);
|
||||
return LINK_CONTROL_LEN;
|
||||
}
|
||||
static void unpackControl(const uint8_t *in, LinkControl *c) {
|
||||
c->seq = get_u32(in + 0);
|
||||
c->t_ms = get_u32(in + 4);
|
||||
c->flags = in[8];
|
||||
for (int i = 0; i < 16; i++)
|
||||
c->ch[i] = get_u16(in + 9 + 2 * i);
|
||||
}
|
||||
|
||||
// ---- отправка кадра ----
|
||||
static void sendFrame(uint8_t type, const uint8_t *payload, uint8_t len) {
|
||||
uint8_t buf[3 + LINK_MAX_PAYLOAD + 2];
|
||||
buf[0] = LINK_VERSION;
|
||||
buf[1] = type;
|
||||
buf[2] = len;
|
||||
if (len)
|
||||
memcpy(buf + 3, payload, len);
|
||||
uint16_t crc = linkCrc16(buf, 3 + len);
|
||||
uint8_t hdr[2] = {LINK_SYNC0, LINK_SYNC1};
|
||||
Serial.write(hdr, 2);
|
||||
Serial.write(buf, 3 + len);
|
||||
uint8_t crcb[2] = {(uint8_t)(crc & 0xFF), (uint8_t)(crc >> 8)};
|
||||
Serial.write(crcb, 2);
|
||||
}
|
||||
|
||||
void usbSendControl(const LinkControl *c) {
|
||||
uint8_t pl[LINK_CONTROL_LEN];
|
||||
uint8_t n = packControl(c, pl);
|
||||
sendFrame(LINK_CONTROL, pl, n);
|
||||
}
|
||||
|
||||
void usbSendIdent(const LinkIdent *id) {
|
||||
uint8_t pl[LINK_IDENT_LEN];
|
||||
pl[0] = id->role;
|
||||
pl[1] = id->proto_ver;
|
||||
put_u16(pl + 2, id->fw_ver);
|
||||
memcpy(pl + 4, id->uid, 12);
|
||||
sendFrame(LINK_IDENT, pl, LINK_IDENT_LEN);
|
||||
}
|
||||
|
||||
void usbSendLog(const char *msg) {
|
||||
uint8_t len = (uint8_t)strnlen(msg, LINK_MAX_PAYLOAD);
|
||||
sendFrame(LINK_LOG, (const uint8_t *)msg, len);
|
||||
}
|
||||
|
||||
void usbLinkInit() {
|
||||
Serial.begin(115200);
|
||||
}
|
||||
|
||||
// ---- стейт-машина приёма ----
|
||||
enum RxState { WAIT_S0, WAIT_S1, READ_HDR, READ_PAYLOAD, READ_CRC };
|
||||
static RxState s_state = WAIT_S0;
|
||||
static uint8_t s_hdr[3]; // VER, TYPE, LEN
|
||||
static uint8_t s_hdrGot;
|
||||
static uint8_t s_payload[LINK_MAX_PAYLOAD];
|
||||
static uint8_t s_payGot;
|
||||
static uint8_t s_crc[2];
|
||||
static uint8_t s_crcGot;
|
||||
|
||||
static void resync() { s_state = WAIT_S0; s_hdrGot = s_payGot = s_crcGot = 0; }
|
||||
|
||||
void usbLinkPoll(LinkRxEvent *ev) {
|
||||
ev->type = 0;
|
||||
while (Serial.available() > 0) {
|
||||
uint8_t b = (uint8_t)Serial.read();
|
||||
switch (s_state) {
|
||||
case WAIT_S0:
|
||||
if (b == LINK_SYNC0) s_state = WAIT_S1;
|
||||
break;
|
||||
case WAIT_S1:
|
||||
s_state = (b == LINK_SYNC1) ? READ_HDR : WAIT_S0;
|
||||
s_hdrGot = 0;
|
||||
break;
|
||||
case READ_HDR:
|
||||
s_hdr[s_hdrGot++] = b;
|
||||
if (s_hdrGot == 3) {
|
||||
// s_hdr: VER, TYPE, LEN
|
||||
if (s_hdr[0] != LINK_VERSION || s_hdr[2] > LINK_MAX_PAYLOAD) {
|
||||
resync(); // неверная версия/длина — сдвиг и поиск заново
|
||||
break;
|
||||
}
|
||||
s_payGot = 0;
|
||||
s_state = (s_hdr[2] == 0) ? READ_CRC : READ_PAYLOAD;
|
||||
}
|
||||
break;
|
||||
case READ_PAYLOAD:
|
||||
s_payload[s_payGot++] = b;
|
||||
if (s_payGot == s_hdr[2])
|
||||
s_state = READ_CRC;
|
||||
break;
|
||||
case READ_CRC:
|
||||
s_crc[s_crcGot++] = b;
|
||||
if (s_crcGot == 2) {
|
||||
uint8_t chk[3 + LINK_MAX_PAYLOAD];
|
||||
chk[0] = s_hdr[0]; chk[1] = s_hdr[1]; chk[2] = s_hdr[2];
|
||||
memcpy(chk + 3, s_payload, s_hdr[2]);
|
||||
uint16_t want = (uint16_t)s_crc[0] | ((uint16_t)s_crc[1] << 8);
|
||||
uint16_t got = linkCrc16(chk, 3 + s_hdr[2]);
|
||||
uint8_t type = s_hdr[1];
|
||||
uint8_t len = s_hdr[2];
|
||||
resync();
|
||||
if (want == got) {
|
||||
if (type == LINK_CONTROL && len == LINK_CONTROL_LEN) {
|
||||
unpackControl(s_payload, &ev->control);
|
||||
ev->type = LINK_CONTROL;
|
||||
return;
|
||||
} else if (type == LINK_HELLO || type == LINK_IDENT || type == LINK_LOG) {
|
||||
ev->type = type;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// битый CRC или неизвестный тип — молча ищем следующий кадр
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Бинарный протокол поверх USB CDC (Serial): кадрирование, CRC16, сериализация
|
||||
// CONTROL/IDENT/LOG и стейт-машина приёма с ресинхронизацией. Общий для TX и RX.
|
||||
#pragma once
|
||||
#include "link_protocol.h"
|
||||
|
||||
// Результат опроса входящего потока.
|
||||
struct LinkRxEvent {
|
||||
uint8_t type; // 0 = в этот вызов целого кадра нет; иначе LinkType
|
||||
LinkControl control; // валидно при type == LINK_CONTROL
|
||||
};
|
||||
|
||||
void usbLinkInit();
|
||||
|
||||
// Разобрать доступные байты. Возвращает максимум один полный валидный кадр за
|
||||
// вызов (type != 0). Вызывать в цикле, пока event.type != 0, затем дальше.
|
||||
void usbLinkPoll(LinkRxEvent *ev);
|
||||
|
||||
void usbSendControl(const LinkControl *c);
|
||||
void usbSendIdent(const LinkIdent *id);
|
||||
void usbSendLog(const char *msg);
|
||||
|
||||
// Общий CRC16-CCITT (poly 0x1021, init 0xFFFF). Экспортирован для тестов.
|
||||
uint16_t linkCrc16(const uint8_t *data, uint16_t len);
|
||||
Reference in New Issue
Block a user