串口DMA学习-从原理到实践-基于hal库

前言:这篇文章是使用DMA实现串口接收数据的知识总结,部分内容来自网上文章

一、串口DMA接收的意义

直接说,就是快!直接存储器存取( DMA )用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须 CPU 干预,数据可以通过 DMA 快速地移动,这就节省了 CPU 的资源来做其他操作。比如移动一个外部内存的区块到芯片内部更快的内存区。[1]

二、DMA的主要特征

  • 每个通道都直接连接专用的硬件 DMA 请求,每个通道都同样支持软件触发。这些功能通过软件来配置。
  • 在同一个 DMA 模块上,多个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),优先权设置相等时由硬件决定(请求 0 优先于请求 1 ,依此类推,可以参考STM32数据手册)。
  • 独立的源和目标数据区的传输宽度(字节、半字、全字),模拟打包和拆包的过程。源和目标地址必须按数据传输宽度对齐。
  • 支持循环的缓冲器管理(会把原来的数据覆盖)。
  • 每个通道都有 3 个事件标志(DMA 半传输, DMA 传输完成和 DMA 传输出错),这 3 个事件标志逻辑或成为一个单独的中断请求。
  • 存储器和存储器间的传输(仅 DMA2 可以)。
  • 外设和存储器、存储器和外设之间的传输。
  • 闪存、SRAM 、外设的 SRAM 、APB1 、APB2 和 AHB 外设均可作为访问的源和目标。
  • 可编程的数据传输数目:最大为65535(216-1)。

三、串口使用DMA的主要两种方式:定长数据和不定长数据

两种方式的区别

定长数据就是连续传送的,每次传输字节数固定的数据,比如各种数据上报模块,如本人使用过的:激光测距模块,姿态传感器,水质传感器,其上报数据以帧为单位,最后前几个字节为帧头,中间为数据为,最后几个字节为校验位。
接收不定长数据相当于和串口聊天,每次聊天的内容都不一定一样,但每句话都会有间隔的时间,可以理解为空闲时间,由此可以利用串口空闲中断来判断一次数据传输是否完成[2]

串口DMA传输定长数据

这里仅仅讲解我工作中用到的,传输定长数据
首先要注意的一点事,dma收或发都会引起dma中断,而我这里只要使用dma收中断,所以要使用 __HAL_DMA_DISABLE_IT(&hdma_usart1_tx, DMA_IT_TC);来关闭串口发送完成中断(不使用dma发送数据),开启发送完成中断

1
2
3
//开启DMA接收完成中断;关闭DMA发送完成中断:
__HAL_DMA_ENABLE_IT(&hdma_usart1_rx, DMA_IT_TC);
__HAL_DMA_DISABLE_IT(&hdma_usart1_tx, DMA_IT_TC);

然后启动dma接收

1
HAL_UART_Receive_DMA(&huart1,USART1_RX_BUF,sizeof(USART1_RX_BUF));

这里调用了HAL_UART_Receive_DMA函数,查看函数定义的注释可以直接跳转函数说明
可以看到该函数有三个参数,主要关注第二个和第三个参数,第二个参数是要写入数据的data指针,第三个参数是要接收的数据大小。

然后是在中断处理函数里自定义处理逻辑,这里中断函数可能不清楚是哪个,其实hal库的结构还是很清晰的,在stm32cubemx配置了dma后,dma中断处理函数自动生成在stm32fxxx_it.c里


然后在cubemx里确认串口收中断是stream2管控的

那么在DMA2_Stream2_IRQHandler里添加自定义处理逻辑即可,这里附上我的代码供参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void DMA2_Stream2_IRQHandler(void)
{
/* USER CODE BEGIN DMA2_Stream2_IRQn 0 */
//判断是否是接收中断
if(__HAL_DMA_GET_FLAG(&hdma_usart1_rx, DMA_FLAG_TCIF2_6))
{
//清除中断标志
__HAL_DMA_CLEAR_FLAG(&hdma_usart1_rx, DMA_FLAG_TCIF2_6);
//先停止dma传送
HAL_UART_DMAStop(&huart1);
//置标志位为1,main()里判断reception并进行数据进一步处理,不要在中断处理函数中直接处理,否则影响之后dma数据接收连贯性
reception_1=1;
//待处理数据拷贝到data数组,data数据作为数据处理缓冲区,因为一会dma接收是会覆盖USART1_RX_BUF里数据的
memcpy(data,USART1_RX_BUF,sizeof(data));
memset(USART1_RX_BUF, 0, sizeof(USART1_RX_BUF));//接收缓冲区全部置0
//重新开启dma接收
HAL_UART_Receive_DMA(&huart1,USART1_RX_BUF,sizeof(USART1_RX_BUF));
}
/* USER CODE END DMA2_Stream2_IRQn 0 */
HAL_DMA_IRQHandler(&hdma_usart1_rx);
/* USER CODE BEGIN DMA2_Stream2_IRQn 1 */

/* USER CODE END DMA2_Stream2_IRQn 1 */
}

