
У нас в отделе 12 разных бинарных протоколов, мы решили сесть и разработать один универсальный протокол. Теперь у нас в отделе 13 разных бинарных протоколов.
Думаю каждому embedder-у известен этот бородатый анекдот.
В этом тексте я бы хотел рассказать про простой бинарный протокол, который я сам придумал много лет тому назад для всяческих практических нужд при разработке и тестировании приборов на микроконтроллерах. Я назвал этот протокол TBFP (Trivial Binary Frame Protocol).
Протокол TBFP обычно нужен в целях тестирования интерфейсов: BLE, RS485, LoRa, RS232 , GFSK, UWB, CAN, UART, LIN, 1wire , 100 BASE-T1 и т. п. Обычно этот протокол был нужен чисто для временных нагрузочных тестов интерфейсов, проведения испытаний, отладки оборудования, прозвонки кабелей, для тестов на потерю данных и прочее. Не более того. По сути TBFP это временный и кустарный аналог ICMP.
На самом деле я не очень люблю бинарные протоколы. Предпочитаю текстовые CLI-подобные протоколы. Однако CLI не всегда влезает в микроконтроллеры с экстремально малыми ресурсами. Либо есть ограничение на трафик. Поэтому и приходится иметь на низком старте какой-то бинарный протокол для отладки прошивки.
TBFP это master-slave протокол. Общение диалоговое: запрос, ответ.
Теоретический минимум
Пакет - массив байт, в котором прописана бинарная структура известного типа данных
Little-Endian - размещение в памяти переменных младший байтом вперед.
payload - полезные данные, которые передает пакет
Преамбула - константный данные в начале пакета, чтобы приемник мог понять, что вот начинается протокольный пакет. Передовая часть пакета.
Туннелирование - это когда бинарный протокол в области payload передает пакеты самого себя.
Пакетная синхронизация - это способ из потока байт понять, где заканчивается предыдущий пакет, и где начинается следующий пакет. В реальном времени.
Требование к протоколу
--протокол должен быть бинарным
--протокол должен быть простым до предела. Чтобы просто нечему было ломаться.
--У пакета должна быть преамбула (синхробайты)
--Преамбула должна быть параметризируемая
--Должен быть порядковый номер пакета (Sequence Number). Разрядностью как минимум 16 бит.
--Должно быть поле, которое отвечает за длину полезных данных
--Многобайтовые поля в заголовках следует передавать в формате Little Endian
--В пакете должна быть контрольная сумма CRC
--Контрольная сумма должна быть в конце пакета
--Должен быть идентификатор типа полезных данных
--Должно быть подтверждение принятого пакета. Которое можно и отключить.
--Должна быть повторная отправка в случае отсутствия подтверждения (ReTx). Которую можно отключить
--Пакетная синхронизация должна производится по преамбуле
--Пакетная синхронизация должна производится также по time-stamp(у)
--Должен быть периодический Hello пакет для каждой node(ы) (keep alive messages)
--Сторожевой таймер на потерю соединения
--Протокол должен позволять обновлять прошивку, перезагружать плату, настраивать RTC, читать-писать физическую память, передавать пакеты самого себя и прочее. В общем позволять делать с электронной платой абсолютно всё.
Почему требования кристаллизировались именно такие можно почитать в тексте тут. Теперь обо всем по порядку...
Ядром любого протокола является структура пакета. Я постарался сделать так, чтобы структура пакета не менялась слишком уж сильно, как в случае с UDS.
1) Преамбула (1 байт)
Преамбула нужна, чтобы программный конечный автомат приема мог выхватывать пакеты из потока байтов. Чтобы на принимаемой стороне прошивка могла выхватить начало пакета. Поэтому в структуре кадра заложена преамбула. Желательно, чтобы значение преамбулы было уникальное. Благодаря преамбуле можно выцепить кандидатов в пакеты из потока случайных байт. Преамбула позволяет не проверять CRC по каждому смещению, а отбросить те данные, где точно не может быть начала пакета.
При этом преамбулу следует параметризировать так как должна быть предусмотрена возможность туннелирования пакетов одного и того же протокола. То есть матрешка TBFP пакетов из одного и того же протокола. Это особенно полезно когда физика трансивера за раз может отправить только N байт (256 байт), а сам пакет, например, M=2N байт ( или 1024 байт). При этом получается N < M. В этом случае маленькими TBFP пакетами передается большой TBFP пакет чисто как поток байтов, только уже в payload-е.
2) Флаги пакета (1 байт)
Каждый пакет имеет поле флагов
Битовое поле |
Размер |
Биты |
Пояснение |
lifetime |
4 |
3-0 |
Время жизни |
reserved |
1 |
5 |
Зарезервировано |
response |
1 |
4 |
Этот бит говорит, что это ответный пакет |
crc8_check_need |
1 |
6 |
Нужно ли проверять CRC |
ack_need |
1 |
7 |
Нужно ли отвечать что принял |
Битовое поле lifetime нужно только в тех случаях, когда интерфейсом является какой-то беспроводной интерфейс. И то не всегда. Например LoRa, UWB, BLE, GFSK, IR и т.п. При положительном lifetime устройство должно ретранслировать пакет уменьшив lifetime на единицу. Это позволяет кратно увеличить дальнобойность радиосвязи. В случае проводных интерфейсов lifetime надо просто обнулить.
Бит ACK нужен для беспроводных интерфейсов. Это повысит надежность передачи данных.
3) Номер пакета SN (2 байта)
ISO26262 требует контроль приема пакетов. Это достигается за счет того, что пакеты непрерывно нумеруются. Таким образом на принимаемой стороне можно зарегистрировать факт потери данных .
Чтобы не тратить всуе лишнее процессороне время и не писать лишний код на разворот байтов это многобайтовое поле следует передавать в формате little-endian. Так программа будет просто быстрее работать, а устройство тратить меньше электричества.
4) Размер полезной нагрузки (2 байта)
Надо указать сколько байт следует ожидать до появления контрольной суммы. Для этого в заголовке пакета есть явное указание размера полезных данных. Это позволит сделать протокол универсальным и сделает возможность передавать данные разной длинны.
5) Идентификатор полезной нагрузки (1 байт)
Программе на той стороне надо как-то дать понять, что делать с данными в области payload. Поэтому есть отдельный байт который скажет как интерпретировать принятые данные.
payload_id |
Пояснение |
0x01 |
Прыгнуть исполнять код по адресу, который указан в payload |
0xFC |
Пакет для чтения или записи памяти |
0x41 |
Пакет подтверждения |
0x43 |
Пакет передачи текста |
0x44 |
Пакет передачи команды для CLI |
0x54 |
Внутри такой же TBFP пакет. Матрёшка. |
0x91 |
Внутри структура нажатой в клавиатуре кнопки |
0xD3 |
RTCM3 GNSS поправки |
0x67 |
внутри DECAWAVE пакет |
0x51 |
Ping пакет |
0x90 |
ответный pong пакет для команды ping |
.... |
остальные значения зарезервированы |
6) Непосредственно полезные данные (от нуля до 0xFFFF байт)
Так как поле задающее размер имеет разрядность 16 бит, то максимум можно передать 65535 байт. Ограничение задается только размером RAM памяти на принимаемой стороне. Этого более чем достаточно для микроконтроллерных прошивок. При этом допускаются и пакеты с нулевым значением payload. Такие пакеты имеют размер всего 8 байт, что позволяет их уместить в одно CAN сообщение. Пакеты нулевого payload можно использовать как раз для hi-load тестов на предмет потери отдельных промежуточных пакетов и нарушения непрерывности потока данных.
7) Контрольная сумма CRC8 (1 байт)
Чтобы защитить данные добавлена контрольная сумма. Как ее вычилсять решать Вам. Есть множество алгоритмов вычисления CRC8. Благодаря CRC на принимаемой стороне программа сможет выявить факт повреждения данных по пути от передатчика к приемнику. CRC не случайно помещена в конец. Это позволит проще вычислять СRС, захватывая как данные, так и сам заголовок.
Алгоритмы поведения протокола TBFP
Мастер должен периодически опрашивать каждую Node(у) даже если нет PayLoad для этой Node(ы). (Требование ISO-26262). Это позволит убедиться, что есть link. То есть, что провода не оторвались, разъёмы не расшатались, софт не завис и тому подобное. В случае пропадания link-а сгенерировать событие аварии и предложить предпринять какие-никаеие меры по ремонту сети. Это называется blink пакеты. Ещё называют Hello пакеты или heart beat пакеты.
На стороне приемника каждый раз, когда приходит любой TBFP пакет надо обнулять программный сторожевой таймер, который работает для этого конкретного соединения. Этот таймер считает вверх. Если сторожевой таймер досчитал до определенного таймаута (условно 10 секунд), то надо выдать в главную консоль управления предупреждение, что возможно что-то не так с link(ом).
Пакетную синхронизацию делать по time-out-у. То есть после продолжительного молчания на шине приемник просто сбрасывает конечный автомат приема в первоначальное состояние и ожидает новой преамбулы от нового пакета.
Достоинства
++TBFP это простой переносимый протокол, который можно легко реализовать на чистом Си на любом микроконтроллере даже в условиях экстремальной нехватки flash памяти программ.
++Структура пакета не меняется от того какие задачи он решает. Даже ответный пакет имеет ту же самую структуру.
Недостатки
--Короткая преамбула. Могут возникать ложные срабатывания при синтаксическом разборе пакетов из потока байт
--Не хватает поля версии самого протокола. Чтобы обеспечить связь со старым оборудованием.
--Нет шифрования.
Итог
Удалось придумать простой бинарный протокол для тестировочных целей при разработке прошивок микроконтроллеров.
Словарь
Сокращение |
Расшифровка |
CRC |
Cyclic redundancy check |
RTC |
real-time clock |
CLI |
Command-line interface |
TBFP |
Trivial Binary Frame Protocol |
CAN |
controller area network |
Источники
Вопросы
--В каких бинарных протоколах есть поле порядковый номер пакета разрядностью минимум 16 бит?
--В каких бинарных протоколах есть порядковый номер передаваемого пакета?
--Существую ли бинарные протоколы реализованные аппаратно?
Комментарии (20)

