Передовой опыт и шаблоны протокола связи

19

Каждый раз, когда я разрабатываю последовательный протокол, который будет использоваться между двумя arduinos, я чувствую, что заново изобретаю колесо. Интересно, есть ли какие-нибудь лучшие практики или образцы, которым следуют люди. Этот вопрос не столько о самом коде, сколько о формате сообщений.

Например, если я хочу сказать, что arduino должен мигать, это первый светодиод 3 раза, который я мог бы отправить:

^L1,F3\n
  • '^': запускает новую команду
  • «L»: определяет команду, (L: направить эту команду на светодиод)
  • «1»: нацелиться на первый светодиод
  • ',': Разделитель командной строки, новое значение в этом сообщении
  • 'F': подкоманда Flash
  • «3»: 3 раза (мигать светодиод три раза)
  • '\ n': завершить команду

Мысли? Как вы обычно подходите к написанию нового последовательного протокола? А что, если я хочу отправить запрос из arduino 1 в arduino 2, а затем получить ответ?

Джереми Гиллик
источник

Ответы:

13

Существует множество способов написания последовательного протокола в зависимости от того, какие функции вы хотите использовать, и сколько проверки ошибок вам нужно.

Вот некоторые из общих вещей, которые вы видите в протоколах точка-точка:

Конец сообщения

Простейшие протоколы ASCII просто имеют конец символьной последовательности сообщения, часто \rили так, \nкак это печатается при нажатии клавиши ввода. Двоичные протоколы могут использовать 0x03или какой-либо другой общий байт.

Начало сообщения

Проблема только с окончанием сообщения состоит в том, что вы не знаете, какие еще байты уже были получены при отправке сообщения. Эти байты будут затем добавлены к сообщению и будут неправильно интерпретированы. Например, если Arduino только что проснулся, в последовательном буфере может быть мусор. Чтобы обойти это, у вас есть начало последовательности сообщений. В вашем примере ^в бинарных протоколах часто0x02

Проверка ошибок

Если сообщение может быть повреждено, нам нужна проверка ошибок. Это может быть контрольная сумма или ошибка CRC или что-то еще.

Побег персонажей

Возможно, контрольная сумма добавляет контрольный символ, такой как байт «начало сообщения» или «конец сообщения», или сообщение содержит значение, равное контрольному символу. Решение состоит в том, чтобы ввести escape-символ. Экранирующий символ помещается перед модифицированным управляющим символом, поэтому фактический управляющий символ отсутствует. Например, если начальный символ 0x02, используя escape-символ 0x10, мы можем отправить значение 0x02 в сообщении в виде пары байтов 0x10 0x12 (управляющий символ байта XOR)

Номер пакета

Если сообщение повреждено, мы можем запросить повторную отправку с помощью сообщения nack или повторить попытку, но если было отправлено несколько сообщений, то только последнее сообщение может быть повторно отправлено. Вместо этого пакету может быть присвоен номер, который переворачивается после определенного количества сообщений. Например, если это число равно 16, передающее устройство может сохранить последние 16 отправленных сообщений, и, если они были повреждены, принимающее устройство может запросить повторную отправку с использованием номера пакета.

длина

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

Специфичный для Arduino

При разработке протокола для Arduino первое соображение заключается в том, насколько надежен канал связи. Если вы отправляете данные по большинству беспроводных сред, XBee, WiFi и т. Д., В них уже встроены средства проверки ошибок и повторных попыток, и поэтому нет смысла вставлять их в ваш протокол. Если вы отправляете через RS422 на пару километров, это будет необходимо. Вещи, которые я бы включил, это начало и конец сообщения, как у вас. Моя типичная реализация выглядит примерно так:

>messageType,data1,data2,…,dataN\n

Разделение частей данных запятой позволяет легко выполнять синтаксический анализ, и сообщение отправляется с использованием ASCII. Протоколы ASCII хороши тем, что вы можете вводить сообщения в последовательный монитор.

Если вам нужен двоичный протокол, возможно, для сокращения размеров сообщений, вам придется реализовать экранирование, если байт данных может совпадать с байтом управления. Двоичные управляющие символы лучше подходят для систем, где требуется полный спектр проверки ошибок и повторных попыток. При желании полезная нагрузка может быть ASCII.

geometrikal
источник
Разве не возможно, что мусор перед реальным началом кода сообщения может содержать начало кода управления сообщениями? Как бы вы справились с этим?
CMCDragonkai
@CMCDragonkai Да, это возможно, особенно для однобайтовых управляющих кодов. Однако, если вы встретите управляющий код запуска в середине анализа сообщения, сообщение отбрасывается и анализ перезапускается.
геометрикал
9

У меня нет никакой формальной экспертизы по последовательным протоколам, но я использовал их довольно много раз, и более или менее остановился на этой схеме:

(Заголовок пакета) (байт идентификатора) (данные) (контрольная сумма fletcher16) (нижний колонтитул пакета)

Я обычно делаю 2 байта заголовка и 1 байт нижнего колонтитула. Мой парсер сбросит все, когда увидит новый заголовок пакета, и попытается проанализировать сообщение, если увидит нижний колонтитул. Если контрольная сумма не пройдена, сообщение не будет отброшено, но продолжится добавление до тех пор, пока не будет найден символ нижнего колонтитула и контрольная сумма завершится успешно. Таким образом, нижний колонтитул должен быть только один байт, поскольку коллизии не нарушают сообщение.

Идентификатор является произвольным, иногда с длиной секции данных, являющейся нижним полубайтом (4 бита). Можно использовать второй бит длины, но я обычно не беспокоюсь, поскольку для правильного анализа не требуется знать длину, поэтому просмотр правильной длины для данного идентификатора является еще одним подтверждением правильности сообщения.

