> 技术文档 > 【STM32】使用HAL库的“DMA+空闲中断”实现环形缓冲区进行串口收发数据(含详细思路)_stm32 hal dma中断

【STM32】使用HAL库的“DMA+空闲中断”实现环形缓冲区进行串口收发数据(含详细思路)_stm32 hal dma中断



文章目录

  • 前言
  • 一、DMA是什么?
  • 二、相关配置
  • 三、DMA工作模式
    • 【1】直传模式(Direct Mode)
    • 【2】FIFO模式(FIFO Mode)
      • 友情提醒
      • 介绍
      • 普通模式下
      • 循环模式下
      • FIFO Mode具体分析
  • 四、DMA接收示例
  • 五. 使用HAL库自带的HAL_UARTEx_ReceiveToIdle_DMA()函数实现DMA+空闲中断(相当多坑)
    • 1.具体实现上与只使用DMA接收的区别
    • 2.普通模式(不感兴趣的可以忽略)
    • 3.循环模式
      • 实现思路
      • 使用STM32Cube FW_F4 V1.28.2 之前版本的请仔细阅读以下内容(新版本请忽略)!!!
        • ***注意:下面的做法只有需要对数据分帧的才用关注,不然请忽略!!!***
      • 总结说明
  • 六、使用DMA进行串口发送
    • 大概的工作流程
  • 后话

前言

过往一直在用标准库进行开发,最近手头上有一批不用的板子,一看MCU是STM32F4系列的(以前玩的都还是STM32F1),莫名产生了“踏上新时代的船”的想法,直接下载STM32CubeIDE,使用HAL库开发来调通这些板子。我第一步调试的就是几乎不管什么应用场景都会涉及到的串口,又因为看到好几篇使用串口DMA的文章,决定使用以前没用过的DMA+空闲中断来进行串口接收数据。
第一次写博客,没想到来写前言时已经过万字了。之所以花那么多时间来表述这些学习成果,是因为学习过程中浏览到很多其他网友(其中不少是嵌入式初学者)调试DMA和空闲中断时遇到各种各样的问题,而他们在借鉴的文章中留言这些问题往往很难得到博主及时回应。希望不管是初学者还是有一定经验的开发者,都能从这篇文章中获得自己想要的东西。抛砖引玉,欢迎大家在评论区留言交流!


一、DMA是什么?

在传统的计算机系统中,外设设备需要通过CPU来控制数据的传输。当外设设备需要读取或写入数据时,需要向CPU发出请求,CPU则负责处理这些请求和数据传输的操作。这种方式会占用CPU的时间和资源,降低计算机系统的整体性能。
DMA控制器是一个特殊的硬件设备,它可以直接和系统内存进行数据传输,而不需要通过CPU来控制。在数据传输过程中,外设设备会向DMA控制器发送请求,告诉它需要读取或写入的数据的地址和大小。然后DMA控制器会直接从内存中读取或写入数据,完成数据传输的过程。这样就可以减少CPU的负担,提高数据传输的速度和效率。

二、相关配置

在HAL库的初始化中,DMA的全满中断默认使能,实际上半满中断也是默认使能的,感兴趣的可以看看函数
HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) 的代码。

外设->内存 模式下,如果是直传模式,每当有数据被外设送入到FIFO,这些数据会直接从FIFO送到目标地址存储。如果是FIFO模式,FIFO中的数据会等待中断再被传输:
【STM32】使用HAL库的“DMA+空闲中断”实现环形缓冲区进行串口收发数据(含详细思路)_stm32 hal dma中断

三、DMA工作模式

【1】直传模式(Direct Mode)

直传模式下的普通模式循环模式,半满和全满中断都是按照用户配置的\"接收数据长度\"(也就是NDTR)正常产生。

【2】FIFO模式(FIFO Mode)

友情提醒

这部分的内容有点复杂,本人实际应用也没有使用上,只是学习时顺带研究调试了一下,本意只是大概搞明白和Direct Mode的基本区别,没想到疑惑一个接一个,很多地方用户手册也没说明白,最后居然记录了这么多!我将这部分的学习成果分享出来,希望其他使用FIFO Mode的朋友遇到问题了有个参考。不感兴趣的可以直接忽略这部分。

