Stm32F3Xx + Freertos. Modbus Rtu С Аппаратным Rs485 И Crc Без Таймеров И Семафоров

Всем привет! Сравнительно недавно, окончив университет, я оказался в небольшой компании, занимающейся разработкой электроники.

Одной из первых задач, с которыми я столкнулся, была необходимость реализовать протокол Modbus RTU Slave с использованием STM32. Я написал это со стыдом, но я начал видеть этот протокол от проекта к проекту и решил провести рефакторинг и оптимизировать библиотеку с помощью FreeRTOS.



Введение

В текущих проектах я часто использую комбинацию STM32F3xx + FreeRTOS, поэтому решил максимально использовать аппаратные возможности этого контроллера.

В частности:

  • Получение/отправка с использованием DMA
  • Возможность аппаратного расчета CRC
  • Возможность аппаратной поддержки RS485.
  • Определение окончания посылки посредством аппаратных возможностей USART, без использования таймера
Сразу оговорюсь: здесь я не описываю спецификацию протокола Modbus и то, как с ним работает мастер, об этом можно прочитать здесь И здесь .



Конфигурационный файл

Для начала я решил немного упростить задачу переноса кода между проектами, хотя бы в рамках одного семейства контроллеров.

Поэтому я решил написать небольшой файл conf.h, который позволит мне быстро перенастроить основные части реализации.

ModbusRTU_conf.h

  
  
  
  
  
  
  
  
  
  
  
   

#ifndef MODBUSRTU_CONF_H_INCLUDED #define MODBUSRTU_CONF_H_INCLUDED #include "stm32f30x.h" extern uint32_t SystemCoreClock; /*Registers number in Modbus RTU address space*/ #define MB_REGS_NUM 4096 /*Slave address*/ #define MB_SLAVE_ADDRESS 0x01 /*Hardware defines*/ #define MB_USART_BAUDRATE 115200 #define MB_USART_RCC_HZ 64000000 #define MB_USART USART1 #define MB_USART_RCC RCC->APB2ENR #define MB_USART_RCC_BIT RCC_APB2ENR_USART1EN #define MB_USART_IRQn USART1_IRQn #define MB_USART_IRQ_HANDLER USART1_IRQHandler #define MB_USART_RX_RCC RCC->AHBENR #define MB_USART_RX_RCC_BIT RCC_AHBENR_GPIOAEN #define MB_USART_RX_PORT GPIOA #define MB_USART_RX_PIN 10 #define MB_USART_RX_ALT_NUM 7 #define MB_USART_TX_RCC RCC->AHBENR #define MB_USART_TX_RCC_BIT RCC_AHBENR_GPIOAEN #define MB_USART_TX_PORT GPIOA #define MB_USART_TX_PIN 9 #define MB_USART_TX_ALT_NUM 7 #define MB_DMA DMA1 #define MB_DMA_RCC RCC->AHBENR #define MB_DMA_RCC_BIT RCC_AHBENR_DMA1EN #define MB_DMA_RX_CH_NUM 5 #define MB_DMA_RX_CH DMA1_Channel5 #define MB_DMA_RX_IRQn DMA1_Channel5_IRQn #define MB_DMA_RX_IRQ_HANDLER DMA1_Channel5_IRQHandler #define MB_DMA_TX_CH_NUM 4 #define MB_DMA_TX_CH DMA1_Channel4 #define MB_DMA_TX_IRQn DMA1_Channel4_IRQn #define MB_DMA_TX_IRQ_HANDLER DMA1_Channel4_IRQHandler /*Hardware RS485 support 1 - enabled other - disabled */ #define MB_RS485_SUPPORT 0 #if(MB_RS485_SUPPORT == 1) #define MB_USART_DE_RCC RCC->AHBENR #define MB_USART_DE_RCC_BIT RCC_AHBENR_GPIOAEN #define MB_USART_DE_PORT GPIOA #define MB_USART_DE_PIN 12 #define MB_USART_DE_ALT_NUM 7 #endif /*Hardware CRC enable 1 - enabled other - disabled */ #define MB_HARDWARE_CRC 1 #endif /* MODBUSRTU_CONF_H_INCLUDED */

