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. Пришлось кое-что поправить.
В следующий раз я покажу как пользоваться этой библиотекой.
Так сербы вроде )
Не помню. У них среда разработки говно. Поэтому даже с диска стёр.