Русский
Русский
English
Статистика
Реклама

Из песочницы 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 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:

USART configure
    /*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 в регистре СR1. Важно отметить, что не все USART на борту могут иметь эту функцию, так что внимательней читайте RM!
  2. Таймаут настраивается через регистр RTOR. В него заносится значение таймаута в битах, то есть длина 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 типа FromISR и если указать приоритет выше, могут случиться нехорошие вещи вплоть до полной блокировки таска. Этот дефайн обычно определен в файле FreeRTOS_Config.h, почитать можно тут
  4. RS485 настраивается двумя битфилдами: USART_CR1_DEAT и USART_CR1_DEDT. Эти битфилды подволяют задать время снятия и установки сигнала DE до и после отправки в размерностях 1/16 или 1/8 бита в зависимости от параметра oversampling модуля USART. Остается только включить функцию в регистре CR3 битом USART_CR3_DEM, обо всем остальном позаботится железо.

Натсройка DMA:

Настройка 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 канал в начало буфера и слушаем снова.

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

Interrupt handlers
/*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
/*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
К списку статей
Опубликовано: 11.10.2020 18:05:36
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Программирование микроконтроллеров

Stm32

Modbus rtu

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru