2. DMA基础概念:DMA控制器、DMA传输模式与DMA描述符
DMA(Direct Memory Access,直接存储器访问)是网关设备中实现高速数据搬移的核心硬件机制。在中断丢失与DMA超时问题的排查中,理解DMA控制器的工作原理、传输模式以及描述符结构是定位问题的前提。本节将从这三个维度展开,建立DMA调试的知识基础。
2.1 DMA控制器架构
DMA控制器是独立于CPU的硬件模块,负责在外设与内存之间、或内存与内存之间直接传输数据。典型的DMA控制器包含以下关键组件:
| 组件 | 功能描述 | 调试关注点 |
|---|---|---|
| 通道(Channel) | 独立的DMA传输路径,每个通道可绑定一个外设请求 | 通道是否被抢占、优先级配置 |
| 源地址寄存器(SAR) | 存储数据源地址(外设FIFO或内存地址) | 地址对齐、地址递增模式 |
| 目的地址寄存器(DAR) | 存储数据目的地址 | 目的地址是否可写、是否跨越4KB边界 |
| 传输计数寄存器(CTL/NDT) | 记录剩余待传输的数据量(字节/字/块) | 计数归零时是否产生中断 |
| 控制/状态寄存器(CSR) | 配置传输方向、模式、中断使能,反映传输状态 | 错误标志位、完成标志位 |
排查要点: 当DMA超时发生时,首先检查CSR中的错误标志(如总线错误、传输中止),其次检查NDT寄存器是否卡在非零值——这通常意味着传输未完成或描述符链断裂。
2.2 DMA传输模式
根据应用场景的不同,DMA控制器支持多种传输模式。网关驱动开发中最常遇到以下三种模式,它们的差异直接影响中断产生时机和超时判定逻辑。
2.2.1 单次传输模式(Single Transfer Mode)
- 工作方式: 每次DMA请求只传输一个数据单元(字节/半字/字),传输完成后释放总线。
- 中断触发: 每次传输完成都会产生中断(如果使能)。
- 适用场景: 低速外设(如UART、I2C),数据量小且不连续。
- 排查风险: 中断频率高,容易因中断丢失导致数据错位。
2.2.2 循环传输模式(Circular/Burst Transfer Mode)
- 工作方式: 配置一次后,DMA自动循环传输,地址回绕到起始位置继续传输。
- 中断触发: 每完成一轮传输(或达到半满/全满阈值)产生中断。
- 适用场景: 音频流、ADC连续采样、环形缓冲区。
- 排查风险: 地址回绕逻辑错误导致数据覆盖,或中断未及时清除导致传输停滞。
2.2.3 链表传输模式(Linked List / Descriptor Chain Mode)
- 工作方式: 通过描述符链表串联多个传输任务,DMA自动加载下一个描述符继续传输。
- 中断触发: 可在每个描述符完成后产生中断,或仅在链表末尾产生中断。
- 适用场景: 网络数据包收发、高速存储(SD/MMC)、网关多通道数据汇聚。
- 排查风险: 描述符指针断裂、描述符地址未对齐、链表环回错误——这是DMA超时的最常见根源。
经验法则: 在网关驱动中,90%的DMA超时问题发生在链表传输模式。排查时优先检查描述符链表的完整性——使用
dmaengine_slave_config后的描述符指针是否有效,以及dma_submit_error返回值是否被正确处理。
2.3 DMA描述符
DMA描述符(Descriptor)是链表传输模式的核心数据结构。每个描述符定义了一次DMA传输的全部参数,多个描述符通过指针串联形成传输任务链。描述符通常由硬件固件定义,驱动通过内存映射进行配置。
2.3.1 描述符结构
一个标准的DMA描述符通常包含以下字段(以常见的struct dma_async_tx_descriptor和硬件描述符为例):
| 字段 | 偏移 | 位宽 | 说明 | 调试检查项 |
|---|---|---|---|---|
| NEXT_DESC_PTR | 0x00 | 32/64位 | 下一个描述符的物理地址,0表示链尾 | 地址是否有效、是否对齐到8字节 |
| SRC_ADDR | 0x08 | 32/64位 | 源数据地址(外设FIFO或内存缓冲区) | 地址是否在可访问范围内 |
| DST_ADDR | 0x10 | 32/64位 | 目的数据地址 | 是否与SRC重叠(内存拷贝时) |
| CONTROL | 0x18 | 32位 | 传输大小、方向、中断使能、传输类型 | 中断位是否置1、传输大小是否匹配 |
| STATUS | 0x1C | 32位 | 传输完成状态、错误标志(硬件写入) | 完成位是否置1、错误位是否清零 |
2.3.2 描述符链表工作流程
- 初始化: 驱动分配连续物理内存,填充描述符字段,设置NEXT_DESC_PTR形成链表。
- 提交: 将链表头描述符地址写入DMA控制器的描述符基址寄存器,启动传输。
- 执行: DMA控制器自动加载当前描述符,完成传输后通过NEXT_DESC_PTR加载下一个描述符。
- 中断: 当描述符的CONTROL字段中中断使能位为1时,传输完成后触发中断。
- 完成回调: 驱动在中断处理中调用
dma_cookie_complete,更新cookie状态,唤醒等待队列。
常见陷阱: 描述符链表断裂是DMA超时的首要原因。典型场景包括:
(1) NEXT_DESC_PTR指向已释放的内存区域;
(2) 描述符未进行cache一致性维护(DMA内存需要
(3) 链表最后一个描述符的NEXT_DESC_PTR未清零,导致DMA控制器持续读取无效地址。
(1) NEXT_DESC_PTR指向已释放的内存区域;
(2) 描述符未进行cache一致性维护(DMA内存需要
dma_alloc_coherent或dma_map_single);(3) 链表最后一个描述符的NEXT_DESC_PTR未清零,导致DMA控制器持续读取无效地址。
2.4 描述符编程示例
以下是一个简化的DMA描述符初始化代码片段,展示如何在Linux内核驱动中配置链表传输:
/* 描述符硬件结构定义(以某款DMA控制器为例) */
struct dma_hw_desc {
u32 next_desc; /* 下一个描述符物理地址 */
u32 src_addr; /* 源地址 */
u32 dst_addr; /* 目的地址 */
u32 control; /* 控制字:大小[15:0], 中断使能[16], 方向[17] */
u32 status; /* 状态字:完成[0], 错误[1] */
} __attribute__((packed));
/* 初始化描述符链表 */
int dma_desc_chain_init(struct dma_hw_desc *desc_base, int num_desc,
dma_addr_t src_base, dma_addr_t dst_base, int size)
{
int i;
dma_addr_t desc_phys = dma_map_single(dev, desc_base,
num_desc * sizeof(struct dma_hw_desc),
DMA_TO_DEVICE);
for (i = 0; i < num_desc; i++) {
/* 填充传输参数 */
desc_base[i].src_addr = src_base + i * size;
desc_base[i].dst_addr = dst_base + i * size;
desc_base[i].control = size | (1 << 16); /* 使能中断 */
desc_base[i].status = 0;
/* 链接到下一个描述符 */
if (i < num_desc - 1)
desc_base[i].next_desc = desc_phys + (i + 1) * sizeof(struct dma_hw_desc);
else
desc_base[i].next_desc = 0; /* 链尾 */
}
/* 确保描述符内存对DMA控制器可见 */
dma_wmb(); /* 写内存屏障 */
return 0;
}
关键点: 上述代码中
dma_wmb()用于保证描述符写入顺序对DMA硬件可见。如果缺少这个屏障,DMA控制器可能读到未完全初始化的描述符,导致传输异常。在调试超时问题时,检查描述符内存的物理地址是否与DMA控制器的描述符基址寄存器一致,是快速定位问题的有效手段。
2.5 本节小结
- DMA控制器由通道、地址寄存器、计数寄存器和状态寄存器组成,调试时优先检查CSR和NDT。
- 三种传输模式中,链表模式是网关驱动的主力模式,也是超时问题的高发区。
- 描述符链表断裂、cache一致性问题、中断使能位配置错误是DMA超时的三大硬件根源。
- 在驱动代码中,使用
dma_alloc_coherent分配描述符内存,并配合dma_wmb/dma_rmb保证内存序,是预防问题的基本实践。