Несмотря на свою древность, 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 реализован в виде конечного автомата, что позволяет работать протоколу параллельно с другими задачами.
Рассмотрим функции протокола:
void BinEx_Init(void); void BinEx_Process(void); BinExStatus_t BinEx_TxStatus(void); //статус передатчика BinExRetCode_t BinEx_TxBegin(uint16_t len); //Запустить передачу данных uint8_t *BinEx_TxGetBuffPntr(void); //получить указатель на буфер передатчика BinExStatus_t BinEx_RxStatus(void); //Получить статус приемника BinExRxExtendedStatus_t BinEx_RxExtendedStatus(void); //Получить статус приемника боллее подробно BinExRetCode_t BinEx_RxBegin(void); //Разрешить прием пакета uint16_t BinEx_RxDataLen(void); //Получить длину принятого пакета uint8_t *BinEx_RxGetBuffPntr(void); //Получить указатель на буфер приемника
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:
#include "stm32f0xx.h" #include "binex.h" #include "SysClock.h" static uint8_t state = 0; void TxTestProc(void) { static uint8_t *tx_buff; static uint8_t *rx_buff; static uint16_t rx_len; switch(state) { case 0: BinEx_RxBegin(); //Запускаем прием данных state = 1; break; ////////////////////////////////////// case 1: if(BinEx_RxStatus() == BINEX_READY) //Если получили пакет данных state = 2; else if(BinEx_RxStatus() == BINEX_ERROR) //Если возникла ошибка при приеме state = 3; break; ////////////////////////////////////// case 2: //отправляем принятый пакет if(BinEx_TxStatus() == BINEX_READY) //Если передатчик освободился после предыдущей передачи { //Получаем буферы приемника и передатчика tx_buff = BinEx_TxGetBuffPntr(); rx_buff = BinEx_RxGetBuffPntr(); rx_len = BinEx_RxDataLen(); //Копируем буфер приемника в буфер передатчика for(int i=0; i<rx_len; i++) { tx_buff[i] = rx_buff[i]; } //Запускаем передачу принятого пакета BinEx_TxBegin(rx_len); //переходим в исходное состояние state = 0; } break; ////////////////////////////////////// case 3: //Ошибка приема if(BinEx_TxStatus() == BINEX_READY) //Если передатчик освободился после предыдущей передачи { //Получаем буферы приемника и передатчика tx_buff = BinEx_TxGetBuffPntr(); tx_buff[0] = 1; //Запускаем передачу принятого пакета BinEx_TxBegin(1); //переходим в исходное состояние state = 0; } break; ////////////////////////////////////// } } void main(void) { SysClockInit(); BinEx_Init(); for(;;) { BinEx_Process(); TxTestProc(); } }
Рассмотрим наш тестовый конечный автомат TxTestProc(). В исходном состоянии мы разрешаем прием пакета данных и переходим в состояние 1. В состоянии 1 мы дожидаемся окончания процесса, и в случае его успеха переходим в состояние 2, а если возникли ошибки, то в состояние 3.
Рассмотрим состояние 2. Первым делом мы убеждаемся в том, что передатчик в данный момент ни чем не занят. Далее, получаем указатели на буферы приемника и передатчика, получаем количество принятых байт, и копируем буфер приемника в буфер передатчика. Затем запускаем процесс передачи и переходим в исходное состояние.
В состояние 3 мы можем попасть в случае, если при приеме данных у нас возникли какие-либо ошибки. Здесь мы так же проверяем окончание процесса передачи данных, получаем указатель на буфер передатчика, присваиваем нулевому элементу значение 1, и запускаем процесс передачи пакета, длинной 1 байт. После этого переходим в исходное состояние.
Реализация протокола для ПК на C#
Для работы с протоколом со стороны ПК написал небольшой класс на C#, который реализует все необходимое. Класс называется BinExchange. Вот небольшой пример работы с ним:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace BinExchange { class Program { static BinExchange BinEx = new BinExchange(32); static void Main(string[] args) { Random rnd = new Random(); BinEx.Open("COM5", 9600); byte[] tx = new byte[32]; rnd.NextBytes(tx); BinEx.Write(tx); byte[] rx = BinEx.Read(); for (int i = 0; i < rx.Length; i++) { Console.Write(rx[i].ToString() + " "); } Console.WriteLine(); Console.ReadKey(); } } }
Мы генерируем массив случайных байт с помощью rnd.NextBytes(tx) и отправляем его в микроконтроллер. Затем, читаем то, что нам вернул МК и выводим в консоль. Так же возможна работа в асинхронном режиме. В этом случае, при получении очередного пакета данных, возникает событие Bin_DataReceived(), в котором мы читаем полученный пакет:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace BinExchange { class Program { static BinExchange BinEx = new BinExchange(32); static void Main(string[] args) { Random rnd = new Random(); BinEx.Open("COM5", 9600); BinEx.DataReceived += Bin_DataReceived; byte[] tx = new byte[32]; rnd.NextBytes(tx); BinEx.Write(tx); Console.ReadKey(); } private static void Bin_DataReceived() { byte[] rx = BinEx.Read(); for(int i=0; i<rx.Length;i++) { Console.Write(rx[i].ToString() + " "); } Console.WriteLine(); } } }
На этом вроде как все! Статья получилась довольно объемной, надеюсь, она будет кому-нибудь полезна. Спасибо за внимание, всем пока 🙂
Ссылки:
Проект на GitHub: https://github.com/DiMoonElec/BinExchange