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.循环模式
  • 实现思路
  • 解决方法
  • 总结说明
  • 六、使用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中的数据会等待中断再被传输:

    三、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)的倍数,不满足要求的配置是被禁止的:FIFO
    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对数据的存储和输送:

    普通模式下

    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 4
    36 1 1 6
    48 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:
      2

    2. 接收到1字节数据后,NDTR=7:
      3

    3. 再接收到2字节数据后,NDTR=5:
      4

    4. 当缓冲区被写满后,即NDTR为0时会触发全满中断。普通模式下需手动清除标志位;循环模式下NDTR会自动重置(到0时自动重置回缓冲区长度),写指针回到缓冲区起始地址,且此时会触发中断回调函数:
      5

    5. 再接收到数据时,将从缓冲区起始位置重新开始写入:
      6


    五. 使用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,写入数据长度 = Sizerear
  • Size <= rear,写入数据长度 = Size + (sizeof(RxBuffer)rear)。
  • 紧接而来就有一个坑了:看到这里大家可能会想到,把全满中断关掉,反正也用不上,开着还会进入中断回调,还可能影响到上面的运算逻辑。这个想法本身是没问题的,但实际上回看上面普通模式贴的代码就会发现一个问题:

    	 /* 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,自然就不会调用回调函数了!

    解决方法

    首先我们迫不得已还是要开全满中断,解决思路如下:

  • 进入中断回调时若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流。
    DMA Stream配置

    大概的工作流程

    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(),用户可以重写该函数来实现自己的需求。

    后话

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

    作者:花舞君

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32使用HAL库DMA空闲中断实现环形缓冲区串口数据收发详解

    发表回复