STM32 RS-485 Modbus RTU

Modbus -это один из протоколов обмена промышленного и инженерного оборудования, был разработан фирмой Modicon в 1979 году. Modbus — открытый протокол и эта открытость является как его плюсом так и минусом. Кому интерсно более полная информация вот википедия ru.wikipedia.org/wiki/Modbus, моя цель показать как просто реализовать и использовать данный протокол.


Итак начнемс. Первое Modbus RTU — это протокол master-slave это значит, что комуникацию инициирует master — напрмер ПК а контроллер ему может только отвечать. Маленький экскурс в используемые типы данных это 1бит для дискретных типов и 16бит для налоговых (и цифровых тоже). Как показала жизнь работа с 1битными переменными не очень удобна и собственно нечасто используется производителями (ну я не часто это видел :) )- обычно эти сигналы пакуются в 16 битный тип -собственно это и есть Modbus регистр о нем мы и будем толковать. Если кто читал википедию, то обратил внимание на то, что в качестве одного из недостатков протокола указана не специфицированность типов данных. Что это значит? Есть у вас устройство которое должно передавать кроме int типа еще и float, но в спецификации этого нет и вы как считаете нужнуным говорите что мол де те два регистра это float, а следующие 100 это int которые для получения величины надо делить на 100, а последний регистр это 16 дискретных входов и только вы и карта регистров — которую вы должны оставить будете знать это, ваше устройство никому об этом сказать не сможет. Для примера в некоторых других более поздних протоколах есть специфичные типы данных как byte,int,float так и строковые. Поэтому самый простой вариант использования протокола это одна переменная типа int16_t — один регистр такова и будет наша реализация. Да еще чуть не забыл мы будет делать бинарную версию протокола Modbus RTU. Поскольку проект для CooCox IDE состоит из нескольких файлов и многабукафф прикладываю его весь — работает USART1 -RS232 и USART3-RS485 + моргают светодиодики.
Для начала нам надо написать нечто подобное тому, что было в предыдущей статье про усарт с прерываниями, но несколько более осмысленное. Чтобы было удобно работать мы создадим структуру данных для того чтобы удобно передавать данные в функции.
Вот мой .h файл

#define OBJ_SZ 123 //это количество объектов
#define SETUP 4 //это просто количество данных в массиве 0-элемент которого означает адрес

//PARAMETERRS ARRAY 0 PARAMETER = MODBUS ADDRESS
unsigned char SET_PAR[SETUP];//0-элемент это адрес

//OBJECT ARRAY WHERE READING AND WRITING OCCURS
int res_table[OBJ_SZ];//массив с объектами то откуда мы читаем и куда пишем

//buffer uart
#define BUF_SZ 256 //размер буфера
#define MODBUS_WRD_SZ (BUF_SZ-5)/2 //максимальное количество регистров в ответе

//uart structure
typedef struct {
unsigned char buffer[BUF_SZ];//буфер
unsigned int rxtimer;//этим мы считаем таймоут
unsigned char rxcnt; //количество принятых символов
unsigned char txcnt;//количество переданных символов
unsigned char txlen;//длина посылки на отправку
unsigned char rxgap;//окончание приема
unsigned char protocol;//тип протокола - здесь не используется
unsigned char delay;//задержка
} UART_DATA;

UART_DATA uart3,uart1;//структуры для соответсвующих усартов

void MODBUS_SLAVE(UART_DATA *MODBUS);//функция обработки модбас и формирования ответа


Для чего все это надо — модбас сериалные коммуникации основаны на таймоутах т.е. прием заканчивается если перерыв между принятыми символами 3.5 символа ну и перезапрос master-ом slave происходит через другой таймоут порядка 100- 500ms и нам как то надо считать эти задержки в slave — мы считаем только 3.5 символа. Для того чтобы их считать нам надо иметь таймер мы используем TIM6 — простой таймер.


void SetupTIM6()
{
	NVIC_InitTypeDef  NVIC_InitStructure;
	TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;

	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6 , ENABLE);
    TIM_DeInit(TIM6);

    //0.0001 sec setup APB=36Mhz/(36*100)
    TIM_TimeBaseStructure.TIM_Prescaler= 36;//на эту величину поделим частоту шины
    TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1;//частота без деления 36Мгц
    TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up;//считаем вверх
    TIM_TimeBaseStructure.TIM_Period=100;//до этого значения будет считать таймер
    TIM_TimeBaseInit(TIM6, &TIM_TimeBaseStructure);
    TIM_ClearFlag(TIM6, TIM_FLAG_Update);
    TIM_ITConfig(TIM6,TIM_IT_Update,ENABLE);
    TIM_Cmd(TIM6, ENABLE);


      // Настройка прерывания
      NVIC_InitStructure.NVIC_IRQChannel = TIM6_IRQn;
      NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
      NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
      NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
      NVIC_Init(&NVIC_InitStructure);

      TIM_ITConfig(TIM6,TIM_IT_Update,ENABLE);
}


