2. DMA控制器硬件架构:DMA通道、请求线、仲裁机制、传输宽度与突发传输

DMA(Direct Memory Access)控制器是嵌入式系统中负责在外设与存储器之间、或存储器与存储器之间直接搬运数据的硬件引擎。理解其内部架构是驱动移植与性能调优的基础。本节从五个核心维度拆解DMA控制器的硬件组成与运行原理。

2.1 DMA通道(Channel)

DMA通道是独立的数据搬运路径,每个通道包含一组寄存器(源地址、目的地址、传输计数、控制配置),可独立配置并服务于一个外设或一次传输任务。多通道设计允许并发处理多个DMA请求。

关键特性:
  • 独立性:每个通道拥有自己的配置寄存器组,互不干扰。
  • 优先级可编程:软件可为每个通道分配优先级(高/低/固定轮询)。
  • 传输粒度:支持字节(8-bit)、半字(16-bit)、字(32-bit)传输宽度。
  • 循环模式:支持自动重载传输计数,用于连续数据流(如音频ADC)。
DMA多通道架构示意图 外设A (UART) 外设B (SPI) 外设C (I2C) 通道0 优先级:高 通道1 优先级:中 通道2 优先级:低 仲裁器 固定优先级 +轮询 系统总线 AHB / AXI SRAM Flash 外设寄存器

2.2 DMA请求线(Request Line)

外设通过硬件请求线向DMA控制器发出数据传输请求。每条请求线对应一个外设事件(如UART接收缓冲区非空、定时器溢出、ADC转换完成)。DMA控制器检测到请求后,根据配置启动一次传输。

请求类型 触发条件 典型应用
硬件请求 外设硬件信号电平/边沿 UART RX、SPI TX、I2C数据就绪
软件触发 写寄存器启动位 存储器到存储器拷贝
外设事件 定时器比较匹配、ADC转换结束 定时触发ADC采样搬运
链式请求 前一个DMA传输完成 描述符链表自动加载下一段传输
驱动适配要点:在Linux DMA引擎框架中,dma_request_channel() 函数通过匹配 dma_slave_map 表将外设请求线与DMA通道绑定。设备树中需正确声明 dmas 和 dma-names 属性。

2.3 仲裁机制(Arbitration)

当多个DMA通道同时请求总线访问时,仲裁器决定哪个通道获得总线控制权。仲裁策略直接影响实时性和吞吐量。

DMA仲裁机制流程图 多个通道请求 优先级相同? 是 轮询 Round-Robin 否 固定优先级 高优先先服务 获得总线授权
常见陷阱:在I2C高速传输(400kHz以上)场景中,若DMA通道优先级设置不当,低优先级通道可能因频繁被抢占而导致I2C时钟拉伸超时,引发总线错误。建议将I2C DMA通道设为最高优先级。

2.4 传输宽度(Transfer Width)

传输宽度定义DMA每次总线访问搬运的数据位宽。源和目的宽度可以不同,硬件自动进行对齐与填充。

宽度枚举 位宽 典型外设 地址递增步长
DMA_SLAVE_BUSWIDTH_1_BYTE 8-bit UART、I2C、低速SPI 1
DMA_SLAVE_BUSWIDTH_2_BYTES 16-bit 音频I2S、16位ADC 2
DMA_SLAVE_BUSWIDTH_4_BYTES 32-bit 高速SPI、并行LCD、以太网 4
DMA_SLAVE_BUSWIDTH_8_BYTES 64-bit DDR控制器、高性能加速器 8
宽度不匹配处理:当源宽度为8-bit、目的宽度为32-bit时,DMA控制器自动将4个字节打包为一个字写入目的地址。驱动中需通过 struct dma_slave_config 的 src_addr_width 和 dst_addr_width 字段分别配置。

2.5 突发传输(Burst Transfer)

突发传输允许DMA在一次总线仲裁授权后连续传输多个数据单元,减少总线仲裁开销,提高有效带宽。突发长度(Burst Length)表示一次突发中传输的数据单元个数。

单次传输 vs 突发传输(Burst=4) 单次传输: D1 D2 D3 D4 每次传输都需要仲裁 突发传输(Burst=4): D1 D2 D3 D4 一次仲裁,连续传输4个数据单元 突发传输优势 • 减少总线仲裁次数,降低延迟 • 提高总线利用率(有效数据/总周期) • 适合高速外设(如I2C FIFO深度≥4) • 突发长度需匹配FIFO深度,避免溢出 • 典型值:4、8、16(受总线协议限制)

2.6 综合配置示例

以下代码展示在Linux DMA引擎框架中配置I2C从设备DMA传输的典型流程,涵盖通道、请求线、传输宽度与突发长度设置:

#include <linux/dmaengine.h>
#include <linux/dma-mapping.h>

struct dma_slave_config config = {
    .direction = DMA_DEV_TO_MEM,          // I2C接收:外设→内存
    .src_addr = (phys_addr_t)i2c_base + 0x10, // I2C数据寄存器地址
    .src_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE, // I2C每次1字节
    .src_maxburst = 4,                    // 突发长度4,匹配I2C FIFO深度
    .dst_addr = (phys_addr_t)buf_phys,    // 目的内存物理地址
    .dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES, // 内存按32-bit对齐
    .dst_maxburst = 4,
};

struct dma_chan *chan;
chan = dma_request_channel(mask, filter_fn, "i2c0_rx");
if (!chan) {
    dev_err(dev, "Failed to request DMA channel\n");
    return -ENODEV;
}

dmaengine_slave_config(chan, &config);

struct dma_async_tx_descriptor *tx;
tx = dmaengine_prep_slave_sg(chan, sgl, nr_sg, DMA_DEV_TO_MEM,
                             DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
tx->callback = dma_rx_callback;
tx->callback_param = priv;

dmaengine_submit(tx);
dma_async_issue_pending(chan);
工程实践建议:
  • 在I2C驱动中,建议将 src_maxburst 设置为I2C控制器FIFO深度的一半,避免FIFO溢出。
  • 使用 dma_get_slave_caps() 查询控制器支持的突发长度范围,不同IP核差异较大。
  • 对于多通道并发场景,合理分配优先级并启用轮询模式可避免低优先级通道饿死。