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控制 |
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控制器收到请求后,进入仲裁与总线获取阶段:
- 优先级仲裁:多通道DMA控制器根据固定优先级或轮询策略,选择当前最高优先级的请求。
- 总线保持请求:DMA控制器向CPU发出HOLD信号(总线保持请求),请求CPU释放系统总线。
- 总线授权:CPU在完成当前总线周期后,将地址/数据/控制总线置为高阻态,并通过HLDA(Hold Acknowledge)信号回应DMA控制器。
- 接管总线: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,源地址递增,目的地址固定。
- 内存→内存:源地址和目的地址均递增,常用于缓冲区拷贝或帧缓冲更新。
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传输进入完成阶段。驱动需要完成以下收尾工作:
- 确认传输状态:读取DMA状态寄存器,确认无错误。
- 释放总线:DMA控制器自动释放HOLD信号,CPU恢复对总线的完全控制。
- 回调通知:调用上层注册的完成回调函数(如
dma_async_tx_callback),通知驱动层数据已就绪。 - 描述符回收:将使用完毕的描述符归还给空闲链表,或标记为可重用。
- 启动下一轮:如果存在待处理的传输请求,立即提交下一个描述符,实现流水线传输。
| 阶段 | 驱动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中执行。