STM32F3xx + FreeRTOS。具有硬件RS485和CRC的Modbus RTU,无需计时器和信号灯

你好!相对最近,我大学毕业后进入一家从事电子产品开发的小公司。我遇到的第一个问题是需要使用STM32实现Modbus RTU从站协议。我花了一半的钱写了下来,但是我开始在项目之间满足这个协议,所以我决定使用FreeRTOS重构和优化lib。



介绍



在当前项目中,我经常使用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 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;




这里有几点:



  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_DEATUSART_CR1_DEDT这些位域允许您设置在发送之前和之后以1/16或1/8位发送和接收DE信号的时间,具体取决于USART模块的过采样参数。仅保留通过USART_CR3_DEM启用CR3寄存器中的功能,其余的工作将由硬件负责。


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通道扔到缓冲区的开头,然后再次侦听。



中断处理程序:



中断处理程序
/*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

与通过二进制信号量解除阻止任务相比,通过直接通知解除阻止RTOS任务的速度提高了45%,并且使用的RAM更少

有时,在FreeRTOS_Config.h中,xTaskGetCurrentTaskHandle()函数未包含在程序集中,在这种情况下,您需要向该文件添加一行:



#define INCLUDE_xTaskGetCurrentTaskHandle 1


不使用信号灯,固件损失了将近1 kB。小事,当然,但很好。



发送和接收功能:



发送和接收
/*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通道。接收时,通过USART_CR2_RTOEN启用跟踪CR2寄存器中的超时的功能



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寄存器-读取将是错误的,必须使用字节访问。我已经在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进行解锁,初始化硬件并等到我们睡眠直到通知到达。如有必要,可以使用timeout值代替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-读取寄存器,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字节)的堆栈就足够了这是我在configMINIMAL_STACK_SIZE定义中设置的大小供参考:最初我错误地认为configMINIMAL_STACK_SIZE以字节为单位设置,但是如果我添加的不够,但是,在RAM较少的F0控制器上工作,我不得不计算一次堆栈,结果发现configMINIMAL_STACK_SIZE是portSTACK_TYPE类型的尺寸中设置的,该尺寸在文件portmacro.h

#define portSTACK_TYPE    uint32_t


结论



这种Modbus RTU实施可最佳利用STM32F3xx微控制器的硬件功能。



输出固件以及操作系统和优化-o2的权重为:程序大小:5492字节,数据大小:112字节。在6 KB的背景下,从信号量中丢失1 KB看起来很重要。



可以移植到其他系列,例如F0支持超时和RS485,但是硬件CRC存在问题,因此您可以使用软件计算方法来获得。DMA中断处理程序在组合在一起的地方可能也有所不同。



链接到github



也许对某人有用。



有用的链接:






All Articles