Linux内核DMA控制器驱动开发从入门到精通 - 第3节
3.1 DMA控制器内部组成
DMA(Direct Memory Access)控制器是系统中负责在无需CPU干预的情况下,直接在外设与内存之间、内存与内存之间搬运数据的硬件模块。理解其内部组成是驱动开发的基础。
3.1.1 核心组件架构
一个典型的DMA控制器内部包含以下关键单元:
- 通道(Channel):每个通道独立管理一条数据传输链路,支持并发传输。
- 请求线(Request Line):外设通过硬件信号线向DMA控制器发起传输请求。
- 地址寄存器(Address Register):存储源地址和目标地址,支持递增/递减模式。
- 控制寄存器(Control Register):配置传输方向、数据宽度、突发长度、中断使能等参数。
- 状态寄存器(Status Register):指示传输完成、错误、通道忙等状态。
- 仲裁器(Arbiter):当多个通道同时请求时,决定优先级顺序。
驱动开发要点: 在Linux内核中,每个DMA通道对应一个
dma_chan 结构体,通过 dma_request_channel() 申请。地址寄存器和控制寄存器的操作封装在 dma_slave_config 和 dma_async_tx_descriptor 中。
3.1.2 通道与请求线映射
每个DMA通道可以连接一个或多个外设请求线。在硬件设计上,请求线通过多路选择器(MUX)连接到通道。驱动开发时需要配置通道与请求线的映射关系。
| 通道编号 | 默认请求线 | 可配置外设 | 典型用途 |
|---|---|---|---|
| Ch 0 | REQ 0 | UART TX | 串口发送 |
| Ch 1 | REQ 1 | UART RX | 串口接收 |
| Ch 2 | REQ 2 | SPI TX | SPI发送 |
| Ch 3 | REQ 3 | SPI RX | SPI接收 |
| Ch 4 | REQ 4 | I2C / 定时器 | 内存到内存 |
驱动提示: 在设备树中通过
dma-requests 属性指定请求线数量,通过 dmas 和 dma-names 属性绑定通道与外设。例如:dmas = <&dma0 0 0x10> 表示使用DMA0的通道0,请求线为0x10。
3.2 地址寄存器与控制寄存器详解
3.2.1 地址寄存器
每个DMA通道包含两个地址寄存器:
- 源地址寄存器(SAR):存放数据源的内存地址或外设地址。
- 目标地址寄存器(DAR):存放数据目标的内存地址或外设地址。
地址寄存器支持以下模式:
| 模式 | 描述 | 适用场景 |
|---|---|---|
| 递增模式 | 每次传输后地址自动增加数据宽度 | 内存到内存、内存到外设(内存端) |
| 递减模式 | 每次传输后地址自动减少数据宽度 | 特殊缓冲区处理 |
| 固定模式 | 地址保持不变 | 外设FIFO寄存器访问 |
3.2.2 控制寄存器
控制寄存器是DMA通道配置的核心,典型位域定义如下:
/* 控制寄存器位域定义示例 */
#define DMA_CTRL_ENABLE (1 << 0) /* 通道使能 */
#define DMA_CTRL_DIR_MEM2MEM (0 << 1) /* 内存到内存 */
#define DMA_CTRL_DIR_MEM2DEV (1 << 1) /* 内存到外设 */
#define DMA_CTRL_DIR_DEV2MEM (2 << 1) /* 外设到内存 */
#define DMA_CTRL_WIDTH_8BIT (0 << 3) /* 8位数据宽度 */
#define DMA_CTRL_WIDTH_16BIT (1 << 3) /* 16位数据宽度 */
#define DMA_CTRL_WIDTH_32BIT (2 << 3) /* 32位数据宽度 */
#define DMA_CTRL_BURST_1 (0 << 5) /* 突发长度1 */
#define DMA_CTRL_BURST_4 (1 << 5) /* 突发长度4 */
#define DMA_CTRL_BURST_8 (2 << 5) /* 突发长度8 */
#define DMA_CTRL_IE_COMPLETE (1 << 8) /* 传输完成中断使能 */
#define DMA_CTRL_IE_ERROR (1 << 9) /* 错误中断使能 */
常见错误: 配置控制寄存器时,必须先清除使能位(ENABLE),再修改其他配置位,最后重新使能。否则可能导致硬件状态机紊乱,产生不可预期的传输行为。
3.3 传输类型
DMA控制器支持三种基本传输类型,每种类型对应不同的硬件配置和驱动接口。
3.3.1 内存到内存(Memory-to-Memory)
源和目标都是内存地址,通常用于数据拷贝、缓冲区移动、内存初始化等场景。不需要外设请求线触发,由软件启动传输。
- 特点:无需外设参与,CPU只需设置源地址和目标地址。
- 触发方式:软件写通道使能位立即启动。
- 典型应用:
memcpy加速、帧缓冲拷贝、加密/校验预处理。 - Linux驱动接口:
dma_async_memcpy_buf_to_buf()
3.3.2 内存到外设(Memory-to-Device)
数据从内存传输到外设的FIFO或寄存器,通常用于发送数据。外设通过请求线通知DMA控制器需要数据。
- 特点:内存地址递增,外设地址固定。
- 触发方式:外设断言请求线,DMA控制器响应后从内存读取数据写入外设。
- 典型应用:UART发送、SPI发送、I2C发送、LCD显示数据传输。
- Linux驱动接口:
dmaengine_prep_slave_sg()方向设置为DMA_MEM_TO_DEV
3.3.3 外设到内存(Device-to-Memory)
数据从外设的FIFO或寄存器传输到内存,通常用于接收数据。外设收到数据后通过请求线通知DMA控制器取走数据。
- 特点:外设地址固定,内存地址递增。
- 触发方式:外设断言请求线,DMA控制器从外设读取数据写入内存。
- 典型应用:UART接收、SPI接收、I2C接收、ADC采样数据采集。
- Linux驱动接口:
dmaengine_prep_slave_sg()方向设置为DMA_DEV_TO_MEM
3.3.4 传输类型在Linux驱动中的配置
在Linux DMA引擎框架中,通过 dma_slave_config 结构体配置传输参数:
struct dma_slave_config config = {
.direction = DMA_MEM_TO_DEV, /* 传输方向 */
.src_addr = (dma_addr_t)buf_phys, /* 源物理地址 */
.dst_addr = (dma_addr_t)uart_base, /* 目标物理地址 */
.src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES, /* 源数据宽度 */
.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES, /* 目标数据宽度 */
.src_maxburst = 4, /* 源突发长度 */
.dst_maxburst = 4, /* 目标突发长度 */
};
/* 应用配置到DMA通道 */
dmaengine_slave_config(chan, &config);
/* 准备传输描述符 */
desc = dmaengine_prep_slave_sg(chan, sgt, nents, DMA_MEM_TO_DEV,
DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
/* 提交并启动传输 */
dmaengine_submit(desc);
dma_async_issue_pending(chan);
关键点:
src_addr和dst_addr必须是物理地址,不能是虚拟地址。- 对于内存到内存传输,使用
dma_async_memcpy_buf_to_buf()简化操作。 - 突发长度(maxburst)应与外设FIFO深度匹配,避免溢出或欠载。
- 传输完成回调函数中必须调用
dma_cookie_complete()更新状态。
3.4 本章小结
DMA控制器的硬件原理是驱动开发的理论基础。核心要点包括:
- 每个通道独立管理一条传输链路,包含地址寄存器、控制寄存器和状态寄存器。
- 请求线是外设与DMA控制器之间的硬件握手信号,驱动中通过设备树配置映射关系。
- 三种传输类型(内存到内存、内存到外设、外设到内存)对应不同的硬件触发方式和地址模式。
- Linux DMA引擎框架通过
dma_slave_config和dmaengine_prep_slave_sg()封装了硬件寄存器的操作。