[Программирование микроконтроллеров] STM32F3xx + FreeRTOS. Modbus RTU с аппаратным RS485 и CRC без таймеров и семафоров
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Всем привет! Относительно недавно, закончив ВУЗ, я попал в небольшую компанию, которая занималась разработкой электроники. Одна из первых задач с которой я столкнулся — необходимость в реализации Modbus RTU Slave протокола с использованием STM32. С грехом пополам я её тогда написал, однако этот протокол начал встречаться мне из проекта в проект и я решил написать зарефакторить и оптимизировать либу с использованием FreeRTOS.
Введение
В текущих проектах я часто использую связку STM32F3xx + FreeRTOS, поэтому решил максимально использовать аппаратные возможности данного контроллера. В частности:
- Прием/отправку с использованием DMA
- Возмоность аппаратный расчета CRC
- Возможность аппаратной поддержки RS485
- Определение конца посылки через аппаратные возможности USART, без использования таймера
Сразу оговорюсь, тут я не описываю спецификацию протокла Modbus и как с ним работает мастер, об этом можно почитать тут и тут.
Файл конфигурации
Для начала, я решил номного упростить задачу переноса кода между проектами, хотя бы в рамках одного семейства контроллеров. Поэтому я решил написать небольщой conf.h файл, который позволит быстренько переконфигурировать основные части реализации.
ModbusRTU_conf.h
SPL
#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(pin, port, rcc, irq)
- Параметры каналов DMA(rcc, irq)
- Включение/отключение аппаратного CRC и RS485
Конфигурация железа
В данной реализации я использую обычный CMSIS, не из-за религиозных убеждений, просто мне так проще и меньше зависимостей. Настройку портов я описывать не буду, это можно посмотреть по ссылке на гитхаб которая будет внизу.
Начнем с настройки USART:
USART configure
SPL
/*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:
Настройка DMA
SPL
/*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 канал в начало буфера и слушаем снова.
Обработчики прерываний:
Interrupt handlers
SPL
/*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 кБ. Мелочь конечно, но приятно.
Функции отправки и приема:
Send and Receve
SPL
/*Configure DMA to receive mode*/
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, где-то они бывают совмещенными.
Ссылка на гитхаб
Возможно кому-то пригодится.
Полезные ссылки:
===========
Источник:
habr.com
===========
Похожие новости:
- [Программирование микроконтроллеров] STM32 и LCD2004A без I2C интерфейса
- [Программирование микроконтроллеров, Производство и разработка электроники, DIY или Сделай сам, Звук] MIDI2USB – музыка нас связала
- [Программирование микроконтроллеров] Передача аналогового тв сигнала с помощью STM32
- [Программирование микроконтроллеров] Разработка измерительного прибора ИРИС
- [Беспроводные технологии, Программирование микроконтроллеров] Как подключить АЦП HX711 к NRF52832
- [C, JavaScript, Интернет вещей, Программирование микроконтроллеров, Разработка для интернета вещей] Термостат на ThingJS (beta)
- [Системное программирование, Программирование микроконтроллеров, Компьютерное железо] Моделирование прошивки в среде ModelSim с использованием моделей на языке SystemC
- [Гаджеты, Информационная безопасность, Программирование микроконтроллеров, Умный дом] Исследователь в рамках теста заразил умную кофеварку вымогателем и запустил на ней майнинг криптовалюты
- [3D-принтеры, DIY или Сделай сам, Программирование микроконтроллеров] Как управлять CNC-роутером, не привлекая внимания…
- [Интернет вещей, Программирование микроконтроллеров, Разработка для интернета вещей, Разработка систем связи] MQTTv5.0: Обзор новых функций. Часть 2
Теги для поиска: #_programmirovanie_mikrokontrollerov (Программирование микроконтроллеров), #_stm32, #_modbus_rtu, #_programmirovanie_mikrokontrollerov (
Программирование микроконтроллеров
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 19:55
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Всем привет! Относительно недавно, закончив ВУЗ, я попал в небольшую компанию, которая занималась разработкой электроники. Одна из первых задач с которой я столкнулся — необходимость в реализации Modbus RTU Slave протокола с использованием STM32. С грехом пополам я её тогда написал, однако этот протокол начал встречаться мне из проекта в проект и я решил написать зарефакторить и оптимизировать либу с использованием FreeRTOS. Введение В текущих проектах я часто использую связку STM32F3xx + FreeRTOS, поэтому решил максимально использовать аппаратные возможности данного контроллера. В частности:
Сразу оговорюсь, тут я не описываю спецификацию протокла Modbus и как с ним работает мастер, об этом можно почитать тут и тут. Файл конфигурации Для начала, я решил номного упростить задачу переноса кода между проектами, хотя бы в рамках одного семейства контроллеров. Поэтому я решил написать небольщой conf.h файл, который позволит быстренько переконфигурировать основные части реализации. ModbusRTU_conf.hSPL#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 */ Наиболее часто, на мой взгляд, меняются следующие вещи:
Конфигурация железа В данной реализации я использую обычный CMSIS, не из-за религиозных убеждений, просто мне так проще и меньше зависимостей. Настройку портов я описывать не буду, это можно посмотреть по ссылке на гитхаб которая будет внизу. Начнем с настройки USART: USART configureSPL/*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; Тут есть несколько моментов:
Натсройка DMA: Настройка DMASPL/*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 канал в начало буфера и слушаем снова. Обработчики прерываний: Interrupt handlersSPL/*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
#define INCLUDE_xTaskGetCurrentTaskHandle 1
Без использования семафора прошивка похудела почти на 1 кБ. Мелочь конечно, но приятно. Функции отправки и приема: Send and ReceveSPL/*Configure DMA to receive mode*/
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, где-то они бывают совмещенными. Ссылка на гитхаб Возможно кому-то пригодится. Полезные ссылки: =========== Источник: habr.com =========== Похожие новости:
Программирование микроконтроллеров ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 19:55
Часовой пояс: UTC + 5