На мой взгляд, наиболее распространенные вещи, которые меняются:
  • Адрес устройства и размер адресного пространства
  • Тактовая частота и параметры контактов USART (контакт, порт, rcc, irq)
  • Параметры канала DMA (rcc, irq)
  • Включить/отключить аппаратную CRC и RS485.


Конфигурация оборудования

В этой реализации я использую обычную CMSIS не из-за религиозных убеждений, просто мне так проще и зависимостей меньше.

Настройки порта описывать не буду, посмотреть можно по ссылке на GitHub которая будет ниже.

Начнем с настройки USART: Настройка USART

/*Configure USART*/ /*CR1: -Transmitter/Receiver enable; -Receive timeout interrupt enable*/ MB_USART->CR1 = 0; MB_USART->CR1 |= (USART_CR1_TE | USART_CR1_RE | USART_CR1_RTOIE); /*CR2: -Receive timeout - enable */ MB_USART->CR2 = 0; /*CR3: -DMA receive enable -DMA transmit enable */ MB_USART->CR3 = 0; MB_USART->CR3 |= (USART_CR3_DMAR | USART_CR3_DMAT); #if (MB_RS485_SUPPORT == 1) /*Cnfigure RS485*/ MB_USART->CR1 |= USART_CR1_DEAT | USART_CR1_DEDT; MB_USART->CR3 |= USART_CR3_DEM; #endif /*Set Receive timeout*/ //If baudrate is grater than 19200 - timeout is 1.75 ms if(MB_USART_BAUDRATE >= 19200) MB_USART->RTOR = 0.00175 * MB_USART_BAUDRATE + 1; else MB_USART->RTOR = 35; /*Set USART baudrate*/ /*Set USART baudrate*/ uint16_t baudrate = MB_USART_RCC_HZ / MB_USART_BAUDRATE; MB_USART->BRR = baudrate; /*Enable interrupt vector for USART1*/ NVIC_SetPriority(MB_USART_IRQn, configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY); NVIC_EnableIRQ(MB_USART_IRQn); /*Enable USART*/ MB_USART->CR1 |= USART_CR1_UE;

Здесь есть несколько моментов:

  1. В семействе F3, как и во многих других, например F0, имеется настраиваемая функция тайм-аута при тишине на линии; этот таймер отсчитывает от последнего полученного стопового бита и сбрасывается, если был получен следующий кадр.

    Мы будем использовать прерывание по таймауту, чтобы определить окончание отправки.

    Кстати, в серии F1 такой функции не было, поэтому пришлось использовать аппаратный таймер.

    Битовые прерывания включены USART_CR1_RTOIE в реестре CR1 .

    Важно отметить, что не все USART на борту могут иметь эту функцию, поэтому внимательно читайте РМ!

  2. Таймаут настраивается через регистр РТОР .

    Он содержит значение таймаута в битах, то есть длиной 3,5 символа, что означает окончание отправки соответствует значению 35 (1 символ — 8 бит + 1 стартовый бит + 1 стоповый бит).

    Для скоростей более 19200 бод/с можно использовать интервал 1,75 мс, который также можно выразить в длине символов:

    MB_USART->RTOR = 0.00175 * MB_USART_BAUDRATE + 1;

  3. Мы будем использовать прерывание по таймауту, чтобы определить конец сообщения и разбудить задачу OC, поэтому приоритет прерывания должен быть указан как минимум как configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY или выше, поскольку это прерывание использует функцию FreeRTOS, например ОтISR а если вы укажете более высокий приоритет, могут произойти плохие вещи, в том числе полная блокировка задачи.

    Это определение обычно определяется в файле FreeRTOS_Config.h, вы можете прочитать его здесь

  4. RS485 настроен с двумя битовыми полями: USART_CR1_DEAT И USART_CR1_DEDT .

    Эти битовые поля позволяют установить время снятия и установки сигнала DE до и после отправки в размерностях 1/16 или 1/8 бит, в зависимости от параметра передискретизации модуля USART. Остается только включить функцию в реестре CR3 кусочек USART_CR3_DEM , обо всем остальном позаботится оборудование.

