3. DMA传输描述符:描述符结构、链表配置、源地址与目的地址对齐要求

DMA传输描述符(Descriptor)是DMA控制器与系统软件之间交换传输控制信息的核心数据结构。在嵌入式Linux系统中,描述符不仅定义了单次DMA传输的源地址、目的地址和传输长度,还通过链表机制实现连续、多块的传输调度。本章深入剖析描述符的硬件结构、链表配置方法以及地址对齐的工程约束。

3.1 描述符结构

描述符通常由硬件IP核定义,不同厂商(如NXP i.MX系列、TI OMAP、STM32)的描述符字段略有差异,但核心字段高度一致。下表列出了典型的DMA描述符字段布局:

字段 位宽 描述
SRC_ADDR 32/64 bit 源数据缓冲区物理地址
DST_ADDR 32/64 bit 目的数据缓冲区物理地址
TRANSFER_SIZE 16/32 bit 本次传输的字节数(或帧数×帧大小)
CONTROL 16/32 bit 传输方向、中断使能、传输类型、地址增减模式
NEXT_DESC_PTR 32/64 bit 下一个描述符的物理地址(链表关键字段)
STATUS 16/32 bit 传输完成标志、错误标志(硬件写回)
工程提示:在移植驱动时,务必确认描述符在内存中的对齐要求。多数DMA控制器要求描述符起始地址按8字节或16字节对齐,否则会触发总线错误或传输异常。

以下是一个基于C语言的描述符结构体定义示例(以32位地址为例):

struct dma_descriptor {
    uint32_t src_addr;        /* 源地址 */
    uint32_t dst_addr;        /* 目的地址 */
    uint32_t transfer_size;   /* 传输大小(字节) */
    uint32_t control;         /* 控制字 */
    uint32_t next_desc_ptr;   /* 下一个描述符指针 */
    uint32_t status;          /* 状态字(硬件更新) */
} __attribute__((aligned(16))); /* 强制16字节对齐 */
关键字段解析:
  • CONTROL字段:通常包含方向位(内存→外设/外设→内存/内存→内存)、地址增减模式(递增/固定)、数据宽度(8/16/32位)、中断使能位。
  • STATUS字段:传输完成后硬件自动置位完成标志,驱动通过轮询或中断读取该字段判断传输结束。
  • NEXT_DESC_PTR:若为NULL(0),表示链表结束;否则指向下一个描述符的物理地址。

3.2 描述符链表配置

描述符链表(Descriptor Chain)是DMA实现连续多块传输的核心机制。软件预先在内存中构建一个描述符数组或链表,每个描述符指向一块数据缓冲区,最后一个描述符的next_desc_ptr置为0。DMA控制器自动从链表头部开始依次处理每个描述符,直到遇到空指针。

链表配置的典型步骤如下:

  1. 分配内存:为描述符数组分配连续物理内存,确保对齐要求。
  2. 初始化描述符:填充每个描述符的源地址、目的地址、传输大小、控制字。
  3. 链接描述符:将第i个描述符的next_desc_ptr指向第i+1个描述符的物理地址。
  4. 设置链表头:将链表第一个描述符的物理地址写入DMA控制器的链表基址寄存器。
  5. 启动传输:使能DMA通道,控制器自动遍历链表。
避坑指南:描述符链表必须位于DMA可访问的内存区域(如DDR或OCRAM),且不能使用Cacheable区域,否则会导致描述符读取不一致。建议使用dma_alloc_coherent()分配一致性内存。

下面是一个描述符链表配置的代码片段:

struct dma_descriptor *desc_chain;
dma_addr_t desc_phys;
int num_desc = 4;

/* 分配一致性内存 */
desc_chain = dma_alloc_coherent(dev,
                num_desc * sizeof(struct dma_descriptor),
                &desc_phys, GFP_KERNEL);

for (int i = 0; i < num_desc; i++) {
    desc_chain[i].src_addr = src_buf_phys + i * BUF_SIZE;
    desc_chain[i].dst_addr = dst_buf_phys + i * BUF_SIZE;
    desc_chain[i].transfer_size = BUF_SIZE;
    desc_chain[i].control = DMA_DIR_MEM_TO_MEM | DMA_INTERRUPT_EN;
    desc_chain[i].status = 0;
    /* 链接:最后一个描述符的next_ptr置0 */
    desc_chain[i].next_desc_ptr = (i < num_desc - 1) ?
                                  desc_phys + (i+1) * sizeof(struct dma_descriptor) : 0;
}

