UART 通讯DMA+IDLE模式笔记

要彻底理解 "DMA+IDLE 模式下,DMA 中断和 UART IDLE 中断谁先触发",必须深入剖析硬件数据流的物理传输过程寄存器标志位的触发时序 以及软件状态机的处理逻辑

直接先说结论:在标准的 "DMA+IDLE" 不定长接收模式下,通常只有 UART 的 IDLE 中断会触发,DMA 的传输完成(TC)中断几乎不会触发(除非数据长度恰好等于 DMA 缓冲区大小)。 即使在数据长度超过缓冲区的循环模式下,触发顺序也是:DMA 搬运数据(不触发 TC)→ UART 检测到空闲总线 → 触发 IDLE 中断

下面从 "物理层数据流"、"寄存器状态流" 和 "软件处理流" 三个维度进行详细拆解,并结合具体的代码示例和时序图进行说明。


一、 核心概念与前置条件

在开始分析之前,我们必须明确以下几个核心定义和标准配置,这是讨论的基础:

  1. DMA+IDLE 模式的标准配置

    • UART 配置 :开启 UART 外设,配置波特率 / 数据位 / 停止位,使能 UART IDLE 中断UART_IT_IDLE)。
    • DMA 配置 :开启 DMA 时钟,配置 DMA 通道为外设到内存(Peripheral to Memory) ,外设地址为UARTx->DR(数据寄存器),内存地址为自定义的接收缓冲区,传输长度(NDTR)设为缓冲区大小(例如 256 字节)。
    • 关键细节 :通常我们不开启 DMA 的传输完成中断(DMA_IT_TC),只开启 UART 的 IDLE 中断。这是理解本问题的关键。
  2. 两个核心中断的触发条件(硬件层面)

    • DMA TC 中断 :当 DMA 的NDTR(数据传输数量寄存器)从N递减到0时,硬件置位 DMA 的TCIF(传输完成中断标志)。它只关心 "是否搬完了预设的 N 个数据",不关心 UART 总线是否空闲。
    • UART IDLE 中断 :当 UART 检测到RX 引脚在 "一个完整字节帧的时间内" 没有电平跳变 (即总线空闲)时,硬件置位 UART 的IDLE(空闲线检测)标志。它只关心 "总线是否空闲",不关心 DMA 搬了多少数据。

二、 详细的数据流与状态流分析

以一个最典型的工程场景为例:

  • DMA 缓冲区大小 :256 字节(NDTR = 256
  • 实际接收数据长度:100 字节(不定长,小于缓冲区)
  • DMA 模式:正常模式(Normal)或循环模式(Circular)

将整个过程分为 "初始化阶段"、"数据传输阶段"、"帧结束检测阶段" 三个步骤。

阶段一:初始化与启动(软件配置)

这是数据流的起点,全部由软件配置完成。

  1. 软件配置寄存器
    • 配置 UART 的CR1寄存器,置位UE(使能 UART)和IDLEIE(使能 IDLE 中断)。
    • 配置 DMA 的CCR寄存器,设置方向为外设到内存,置位EN(使能 DMA 通道)。
    • 写入 DMA 的NDTR寄存器 = 256。
  2. 硬件进入待机状态
    • UART 的 RX 引脚持续检测起始位。
    • DMA 的通道处于挂起状态,等待 UART 发出的 DMA 请求(DREQ)。
    • 关键状态UART_SR.IDLE = 0DMA_LISR.TCIF = 0DMA_CNDTR = 256

阶段二:数据到达与 DMA 搬运(硬件自动完成)

这是核心的数据流阶段,完全由硬件自动完成,不需要 CPU 干预,这也是 DMA 的优势所在。

假设此时发送端发来了 100 个字节的数据(0x010x64)。

  1. 第 1 个字节到达(0x01)
    • UART 端 :UART 通过 RX 引脚接收到起始位,经过移位寄存器接收完 8 位数据,硬件自动将数据存入UART_DR(数据寄存器)。
    • 标志位置位 :硬件置位UART_SR.RXNE(接收数据寄存器非空)。
    • DMA 请求 :由于我们配置了 UART DMA 接收,硬件会自动向 DMA 控制器发送一个DREQ 信号(DMA 请求)。
  2. DMA 搬运第 1 个字节
    • DMA 响应:DMA 控制器收到 DREQ 信号,仲裁通过后,接管总线控制权。
    • 数据传输 :DMA 从UART_DR读取 1 个字节(0x01),写入到内存缓冲区的Buffer[0]地址。
    • 寄存器更新
      • 硬件自动清除UART_SR.RXNE标志(因为 DMA 读取了 DR)。
      • DMA_CNDTR寄存器硬件自动减 1:从 256 变为 255。
      • 内存地址指针(DMA_CMAR)自动加 1,指向下一个位置Buffer[1]
  3. 后续 99 个字节的重复过程
    • 上述步骤(接收 1 字节 -> DMA 请求 -> 搬运 -> NDTR 减 1)重复发生 99 次。
    • 关键状态(第 100 个字节搬运完成后)
      • DMA_CNDTR = 256 - 100 = 156(注意:没有减到 0!)。
      • UART_SR.RXNE = 0(数据已被取走)。
      • DMA_LISR.TCIF = 0(因为 NDTR 还没到 0,所以 DMA 传输完成标志没有置位)。
      • UART_SR.IDLE = 0(因为刚才一直在发数据,总线没有空闲)。

阶段三:帧结束与 IDLE 中断触发(关键!)

这是最关键阶段。

  1. 总线进入空闲状态
    • 发送端发完第 100 个字节(0x64)的停止位后,不再发送数据。
    • UART 的 RX 引脚保持在高电平(逻辑 1)。
  2. UART 硬件检测 IDLE
    • UART 硬件内部有一个波特率计数器。它会在检测到停止位后,开始计数。
    • 如果在 **"一个完整字节帧的时间"**(通常是 10 位或 11 位波特率周期,取决于配置)内,RX 引脚一直没有检测到起始位(下降沿),硬件就会判定 "总线空闲"。
  3. IDLE 中断触发(此时只有这个中断触发)
    • 硬件置位标志UART_SR.IDLE 被硬件置为 1。
    • NVIC 响应 :由于我们在初始化时使能了 IDLE 中断,NVIC(嵌套向量中断控制器)会响应这个中断,CPU 跳转到USARTx_IRQHandler函数。
    • 关键点
      • 此时DMA_CNDTR仍然是 156,没有变成 0
      • 因此,DMA 的 TCIF 标志仍然是 0,DMA 中断不会触发
      • 结论 :在这个场景下,只有 UART IDLE 中断发生,DMA 中断完全不发生

三、 特殊情况:数据长度超过缓冲区大小(循环 DMA 模式)

如果配置 DMA 为循环模式(Circular),且发送的数据长度(例如 300 字节)超过了缓冲区大小(256 字节),流程会有细微变化,但核心逻辑不变。

  1. 前 256 字节传输
    • DMA 搬运 256 字节,NDTR从 256 减到 0。
    • 此时DMA_LISR.TCIF 被硬件置位(如果我们开启了 DMA TC 中断,此时会触发 DMA 中断)。
    • 由于是循环模式,硬件自动将NDTR重新装载为 256,内存指针回到Buffer[0],继续接收。
  2. 后 44 字节传输
    • DMA 继续搬运 44 字节,NDTR从 256 减到 212。
    • 发送端停止发送,总线空闲。
  3. IDLE 中断触发
    • UART 检测到空闲,置位IDLE标志,触发 IDLE 中断。
  4. 此时的中断顺序(如果都开启了)
    • DMA TC 中断先触发(在 NDTR 变 0 的瞬间)。
    • UART IDLE 中断后触发(在总线空闲的瞬间)。

但是 :在实际的工程代码中,我们使用 "DMA+IDLE" 就是为了处理不定长数据 。我们根本不知道数据会来多少,所以我们通常不会开启 DMA TC 中断 。我们只需要在 IDLE 中断里读取NDTR的值,就能算出这次收了多少数据。


四、 软件处理流(代码层面的状态机)

在代码中,处理这个流程的逻辑通常如下(以 STM32H7/HAL 库为例):

  1. 初始化代码
cpp 复制代码
/* 1. 启动DMA+IDLE接收 */
/* 注意:这里没有开启DMA中断,只开启了UART的全局中断 */
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buffer, BUFFER_SIZE);

