1. Тема дня

Проектируем бинарный протокол поверх UART, RS-485, TCP, UDP, CAN FD, MQTT binary payload и HIL-связи с Raspberry Pi. Основные темы:

text
framing
COBS / delimiter
length
CRC
endianness
versioning
TLV
sequence/request_id
stream parser
resynchronization

Главная мысль: транспортный read() не равен сообщению. TCP и UART дают поток байтов, поэтому сообщение нужно сначала выделить из потока, проверить границы, длины и CRC, и только потом отдавать application layer.

2. Зачем это нужно в проектах

Для HIL Raspberry Pi может отправлять:

text
SET_INPUT A03 HIGH
INJECT_MODEM_URC
READ_DIAGNOSTICS
RESET_FAULT_COUNTERS
START_TEST
GET_TEST_RESULT

Текстовый CLI удобен человеку, но бинарный протокол удобнее для автотестов:

  • фиксированные типы;
  • меньше объем;
  • sequence/correlation ID;
  • CRC;
  • строгие лимиты;
  • проще fuzzing.

Для удаленных модулей входов бинарный протокол может передавать:

text
raw input mask
confirmed phase
ADC samples
conflict flags
sequence
measurement timestamp
power status

Если один байт потерян, parser должен отбросить испорченный кадр и восстановить синхронизацию на следующем.

3. Теория

Уровни протокола

text
transport adapter
  UART/TCP/UDP/CAN
frame decoder
  delimiter, COBS, length, CRC
message decoder
  version, type, TLV, schema
application service
  authorization, execution, state changes

Frame parser не должен включать GPIO, MQTT, NVS или reset.

Предлагаемый формат

Для UART/RS-485:

text
COBS(encoded logical frame) + 0x00 delimiter

После COBS-декодирования:

text
Offset Size Field
0      2    Magic = 0x4550 "EP"
2      1    Protocol major version
3      1    Header length
4      2    Message type
6      2    Flags
8      4    Sequence
12     4    Payload length
16     ...  Optional header extensions
...    N    Payload
...    4    CRC32C

Зачем сразу magic, delimiter, length и CRC:

  • delimiter находит физическую границу кадра;
  • COBS гарантирует отсутствие delimiter внутри payload;
  • magic отсеивает мусор и другие протоколы;
  • length защищает от чтения за буфер;
  • CRC отсеивает поврежденные данные.

Не передавай C-структуры напрямую

Плохо:

c
typedef struct {
    uint8_t version;
    uint32_t sequence;
    uint16_t value;
} packet_t;
uart_write_bytes(UART_NUM_1, &packet, sizeof(packet));

Проблемы: padding, endianness, размер enum, ABI, unaligned access, неинициализированные байты. Правильно: явно писать поля в фиксированном порядке и endian.

c
static void write_u32_be(uint8_t *p, uint32_t value)
{
    p[0] = (uint8_t)(value >> 24U);
    p[1] = (uint8_t)(value >> 16U);
    p[2] = (uint8_t)(value >> 8U);
    p[3] = (uint8_t)value;
}

Безопасный порядок parsing

text
1. Минимальный размер.
2. Magic.
3. Version.
4. Header length.
5. Payload maximum.
6. Точный общий размер.
7. CRC.
8. Flags/schema.
9. Dispatch.

До проверки CRC прикладная логика не вызывается.

TLV

TLV = Type-Length-Value.

text
Type:   uint16 big-endian
Length: uint16 big-endian
Value:  Length bytes

Можно зарезервировать старший бит Type:

text
type & 0x8000 == 0 -> optional unknown TLV can be skipped
type & 0x8000 != 0 -> critical unknown TLV rejects message

4. Типичные ошибки

  • Один uart_read() считается одним кадром.
  • Packed C-структуры используются как wire format.
  • payload_length из сети напрямую идет в malloc().
  • CRC проверяется после разбора payload.
  • После UART overflow parser продолжает текущий кадр.
  • Используется только magic без CRC/length.
  • Length prefix на шумной UART без resync.
  • Minor update меняет смысл старого поля.
  • TLV parser не ограничивает число TLV.
  • Sequence ошибочно считается криптографической защитой.

5. Практическое задание на 30-60 минут

Создай PROTOCOL_SPEC.md:

markdown
# Embedded Protocol v1
## Transport
UART and RS-485:
- COBS encoded
- 0x00 frame delimiter
TCP:
- same COBS stream for initial implementation
UDP:
- one decoded logical frame per datagram
## Limits
Maximum header: 64 bytes
Maximum payload: 1024 bytes
Maximum decoded frame: 1092 bytes
No runtime allocation in parser
## Byte order
All multi-byte integers are big-endian.
## Validation order
1. COBS
2. Minimum length
3. Magic
4. Version
5. Header length
6. Payload limit
7. Exact total length
8. CRC32C
9. Message schema
10. Application authorization

Реализуй модули:

text
cobs.c / cobs.h
protocol_frame.c / protocol_frame.h
protocol_stream.c / protocol_stream.h

Unit-тесты:

text
1. Валидный кадр целиком.
2. Тот же кадр по одному байту.
3. Два кадра одним feed().
4. Пустые delimiters.
5. Измененный byte -> BAD_CRC.
6. Header length < 16 -> reject.
7. Payload length > max -> reject.
8. После ошибочного кадра следующий валидный принимается.
9. Unknown optional TLV пропускается.
10. Unknown critical TLV отвергается.

6. Что почитать или попробовать дальше

  • ESP-IDF UART driver: event queue, RX timeout, FIFO overflow, ring-buffer overflow.
  • STM32 ReceiveToIdle_DMA для UART + DMA.
  • RFC 8949 CBOR, если понадобится более гибкий формат.
  • Protocol Buffers wire format, но учитывать deterministic encoding при подписи.
Один uart_read() возвращает половину кадра, следующий — оставшуюся часть. Какое решение соответствует уроку?

Критерии: Framing и проверка должны предшествовать прикладным side effects.

Задание

Повреждённый UART-кадр сопровождается корректным. Опишите ожидаемый результат parser и два теста восстановления без вызова прикладной логики для повреждённого кадра.

Критерии самопроверки: Укажите ограниченные проверки, resync и одинаковый результат при разных границах chunk. Отказ CRC должен предшествовать dispatch.

Показать ответ автора

Отбросить повреждённый кадр, восстановить синхронизацию на следующей границе и принять следующий корректный кадр после проверок длины/CRC. Передать пару одним feed(), затем те же байты несколькими feed(). Dispatch должен получить только корректный кадр.