3. DMA传输流程:DMA请求、DMA响应、DMA传输、DMA中断、DMA完成

DMA(Direct Memory Access)传输的核心价值在于解放CPU,让数据在内存与外设之间、或内存与内存之间直接搬运。理解DMA的完整传输流程,是驱动开发的基础。本章将拆解从请求发起到传输完成的五个关键阶段。

3.1 五阶段总览

一个标准的DMA传输事务,在硬件层面可划分为以下五个连续阶段:

阶段 主导者 关键动作 总线状态
DMA请求 外设 / 软件 外设置位DMA请求信号(DREQ),或软件写寄存器触发 外设占用总线请求
DMA响应 DMA控制器 仲裁优先级,向CPU发出总线保持请求(HOLD),获授权后接管总线 CPU释放总线,DMA成为总线主控
DMA传输 DMA控制器 按配置的源地址、目的地址、传输宽度、计数,逐字节/字搬运 DMA驱动总线,CPU暂停或并行
DMA中断 DMA控制器 传输计数归零或错误时,向中断控制器提交中断请求 DMA释放总线,CPU响应中断
DMA完成 CPU / 驱动 中断服务例程(ISR)处理完成回调,清理描述符,启动下一轮 总线回归CPU控制
DMA传输五阶段流程 DMA请求 DREQ / 软件触发 DMA响应 HOLD / 总线授权 DMA传输 源→目的 搬运 DMA中断 TC / 错误中断 DMA完成 ISR / 回调 总线控制权变化 CPU控制 → DMA请求 → DMA响应(CPU释放) → DMA传输(DMA主控) → 中断(CPU恢复) → CPU控制 t0 t1 t2 t3 t4 时间轴 →

3.2 DMA请求(DMA Request)

DMA传输的起点是请求信号的生成。根据触发源不同,分为两类:

  • 硬件请求(外设驱动):外设(如UART、SPI、ADC)在准备好数据时,通过专用引脚DREQ(DMA Request)向DMA控制器发出脉冲或电平信号。例如UART接收FIFO达到阈值时,自动拉高DREQ。
  • 软件请求(内存到内存):CPU通过写DMA控制器的软件触发寄存器(如DMAC.SWREQ)来启动传输。常用于内存拷贝或缓冲区搬运。
驱动要点:在Linux DMA引擎驱动中,dma_request_channel() 用于申请一个DMA通道,而 dmaengine_prep_slave_sg() 或 dmaengine_prep_dma_memcpy() 用于准备传输描述符。请求阶段对应的是描述符的构建与提交。

3.3 DMA响应(DMA Response)

DMA控制器收到请求后,进入仲裁与总线获取阶段:

  1. 优先级仲裁:多通道DMA控制器根据固定优先级或轮询策略,选择当前最高优先级的请求。
  2. 总线保持请求:DMA控制器向CPU发出HOLD信号(总线保持请求),请求CPU释放系统总线。
  3. 总线授权:CPU在完成当前总线周期后,将地址/数据/控制总线置为高阻态,并通过HLDA(Hold Acknowledge)信号回应DMA控制器。
  4. 接管总线:DMA控制器成为总线主控,开始驱动地址线和控制信号。
注意:在周期窃取(Cycle Stealing)模式下,DMA只在CPU空闲的总线周期中插入传输,CPU几乎不受影响;而在突发(Burst)模式下,DMA会长时间占用总线,导致CPU延迟。驱动开发中需根据外设实时性要求选择合适的传输模式。

3.4 DMA传输(DMA Transfer)

这是DMA真正搬运数据的阶段。DMA控制器内部维护一组传输参数寄存器:

寄存器 作用 典型位宽
SAR(Source Address Register) 源地址,每次传输后自动递增/递减/不变 32/64位
DAR(Destination Address Register) 目的地址,同样支持地址修正 32/64位
CTL(Control Register) 传输宽度(8/16/32/64位)、突发长度、传输方向 32位
CNT(Transfer Count Register) 剩余传输次数,每完成一次递减,归零时触发中断 16/32位

传输过程按源→目的方向分为三种基本类型:

  • 外设→内存:如UART接收数据存入RAM,源地址固定(外设数据寄存器),目的地址递增。
  • 内存→外设:如从RAM发送数据到SPI发送FIFO,源地址递增,目的地址固定。
  • 内存→内存:源地址和目的地址均递增,常用于缓冲区拷贝或帧缓冲更新。
DMA传输方向示意图 外设 (UART/SPI) 源地址固定 DMA 内存 (RAM) 目的地址递增 内存 (RAM) 源地址递增 DMA 外设 (SPI/DAC) 目的地址固定 内存A (源) 地址递增 内存B (目的) 地址递增 DMA搬运

3.5 DMA中断(DMA Interrupt)

当DMA传输完成指定次数(CNT归零)或发生错误(如总线错误、对齐错误)时,DMA控制器会向中断控制器(GIC/APIC)提交中断请求。中断类型包括:

  • 传输完成中断(TC Interrupt):正常传输结束,驱动可在ISR中释放缓冲区、唤醒等待队列。
  • 错误中断(Error Interrupt):传输异常,驱动需检查状态寄存器并重置通道。
  • 半传输中断(Half Transfer Interrupt):部分DMA控制器支持在传输到一半时触发中断,用于双缓冲(Ping-Pong)场景。
Linux驱动中的中断处理:在DMA引擎驱动中,中断处理函数通常注册为 dma_irq_handler()。该函数读取DMA控制器的状态寄存器,判断中断源,然后调用 dma_cookie_complete() 标记描述符完成,最后唤醒等待的 dma_async_tx_descriptor 回调。

3.6 DMA完成(DMA Completion)

中断服务例程执行完毕后,DMA传输进入完成阶段。驱动需要完成以下收尾工作:

  1. 确认传输状态:读取DMA状态寄存器,确认无错误。
  2. 释放总线:DMA控制器自动释放HOLD信号,CPU恢复对总线的完全控制。
  3. 回调通知:调用上层注册的完成回调函数(如 dma_async_tx_callback),通知驱动层数据已就绪。
  4. 描述符回收:将使用完毕的描述符归还给空闲链表,或标记为可重用。
  5. 启动下一轮:如果存在待处理的传输请求,立即提交下一个描述符,实现流水线传输。
阶段 驱动API / 操作 说明
请求 dma_request_channel() 申请DMA通道,绑定过滤参数
准备 dmaengine_prep_slave_sg() 构建分散/聚集描述符
提交 dmaengine_submit() 将描述符提交到硬件队列
启动 dma_async_issue_pending() 触发DMA控制器开始传输
完成 dma_async_tx_callback 中断中回调,通知传输结束
性能优化:在高速传输场景(如NVMe、网卡),建议使用描述符链(Descriptor Chain)或循环描述符(Cyclic Descriptor),避免每次传输都触发中断,减少CPU干预。DMA完成阶段应尽量轻量,将耗时操作推迟到工作队列或tasklet中执行。