2. 中断服务函数(USART1_IRQHandler)

这是状态流的核心处理点。

cpp 复制代码
void USART1_IRQHandler(void)
{
    /* 检查是否是IDLE中断 */
    if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE))
    {
        /* 1. 清除IDLE标志位(必须先读SR再读DR,或者直接写清除位) */
        __HAL_UART_CLEAR_IDLEFLAG(&huart1);
        
        /* 2. 停止DMA(防止处理数据时新数据进来造成混乱) */
        HAL_UART_DMAStop(&huart1);
        
        /* 3. 计算接收到的数据长度 = 初始长度 - DMA剩余长度 */
        /* 这是最关键的一步,通过NDTR反推收到了多少 */
        uint16_t received_len = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(huart1.hdmarx);
        
        /* 4. 通知主循环处理数据(例如通过队列、标志位) */
        // ... 数据入队操作 ...
        
        /* 5. 重新启动DMA+IDLE,准备接收下一帧 */
        HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buffer, BUFFER_SIZE);
    }
    
    /* 调用HAL库的默认处理函数(处理错误等) */
    HAL_UART_IRQHandler(&huart1);
}

五、 总结:一张图看懂时序

为了更直观地理解,可以用如下的简化时序图来表示:

最终核心结论

  1. 触发源不同:DMA 中断看 "数量"(NDTR 是否为 0),IDLE 中断看 "时间"(总线是否空闲)。
  2. 标准场景下的赢家 :在处理不定长数据 的标准 "DMA+IDLE" 模式中,数据长度几乎永远不会恰好等于缓冲区大小 。因此,永远是只有 UART IDLE 中断触发,DMA 中断不会触发
  3. 软件设计的核心 :我们的软件逻辑根本不依赖 DMA 中断,只需要在 IDLE 中断里读取NDTR寄存器来计算长度即可。这就是为什么这个模式叫 "DMA+IDLE ",而不是 "DMA TC+IDLE"。
相关推荐
许长安1 小时前
protobuf 使用详解
c++·经验分享·笔记·中间件
hello_读书就是赚钱2 小时前
提示词工程学习笔记
笔记·学习
二哈赛车手2 小时前
新人笔记---多策略搭建策略执行链实现RAG检索后过滤
java·笔记·spring·设计模式·ai·策略模式
LCG元2 小时前
STM32实战:基于STM32F103的SPI通信驱动W25Qxx Flash存储
stm32·单片机·嵌入式硬件
iCxhust2 小时前
【无标题】8086/8088裸机对于学习微机原理的重要意义
汇编·单片机·嵌入式硬件·嵌入式·微机原理
Brilliantwxx2 小时前
【C++】String的模拟实现(代码实现与坑点讲解)
开发语言·c++·笔记·算法
zhangrelay3 小时前
ROS Kinetic-信号与系统-趣味案例
linux·笔记·学习·ubuntu
asjodnobfy3 小时前
啥是电压应力
嵌入式硬件·硬件工程
羊群智妍3 小时前
2026 GEO监测工具|AI搜索优化技术方案与选型
笔记