Бинарный протокол обмена данными по RS232 BinExchange

Несмотря на свою древность, RS232 и его вариации до сих пор широко используются в различных системах автоматизации и в бытовых приборах. И это все потому, что COM-порт очень прост в освоении. И еще существует большое количество переходников USB-UART, которые позволяют добавить интерфейс USB в свой девайс без мучительного изучения стандарта USB и покупки VID. Однако, встает вопрос о том, каким образом передавать байты информации по последовательному порту. В этой статье мы рассмотрим мое решение данного вопроса, которое называется BinExchange protocol.

Обзор протокола Modbus

Для начала давайте рассмотрим одно из существующих решений, которое является промышленным стандартном, а именно протокол Modbus.

Modbus является пакетным протоколом обмена данными с архитектурой ведущий-ведомый. Modbus в основном используется для создания сетей поверх RS485. Существует 3 варианта Modbus:

  • Modbus ASCII — текстовый вариант протокола, начало пакета помечается символом «:», конец CR/LF. Из достоинств можно выделить простоту реализации. Из минусов — скорость обмена данными в 2 раза ниже в сравнении с двоичной реализацией Modbus RTU, так как на каждый байт приходится 2 ASCII-символа.
  • Modbus RTU — двоичная реализация протокола. Пакеты отделяются друг от друга интервалом тайм-аута, равного не меньше 3,5 символов при данной скорости передачи. Между байтами данных не должно быть пауз, превышающих 1,5 символа, т.е. данные должны идти сплошным потоком. В качестве достоинства можно выделить довольно высокую скорость передачи данных, так как в пакете содержится намного меньше служебной информации, чем в текстовой реализации протокола. Недостаток — сложнее реализовать, в сравнении с Modbus ASCII. Стоит отметить, что в некоторых микроконтроллерах STM32 в модуле UART есть аппаратная поддержка тайм-аутов, которую можно использовать для реализации протокола Modbus RTU.
  • Modbus TCP — Modbus через интернет, в данной статье нас не интересует.

В принципе, для передачи данных между МК и ПК, либо между двумя МК, можно использовать протокол Modbus. Однако, все же это сетевой протокол, и в передаваемых пакетах есть информация об сетевом адресе устройства, что избыточно для случая обмена данными между 2-мя устройствами. Но это не так страшно. Основное неудобство заключается в обмене данными в режиме Ведущий-Ведомый, не совсем удобный способ обмена данными через виртуальные регистры устройства и ограничение на длину передаваемого пакета (более подробно можно почитать в Википедии, либо еще где-нибудь в интернете).

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

Свой вариант протокола

Свою версию «идеального» протокола обмена данными по RS232/UART я вижу так:

  • соединение устройств типа точка-точка, обмен может инициировать любая сторона;
  • режим обмена данных — пакетный, длина макета может быть меньше, либо равна заранее установленного значения;
  • CRC16 для передаваемых данных;
  • простота реализации протокола как на стороне ПК, так и МК.

Первое, с чем я не хочу иметь дела, это разные тайм-ауты, так как это требует дополнительных телодвижений при реализации протокола в МК, и различные неприятности при написании программы для ПК.

Давайте рассмотрим вариант текстовой реализации протокола. Пусть в нем начало пакета будет обозначаться символом «:», а конец «=». Каждый байт будет конвертироваться в HEX-строку, состоящую из 2-х ASCII-символов. Последние 2 байта пакета — CRC16. Итого, пакет будет иметь следующий вид:

:AABBCCDDEEFF=

где AABBCCDD — полезные данные, в данном случае 4 байта, EEFF — контрольная сумма, 2 байта.

В принципе удобно и наглядно. При большом желании, пакеты можно генерить в уме и отправлять вручную прямо из консоли, если научитесь устному счету CRC16))) Служебной информации в таком пакете 2*n + 6, n-количество передаваемых байт, но если скорость передачи не очень важна, то такой вариант является приемлемым. Такая реализация себя неплохо зарекомендовала в нескольких моих проектах.

Однако, хочется все же уменьшить количество служебной информации в пакете для увеличения пропускной способности протокола при той же скорости работы UART. Можно в качестве системы кодирования пакета использовать не HEX, а что-то типа Base64, Base128, и т.д. Но давайте все же обратимся к бинарной реализации протокола. Возникает вопрос, а как нам тогда отделять один пакет от другого? Очень просто: зарезервируем один специальный байт, с которого будет начинаться каждый новый пакет. Но как быть, если этот байт будет встречаться в самих передаваемых данных? Ответ очень простой — будем использовать экранирование: если он будет попадаться в передаваемых данных, то мы просто отправим его 2 раза подряд.

Специальный символ конца пакета использовать не будем, вместо этого в начале пакета будем отправлять его длину. Ну и не забываем про контрольную сумму, она будет идти в самом конце. Итого, имеем следующую структуру пакета:

S 0 L0 L1 D0 D1 D2 D3 … Dn C0 C1

  • S — спец. байт начала пакета
  • L0, L1 — длина полезных данных пакета
  • D — полезные данные
  • C0, C1 — контрольная сумма CRC16