Контрольная сумма fletcher16 представляет собой 2-байтовую контрольную сумму почти такого же качества, что и CRC, но ее гораздо проще реализовать. некоторые детали здесь . Код может быть таким простым:

for(int i=0; i < bufSize; i++ ){
   sum1 = (sum1 + buffer[i]) % 255;
   sum2 = (sum2 + sum1) % 255;
}
uint16_t checksum = (((uint16_t)sum1)<<8) | sum2;

Я также использовал систему вызова и ответа для критических сообщений, где ПК будет отправлять сообщение каждые 500 мс или около того, пока не получит сообщение OK с контрольной суммой всего исходного сообщения в качестве данных (включая исходную контрольную сумму).

Эта схема, конечно, не очень подходит для ввода в терминале, как это было бы в вашем примере. Ваш протокол кажется довольно хорошим для того, чтобы быть ограниченным ASCII, и я уверен, что это проще для быстрого проекта, который вы хотите иметь возможность непосредственно читать и отправлять сообщения. Для больших проектов хорошо иметь плотность двоичного протокола и безопасность контрольной суммы.

BrettAM
источник
Так как "[ваш] парсер сбросит все, когда увидит новый заголовок пакета", мне интересно, не создает ли это проблем, если случайно заголовок встречается внутри данных?
человечествоANDpeace
@humanityANDpeace Причиной отбрасывания является то, что когда пакет обрезается, он никогда не будет правильно анализироваться, поэтому, когда вы решаете его мусор и идете дальше? Самое простое, и, по моему опыту, достаточно хорошее решение - отбросить плохой пакет, как только появится следующий заголовок. Я использовал 16-битный заголовок без проблем, но вы можете сделать его длиннее, если определенность важнее, чем пропускная способность.
BrettAM
Так что то, что вы называете заголовком, является чем-то вроде магической 16-битной комбинации. т.е. 010101001 10101010, верно? Я согласен, что изменение только 1/256 * 256, но оно также отключает использование этого 16-битного кода внутри ваших данных, иначе это будет неправильно интерпретировано как заголовок, и вы отбрасываете сообщение, верно?
человечествоANDpeace
@humanityANDpeace Я знаю, что это год спустя, но вам нужно ввести последовательность побега. Перед отправкой сервер проверяет полезные данные на наличие специальных байтов, а затем экранирует их другим специальным байтом. Клиентская сторона, вы должны собрать исходную полезную нагрузку вместе. Это означает, что вы не можете отправлять пакеты фиксированной длины и усложняет реализацию. Есть много стандартов последовательных протоколов, которые можно выбрать, чтобы решить эту проблему. Вот очень хорошее чтение по теме .
RubberDuck
1

Если вы знакомы со стандартами, вы можете взглянуть на кодировку ASN.1 / BER TLV. ASN.1 - это язык, используемый для описания структур данных, созданных специально для связи. BER - это метод TLV для кодирования данных, структурированных с использованием ASN.1. Проблема в том, что кодировка ASN.1 может быть в лучшем случае хитрой. Создание полноценного компилятора ASN.1 - это проект сам по себе (и особенно сложный, думаю, месяцы ).


Вероятно, лучше сохранить только структуру TLV. TLV в основном состоит из трех элементов: тега, длины и поля значения. Тег определяет тип данных (текстовая строка, строка октета, целое число и т. Д.) И длину и длину значения .

В BER T также обозначает, является ли значение набором структур TLV (сам построенный узел) или непосредственно значением (примитивный узел). Таким образом, вы можете создать дерево в двоичном виде, очень похожее на XML (но без дополнительных издержек XML).

Пример:

TT LL VV
02 01 FF

является целым числом (тегом 02) с длиной значения 1 (длина 01) и значения -1 (значение FF). В ASN.1 / BER целые числа являются знаковыми большими порядковыми номерами, но вы, конечно, можете использовать свой собственный формат.

TT LL (TT LL VV, TT LL VV VV)
30 07  02 01 FF  02 02 00 FF

является последовательностью (списком) длиной 7, содержащей два целых числа, одно со значением -1, а другое со значением 255. Два целых кодирования вместе составляют значение последовательности.

Вы можете просто добавить это в онлайн-декодер, не так ли?


Вы также можете использовать неопределенную длину в BER, что позволит вам передавать данные. В этом случае вам нужно правильно проанализировать ваше дерево. Я бы посчитал это продвинутой темой, вам нужно сначала знать ширину и разбор глубины, например.


Использование схемы TLV позволяет вам думать о любой структуре данных и кодировать ее. ASN.1 идет намного дальше, предоставляя вам уникальные идентификаторы (OID), варианты выбора (во многом как C-объединения), включения других структур ASN.1 и т. Д. И т. Д., Но это может быть излишним для вашего проекта. Вероятно, лучшими из известных сегодня структур, определенных ASN.1, являются сертификаты, используемые вашим браузером.

Мартен Бодьюс
источник
0

Если нет, у вас есть основы покрыты. Ваши команды могут создавать и читать как люди, так и машины, что является большим плюсом. Вы можете добавить контрольную сумму, чтобы обнаружить неправильно сформированную команду или команду, поврежденную при передаче, особенно если ваш канал содержит длинный кабель или радиоканал.

Если вам нужна промышленная мощь (ваше устройство не должно причинять или позволять кому-либо пострадать или умереть; вам нужны высокие скорости передачи данных, устранение неисправностей, обнаружение пропущенных пакетов и т. Д.), Тогда посмотрите некоторые стандартные протоколы и методы проектирования.

JRobert
источник