3. DMA控制器基础:DMA工作原理、DMA传输模式
直接存储器访问(Direct Memory Access, DMA)是PCIe设备实现高性能数据传输的核心硬件机制。DMA控制器允许外设与系统内存之间直接交换数据,无需CPU逐字节干预,从而将CPU从繁重的数据搬运任务中解放出来,专注于协议处理与上层应用逻辑。
3.1 DMA工作原理
DMA传输的基本流程由DMA控制器(DMAC)自主完成。CPU只需配置传输参数(源地址、目的地址、传输长度、传输方向),然后启动DMA通道。DMAC接管总线控制权,驱动数据在源与目的之间流动,传输完成后通过中断通知CPU。
核心优势:一次DMA传输可搬运数千字节甚至数兆字节数据,CPU仅在初始化与完成阶段介入,传输过程中可并行执行其他任务,显著降低CPU占用率并提升系统吞吐量。
3.2 DMA传输模式
根据数据缓冲区的组织方式和传输粒度,DMA控制器支持三种基本传输模式:块传输、散聚传输和循环传输。PCIe驱动开发中,选择合适的传输模式直接影响带宽利用率和延迟表现。
| 传输模式 | 缓冲区组织 | 描述符数量 | 适用场景 | PCIe典型应用 |
|---|---|---|---|---|
| 块传输 | 单一连续物理内存块 | 1个 | 小数据量、低延迟 | 寄存器批量读写、控制消息 |
| 散聚传输 | 多个不连续物理内存块 | 多个(链表) | 大数据量、避免内存拷贝 | 网络数据包、存储I/O |
| 循环传输 | 固定大小的环形缓冲区 | 循环描述符链 | 流式数据、持续传输 | 视频采集、音频流、高速数据采集 |
3.3 块传输(Block Transfer)
块传输是最简单的DMA模式。CPU分配一块连续的物理内存,将起始地址、长度和传输方向写入DMA控制器的寄存器,然后启动传输。DMAC从源地址连续读取数据,写入目的地址,直到传输长度归零。
驱动注意:块传输要求物理地址连续。在Linux内核中,可以使用
dma_alloc_coherent() 或 kmalloc() 配合GFP_DMA标志分配连续DMA缓冲区。对于大块内存(>4MB),连续物理页分配可能失败,此时应考虑散聚传输。
3.4 散聚传输(Scatter-Gather Transfer)
散聚传输(SG DMA)通过描述符链表将多个不连续的物理内存块组织成一个逻辑传输任务。每个描述符记录一个缓冲区的物理地址和长度,描述符之间通过指针链接。DMAC遍历链表,依次传输每个描述符指向的数据块。
关键设计:描述符链表必须位于DMA可访问的物理内存中。Linux内核使用
sg_init_table() 和 dma_map_sg() 构建SG表。描述符链表的最后一个描述符应设置“链结束”标记,或指向一个空描述符以终止传输。
3.5 循环传输(Ring Buffer / Cyclic Transfer)
循环传输使用固定大小的环形缓冲区(Ring Buffer)和循环描述符链。描述符在逻辑上首尾相连,形成一个环。DMAC持续遍历环中的描述符,完成一轮后自动回到起点继续传输,无需CPU重新配置。这种模式特别适合周期性或流式数据场景。
典型应用:高速数据采集卡(如软件无线电SDR)、视频帧捕获、音频流处理。循环传输配合双缓冲(Double Buffering)机制,可实现“零拷贝”流式处理:CPU处理一个缓冲区的同时,DMAC填充另一个缓冲区。
3.6 三种模式对比与选型建议
| 维度 | 块传输 | 散聚传输 | 循环传输 |
|---|---|---|---|
| 内存连续性要求 | 物理连续 | 不连续(分散) | 物理连续或分散均可 |
| 描述符管理开销 | 极低(1个描述符) | 中等(链表遍历) | 低(固定环,无动态分配) |
| CPU干预频率 | 每次传输后中断 | 每次传输后中断 | 可配置(按帧/按周期中断) |
| 适合数据量 | 小(≤4KB~1MB) | 大(MB~GB级) | 中等(流式持续传输) |
| Linux内核API | dma_alloc_coherent |
dma_map_sg / dma_unmap_sg |
dma_alloc_coherent + 环形队列管理 |
| PCIe驱动典型场景 | 配置空间读写、小包控制 | NVMe读写、网络收发 | 视频采集、SDR、数据流 |
选型建议:
- 控制类小数据(<4KB)优先使用块传输,延迟最低,实现最简单。
- 大数据块且内存碎片化严重时,必须使用散聚传输,避免物理连续分配失败。
- 持续流式数据(如ADC采样、视频帧)使用循环传输,配合中断合并(Interrupt Coalescing)可大幅降低CPU负载。
- 现代PCIe设备(如NVMe SSD)通常同时支持SG和循环模式,驱动初始化时根据队列类型选择。
3.7 关键寄存器与描述符结构
DMA控制器的编程接口通常包含以下核心寄存器组:
| 寄存器名称 | 功能描述 | 位宽 |
|---|---|---|
| DMA控制寄存器 | 启动/停止DMA通道、复位、中断使能 | 32位 |
| 源地址寄存器 | 块传输模式下源端物理地址 | 64位(PCIe地址空间) |
| 目的地址寄存器 | 块传输模式下目的端物理地址 | 64位 |
| 传输长度寄存器 | 待传输的字节数 | 32位(最大4GB-1) |
| 描述符基址寄存器 | SG/循环模式下描述符链表的物理基地址 | 64位 |
| 描述符大小寄存器 | 每个描述符的固定大小(字节) | 16位 |
| 当前描述符指针 | 硬件正在处理的描述符索引或地址(只读) | 32/64位 |
| 中断状态寄存器 | 指示传输完成、错误等中断源 | 32位 |
典型的DMA描述符结构(以64位PCIe地址为例):
struct dma_descriptor {
uint64_t src_addr; /* 源物理地址(PCIe总线地址) */
uint64_t dst_addr; /* 目的物理地址 */
uint32_t length; /* 传输字节数 */
uint16_t control; /* 控制标志:中断使能、链结束、方向等 */
uint16_t status; /* 硬件更新:完成标志、错误码 */
uint64_t next_desc; /* 下一个描述符物理地址(SG/循环模式) */
} __attribute__((packed));
地址映射注意:描述符中的地址必须是PCIe总线地址(Bus Address),而非CPU虚拟地址或物理地址。在Linux内核中,使用
dma_map_single() 或 dma_map_sg() 获取总线地址。描述符结构体必须使用 __attribute__((packed)) 防止编译器填充,确保硬件解析正确。
3.8 本章小结
- DMA控制器通过接管总线控制权,实现CPU零干预的数据搬运,是PCIe高性能传输的基石。
- 块传输最简单,适合小数据、低延迟场景,但要求物理连续内存。
- 散聚传输通过描述符链表聚合分散缓冲区,避免内存拷贝,是大数据量传输的首选。
- 循环传输利用环形描述符链实现流式持续传输,配合双缓冲可实现零拷贝流水线。
- 驱动开发中需根据数据特征、内存约束和延迟要求选择合适的传输模式,并正确管理总线地址映射与描述符生命周期。