1. Тема дня
Проектируем бинарный протокол поверх UART, RS-485, TCP, UDP, CAN FD, MQTT binary payload и HIL-связи с Raspberry Pi. Основные темы:
framing
COBS / delimiter
length
CRC
endianness
versioning
TLV
sequence/request_id
stream parser
resynchronizationГлавная мысль: транспортный read() не равен сообщению. TCP и UART дают поток байтов, поэтому сообщение нужно сначала выделить из потока, проверить границы, длины и CRC, и только потом отдавать application layer.
2. Зачем это нужно в проектах
Для HIL Raspberry Pi может отправлять:
SET_INPUT A03 HIGH
INJECT_MODEM_URC
READ_DIAGNOSTICS
RESET_FAULT_COUNTERS
START_TEST
GET_TEST_RESULTТекстовый CLI удобен человеку, но бинарный протокол удобнее для автотестов:
- фиксированные типы;
- меньше объем;
- sequence/correlation ID;
- CRC;
- строгие лимиты;
- проще fuzzing.
Для удаленных модулей входов бинарный протокол может передавать:
raw input mask
confirmed phase
ADC samples
conflict flags
sequence
measurement timestamp
power statusЕсли один байт потерян, parser должен отбросить испорченный кадр и восстановить синхронизацию на следующем.
3. Теория
Уровни протокола
transport adapter
UART/TCP/UDP/CAN
frame decoder
delimiter, COBS, length, CRC
message decoder
version, type, TLV, schema
application service
authorization, execution, state changesFrame parser не должен включать GPIO, MQTT, NVS или reset.
Предлагаемый формат
Для UART/RS-485:
COBS(encoded logical frame) + 0x00 delimiterПосле COBS-декодирования:
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-структуры напрямую
Плохо:
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.
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
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.
Type: uint16 big-endian
Length: uint16 big-endian
Value: Length bytesМожно зарезервировать старший бит Type:
type & 0x8000 == 0 -> optional unknown TLV can be skipped
type & 0x8000 != 0 -> critical unknown TLV rejects message4. Типичные ошибки
- Один 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:
# 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Реализуй модули:
cobs.c / cobs.h
protocol_frame.c / protocol_frame.h
protocol_stream.c / protocol_stream.hUnit-тесты:
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 при подписи.
Критерии: Framing и проверка должны предшествовать прикладным side effects.
Задание
Повреждённый UART-кадр сопровождается корректным. Опишите ожидаемый результат parser и два теста восстановления без вызова прикладной логики для повреждённого кадра.
Критерии самопроверки: Укажите ограниченные проверки, resync и одинаковый результат при разных границах chunk. Отказ CRC должен предшествовать dispatch.
Показать ответ автора
Отбросить повреждённый кадр, восстановить синхронизацию на следующей границе и принять следующий корректный кадр после проверок длины/CRC. Передать пару одним feed(), затем те же байты несколькими feed(). Dispatch должен получить только корректный кадр.