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寄存器是否卡在非零值——这通常意味着传输未完成或描述符链断裂。
DMA控制器内部架构 系统总线(AHB/AXI) 通道0 SAR: 0x4000_1000 DAR: 0x8000_2000 NDT: 1024 CSR: 0x0005 通道1 SAR: 0x4000_2000 DAR: 0x8000_3000 NDT: 512 CSR: 0x0001 通道2 SAR: 0x4000_3000 DAR: 0x8000_4000 NDT: 2048 CSR: 0x0000 仲裁器 优先级 轮询/固定 请求屏蔽 外设请求0 外设请求1 外设请求2 中断控制器(GIC) 调试入口: 读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返回值是否被正确处理。
三种DMA传输模式对比 单次传输模式 请求 → 传输1 → 中断 请求 → 传输2 → 中断 请求 → 传输3 → 中断 每次传输独立触发中断 循环传输模式 传输1 → 传输2 → 传输3 ↓ 地址回绕 ↑ 传输6 ← 传输5 ← 传输4 阈值中断或半满中断 链表传输模式 Desc0 → Desc1 → Desc2 ↓ 指针链接 ↓ Desc5 ← Desc4 ← Desc3 中断可选:每描述符/链尾 网关驱动选型建议 低速外设(UART/SPI)→ 单次模式 | 音频/传感器 → 循环模式 | 网络/存储/多通道 → 链表模式

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 描述符链表工作流程

  1. 初始化: 驱动分配连续物理内存,填充描述符字段,设置NEXT_DESC_PTR形成链表。
  2. 提交: 将链表头描述符地址写入DMA控制器的描述符基址寄存器,启动传输。
  3. 执行: DMA控制器自动加载当前描述符,完成传输后通过NEXT_DESC_PTR加载下一个描述符。
  4. 中断: 当描述符的CONTROL字段中中断使能位为1时,传输完成后触发中断。
  5. 完成回调: 驱动在中断处理中调用dma_cookie_complete,更新cookie状态,唤醒等待队列。
常见陷阱: 描述符链表断裂是DMA超时的首要原因。典型场景包括:
(1) NEXT_DESC_PTR指向已释放的内存区域;
(2) 描述符未进行cache一致性维护(DMA内存需要dma_alloc_coherent或dma_map_single);
(3) 链表最后一个描述符的NEXT_DESC_PTR未清零,导致DMA控制器持续读取无效地址。
DMA描述符链表结构 描述符 0 NEXT → 0x1000_0040 SRC: 0x4000_1000 DST: 0x8000_2000 CTL: 1024, INT_EN STAT: 0x0001 (完成) 已传输完成 NEXT_PTR 描述符 1 NEXT → 0x1000_0080 SRC: 0x4000_2000 DST: 0x8000_3000 CTL: 512, INT_EN STAT: 0x0000 (进行中) 正在传输 NEXT_PTR 描述符 2 NEXT → 0x0000_0000 SRC: 0x4000_3000 DST: 0x8000_4000 CTL: 2048, INT_EN STAT: 0x0000 (等待) 等待传输 链尾:NEXT = NULL 调试检查清单 ① 每个描述符的NEXT_PTR是否指向有效物理地址?② 链尾NEXT_PTR是否为0?③ 描述符内存是否cache一致? 中断:每个描述符完成后触发

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保证内存序,是预防问题的基本实践。