Если в передаваемых данных будет встречаться наш спец. симпол, то пакет будет выглядеть так:

S 0 L0 L1 D0 D1 S S D3 … Dn C0 C1

или так:

S 0 L0 L1 D0 D1 D2 D3 … Dn S S C1

или даже так:

S 0 L0 S S D0 D1 D2 D3 … Dn C0 C1

Думаю, с этим все понятно.

Реализация

Реализацию протокола BinExchange приведу для микроконтроллера STM32F030, однако, его можно с легкостью перенести на любой другой МК, нужно только переписать драйвер UART. Модуль BinExchange реализован в виде конечного автомата, что позволяет работать протоколу параллельно с другими задачами.

Рассмотрим функции протокола:

BinEx_Init() — инициализация протокола

BinEx_Process() — процесс конечного автомата протокола, вызывается в бесконечном цикле в main()

Функции передатчика:

BinEx_TxStatus() — статус передатчика, возвращает следующие значения:

  • BINEX_READY — готов к следующей передаче;
  • BINEX_RUN — в данный момент ведется передача.

BinEx_TxBegin(len) — отправить len элементов из буфера передатчика. Возвращает:

  • BINEX_OK — передача успешно запущена;
  • BINEX_BUSY — в данный момент еще не закончена предыдущая передача данных.

BinEx_TxGetBuffPntr() — если в данный момент передатчик находится в состоянии BINEX_READY, то возвращает указатель на буфер передатчика uint8_t *, если передатчик в состоянии BINEX_RUN, то возвращает ноль.

Функции приемника:

BinEx_RxStatus() — Получить статус приемника. Возвращает следующие значения:

  • BINEX_READY — приемник находится в режиме ожидания и не обрабатывает входной поток данных;
  • BINEX_RUN — приемник находится в режиме приема данных и ждет получения пакета. После получения пакета перейдет в состояние BINEX_READY, либо в BINEX_ERROR, если возникла какая-либо ошибка;
  • BINEX_ERROR — произошла ошибка приема пакета, для получения более подробной информации необходимо воспользоваться функцией BinEx_RxExtendedStatus().

BinEx_RxExtendedStatus() — подробная информация о статусе приемника. Возвращает следующие значения:

  • RXPACK_OK — пакет принят успешно;
  • RXPACK_CRCERR — ошибка контрольной суммы пакета;
  • RXPACK_TOO_LONG — длина пакета, указанного в заголовке, больше максимальной длины пакета BINEX_BUFFLEN.

BinEx_RxBegin() — начать прием пакета, приемник переходит в режим обработки потока входных данных. Возвращаемые значения:

  • BINEX_OK — приемник запущен успешно.

BinEx_RxDataLen() — получить длину принятого пакета. Возвращает актуальные данные после перехода приемника из состояния BINEX_RUN  в состояние BINEX_READY.

BinEx_RxGetBuffPntr() — если приемник находится в состоянии BINEX_READY, то возвращает указатель на буфер приемника, иначе ноль.

Практика

Перейдем теперь к практике работы с протоколом BinExchange. Приведу код, который принимает пакет данных и отправляет его обратно без изменений. Что-то типа ping-а))) Код IAR для stm32f030:

Рассмотрим наш тестовый конечный автомат TxTestProc(). В исходном состоянии мы разрешаем прием пакета данных и переходим в состояние 1. В состоянии 1 мы дожидаемся окончания процесса, и в случае его успеха переходим в состояние 2, а если возникли ошибки, то в состояние 3.

Рассмотрим состояние 2. Первым делом мы убеждаемся в том, что передатчик в данный момент ни чем не занят. Далее, получаем указатели на буферы приемника и передатчика, получаем количество принятых байт, и копируем буфер приемника в буфер передатчика. Затем запускаем процесс передачи и переходим в исходное состояние.

В состояние 3 мы можем попасть в случае, если при приеме данных у нас возникли какие-либо ошибки. Здесь мы так же проверяем окончание процесса передачи данных, получаем указатель на буфер передатчика, присваиваем нулевому элементу значение 1, и запускаем процесс передачи пакета, длинной 1 байт. После этого переходим в исходное состояние.

Реализация протокола для ПК на C#

Для работы с протоколом со стороны ПК написал небольшой класс на C#, который реализует все необходимое. Класс называется BinExchange. Вот небольшой пример работы с ним:

Мы генерируем массив случайных байт с помощью rnd.NextBytes(tx) и отправляем его в микроконтроллер. Затем, читаем то, что нам вернул МК и выводим в консоль. Так же возможна работа в асинхронном режиме. В этом случае, при получении очередного пакета данных, возникает событие Bin_DataReceived(), в котором мы читаем полученный пакет:

 

На этом вроде как все! Статья получилась довольно объемной, надеюсь, она будет кому-нибудь полезна. Спасибо за внимание, всем пока 🙂

Ссылки:

Проект на GitHub: https://github.com/DiMoonElec/BinExchange

 

Метки: . Закладка Постоянная ссылка.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *