Linux DMA Engine 驱动开发深度实战:从 dmaengine API 到 Slave DMA 传输工程实践
DMA(Direct Memory Access)是所有现代计算机系统中绕过 CPU 进行数据搬运的基础设施。Linux 内核自 2.6 时代引入 dmaengine 框架以来,已经发展出一套完整的 DMA 控制器驱动和客户端 API 体系。然而在开源社区中,关于 dmaengine 驱动端(即控制器端)开发的系统性文章极为稀缺——绝大多数文档聚焦于如何使用 DMA 做 Memcpy 或 PCIe BMDMA,而忽略了如何为一个自定义 SoC DMA 控制器编写内核驱动本身。本文将从 dmaengine 框架的核心数据结构出发,系统性地讲解驱动端开发的完整流程,并给出可落地的工程实践。
一、dmaengine 框架架构总览
在深入驱动代码之前,先理解 dmaengine 的三层结构:
┌─────────────────────────────────────────────────────────┐
│ Client Driver (SPI/I2S/UART) │
│ 调用 dmaengine_prep_slave_sg / dmaengine_submit │
├─────────────────────────────────────────────────────────┤
│ dmaengine Core (drivers/dma/dmaengine.c) │
│ 资源分配、channel 管理、_descriptor 生命周期 │
├─────────────────────────────────────────────────────────┤
│ DMA Controller Driver (Vendor-specific) │
│ 实现 struct dma_device_ops / struct dma_chan 操作 │
│ 描述符构建、硬件寄存器编程、中断处理 │
└─────────────────────────────────────────────────────────┘
核心数据结构的关系:
struct dma_device—— 代表整个 DMA 控制器,一个驱动实例对应一个struct dma_chan—— 代表一个 DMA 通道,是客户端申请的基本单位struct virt_dma_desc/struct virt_dma_chan—— dmaengine 提供的虚拟通道抽象,帮助驱动管理 descriptor 的 pending/issued/active 队列struct dma_async_tx_descriptor—— 描述一次异步传输的元数据
对于驱动开发者而言,核心工作是实现一组硬件操作回调,并将这些回调注册到 dma_device 中。
二、驱动骨架:注册一个 DMA 控制器
最简的驱动骨架如下(基于 kernel 6.x):
#include <linux/dmaengine.h>
#include <linux/dma-mapping.h>
#include <linux/dma-map-ops.h>
struct my_dma_chan {
struct virt_dma_chan vc;
void __iomem *regs;
struct my_dma_desc *current;
bool paused;
spinlock_t lock;
};
struct my_dma_dev {
struct dma_device dma_dev;
void __iomem *base;
struct my_dma_chan channels[MAX_CHANNELS];
};
static const struct dma_chan_ops my_dma_chan_ops;
static int my_dma_probe(struct platform_device *pdev)
{
struct my_dma_dev *mdev;
struct dma_device *ddev;
int i, ret;
mdev = devm_kzalloc(&pdev->dev, sizeof(*mdev), GFP_KERNEL);
if (!mdev)
return -ENOMEM;
ddev = &mdev->dma_dev;
INIT_LIST_HEAD(&ddev->channels);
/* 声明支持的传输类型 */
dma_cap_set(DMA_SLAVE, ddev->cap_mask);
dma_cap_set(DMA_CYCLIC, ddev->cap_mask);
dma_cap_set(DMA_INTERLEAVE, ddev->cap_mask);
/* 设置回调函数 */
ddev->dev = &pdev->dev;
ddev->device_alloc_chan_resources = my_dma_alloc_chan_resources;
ddev->device_free_chan_resources = my_dma_free_chan_resources;
ddev->device_prep_slave_sg = my_dma_prep_slave_sg;
ddev->device_prep_dma_cyclic = my_dma_prep_dma_cyclic;
ddev->device_prep_interleaved_dma = my_dma_prep_interleaved;
ddev->device_config = my_dma_config;
ddev->device_pause = my_dma_pause;
ddev->device_resume = my_dma_resume;
ddev->device_terminate_all = my_dma_terminate_all;
ddev->device_tx_status = my_dma_tx_status;
ddev->device_issue_pending = my_dma_issue_pending;
/* SG 约束 */
ddev->copy_align = DMAENGINE_ALIGN_4_BYTES;
ddev->src_addr_widths = BIT(DMA_SLAVE_BUSWIDTH_1_BYTE) |
BIT(DMA_SLAVE_BUSWIDTH_2_BYTES) |
BIT(DMA_SLAVE_BUSWIDTH_4_BYTES);
ddev->dst_addr_widths = BIT(DMA_SLAVE_BUSWIDTH_1_BYTE) |
BIT(DMA_SLAVE_BUSWIDTH_2_BYTES) |
BIT(DMA_SLAVE_BUSWIDTH_4_BYTES);
ddev->directions = BIT(DMA_MEM_TO_DEV) | BIT(DMA_DEV_TO_MEM) |
BIT(DMA_MEM_TO_MEM);
ret = dmaenginem_async_device_register(ddev);
if (ret) {
dev_err(&pdev->dev, "Failed to register DMA device\n");
return ret;
}
/* 注册 suspend/resume */
platform_set_drvdata(pdev, mdev);
return 0;
}
关键要点:
dma_cap_set声明控制器支持的能力。如果只支持 slave DMA 就只设DMA_SLAVE;支持循环缓冲设DMA_CYCLIC;支持交织传输设DMA_INTERLEAVE。dmaenginem_async_device_register()是 5.x+ 推荐的注册函数,它会异步初始化 channel 并发送 uevent,让 DMA controller 在 probe 阶段就可以被访问。- src_addr_widths / dst_addr_widths / directions 这三个字段是 dmaengine core 做客户端请求合法性检查的依据,务必精确填写。
三、核心操作详解
3.1 alloc/free_chan_resources:通道资源管理
static int my_dma_alloc_chan_resources(struct dma_chan *chan)
{
struct my_dma_chan *mchan = to_my_dma_chan(chan);
struct my_dma_dev *mdev = to_my_dma_dev(chan->device);
/* 使能通道时钟 */
ret = clk_prepare_enable(mchan->clk);
if (ret)
return ret;
/* 配置默认仲裁优先级 */
writel(CHAN_PRIORITY_DEFAULT, mchan->regs + CHAN_REG_PRIORITY);
spin_lock_init(&mchan->lock);
return 0;
}
在 free 阶段,务必确保在释放资源前调用 dmaengine_terminate_sync()(从驱动内部看,客户端调用 terminate 后的 free 路径保证 channel 已停止)。
3.2 prep_slave_sg:构建 Slave Scatter-Gather 传输
这是客户端驱动最常调用的接口,用于提交一个或一组 memory buffer 到外设 FIFO 的传输:
static struct dma_async_tx_descriptor *
my_dma_prep_slave_sg(struct dma_chan *chan, struct scatterlist *sgl,
unsigned int sg_len, enum dma_transfer_direction dir,
unsigned long flags, void *context)
{
struct my_dma_chan *mchan = to_my_dma_chan(chan);
struct my_dma_desc *desc;
struct scatterlist *sg;
int i;
if (sg_len > MAX_DESC_ENTRIES)
return NULL;
desc = kzalloc(sizeof(*desc) + sg_len * sizeof(desc->nodes[0]),
GFP_NOWAIT);
if (!desc)
return NULL;
desc->direction = dir;
desc->sg_count = sg_len;
desc->cyclic = false;
for_each_sg(sgl, sg, sg_len, i) {
desc->nodes[i].addr = sg_dma_address(sg);
desc->nodes[i].len = sg_dma_len(sg);
/* 硬件 FIFO 宽度校验 */
if (dir == DMA_MEM_TO_DEV) {
desc->nodes[i].src_addr = sg_dma_address(sg);
desc->nodes[i].dst_addr = mchan->slave_config.dst_addr;
desc->nodes[i].src_addr_width = mchan->src_width;
desc->nodes[i].dst_addr_width = mchan->dst_width;
}
}
/* 在 virt_dma 框架下初始化 descriptor */
desc->vd.tx.tx_submit = vchan_tx_init;
desc->vd.tx.flags = flags;
desc->vd.tx.phys = 0;
return vchan_tx_prep(&mchan->vc, &desc->vd, flags);
}
工程细节:
GFP_NOWAIT是必须的,因为prep_*函数可能在中断上下文中调用- 硬件 FIFO 宽度(
src_addr_width/dst_addr_width)必须在device_config阶段通过dmaengine_slave_config()传递,并记录在my_dma_chan中 - 控制器如有硬件 Linked List 或 Scatter-Gather 支持,尽量在硬件层面做链式装载以减少中断频率
3.3 prep_dma_cyclic:音频场景的零中断方案
循环传输在音频(ALSA PCM DMA)、SDIO 等场景中是刚需。它利用控制器自身的环形缓冲能力,在中断触发时只切换过半/完成回调,从而大幅降低中断频率:
static struct dma_async_tx_descriptor *
my_dma_prep_dma_cyclic(struct dma_chan *chan, dma_addr_t buf_addr,
size_t buf_len, size_t period_len,
enum dma_transfer_direction dir,
unsigned long flags)
{
struct my_dma_desc *desc;
unsigned int periods;
if (buf_len % period_len)
return NULL;
periods = buf_len / period_len;
if (periods < 2 || periods > MAX_PERIODS)
return NULL;
desc = kzalloc(sizeof(*desc) + periods * sizeof(desc->nodes[0]),
GFP_NOWAIT);
if (!desc)
return NULL;
desc->direction = dir;
desc->cyclic = true;
desc->buf_addr = buf_addr;
desc->buf_len = buf_len;
desc->period_len = period_len;
desc->sg_count = periods;
for (i = 0; i < periods; i++) {
desc->nodes[i].addr = buf_addr + i * period_len;
desc->nodes[i].len = period_len;
}
desc->vd.tx.tx_submit = vchan_tx_init;
return vchan_tx_prep(&mchan->vc, &desc->vd, flags);
}
实战经验:
- period_len 的选择直接影响中断粒度。音频双声道 48kHz 16bit 场景下,典型值取 4096 bytes(对应 ~21ms),中断频率约 238 Hz
- 在硬件层面,周期性传输通常需要配置
Cyclic Buffer Mode或Repeat Count寄存器 - 务必在 prep 阶段校验 buf_len 必须是 period_len 的整数倍,否则会在中断中产生越界
3.4 issue_pending:启动传输与硬件队列装载
issue_pending 是 spawn point,从 virt_dma 的 issued_list 中取出 descriptor 并写到硬件:
static void my_dma_issue_pending(struct dma_chan *chan)
{
struct my_dma_chan *mchan = to_my_dma_chan(chan);
struct virt_dma_desc *vd;
unsigned long flags;
spin_lock_irqsave(&mchan->vc.lock, flags);
if (vchan_issue_pending(&mchan->vc) && !mchan->current) {
vd = list_first_entry(&mchan->vc.desc_issued_list,
struct virt_dma_desc, node);
list_del(&vd->node);
mchan->current = to_my_dma_desc(vd);
my_dma_start_transfer(mchan);
}
spin_unlock_irqrestore(&mchan->vc.lock, flags);
}
static void my_dma_start_transfer(struct my_dma_chan *mchan)
{
struct my_dma_desc *desc = mchan->current;
/* 写入第一个节点地址并启动 */
writel(lower_32_bits(desc->nodes[0].addr),
mchan->regs + CHAN_REG_SRC_ADDR);
writel(desc->nodes[0].len | CHAN_START_IRQ_EN,
mchan->regs + CHAN_REG_CONTROL);
mchan->completed_node = 0;
}
关键设计:
- 在
issue_pending中只启动第一个节点(或第一个硬件链表项),后续节点通过中断或硬件链式触发推进 - 如果控制器硬件支持 LLI(Linked List Item)描述符,应该在
start_transfer中一次性装载全部节点链以减少 CPU 干预
四、中断处理与描述符生命周期
中断处理是驱动中最复杂的部分。以支持 LLI 的控制器为例:
static irqreturn_t my_dma_irq(int irq, void *dev_id)
{
struct my_dma_dev *mdev = dev_id;
struct my_dma_chan *mchan;
struct my_dma_desc *desc;
u32 status;
int i;
for (i = 0; i < MAX_CHANNELS; i++) {
mchan = &mdev->channels[i];
status = readl(mchan->regs + CHAN_REG_IRQ_STATUS);
if (!(status & CHAN_IRQ_MASK))
continue;
/* 清中断 */
writel(status & CHAN_IRQ_MASK, mchan->regs + CHAN_REG_IRQ_CLEAR);
desc = mchan->current;
if (!desc)
continue;
if (status & CHAN_IRQ_BLOCK) {
mchan->completed_node++;
if (desc->cyclic) {
vchan_cyclic_callback(&desc->vd);
} else if (mchan->completed_node >= desc->sg_count) {
/* 传输完成 */
spin_lock(&mchan->vc.lock);
vchan_cookie_complete(&desc->vd);
mchan->current = NULL;
spin_unlock(&mchan->vc.lock);
/* 检查 issued_list,启动下一个传输 */
tasklet_schedule(&mchan->vc.task);
}
}
if (status & CHAN_IRQ_ERROR) {
/* 错误处理:终止通道并通知客户端 */
dev_err(mdev->dma_dev.dev,
"DMA channel %d error irq status 0x%08x\n",
i, status);
}
}
return IRQ_HANDLED;
}
vchan_cookie_complete() 是 dmaengine 框架的标准回调通知机制,它会将 descriptor 从 active_list 移到 done_list,并调用客户端在 tx_submit() 时注册的 callback。在中断上下文里必须先获取 vc.lock(spin_lock 版本)再调用此函数。
五、terminate 与 pause/resume:流式控制实现
流式控制是 dmaengine 相对容易出错的地方。重点关注 terminate_all 必须真正停止硬件:
static int my_dma_terminate_all(struct dma_chan *chan)
{
struct my_dma_chan *mchan = to_my_dma_chan(chan);
unsigned long flags;
LIST_HEAD(head);
spin_lock_irqsave(&mchan->vc.lock, flags);
/* 1. 硬件停止 */
writel(CHAN_CTRL_STOP, mchan->regs + CHAN_REG_CONTROL);
writel(0, mchan->regs + CHAN_REG_IRQ_MASK);
/* 2. 等待硬件实际停止 */
readl_relaxed_poll_timeout(mchan->regs + CHAN_REG_STATUS,
val, !(val & CHAN_STATUS_BUSY),
1, 1000);
/* 3. 将该通道所属的 virt_dma 清空 */
vchan_get_all_descriptors(&mchan->vc, &head);
mchan->current = NULL;
spin_unlock_irqrestore(&mchan->vc.lock, flags);
/* 4. 在进程上下文中调用回调 */
vchan_dma_desc_free_list(&mchan->vc, &head);
return 0;
}
实战踩坑经验:
- 不能在 spin_lock 中调用
vchan_dma_desc_free_list(),因为它最终会调用dma_descriptor_unmap(),后者可能触发 IOMMU 的 TLB 同步操作,在某些架构上会 sleep pause实现不是简单关闭中断,而是必须让硬件暂停在当前 block边界,并且在resume时能在恢复后继续传输- 对于有 embedded descriptor 的控制器(如 dw_dmac),terminate 前要清
LLI指针防止硬件执行野指针
六、DMA_INTERLEAVE:高维数据搬运
DMA_INTERLEAVE 能力允许 DMA 控制器做非连续、带 skip 的传输——常见于图像处理(搬运 ROI 区域)、矩阵转置上游数据准备等场景。
static struct dma_async_tx_descriptor *
my_dma_prep_interleaved(struct dma_chan *chan,
struct dma_interleaved_template *xt,
unsigned long flags)
{
struct my_dma_desc *desc;
size_t dst_sg;
size_t src_sg;
/* 校验:控制器仅支持 4-byte aligned 的 frame interleaving */
if (!IS_ALIGNED(xt->sgl[0].size, 4) ||
!IS_ALIGNED(xt->numf, 1))
return NULL;
desc = kzalloc(sizeof(*desc), GFP_NOWAIT);
if (!desc)
return NULL;
desc->direction = xt->dir;
desc->cyclic = false;
desc->interleave true;
desc->xt = *xt;
/* 将交错模板映射到控制器寄存器:
* frame_size → XT_FRAME_LEN
* numf → XT_FRAME_COUNT
* sgl.size → XT_BLOCK_SIZE
* sgl.steps → XT_FRAME_STEP
*/
writel(xt->sgl[0].size, mchan->regs + CHAN_REG_XT_BLOCK);
writel(xt->numf, mchan->regs + CHAN_REG_XT_FRAMES);
writel(xt->frame_size, mchan->regs + CHAN_REG_XT_FRAME_STEP);
desc->vd.tx.tx_submit = vchan_tx_init;
return vchan_tx_prep(&mchan->vc, &desc->vd, flags);
}
在开源驱动中,xilinx_dpdma 和 mcdp_dma 是 interleaved DMA 的参考实现。如果你的 SoC 支持 scatter/gather LLI 但不直接支持 interleave 模式,可以在驱动内部将 interleaved 模板展开为 SG 列表——代价是增加 descriptor 节点数量。
七、DMA_SLAVE_BUSWIDTH 与 FIFO 阈值的实战考量
在 slave DMA 场景中,源宽度和目的宽度通常不同——例如从 32-bit APB SRAM(源宽度 4 bytes)写到 16-bit I2S TX FIFO(目的宽度 2 bytes)。这就要求控制器支持 auto-reload 或 bus-width 转换。
static int my_dma_config(struct dma_chan *chan,
struct dma_slave_config *config)
{
struct my_dma_chan *mchan = to_my_dma_chan(chan);
mchan->slave_config.dst_addr = config->dst_addr;
mchan->slave_config.dst_addr_width = config->dst_addr_width;
mchan->slave_config.dst_maxburst = config->dst_maxburst;
mchan->slave_config.src_addr = config->src_addr;
mchan->slave_config.src_addr_width = config->src_addr_width;
mchan->slave_config.src_maxburst = config->src_maxburst;
/* 硬件 FIFO 阈值配置:
* - dst_maxburst < FIFO_DEPTH 时,设置 FIFO_THRESHOLD = dst_maxburst - 1
* - 若控制器支持 dynamic burst,根据 period_len 计算最优 burst
*/
if (config->dst_addr_width < mchan->fifo_depth) {
writel(config->dst_addr_width - 1,
mchan->regs + CHAN_REG_FIFO_THRESH);
}
return 0;
}
maxburst参数直接关联 FIFO 阈值:FIFO 满时触发 DMA request,burst 数对应一次 DMA 传输的字节数- 对于循环音频传输,
period_len / (data_width * channel_count)通常就是硬件应该用的burst count src_addr_width必须能被dst_addr_width整除,否则会产生 bus width mismatch 错误
八、DMA_MEM_TO_MEM:Memcpy 通道的特殊处理
如果你的控制器支持 memorial-to-memorial 传输(许多 SoC DMA 都支持),还需要实现 device_prep_dma_memcpy。但要注意一点:memcpy 通道和 slave 通道通常是物理上分离的,很多 SoC 的 DMA 控制器有多个通道,一部分只能做 slave,一部分只能做 memcpy。
static struct dma_async_tx_descriptor *
my_dma_prep_dma_memcpy(struct dma_chan *chan, dma_addr_t dest,
dma_addr_t src, size_t len, unsigned long flags)
{
struct dma_async_tx_descriptor *tx;
/* 要求:长度必须对齐到最小总线宽度 */
if (!IS_ALIGNED(len, 4) ||
!IS_ALIGNED(dest, 4) ||
!IS_ALIGNED(src, 4))
return NULL;
tx = my_dma_prep_sg(chan, NULL, 0, dest, src, len,
DMA_MEM_TO_MEM, flags);
/* ... 实际硬件配置 ... */
return tx;
}
参考实现:pl330_dma 和 bcm2835-dma。这两款驱动的 memcpy 通道和 slave 通道分离模式非常经典。
九、Device Tree 绑定与客户端驱动发现
DMA controller 设备树节点示例:
my_dma: dma-controller@10020000 {
compatible = "myvendor,my-dma-v2";
reg = <0x10020000 0x10000>;
interrupts = <GIC_SPI 42 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clk_dma>;
#dma-cells = <1>;
dma-channels = <8>;
dma-requests = <32>;
};
客户端(如 I2S 驱动)在 dts 中引用:
i2s: sound@10030000 {
compatible = "myvendor,my-i2s";
dmas = <&my_dma 4>, <&my_dma 5>;
dma-names = "tx", "rx";
};
客户端通过 dma_request_chan(dev, "tx") 获取通道,内部走 of_dma_router 查找路由表完成 request→channel 的映射。
调试 tip:如果客户端 dma_request_chan 返回 -EPROBE_DEFER,检查 controller 是否比 client 晚 probe——这在新版 kernel 中不再致命,client 会等待。
十、性能调优与生产实践要点
10.1 中断合并为关键指标
对于一个支持 16 通道的音频 DMA 控制器,若每 block 产生一次中断,1 ms 粒度下每秒产生 16000 次中断。通过配置 LLI 链式传输(每 16 个 block 才进一次中断),中断频率降至 1000 Hz,CPU 占用下降 90% 以上。
10.2 资源占用与释放的最佳实践
- alloc 和 free 必须成对:clients 可能在 stream on/off 多次调用 request/free,务必保证无泄漏
- terminate_all 之后不要复用 virt_dma_desc:在启动新的传输前应从 issued_list 重新取出
10.3 多通道仲裁与优先级
在有多通道同时请求的场景(如 ADC+DAC 同步采集),寄存器级的通道优先级仲裁是关键。若硬件轮询,CPU 可通过动态调整优先级寄存器实现 QoS。
10.4 DMA coherent 与 DMA streaming 的选择
- 对于 surround 音频双缓冲(2 * period_len),使用
dma_alloc_coherent()分配环形 buffer,避免 SGI 同步开销 - 对于从用户态拷贝的 playback/capture buffer,必须使用
dma_map_sg()并设置DMA_ATTR_SKIP_CPU_SYNC以减轻 cache flush 开销
十一、调试与可观测性
dmaengine 提供 sysfs 接口:/sys/kernel/debug/dmaengine/summary。但往往不够用。实战中建议在驱动里加 tracepoint:
#include <trace/events/dmaengine.h>
static void my_dma_start_transfer(struct my_dma_chan *mchan)
{
/* ... 硬件操作 ... */
trace_dmaengine_start(mchan->vc.chan.device->dev,
to_virt_dma_desc(mchan->current));
}
在驱动端 ftrace 可以追踪 dmaengine_prep_* / dmaengine_submit / dma_async_issue_pending 的完整时序,定位客户端侧中断延迟和 descriptor 堆积问题。
十二、总结
Linux dmaengine 驱动开发是连接硬件设计和系统软件的桥梁。核心套路是:
- 声明能力 →
dma_cap_set+src/dst_addr_widths+directions - 实现回调 → prep_slave_sg / prep_dma_cyclic / prep_interleaved / config / terminate / issue_pending
- 管理 virt_dma 框架 → 用
vchan_tx_prep、vchan_cookie_complete、vchan_get_all_descriptors与 core 交互 - 中断通知 →
vchan_cyclic_callback处理循环,vchan_cookie_complete处理单次 - 流式控制 → pause/resume 保证 FIFO 一致性,terminate_all 保证无野指针
- DT 绑定 →
dma-requests/dma-channels/dma-geqable兼容框架约定
dmaengine 框架在 6.x 持续演进中,引入了 DMA_ASYNC_TX_KMEM_CACHE 标志以加速 descriptor 分配,以及对 async device 注册的推荐。掌握这套 API,你就能在 SoC 驱动开发中将 DMA 这个最古老的计算机概念发挥到极致。

发表评论 取消回复