Настройка DMA: Настройка прямого доступа к памяти

/*Configure DMA Rx/Tx channels*/ //Rx channel //Max priority //Memory increment //Transfer complete interrupt //Transfer error interrupt MB_DMA_RX_CH->CCR = 0; MB_DMA_RX_CH->CCR |= (DMA_CCR_PL | DMA_CCR_MINC | DMA_CCR_TCIE | DMA_CCR_TEIE); MB_DMA_RX_CH->CPAR = (uint32_t)&MB_USART->RDR; MB_DMA_RX_CH->CMAR = (uint32_t)MB_Frame; /*Set highest priority to Rx DMA*/ NVIC_SetPriority(MB_DMA_RX_IRQn, 0); NVIC_EnableIRQ(MB_DMA_RX_IRQn); //Tx channel //Max priority //Memory increment //Transfer complete interrupt //Transfer error interrupt MB_DMA_TX_CH->CCR = 0; MB_DMA_TX_CH->CCR |= (DMA_CCR_PL | DMA_CCR_MINC | DMA_CCR_DIR | DMA_CCR_TCIE | DMA_CCR_TEIE); MB_DMA_TX_CH->CPAR = (uint32_t)&MB_USART->TDR; MB_DMA_TX_CH->CMAR = (uint32_t)MB_Frame; /*Set highest priority to Tx DMA*/ NVIC_SetPriority(MB_DMA_TX_IRQn, 0); NVIC_EnableIRQ(MB_DMA_TX_IRQn);

Поскольку Modbus работает в режиме запрос-ответ, мы используем один буфер и для приема, и для передачи.

Они получили его в буфере, обработали там и отправили оттуда.

Во время обработки ввод не принимается.

Канал Rx DMA помещает данные из регистра приема USART (RDR) в буфер, канал Tx DMA, наоборот, из буфера в регистр отправки (TDR).

Нам нужно прервать канал Tx, чтобы определить, что ответ получен и мы можем переключиться в режим приема.

Прерывать канал Rx по сути не обязательно, поскольку мы предполагаем, что посылка Modbus не может быть больше 256 байт, но что, если на линии есть шум и кто-то отправляет байты хаотично? Для этого я сделал буфер размером 257 байт, и если происходит прерывание от Rx DMA, то это означает, что кто-то громит линию, и мы переносим канал Rx в начало буфера и слушаем снова.

Обработчики прерываний: Обработчики прерываний