Что мы делаем в обработчике таймера:


void TIM6_IRQHandler(void)
{
	TIM_ClearITPendingBit(TIM6, TIM_IT_Update);//очищаем прерывания

	//моргаем светодиодом дабы показать активность таймера
        if(GPIO_ReadOutputDataBit  ( GPIOD,GPIO_Pin_11))
	     GPIO_WriteBit(GPIOD,GPIO_Pin_11,Bit_RESET);
	else
		GPIO_WriteBit(GPIOD,GPIO_Pin_11,Bit_SET);

	   //если наш таймер больше уставки задержки и есть символы то есть gap -перерыв в посылке 
           //и можно ее обрабатывать
           if((uart3.rxtimer++>uart3.delay)&(uart3.rxcnt>1))
	   uart3.rxgap=1;
	   else
	   uart3.rxgap=0;
           
           //тоже самое для usart1
	   if((uart1.rxtimer++>uart1.delay)&(uart1.rxcnt>1))
	   uart1.rxgap=1;
	   else
	   uart1.rxgap=0;

}


Обработчик прерываний usart нужен для двух вещей — принятия и отправки символов с включением и выключением соотвествующих прерываний


void USART3_IRQHandler(void)
{
	//Receive Data register not empty interrupt
  	if(USART_GetITStatus(USART3, USART_IT_RXNE) != RESET)
   {
  		 USART_ClearITPendingBit(USART3, USART_IT_RXNE)//очистка признака прерывания
  		uart3.rxtimer=0;

  			if(uart3.rxcnt>(BUF_SZ-2))
  			uart3.rxcnt=0;

  		 	uart3.buffer[uart3.rxcnt++]=USART_ReceiveData (USART3);


   }

  	//Transmission complete interrupt
    if(USART_GetITStatus(USART3, USART_IT_TC) != RESET)
  	{

  		USART_ClearITPendingBit(USART3, USART_IT_TC);//очистка признака прерывания
  		  if(uart3.txcnt<uart3.txlen)
  		{
  			USART_SendData(USART3,uart3.buffer[uart3.txcnt++]);//Передаем
  		}
  		 else
  		{
                 //посылка закончилась и мы снимаем высокий уровень сRS485 TXE
  		 uart3.txlen=0;
  		 GPIO_WriteBit(GPIOB,GPIO_Pin_2,Bit_RESET);
  		 GPIO_WriteBit(GPIOD,GPIO_Pin_10,Bit_RESET);
  		 USART_ITConfig(USART3, USART_IT_RXNE, ENABLE);
  		 USART_ITConfig(USART3, USART_IT_TC, DISABLE);
  		 TIM_ITConfig(TIM6,TIM_IT_Update,ENABLE);
  		}
  	}

}


Но для срабатывания прерывания по окончанию передачи надо сначала его включить — это происходит в главном цикле при помощи функции

void net_tx3(UART_DATA *uart)
{
  if((uart->txlen>0)&(uart->txcnt==0))
  {
	    USART_ITConfig(USART3, USART_IT_RXNE, DISABLE);//выкл прерывание на прием
	    USART_ITConfig(USART3, USART_IT_TC, ENABLE);//включаем на окочание передачи

	  	//моргаем светодиодом и включаем rs485 на передачу
                GPIO_WriteBit(GPIOB,GPIO_Pin_2,Bit_SET);
	  	GPIO_WriteBit(GPIOD,GPIO_Pin_10,Bit_SET);

	  	USART_SendData(USART3, uart->buffer[uart->txcnt++]);
  }

}

т.е. если в нее попадает признак готовности к отправке uart->txlen>0 и еще ничего непередано uart->txcnt==0 она инициализирует отправку.
Перед началом связи нам надо инициализировать адрес и указать промежуток который будет являться признаком окончания передачи


SET_PAR[0]=1;//адрес устройства

       //timer 0.0001sec one symbol on 9600 ~1ms
       uart1.delay=30; //таймаут приема

       //timer 0.0001sec one symbol on 9600 ~1ms
       uart3.delay=30; //таймаут приема



Тут мы немножко бежали впереди паровоза не посмотрели, что происходит в главном цикле:


     //Main loop
     while(1)
     {

    	if(uart3.rxgap==1)//ждем gap - т.е. промежуток
    	 {
     	 MODBUS_SLAVE(&uart3);//проверяем и если пакет нам по адресу и не битый то формируем ответ
    	 net_tx3(&uart3);//если есть признак готовности посылки инициализируем передачу
    	 }
        
        //тоже само только для усарт1 
    	if(uart1.rxgap==1)
    	  {
    	    MODBUS_SLAVE(&uart1);
    	   net_tx1(&uart1);
      	  }

     }


