Всем привет! Сравнительно недавно, окончив университет, я оказался в небольшой компании, занимающейся разработкой электроники.
Одной из первых задач, с которыми я столкнулся, была необходимость реализовать протокол Modbus RTU Slave с использованием STM32. Я написал это со стыдом, но я начал видеть этот протокол от проекта к проекту и решил провести рефакторинг и оптимизировать библиотеку с помощью FreeRTOS.
Введение
В текущих проектах я часто использую комбинацию STM32F3xx + FreeRTOS, поэтому решил максимально использовать аппаратные возможности этого контроллера.В частности:
- Получение/отправка с использованием DMA
- Возможность аппаратного расчета CRC
- Возможность аппаратной поддержки RS485.
- Определение окончания посылки посредством аппаратных возможностей USART, без использования таймера
Конфигурационный файл
Для начала я решил немного упростить задачу переноса кода между проектами, хотя бы в рамках одного семейства контроллеров.Поэтому я решил написать небольшой файл 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;
Здесь есть несколько моментов:
- В семействе F3, как и во многих других, например F0, имеется настраиваемая функция тайм-аута при тишине на линии; этот таймер отсчитывает от последнего полученного стопового бита и сбрасывается, если был получен следующий кадр.
Мы будем использовать прерывание по таймауту, чтобы определить окончание отправки.
Кстати, в серии F1 такой функции не было, поэтому пришлось использовать аппаратный таймер.
Битовые прерывания включены USART_CR1_RTOIE в реестре CR1 .
Важно отметить, что не все USART на борту могут иметь эту функцию, поэтому внимательно читайте РМ!
- Таймаут настраивается через регистр РТОР .
Он содержит значение таймаута в битах, то есть длиной 3,5 символа, что означает окончание отправки соответствует значению 35 (1 символ — 8 бит + 1 стартовый бит + 1 стоповый бит).
Для скоростей более 19200 бод/с можно использовать интервал 1,75 мс, который также можно выразить в длине символов:
MB_USART->RTOR = 0.00175 * MB_USART_BAUDRATE + 1;
- Мы будем использовать прерывание по таймауту, чтобы определить конец сообщения и разбудить задачу OC, поэтому приоритет прерывания должен быть указан как минимум как configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY или выше, поскольку это прерывание использует функцию FreeRTOS, например ОтISR а если вы укажете более высокий приоритет, могут произойти плохие вещи, в том числе полная блокировка задачи.
Это определение обычно определяется в файле FreeRTOS_Config.h, вы можете прочитать его здесь
- RS485 настроен с двумя битовыми полями: USART_CR1_DEAT И USART_CR1_DEDT .
Эти битовые поля позволяют установить время снятия и установки сигнала DE до и после отправки в размерностях 1/16 или 1/8 бит, в зависимости от параметра передискретизации модуля USART. Остается только включить функцию в реестре CR3 кусочек USART_CR3_DEM , обо всем остальном позаботится оборудование.
/*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-
Компьютер На Луне
19 Oct, 24 -
Жесткий Диск Seagate Savvio 10K.4
19 Oct, 24 -
Заметки Нового Менеджера Клиентского Проекта
19 Oct, 24 -
Хабра-Люди По Версии Паблика
19 Oct, 24