【STM32 HAL库实战】串口DMA+空闲中断实现不定长数据接收
【STM32 HAL库实战】串口DMA+空闲中断实现不定长数据接收
1 STM32CubeMX配置部分
1.1 初始化配置
打开STM32CubeMX,点击ACCESS TO MCU SELECTOR,在Commercial Part Number中输入MCU型号,例如我在这里输入了STM32L431RCT6。选中正确型号然后双击进入下一步点击配置界面。
1.2 SYS配置如图
注意开启Debug的IO口。
1.3 RCC配置如图
开启了外部晶振,若没有则都选择Diable。
1.4 USART1配置
NVIC设置:注意这里需要打开USART1 global interrupt全局中断。
DMA设置:打开串口接收DMA,模式选择Normal。(TX的DMA也要添加进来,此处图中没有标出)
2 软件执行流程
需要关注下面几个函数:
// 在阻塞模式下接收一定数量的数据,直到接收到预期数量的数据或发生空闲事件
HAL_UARTEx_ReceiveToIdle(UART_HandleTypeDef* huart, uint8_t* pData, uint16_t Size, uint16_t* RxLen, uint32_t Timeout);
// 在中断模式下接收一定数量的数据,直到接收到预期数量的数据或发生空闲事件
HAL_UARTEx_ReceiveToIdle_IT(UART_HandleTypeDef* huart, uint8_t* pData, uint16_t Size);
// 在DMA模式下接收一定数量的数据,直到接收到预期数量的数据或发生空闲事件
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, U2_rx_buffer, sizeof(U2_rx_buffer)); //串口中断+dma
__HAL_DMA_DISABLE_IT(&hdma_usart2_rx,DMA_IT_HT); //关闭dma接收半满中断函数
// 使用空闲中断时的接收回调函数
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
void HAL_UART_IRQHandler(UART_HandleTypeDef *huart)
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
2.1 HAL_UARTEx_ReceiveToIdle_DMA()
HAL库中通过HAL_UARTEx_ReceiveToIdle_DMA()函数可以方便的实现串口空闲中断,下面来分析一下这个实现的过程。
首先来看这个函数本身,可以看到函数中大部分的内容是条件判断,函数中会把接收类型设置成HAL_UART_RECEPTION_TOIDLE,然后开启DMA接收,清除一次IDLEF标志位,重新开启IDLEF标志位。
1 | /** |
注意看status = UART_Start_Receive_DMA(huart, pData, Size),该函数的定义中给串口DMA传输完成一半时的回调函数赋了值。(该函数会使能传输完成、半传输、传输错误等中断)。
这样在执行下面的HAL_DMA_Start_IT函数时,就会开启DMA_IT_HT、DMA_IT_TC、DMA_IT_TE这个中断。
开启这个中断后会影响程序的流程吗?我们来看DMA_IT_HT中断的回调函数UART_DMARxHalfCplt中的内容。可以看到,发生串口DMA传输过半完成中断时,会调用一次 HAL_UARTEx_RxEventCallback回调函数。也就是说调用HAL_UARTEx_ReceiveToIdle_DMA就会默认开启DMA_IT_HT中断。这与我们预期不符,我们希望在传输完成,等待数据接收完整后再进入回调函数,处理完整的数据,接收完整的一轮数据后执行一次HAL_UARTEx_RxEventCallback回调函数,而因为默认开启了DMA_IT_HT中断后,一轮数据接收意外的执行了两次HAL_UARTEx_RxEventCallback回调函数。(使用HAL_UARTEx_ReceiveToIdle_DMA就会默认开启DMA(DMA_IT_TC | DMA_IT_HT | DMA_IT_TE),这样接收一次数据会进两次HAL_UARTEx_RxEventCallback回调函数)
开启DMA_IT_HT中断后,提前进入了回调函数。为了避免出错,我们需要调用__HAL_DMA_DISABLE_IT(),把这个中断手动关闭。(关闭dma接收半满中断函数,这样我们在接收一组数据时就不会触发半满中断,dma就可以接收一组数据)
搞明白其中的逻辑,我们接着回到HAL_UARTEx_ReceiveToIdle_DMA中剩下的部分。调用函数会将当前的串口接收类型设置为HAL_UART_RECEPTION_TOIDLE。
1 | /* Set Reception type to reception till IDLE Event*/ |
调用UART_Start_Receive_DMA(),启用DMA将接收到的数据放在指针 pData 指向的位置。
在接收正确的情况下,会执行以下的内容。
1 | __HAL_UART_CLEAR_FLAG(huart, UART_CLEAR_IDLEF); // 清除UART_CLEAR_IDLEF标志位 |
在参考手册中关于CR1寄存器IDLEIE标志位的介绍。
IDLEIE: IDLE interrupt enable This bit is set and cleared by software. 开启IDLE中断。这个标志位由软件控制。
0: Interrupt is inhibited 中断被禁用。
1: A USART interrupt is generated whenever IDLE=1 in the USART_ISR register 串口空闲时产生串口中断
于是我们可以得知,调用HAL_UARTEx_ReceiveToIdle_DMA()函数后只要发生了串口空闲事件,就会产生串口中断。以及默认开启了DMA_IT_HT这个中断,在DMA的数据传输达到预期数据量的一半时会触发中断,执行回调函数。DMA_IT_HT中断这是我们不需要的,不做过多的分析,我们只需手动关闭即可。我们接下来看串口中断处理函数中的情况。
因为我们在STM32CubeMX中勾选了USART1串口全局中断,这里已经自动生成了代码。开启IDLEF标志位后,如果串口中断来临就会执行中断处理函数 void USART1_IRQHandler(void)。USART2_IRQHandler(void)再调用HAL_UARTEx_RxEventCallback回调函数。
1 | // 由于dma采用的模式:Normal,DMA会在完成一次接收后自动关闭,再次需要重新手动打开 |
2.2 HAL_UART_IRQHandler(&huart1)
接下来继续查看HAL_UART_IRQHandler(&huart1)中的内容,该函数中会判断各种中断类型,并执行对应的操作,主要关注其中关于IDLE中断的部分。首先判断串口的接收类型 ReceptionType,在 HAL_UARTEx_ReceiveToIdle_DMA() 中已经被设为 HAL_UART_RECEPTION_TOIDLE, __HAL_UART_CLEAR_FLAG(huart, UART_CLEAR_IDLEF) 等带有CLEAR的函数会清除相关中断的标志位。
查看与DMA串口空闲中断相关的部分,我们可以简单的理解为,在条件判断都满足的情况下,发生串口空闲中断以后,会开启DMA功能并调用回调函数HAL_UARTEx_RxEventCallback()。如果接收过程中发生错误,会调用HAL_UART_ErrorCallback();
1 | /** |
2.3 HAL_UARTEx_RxEventCallback()
我们先来分析HAL_UARTEx_RxEventCallback()这个回调函数。这是一个__weak类型的函数,对于这种类型的函数我们不能在原位修改,需要在其他位置手动实现。
1 | /** |
以下是我手动重新实现的HAL_UARTEx_RxEventCallback()函数,发生串口空闲中断后会被调用。STM32CubeMX中,DMA的Mode我们选择了Normal,DMA会在完成一次接收后自动关闭,再次调用需要重新手动打开。
1 | void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef * huart, uint16_t Size) |
第二种方法:
关闭dma接收中断,这样dma只需要搬运数据,当触发串口中断标志位时将会进入回调函数进行处理。
2.4 HAL_UART_ErrorCallback()
然后来看HAL_UART_ErrorCallback()这个回调函数,串口发生错误时会调用这个回调函数。
1 | /** |
同样这也是个__weak类型的函数,我们也来手动实现一下。
主要功能为:发生错误时调用HAL_UARTEx_ReceiveToIdle_DMA()来重新开启串口空闲中断和DMA。
1 | void HAL_UART_ErrorCallback(UART_HandleTypeDef * huart) |
2.5 HAL_UARTEx_ReceiveToIdle_IT()
1 | /** |
使用方法:
1.在主函数中调用HAL_UARTEx_ReceiveToIdle_IT()
2.然后在回调函数 HAL_UARTEx_RxEventCallback()中做相应处理。
1 | int main(void) |
1 | //空闲中断回调函数,参数Size为串口实际接收到数据字节数 |
2.6 关于dma
1个字节由8个比特组成,对于115200波特率,每个比特的持续时间为1/115200秒,每个字节的传输时间为8 * 8.68微秒 = 约69.4微秒。115200bps波特率,1s传输11520字节,大约69us需响应一次中断。串口一般来说是低速通信,波特率通常小于等于115200bps。因此对于数据量不大的通信场景,一般没必要使用DMA,完全不需要。
对于数量大,或者波特率提高时,频繁的进入中断可能会导致出现问题,所以采用dma进行搬运。但是dma使用时需要注意一些细节:
dma收一半还是接收固定字节?
使用dma+串口空闲中断时,如果使用HAL_UARTEx_ReceiveToIdle_DMA()实现串口不定长数据接收,需要手动关闭dma接收半满中断或者关闭dma接收中断。
3 调用实例
3.1 调用步骤
- 在while()循环前,调用一次 HAL_UARTEx_ReceiveToIdle_DMA()
- 重新实现HAL_UARTEx_RxEventCallback()
- 在“usart.h”中声明对应的DMA_HandleTypeDef结构体以便在外部调用,用于关闭DMA_IT_HT中断
- 重新实现HAL_UART_ErrorCallback()
usart.h中新增内容如下:
1 | /* USER CODE BEGIN Private defines */ |
完整的main.c文件内容如下:
1 | /* Includes ------------------------------------------------------------------*/ |
3.2 测试结果
3.3 总结
利用HAL_UARTEx_ReceiveToIdle_DMA()可以方便的实现串口不定长数据接收,使用时有2个地方需要注意:
- 需要手动关闭
DMA_IT_HT中断 - 记得手动实现
HAL_UART_ErrorCallback()
4 踩坑
4.1 问题描述
大部分文章中,习惯把 HAL_UARTEx_ReceiveToIdle_DMA(&huart1,rx_buffer,BUF_SIZE); //重新开启串口空闲中断和DMA 接收放在重新实现的函数 void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) 中,而不是放在void USART1_IRQHandler(void)中的方式。
1 | void USART1_IRQHandler(void) |
其他大部分文章如下所示放置。
1 | void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) |
放置在回调函数HAL_UARTEx_RxEventCallback中似乎也说的通,产生串口空闲中断时,会调用回调函数,并在其中重新开启串口空闲中断和DMA,等待下一次串口空闲中断来临。但是这样做会不会有bug?
实际测试一下:
首先,串口初始化时波特率配置成 9600bps。
4.2 测试流程
测试流程:第一次发送,波特率正确9600bps,发送消息后收到正确的回复。
第二次发送: 波特率错误 115200bps 发送消息后收不到回复。
第三次发送 波特率改回正确的9600bps 发送后依然收不到回复。
以上就是模拟波特率不小心设置错误的情况,居然产生了严重的问题,即使改回正确的波特率依然无法收到回复。
说明这种做法存在一定的风险。
经过大量的排查,最后确定问题就出在函数 HAL_UARTEx_ReceiveToIdle_DMA(&huart1,rx_buffer,BUF_SIZE)放置的位置。
波特率正确的正常情况下,USART的CR1寄存器中,IDLEIE位打开,ISR寄存器中IDLE位关闭。
同时对应DMA通道寄存器的情况如下:
目前收发正常。
也能正常进入回调函数。
接着改成错误的波特率,收不到回复。
并且仿真未在回调函数中的断点处停下,说明不再进入回调函数。
查看DMA寄存器,发现DMA没有正确打开(与之前的截图对比)。
查看USART寄存器发现也未正确开启。
4.3 原因
串口波特率错误时,不再进入回调函数。一轮接收结束时,串口空闲中断与DMA均被关闭,而两者的重启在回调函数中通过HAL_UARTEx_ReceiveToIdle_DMA(&huart1,rx_buffer,BUF_SIZE);开启。这就导致波特率错误时,无法接收后续的新数据。
4.4 总结
HAL_UARTEx_ReceiveToIdle_DMA(&huart1,rx_buffer,BUF_SIZE);必须放在void USART1_IRQHandler(void)中。
这样即使接收错误,也能重新开启串口空闲中断和DMA,不影响下次接收。
4.5 最新解释
解决波特率设置错误的方法有问题,当波特率不匹配的时候,UART会报错并产生中断(应该是帧错误或者溢出错误ORT/FE),在中断处理函数HAL_UART_IRQHandler中会关闭DMA,所以改成正确的波特率也无法恢复。
因为串口异常后关闭DMA,之后你调用HAL_UARTEx_ReceiveToIdle_DMA来重置接收确实解决了问题,但是每次UART中断都重置接收肯定是不对的,因为有很多UART事件都会触发中断,比如TXE/CTS/TC/RXNE等,这种情况也重置接收就可能导致接收异常。
正确的处理方式是在HAL_UART_ErrorCallback中重置接收。
还有一个问题就是使用HAL_UARTEx_ReceiveToIdle_DMA来接收,会开启三个中断,DMA半满/DMA溢满/UART空闲,这三个中断都会调用HAL_UARTEx_RxEventCallback,你的这个callback根本没有去判断中断源,真的没问题吗?



























