Бинарный протокол обмена данными по 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 реализован в виде конечного автомата, что позволяет работать протоколу параллельно с другими задачами.

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

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

 

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

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

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