Top.Mail.Ru

Часть 18. Не ModBUS, кое-что попроще.

ModBUS. О нем много было рассказано, много копий сломано. Но сложилась ситуация таким образом, что никто и ничего не рассказал о том, как пишется прошивка для ведомого, а не для ведущего. Как создаются регистры, как они заполняются, каким образом отыскивается нужный регистр. Вот общение с ведомым обсосано со всех сторон, но разговор сейчас не о нём. Разговор о библиотеке, которая позволяет общаться с ведомым, когда у него висит не очень много датчиков. Например, в трёх точках на расстоянии 30 метров друг от друга нужно собрать данные о температуре, давлении, влажности, освещённости и так далее. А затем передать ведущему. Данный протокол подсмотрен в библиотеках фирмы Mikroelektronica (да, да именно так и пишется, как это не удивительно). Только у них он может передать три байта, я расширил до 32-х.

Идея передачи протокола такова. Передаётся стартовый байт, адрес ведомого, количество передаваемых байт, 5 байт коррекции, массив данных, CRC8, стоповый байт. По стартовому и стоповому байту определяется начало и конец пакета, CRC позволяет проверить корректность передаваемых данных, байты корректировки корректируют байты данных, если они похожи на байты начала/конца передачи.

байта Назначение Байт Бит Описание
1 Стартовый байт 0х96 0-7
2 Адрес ведомого 0х01 - 0хFF 0-7
3 Количество байт 0х01 - 0х3F 0-5 Адрес
6 Флаг коррекции адреса
7

1 - передача от ведущего
0 - передача от ведомого

4 Байт коррекции маски

0x10
0х08
0х04
0х02
0х01

0-7

1 в четвёртом разряде указывает на необходимость корректирования CRC

5 Байты коррекции данных 0х01 - 0хFF 0-7 Четыре байта, объединены в 32-разрядную переменную, каждый бит которой установленный в 1, указывает на необходимость корректировки соответствующего ей байта данных
6 0х01 - 0хFF 0-7
7 0х01 - 0хFF 0-7
8 0х01 - 0хFF 0-7
9 ... N Данные 0х00 - 0хFF 0-7 От одного до 32-х байт
N + 1 CRC 0х00 - 0хFF 0-7

CRC - считается по некорректированным данным

N + 2 Стоповый байт A9 0-7

В таблице расписан каждый байт и за что он отвечает. Можно довольно часто встретить слово "коррекция". Что это и с чем едят?

Всё довольно интересно. Мы имеем протокол, который начало и конец пакета отлавливает по двум байтам, стартовому - 0х96 и стоповому - 0xA9, всё, что заключено между ними и является необходимым нам пакетом. Но тут нам встретился байт, который равен или стартовому или стоповому байту. И всё, пакет пропал, мы никогда не примем его, пока байт не изменится.

Чтобы этого не происходило, младший бит этого упрямого байта инвертируется и пакет спокойно передаётся. На принимающей стороне этот бит инвертируется обратно. А для того, чтобы знать, нужно нам корректировать этот байт или нет, существует 4 байта, в которых содержится маска. Эти 4 байта объединены в 32-разрядную переменную, каждый бит которой, со старшего разряда, указывает, нужно ли корректировать текущий байт. Из-за этой разрядности у нас и введено ограничение на количество байт в пакете данных. Но у нас возникают вторые грабли. Один из байтов в этой переменной тоже может принять значение старт/стоп байта. Вот тут у нас вступает в борьбу за правильную передачу "байт коррекции маски". В нем с помощью единичного значения в одном из 4-х младших бит, указывается, нужно ли корректировать байт маски и один, 5-й, бит отведён под корректор CRC. Ведь CRC тоже может быть равна старт/стоп байту. Сам этот байт принять такое значение не может, поэтому мы избавляемся от проблемы "кто будет контролировать контролирующего".

Ввиду того, что адресов периферии у нас может быть 254, мы не можем гарантировать, что адрес не совпадёт с старт/стопом. Поэтому в байте, в котором указывается количество передаваемых байт, 6-й бит является индикатором коррекции адреса. Ну а 7-й бит указывает, от кого идёт передача: от ведущего или ведомого.

Ну и по-быстрому подготовка и передача пакета. Как и описывал, всё разделено по шагам. Для заголовка у нас создаётся структура:

