У нас в отделе 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 байт)

Каждый пакет имеет поле флагов

Битовое поле

Размер
поля, bit

Биты

Пояснение

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)


  1. viordash
    25.11.2025 20:45

    Не хватает поля версии самого протокола. Чтобы обеспечить связь со старым оборудованием

    версия 2 -> preamble 0xA6
    версия 3 -> preamble 0xA7
    и т.д.


    1. aabzel Автор
      25.11.2025 20:45

      Ну конечно! Спасибо. Я бы до такого никогда не додумался.


  1. JBFW
    25.11.2025 20:45

    А если вместо CRC автокоррекцию использовать? Хемминг, Рида-Соломона?Восстановился код - ок, не восстановился - сильно битый.


    1. aabzel Автор
      25.11.2025 20:45

      Это уж слишком сложно.


      1. Danusha0000000
        25.11.2025 20:45

        нейросети скажи сделает эту штуку)) ну что ты как не русский)


  1. NightShad0w
    25.11.2025 20:45

    Чтобы не тратить всуе лишнее процессороне время и не писать лишний код на разворот байтов это многобайтовое поле следует передавать в формате little-endian.

    А микроконтроллеры чаще little-endian? Контринтуитивно передавать куда-то байты не в сетевом порядке байт.


    1. aabzel Автор
      25.11.2025 20:45

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


  1. randomsimplenumber
    25.11.2025 20:45

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


    1. aabzel Автор
      25.11.2025 20:45

      Всё очень просто. Есть беспроводной интерфейс (например LoRa) и драйвер интерфейса взятый из интернета.
      Надо проверить как ведет себя реализация на Hi-load.

      Классический тест. Посылать сплошной поток пакетов с увеличением порядкового номера пакетов. Оставить на 24 часа.

      Через сутки прийти и проверить убедиться, что на принимающей стороне не потерялось ни одного пакета.

      Вот и нужен протокол с 16битным SN пакета.


      1. randomsimplenumber
        25.11.2025 20:45

        printf(счетчик, crc)


        1. aabzel Автор
          25.11.2025 20:45

          Выглядит, как hardcode.
          Ну, Ок, однако надо еще и команду на reboot как-то посылать.


          1. randomsimplenumber
            25.11.2025 20:45

            По недоверенному каналу, который как раз тестируется на предмет потерь, странно передавать команды.


            1. aabzel Автор
              25.11.2025 20:45

              Уж лучше так, чем никак.


    1. aabzel Автор
      25.11.2025 20:45

      Коллеги вообще топят за использование Protobuf


  1. 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".


  1. yappari
    25.11.2025 20:45

    Короткая преамбула. Могут возникать ложные срабатывания при синтаксическом разборе пакетов из потока байт

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


    1. aabzel Автор
      25.11.2025 20:45

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


      1. DrGluck07
        25.11.2025 20:45

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


  1. DrGluck07
    25.11.2025 20:45

    По поводу 8-битного CRC. Внутри железки платы общались через I2C. Внезапно раз в несколько секунд на дисплее стали появляться ошибки и неправильные парадоксальные данные. Полезли разбираться, оказалось, что иногда приходят пакеты с правильным CRC, но внутри пакета часть данных явно из другого пакета. В итоге оказалось, что примерно треть пакетов приходит битая с неправильным CRC. Они, естественно, отбрасываются. Но иногда CRC случайно совпадал. На шине передаётся пара сотен пакетов в секунду, часть из них битые, примерно у каждого 1 из 256 совпадает CRC, что логично.

    Конечно мы нашли ошибку в работе с буферами на передающей стороне. И количество ошибочных пакетов упало до нуля. Но всё равно решили изменить CRC на 16-битное.


  1. Strijar
    25.11.2025 20:45

    Сейчас примерно для таких задач использую старый добрый KISS