STM32 HAL库中,UART工作在DMA模式时是否需要打开串口中断?

目录

  • 问题引入
  • 实用结论
  • 推理过程
  • 小试验
  • 再看HAL库
  • 刨根
  • UART传输完成中断产生过程
  • UART在DMA模式下
  • 总结
  • 问题引入

    最近学习了stm32(F4xx)的串口在DMA模式下的使用,期间以ST官方提供的例程进行参考学习,发现其初始化过程中是打开了UART的中断的,而且HAL库中stm32f4xx_hal_uart.c文件中的DMA模式使用说明里也有这么一句话:

    (+++) Configure the USARTx interrupt priority and enable the NVIC USART IRQ handle
         (used for last byte sending completion detection in DMA non circular mode)
    

    即在非循环模式下(也就是发完一次数据就停止的常用模式)需要配置串口中断,以使DMA发送完毕后能够触发中断,告诉CPU自己发完了。
    那么问题就来了,使用DMA就是为了解放CPU,为了达到这个目的,DMA发送完毕常采用中断模式而非轮询模式,而中文参考手册里也说DMA的每个数据流都配有中断处理函数,那发送结束时直接由DMA产生中断不就行了,为何还要打开UART的中断呢?而且如果打开了UART中断,是否会导致每发送一个字节就会触发一次发送完成中断?那不就帮倒忙了。
    网上搜索暂没找到满意的解答,希望这篇文章能为有同样困惑的uu解解惑。

    实用结论

    先直接上结论,急着用的uu请放心使用这个结论:
    如果是在HAL库基础上编程的话,DMA模式下的UART是需要开中断的,但是并不全开。
    具体而言,
    一、其中要打开的部分指的是:

    1. 在NVIC上使能对应串口中断源,并设定抢占优先级和子优先级,比如使用USART1的话:
    		HAL_NVIC_EnableIRQ(USART1_IRQn);				//使能USART1中断通道
    		HAL_NVIC_SetPriority(USART1_IRQn,0,0);			//抢占优先级0,子优先级0	
    
    1. 写中断处理函数和回调函数(这里针对传输完毕回调函数)
    //传输完毕回调函数
    void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
    {
    	if(huart->Instance==USART1)
    	{
    		//实现个性化的内容
    	}
    }
    //中断处理函数
    void USART1_IRQHandler(void)
    {
    	HAL_UART_IRQHandler(&UART1_Handler);
    }
    

    二、 不要打开的部分指的是:
    不要使能UART的硬件中断,在这里也就是不要给UART的USART_CR1的TCIE位置1,即你的初始化代码里不应该出现:

    SET_BIT(huart->Instance->CR1, USART_CR1_TCIE);
    

    这类代码。
    到这里,如果你不感兴趣具体原因的话就可以跑路了,祝食用愉快。

    推理过程

    正式推理之前,先梳理一下你将能在这个推理过程中收获什么:

    1. 对UART中断过程更深入的理解;
    2. 通览HAL库中UART在DMA工作模式下的代码结构框架;
    3. 对DMA和底层硬件合作的逻辑有更深的认识,这可以举一反三到SPI、IIC的DMA模式编程中;

    Let’s begin the journey!

    回到最开始的疑虑,在DMA数据流配有发送完毕中断的前提下,为何还需要UART中断来实现传输完成中断?打开串口中断,是否会导致发送过程中UART每发送完一个字节就触发一次传输完成中断?

    在本例中,采用的是USART1,其发送引脚对应的数据流是DMA2_Stream7,通道是DMA_CHANNEL_4
    因此对于UART,其中断入口是:

    void USART1_IRQHandler(void)
    {
    	HAL_UART_IRQHandler(&UART1_Handler);
    }
    

    对于DMA,其中断入口是:

    void DMA2_Stream7_IRQHandler(void)
    {
    	HAL_DMA_IRQHandler(&UART1TxDMA_Handler);
    }
    

    两者发送完的回调函数分别是:

    //UART传输完成回调函数
    void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);
    //DMA传输完成回调函数
    void UART_DMATransmitCplt(DMA_HandleTypeDef *hdma);
    

    而DMA传输完成回调函数会调用

    //UART传输完成回调函数
    void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);
    

    因此代码结构可以表示为:DMA和UART中断结构
    我们个性化的代码,正是在HAL_UART_TxCpltCallback中实现的;

    因此,为了知道两者的中断是否都触发了,触发了多少次,可以先在中断处理函数和回调函数处加入LED翻转代码来协助判断。

    小试验

    1. 试验一
      1.1 试验描述
      USART1_IRQHandler,DMA2_Stream7_IRQHandler两个中断处理函数中分别加入LED翻转程序,主程序进行USART1DMA模式下的传输。
      1.2 试验结果
      发现无论在哪一个中断处理函数中加灯泡翻转程序,LED等均会在DMA传输结束时刻点亮,也就是两个中断处理函数都在DMA发送结束时调用了一次。
    2. 试验二
      2.1 试验描述
      仅在传输完成回调函数HAL_UART_TxCpltCallback中添设LED翻转程序,主程序不变。
      2.2 试验结果
      在DMA传输结束时LED点亮,并不熄灭,也即该回调函数之调用了一次。
    3. 试验结论
      两个中断函数都调用了一次,而回调函数却只调用了一次,那么必有一个中断处理函数是没有直接调用回调函数的。

    再看HAL库

    经过一番折腾,我确定了调用回调函数的支路是UART这一支,而DMA这一支虽然调用了中断处理函数,但并没有调用回调函数。
    可见, 猫腻就藏在HAL库提供的DMA中断处理函数UART_DMATransmitCplt上,其实现代码如下:

    static void UART_DMATransmitCplt(DMA_HandleTypeDef *hdma)
    {
    //...处省略非重要代码
     ...
      /* DMA Normal mode*/
      ...
      //关闭DMA模式
        CLEAR_BIT(huart->Instance->CR3, USART_CR3_DMAT);
      //使能UART的TC中断
        SET_BIT(huart->Instance->CR1, USART_CR1_TCIE);
    ...
      /* DMA Circular mode */
    ...
        /*Call registered Tx complete callback*/
        //调用HAL_UART_TxCpltCallback
        huart->TxCpltCallback(huart);
        ...
     }
    

    原来只有在循环模式下,它才会调用HAL_UART_TxCpltCallback,在非循环模式下,会关闭UART的DMA模式,并使能硬件层面上的UART传输完成中断。这就解释了为什么中断处理函数两者都调用了,但是干实事的中断回调函数只有UART这条支路调用了,这就是为什么最开始有必要打开UART的中断,否则将无法在传输完毕时调用我们想让程序执行的代码。正呼应了HAL库中的

    (+++) Configure the USARTx interrupt priority and enable the NVIC USART IRQ handle
         (used for last byte sending completion detection in DMA non circular mode)
    

    刨根

    现在仍有两个小问题有待解决:

    1. 为什么传输过程中每次UART发送完单个字节后没有触发传输完成中断;
    2. 在发送完毕的最后一小段时间里,程序是如何达到如上的效果的;

    为了解决这两个问题,需要回顾一下UART传输完毕中断发生的过程:

    UART传输完成中断产生过程

    图片:
    UART中断映射图
    如图为UART中断映射图,只看上面部分(对应发送中断),常用的有TC(发送完成)和TXE(发送数据寄存器为空)
    而中文参考手册里也对它们硬件置1的条件予以说明:
    TXE置1条件
    TC置1条件
    结合下图进行解释:
    UART寄存器结构
    当UART在中断模式下发送时:

    1. 对于TXE,每当发送数据寄存器内的内容传输给移位寄存器(由硬件自己完成),并开始从移位寄存器一位一位通过TC引脚往外送,TXE就会置1;此时如果TXEIE为1(即TXE中断使能),就会调用中断,中断处理的内容是将下一个待传输的数据内容放到数据寄存器里,并清零TXE标志位(HAl库里是这么写都),这样循环往复,直到要发送的内容都发完;
    2. 对于TC,当发送完最后一个数据内容之后,如果此时TXE为1(也即数据寄存器里不再有新的内容),说明传输结束了,TC置1,如果TCIE为1(即TC中断使能),就会调用传输完成中断,告诉CPU说可以进行后续操作。

    UART在DMA模式下

    而在DMA工作模式下,仍旧结合图片:
    DMA模式下的寄存器情况
    DMA模式下,每当TXE置1后,不会触发中断来打扰CPU,而是给DMA发送请求,让DMA发送下一段数据内容,并给TXE清零,这解释“为什么传输过程中每次UART发送完单个字节后没有触发传输完成中断 ”这个问题;
    对于第二个问题“在发送完毕的最后一小段时间里,程序是如何达到如上的效果的 ”,对HAL库里的代码实现消化如下:
    通过之前的分析,我们已经知道,当DMA把最后一个数据内容通过总线发送给UART的数据寄存器之后,DMA对应的数据流就会触发传输完成中断,调用UART_DMATransmitCplt ,而在这个回调函数中,采用非循环模式时,会关闭UART的DMA模式,并将对应的UART的传输完成中断使能;
    同时UART在收到最后一段数据之后就开始卖力地传输(先从发送数据寄存器复制到移位寄存器,再从移位寄存器一位一位通过TX引脚开始传输),开始传输的时刻TXE就置1了,发送完之后UART回过头来一看,TXE为1,所以也跟着置1了。而前边DMA传输完成回调函数中又把TCIE中断使能位置1了,所以UART就会触发传输完成中断。
    接下来就是调用我们熟悉HAL_UART_TxCpltCallback回调函数了。到此,问题解决。

    最终重点再呼应前文为什么说UART中断只要打开一部分,即需要在NVIC中使能+配置好中断处理函数和回调函数,而不要使能中断标志位。

    1. 之所以要“在NVIC中使能+配置好中断处理函数和回调函数”,是因为DMA传输完成中断最终其实是通过UART传输完成中断实现的,所以要提前准备好所需的API、打开必要的通道;
    2. 在DMA发送过程中,DMA是不希望UART传输完成中断来添乱的,所以一开始不应该在硬件层面使能TCIE,这个决定权交由DMA,当其传输完毕后再将其打开; 事实上,在开始传输的API“HAL_UART_Transmit_DMA ”里会进行如下操作:
     __HAL_UART_CLEAR_FLAG(huart, UART_FLAG_TC);
    

    即传输前先清除TC标志位,可见DMA多么担心UART在完成工作前就大声嚷嚷打扰到主子啊/捂脸。

    总结

    本文围绕“用HAL编程时,UART工作在DMA模式下,串口中断是否需要打开”这一问题,经过推理,得出的结论是UART中断要打开,但只开一部分(具体参考文章开头部分)。
    在推理的过程中,对HAL实现UART的DMA模式传输、UART发送中断的寄存器变化、DMA模式下的请求过程进行了梳理,这些内容其实可以用来平行地理解SPI和IIC(至少仍是小白的我瞄了一眼例程感觉大差不差),所以可以为我们未来的学习扫除一些障碍。
    最后,非常感谢uu能耐心地看到这里,本人能力有限,不免会有不少疏漏,表达上也会存在欠精准的地方,但还是希望能够多多少少帮到你/爱心,我们一起加油,在硬件开发的海洋里遨游!

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32 HAL库中,UART工作在DMA模式时是否需要打开串口中断?

    发表回复