struct _Heading
{
  uint8_t StartByte = 0x96;                       // Стартовый байт
  uint8_t Address;                                // Адрес периферии
  uint8_t QuantBytes;                             // Размер пакета данных
  uint8_t MaskCorrect;                            // Корректор маски и CRC
  uint32_t Mask;                                  // Корректор данных
} __attribute__ ((__packed__));

Класс:

class RS485: public uart
{
  public:
    RS485(USART_TypeDef *Usart, bool Alternate, GPIO_TypeDef *GPIO_Port, uint16_t GPIO_Pin, uint8_t Address = 0) :
        uart::uart(Usart, Alternate)
    {
      _GPIO_Port = GPIO_Port;
      _GPIO_Pin = GPIO_Pin;
      _SetPin(_GPIO_Port, _GPIO_Pin, Output, Pull_Down);
      _DigitalWriteBit(_GPIO_Port, _GPIO_Pin, Low);                                       // По умолчанию слушаем линию
      _Address = Address;
      if(_Address) _Role = RS485_SLAVE; else _Role = RS485_MASTER;                        // Если адрес назначен, мы ведомый
    }

Ему передаётся порт UART, альтернативные выводы, порт и вывод для управления микросхемой RS485 на приём/передачу и адрес. Если адрес указать, то устройство инициализируется как ведомое, если не указывать, как ведущее. По умолчанию RS485 инициализируется на прослушку линии.

Функций у нас четыре:

size_t  Send(uint8_t *Buffer, uint8_t Length, uint8_t TxAddr = 0);                          // Посылаем данные. Если TxAddr не ноль, посылка ведомому
size_t  Receive(uint8_t *Buffer);                                                           // Читаем данные в буфер
size_t  SendReceive(uint8_t *BufferTX, uint8_t *BufferRX, uint8_t Length, uint8_t TxAddr);  // Используется только мастером
void    setTimeOut(uint16_t TimeOut) {_TimeOut = TimeOut;}                                  // Установка таймаута, отличного от 1000 мс
  • Send() - подготавливает и передаёт пакет. Данные должны находится в буфере, указатель на который передаётся в переменную Buffer. В переменной Length указывается количество передаваемых байт. Причём sizeof(Buffer) здесь не прокатит. Передастся весь буфер, который мы выделим, а не только те данные, которые мы в него набили. TxAddr - адрес ведомого, которому предназначены данные, используется ведущим. Если ведомый отдаёт ведущему данные, адрес не указывается. Возвращает количество переданных байт или ошибку.
  • Receive() - принимает пакет и производит его разбор. Использует только указатель на буфер, куда будут переданы данные. Возвращает количество принятых байт или ошибку.
  • SendReceive() - должна использоваться только ведущим, передаёт запрос и принимает ответ. Функция не проверена.
  • setTimeOut() - таймаут на операцию 1 секунда, если нужно больше, перед началом использования функций библиотеки необходимо установить нужный.

Разбор функции Send():

_DigitalWriteBit(_GPIO_Port,_GPIO_Pin, High); // Переключаем приёмопередатчик на передачу

Начинаем с переключения драйвера на передачу, затем инициализируем структуру заголовка:

/*
   * Инициализируем структуру заголовка и переменные
*/
Heading.StartByte   = _StartByte;                                                       // Стартовый байт
Heading.Address     = TxAddr;                                                           // Адрес ведомого
Heading.QuantBytes  = Length;                                                           // Количество передаваемых/принятых байт
Heading.MaskCorrect = 0;                                                                // Корректор маски
Heading.Mask        = 0;                                                                // Маска корректировки данных

Определяемся, от кого передача - ведущего или ведомого - и корректируем бит, указывающий на это. Если адрес стал совпадать со старт/стоп байтом, инвертируем младший бит и делаем соответствующую отметку:

// --------- Инициализация Master/Slave и модификация флагов количества передаваемых байт -------
  if(_Role == RS485_MASTER) Heading.QuantBytes |= 0x80;                                   // Если мастер, выставляем первый бит
  if((TxAddr == _StartByte) || (TxAddr == _StopByte))                                     // Если байт адреса стал похож на старт/стоп байты,
  {
    Heading.Address = TxAddr ^ 0x01;                                                      // инвертруем бит
    Heading.QuantBytes |= 0x40;                                                           // и выставляем нужный флаг
  }

В цикле проверяем все байты на совпадение со старт/стопом и, при необходимости, в маске корректора выставляем необходимые биты:

// ---------- Подготавливаем буфер передачи ---------
  _DataBuff = Buffer;                                                                     // Присваиваем нашему указателю адрес буфера
  uint32_t I=0x80000000;                                                                  // Первый бит маскировщика в "1"