NightShad0w
25.11.2025 20:45Чтобы не тратить всуе лишнее процессороне время и не писать лишний код на разворот байтов это многобайтовое поле следует передавать в формате little-endian.
А микроконтроллеры чаще little-endian? Контринтуитивно передавать куда-то байты не в сетевом порядке байт.

aabzel Автор
25.11.2025 20:45Да. Arm Cortex -m little endian. Хотя компилятор gcc имеет ключи переключения на big endian. Однако я их ни разу не проверял.

randomsimplenumber
25.11.2025 20:45Не совсем понятно, зачем для тестирования интерфейсов свой собственный diy протокол.

aabzel Автор
25.11.2025 20:45Всё очень просто. Есть беспроводной интерфейс (например LoRa) и драйвер интерфейса взятый из интернета.
Надо проверить как ведет себя реализация на Hi-load.
Классический тест. Посылать сплошной поток пакетов с увеличением порядкового номера пакетов. Оставить на 24 часа.
Через сутки прийти и проверить убедиться, что на принимающей стороне не потерялось ни одного пакета.
Вот и нужен протокол с 16битным SN пакета.
randomsimplenumber
25.11.2025 20:45printf(счетчик, crc)

aabzel Автор
25.11.2025 20:45Выглядит, как hardcode.
Ну, Ок, однако надо еще и команду на reboot как-то посылать.
randomsimplenumber
25.11.2025 20:45По недоверенному каналу, который как раз тестируется на предмет потерь, странно передавать команды.

