diff --git a/device_firmware/cart_firmware/src/device_id.h b/device_firmware/cart_firmware/src/device_id.h new file mode 100644 index 0000000..44a7fdf --- /dev/null +++ b/device_firmware/cart_firmware/src/device_id.h @@ -0,0 +1,9 @@ +// 96-битный уникальный идентификатор чипа STM32G431 (UID base 0x1FFF7590). +// Используется в IDENT, чтобы ПК отличал конкретные экземпляры устройств. +#pragma once +#include +#include + +static inline void cartReadUid(uint8_t uid[12]) { + memcpy(uid, (const void *)0x1FFF7590UL, 12); +} diff --git a/device_firmware/cart_firmware/src/link_protocol.h b/device_firmware/cart_firmware/src/link_protocol.h new file mode 100644 index 0000000..767a2f8 --- /dev/null +++ b/device_firmware/cart_firmware/src/link_protocol.h @@ -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 + +// --- кадрирование --- +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 дольше -> нейтраль на проводе diff --git a/device_firmware/cart_firmware/src/usb_link.cpp b/device_firmware/cart_firmware/src/usb_link.cpp new file mode 100644 index 0000000..f27b765 --- /dev/null +++ b/device_firmware/cart_firmware/src/usb_link.cpp @@ -0,0 +1,148 @@ +#include "usb_link.h" +#include +#include + +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; + } + } +} diff --git a/device_firmware/cart_firmware/src/usb_link.h b/device_firmware/cart_firmware/src/usb_link.h new file mode 100644 index 0000000..e6d94a1 --- /dev/null +++ b/device_firmware/cart_firmware/src/usb_link.h @@ -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);