Немного забегая вперед. Чтобы проверить функциональность устройства есть программа modbus poll — www.modbustools.com/download.asp. она позволяет связаться с устройством и просмотреть выбранные регистры. При старте уже выводится некоторый набор регистров и используется функция 03 — это нас устраивает. Только в меню Connecttion надо отключить Auto connect иначе при коннекте она будет проскакивать функцию настройки порта а в ней надо поставить 9600,N,8,1 и ваш компорт адрес нашего устройства 1.



что нибудь записать в регистры можно из меню Functions -06: Write single register

Собственно пока все чуть позже допишу про функцию MODBUS_SLAVE() и карту данных.

  • +1
  • 19 августа 2011, 20:40
  • GYUR22
  • 1
Файлы в топике: modbus.zip

Комментарии (41)

RSS свернуть / развернуть
Вижу используете самописную библиотеку для modbus, смотрели в сторону того же freemodbus? Мастером у Вас компьютер, какой программой пользуетесь?
0
  • avatar
  • John
  • 19 августа 2011, 22:55
чужие библиотеки обычно применяю если тока другого выхода нет или так проще+ я делал лицензионно свободный код для устройства. Программа которую применяю на пк это OPC сервер нашей компании называется EDE.
0
DELL ATG630. Классная машинка с наличием com-порта
0
это D630 — мне они очень нравятся поэтому я свой и выкупил когда пришло время замены
0
у самого такой почти три года был рабочей машинкой
0
так и было теперь дома работает :)
0
Чето в гугле нет ничего про этот OPC EDE
можешь сюда выложить если есть возможность?
0
выложить не могу. Пишите в личку.
0
прием заканчивается если перерыв между принятыми символами 3.5 символа
ненужное ограничение, лучше сразу контролировать принятый пакет на правильность в момент приема каждого байта

if(uart3.rxcnt>(BUF_SZ-2))
uart3.rxcnt=0;

это не надежно, ИМХО по переполнению буфера лучше просто прекратить дальнейший прием, а счетчик обнулять по тайм-ауту приема
0
«ненужное ограничение, лучше сразу контролировать принятый пакет на правильность в момент приема каждого байта» — не совсем так…
Если каждый раз проверять пакет и считать crc — то это относительно ресурсоемко и ненужно

«if(uart3.rxcnt>(BUF_SZ-2))
uart3.rxcnt=0;

это не надежно, ИМХО по переполнению буфера лучше просто прекратить дальнейший прием, а счетчик обнулять по тайм-ауту приема»

это дополнительное условие — просто чтобы не переполнялся буфер если например что то будет гадить в порт.

а счетчик обнуляется по таймауту и так — если внимательно посмотреть
0
это дополнительное условие — просто чтобы не переполнялся буфер если например что то будет гадить в порт.

вот в таком случае лучше прекратить запись буфер (не обнулять счетчик) и просто ждать тайм-аут
0
так и получается :)
0
имел ввиду лучше сделать так:
if(uart3.rxcnt<=(BUF_SZ-2)) uart3.buffer[uart3.rxcnt++]=USART_ReceiveData (USART3);
0
но счетчик должен быть обязательно обнулен т.к. иначе следующая посылка не примется.
0
на столе оставьте ноут и плату + что необходимо для данного поста и на фото будет гораздо приятнее смотреть. Ну это я так, для эстетики. :-)
0
разбирает завалы на столе… :)
0
Когда прёт идея и работа у меня на столе такая срачь, что страшно смотреть)) Потом разгребаю все. Тут уж не до эстетики.
0
Это все замечательно, но почему у вас в исходниках в условиях используются битовые (&), а не логические (&&) операторы? И местами попадается код в заголовочных файлах, что тоже сомнительная идея.
0
  • avatar
  • AlexG
  • 07 сентября 2011, 11:55
ээ какой код в заголовочных файлах...?
0
Не совсем точно выразился, я про объявления переменных в controller.h:
0
Не совсем точно выразился, я про объявления переменных в controller.h:

//PARAMETERRS ARRAY 0 PARAMETER = MODBUS ADDRESS
unsigned char SET_PAR[SETUP];

//OBJECT ARRAY WHERE READING AND WRITING OCCURS
int res_table[OBJ_SZ];

UART_DATA uart3,uart1;


controller.h затем включается в main.c и modbus_slave.c и получается дублирование переменных в программе
0
соглашусь, что надо писать extern в h, но на практике gcc как для avr так и для ARM ест это нормально поэтому на это тупо забивалось.
0
заметил такую вещь, при конфигура пина RX (PA10)
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;