vmx
25.11.2025 20:45В каких бинарных протоколах есть поле порядковый номер пакета разрядностью минимум 16 бит?
В TCP? Там есть 32-битный sequence number.
В каких бинарных протоколах есть порядковый номер передаваемого пакета?
Иногда в "односторонние" UDP-based протоколы добавляют порядковые номера, чтобы понимать сколько пакетов потерялось. В netflow/ipfix есть sequence number.
Существую ли бинарные протоколы реализованные аппаратно?
Какой-то очень сложный вопрос. Сейчас грань между "программно" и "аппаратно" немного размыта, но вообще сетевой стек (Ethernet, IP и даже TCP/UDP) парсится и модифицируется на многих бытовых сетевых карточках "аппаратно": https://en.wikipedia.org/wiki/TCP_offload_engine
Хотя это старая статья, сейчас даже мало кто говорит "tcp offload", обычно это называют "nic offloads".

yappari
25.11.2025 20:45Короткая преамбула. Могут возникать ложные срабатывания при синтаксическом разборе пакетов из потока байт
Для этого придумали Byte stuffing. Чуть сложнее, поэтому следует определиться, что важнее: размер или надёжность выхватывания пакета.

aabzel Автор
25.11.2025 20:45Byte stuffing - очень крутая технология, но информации по ней на русском исчезающе мало.

DrGluck07
25.11.2025 20:45Да вроде даже на википедии всё понятно описано. А ещё на микроконтроллерах раньше любили использовать 9-битную передачу. Тогда признаком начала кадра служил девятый бит. Но мы уже давно так не делаем.

DrGluck07
25.11.2025 20:45По поводу 8-битного CRC. Внутри железки платы общались через I2C. Внезапно раз в несколько секунд на дисплее стали появляться ошибки и неправильные парадоксальные данные. Полезли разбираться, оказалось, что иногда приходят пакеты с правильным CRC, но внутри пакета часть данных явно из другого пакета. В итоге оказалось, что примерно треть пакетов приходит битая с неправильным CRC. Они, естественно, отбрасываются. Но иногда CRC случайно совпадал. На шине передаётся пара сотен пакетов в секунду, часть из них битые, примерно у каждого 1 из 256 совпадает CRC, что логично.
Конечно мы нашли ошибку в работе с буферами на передающей стороне. И количество ошибочных пакетов упало до нуля. Но всё равно решили изменить CRC на 16-битное.
viordash
версия 2 -> preamble 0xA6
версия 3 -> preamble 0xA7
и т.д.
aabzel Автор
Ну конечно! Спасибо. Я бы до такого никогда не додумался.