/*DMA Rx interrupt handler*/ void MB_DMA_RX_IRQ_HANDLER(void) { if(MB_DMA->ISR & (DMA_ISR_TCIF1 << ((MB_DMA_RX_CH_NUM - 1) << 2))) MB_DMA->IFCR |= (DMA_IFCR_CTCIF1 << ((MB_DMA_RX_CH_NUM - 1) << 2)); if(MB_DMA->ISR & (DMA_ISR_TEIF1 << ((MB_DMA_RX_CH_NUM - 1) << 2))) MB_DMA->IFCR |= (DMA_IFCR_CTEIF1 << ((MB_DMA_RX_CH_NUM - 1) << 2)); /*If error happened on transfer or MB_MAX_FRAME_SIZE bytes received - start listening*/ MB_RecieveFrame(); } /*DMA Tx interrupt handler*/ void MB_DMA_TX_IRQ_HANDLER(void) { MB_DMA_TX_CH->CCR &= ~(DMA_CCR_EN); if(MB_DMA->ISR & (DMA_ISR_TCIF1 << ((MB_DMA_TX_CH_NUM - 1) << 2))) MB_DMA->IFCR |= (DMA_IFCR_CTCIF1 << ((MB_DMA_TX_CH_NUM - 1) << 2)); if(MB_DMA->ISR & (DMA_ISR_TEIF1 << ((MB_DMA_TX_CH_NUM - 1) << 2))) MB_DMA->IFCR |= (DMA_IFCR_CTEIF1 << ((MB_DMA_TX_CH_NUM - 1) << 2)); /*If error happened on transfer or transfer completed - start listening*/ MB_RecieveFrame(); } /*USART interrupt handler*/ void MB_USART_IRQ_HANDLER(void) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; if(MB_USART->ISR & USART_ISR_RTOF) { MB_USART->ICR = 0xFFFFFFFF; //MB_USART->ICR |= USART_ICR_RTOCF; MB_USART->CR2 &= ~(USART_CR2_RTOEN); /*Stop DMA Rx channel and get received bytes num*/ MB_FrameLen = MB_MAX_FRAME_SIZE - MB_DMA_RX_CH->CNDTR; MB_DMA_RX_CH->CCR &= ~DMA_CCR_EN; /*Send notification to Modbus Handler task*/ vTaskNotifyGiveFromISR(MB_TaskHandle, &xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } }

Обработчики DMA довольно просты: всё отправил — сбрасываем флаги, переходим в режим приема, получили 257 байт — ошибка кадра, сбрасываем флаги, снова переходим в режим приема.

Процессор USART сообщает нам, что поступило определенное количество данных, а затем наступает тишина.

Кадр готов, определяем количество принятых байт (максимальное количество байт приема DMA — это количество, которое осталось принять), отключаем прием и пробуждаем задачу.

Одно предостережение: раньше я использовал двоичный семафор для пробуждения задачи, но разработчики FreeRTOS рекомендуют использовать Уведомление о задаче :

Разблокировка задачи RTOS с помощью прямого уведомления происходит на 45 % быстрее и использует меньше оперативной памяти, чем разблокировка задачи с помощью двоичного семафора.

Иногда в FreeRTOS_Config.h иногда функция не входит в сборку xTaskGetCurrentTaskHandle() , в этом случае в этот файл нужно добавить строку:

#define INCLUDE_xTaskGetCurrentTaskHandle 1

Без использования семафора прошивка потеряла почти 1кБ.

Это мелочь, конечно, но приятно.

Функции отправки и получения: Отправить и получить /*Настройка DMA для режима приема*/

void MB_RecieveFrame(void) { MB_FrameLen = 0; //Clear timeout Flag*/ MB_USART->CR2 |= USART_CR2_RTOEN; /*Disable Tx DMA channel*/ MB_DMA_RX_CH->CCR &= ~DMA_CCR_EN; /*Set receive bytes num to 257*/ MB_DMA_RX_CH->CNDTR = MB_MAX_FRAME_SIZE; /*Enable Rx DMA channel*/ MB_DMA_RX_CH->CCR |= DMA_CCR_EN; } /*Configure DMA in tx mode*/ void MB_SendFrame(uint32_t len) { /*Set number of bytes to transmit*/ MB_DMA_TX_CH->CNDTR = len; /*Enable Tx DMA channel*/ MB_DMA_TX_CH->CCR |= DMA_CCR_EN; }

Обе функции повторно инициализируют каналы DMA. При приеме включается функция контроля таймаута в регистре CR2 кусочек USART_CR2_RTOEN .



КПР

Перейдем к аппаратному расчету CRC. Эта функция контроллера мне всегда мозолила глаз, но всегда как-то не получалось, в некоторых сериях нельзя было задать произвольный полином, в некоторых нельзя было изменить размерность полинома, и так на.

В F3 все нормально, и полином ставил, и размер менял, но пришлось сделать одно приседание:

uint16_t MB_GetCRC(uint8_t * buffer, uint32_t len) { MB_CRC_Init(); for(uint32_t i = 0; i < len; i++) *((__IO uint8_t *)&CRC->DR) = buffer[i]; return CRC->DR; }

Оказалось, что просто закидываем в регистр побайтно Д.

Р.

нельзя — расчет будет неверным, нужно использовать побайтовый доступ.

Я уже видел такие «фокусы» STM с SPI-модулем, в который хочется писать побайтно.



Задача



void MB_RTU_Slave_Task(void *pvParameters) { MB_TaskHandle = xTaskGetCurrentTaskHandle(); MB_HWInit(); while(1) { if(ulTaskNotifyTake(pdTRUE, portMAX_DELAY)) { uint32_t txLen = MB_TransactionHandler(MB_GetFrame(), MB_GetFrameLen()); if(txLen) MB_SendFrame(txLen); else MB_RecieveFrame(); } } }

В нем мы инициализируем указатель на задачу, это необходимо, чтобы использовать его для разблокировки через TaskNotification, инициализируем оборудование и ждем, пока придет уведомление.

При необходимости вы можете вместо этого портMAX_DELAY установите значение таймаута, чтобы определить, что в течение определенного времени не было соединения.

Если пришло уведомление, мы обрабатываем посылку, генерируем ответ и отправляем ее, но если рама приходит сломанной или не на тот адрес, мы просто ждем следующую.



/*Handle Received frame*/ static uint32_t MB_TransactionHandler(uint8_t * frame, uint32_t len) { uint32_t txLen = 0; /*Check frame length*/ if(len < MB_MIN_FRAME_LEN) return txLen; /*Check frame address*/ if(!MB_CheckAddress(frame[0])) return txLen; /*Check frame CRC*/ if(!MB_CheckCRC(*((uint16_t*)&frame[len - 2]), MB_GetCRC(frame, len - 2))) return txLen; switch(frame[1]) { case MB_CMD_READ_REGS : txLen = MB_ReadRegsHandler(frame, len); break; case MB_CMD_WRITE_REG : txLen = MB_WriteRegHandler(frame, len); break; case MB_CMD_WRITE_REGS : txLen = MB_WriteRegsHandler(frame, len); break; default : txLen = MB_ErrorHandler(frame, len, MB_ERROR_COMMAND); break; } return txLen; }

Сам обработчик особого интереса не представляет: проверка длины кадра/адреса/CRC и формирование ответа или ошибки.

Эта реализация поддерживает три основные функции: 0x03 — чтение регистров, 0x06 — запись в регистр, 0x10 — запись нескольких регистров.

Обычно этих функций мне хватает, но при желании можно легко расширить функционал.

Итак, запуск:

int main(void) { NVIC_SetPriorityGrouping(3); xTaskCreate(MB_RTU_Slave_Task, "MB", configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY + 1, NULL); vTaskStartScheduler(); }

Для работы задачи достаточно размера стека 32 x uint32_t (или 128 байт); это именно тот размер, который я установил в Define configMINIMAL_STACK_SIZE .

Для справки: я изначально ошибочно предположил, что configMINIMAL_STACK_SIZE указывается в байтах, если не хватало, добавлял еще, однако при работе с контроллерами F0, где оперативной памяти меньше, пришлось один раз посчитать стек и получилось, что configMINIMAL_STACK_SIZE указан в размерах типа портSTACK_TYPE , который определен в файле portmacro.h

#define portSTACK_TYPE uint32_t



Заключение

Эта реализация Modbus RTU оптимально использует аппаратные возможности микроконтроллера STM32F3xx. Вес выходной прошивки вместе с оптимизацией ОС и -o2 составил: Размер программы: 5492 байт, Размер данных: 112 байт. На фоне 6кБ потеря 1кБ от семафоров выглядит существенной.

Возможен перенос на другие семейства, например F0 поддерживает таймаут и RS485, но есть проблема с аппаратной CRC, поэтому можно обойтись программным методом расчета.

Также могут быть различия в обработчиках прерываний DMA; в некоторых случаях они сочетаются.

Ссылка на гитхаб Возможно, это будет кому-то полезно.

Полезные ссылки:

Теги: #Программирование микроконтроллеров #stm32 #FREERTOS #modbus rtu
Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.