подключаю к com-порту, пакеты принимаются на ура, всё как посылаю.
отрубаю com-порт, вывод RX в воздухе, эта сабака упорно заходит в прерывания по приёму и кидает в буфер всякий мусор:
//Receive Data register not empty interrupt
if(USART_GetITStatus(USART3, USART_IT_RXNE) != RESET)

пробовал переконфигуроровать в остальные режимы пин PA10
typedef enum
{ GPIO_Mode_AIN = 0x0,
GPIO_Mode_IN_FLOATING = 0x04,
GPIO_Mode_IPD = 0x28,
GPIO_Mode_IPU = 0x48,
GPIO_Mode_Out_OD = 0x14,
GPIO_Mode_Out_PP = 0x10,
GPIO_Mode_AF_OD = 0x1C,
GPIO_Mode_AF_PP = 0x18
}GPIOMode_TypeDef;

результата не дало, работает только GPIO_Mode_IN_FLOATING. Неужели нужно в к железки допаявать резюк с подтяжкой?
0
обычно подтяжку обеспечивает сам драйвер — если его нет то превед мусору на входе
0
Скажите, пожалуйста, как измерять таймауты, если используется аппаратный FIFO RX (у меня LPC1768), ведь время между чтениями из FIFO не равно реальному времени между байтами пакета. Вроде как есть аппаратный таймер в UART-ах специально для таких измерений, но запустить его у меня пока не получилось, прерывание молчит :(

По этой же причине (FIFO TX) запись пакета на отправку происходит быстрее, чем завершается сама отправка, а как раз прерывания по окончанию отправки и нету, есть только флаг (нужно же еще RS485 выключить) :( Флаг опрашивать можно в таймере (что некрасиво) или в отдельном потоке. Ничего другого в голову не пришло.

И насчет потоков, кто знает, насколько надежна стандартная библиотека RTOS от KEIL-а, и насколько медленнее получается на ней проект, по сравнению с классическим программированием?
0
  • avatar
  • Lost
  • 31 января 2012, 19:34
чето как то все сложно у вас…
как я понимаю в stm32 нет буфера — регистр и регистр поэтому тут я плохой советчик.
lpc-ки же умеют сами ногой дрыгать для 485 вроде?
0
UART1 умеет, UART0,2,3 не умеют :(
В потоке флаг отправки опрашивать плохое решение. Так что пока отключил у UART0 FIFO и все стало работать как у вас.
0
Замечательная статья! Только Modbus тут совершенно не при чем ибо всегда полагал, что Modbus это протокол реализующий функции, а не UART по прерыванию.
0
Вот и зря:). Пересмотрите свое мировоззрение на счет MODBUS RTU. MODBUS RTU UART по прыванием плюс контрольная сумма.
0
На начальном этапе бурной жизнедеятельности у меня был свой протокол, работающий по UART по прерыванию и однобайтная контрольная сумма тоже была. Но это не был Modbus. Теперь есть. И то, что описано в статье это не Modbus.
Контрольную сумму тоже не так вычисляю, надо бы проанализировать, что лучше.
void crc(unsigned char *pBuffer, unsigned char usBufferSize)
{
unsigned int usResult = 0xFFFF;
unsigned char j;
unsigned char i;

for(j = 0; j < usBufferSize; j++)
{
usResult = usResult ^ pBuffer[j];
for(i = 0; i < 8; i++)
{
if(usResult & 0x01)
{
usResult >>= 1;
usResult = usResult ^ 0xA001;
}
else usResult >>= 1;
}
}
CRCLo = (char) usResult;
CRCHi = (char)(usResult>>8);
}
0
А что тогда есть Modbus?
0
Аппаратно включать/выключать передачу нельзя?
0
Не совсем ясно — он платный?
0
3-d party должен быть платным
ps слышал что раньше стартовать с разработкой knx устройств стоило ~40KEUR (стэк, поездка, обучение)
0
По ссылке кроме флайера и рекламы больше ничего нет.KNX был, есть и будет непозволительно дорогим решением для автоматизации. Мы пойдём другим путём. Модбас называется.
0
Есть еще BACnet — он поудобнее но и сложнее в разы
0
#define MODBUS_WRD_SZ (BUF_SZ-5)/2 //максимальное количество регистров в ответе
Поясните пожалуйста (BUF_SZ-5)/2 — отнимаем 5- что это за цифра, и зачем делить на 2? Какая логика расчета?
0
5 =заголовок+crc
а регистры 16 битные
0
Если под заголовком имеется в виду адрес (1 байт) и функцию (1 байт) + CRC (2 байта) — все равно не получаться. Не одолею никак modbus этот…
0
//MODBUS->buffer[0] = adress — stays a same as in received
//MODBUS->buffer[1] = query type — - stay a same as in recived
//MODBUS->buffer[2] = data byte count

+CRC
те забыли длину посылки приплюсовать
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.