STM32F3xx + FreeRTOS. Modbus RTU with hardware RS485 and CRC without timers and semaphores

Hello! Relatively recently, after graduating from high school, I got into a small company that was engaged in the development of electronics. One of the first problems I encountered was the need to implement the Modbus RTU Slave protocol using STM32. With a sin in half, I then wrote it, but I started to meet this protocol from project to project and I decided to refactor and optimize lib using FreeRTOS.



Introduction



In current projects, I often use the STM32F3xx + FreeRTOS bundle, so I decided to make the most of the hardware capabilities of this controller. In particular:



  • Receiving / sending using DMA
  • Possibility of hardware CRC calculation
  • RS485 hardware support
  • End of parcel detection via USART hardware capabilities, without using a timer


I'll make a reservation right away, here I am not describing the specification of the Modbus protocol and how the master works with it, you can read about this here and here .



configuration file



To begin with, I decided to simplify the task of transferring code between projects, at least within the same family of controllers. So I decided to write a small conf.h file that would allow me to quickly reconfigure the main parts of the implementation.



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 */




Most often, in my opinion, the following things change:



  • Device address and address space size
  • Clock frequency and parameters of USART pins (pin, port, rcc, irq)
  • DMA channel parameters (rcc, irq)
  • Enable / Disable Hardware CRC and RS485


Iron configuration



In this implementation, I use the usual CMSIS, not because of religious beliefs, it's just easier for me and less dependencies. I will not describe the port settings, you can see it at the link to the github which will be below.



Let's start by setting up the 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;




There are several points here:



  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 is configured with two bitfields: USART_CR1_DEAT and USART_CR1_DEDT . These bitfields allow you to set the time for removing and setting the DE signal before and after sending in 1/16 or 1/8 bits, depending on the oversampling parameter of the USART module. It remains only to enable the function in the CR3 register with the USART_CR3_DEM bit , the hardware will take care of the rest.


DMA setting:



DMA setup
    /*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);




Since Modbus operates in a request-response mode, we use one buffer for both reception and transmission. Received in the buffer, processed there and sent from it. No input is accepted during processing. The Rx DMA channel puts data from the USART receive register (RDR) into the buffer, the Tx DMA channel, on the contrary, from the buffer into the send register (TDR). We need to interrupt the Tx channel to determine that the answer is gone and we can switch to receive mode.



Interrupting the Rx channel is essentially unnecessary, because we assume that the Modbus package cannot be more than 256 bytes, but what if there is noise on the line and someone is randomly sending bytes? To do this, I made a buffer of 257 bytes, and if an Rx DMA interrupt happens, it means that someone is "littering" the line, and we throw the Rx channel to the beginning of the buffer and listen again.



Interrupt handlers:



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 handlers are quite simple: sent everything - clean the flags, switch to receive mode, received 257 bytes - frame error, clean moisture, switch to receive mode again.



The USART processor tells us that a certain amount of data came in and then there was silence. The frame is ready, we determine the number of bytes received (the maximum number of DMA receive bytes - the amount that remains to be received), turn off the reception, wake up the task.



One caveat, I used to use a binary semaphore to wake up the task, but the FreeRTOS developers recommend using TaskNotification :

Unblocking an RTOS task with a direct notification is 45% faster and uses less RAM than unblocking a task with a binary semaphore

Sometimes in FreeRTOS_Config.h the xTaskGetCurrentTaskHandle () function is not included in the assembly , in which case you need to add a line to this file:



#define INCLUDE_xTaskGetCurrentTaskHandle 1


Without using a semaphore, the firmware has lost almost 1 kB. A trifle, of course, but nice.



Send and receive functions:



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;
}


Both functions re-initialize DMA channels. When receiving, the function tracking the timeout in the CR2 register is enabled by the USART_CR2_RTOEN bit .



CRC



Let's move on to the hardcore CRC calculation. This function of the eye controller always annoyed me, but somehow it never worked out, in some series it was impossible to set an arbitrary polynomial, in some series it was impossible to change the dimension of the polynomial, and so on. In F3, everything is fine, and set the polynomial and change the size, but I had to do one squat:



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;
}


It turned out that it is impossible to just throw byte-by-byte into the DR register - it will be wrong to read, you must use byte-access. I have already met such "freaks" in STM with the SPI module in which I want to write byte-by-byte.



Task



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();
        }
    }
}


In it, we initialize the pointer to the task, this is necessary in order to use it to unlock through TaskNotification, initialize the hardware and wait until we sleep until the notification arrives. If necessary, you can put a timeout value instead of portMAX_DELAY to determine that there has been no connection for a certain time. If the notification has arrived, we process the parcel, form a response and send it, but if the frame has arrived broken or at the wrong address, we just wait for the next one.



/*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;
}


The handler itself is of no particular interest: checking the frame / address / CRC length and generating a response or error. This implementation supports three main functions: 0x03 - Read Registers, 0x06 - Write register, 0x10 - Write Multiple Registers. Usually, these functions are enough for me, but if you wish, you can expand the functionality without problems.



Well, start:



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


For the task to work, a stack with a size of 32 x uint32_t (or 128 bytes) is enough ; this is the size I set in the configMINIMAL_STACK_SIZE definition . For reference: initially I mistakenly assumed that configMINIMAL_STACK_SIZE is set in bytes, if I didn’t add enough, however, working with F0 controllers, where there is less RAM, I had to count the stack once and it turned out that configMINIMAL_STACK_SIZE was set in dimensions of the portSTACK_TYPE type , which is defined in file portmacro.h

#define portSTACK_TYPE    uint32_t


Conclusion



This Modbus RTU implementation makes optimal use of the hardware capabilities of the STM32F3xx microcontroller.



The weight of the output firmware together with the OS and the -o2 optimization was: Program size: 5492 Bytes, Data size: 112 bytes. Against the background of 6 KB, losing 1 KB from semaphores looks significant.



Portability to other families is possible, for example F0 supports timeout and RS485, but there is a problem with the hardware CRC, so you can get by with the software calculation method. There may also be differences in the DMA interrupt handlers, somewhere they are combined.



Link to github



Perhaps it will be useful to someone.



Useful links:






All Articles