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 - передача от ведущего |
|||
| 4 | Байт коррекции маски |
0x10 |
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 | Стоповый байт | 0х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. Пришлось кое-что поправить.
В следующий раз я покажу как пользоваться этой библиотекой.




Так сербы вроде )
Не помню. У них среда разработки говно. Поэтому даже с диска стёр.