可以看到,在主函数这里首先判断了一下中断类型,这是因为这个是dma的全局中断(global interrupt) ,从函数注释就能看出来

那么什么是全局中断?
阅读datasheet可以看到,在DMA状态寄存器一节中

全局中断标志位为1,说明通道中产生TE/HT/TC事件,
TE:传输错误事件;TC:传输完成事件;HT:半传输事件;所以这三种都可能导致进入这个中断函数,同时DMA收/发都会引起这三种事件,所以一定要判断一下。
之后在主函数中判断reception处理data[]里的数据即可

串口DMA传输不定长数据

这里没有实操过,大概讲一下hal库的实现思路:

  1. 开启串口空闲中断
  2. 关闭DMA传输完成中断
  3. 使能DMA接收
  4. 在串口空闲中断的回调函数(串口空闲中断是可以通过回调函数进行数据处理的)中进行处理,包括
    1. 清除中断标志位,
    2. 获取当前已经传输的数据数量(记为a)
    3. 将数量为a的dma缓冲区数据复制到待处理缓冲区,
    4. 设置reception=1告诉主函数可以处理数据,
    5. 重新使能DMA接收。
  5. 主函数根据reception是否为1来判断是否进行处理。
    注:上面所说的DMA传输完成中断使能DMA接收是两回事,在HAL库中分别为__HAL_DMA_ENABLE_IT();HAL_UART_Receive_DMA(),不要混淆了

踩过的坑

  1. 注意区分串口中断和串口空闲中断的区别,我一开始以为这两个是一样的,结果,但是串口空闲中断要求数据发送间有间隙,只有数据空闲时才会进入中断,而我使用的模块是连续发送的数据,所以导致程序一直不进入中断。(串口空闲中断的学习以后可能单独会写一篇来记录)
  2. 之前在网上查看hal库dma的使用方法,大多数文章对于原理讲解的很清楚,可是具体到实践上,对于一个简单的hal库如何实现dma接收数据处理的实现,是通过中断服务函数,还是通过重写回调函数实现都没有讲解,导致耽误了一些时间。这里还是建议直接阅读hal库源代码学习,因为hal库本身代码每行都有注释,虽然是英文,但是只要翻译一下就能轻松理解,同时函数间的调用关系也十分清晰,比网上一些讲得一知半解的文章好多了。

四、DMA高级用法

1. 双缓冲区

双缓冲区的意义:

如果接收中断间隔时间非常短(即发送数据帧的速率很快),MCU来不及处理此次接收到的数据,又产生中断,这时不能直接开启DMA通道,否则数据会被覆盖。有2种方式解决。[2]

  1. 在重新开启接收DMA通道之前,将DMA_Rx_Buf缓冲区里面的数据复制到另外一个数组中,然后再开启DMA,然后马上处理复制出来的数据。
  2. 建立双缓冲,设置一个缓冲区标志(用来指示当前处在哪个缓冲区),每完成一次传输就通过重新配置DMA_MemoryBaseAddr的缓冲区地址,下次传输数据就会保存到新的缓冲区中,可以通过自定义缓存区标志来判断和切换,这样可以避免缓冲区数据来不及处理就被覆盖的情况,也能为处理数据留出更多的时间(指到下次传输完成)。
实现方法

[2][3]

其他参考文章

【STM32のHAL库开发】用DMA中断来接收串口数据


串口DMA学习-从原理到实践-基于hal库
http://lightandsqlt.site/2024/12/10/学习/串口DMA学习-从原理到实践-基于hal库/
作者
Ethan
发布于
2024年12月10日
许可协议