  for(uint8_t J=0; J<Length; J++ )                                                        // И начинаем набивать буфер
  {
    SendBuff[J] = *_DataBuff;                                                             // Присваиваем буферу передачи указатель на передаваемый буфер
    if((*(uint8_t*) _DataBuff == _StartByte) || (*(uint8_t*) _DataBuff == _StopByte))     // Если передаваемый байт похож на на старт/стоп,
    {
      SendBuff[J] ^= 0x01;                                                                // то инвертируем его
      Heading.Mask |= I;                                                                  // и в маске выставляем нужный бит
    }
    I = I>>1;                                                                             // Сдвигаем маскировщик на следующую позицию
    _DataBuff++;                                                                          // Выбираем следующий байт
  }

Проверяем проверяющего и, если он неправ, в корректоре маски выставляем необходимый бит, не забывая откорректировать соответствующий байт:

// ---------- Проверяем регистр маски и вносим корректировки там, где необходимо -----------
  if(((Heading.Mask & 0xFF000000) == (uint32_t) (_StartByte << 24)) || ((Heading.Mask & 0xFF000000) == (uint32_t)(_StopByte << 24)))
  {
    Heading.MaskCorrect |= MaskCorrect_3;
    Heading.Mask ^= 0x01000000;
  }
  if(((Heading.Mask & 0x00FF0000) == (uint32_t)(_StartByte << 16)) || ((Heading.Mask & 0x00FF0000) == (uint32_t)(_StopByte << 16)))
  {
    Heading.MaskCorrect |= MaskCorrect_2;
    Heading.Mask ^= 0x00010000;
  }
  if(((Heading.Mask & 0x0000FF00) == (uint32_t)(_StartByte << 8)) || ((Heading.Mask & 0x0000FF00) == (uint32_t)(_StopByte << 8)))
  {
    Heading.MaskCorrect |= MaskCorrect_1;
    Heading.Mask ^= 0x00000100;
  }
  if(((Heading.Mask & 0x000000FF) == (uint32_t)_StartByte) || ((Heading.Mask & 0x000000FF) == (uint32_t)_StopByte))
  {
    Heading.MaskCorrect |= MaskCorrect_0;
    Heading.Mask ^= 0x00000001;
  }

Осталось рассчитать CRC, поправить ее, если это необходимо, и сделать соответствующую отметку:

// Рассчитываем CRC !!! нескорректированного !!! буфера передачи
 _CRC_8 = crc8(Buffer, Length);                                                          // Считаем CRC буфера
 if((_CRC_8 == _StartByte) || (_CRC_8 == _StopByte))
 {
   _CRC_8 ^= 0x01;                                                                       // Если CRC совпадает со стартовым или стоповым битом, инвертируем младший бит
   Heading.MaskCorrect |= MaskCRC;                                                       // и отмечаем, что CRC скорректирован
 }

Ну и всё то, что мы здесь намудрили, выплёвываем в линию и переключаемся на приём. Но не раньше окончания передачи:

// Выплёвываем пакет
Transmit((uint8_t*) &Heading, sizeof(Heading));                                         // Передаём заголовок
Transmit(SendBuff, Length);                                                             // Передаём буфер
Transmit(_CRC_8);                                                                       // Передаём CRC
Transmit(_StopByte);                                                                    // Передаём Стоп

while(!isTxFinish()){;}                                                                 // Ждём окончания передачи
_DigitalWriteBit(_GPIO_Port,_GPIO_Pin, Low);                                            // Переключаем приёмопередатчик на приём

Приём разбирать не будем, так как он похож на передачу. Единственный нюанс: сначала принимается заголовок полностью, проверяется, нужна ли его корректировка, и только потом проверяется, а нам ли счастье подвалило. Если нам, продолжаем принимать дальше, корректируя по необходимости. Как пришёл весь пакет, считаем CRC и всё. Если не нам, отбрасываем пакет.

Как всегда, библиотека на Яндекс диске, в разделе Library, называется RS485 и ссылка на тему на форуме. Всем, кто скачал её раньше, перекачайте и STM32, и Library. Пришлось кое-что поправить.

В следующий раз я покажу как пользоваться этой библиотекой.

Подписаться
Уведомить о
guest

2 комментариев
Старые
Новые
Межтекстовые Отзывы
Посмотреть все комментарии
Aveal
Администратор
2 лет назад

да, да именно так и пишется, как это не удивительно

Так сербы вроде )

2
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x