【STM32】RTT Studio中HAL库UART DMA应用深度教程

文章目录

  • 一、简介
  • 1.关于DMA
  • 2.DMA使用场景
  • 3.DMA控制结构
  • 4.IDLE空闲中断
  • 5.实现方法
  • 二、RTT配置
  • 三、串口收发流程
  • 四、完整代码
  • 五、测试验证
  • 一、简介

    1.关于DMA

       DMA(Direct Memory Access,直接存储器访问) 是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依赖于CPU的大量中断负载。否则,CPU需要从来源把每一片段的资料复制到暂存器,然后把它们再次写回到新的地方。在这个时间中,CPU对于其他的工作来说就无法使用。
      简单的来说,能控制主存内部读写,这样有利于减轻CPU负担,加快读取速度。
      在没有DMA之前,串口每次发送数据时都要由CPU将源地址上的数据拷贝到串口发送的相关寄存器上;串口每次接收数据时都要由CPU将发送来的数据拷贝到主存上。而加了DMA后,只需要告诉DMA源地址和目标地址,DMA通道就能够自动进行数据的转移,即CPU只需要告诉DMA:串口需要发送的数据在哪里,串口接收到的数据应该存在哪里,运输的工作则交由DMA去做,运输期间CPU就可以去处理别的事情,这就大大提高了CPU的运行效率。


    2.DMA使用场景

    DMA用在只需要传输数据,不需要处理数据的地方,有三种传输方式:

  • 外设→存储器 (例如:将串口RDR寄存器写入某数据buf)
  • 存储器→外设 (例如:将某数据buf写入串口TDR寄存器)
  • 存储器→存储器(例如:复制某特别大的数据buf)

  • 3.DMA控制结构

    Stm32F4最多有:2个DMA控制器,各8个数据流,每个数据流有8个通道(或请求),每个通道有一个仲裁器,用于处理请求的优先级。


    4.IDLE空闲中断

    (1)STM32 IDLE 接收空闲中断—接受完一帧数据,触发中断
      在使用串口接受字符串时,可以使用空闲中断(IDLEIE置1,即可使能空闲中断),这样在接收完一个字符串,进入空闲状态时,即将IDLE置1,便会激发一个空闲中断。在中断处理函数,我们可以解析这个字符串。

    (2)STM32的IDLE的中断产生条件
      在串口无数据接收的情况下,不会产生,当清除IDLE标志位后,必须有接收到第一个数据后,才开始触发,一但接收的数据断流,没有接收到数据,即产生IDLE中断

    (3)STM32 RXNE接收数据中断—接受到一个字节的数据,触发中断
      当串口接收到一个bit的数据时,(读取到一个停止位) 便会触发 RXNE接收数据中断


    5.实现方法

    利用串口IDLE空闲中断的方式接收一帧数据,方法如下:

  • 选择一个串口,并配置成空闲中断IDLE模式且使能DMA接收,并同时设置接收缓冲区和初始化DMA。
  • 初始化完成之后,当外部给单片机发送数据的时候,假设这次接受的数据长度是200个字节,那么在单片机接收到一个字节的时候并不会产生串口中断,而是DMA在后台把数据默默地搬运到你指定的缓冲区数组里面。
  • 当整帧数据发送完毕之后串口才会产生一次中断,此时可以利用__HAL_DMA_GET_COUNTER(&hdma_usart1_rx)。
  • 函数计算出当前DMA接收存储空间剩余字节:本次的数据接受长度=预先定义的接收总字节-接收存储空间剩余字节。
  • 例如:本次串口接受200个字节,HAL_UART_Receive_DMA(&huart1,rx_buffer,200);//打开DMA接收。然后我发送了HelloWord9个字节的数据长度,那么此时 GET_COUNTER函数读出来 接收存储空间剩余字节 就是191个字节,实际接受的字节(9) = 预先定义的接收总字节(200) – __HAL_DMA_GET_COUNTER()(191):9 = 200-191


    二、RTT配置

    (1)配置串口:主要是配置使用的串口号,以及打开RTT所需要的驱动函数。

    /** After configuring corresponding UART or UART DMA, you can use it.
     *
     * STEP 1, define macro define related to the serial port opening based on the serial port number
     *                 such as     #define BSP_USING_UART1
     *
     * STEP 2, according to the corresponding pin of serial port, define the related serial port information macro
     *                 such as     #define BSP_UART1_TX_PIN       "PA9"
     *                             #define BSP_UART1_RX_PIN       "PA10"
     *
     * STEP 3, if you want using SERIAL DMA, you must open it in the RT-Thread Settings.
     *                 RT-Thread Setting -> Components -> Device Drivers -> Serial Device Drivers -> Enable Serial DMA Mode
     *
     * STEP 4, according to serial port number to define serial port tx/rx DMA function in the board.h file
     *                 such as     #define BSP_UART1_RX_USING_DMA
     *
     */
    
    #define BSP_USING_UART1
    #define BSP_UART1_TX_PIN       "PA9"
    #define BSP_UART1_RX_PIN       "PA10"
    
    #define BSP_USING_UART3
    #define BSP_UART3_TX_PIN       "PD8"
    #define BSP_UART3_RX_PIN       "PD9"
    

    (2)配置初始化函数:主要是使用CubeMx配置生成串口的初始化代码,并将代码复制到board.c中。

    extern DMA_HandleTypeDef hdma_usart3_rx;
    extern DMA_HandleTypeDef hdma_usart3_tx;
    
    /**
    * @brief USART MSP Initialization
    * This function configures the hardware resources used in this example
    * @param husart: USART handle pointer
    * @retval None
    */
    void HAL_UART_MspInit(UART_HandleTypeDef* huart)
    {
        GPIO_InitTypeDef GPIO_InitStruct = {0};
        if (huart->Instance == USART3)
        {
            /* Peripheral clock enable */
            __HAL_RCC_USART3_CLK_ENABLE();
            __HAL_RCC_GPIOD_CLK_ENABLE();
    
            /**USART3 GPIO Configuration
             PD8     ------> USART3_TX
             PD9     ------> USART3_RX
             */
            GPIO_InitStruct.Pin = GPIO_PIN_8 | GPIO_PIN_9;
            GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
            GPIO_InitStruct.Pull = GPIO_PULLUP;
            GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
            GPIO_InitStruct.Alternate = GPIO_AF7_USART3;
            HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);
    
            /* USART3 DMA Init */
            /* USART3_RX Init */
            hdma_usart3_rx.Instance = DMA1_Stream1;
            hdma_usart3_rx.Init.Channel = DMA_CHANNEL_4;
            hdma_usart3_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
            hdma_usart3_rx.Init.PeriphInc = DMA_PINC_DISABLE;
            hdma_usart3_rx.Init.MemInc = DMA_MINC_ENABLE;
            hdma_usart3_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
            hdma_usart3_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
            hdma_usart3_rx.Init.Mode = DMA_NORMAL;
            hdma_usart3_rx.Init.Priority = DMA_PRIORITY_LOW;
            hdma_usart3_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
            if (HAL_DMA_Init(&hdma_usart3_rx) != HAL_OK)
            {
                Error_Handler();
            }
    
            __HAL_LINKDMA(huart, hdmarx, hdma_usart3_rx);
    
            /* USART3_TX Init */
            hdma_usart3_tx.Instance = DMA1_Stream3;
            hdma_usart3_tx.Init.Channel = DMA_CHANNEL_4;
            hdma_usart3_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
            hdma_usart3_tx.Init.PeriphInc = DMA_PINC_DISABLE;
            hdma_usart3_tx.Init.MemInc = DMA_MINC_ENABLE;
            hdma_usart3_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
            hdma_usart3_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
            hdma_usart3_tx.Init.Mode = DMA_NORMAL;
            hdma_usart3_tx.Init.Priority = DMA_PRIORITY_LOW;
            hdma_usart3_tx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
            if (HAL_DMA_Init(&hdma_usart3_tx) != HAL_OK)
            {
                Error_Handler();
            }
    
            __HAL_LINKDMA(huart, hdmatx, hdma_usart3_tx);
    
            /* USART3 interrupt Init */
            HAL_NVIC_SetPriority(USART3_IRQn, 0, 0);
            HAL_NVIC_EnableIRQ(USART3_IRQn);
        }
    }
    

    三、串口收发流程

    (1)接收数据的流程:

  • 首先在初始化的时候打开DMA接收
  • 当MCU通过USART接收外部发来的数据时,在进行第①②③步的时候,DMA直接将接收到的数据写入缓存rx_buffer[100] //接收数据缓存数组
  • 程序此时也不会进入接收中断,在软件上无需做任何事情,要在初始化配置的时候设置好配置就可以了。
  • (2)数据接收完成的流程:

  • 当数据接收完成之后产生接收空闲中断④
  • 在中断服务函数中:
      a:判断是否为IDLE接受空闲中断
      b:在中断服务函数中将接收完成标志位置1
      c:关闭DMA防止在处理数据时候接收数据,产生干扰。
      d:计算出接收缓存中的数据长度,清除中断位,
  • while循环 主程序流程:
      a:主程序中检测到接收完成标志被置1
      b:进入数据处理程序,现将接收完成标志位置0,
      c:将接收到的数据重新发送到上位机
      d:重新设置DMA下次要接收的数据字节数,使能DMA进入接收数据状态。

  • 四、完整代码

    1.uart.c文件

    #include "uart.h"
    
    volatile uint8_t rx_len = 0;            // 接收一帧数据的长度
    volatile uint8_t recv_end_flag = 0;     // 一帧数据接收完成标志
    uint8_t rx_buffer[100]={0};             // 接收数据缓存数组
    
    void MX_USART3_UART_Init(void)
    {
        /* DMA controller clock enable */
        __HAL_RCC_DMA1_CLK_ENABLE();
    
        huart3.Instance = USART3;
        huart3.Init.BaudRate = 115200;
        huart3.Init.WordLength = UART_WORDLENGTH_8B;
        huart3.Init.StopBits = UART_STOPBITS_1;
        huart3.Init.Parity = UART_PARITY_NONE;
        huart3.Init.Mode = UART_MODE_TX_RX;
        huart3.Init.HwFlowCtl = UART_HWCONTROL_NONE;
        huart3.Init.OverSampling = UART_OVERSAMPLING_16;
        if (HAL_UART_Init(&huart3) != HAL_OK)
        {
            Error_Handler();
        }
    
        /* DMA interrupt init */
        /* DMA1_Stream1_IRQn interrupt configuration */
        HAL_NVIC_SetPriority(DMA1_Stream1_IRQn, 0, 0);
        HAL_NVIC_EnableIRQ(DMA1_Stream1_IRQn);
    
        /* DMA1_Stream3_IRQn interrupt configuration */
        HAL_NVIC_SetPriority(DMA1_Stream3_IRQn, 0, 0);
        HAL_NVIC_EnableIRQ(DMA1_Stream3_IRQn);
    
        __HAL_UART_ENABLE_IT(&huart3, UART_IT_IDLE); //使能IDLE中断
        //DMA接收函数,此句一定要加,不加接收不到第一次传进来的实数据,是空的,且此时接收到的数据长度为缓存器的数据长度
        HAL_UART_Receive_DMA(&huart3, rx_buffer, BUFFER_SIZE);
    }
    
    /**
     * @brief 串口发送功能函数
     * @param buf:发送数据
     * @param len:数据长度
     */
    void DMA_Usart_Send(uint8_t *buf,uint8_t len)
    {
        // 判断是否发送正常,如果出现异常则进入异常中断函数
        if (HAL_UART_Transmit_DMA(&huart3, buf, len) != HAL_OK)
        {
            Error_Handler();
        }
    }
    
    /**
     * @brief 串口接收功能函数
     * @param Data:接收数据
     * @param len:接收长度
     */
    void DMA_Usart1_Read(uint8_t *Data,uint8_t len)
    {
        // 重新打开DMA接收
        HAL_UART_Receive_DMA(&huart3, Data, len);
    }
    
    /**
      * @brief This function handles DMA1 stream1 global interrupt.
      */
    void DMA1_Stream1_IRQHandler(void)
    {
        HAL_DMA_IRQHandler(&hdma_usart3_rx);
    }
    
    /**
      * @brief This function handles DMA1 stream3 global interrupt.
      */
    void DMA1_Stream3_IRQHandler(void)
    {
        HAL_DMA_IRQHandler(&hdma_usart3_tx);
    }
    
    /**
      * @brief This function handles USART3 global interrupt.
      */
    void USART3_IRQHandler(void)
    {
        uint32_t tmp_flag = 0;
        uint32_t temp;
    
        tmp_flag = __HAL_UART_GET_FLAG(&huart3, UART_FLAG_IDLE);    // 获取IDLE标志位
        if ((tmp_flag != RESET))                                    // idle标志被置位
        {
    #if 1
            __HAL_UART_CLEAR_IDLEFLAG(&huart3);                     // 清除标志位
    #else
            temp = huart3.Instance->SR;                             // 清除状态寄存器SR,读取SR寄存器可以实现清除SR寄存器的功能
            temp = huart3.Instance->DR;                             // 读取数据寄存器中的数据
    #endif
    
            HAL_UART_DMAStop(&huart3);                              // 停止DMA传输,防止干扰
    
    #if 1
            temp = __HAL_DMA_GET_COUNTER(&hdma_usart3_rx);          // 获取DMA中未传输的数据个数
    #else
            temp  = hdma_usart3_rx.Instance->NDTR;                  // 读取NDTR寄存器,获取DMA中未传输的数据个数,
    #endif
    
            rx_len = BUFFER_SIZE - temp;                            // 总计数减去未传输的数据个数,得到已经接收的数据个数
            recv_end_flag = 1;                                      // 接受完成标志位置1
        }
    
        HAL_UART_Receive_DMA(&huart3, rx_buffer, BUFFER_SIZE);      // 重新打开DMA接收
        HAL_UART_IRQHandler(&huart3);
    }
    

    2.uart.h文件

    #ifndef APPLICATIONS_UART_H_
    #define APPLICATIONS_UART_H_
    
    #include <rtthread.h>
    #include <drv_common.h>
    
    UART_HandleTypeDef huart3;
    DMA_HandleTypeDef hdma_usart3_rx;
    DMA_HandleTypeDef hdma_usart3_tx;
    
    #define BUFFER_SIZE  100
    
    extern  volatile uint8_t rx_len ;       // 接收一帧数据的长度
    extern volatile uint8_t recv_end_flag;  // 一帧数据接收完成标志
    extern uint8_t rx_buffer[100];          // 接收数据缓存数组
    
    extern void MX_USART3_UART_Init(void);
    extern void DMA_Usart_Send(uint8_t *buf,uint8_t len);
    extern void DMA_Usart1_Read(uint8_t *Data,uint8_t len);
    
    #endif /* APPLICATIONS_UART_H_ */
    

    3.main.c文件

    #include <rtthread.h>
    
    #define DBG_TAG "main"
    #define DBG_LVL DBG_LOG
    #include <rtdbg.h>
    #include <string.h>
    #include "uart.h"
    
    int main(void)
    {
        int count = 1;
    
        MX_USART3_UART_Init();
    
        while (count)
        {
            if (recv_end_flag == 1)     // 接收完成标志
            {
                DMA_Usart_Send(rx_buffer, rx_len);
                rx_len = 0;             // 清除计数
                recv_end_flag = 0;      // 清除接收结束标志位
                memset(rx_buffer, 0, rx_len);
            }
        }
    
        return RT_EOK;
    }
    

    五、测试验证

      通过将程序下载到单片机中,可以使用串口助手来进行数据的发送,如下可以看到可以进行正常数据的收发,并且使用uart的DMA功能,可以更好的改善CPU的运行效率,并且也不用暂用CPU,加快器性能。


    作者:0南城逆流0

    物联沃分享整理
    物联沃-IOTWORD物联网 » 【STM32】RTT Studio中HAL库UART DMA应用深度教程

    发表回复