STM32 HAL库中,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是需要开中断的,但是并不全开。
具体而言,
一、其中要打开的部分指的是:
- 在NVIC上使能对应串口中断源,并设定抢占优先级和子优先级,比如使用USART1的话:
HAL_NVIC_EnableIRQ(USART1_IRQn); //使能USART1中断通道
HAL_NVIC_SetPriority(USART1_IRQn,0,0); //抢占优先级0,子优先级0
- 写中断处理函数和回调函数(这里针对传输完毕回调函数)
//传输完毕回调函数
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);
这类代码。
到这里,如果你不感兴趣具体原因的话就可以跑路了,祝食用愉快。
推理过程
正式推理之前,先梳理一下你将能在这个推理过程中收获什么:
- 对UART中断过程更深入的理解;
- 通览HAL库中UART在DMA工作模式下的代码结构框架;
- 对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);
因此代码结构可以表示为:
我们个性化的代码,正是在HAL_UART_TxCpltCallback中实现的;
因此,为了知道两者的中断是否都触发了,触发了多少次,可以先在中断处理函数和回调函数处加入LED翻转代码来协助判断。
小试验
- 试验一
1.1 试验描述
在USART1_IRQHandler,DMA2_Stream7_IRQHandler两个中断处理函数中分别加入LED翻转程序,主程序进行USART1的DMA模式下的传输。
1.2 试验结果
发现无论在哪一个中断处理函数中加灯泡翻转程序,LED等均会在DMA传输结束时刻点亮,也就是两个中断处理函数都在DMA发送结束时调用了一次。 - 试验二
2.1 试验描述
仅在传输完成回调函数HAL_UART_TxCpltCallback中添设LED翻转程序,主程序不变。
2.2 试验结果
在DMA传输结束时LED点亮,并不熄灭,也即该回调函数之调用了一次。 - 试验结论
两个中断函数都调用了一次,而回调函数却只调用了一次,那么必有一个中断处理函数是没有直接调用回调函数的。
再看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)
刨根
现在仍有两个小问题有待解决:
- 为什么传输过程中每次UART发送完单个字节后没有触发传输完成中断;
- 在发送完毕的最后一小段时间里,程序是如何达到如上的效果的;
为了解决这两个问题,需要回顾一下UART传输完毕中断发生的过程:
UART传输完成中断产生过程
图片:
如图为UART中断映射图,只看上面部分(对应发送中断),常用的有TC(发送完成)和TXE(发送数据寄存器为空)
而中文参考手册里也对它们硬件置1的条件予以说明:
结合下图进行解释:
当UART在中断模式下发送时:
- 对于TXE,每当发送数据寄存器内的内容传输给移位寄存器(由硬件自己完成),并开始从移位寄存器一位一位通过TC引脚往外送,TXE就会置1;此时如果TXEIE为1(即TXE中断使能),就会调用中断,中断处理的内容是将下一个待传输的数据内容放到数据寄存器里,并清零TXE标志位(HAl库里是这么写都),这样循环往复,直到要发送的内容都发完;
- 对于TC,当发送完最后一个数据内容之后,如果此时TXE为1(也即数据寄存器里不再有新的内容),说明传输结束了,TC置1,如果TCIE为1(即TC中断使能),就会调用传输完成中断,告诉CPU说可以进行后续操作。
UART在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中使能+配置好中断处理函数和回调函数,而不要使能中断标志位。
- 之所以要“在NVIC中使能+配置好中断处理函数和回调函数”,是因为DMA传输完成中断最终其实是通过UART传输完成中断实现的,所以要提前准备好所需的API、打开必要的通道;
- 在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能耐心地看到这里,本人能力有限,不免会有不少疏漏,表达上也会存在欠精准的地方,但还是希望能够多多少少帮到你/爱心,我们一起加油,在硬件开发的海洋里遨游!