/* 将链表基址写入DMA控制器寄存器 */
writel(desc_phys, dma_base + DMA_CHAN_BASE_ADDR);

下图展示了描述符链表的硬件遍历流程:

描述符链表硬件遍历流程 DMA控制器 描述符0 SRC/DST/CTRL next→描述符1 描述符1 SRC/DST/CTRL next→描述符2 描述符2 SRC/DST/CTRL next→NULL(结束) 遍历顺序 描述符0 → 描述符1 → 描述符2 → 结束 硬件自动加载next_ptr

3.3 源地址与目的地址对齐要求

地址对齐是DMA传输中极易出错但至关重要的约束。不同DMA控制器和总线协议对地址对齐的要求不同,主要分为以下三个层次:

对齐层次 要求 违反后果
描述符对齐 描述符基址按8/16/32字节对齐 总线错误、DMA挂死
缓冲区地址对齐 源/目的地址按传输数据宽度对齐(如32位传输需4字节对齐) 数据错位、性能下降
传输大小对齐 传输字节数应为数据宽度的整数倍 部分数据丢失、传输不完整
对齐规则速查表(常见场景):
  • 内存→内存(MEM2MEM):源和目的地址均需按传输宽度对齐。若使用32位传输,地址必须4字节对齐。
  • 内存→外设(MEM2PER):源地址需对齐;目的地址(外设FIFO地址)通常固定对齐,无需软件调整。
  • 外设→内存(PER2MEM):源地址(外设FIFO)固定对齐;目的地址需对齐。
  • I2C/SPI等串行外设:通常使用8位传输宽度,地址只需1字节对齐,但描述符本身仍需按硬件要求对齐。

在驱动移植过程中,对齐问题常出现在以下场景:

  • 网络协议栈缓冲区:skb数据起始地址可能未对齐,需使用skb_reserve()或拷贝到对齐缓冲区。
  • 音频ALSA缓冲区:period大小可能不是DMA传输宽度的整数倍,需在驱动层做对齐处理。
  • I2C消息合并:多个I2C消息合并为一次DMA传输时,需确保总长度对齐。

下图展示了地址对齐与未对齐对DMA传输的影响:

地址对齐 vs 未对齐对DMA传输的影响 ✅ 对齐传输(4字节对齐) Byte0 Byte1 Byte2 Byte3 → 一次32位突发读取,效率高 ❌ 未对齐传输(地址偏移1字节) Byte0 Byte1 Byte2 Byte3 → 需要两次总线访问拼接,效率低 对齐检查方法 (addr & (width-1)) == 0 例如:4字节对齐检查 (addr & 0x3) == 0
工程实践建议:
  • 在驱动初始化阶段,使用BUILD_BUG_ON()编译时检查描述符结构体大小和对齐。
  • 在DMA传输提交函数中,增加运行时对齐检查,若未对齐则返回-EINVAL。
  • 对于无法保证对齐的缓冲区(如用户态传入的地址),使用dma_map_single()并检查返回的dma_addr是否满足对齐要求。

3.4 描述符配置的典型错误与调试方法

在I2C驱动移植适配DMA时,描述符配置错误是最常见的故障来源。下表总结了典型错误及其调试手段:

错误类型 现象 调试方法
描述符地址未对齐 DMA启动后立即超时或总线错误 检查dma_alloc_coherent返回地址的低位
next_ptr指向无效地址 传输完第一个描述符后挂死 打印链表所有描述符的next_ptr值
传输大小与数据宽度不匹配 最后几个字节丢失或数据错位 检查transfer_size % data_width == 0
控制字方向位错误 数据全为0或外设无响应 对比寄存器手册确认方向位定义
调试技巧:在描述符提交后、启动DMA前,通过print_hex_dump()打印描述符内存内容,与寄存器手册逐字段比对,可快速定位配置错误。