DMA与空闲中断下的串口非阻塞收发技术详解:HAL库学习心得

DMA + 空闲中断的串口不定长非阻塞收发(HAL库学习笔记)

  • 代码的相应解释
  • 代码功能
  • 相关术语的解释
  • 阻塞与非阻塞方式的区别
  • 接收完成中断回调函数
  • STM32CubeMX 配置
  • 代码编写
  • 代码逻辑
  • 测试结果
  • 代码的相应解释

    代码功能

    本次代码为 DMA+空闲中断来实现串口的不定长收发(非阻塞方式)
    适合需要快速响应和低CPU占用的嵌入式场景

    相关术语的解释

    阻塞与非阻塞方式的区别

    阻塞:函数调用后,程序会一直等待直到数据传输完成才继续执行
    不断检查串口状态寄存器直到操作完成,通常允许设置超时时间,防止无限等待。CPU长时间处于等待状态,浪费计算资源对于简单的项目可能干扰不大,对于多线程的任务,采用阻塞的方式往往程序会卡死。
    代码示例

     `HAL_UART_Transmit(&huart1, data, sizeof(data), 100);
      HAL_UART_Receive(&huart1, buffer, sizeof(buffer), 100);`
    

    非阻塞式:函数调用后立即返回,数据传输在后台进行,允许CPU在传输期间处理其他任务,适合大数据量或高频率通信,不占用大量的CPU资源,相比阻塞方式代码更为复杂需要用到中断或者是DMA
    代码示例

    HAL_UART_Transmit_IT(&huart1, data, sizeof(data));//DMA方式
    
    HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buffer, sizeof(rx_buffer));
    //中断方式
    // 发送完成回调函数
    void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
        if (huart == &huart1) {
            // 处理发送完成逻辑
        }
    }
    

    接收完成中断回调函数

    下面这个函数为串口的接收完成中断回调函数,也是我们本次功能实现的核心代码
    这个回调函数会有两种情况被触发:
    1.总线空闲中断(Idle Interrupt):当UART总线在一段时间内无新数据(即总线空闲)时触发。
    2.DMA接收完成(DMA Transfer Complete):当DMA接收缓冲区填满(达到设定的最大长度)时触发。
    也就是当Size这个参数等于缓冲区的大小,文章后面会给出实例代码

    void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size{
    }
    

    STM32CubeMX 配置


    选择外部高速时钟HSE

    选择Dubug方式

    选择Asynchronous(异步通信)其他参数如波特率 校验位 停止位 默认初始就好

    同步通信:发送方在发送数据时,接收方必须处于接收状态,数据传输是实时进行的。数据传输准确但占用资源 例如(SPI I2C
    异步通信:发送方可以在任何时间发送数据,而接收方可以在任意时间接收数据,双方的操作相互独立。时间独立适应性强,协议设计相对复杂(例如串口)
    异步通信不需要共享时钟信号 不需要clk信号线,串口仅需要两根线(Tx Rx


    在串口页面点击DMA Setting配置DMA通道点击Add添加两个通道,并且为两个通道更改对应的串口名字
    DMA可以理解为不占用CPU资源 找个黑奴给你搬砖
    数据搬运方向(Direction)
    TX: 内存(Memory)–>外设(Peripheral)
    RX: 外设(Peripheral)–>内存(Memory)



    点击NVIC 配置中断我这里优先级选择为1


    然后配置对应时钟 我使用的开发板为G474所以我频率配置的高一些,如果你使用的F1C8T6可以直接配置为主频72M



    这里一定要勾选为HSI 内部高速晶振 否则你的串口会输出乱码
    因为有些单片机没有外部高速晶振HSE

    ![在这里插入图片描述](https://i3.wp.com/i-blog.csdnimg.cn/direct/7b7055578b084ac491a30675c1277fc7.png

    代码编写


    添加这四个头文件因为我们要仿造printf编写一个非阻塞的打印函数

    生成代码之后打开我们进入串口的主函数
    在串口的主函数中已经帮我们配好了DMA通道的参数还有串口的初始化
    我们下拉到最下面找到这个写函数的地方(我图中圈的位置)
    添加相应的变量

    uint8_t rxBuffer[50];              //接受缓冲区
    volatile uint8_t isUART3Busy = 0;  // 发送状态标志
    uint8_t txBuffer[512];             // 发送缓冲区
    

    编写打印函数 u1_printf_nonblocking

    // 非阻塞发送函数
    // 相关参数解释"char *fmt"格式化字符串通过占位符如(%d)指定"..."(可变参数列表)的参数类型
    void u1_printf_nonblocking(char *fmt, ...) {
       
      	va_list ap;
        va_start(ap, fmt);
        int len = vsnprintf((char *)txBuffer, sizeof(txBuffer), fmt, ap);
        va_end(ap); //可变参数的处理将格式化的字符串写入txBuffer
    
        if (len > 0) {
            // 检查DMA是否在忙
            if (isUART3Busy) {
                
                return;  // 忙则直接返回
            }
    
            isUART3Busy = 1;  // 标记为忙 避免其他代码触发发送时冲突
            HAL_UART_Transmit_DMA(&huart3, txBuffer, len);//以DMA方式发送txBuffer中的数据
        }
    }
    

    编写接受完成中断回调函数

    void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {//接收完成中断回调
        if (huart == &huart3) {
            // Size 参数由 HAL_UARTEx_RxEventCallback 自动传入,表示实际接收到的数据字节数 实现接什么发什么
    			  // %.*s 动态绑定长度和缓冲区地址
            u1_printf_nonblocking("Received: %.*s\r\n", Size, rxBuffer);
            
            // 重新启动接收
            HAL_UARTEx_ReceiveToIdle_DMA(&huart3, rxBuffer, sizeof(rxBuffer));
            __HAL_DMA_DISABLE_IT(&hdma_usart3_rx, DMA_IT_HT);
        }
    }
    

    编写发送完成中断回调函数

    void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { //DMA发送完成后会调用这个函数
        if (huart == &huart3) {
            isUART3Busy = 0;  // 标记为空闲
        }
    }
    

    在主函数中开启DMA接收 关闭DMA半完成中断 并且声明相关变量

    extern uint8_t rxBuffer[50];
    extern DMA_HandleTypeDef hdma_usart3_rx;//在主函数声明相关参数
    int main(void)
    {
    
      /* USER CODE BEGIN 1 */
    
      /* USER CODE END 1 */
    
      /* MCU Configuration--------------------------------------------------------*/
    
      /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
      HAL_Init();
    
      /* USER CODE BEGIN Init */
    
      /* USER CODE END Init */
    
      /* Configure the system clock */
      SystemClock_Config();
    
      /* USER CODE BEGIN SysInit */
    
      /* USER CODE END SysInit */
    
      /* Initialize all configured peripherals */
      MX_GPIO_Init();
      MX_DMA_Init();
      MX_USART1_UART_Init();
      /* USER CODE BEGIN 2 */
      HAL_UARTEx_ReceiveToIdle_DMA(&huart3,rxBuffer,sizeof(rxBuffer));
      //第三个参数填最大能接收的长度 接受不定长 启动串口3的DMA接收
      __HAL_DMA_DISABLE_IT(&hdma_usart3_rx,DMA_IT_HT);
    	//禁用DMA的半传输中断 该中断通常用于双缓冲机制 前半段传输完成后 可以在处理前半段的数据时DMA继续接收后半中断
    	//我们启动不定长接收 数据结束的标志是总线空闲(idle),而非固定长度,所以禁用此中断
      u1_printf_nonblocking("Hello World!\r\n%d,%x,%o,%f\r\n", 100, 100, 100, 45.78);//测试代码上电复位发送一次	
      /* USER CODE END 2 */
     
      /* Infinite loop */
      /* USER CODE BEGIN WHILE */
      while (1)
      {
        /* USER CODE END WHILE */
    
        /* USER CODE BEGIN 3 */
    		
    
      }
      /* USER CODE END 3 */
    }
    
    

    这些函数放入主函数中初始化函数的下面就可以
    也别忘了要在usart.h中声明一下u1_printf_nonblocking这个函数不然在主函数中是用不了

    点击小剪刀勾选Use McroLIB识别字符串

    代码逻辑

    在主函数中初始化HAL_UARTEx_ReceiveToIdle_DMA(&huart3, rxBuffer, sizeof(rxBuffer))
    启动UART3的DMA接收,配置为接收数据直到检测到总线空闲(Idle)或缓冲区满(50字节)
    禁用DMA的半传输中断(DMA_IT_HT),仅关注空闲事件
    接受数据流程:
    当UART3接收到数据:DMA将数据存入 rxBuffer,直到触发以下条件之一:
    接收满50字节(缓冲区满)
    检测到总线空闲(Idle)

    接收完成后,HAL库自动调用 HAL_UARTEx_RxEventCallback,并传入实际接收的字节数 Size
    在回调函数中:
    通过u1_printf_nonblocking(“Received: %.*s\r\n”, Size, rxBuffer) 将接收到的数据回显(接什么发什么)
    重新调用 HAL_UARTEx_ReceiveToIdle_DMA 启动下一次接收,禁用半传输中断,避免处理不完整数据(跟主函数初始化一样)
    发送数据流程:
    调用 u1_printf_nonblocking 发送数据:
    使用 vsnprintf 将格式化字符串写入 txBuffer
    检查 isUART3Busy 标志
    若为0(空闲):启动DMA发送(HAL_UART_Transmit_DMA),并置 isUART3Busy = 1。
    若为1(忙碌):直接返回,放弃本次发送(防止数据覆盖)。
    DMA发送完成后:
    HAL库自动调用 HAL_UART_TxCpltCallback。
    在回调中置 isUART3Busy = 0,标记发送通道空闲

    测试结果

    正确打印出相关参数 并且在发送你好之后数据会回显

    4月5日改动
    isUART3Busy标志用于标记UART发送状态。当连续调用u1_printf_nonblocking时,若前一次发送未完成(isUART3Busy=1),后续调用直接返回,数据被丢弃。
    例如,两次快速调用打印函数时,第二个调用因标志位未复位而被跳过,所以为了解决数据丢失的问题我们会引入环形缓冲区

    添加缓冲区后的完整代码:

    // --- 配置 ---
    #define TX_QUEUE_SIZE  4
    #define TX_BUFFER_SIZE 512
    
    typedef struct {
        uint8_t buffer[TX_BUFFER_SIZE];
        uint16_t len;
    } TxItem;
    
    // --- 全局变量 ---
    volatile TxItem txQueue[TX_QUEUE_SIZE];
    volatile uint8_t txHead = 0;
    volatile uint8_t txTail = 0;
    volatile uint8_t isUART3Busy = 0;
    uint8_t rxBuffer[50];
    
    // --- 临界区保护宏(根据MCU实现,如STM32使用__disable_irq/__enable_irq)---
    #define ENTER_CRITICAL() __disable_irq()
    #define EXIT_CRITICAL()  __enable_irq()
    
    // --- 非阻塞发送函数(带队列和临界区保护)---
    void u1_printf_nonblocking(const char *fmt, ...) {
        va_list ap;
        va_start(ap, fmt);
    
        // 格式化数据到当前队列头
        uint16_t current_head;
        int len;
        ENTER_CRITICAL();
        current_head = txHead;
        EXIT_CRITICAL();// // --- 临界区保护开始:禁止中断
    
        len = vsnprintf((char *)txQueue[current_head].buffer, TX_BUFFER_SIZE, fmt, ap);
        va_end(ap);
    
        if (len <= 0 || len >= TX_BUFFER_SIZE) return;
    
        ENTER_CRITICAL();
        uint8_t next_head = (current_head + 1) % TX_QUEUE_SIZE;
        if (next_head != txTail) {  // 队列未满
            txQueue[current_head].len = len;
            txHead = next_head;
    
            // 若UART空闲,立即启动发送
            if (!isUART3Busy) {
                isUART3Busy = 1;
                TxItem *item = &txQueue[txTail];
                txTail = (txTail + 1) % TX_QUEUE_SIZE;
                HAL_UART_Transmit_DMA(&huart3, item->buffer, item->len);
            }
        } else {
            // 队列满,可在此添加错误处理
        }
        EXIT_CRITICAL();
       // --- 临界区保护结束:恢复中断 ---
    }
    
    // --- DMA发送完成回调 ---
    void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
        if (huart == &huart3) {
            ENTER_CRITICAL();
            isUART3Busy = 0;
    
            // 检查队列中是否还有数据待发送
            if (txTail != txHead) {
                isUART3Busy = 1;
                TxItem *item = &txQueue[txTail];
                txTail = (txTail + 1) % TX_QUEUE_SIZE;
                HAL_UART_Transmit_DMA(&huart3, item->buffer, item->len);
            }
            EXIT_CRITICAL();
        }
    }
    
    // --- 接收完成回调---
    void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {
        if (huart == &huart3) {
            u1_printf_nonblocking("Received: %.*s\r\n", Size, rxBuffer);
            HAL_UARTEx_ReceiveToIdle_DMA(&huart3, rxBuffer, sizeof(rxBuffer));
            __HAL_DMA_DISABLE_IT(&hdma_usart3_rx, DMA_IT_HT);
        }
    }
    

    本文章为本人学习阶段所写 更多想法是为了作为学习笔记 有不对的地方请大佬们多多批评指正
    CUBEMX最新版本6.14配置可能会有BUG 建议使用6.12及以下

    作者:还在点灯@

    物联沃分享整理
    物联沃-IOTWORD物联网 » DMA与空闲中断下的串口非阻塞收发技术详解:HAL库学习心得

    发表回复