Введение
В текущих проектах я часто использую связку STM32F3xx + FreeRTOS, поэтому решил максимально использовать аппаратные возможности данного контроллера. В частности:
- Прием/отправку с использованием DMA
- Возмоность аппаратный расчета CRC
- Возможность аппаратной поддержки RS485
- Определение конца посылки через аппаратные возможности USART, без использования таймера
Сразу оговорюсь, тут я не описываю спецификацию протокла Modbus и как с ним работает мастер, об этом можно почитать тут и тут.
Файл конфигурации
Для начала, я решил номного упростить задачу переноса кода между проектами, хотя бы в рамках одного семейства контроллеров. Поэтому я решил написать небольщой 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 support1 - enabledother - 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 enable1 - enabledother - disabled */ #define MB_HARDWARE_CRC 1#endif /* MODBUSRTU_CONF_H_INCLUDED */
Наиболее часто, на мой взгляд, меняются следующие вещи:
- Адрес устройства и размер адресного простарнства
- Частота тактирования и параметры пинов USART(pin, port, rcc, irq)
- Параметры каналов DMA(rcc, irq)
- Включение/отключение аппаратного CRC и RS485
Конфигурация железа
В данной реализации я использую обычный CMSIS, не из-за религиозных убеждений, просто мне так проще и меньше зависимостей. Настройку портов я описывать не буду, это можно посмотреть по ссылке на гитхаб которая будет внизу.
Начнем с настройки 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 в регистре СR1. Важно отметить, что не все USART на борту могут иметь эту функцию, так что внимательней читайте RM!
- Таймаут настраивается через регистр RTOR. В него
заносится значение таймаута в битах, то есть длина 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 типа FromISR и если указать приоритет выше, могут случиться нехорошие вещи вплоть до полной блокировки таска. Этот дефайн обычно определен в файле FreeRTOS_Config.h, почитать можно тут
- RS485 настраивается двумя битфилдами: USART_CR1_DEAT и USART_CR1_DEDT. Эти битфилды подволяют задать время снятия и установки сигнала DE до и после отправки в размерностях 1/16 или 1/8 бита в зависимости от параметра oversampling модуля 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 рекомендуют использовать TaskNotification:
Unblocking an RTOS task with a direct notification is 45% faster and uses less RAM than unblocking a task with a binary semaphoreИногда в FreeRTOS_Config.h бывает не включена в сборку функция xTaskGetCurrentTaskHandle(), в таком случае нужно добавить строку в этой файл:
#define INCLUDE_xTaskGetCurrentTaskHandle 1
Без использования семафора прошивка похудела почти на 1 кБ. Мелочь конечно, но приятно.
Функции отправки и приема:
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
Переходим к хардварному расчету 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;}
Оказалось, что просто так побайтно закидывать в регистр DR нельзя считать будет неправильно, надо использовать byte-access. Такие выкрутасы у 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, инициализируем железо и ждем спим пока не придет уведомление. Если необходимо, можно вместо portMAX_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 Read Registers, 0x06 Write register, 0x10 Write Multiple Registers. Обычно, мне достаточно этих функций, но при желании можно без проблем расширить функционал.
Ну и запуск:
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 байт) именно такой размер у меня выставлен в дефайне configMINIMAL_STACK_SIZE. Для справки: изначально я ошибочно предполагал, что configMINIMAL_STACK_SIZE задается в байтах, если не хватало добавлял еще, однако, работая с F0 контроллерами, где RAM поменьше, один раз пришлось посчитать стек и оказалось, что configMINIMAL_STACK_SIZE задается в размерностях типаportSTACK_TYPE, который определен в файле portmacro.h
#define portSTACK_TYPE uint32_t
Заключение
Данная реализация Modbus RTU оптимально использует аппаратные возможности микроконтроллера STM32F3xx.
Вес выходной прошивки вместе с ОС и оптимизацией -o2 составил: Program size: 5492 Байта, Data size: 112 байт. На фоне 6 кБ похудение на 1 кБ от семафоров, выглядит существенно.
Перенос на другие семейства возможен, например F0 поддерживает таймаут и RS485, однако там есть проблема с аппаратным CRC, так что можно обойтись софтовым методом расчета. Также могут быть различия в обработчиках прерываний DMA, где-то они бывают совмещенными.
Ссылка на гитхаб
Возможно кому-то пригодится.
Полезные ссылки: