(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是需要开中断的,但是并不全开。
具体而言,
一、其中要打开的部分指的是:

  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能耐心地看到这里,本人能力有限,不免会有不少疏漏,表达上也会存在欠精准的地方,但还是希望能够多多少少帮到你/爱心,我们一起加油,在硬件开发的海洋里遨游!

  • 46
    点赞
  • 69
    收藏
    觉得还不错? 一键收藏
  • 12
    评论
在使用STM32 HAL库进行串口通信时,可以使用中断接收和DMA发送的方式来提高通信效率。 首先需要初始化串口,并配置接收中断和DMA发送。以下是一个示例代码: ``` UART_HandleTypeDef huart1; void MX_USART1_UART_Init(void) { huart1.Instance = USART1; huart1.Init.BaudRate = 115200; huart1.Init.WordLength = UART_WORDLENGTH_8B; huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_NONE; huart1.Init.Mode = UART_MODE_TX_RX; huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart1.Init.OverSampling = UART_OVERSAMPLING_16; if (HAL_UART_Init(&huart1) != HAL_OK) { Error_Handler(); } /* Enable the UART Parity Error Interrupt */ HAL_UART_Receive_IT(&huart1, rxBuffer, 1); /* Enable the DMA transfer for transmit */ HAL_UART_Transmit_DMA(&huart1, txBuffer, strlen((char *)txBuffer)); } void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart == &huart1) { /* process received data */ HAL_UART_Receive_IT(&huart1, rxBuffer, 1); } } void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart == &huart1) { /* transmit completed */ } } void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) { if (huart == &huart1) { /* handle UART error */ } } ``` 在上面的代码中,`USART1`是串口的实例,`rxBuffer`和`txBuffer`是接收和发送缓冲区。在串口初始化时,使用`HAL_UART_Receive_IT`函数开启接收中断,并使用`HAL_UART_Transmit_DMA`函数开启DMA发送。在接收中断回调函数`HAL_UART_RxCpltCallback`中,可以对接收到的数据进行处理,并继续接收下一个字节。在发送完成回调函数`HAL_UART_TxCpltCallback`中,可以进行一些操作,例如将发送缓冲区中的数据更新,等待下一次发送。在出现UART错误时,`HAL_UART_ErrorCallback`函数会被调用,可以在该函数中处理错误。 需要注意的是,在使用DMA发送时,需要保证发送缓冲区的数据不会被修改,直到DMA发送完成。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值