介绍

在两边数据流数据宽度不一致的情况下,需要设置FIFO Mode(或叫\"突发模式\")。
当数据宽度不一致,一方的单份数据可能是另一方的多份数据,若使用直传模式每次都从FIFO中搬运同样长度的数据,一定会出现数据过载或欠载的情况。因此突发模式通过在FIFO中控制输出单份数据的长度,避免数据流出错。
每个数据流配置有一个4字即16字节的FIFO,在突发模式下,通过配置溢出阈值数据宽度节拍数来设置每次在FIFO中搬运的数据长度。

  • Burst Size (节拍数):每次在FIFO搬运数据的份数;
  • Data Width (数据宽度):每份被搬运数据的字长;
  • Threshold (FIFO阈值):FIFO产生中断所需达到的阈值(1/4满【4字节】、半满【8字节】、3/4满【12字节】、全满【16字节】)。
    显而易见的是需要保证搬运数据时FIFO中的数据长度满足该次搬运的数据长度要求,否则数据流就可能出错,这意味着中断时FIFO中的数据长度需要是(Burst Size × Data Width)的倍数,不满足要求的配置是被禁止的:【STM32】使用HAL库的“DMA+空闲中断”实现环形缓冲区进行串口收发数据(含详细思路)_stm32 hal dma中断
    ps:上图表格没有写出MBURST=single tranfer的情况(即每份数据都会被立即传输),因为这与直传模式一样,事实上直传模式下MBURST会由硬件强制置成single tranfer。

举个例子,若数据宽度设置为Byte,溢出阈值设置为3/4,那每当FIFO缓冲12字节数据就会产生中断请求搬运数据到目标地址:

  • 若Burst Size设为4,那就是一次搬运(4 × 1Byte = 4Bytes),也就是一次中断会发生12/4 = 3份数据传输,是不会导致数据流出错的;
  • 但若Burst Size设为8,12字节数据无法分成2份被搬运,因此这种配置是被禁止的。

下图非常好地描述了突发模式中FIFO对数据的存储和输送:

【STM32】使用HAL库的“DMA+空闲中断”实现环形缓冲区进行串口收发数据(含详细思路)_stm32 hal dma中断

普通模式下

NDTR变为0时或达到FIFO阈值时都会触发中断,半满中断和全满中断的标志位都有可能为1,下文会详细分析。无论哪种中断标志位组合都会导致禁止中断使能(非循环模式下,HAL库的中断处理函数中会把中断标志位为1的中断类型禁掉):

/* Half Transfer Complete Interrupt management ******************************/ if ((tmpisr & (DMA_FLAG_HTIF0_4 << hdma->StreamIndex)) != RESET) {...}/* Disable the half transfer interrupt if the DMA mode is not CIRCULAR */ else {...}
 /* Transfer Complete Interrupt management ***********************************/ if ((tmpisr & (DMA_FLAG_TCIF0_4 << hdma->StreamIndex)) != RESET) {...} /* Disable the transfer complete interrupt if the DMA mode is not CIRCULAR */ else {...}

此时需要重置NDTR,否则不会再产生任何中断。由于HAL库的中断处理函数会自动清除标志位,重新使能中断;我们只需在回调函数中再次调用HAL_UART_Receive_DMA()即可重置NDTR。

循环模式下

当NDTR变为0时不会触发中断,且NDTR会直接重置;由于处于循环模式,HAL库的中断处理函数不会禁止中断使能位。只有FIFO数据达到阈值时才可能会产生中断。

FIFO Mode具体分析

看到这里大家可能会有种违和感,FIFO Mode的中断理应是由每个Stream的FIFO配置的\"FIFO阈值\"决定的,而FIFO大小是固定16字节的,也就是说触发中断的阈值也是几个固定的数,这个过程中和用户自己设置的NDTR又有什么关联?
下面列几个FIFO Mode中循环模式下的实验现象,数据宽度均为Byte(黄色标注为发生中断):

【1】FIFO Threshold:3/4 ,NDTR:14,此时FIFO阈值 = 12 > 7 = 1/2 NDTR:

接收数据长度 TCIF(全满中断标志) HTIF(半满中断标志) NDTR 0 0 0 14 4 0 0 10 8 0 0 6 12 0 1 2 16 0 0 12 20 0 0 8 24 1 1 436 1 1 648 1 0 8

【2】FIFO Threshold:1/4 ,NDTR:6,此时FIFO阈值 = 4 > 3 = 1/2 NDTR:

接收数据长度 TCIF(全满中断标志) HTIF(半满中断标志) NDTR 0 0 0 6 4 0 1 2 8 1 0 4 12 1 1 6 16 0 1 2 20 1 0 4 24 1 1 6

【3】FIFO Threshold:1/4 ,NDTR:10,此时FIFO阈值 = 4 < 5 = 1/2 NDTR:

接收数据长度 TCIF(全满中断标志) HTIF(半满中断标志) NDTR 0 0 0 10 4 0 0 6 8 0 1 2 12 1 0 8 16 0 1 4 20 1 0 10 24 0 0 6 28 0 1 2

只看前两个实验现象的话,虽然确实触发中断了,但可能对两个标志位的值一头雾水;但对照第三个实验数据就一目了然了:实验三中接收数据长度为4和24时,即使长度达到FIFO阈值了却仍因为两个中断标志位都为0而无法触发中断。说明即使在FIFO Mode,两个中断标志位仍是由NDTR决定的!
FIFO Mode下,半满和全满标志位仍会因为NDTR变为初始值一半和0时“变化”,之所以带“引号”是因为标志位的值不会实时变化反映出来,即使到了半满和全满,Debug里看标志位也还是0。但当FIFO中数据达到FIFO阈值时,MCU会判断之前记录的半满和全满标志位是不是为1,如果有任意一个为1则触发中断。可以说,FIFO阈值是触发中断的第一条件,半满全满中断标志位是触发中断的第二条件。

四、DMA接收示例

下面以Direct Mode、循环模式下一个8字节大小的接收缓冲区示例,单片机串口接收完后打印缓冲区内的数据:

  1. 初始化后,NDTR=8:
    【STM32】使用HAL库的“DMA+空闲中断”实现环形缓冲区进行串口收发数据(含详细思路)_stm32 hal dma中断

  2. 接收到1字节数据后,NDTR=7:
    【STM32】使用HAL库的“DMA+空闲中断”实现环形缓冲区进行串口收发数据(含详细思路)_stm32 hal dma中断

  3. 再接收到2字节数据后,NDTR=5:
    【STM32】使用HAL库的“DMA+空闲中断”实现环形缓冲区进行串口收发数据(含详细思路)_stm32 hal dma中断

  4. 当缓冲区被写满后,即NDTR为0时会触发全满中断。普通模式下需手动清除标志位;循环模式下NDTR会自动重置(到0时自动重置回缓冲区长度),写指针回到缓冲区起始地址,且此时会触发中断回调函数:
    【STM32】使用HAL库的“DMA+空闲中断”实现环形缓冲区进行串口收发数据(含详细思路)_stm32 hal dma中断

  5. 再接收到数据时,将从缓冲区起始位置重新开始写入:
    【STM32】使用HAL库的“DMA+空闲中断”实现环形缓冲区进行串口收发数据(含详细思路)_stm32 hal dma中断


五. 使用HAL库自带的HAL_UARTEx_ReceiveToIdle_DMA()函数实现DMA+空闲中断(相当多坑)

DMA接收的好处不再赘述,缺点是DMA需要FIFO阈值达到设定值才能触发中断,如果接收的数据少于这个阈值,MCU就无法通知我们缓冲区的数据更新了。因此空闲中断就派上用场了:每当串口接收完一个字节数据后检测到下一个bit不为起始位,说明接收完连续的一帧数据了,就会产生空闲中断。

1.具体实现上与只使用DMA接收的区别

使用HAL库自带的HAL_UARTEx_ReceiveToIdle_DMA()函数,当DMA触发了半满中断或全满中断时,同样是会进入函数到HAL_DMA_IRQHandler()的 ,区别在于调用的回调函数不同了。以半满中断为例:

static void UART_DMARxHalfCplt(DMA_HandleTypeDef *hdma){/* --- 无关的已省略 --- *//* Check current reception Mode : If Reception till IDLE event has been selected : use Rx Event callback */ if (huart->ReceptionType == HAL_UART_RECEPTION_TOIDLE) { /*Call legacy weak Rx Event callback*/ HAL_UARTEx_RxEventCallback(huart, huart->RxXferSize / 2U);} else { /*Call legacy weak Rx Half complete callback*/ HAL_UART_RxHalfCpltCallback(huart); }

可以看到,若串口接收模式被设置成空闲中断接收,回调函数会调用HAL_UARTEx_RxEventCallback()而不是 HAL_UART_RxHalfCpltCallback(),这也是为什么接收到数据后不是进入之前DMA接收设置的回调函数的原因。
结论:使用HAL_UARTEx_ReceiveToIdle_DMA(),不管是半满中断、全满中断还是空闲中断,都是调用同一个回调函数HAL_UARTEx_RxEventCallback()。用户需要重写该函数,三种中断的处理都在里面进行。

2.普通模式(不感兴趣的可以忽略)

接下来看一下为什么普通模式下,接收完一次数据DMA就不工作了:
普通模式下触发空闲中断时,HAL库串口中断函数中会自动进行配置(已把不相关的代码省去):

void HAL_UART_IRQHandler(UART_HandleTypeDef *huart){/* --- 无关的已省略 --- */ /* Check current reception Mode : If Reception till IDLE event has been selected : */ if ((huart->ReceptionType == HAL_UART_RECEPTION_TOIDLE) && ((isrflags & USART_SR_IDLE) != 0U) && ((cr1its & USART_SR_IDLE) != 0U)) { __HAL_UART_CLEAR_IDLEFLAG(huart);/* Check if DMA mode is enabled in UART */if (HAL_IS_BIT_SET(huart->Instance->CR3, USART_CR3_DMAR)){ /* DMA mode enabled */ uint16_t nb_remaining_rx_data = (uint16_t) __HAL_DMA_GET_COUNTER(huart->hdmarx); if ((nb_remaining_rx_data > 0U) && (nb_remaining_rx_data < huart->RxXferSize)) { /* In Normal mode, end DMA xfer and HAL UART Rx process*/ if (huart->hdmarx->Init.Mode != DMA_CIRCULAR) {/* Disable the DMA transfer for the receiver request by resetting the DMAR bit in the UART CR3 register */ ATOMIC_CLEAR_BIT(huart->Instance->CR3, USART_CR3_DMAR); /* At end of Rx process, restore huart->RxState to Ready */ huart->ReceptionType = HAL_UART_RECEPTION_STANDARD;  ATOMIC_CLEAR_BIT(huart->Instance->CR1, USART_CR1_IDLEIE);/* Last bytes received, so no need as the abort is immediate */ (void)HAL_DMA_Abort(huart->hdmarx);} /*Call legacy weak Rx Event callback*/ HAL_UARTEx_RxEventCallback(huart, (huart->RxXferSize - huart->RxXferCount)); } } }}//end of function

需要关注的几个点:

  1. 首先判断RxEventType是否为HAL_UART_RECEPTION_TOIDLE,且处理函数会自动清空IDLE标志;
  2. DMAR置0导致了禁止DMA Mode的接收;
  3. ReceptionType从HAL_UART_RECEPTION_TOIDLE变为了HAL_UART_RECEPTION_STANDARD,这会导致下次接收无法进入该段处理程序;
  4. IDLEIE置0导致了空闲中断的失效;
  5. HAL_DMA_Abort()禁止了DMA流。

这就是接收一次数据后不论DMA设置的Buffer有没有满(也就是NDTR不为0),DMA传输都会停止的原因:触发了串口空闲中断。前4点是从串口配置禁止,第5点是从DMA配置禁止。所以普通模式下使用空闲中断要留个心眼:空闲中断是会影响到DMA的配置的,此时DMA作用只是作为单次数据接收的缓冲区,每次接收完都会重置。
结论:普通模式下,使用HAL_UARTEx_ReceiveToIdle_DMA(),一旦进入过空闲中断就只能再一次调用HAL_UARTEx_ReceiveToIdle_DMA()来开启DMA和串口的对应配置。

3.循环模式

反过来说,循环模式下,空闲中断就和DMA的运作独立开来了。DMA配置的Memory可以看作一个环形缓冲区,里面的数据并不会因为空闲中断清空。当我们配置好后,就得到了一个会自动写入数据的环形缓冲区,接下来就是如何从里面读取数据了。
关于环形缓冲区的介绍可以看这篇文章:ring buffer,一篇文章讲透它,讲得非常通俗易懂。

实现思路

读数据的前提肯定是有新的数据写入。空闲中断会通知我们数据更新了,因此首当其冲的问题是如何获取新写入到缓冲区数据的长度。 循环模式跟普通模式不同,NDTR不会每次接收完都被重置,但传入HAL_UARTEx_RxEventCallback()的参数Size却是 (预设的缓冲区大小 - NDTR),也就是已用缓冲区大小,具体参考HAL_UART_IRQHandler()下面的几行代码:

/* Check if DMA mode is enabled in UART */ if (HAL_IS_BIT_SET(huart->Instance->CR3, USART_CR3_DMAR)) { /* DMA mode enabled */ uint16_t nb_remaining_rx_data = (uint16_t) __HAL_DMA_GET_COUNTER(huart->hdmarx);//返回的值就是NDTR ...... huart->RxXferCount = nb_remaining_rx_data; ...... HAL_UARTEx_RxEventCallback(huart, (huart->RxXferSize - huart->RxXferCount)); }

因此普通模式下把Size当作写入长度是没问题的,但循环模式下就需要我们进一步计算了。

既然把循环模式的数据存放看作环形缓冲区,那Size不就基本等同于一个自动更新的写索引了吗?我们维护一个自定义写索引rear,每次进入空闲中断都更新一次

rear = (Size != sizeof(RxBuffer))?Size:0;//若Size等于缓冲区长度证明写满了,写指针要回到起点

很容易就能得出写入数据的长度:

  • Size > rear,写入数据长度 = Size - rear
  • Size <= rear,写入数据长度 = Size + (sizeof(RxBuffer) - rear)。

使用STM32Cube FW_F4 V1.28.2 之前版本的请仔细阅读以下内容(新版本请忽略)!!!

多谢评论区里@quote6的一同探讨和调试,我发现在25年4月发布的STM32Cube FW_F4 V1.28.2的固件代码中,数据刚好写满缓冲区的情况已经会同时触发全满中断和空闲中断,因此下述内容请根据自己使用的固件版本来作参考!(2025.8.4新增说明)

紧接而来就有一个坑了:看到这里大家可能会想到,把全满中断关掉,反正也用不上,开着还会进入中断回调,还可能影响到上面的运算逻辑。这个想法本身是没问题的,但实际上回看上面普通模式贴的代码就会发现一个问题:

 /* DMA mode enabled */ uint16_t nb_remaining_rx_data = (uint16_t) __HAL_DMA_GET_COUNTER(huart->hdmarx); if ((nb_remaining_rx_data > 0U) && (nb_remaining_rx_data < huart->RxXferSize)) { if (huart->hdmarx->Init.Mode != DMA_CIRCULAR) {......} /*Call legacy weak Rx Event callback*/ HAL_UARTEx_RxEventCallback(huart, (huart->RxXferSize - huart->RxXferCount)); }

调用回调函数的条件,普通模式和循环模式是一样的:NDTR>0,且NDTR < 预设的BufferSize 。循环模式下一旦NDTR变为0会立刻重置为初始预设值,也就是说如果写入的数据刚好写满缓冲区,进入HAL_UART_IRQHandler() 时是 NDTR == 预设的BufferSize,自然就不会调用回调函数了!

不过虽说不能关全满中断,但其实我们一般不用关心数据是哪次中断进来的,只需要每次中断时把缓冲区的数据读取出来并放到FIFO就好了。因此每次中断回调中我们都对数据处理,就不会出现Size<=rear的情况了(全满中断时rear设成0)。

注意:下面的做法只有需要对数据分帧的才用关注,不然请忽略!!!

但利用空闲中断处理数据的好处就是能对数据进行分帧。如果想实现,就必须在全满中断中解决单次数据刚好写满缓冲区的情况。目前想到的解决思路如下:

  • 进入中断回调时若Size == sizeof(RxBuffer) (或 huart->RxEventType == HAL_UART_RXEVENT_TC),证明触发的是全满中断,此时需要加入一定延时(不能直接用HAL_Delay(),SysTick中断抢占优先级最低,在这里调用只会导致程序卡死),等待DMA传输一个单位数据的周期,然后进行如下判断:
    • NDTR == sizeof(RxBuffer),证明触发中断后没有再往缓冲区里写数据了(中断不会打断DMA传输),本次写入就是刚好写满缓冲区。运算和之前所述一样;
    • NDTR != sizeof(RxBuffer),证明数据还没写完,我们只要等待空闲中断到来再处理就行了,因此直接return即可;
  • 进入中断回调时若Size != sizeof(RxBuffer),证明触发的是空闲中断,和之前所述同样处理。

ps:

  • 写入的数据刚好填满缓冲区,触发的是全满中断而不是空闲中断;
  • 关于触发了全满中断后判断DMA是否仍在写入数据,目前我只能想到加延时再判断,若大家有更好的办法请不吝赐教。

代码仅供参考:

#define UART_RXBUF_MAXSIZE (8)//串口环形缓冲区结构typedef struct{ uint16_t front; //写索引 uint16_t rear;  //读索引 uint8_t buf[UART_RXBUF_MAXSIZE]; //缓冲区}uart_buf_t, * p_uart_buf_t;uart_buf_t rxCirBuf = {};//读取串口缓冲区int get_data_fromRxBuf(uint8_t* data,uint16_t size){p_uart_buf_t p = &rxCirBuf;if(size == 0 || size > sizeof(p->buf))return -1;if(p->front < p->rear){memcpy(data,p->buf + p->front,size);p->front += size;}else{uint16_t rest = sizeof(p->buf) - p->front;memcpy(data,p->buf + p->front,rest);memcpy(data+rest,p->buf,size - rest);p->front = size - rest;}return 1;}//接收中断回调函数void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef * huart, uint16_t Size){if(huart->Instance == USART1){uint16_t BUFF_SIZE = sizeof(rxCirBuf.buf);//判断是否为全满中断if(Size == BUFF_SIZE){delay_us(100);//这里记得要用非中断延时uint16_t NDTR = (uint16_t) __HAL_DMA_GET_COUNTER(huart->hdmarx);if(NDTR != BUFF_SIZE)return;}//计算数据长度uint16_t size = (Size > rxCirBuf.rear)?\\(Size - rxCirBuf.rear):(Size + (BUFF_SIZE - rxCirBuf.rear));//写索引更新rxCirBuf.rear = (Size != BUFF_SIZE)?Size:0;/*TODO:*///测试用:发送接收到的数据uint8_t dSend[size];if(1 == get_data_fromRxBuf(dSend, size))HAL_UART_Transmit(&huart1, dSend, size,20); }}

上面代码的测试部分还包含了读取缓冲区的操作,本文不再展开。

总结说明

上面所述的是如何使用循环模式下的环形缓冲区,包括应该在哪里、在什么时候处理接收的数据,并且如何获取到接收到数据的长度。每个人的应用场景不同,有可能处理的数据每帧都有包头包尾,或每帧数据是定长,或数据中就包含长度信息,因此处理时并不一定要计算size。本文只是介绍了一个实现思路,环形缓冲区也只是进行了最基本的实现,实质上还有很多可挖掘可完善的地方,比如这篇博客里提到的一种情况:处理一帧数据过久会出现的情况(@大文梅)。

六、使用DMA进行串口发送

来都来了,顺便简单讲一下HAL_UART_Transmit_DMA()发送数据的流程。
首先接收数据和发送数据不是同一个DMA流,数据流的方向都不一样,如果发现无法发送数据不妨先检查下是否忘记配置串口Tx的DMA流。
【STM32】使用HAL库的“DMA+空闲中断”实现环形缓冲区进行串口收发数据(含详细思路)_stm32 hal dma中断

大概的工作流程

  1. HAL_UART_Transmit_DMA()里,发送数据前会自动打开中断使能和DMA流使能并清空中断标志位,配置好后令串口控制寄存器的DMAT位置1(USART_CR3_DMAT)使能DMA Mode;
  2. 使能后DMA开始从Memory将数据写入到TDR,而当TDR的数据全部送到移位寄存器后,TXE Flag会置1,说明TDR空了,DMA就又送入下一个数据了。 特别说明一下,整个过程TXEIE位是disabled的,因此每次TXE Flag置1都不会触发中断。另外HAL_UART_Transmit()也是一样,只有HAL_UART_Transmit_IT()会使能TXEIE来利用TXE中断发送数据。 这样大家应该可以理解这三者的区别了,包括阻塞发送和非阻塞发送也清晰了。别再写完 HAL_UART_Transmit_IT(&huart1, rxBuf, Size)HAL_UART_Transmit_DMA(&huart1, rxBuf, Size) 后接一句 memset(rxBuf, 0, Size) 然后问为什么发不出数据或者少数据了;
  3. 当全部数据数传输完成后,NDTR计数也到0了,触发了全满中断从而进入了HAL_DMA_IRQHandler()。HAL库会自动清空中断标志位,若非循环模式还会禁止DMA中断。最后调用库内置的回调函数UART_DMATransmitCplt()
  4. UART_DMATransmitCplt()中会将DMAT置0,禁止DMA Mode;然后USART_CR1_TCIE置1使能传输完成中断。最后调用回调函数HAL_UART_TxCpltCallback(),用户可以重写该函数来实现自己的需求。
    【STM32】使用HAL库的“DMA+空闲中断”实现环形缓冲区进行串口收发数据(含详细思路)_stm32 hal dma中断

后话

可能很多人会想:你这文章也太啰嗦了,我都用HAL库了还要去刨它库函数怎么运行吗?直接写解决方案就行了,顺便把源码附上()。
我的想法是:如果你能一次把功能调通且往后也不会出问题,那确实是可以。但是随着应用场景的变化,外设的变化甚至是MCU的变化,你能保证你的代码同样适用吗?嵌入式开发本身就是不断修正的过程。出问题不是问题,出了问题不会找原因才是问题。 而我文章内容就是一个不断出问题、找原因、解决问题的过程。
举个例子,网上其它使用DMA+空闲中断的教程,都是简单地教你使用HAL_UARTEx_ReceiveToIdle_DMA(),普通模式下要在接收完一次数据后再调用一次。但我在学习过DMA后,第一想法是只调用这个函数一次,因为DMA开启之后没必要每次都去重置,尤其是缓冲区还有很多剩余空间的情况下。我的想法是串口中断应该只会禁止串口的DMA相关使能位,我只要在中断中重新使能就好了。但写了代码之后发现不行,debug时发现整个DMA流都被禁掉了。一看原来HAL库的串口中断函数里调用了HAL_DMA_Abort(huart->hdmarx),空闲中断发生就自动给你把DMA流禁了。虽然我在小标题标明了“不感兴趣的可以忽略”,但其实我相信总有人像我一样感兴趣的~
HAL库开发确实大大简化了开发过程,也降低了开发门槛,但这不代表我们对自己的要求降低了。同样的坑,你过了门槛踩到它时都是一样深的,爬出来也是一样费劲。你是想等有人路过把你拉出来,还是自己慢慢努力爬出来?