STM32串口UART实战-DMA + 空闲中断解析法
超时解析法简单易懂,但在数据量较大或速率较高时,频繁进入中断处理每个字节会消耗不少 CPU 资源。为了解放 CPU,让它专注于更重要的任务,我们可以请出两位强大的帮手:DMA 和 UART 空闲中断
一,DMA + 空闲中断解析法的核心思想
这种方法的核心思想是:
1,DMA (Direct Memory Access): 像一个专业的"搬运工",可以在不需要 CPU 干预的情况下,直接将串口硬件接收到的数据搬运到内存缓冲区。
想象一下: CPU 是个大忙人(老板),内存是仓库,串口硬件是收货口。没有 DMA 时,每来一个包裹(字节),都得老板亲自去收货口取货,再放到仓库里,老板累得够呛。有了 DMA 这个"专业搬运工",老板只需要告诉搬运工:“你去收货口守着,有货来了直接搬到仓库的这个区域,搬满/对方不送了再告诉我。” 之后老板就可以专心处理其他事务了。
2,UART 空闲中断 (Idle Line Detection): 串口硬件能检测到总线上是否出现了一段持续的高电平(空闲状态),这通常标志着一次数据传输(一个数据包或一帧)的结束。当检测到空闲状态时,触发一个中断。
结合起来,流程就变成了:CPU 告诉 DMA “开始搬运”,然后就去忙别的了。DMA 默默地把串口收到的数据搬到内存。当发送方停止发送,总线出现空闲时,硬件触发空闲中断,通知 CPU:“嘿,刚才那波数据传输结束了,DMA 已经把数据都搬到内存的这个位置了,总共搬了这么多字节,你快去处理吧!”
这样,CPU 只在一次数据传输(可能包含很多字节)结束后才需要介入一次,大大提高了效率。
二,全局变量
要使用 DMA + 空闲中断,我们的变量也需要升级:
相比于上一节超时解析法的缓存区,计时器,计数器变量,DMA + 空闲中断有多出以下三个变量
1,
uint8_t uart_rx_dma_buffer[UART_RX_DMA_BUFFER_SIZE];
“DMA缓冲区”:串口硬件接收到的数据会被 DMA "搬运工"直接放到这里。其大小 UART_RX_DMA_BUFFER_SIZE
需要足够容纳预期中一次传输的最大数据量(或者可以设置为一个合理的值,配合空闲中断使用)。
2,
uint8_t uart_dma_buffer[UART_DMA_BUFFER_SIZE];
“处理缓冲区”: 当空闲中断发生时,我们会将 DMA 缓冲区(uart_rx_dma_buffer
) 的数据复制到这个缓冲区进行处理。这样做是为了尽快让 DMA 可以继续接收下一批数据,避免数据丢失。其大小通常与 DMA 缓冲区相同。
3,
volatile uint8_t uart_flag = 0;
“处理标志”: 当空闲中断发生,表示一批数据接收完成并且已经从 DMA缓冲区区复制到中断缓冲区后,此时(uart_flag = 1;
)。主循环中开始处理任务。
上面三个变量声明在uart_app.c中
同时声明在mydefine.h中
三,CobeMX的配置
点进串口,进行DMA设置,选择USART1_RT(接收时使用DMA)
(我们在发送数据时没有必要使用DMA,但是接收大量数据时可以使用)
然后对下面三部分进行配置
配置项总结
- Stream: 即哪条DMA通道,STM32有多个DMA流(Stream),不同芯片数量不同,这里用 DMA2 Stream 2
- Direction:即数据搬运方向 ,Peripheral to Memory,从外设(USART)搬到内存
- Priority:即DMA优先级, Low是低优先级;还有Medium、High、Very High
- Mode:搬运模式,Normal(正常模式,搬完就停);还有Circular模式(搬完自动重头再搬,常用于连续采集)
- Increment Address(Peripheral):即外设地址是否自增, 不打勾,USART数据寄存器固定,地址不能动
- Increment Address(Memory):即内存地址是否自增 ,打勾,接收数据会依次存到内存的不同位置
- Use FIFO:是否用DMA内部缓存区(FIFO), 一般小数据量不需要打勾,大数据或高带宽才用
- Data Width(Peripheral):外设数据宽度 ,这里是Byte(USART接收一个字节)
- Data Width(Memory):内存数据宽度 ,也选Byte(存入数组里,每元素1字节)
- Burst Size:突发传输数量 ,你用的Normal模式,所以不可选,只有在某些高速模式下用
使能串口中断
CubeMX 配置是关键: 要使用 DMA 和空闲中断,必须在 STM32CubeMX 中:
1,为对应的 UART RX 添加 DMA 请求 (通常设置为 Circular 模式,这样 DMA 缓冲区满了会自动回到开头继续写入,但配合空闲中断,Normal 模式更常见)。
2,使能 UART 的全局中断。
(部分芯片/库版本可能需要额外配置使能 IDLE Line 事件中断)。
3,CubeMX 会自动生成 DMA 和 UART 的初始化代码,包括关联 DMA 句柄 (如hdma_usart1_rx
)。
四,模板移植
1,启动 DMA 接收
在串口初始化中加入以下两行代码
1,开启DMA接收
在所有初始化完成之后(通常在 main
函数的初始化代码段末尾),需要调用一个函数来启动第一次 DMA 接收直到缓冲区空闲,此时HAL_UART_IRQHandler会捕捉到空闲状态
HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
为什么叫这个函数名?
Receive To Idle: 这个函数名明确表示了它的意图——接收数据,直到检测到总线空闲。它内部会自动处理 DMA 的配置和启动,并确保相关的 UART 中断(特别是 IDLE 中断)被使能。
此函数有什么用呢?
这个函数就像是对 DMA 搬运工和串口硬件下达指令:"开始工作!请使用 DMA 通道(已在 CubeMX 配置好),将串口huart
收到的数据,持续搬运到内存地址pData
(uart_rx_dma_buffer
) 开始的地方,最多搬运Size
(sizeof(uart_rx_dma_buffer)
) 个字节。
2,关闭半满中断
有时,我们可能不关心 DMA 的"半满"中断(Half Transfer Complete),因为它对于判断一帧结束没有帮助,反而可能干扰空闲中断的处理逻辑。因此,可能会看到这样的代码紧随其后:
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT);
这行代码的作用是明确地关闭 DMA 的"半满中断"功能。
hdma_usart1_rx
是 CubeMX 为 USART1 RX 通道生成的 DMA 句柄。
一旦调用了 HAL_UARTEx_ReceiveToIdle_DMA,CPU 就可以撒手不管数据的字节级接收了,直到中断发生。
2,中断函数
当 UART 检测到总线空闲(IDLE 事件),或者 DMA 传输达到预期的一半/全部(如果使能了 HT/TC 中断)时,会触发 UART 的全局中断。HAL 库的中断处理程序在识别到是这些事件后,会调用这个扩展回调函数:
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
// 1. 确认是目标串口 (USART1)
if (huart->Instance == USART1)
{
// 2. 紧急停止当前的 DMA 传输 (如果还在进行中)
// 因为空闲中断意味着发送方已经停止,防止 DMA 继续等待或出错
HAL_UART_DMAStop(huart);
// 3. 将 DMA 缓冲区中有效的数据 (Size 个字节) 复制到待处理缓冲区
memcpy(uart_dma_buffer, uart_rx_dma_buffer, Size);
// 注意:这里使用了 Size,只复制实际接收到的数据
// 4. 举起"到货通知旗",告诉主循环有数据待处理
uart_flag = 1;
// 5. 清空 DMA 接收缓冲区,为下次接收做准备
// 虽然 memcpy 只复制了 Size 个,但清空整个缓冲区更保险
memset(uart_rx_dma_buffer, 0, sizeof(uart_rx_dma_buffer));
// 6. **关键:重新启动下一次 DMA 空闲接收**
// 必须再次调用,否则只会接收这一次
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, uart_rx_dma_buffer, sizeof(uart_rx_dma_buffer));
// 7. 如果之前关闭了半满中断,可能需要在这里再次关闭 (根据需要)
// __HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT);
}
}
这个回调函数是 DMA+空闲中断方法的核心枢纽:
1,确认身份 (if 判断): 检查是否是目标串口触发的事件。
2,停止 DMA (HAL_UART_DMAStop): 空闲事件发生时,明确停止当前的 DMA 传输。这很重要,因为 DMA 可能还在等待更多数据(如果设置的 Size 比较大)。
3,复制数据 (memcpy): 将 DMA 缓冲区 (uart_rx_dma_buffer
) 中实际接收到的Size
个字节复制到我们的应用缓冲区 (uart_dma_buffer
)。Size 参数非常关键,它告诉我们这次空闲事件发生前,DMA 到底接收了多少数据。
4,设置标志 (uart_flag = 1;): 通知主循环中的处理任务。
5,清空 DMA 缓冲区 (memset): 为下一次接收做好准备。
重新启动接收 (HAL_UARTEx_ReceiveToIdle_DMA): 极其重要! 必须重新调用此函数,才能接收下一次的数据传输。
6,(可选) 禁用半满中断: 如果在初始化时禁用了,通常在重新启动后也需要再次禁用。
这里可以使用sizeof(uart_rx_dma_buffer)代替Size吗?
关于 Size 参数: 在空闲中断场景下,Size 表示从上次启动 ReceiveToIdle_DMA 到本次空闲中断发生之间,DMA 实际接收到的字节数。它可能小于你启动时设置的 DMA 缓冲区总大小。所以 memcpy 时必须使用 Size 作为长度,而不是sizeof(uart_rx_dma_buffer)
,否则会复制无效数据。
3,函数总结
HAL_UARTEx_ReceiveToIdle_DMA可以开启数据接收知道空闲,函数HAL_UART_IRQHandler留意串口总线上的空闲状态,一旦检测到空闲1(表示对方可能发完了一帧),或者 DMA 缓冲区满了,就调用HAL_UARTEx_RxEventCallback函数清除缓冲区数据,并且要在此函数中再次写入 串口初始化时的两个函数:
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, uart_rx_dma_buffer, sizeof(uart_rx_dma_buffer));
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx ,DMA_IT_HT);
即再次开启HAL_UARTEx_ReceiveToIdle_DMA接收数据,关闭__HAL_DMA_DISABLE_IT半满中断
“空闲"的定义:
在 UART 通信中,当没有数据传输时,通信线路(RX 线)会保持在高电平状态(逻辑 ‘1’),这被称为"空闲状态"或"标记状态 (Mark State)”。
所谓的"检测到空闲",是指硬件检测到 RX 线上连续保持高电平的时间,超过了一个完整数据帧(包括起始位、数据位、可能的校验位和停止位)所需要的时间。这通常意味着发送方已经发送完了最后一个字节的停止位,并且之后没有紧接着发送新的起始位,表明一帧(或一个数据包)的传输告一段落。
如何来检查这种空闲状态呢?
STM32 的 UART/USART 外设内部通常有一个专门的逻辑电路来检测这种空闲状态:
1.启动条件: 当你调用HAL_UARTEx_ReceiveToIdle_DMA
(或其他使能了空闲中断的接收函数) 时,HAL 库会通过设置相应的控制寄存器位(例如 IDLEIE - Idle Interrupt Enable)来"激活"这个检测电路。
2.检测过程: 电路会监视 RX 线。每当检测到有效的起始位(从高电平变为低电平)时,它内部的一个计时器(与波特率相关)会被复位。
3.触发条件: 如果这个计时器在没有检测到新的起始位的情况下,一直计数,直到累计时间超过了一个数据帧的长度(具体阈值由硬件设计决定,通常是 10 到 12 个比特时间),硬件就会认为线路进入了"空闲"状态。
4.中断产生: 一旦检测到空闲,硬件会自动在状态寄存器中设置一个"空闲标志位"(例如 IDLEF - Idle Line Detected Flag)。如果此时 IDLEIE 中断使能位也是开启的,那么硬件就会向 CPU 发出一个 UART 中断信号。
这就是为什么HAL_UART_IRQHandler
能够捕捉到空闲事件,并最终调用HAL_UARTEx_RxEventCallback
的原因。 ↩︎