Linux驱动实现DMA支持

DMA允许设备在没有CPU干预的情况下访问主系统内存,使CPU能够专注于其他任务。负责管理DMA事务的外围设备是DMA控制器,它存在于大多数现代处理器和微控制器中。

DMA的工作方式如下:当驱动程序需要传输数据块时,便使用源地址、目标地址和要复制的总字节数设置DMA控制器,然后DMA控制器自动将数据块从源地址传输到目标地址,且不会占用CPU周期,当剩余字节数为0的时候,数据块传输结束并通知驱动程序。

DMA并不总是意味着复制速度会更快,DMA只是一个真正的后台操作,使得CPU可以干其他事情。其次,DMA操作期间通过保持CPU缓存/预取器状态来带来性能上的提升。

设置DMA映射

对于任何类型的DMA传输都需要提供源地址和目标地址,以及要传输的字数。在外设DMA的情况下,此外设的FIFO充当源头或目标,具体取决于传输方向。当外设充当源头时,目标地址是内存位置(内部或外部)​;当外设充当目标时,源地址是内存位置(内部或外部)​。换句话说,DMA传输需要适当的内存映射。

缓存一致性和DMA的概念

在配备缓存的CPU上面,最近访问的内存区域的副本被缓存,甚至为DMA映射的内存区域也会被缓存。现实情况是两个独立设备之间的共享内存通常是产生缓存一致性问题的根源,缓存不一致源于其他设备可能不知道另一个设备的更新写入。另外,缓存一致性确保每个写操作似乎是即时发生的,这意味着共享同一内存区域的所有设备将看到完全相同的更改序列。

假设一个CPU配备了缓存以及一个可以通过DMA直接访问设备的外存,当CPU访问内存位置X时,当前值会存储在缓存中,对X的后续操作将更新X的缓存副本,但不会更新X的外存版本(假设是写回内存),如果在下一次设备试图访问X之前没有将缓存刷新到内存,则设备将收到X的旧值。同样,如果在设备写入新值到内存时,X的缓存副本没有被无效化,CPU将操作X的旧值。

这个问题有两种解决方案。

· 硬件解决方案。这些系统是一致性系统。

· 软件解决方案,其中操作系统负责确保缓存一致性。这些系统是非一致性系统。

DMA的内存映射

为DMA目的分配的内存缓冲区必须相应地被映射。DMA映射包括为DMA分配内存缓冲区,并为此缓冲区生成总线地址。为DMA分配内存缓冲区不是简单用kmalloc分配普通内存,而是分配满足 DMA 控制器苛刻要求的内存 ------ 普通内存可能存在 "物理地址离散、缓存干扰、DMA 不可访问" 等问题,必须专门分配。DMA 控制器是独立于 CPU 的硬件,它访问内存的规则和 CPU 完全不同:

多数 DMA 控制器只认物理连续的地址(无法像 CPU 一样通过 MMU 访问离散物理页);

DMA 访问内存时绕开 CPU 缓存(直接读物理内存),缓冲区需禁用缓存或保证缓存一致性;

部分架构中,DMA 只能访问特定范围的物理地址(比如 ARM 的低端内存区)。

总线地址≠CPU物理地址,DMA控制器访问内存时用的地址由总线桥(如PCIE,AXI)转换而来。

CPU 通过「内存总线」直接访问内存,用的是 "物理地址";

DMA 控制器通常挂在「PCIe/AXI/AMBA 总线」上,通过「总线桥」中转访问内存 ------ 总线桥会对地址做 "重映射"(比如 CPU 物理地址 0x40000000,经过 PCIe 桥后,DMA 看到的总线地址是 0x80000000)。

一致性DMA映射

内核分配一块永远同步的内存,CPU 直接写 "公共桌子"(禁用缓存 / 硬件自动同步草稿纸和桌子),DMA(快递员)看到的永远是最新数据,无需手动操作。

通俗比喻

办公的人被要求 "不准用草稿纸",所有字直接写在公共桌子上 ------ 快递员来取 / 放东西时,看到的就是最新内容,双方永远同步。

流DMA映射

内核临时分配一块内存,不自动同步缓存:CPU 写 "草稿纸(缓存)" 后,必须手动把草稿纸内容 "抄到" 公共桌子上(刷缓存),DMA 才能看到;DMA 写完桌子后,CPU 必须先 "扔掉旧草稿纸"(缓存失效),才能看到 DMA 写的新数据。

通俗比喻

办公的人可以用草稿纸,但快递员来之前,必须把草稿纸上的内容「抄到公共桌子上」(刷缓存);快递员走后,办公的人要「撕掉旧草稿纸」(缓存失效),重新从桌子上抄新内容(避免看旧草稿纸)。

DMA 控制器 / 系统总线的「地址映射表项」是全局有限资源(比如 PCIe 总线的 DMA 地址转换表、ARM 的 IOMMU 页表项):

流映射(如dma_map_single)会占用一个表项,把内存的物理地址转换成 DMA 可识别的总线地址;

这些表项数量极少(比如几百个),如果传输完成后不解映射,表项会被永久占用,最终导致其他设备 / 驱动无法做 DMA 映射(资源泄漏);

解映射(dma_unmap_single)的核心作用之一就是释放这个表项,归还给系统供其他场景使用。

流 DMA 映射的设计初衷是「精准适配单次传输」:

比如 "硬盘读取一个文件到内存",传输前映射→传输中保证一致性→传输完成后解映射;

内存的 "DMA 专用状态" 只需要维持到传输结束,后续内存可能被用于其他场景(比如用户态读写、其他进程使用);

不解映射会让内存一直被标记为 "DMA 在用",既浪费资源,也可能导致其他操作访问出错(比如其他进程写这块内存时,DMA 突然又去读)。

一致性 DMA 映射(dma_alloc_coherent)会:

直接分配一块 "DMA 专用内存",并永久占用对应的总线地址表项(内核标记为 "长期使用");

这些内存从分配起就属于设备(比如网卡的收包缓冲区),直到驱动卸载前都不会被其他场景使用;

频繁解映射 / 重新映射会重复占用 / 释放表项,增加不必要的开销(比如网卡每秒收上千个包,总不能每次都映射 / 解映射)。

一致性 DMA 映射的内存,缓存属性会被永久设置为 "缓存一致"(要么禁用缓存,要么硬件自动同步缓存):

比如 ARM 架构下,一致性内存会被标记为 "设备内存(Device-nGnRE)",全程保持这个属性;

如果中途解映射,会恢复默认缓存属性,导致后续 DMA 传输时缓存不一致(比如 DMA 读物理内存,CPU 写缓存,数据不同步);

只有当驱动卸载、内存被释放(dma_free_coherent)时,缓存属性才会被清理 ------ 这是 "一次性清理",而非中途解映射。

流 DMA 的 "解映射"≠"释放内存":解映射只是解除 DMA 属性,内存还在,后续可以用kfree释放;一致性 DMA 是 "释放内存时自动解映射",无需单独调用解映射函数。

流 DMA 不解映射的后果:地址表项泄漏→系统 DMA 功能瘫痪;内存缓存属性异常→CPU 访问性能暴跌;

一致性 DMA 中途解映射的后果:缓存一致性被破坏→DMA 和 CPU 数据不同步;设备传输出错→丢包 / 数据损坏。

处理DMA映射的主要头文件如下:

了解DMA映射期间执行的操作

(1)假设设备支持DMA,如果驱动程序使用kmalloc()设置缓冲区,则得到一个虚拟地址(称为X)​,该地址尚未指向任何地方。

(2)虚拟内存系统(借助MMU)将X映射到系统RAM中的物理地址(称为Y)​,假设仍有可用的空闲内存。因为DMA不通过虚拟内存系统流动,驱动程序此时可以使用虚拟地址X访问缓冲区,但设备本身无法访问。

(3)在一些简单的系统(没有I/O MMU的系统)中,设备可以直接对物理地址Y进行DMA。但在许多其他系统中,设备可以通过I/O MMU看到主存。因此,I/OMMU硬件可以将DMA地址转换为物理地址。(4)映射相关API介入的地方如下。

· 驱动程序可以将虚拟地址X传递给dma_map_single()函数​,该函数设置任何适当的I/O MMU映射并返回DMA地址Z。

· 驱动程序指示设备对地址Z进行DMA。

· I/O MMU最终将它映射到系统RAM中物理地址Y的缓冲区中。

场景 1:无 I/O MMU 的简单系统(比如低端 ARM 嵌入式)

  • 驱动调用kmalloc(4096, GFP_KERNEL),得到CPU 虚拟地址 X(比如 0xffffff00);
  • CPU 的 MMU 将 X 映射到系统 RAM 的物理地址 Y(比如 0x40000000)------ 此时 CPU 能通过 X 访问内存,但 DMA 不认虚拟地址 X;
  • 驱动调用dma_map_single(dev, X, 4096, DMA_TO_DEVICE):
    内核做两件事:① 刷 CPU 缓存(确保 X 对应的数据同步到物理内存 Y);② 计算总线地址(DMA 地址 Z)= 物理地址 Y(无 I/O MMU 时,总线地址 = 物理地址);
  • 函数返回 Z(即 Y,0x40000000);
  • 驱动把 DMA 地址 Z 写入设备的 DMA 寄存器,指示设备 "对 Z 做 DMA 传输";
  • 设备(DMA 控制器)直接访问总线地址 Z(= 物理地址 Y),和系统 RAM 完成数据传输(无地址转换,直接访问);
  • 传输完成后,驱动调用dma_unmap_single(dev, Z, 4096, DMA_TO_DEVICE):恢复缓存属性,释放映射资源。
    场景 2:有 I/O MMU 的复杂系统(比如 x86 服务器、高端 ARM64)
  • 驱动调用kmalloc(4096, GFP_KERNEL),得到CPU 虚拟地址 X(比如 0xffffff00);
  • CPU 的 MMU 将 X 映射到系统 RAM 的物理地址 Y(比如 0x40000000)------CPU 能访问 X,但 DMA 不认;
  • 驱动调用dma_map_single(dev, X, 4096, DMA_TO_DEVICE):
    内核做三件事:① 刷 CPU 缓存(同步数据到 Y);② 向 I/O MMU 的页表中添加 "设备虚拟地址 Z → 物理地址 Y" 的映射项;③ 返回DMA 地址 Z(设备虚拟地址,比如 0x90000000);
    注意:此时 Z≠Y,Z 是 I/O MMU 给设备分配的 "虚拟地址";
  • 驱动把 DMA 地址 Z 写入设备的 DMA 寄存器,指示设备 "对 Z 做 DMA 传输";
  • 设备(DMA 控制器)发出访问 "DMA 地址 Z(0x90000000)" 的请求;
  • I/O MMU 拦截该请求,查自身页表,将 Z(0x90000000)转换成物理地址 Y(0x40000000);
  • 最终设备通过 I/O MMU 的转换,访问到系统 RAM 的物理地址 Y,完成 DMA 传输;
  • 传输完成后,驱动调用dma_unmap_single:删除 I/O MMU 的页表项,恢复缓存属性。

创建一致性DMA映射

用于长时间,双向的I/O缓冲区。

这个函数负责缓冲区的分配和映射,并返回缓冲区的内核虚拟地址,缓冲区大小为size字节,可由CPU访问。size参数可能让人产生误解,因为get_order()首先使用size来获取与之对应的页幂次order。因此,该映射至少是按页计算的,页数为2的幂。dev是设备结构。dma_handle是指向相关总线地址的输出参数。为映射分配的内存在物理上必须是连续的,flag决定了应该如何分配内存,通常为GFP_KERNEL或GFP_ATOMIC(在原子上下文中)​。

请注意,一致性DMA映射具有以下特点。

· 一致(协同)性,因为缓冲区内容在所有子系统(设备或CPU)中始终相同。

· 同步性,因为设备或CPU的写入可以立即读取,无须担心缓存一致性问题。

要释放映射,可以使用以下函数:

其中,cpu_addr和dma_handle分别对应于dma_alloc_coherent()返回的内核虚拟地址和总线地址。这两个参数是MMU(返回虚拟地址)和I/O MMU(返回总线地址)释放映射所必需的。

创建流DMA映射

流DMA映射本身不分配内存,它只做DMA"属性加持",必须先有一个已经动态分配好的缓冲区,比如说kmalloc/vmalloc/用户空间缓冲区,才能对他做流映射。

流DMA映射内存缓冲区通常在传输之前进行映射,传输之后取消映射。这种映射具有更多的约束条件,并且与一致性DMA映射不同,原因如下。

· 映射需要与之前动态分配的缓冲区一起使用。

· 映射可能接收多个分散的非连续缓冲区。

流映射内核提供dma_map_sg接口,能接受一个"分散-聚集表",里面包含多个非连续的缓冲区。

① 内核会遍历 sg_table,把每个非连续的物理页,通过 IOMMU / 总线桥映射成连续的总线地址;

② DMA 控制器按 "连续的总线地址" 传输数据,内核自动把数据拆 / 合到多个非连续的物理缓冲区里;

③ 对 DMA 来说,看起来是 "连续传输",但实际内存是零散的,不用 CPU 先把数据拷贝到连续缓冲区(省 CPU 资源)。

· 对于读取事务(从设备到CPU)​,缓冲区属于设备,不属于CPU。在CPU可以使用缓冲区之前,首先应该取消对缓冲区的映射(在调用dma_unmap_{single,sg}()之后)​,或者在这些缓冲区上调用dma_sync_{single,sg}_for_cpu()。这样做的主要原因就是为了缓存。(数据从设备(比如SD卡)->DMA->内存,但CPU的缓存可能留着这块内存的旧数据,必须先取消缓存)

· 对于写入事务(从CPU到设备)​,驱动程序应在建立映射之前将数据放入缓冲区。(数据从CPU->内存->DMA->设备, CPU先把数据写到缓存,如果先映射再写数据,映射时内核会刷一次缓存,所以必须先写数据再映射)

· 必须指定传输方向,并且数据应该根据传输方向移动和使用。

流DMA映射有以下两种形式。

· 单缓冲区映射,允许对一个物理上连续的缓冲区进行映射。

· 分散/聚集映射,允许传递多个缓冲区(分散在内存中)​。

"DMA 属性" 核心包含 3 类配置:

缓存属性:决定内存是否禁用缓存、是否需要刷缓存(流映射的核心);

地址映射属性:把内存的物理地址转成 DMA 能认的总线地址(dma_handle);

传输方向属性:指定数据是 "CPU→设备" 还是 "设备→CPU"(流映射必须指定,决定缓存怎么刷)。

对于这两种映射,传输方向应由dma_data_direction类型的枚举符号指定,该类型定义在include/linux/dma-direction.h中,如下所示:

一致性DMA映射隐含着一个针对DMA_BIDIRECTIONAL的方向属性设置。

单缓冲区映射(允许对一个物理上连续的缓冲区进行映射)

单缓冲区映射是一种偶尔才使用的流DMA映射,可以使用dma_map_single()函数设置此类映射

当CPU充当源头(向设备写入数据)或目标(从设备读取数据)时,以及当映射的访问是双向(在一致性DMA映射中隐式使用)时,direction应分别为DMA_TO_DEVICE、DMA_FROM_DEVICE或DMA_BIDIRECTIONAL。dev是硬件设备的底层结构。ptr是输出参数,也是缓冲区的内核虚拟地址。该函数返回dma_addr_t类型的成员,它是由I/O MMU(如果存在的话)返回给设备的总线地址,以便设备可以进行DMA。你应该使用dma_mapping_error()(如果没有错误发生,则必须返回0)来检查映射是否返回有效地址,如果出现错误,请不要继续进行。

可以通过以下函数释放此类映射:

分散/聚集映射(允许传递多个缓冲区------分散在内存中)

分散/聚集映射是一种特殊类型的流DMA映射,它可以在一次操作中传输多个缓冲区,而不是单独映射每个缓冲区并逐个传输它们。假设有多个缓冲区,它们在物理上或许不是连续的,所有这些缓冲区都需要同时被传输到设备或从设备传出。此种情形可能由以下原因引起。

· readv()或writev()系统调用。(用户态程序要一次读写多个不连续的缓冲区)

· 磁盘I/O请求。(页缓存往往是分散的)

· 页的链表或vmalloc分配器分配的内存区域。

使用这样的映射之前必须建立分散的数组元素,每个元素描述一个单独缓冲区的映射情况。上述分散元素在内核中被抽象为scatterlist结构体实例,该结构体定义如下:

为了建立分散列表的映射,需要如下操作:

  • 分配分散的缓冲区
  • 创建分散数据,使用sg_init_table()初始化数组,并使用sg_set_buf()分配内存填充该数据,每个分散元素必须是页大小(结尾除外)
  • 在分散数组上调用dma_map_sg()
  • 一旦完成DMA,就调用dma_unmap_sg()取消对分散数组的映射

虽然可以对多个缓冲区的内容分别进行DMA,但分散/聚集方式可以通过将分散数组的地址和长度(即分散数组中的元素数量)一起发送给设备,一次性地对整个分散列表进行DMA。

C 复制代码
u32 *wbuf, *wbuf2, *wbuf3;
wbuf = kzalloc(SDMA_BUF_SIZE, GFP_DMA);
wbuf2 = kzalloc(SDMA_BUF_SIZE, GFP_DMA);
wbuf3 = kzalloc(SDMA_BUF_SIZE / 2, GFP_DMA);
struct scatterlist sg[3];
sg_init_table(sg, 3);
sg_set_buf(&sg[0], wbuf, SDMA_BUF_SIZE);
sg_set_buf(&sg[1], wbuf2, SDMA_BUF_SIZE);
sg_set_buf(&sg[2], wbuf3, SDMA_BUF_SIZE / 2);
ret = dma_map_sg(dev, sg, 3, DMA_TO_DEVICE);
if (ret != 3) {

}

要取消映射列表,就必须使用dma_unmap_sg_ attrs()函数,该函数定义如下:

其中,dev指向一个设备,该设备已用于映射;sg是要取消的映射列表(实际上是指向映射列表中第一个元素的指针)​;dir是DMA方向,它应该与映射方向对应;nents是分散数组中的元素数量。下面是解除映射的一个例子:

C 复制代码
dma_unmap_sg(dev, sg, 3, DMA_TO_DEVICE);

流DMA映射的隐式和显式缓存一致性

在流DMA映射中,dma_map_single()/dma_unmap_single()和dma_map_sg()/dma_unmap_sg()函数组合用于处理缓存一致性问题。对CPU到设备的情况(DMA_TO_DEVICE),由于在建立映射之前数据必须放在缓冲区中,因此dma_map_sg()/dma_map_single()将处理缓存一致性问题。对于设备到CPU的情况(设置了DMA_FROM_DEVICE方向标志)​,在CPU能够访问缓冲区之前,必须首先释放映射关系,这是因为dma_unmap_single()/dma_unmap_sg()会隐式地处理缓存一致性问题。

然而,如果需要多次使用相同的流式DMA区域,并且在DMA传输之间处理数据,则缓冲区必须正确地同步,以便设备和CPU看到DMA缓冲区的最新且正确的副本。为了避免缓存一致性问题,驱动程序必须在开始从RAM到设备的DMA传输之前调用dma_sync_{single,sg}for_device()(在将数据放入缓冲区之后,在实际将缓冲区交给硬件之前)​。如有必要,这个函数调用将刷新DMA缓冲区所对应的硬件缓存行。类似地,驱动程序不应该在完成从设备到RAM的DMA传输后立即访问内存缓冲区;相反,在读取缓冲区之前,驱动程序应该调用dma_sync{single,sg}_for_cpu(),这将在必要时使相关的硬件缓存行无效。

完成completion概念

在内核编程中,我们经常在当前线程之外启动一个活动,然后等待该活动完成。完成变量是使用等待队列实现的,他们与等待队列的唯一区别就是无须维护等待队列。

使用完成变量之前需要包含如下头文件:

完成变量在内核中表示为completion结构体实例,该结构体可以静态初始化如下:

动态分配时的初始化方式如下:

当驱动程序启动工作(比如DMA事务)必须等待完成时,只需要将完成事件传递给wait_for_completion()函数即可

当完成事件发生后,驱动程序可以使用以下API之一唤醒等待者:

complete()只会唤醒一个等待任务,而complete_all()会唤醒等待该事件完成的所有任务。completion是内核专为单次事件等待设计的同步机制,他的核心是记录"完成事件是否已经发生",哪怕先调用complete()标记事件完成,再掉用wait_for_completion()等待事件完成,等待的线程也不会阻塞,会直接跳过等待逻辑继续执行。而普通等待队列如果先调用wake_up()再调用wait_event(),线程会无限睡眠(因为没抓到唤醒信号)。

DMA引擎API

DMA引擎用于开发DMA控制器驱动程序,是一个通用的内核框架,这个框架使得客户驱动程序即从设备能够请求并使用DMA通道从控制器发起DMA传输。

DMA引擎框架是Linux内核为统一管理所有DMA控制器(包括Soc全局DMA,从设备DMA引擎)而设计的标准化软件层。

头文件如下:

(1)通知内核关于设备的DMA寻址能力。

struct dma_device 代表一个具体的dma控制器,框架通过它记录DMA的硬件特征(寻址范围,支持的传输类型,通道数),外设驱动通过它告知内核该DMA访问哪些地址

(2)向一个DMA通道发出请求。

struct dma_chan 代表DMA控制器的物理通道,外设驱动调用dma_request_channel()向框架申请通道,框架负责分配空闲通道

(3)如果成功,配置这个DMA通道。

struct dma_set_* 框架提供统一API(比如设置传输方向、地址对齐、DMA模式),底层自动转换成对应DMA控制器的寄存器配置

(4)准备或配置DMA传输。这一步将返回表示DMA传输的传输描述符。

struct dma_async_tx_descriptor 框架返回的传输任务描述符,封装了本次传输所有的信息,SG表,传输长度,方向,回调函数

(5)使用传输描述符提交DMA传输。然后将DMA传输添加到与指定通道相对应的控制器挂起队列中。这一步将返回一个特殊的cookie,你可以用它来检查DMA活动的进展情况。

dma_cookie_t 提交描述符后框架返回的"任务ID",外设驱动可以用这个cookie查询传输状态,取消传输,提交后框架会把描述符加入DMA控制器的挂起队列

(6)在指定的通道上开始DMA传输,以便在该通道空闲时启动控制器挂起队列中的第一个DMA传输。

dma_async_issue_pending() 框架调用该接口后,DMA控制器会检查通道是否空闲,若空闲立即执行挂起队列中的第一个传输任务,若忙则等待当前传输完成后自动执行

DMA控制器接口

控制器执行内存传输(无须CPU干预),通道则是客户端驱动程序(支持DMA的驱动程序)向控制器提交作业的方式。

DMA控制器被抽象为struct dma_device结构体实例。

DMA控制器在Linux内核中被抽象为dma_device结构体实例。就其本身而言,控制器在没有客户端的情况下是没有用的,这是因为客户端会使用控制器所暴露的通道;而且控制器驱动程序必须暴露回调函数给通道,用于通道配置。dma_device结构体定义如下:

chancnt:指定控制器支持多少个DMA通道

channels:struct dma_chan数据结构链表,对应于控制器所暴露的DMA通道。

privatecnt:表示dma_request_channel()函数请求了多少个DMA通道,该函数是用于请求DMA通道的DMA引擎API

cap_mask:一个或多个dma_capability标志,代表控制器能力,他的取值如下:


DMA_XFER_UNSPEC 未指定 / 未知的传输类型(默认 / 占位值) 用于初始化、兼容性适配,驱动实际使用时需替换为具体类型(如 DMA_SLAVE);

DMA_MEMCPY 内存→内存的纯内存 DMA 传输(无外设参与) SoC 全局 DMA 控制器的内存缓冲区高速拷贝(如把 wbuf 数据拷贝到 wbuf2);

DMA_SLAVE 从设备(外设)↔ 系统内存的 DMA 传输(最核心!) 你代码中的 SDMA 传输(SD 卡 ↔ 内存)、UART/SPI 外设 ↔ 内存传输、网卡 ↔ 内存数据包传输;是从设备 DMA 引擎的标配类型,支持单向(读 / 写)或双向传输;

DMA_CYCLIC 循环 DMA 传输(环形缓冲区↔外设) 音频设备(I2S)持续音频流传输、ADC/DAC 连续采样 / 输出、CAN 总线持续数据收发;

DMA_INTERLEAVE 交错 DMA 传输(多组数据按规则交织) 视频设备(YUV 数据交错传输)、多通道 ADC 采样数据交错接收、多媒体外设的交织数据流;

DMA_PRIVATE DMA 控制器私有传输(不对外暴露) DMA 控制器自身硬件初始化 / 配置,驱动层几乎不用;

DMA_ASYNC_TX 异步事务 DMA 传输(结合 async_tx 框架) 带硬件加速的场景(如网卡数据校验和计算、加密芯片的 DMA 加解密);

DMA_INTERLEAVE 多组数据按硬件级规则交错传输(地址 / 长度 / 通道交错),而非连续传输

DMA_XOR DMA 控制器硬件加速执行 XOR(异或)计算,边传输数据边完成校验(替代 CPU 计算)(异或)计算,边传输数据边完成校验(替代 CPU 计算)

DMA_PQ DMA 硬件加速执行 PQ(Reed-Solomon 奇偶校验)计算(RAID 6 核心算法),边传输边校验

DMA_XOR_VAL DMA 硬件基于预设 XOR 校验值,验证传输数据的完整性(对比计算结果与预设值)

DMA_PQ_VAL DMA 硬件验证 PQ 校验值是否匹配(RAID 6 数据读取完整性校验)

DMA_MEMSET_SG DMA 硬件批量填充分散的 SG 表缓冲区为指定值(类似 memset,但由 DMA 完成)

DMA_INTERRUPT 【非传输类型】是 DMA 传输描述符的属性标记(而非 dma_transaction_type 成员),表示 "传输完成后触发硬件中断"

例如,它在i.MX DMA控制器驱动程序中的设置如下:

src_addr_widths:设备支持的源地址宽度的位掩码。这个宽度必须以字节为单位提供。例如,如果设备支持的宽度为4,那么位掩码应该设置为BIT(4)。

dst_addr_widths:设备支持的目的地址宽度的位掩码。

directions:设备支持的从属方向的位掩码。因为enum dma_transfer_direction不包括每种类型的位标志,所以DMA控制器应该设置BIT(),并且同样应该由控制器进行检查。

enum dma_transfer_direction(如DMA_TO_DEVICE/DMA_FROM_DEVICE)本身是枚举值(比如 0、1、2),而非位标志,因此内核要求用BIT(<枚举值>)转成位掩码 ------ 比如支持 "设备到内存" 和 "内存到设备",则directions = BIT(DMA_FROM_DEVICE) | BIT(DMA_TO_DEVICE),本质还是利用位掩码的上述优势,统一校验逻辑。

它在i.MX SDMA控制器驱动程序中的设置如下:

device_alloc_chan_resources:一个通用的回调函数,用于分配资源并返回分配的描述符的数量。当在这个控制器上请求一个通道时,该函数将由DMA引擎核心调用。

device_free_chan_resources:一个控制器回调函数,用于释放DMA通道的资源。该函数依赖于控制器的能力,如果在cap_mask中设置了能力位掩码,就必须提供相关的能力信息。

device_prep_dma_memcpy:准备执行memcpy操作。如果在cap_mask中设置了DMA_MEMCPY,则必须设置这个字段。对于每个设置的标志,必须提供相应的回调函数,否则控制器注册将失败。这适用于所有的device_prep_*回调函数。

device_prep_dma_xor:准备执行XOR操作。

device_prep_dma_xor_val:准备执行XOR验证操作。

device_prep_dma_memset:准备执行memset操作。

device_prep_dma_memset_sg:准备对分散列表执行memset操作

device_prep_dma_interrupt:准备执行链末中断操作。

device_prep_slave_sg:构建 "从设备(SD 卡)↔ 内存" 的 SG 表传输描述符。

device_prep_dma_cyclic:准备执行循环的DMA操作。这样的DMA操作在音频或UART驱动程序中经常使用。该函数需要一个大小为buf_len的缓冲区。在传输完period_len字节后,该函数将被调用。

device_prep_interleaved_dma:构建 "交错传输" 描述符(多媒体多通道交织)。

device_config:向通道推送新的配置。如果推送成功,返回0,否则返回错误码。

device_pause:暂停通道上当前的任何传输。如果暂停有效,返回0,否则返回错误码。

device_resume:恢复通道上先前暂停的任何传输。如果恢复有效,返回0,否则返回错误码。

device_terminate_all:终止一个通道上的所有传输。如果成功,返回0,否则返回错误码。

device_synchronize:数据传输终止后,将这一情况通知当前上下文。

device_tx_status:对传输完成情况进行投票。可选的txstate参数可以用来获得一个结构体,该结构体包含辅助传输状态的信息;否则,该调用将只返回简单的状态码。

device_issue_pending:一个强制性的回调函数,用于将待处理事务推送到硬件。它是dma_async_issue_ pending() API的后端函数。

虽然大多数驱动程序通过dma_chan->dma_dev->device_prep_dma_直接调用这些回调函数,但你应该使用DMA引擎API dmaengine_prep_,这些API在调用适当的回调函数之前,还会进行一些合理性检查。例如,对于内存到内存复制,驱动程序应该使用device_prep_dma_memcpy()封装函数。

DMA通道数据结构

DMA通道是客户端驱动程序向DMA控制器提交DMA事务(I/O数据传输)的方式。它的工作原理如下:具有DMA能力的驱动程序(客户端驱动程序)请求一个(或多个)通道,然后重新配置这个(或这些)通道,并要求控制器使用这个(或这些)通道来完成所提交的DMA传输。通道的定义如下:

C 复制代码
struct dma_chan {
	struct dma_device *device;
	struct device *slave;
	dma_cookie_t cookie;
	dma_cookie_t completed_cookie;
	[...]
};

你可以把DMA通道看作I/O数据传输的高速公路。以上数据结构中每个字段的含义如下。

· device:这是一个指向提供该通道的DMA设备(控制器)的指针。如果通道申请成功,则这个字段永远不能为NULL,因为一个通道总是属于一个控制器。

· slave:这是一个指向使用该通道的设备的底层struct device数据结构的指针(该设备的驱动程序是一个客户端驱动程序)​。

· cookie:该通道最后返回给客户端的cookie值。

· completed_cookie:该通道最后完成的cookie值。struct dma_chan数据结构的完整定义可以在include/linux/dmaengine.h中找到。
在DMA引擎框架中,cookie只不过是一个DMA事务标识符,用于检查它所标识的事务的状态和进展情况。

cookie = 你在快递站(DMA 通道)寄的 "最后一个快递的单号"(唯一、递增);

completed_cookie = 快递站看板上写的 "最后一个派送完成的快递单号";

通过对比这两个单号,就能知道 "最新寄的快递有没有送完",也能知道整个通道的传输进度。

DMA事务描述符的数据结构

事务描述符仅用于表征和描述DMA事务(或者说DMA传输)​,而不会执行其他操作。事务描述符在内核中使用struct dma_async_tx_descriptor数据结构来表示,该数据结构定义如下:

以上数据结构中各个字段的含义如下。

· cookie:当前事务的跟踪cookie,用于检查事务的进展情况。

· chan:此操作的目标通道。

· callback:一旦此操作完成,就应该被调用一个函数。

· callback_param:作为回调函数的一个参数给出。

该数据结构的完整定义可以在include/linux/dmaengine.h中找到。

处理DMA设备寻址能力

有些设备可能只支持低24位的寻址。这种限制缘于ISA总线,ISA总线是24位宽的,而DMA缓冲区只能存在于系统内存的16MB低地址中。尽管如此,我们仍然可以使用DMA掩码的概念来告知内核这种限制,目的是让内核知道设备的DMA寻址能力。这可以通过dma_set_mask_and_coherent()函数来实现,该函数的原型如下:

鉴于DMA API保证了相干DMA掩码可以设置为与流式DMA掩码相同或更小,上面的函数将为流DMA映射和一致性DMA映射设置相同的掩码。

然而,对于特殊需求,可以使用dma_set_mask()或dma_set_ coherent_mask()函数来设置相应的掩码。这两个函数的原型如下:

其中,dev是底层设备结构;mask是位掩码,用于描述设备支持哪些位的地址,可以使用DMA_BIT_MASK宏指定要使用的位掩码及其实际的位顺序。

dma_set_mask()和dma_set_coherent_mask()函数都返回0,表示设备可以在给定地址掩码的机器上正常进行DMA。任何其他的返回值都是错误,意味着给定的掩码太小,系统不支持。在这种失败的情况下,可以选择回退到非DMA模式来进行驱动程序中的数据传输;或者,如果DMA支持是强制性的,则简单地禁用设备中要求DMA支持的功能,甚至不对设备进行探测。

建议让你的驱动程序在设置DMA掩码失败时打印一条内核警告[dev_warn()或pr_warn()]消息。下面是一个例子:

C 复制代码
#define PLAYBACK_ADDRESS_BITS DMA_BIT_MASK(32)
#define RECORD_ADDRESS_BITS	DMA_BIT_MASK(24)
struct my_sound_card *card;
struct device *dev;
[...]
if (!dma_set_mask(dev, PLAYBACK_ADDRESS_BITS)) {
	card->playback_enabled = 1;
} else {
	card->playback_enabled = 0;
	dev_warn(dev, "%s: Playback disabled due to DMA limitations\n", card->name);
}

 if (!dma_set_mask(dev, RECORD_ADDRESS_BITS)) {
	card->record_enabled = 1;
} else {
	card->record_enabled = 0;
	dev_warn(dev, "%s: Record disabled due to DMA limitations\n", card->name);
}

在上面的例子中,我们使用DMA_BIT_MASK宏来定义DMA掩码。然后在所需的DMA掩码不被支持的情况下,我们禁用了强制要求DMA支持的功能。在以上任何一种情况下,都会打印一条警告消息。

请求DMA通道

dma_request_channel()函数用于请求一个通道。该函数的原型如下:

其中,mask必须是一个位掩码,这个位掩码代表通道必须满足的能力。它本质上用来指定驱动程序需要执行的传输类型,该传输类型必须在dma_device.cap_mask中得到支持。

dma_cap_zero()和dma_cap_set()函数用于清除掩码和设置我们所需要的能力,例如:

C 复制代码
#include <linux/dmaengine.h>

dma_cap_mask_t mask;

dma_cap_zero(&mask);

dma_cap_set(DMA_SLAVE, &mask);

struct dma_chan *chan = dma_request_channel(&mask, NULL, NULL);
if (!chan) {
	dev_err(dev, "申请通道失败\n");
	return -ENODEV;
}

其中fn是一个回调函数指针,定义如下:

实际上,dma_request_channel()函数会遍历系统中可用的DMA控制器,并为每个DMA控制器寻找一个与请求相对应的通道。如果dma_filter_fn函数(可选)为NULL,dma_request_channel()函数将简单地返回第一个满足能力掩码的通道;否则,当掩码不足以指定所需的通道时,可以将dma_filter_fn函数作为一个过滤器,系统中的每个可用通道都会被传递给这个过滤器。内核会为系统中的每个空闲通道调用一次dma_filter_fn函数。当发现合适的通道时,dma_filter_fn函数应该返回DMA_ACK,这将标记给定的通道为dma_request_channel()函数的返回值。

复制代码
驱动申请通道 → 遍历系统中所有可用DMA控制器 → 遍历控制器内的空闲通道 → 
  第一步:初筛(能力掩码匹配)→ 符合的通道进入候选 →
  第二步:精筛(可选,过滤器函数)→ 候选通道逐个传给过滤器 →
    过滤器返回DMA_ACK → 选中该通道,函数返回 →
    过滤器返回DMA_NACK → 跳过,继续找下一个 →
  无符合条件的通道 → 返回NULL(申请失败)

在调用dma_release_channel()函数之前,通过这个函数分配的通道对调用方是专用的。该函数定义如下:

该函数将释放DMA通道,以使其可被其他客户端请求。

系统中可用的DMA通道可以通过ls /sys/class/dma/命令在用户空间中列出

在上面的代码片段中,chan<通道索引>通道名称与它所属的DMA控制器dma<DMA索引>相连接。一个通道是否在使用,可以通过打印相应通道目录下的in_use文件值来看,如下所示:

配置DMA通道

为了使DMA传输在通道上正常工作,必须对这个通道进行客户端特定的配置。DMA引擎框架使用struct dma_slave_config数据结构来进行这种配置,该数据结构表示DMA通道的运行时配置。这样客户端就能指定诸如DMA方向,DMA地址(源地址和目标地址),总线宽度(单次传输的字节)和DMA突发长度等外设的参数。

示例:SDMA 配置src_addr_width=4(数据单元 = 4 字节)、src_maxburst=16(突发长度 = 16)→ 单次突发传输 16×4=64字节;

通过dmaengine_slave_config()函数,将这种配置作用于底层硬件上,该函数定义如下:

其中,chan是要配置的DMA通道,config是要应用的配置。为了更好地微调这个配置,我们必须看一下struct dma_slave_config数据结构,该数据结构定义如下:

· direction表示在这个从设备通道上,数据现在应该是输入还是输出。可能的值如下:

· src_addr是应该从DMA从机读取数据时所在缓冲区的物理地址(实际上是总线地址)​。

· dst_addr是应该写入DMA从机数据时所在缓冲区的物理地址(实际上也是总线地址)​。如果源地址是内存,这个字段会被忽略。

· src_addr_width是应该读取DMA数据的源寄存器的字节宽度。如果源地址是内存,则这个字段可能会根据架构的不同而被忽略。

· dst_addr_width与src_addr_width相似,但用于目标寄存器。

任何总线宽度都必须是以下枚举值之一

在上面的代码片段中,dma_request_channel()用来请求一个DMA通道,然后使用dmaengine_slave_config()对它进行配置。

C 复制代码
struct dma_chan *my_dma_chan;
dma_addr_t dma_src_addr, dma_dst_addr;
struct dma_slave_config channel_cfg = {0};
struct dma_async_tx_descriptor *tx_desc;
dma_cookie_t cookie;

// 1. 申请DMA通道(掩码需包含DMA_MEMCPY)
dma_cap_zero(my_dma_cap_mask);
dma_cap_set(DMA_MEMCPY, my_dma_cap_mask); // 关键:Memcpy需勾选DMA_MEMCPY能力
my_dma_chan = dma_request_channel(my_dma_cap_mask, NULL, NULL);
if (!my_dma_chan) {
    pr_err("申请Memcpy DMA通道失败\n");
    return -ENODEV;
}

// 2. Slave配置(Memcpy传输下可省略,或仅配置通用参数)
// 注:Memcpy传输下,Slave配置的多数字段会被忽略,此处可省略
channel_cfg.direction = DMA_MEM_TO_MEM;
dmaengine_slave_config(my_dma_chan, &channel_cfg);

// 3. 分配内存并获取物理/总线地址(示例)
char *src_buf = dma_alloc_coherent(NULL, 4096, &dma_src_addr, GFP_KERNEL);
char *dst_buf = dma_alloc_coherent(NULL, 4096, &dma_dst_addr, GFP_KERNEL);
if (!src_buf || !dst_buf) { /* 内存分配失败处理 */ }

// 4. 创建Memcpy传输描述符(核心:指定源/目的地址+长度)
tx_desc = dmaengine_prep_dma_memcpy(
    my_dma_chan,        // DMA通道
    dma_dst_addr,       // 目的内存总线地址(任务级参数)
    dma_src_addr,       // 源内存总线地址(任务级参数)
    4096,               // 传输长度
    DMA_PREP_INTERRUPT  // 传输完成触发中断
);
if (!tx_desc) { /* 描述符创建失败处理 */ }

// 5. 提交任务+启动传输
cookie = tx_desc->submit(tx_desc);
dma_async_issue_pending(my_dma_chan);

// 6. (可选)等待传输完成
dma_sync_wait(my_dma_chan, cookie);

在上面的代码片段中,dma_request_channel()用来请求一个DMA通道,然后使用dmaengine_slave_config()对它进行配置。

配置DMA传输

这一步是为了确定DMA传输的方式。要进行一次DMA传输,就需要用到与DMA通道对应的控制器中的一些函数,这些函数名为device_prep_dma_*,它们可以帮助我们设置传输参数。这些函数会给我们返回一个传输描述符,它是dma_async_tx_descriptor结构体实例,我们可以用它来修改传输的细节,之后再把传输交给DMA通道执行。

C 复制代码
struct dma_device *dma_dev = my_dma_chan->device;
struct dma_async_tx_descriptor *tx_desc = NULL;
tx_desc = dma->dev->device_prep_dma_memcpy(my_dma_chan, dma_dst_addr, dma_src_addr, BUFFER_SIZE, 0);
if (!tx_desc) {
	handle_error();
}

在上面的代码片段中,我们解除了对控制器回调函数的调用,而我们本可以先检查它是否存在。出于理智和可移植性的考虑,建议使用DMA引擎APIdmaengine_prep_*,而不是直接调用控制器回调函数。然后采用以下形式分配tx_desc:

C 复制代码
dma_cap_mask_t mask;
struct dma_chan *chan;
dma_addr_t src_phys, dst_phys;
char *src_buf = dma_alloc_coherent(NULL, 4096, &src_phys, GFP_KERNEL);
char *dst_buf = dma_alloc_coherent(NULL, 4096, &dst_phys, GFP_KERNEL);

dma_cap_zero(&mask);
dma_cap_set(DMA_MEMCPY, &mask);
chan = dma_request_channel(&mask, NULL, NULL);

struct dma_async_tx_descriptor *desc = dmaengine_prep_dma_memcpy(chan, dst_phys, src_phys, 4096, DMA_PREP_INTERRUPT);

desc->submit(desc);
dma_async_issue_pending(chan);

此外,客户端驱动程序可以使用dma_async_tx_descriptor结构体(由dmaengine_prep_*函数给出)的callback元素来设置传输完成后就要调用的函数。

提交DMA传输

为了把事务放到驱动程序的事务待处理队列中,可以使用dmaengine_submit()函数,其原型如下:

该函数是控制器的device_issue_pending回调函数的对外接口。该回调函数会返回一个cookie,你可以通过其他DMA引擎,用该cookie来查看DMA活动的进展。为了检查返回的cookie是否有效,你可以使用dma_submit_error()函数。假设还没有提供completion回调函数,则可以在提交DMA传输前设置它,示例如下:

C 复制代码
struct completion transfer_ok;
init_completion(&transfer_ok);

tx_desc->callback = my_dma_callback;
dma_cookie_t cookie = dmaengine_submit(tx);
if (dma_submit_error(cookie)) {
	[...]
}

要向该回调函数传递一个参数,就必须在描述符的callback_param字段中进行设置。例如,它可以是一个设备状态数据结构。

在每次DMA传输完成后,就会产生一个中断(来自DMA控制器)​,之后启动待处理队列中的下一个传输,一个小任务(tasklet)被激活。如果客户端驱动程序提供了tx_desc->callback回调函数,则小任务被调度时会调用该回调函数。因此,回调函数运行在一个中断上下文中。

发出待处理的DMA请求并等待回调通知

启动传输是DMA传输设置的最后一步。可以通过在通道上调用dma_async_issue_pending()来激活通道待处理队列中的传输。如果通道处于空闲状态,则启动待处理队列中的第一个传输,后续的传输将被排队。DMA操作结束后,执行下一个操作并调度一个tasklet。这个tasklet会调用客户端驱动程序的completion回调函数来通知客户端。dma_async_ issue_pending()函数定义如下:

C 复制代码
#include <linux/dmaengine.h>
#include <linux/completion.h>
#include <linux/interrupt.h>

// 1. 定义completion(可放在设备私有结构体中,而非全局)
struct completion transfer_ok;

// 2. DMA回调函数(运行在中断上下文)
static void my_dma_callback(void *param)
{
    struct completion *comp = (struct completion *)param;
    
    // 触发completion事件,唤醒等待的主线程
    // 注意:complete()可在中断上下文安全调用
    complete(comp);
    
    // 可选:处理DMA完成后的其他逻辑(如状态检查、数据校验)
    pr_info("DMA transfer completed\n");
}

// 3. 发起DMA传输的核心逻辑
int start_dma_transfer(struct dma_chan *chan, struct dma_async_tx_descriptor *tx)
{
    dma_cookie_t cookie;
    
    // 初始化completion(每次传输前必须重新初始化!)
    init_completion(&transfer_ok);

    // 关键:将completion指针赋值给回调参数callback_param
    tx->callback = my_dma_callback;
    tx->callback_param = &transfer_ok;  // 传递completion参数

    // 提交DMA传输
    cookie = dmaengine_submit(tx);
    if (dma_submit_error(cookie)) {
        pr_err("DMA submit failed\n");
        return -EIO;
    }

    // 启动DMA传输
    dma_async_issue_pending(chan);

    // 4. 主线程等待DMA完成(阻塞,超时时间可选,如10秒)
    // 注意:wait_for_completion()运行在进程上下文,不可在中断/原子上下文调用
    if (!wait_for_completion_timeout(&transfer_ok, msecs_to_jiffies(10000))) {
        pr_err("DMA transfer timeout\n");
        return -ETIMEDOUT;
    }

    // DMA完成后的后续处理(如释放资源、通知上层)
    pr_info("DMA transfer success, wake up main thread\n");
    return 0;
}

单缓冲区DMA映射

考虑映射一个单一的缓冲区(流DMA映射)​,并把数据从源地址src传输到目标地址dst。我们可以使用字符设备,如此一来,这个字符设备上的任何写操作都会触发DMA,且任何读操作都会比较源地址和目标地址,并检查它们是否匹配。

C 复制代码
#define pr_fmt(fmt) "DMA-TEST: " fmt
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/init.h>
#include <linux/dma-mapping.h>
#include <linux/fs.h>
#include <linux/dmaengine.h>
#include <linux/device.h>
#include <linux/io.h>
#include <linux/delay.h>
#include <linux/completion.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/gfp.h>

#define DMA_BUF_SIZE (2 * PAGE_SIZE)
#define DEVICE_NAME "dma_test"
static u32 *wbuf;
static u32 *rbuf;
static dma_addr_t wbuf_dma; //IOVA
static dma_addr_t rbuf_dma;
static int dma_result;
static int gMajor; /*major number of device*/
static struct class *dma_test_class;
static struct completion dma_m2m_ok;
static struct dma_chan *dma_m2m_chan;

static void dev_release(struct device *dev)
{
	pr_info("releasing dma capable device\n");
}

//我们使用了静态设备,并在设备结构中设置了设备的DMA掩码。如果是平台驱动程序,则可以使用dma_set_mask_and_coherent()来实现这一点。
static struct device dev = {
	.release = dev_release,
	.coherent_dma_mask = ~0,
	.dma_mask = &dev.coherent_dma_mask,
};

int dma_open(struct inode *inode, struct file *filp)
{
	init_completion(&dma_m2m_ok);
	wbuf = kzalloc(DMA_BUF_SIZE, GFP_KERNEL | GFP_DMA);
	if (!wbuf) {
		pr_err("Failed to allocate wbuf\n");
		return -ENOMEM;
	}
	rbuf = kzalloc(DMA_BUF_SIZE, GFP_KERNEL | GFP_DMA);
	if (!rbuf) {
		kfree(wbuf);
		pr_err("Failed to allocate rbuf\n");
		return -ENOMEM;
	}
	return 0;
}

int dma_release(struct inode *inode, struct file *filp)
{
	kfree(wbuf);
	kfree(rbuf);
	return 0;
}

ssize_t dma_read(struct file *filp, char __user *buf, size_t count, loff_t *offset)
{
	pr_info("DMA result:%d\n", dma_result);
	return 0;
}

static void dma_m2m_callback(void *data)
{
	pr_info("in %s\n", __func__);
	complete(&dma_m2m_ok);
}

ssize_t dma_write(struct file *filp, const char __user *buf, size_t count, loff_t *offset)
{
	u32 *index, i;
	size_t err = count;
	dma_cookie_t cookie;
	dma_cap_mask_t dma_m2m_mask;
	dma_addr_t dma_src, dma_dst;
	struct dma_slave_config dma_m2m_config = {0};
	struct dma_async_tx_desriptor *dma_m2m_desc;

	pr_info("Initializing buffer\n");
	index = wbuf;
	for (i = 0; i < DMA_BUF_SIZE / 4; i++) {
		*(index + i) = 0x56565656;
	}
	data_dump("WBUF initialized buffer," (u8 *)wbuf, DMA_BUF_SIZE);
	pr_info("Buffer initialized\n");
	
	//1、初始化DMA控制器并请求一个通道
	dma_cap_zero(dma_m2m_mask);
	dma_cap_set(DMA_MEMCPY, dma_m2m_mask);
	/*在上面的例子中,也可以用dma_m2m_chan=dma_request_chan_by_mask(&dma_ m2m_mask);注册通道。使用这种方法的好处是,只需要在参数中指定掩码即可,驱动程序不需要理会其他参数。*/
	dma_m2m_chan = dma_request_channel(dma_m2m_mask, NULL, NULL);
	if (!dma_m2m_chan) {
		pr_err("Error requesting the DMA channel\n");
		return -EINVAL;
	} else {
		pr_info("Get DMA channel %d\n", dma_m2m_chan->chan_id);
	}
	
	//2、设置从设备控制器特定参数,为原缓冲区和目标缓冲区创建映射
	dma_m2m_config.direction = DMA_MEM_TO_MEM;
	dma_m2m_config.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
	dmaengine_slave_config(dma_m2m_chan, &dma_m2m_config);
	pr_info("DMA channel configured\n");
	//将内核虚拟地址wbuf转化为DMA可识别的总线地址(IOVA)dma_src
	dma_src = dma_map_single(&dev, wbuf, DMA_BUF_SIZE, DMA_TO_DEVICE); //方向标志的本质是告知内核 "CPU 与 DMA 谁是数据的生产者 / 消费者",而非 "是否有物理外设"。
	if (dma_mapping_error(&dev, dma_src)) {
		pr_err("Could not map src buffer\n");
		err = -ENOMEM;
		goto channel_release;
	}
	dma_dst = dma_map_single(&dev, rbuf, DMA_BUF_SIZE, DMA_FROM_DEVICE);
	if (dma_mapping_error(&dev, dma_dst)) {
		dma_unmap_single(&dev, dma_src, DMA_BUF_SIZE, DMA_TO_DEVICE);
		err = -ENOMEM;
		goto channel_release;
	}
	pr_info("DMA mappings created\n");
	
	//为事务获取描述符
	/*调用dmaengine_prep_dma_memcpy()会导致调用dma_m2m_chan->device->device_ prep_dma_memcpy()。然而,建议使用DMA引擎的方法,因为它更具有可移植性。*/
	dma_m2m_desc = dmaengine_prep_dma_memcpy(dma_m2m_chan, dma_dst, dma_src, DMA_BUF_SIZE, DMA_PREP_INTERRUPT);
	if (!dma_m2m_desc) {
		pr_err("error in prep_dma_sg\n");
		err = -EINVAL;
		goto dma_unmap;
	}
	dma_m2m_desc->callback = dma_m2m_callback;

	cookie = dmaengine_submit(dma_m2m_desc);
	if (dma_submit_error(cookie)) {
		pr_err("Unable to submit the DMA cookie\n");
		err = -EINVAL;
		goto dma_unmap;
	}
	pr_info("Got this cookie: %d\n", cookie);

	dma_async_issue_pending(dma_m2m_chan);
	pr_info("waiting for DMA transaction...\n");
	wait_for_completion(&dma_m2m_ok);

dma_unmap:
	dma_unmap_single(&dev, dma_src, DMA_BUF_SIZE, DMA_TO_DEVICE);
	dma_unmap_single(&dev, dma_dst, DMA_BUF_SIZE, DMA_FROM_DEVICE);
	if (err >= 0) {
		pr_info("Checking if DMA succeed ...\n");
		for (i = 0; i < DMA_BUF_SIZE / 4; I++) {
			if (*(rbuf + i) != *(wbuf + i)) {
				pr_err("Single DMA buffer copy failed!, r = %x, w = %x, %d\n", *(rbuf + i), *(wbuf + i), i);
				return err;
			}
		}
		pr_info("buffer copy passed!\n");
		dma_result = 1;
		data_dump("RBUF DMA bufer", (u8 *)rbuf, DMA_BUF_SIZE);
	}
channel_release:
	dma_release_channel(dma_m2m_chan);
	dma_m2m_chan = NULL;
	return err;
}

struct file_operations dma_fops = {
	.open = dma_open,
	.read = dma_read,
	.write = dma_write,
	.release = dma_release,
};

int __init dma_init_module(void)
{
	int error;
	struct device *dma_test_dev;
	error = register_chrdev(0, "dma_test", &dma_fops);
	if (error < 0) {
		pr_err("DMA test driver can't get major number\n");
		return error;
	}
	gMajor = error;
	pr_info("DMA test major number = %d\n", gMajor);
	dma_test_class = class_create(THIS_MODULE, "dma_test");
	if (IS_ERR(dma_test_class)) {
		pr_err("Error creating dma test module class\n");
		unregister_chrdev(gMajor, "dma_test");
		return PTR_ERR(dma_test_class);
	}
	dma_test_dev = device_create(dma_test_class, NULL, MKDEV(gMajor, 0), NULL, "dma_test");
	if (IS_ERR(dma_test_dev)) {
		pr_err("Error creating dma test class device\n");
		class_destroy(dma_test_class);
		unregister_chrdev(gMajor, "dma_test");
		return PTR_ERR(dma_test_dev);
	}
	dev_set_name(&dev, "dma-test-dev");
	device_register(&dev);
	pr_info("DMA test Driver Module loaded\n");
	return 0;
}

static void __exit dma_cleanup_module(void)
{
	unregister_chrdev(gMajor, "dma_test");
	device_destroy(dma_test_class, MKDEV(gMajor, 0));
	class_destroy(dma_test_class);
	device_unregister(&dev);
	pr_info("DMA test Driver Module Unloaded\n");
}

module_init(dma_init_module);
module_exit(dma_cleanup_module);

MODULE_AUTHOR("XXX");
MODULE_DESCRIPTION("DMA TEST DRIVER");
MODULE_LICENSE("GPL");

其中,wbuf代表源缓冲区,rbuf代表目标缓冲区。我们的实现是基于字符设备的,因此gMajor和dma_test_class被用来表示字符设备的主设备号和类别。

循环DMA说明

循环DMA是一种特殊的DMA传输模式,其中I/O外设驱动数据事务,并通过周期性的基础触发重复DMA传输。

在使用DMA控制器公开的回调函数时,你已经看到了dma_device.device_prep_dma_cyclic,它是dmaengine_prep_dma_cyclic()函数的后端。该函数的原型如下:

其中chan是分配的dma通道结构,buf_addr是映射DMA缓冲区的句柄,buf_len是DMA缓冲区的大小,period_len是一个循环周期的大小,dir是DMA传输的方向,flags是DMA传输的控制标志。如果成功,则返回一个DMA通道描述符结构,用于为DMA传输分配completion回调函数。大多数情况下,flags对应于DMA_PREP_INTERRUPT,这意味着在每个循环周期完成时应调用DMA传输的回调函数。

循环模式主要用于tty驱动程序,其中数据被馈送到FIFO环形缓冲区中。在此模式下,分配的DMA缓冲区被分成大小相等的周期(通常称为循环周期)​,以便每当完成一个这样的传输时,就会调用回调函数。

已实现的回调函数用于跟踪环形缓冲区的状态,缓冲区的管理是使用内核环形缓冲区API实现的(因此需要包含<linux/circ_buf.h>)​

循环周期(Cycle/Period):把整块 DMA 环形缓冲区平均分成 N 个等大的子缓冲区(比如 16KB 缓冲区分成 8 个周期,每个周期 2KB)。

✅ 为什么要分周期?

如果等整块 DMA 缓冲区写满再通知 CPU,会导致数据延迟(比如 16KB 缓冲区在 1Mbps 波特率下,写满需要约 128ms),不适合实时性要求高的场景;

分成小周期后,每写满一个周期就通知 CPU,既保证 "批量传输(减少中断)",又保证 "低延迟(及时处理数据)"。

C 复制代码
//准备DMA资源
static int atmel_prepare_rx_dma(struct uart_port *port)
{
	struct atmel_uart_port *atmel_port = to_atmel_uart_port(port);
	struct device *mfd_dev = port->dev->parent;
	struct dma_async_tx_descriptor *desc;
	dma_cap_mask_t mask;
	struct dma_slave_config config;
	struct circ_buf *ring;
	int ret, nent;

	ring = &atmel_port->rx_ring;
	dma_cap_zero(mask);
	dma_cap_set(DMA_CYCLIC, mask);
	
	atmel_port->chan_rx = dma_request_slave_channel(mfd_dev, "rx");
	sg_init_one(&atmel_port->sg_rx, ring->buf, sizeof(struct atmel_uart_char) * ATMEL_SERIAL_RINGSIZE);
	nent = dma_map_sg(port->dev, &atmel_port->sg_rx, 1, DMA_FROM_DEVICE);
	[...]
	ret = dmaengine_slave_config(atmel->port->chan_rx, &config);
	desc = dmaengine_prep_dma_cyclic(atmel_port->chan_rx, sg_dma_address(&atmel_port->sg_rx), sg_dma_len(&atmel_port->sg_rx), sg_dma_len(&atmel_port->sg_rx) / 2, DMA_DEV_TO_MEM, DMA_PREP_INTERRUPT);
	desc->callback = atmel_complete_rx_dma;
	desc->callback_param = port;
	atmel_port->desc_rx = desc;
	atmel_port->cookie_rx = dmaengine_submit(desc);
	dma_async_issue_pending(chan);
	return 0;
chan_err:
	[...]
}

为了提高可读性,错误检查已省略。atmel_prepare_rx_dma()函数首先在请求DMA通道之前设置适当的DMA能力掩码[使用dma_cap_set()]​。通道请求成功后,创建映射(流DMA映射)​,并使用dmaengine_slave_config()配置通道。然后通过dmaengine_prep_dma_cyclic()获取循环DMA的传输描述符,并使用DMA_PREP_INTERRUPT指令告知DMA引擎核心在每个周期传输结束时调用回调函数。最后,将获得的传输描述符与回调函数及其参数一起配置,使用dmaengine_submit()提交给DMA控制器,并使用dma_async_issue_pending()触发传输。

atmel_complete_rx_dma()回调函数将调度一个tasklet,这个tasklet的处理程序为atmel_tasklet_rx_func()。该处理程序将调用真正的DMA完成回调函数atmel_rx_from_dma(),该回调函数的实现如下所示:

C 复制代码
static void atmel_rx_from_dma(struct uart_port *port)
{
	struct atmel_uart_port *atmel_port = to_atmel_uart_port(port);
	struct tty_port *tport = &port->state->port; // TTY端口结构体
	struct circ_buf *ring = &atmel_port->rx_ring;
	struct dma_chan *chan = atmel_port->chan_rx;
	struct dma_tx_state state; //DMA传输状态结构体,核心字段residue(剩余未传输字节数)
	struct dma_status dmastat; //DMA传输状态枚举
	size_t count;
	dmastat = dmaengine_tx_status(chan, atmel_port->cookie_rx, &state);
	dma_sync_sg_for_cpu(port->dev, &atmel_port->sg_rx, 1, DMA_FROM_DEVICE); //缓存同步
	ring->head = sg_dma_len(&atmel_port->sg_rx) - state.residue; //DMA环形缓冲区总长度-剩余未传输字节数 = 已传输字节数
	if (ring->head < ring->tail) { //head < tail说明有绕回
		count = sg_dma_len(&atmel_port->sg_rx) - ring->tail; //计算第一段数据长度
		tty_insert_flip_string(tport, ring->buf + ring->tail, count); //把数据写入flip缓冲区
		ring->tail = 0; //tail指针绕回缓冲区开头
		port->icount.rx += count; //更新串口接受字节计数
	}
	if (ring->tail < ring->head) { //未绕回场景
		count = ring->head - ring->tail; //计算连续数据长度
		tty_insert_flip_string(tport, ring->buf + ring->tail, count);
		if (ring->head >= sg_dma_len(&atmel_port->sg_rx))
			ring->head = 0;
		ring->tail = ring->head;
		port->icount.rx += count;
	}
	dma_sync_sg_for_device(port->dev, &atmel_port->sg_rx, 1, DMA_FROM_DEVICE); //把CPU修改的缓冲区指针从缓存刷到物理内存
	[...]
	tty_flip_buffer_push(tport); //通知tty交付数据
	[...]
}

DMA和设备树绑定

消费者绑定根据SDMA事件映射表,以下代码显示了i.MX 6Dual/6Quad中外设的DMA请求信号:

dts 复制代码
uart1:serial @02020000 {
	compatible = "fsl,imx6sx-uart", "fsl,imx21-uart";
	reg = <0x02020000 0x4000>
	interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
	clocks = <&clks IMX6SX_CLK_UART_IPG>, <&clks IMX6SX_CLK_UART_SERIAL>;
	clock-names = "ipg", "per";
	dmas = <&sdma 25 4 0>, <&sdma 26 4 0>;
	dma-names = "rx", "tx";
	status = "disabled";
};
  1. compatible = "fsl,imx6sx-uart", "fsl,imx21-uart";
    核心作用:内核驱动通过该字符串匹配 "对应的串口驱动程序"(驱动匹配的核心依据);
    兼容层级(优先级从高到低):
    fsl,imx6sx-uart:i.MX6SX 芯片专属的 UART 兼容标识,内核优先匹配 i.MX6SX 定制的 UART 驱动;
    fsl,imx21-uart:i.MX21 通用 UART 兼容标识(兜底),若没有专属驱动,会匹配 i.MX 系列通用 UART 驱动(内核路径:drivers/tty/serial/imx.c);
    关键:i.MX 系列串口驱动(imx.c)正是通过识别 fsl,imx21-uart 等兼容字符串,完成驱动与硬件的绑定。
  2. reg = <0x02020000 0x4000>;
    核心作用:定义 UART1 寄存器的物理地址范围,内核会通过该配置完成 "物理地址→虚拟地址" 的映射,从而访问 UART1 硬件寄存器;
    参数拆解:
    0x02020000:UART1 寄存器的起始物理地址(i.MX6SX 芯片手册中定义的 UART1 基地址);
    0x4000:寄存器区域的长度(16KB),即内核可访问 0x02020000 ~ 0x02023FFF 地址空间,覆盖 UART1 所有控制 / 状态 / 数据寄存器。
  3. interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
    核心作用:配置 UART1 的中断信息,内核通过该配置注册 UART1 的中断处理函数(比如串口收发中断);
    参数拆解(i.MX6SX 是 ARM Cortex-A9 架构,用 GIC 中断控制器):
    GIC_SPI:中断类型 → SPI(Shared Peripheral Interrupt,共享外设中断),是 i.MX6SX 外设中断的主流类型(区别于 PPI 私有外设中断);
    26:中断号 → UART1 对应的硬件中断编号(i.MX6SX 芯片手册中定义的 SPI 26 分配给 UART1);
    IRQ_TYPE_LEVEL_HIGH:中断触发方式 → 高电平触发(LEVEL_HIGH),表示只要 UART1 中断请求引脚为高电平,就会触发中断(区别于边沿触发 EDGE_RISING/EDGE_FALLING)。
  4. clocks = <&clks IMX6SX_CLK_UART_IPG>, <&clks IMX6SX_CLK_UART_SERIAL>;
    核心作用:配置 UART1 依赖的时钟源(i.MX 系列 UART 必须配置两类时钟才能工作);
    参数拆解:
    &clks:时钟控制器节点的引用(设备树中需提前定义 clks 节点,管理全芯片时钟);
    IMX6SX_CLK_UART_IPG:IPG 时钟(Internal Peripheral Bus Clock)→ 用于 UART1 的寄存器访问、总线交互(保证寄存器读写时序);
    IMX6SX_CLK_UART_SERIAL:串口参考时钟(Serial Clock)→ 用于生成 UART 波特率(波特率 = 参考时钟 / 分频系数)。
  5. clock-names = "ipg", "per";
    核心作用:给 clocks 中的时钟源命名,驱动通过 "名称" 而非 "顺序" 区分时钟(避免顺序错乱);
    一一对应关系:
    "ipg" → 对应 clocks 中第一个时钟(IMX6SX_CLK_UART_IPG);
    "per"(peripheral,外设)→ 对应 clocks 中第二个时钟(IMX6SX_CLK_UART_SERIAL);
    驱动侧逻辑:i.MX UART 驱动会调用 devm_clk_get(dev, "ipg") 获取 IPG 时钟,devm_clk_get(dev, "per") 获取串口参考时钟,然后使能时钟并配置分频。
  6. dmas = <&sdma 25 4 0>, <&sdma 26 4 0>;
    核心作用:配置 UART1 收发的 DMA 通道(i.MX6SX 用 SDMA(Smart DMA)控制器,区别于 Atmel 的普通 DMA);
    参数拆解(SDMA 通道配置格式:<&sdma 通道号 请求号 标志>):
    &sdma:SDMA 控制器节点的引用(设备树中需提前定义 sdma 节点);
    第一个 <&sdma 25 4 0> → UART1 接收(RX)DMA 通道:
    25:SDMA 通道号(分配给 UART1 RX 的硬件通道);
    4:SDMA 请求号(i.MX6SX 中 4 对应 UART1 RX 的 DMA 请求);
    0:额外标志(无特殊配置,通常为 0);
    第二个 <&sdma 26 4 0> → UART1 发送(TX)DMA 通道:
    26:SDMA 通道号(分配给 UART1 TX 的硬件通道);
    4:SDMA 请求号(UART1 TX 与 RX 共用请求号 4);
    0:额外标志。
  7. dma-names = "rx", "tx";
    核心作用:给 dmas 中的 DMA 通道命名,和内核驱动中 dma_request_slave_channel(dev, "rx") 严格匹配;
    一一对应关系:
    "rx" → 对应 dmas 中第一个通道(SDMA 25,接收);
    "tx" → 对应 dmas 中第二个通道(SDMA 26,发送);
    关联驱动:和你之前接触的 Atmel 串口 DMA 逻辑一致,驱动调用 dma_request_slave_channel(&uart1_dev, "rx") 时,内核会通过 dma-names 匹配到 SDMA 25 通道。
  8. status = "disabled";
    核心作用:设置 UART1 的默认状态 → disabled 表示内核启动时不初始化该串口;
    启用方式:若需使用 UART1,需在板级设备树(比如 imx6sx-xxx.dts)中覆盖该属性:

dmas属性中的第2个单元格(25和26)对应于DMA请求/事件ID。这些值来自SoC手册(这里的情况是i.MX53)​。dmas属性中的第3个单元格表示使用的优先级。

接下来编写请求指定参数的驱动程序代码。你可以在内核源代码树的drivers/tty/serial/imx.c中找到完整的代码。以下是从设备树中提取元素的代码摘录:

关键调用dma_request_slave_channel()将用of_dma_request_slave_ channel()解析设备节点(在设备树中)​,并根据DMA通道名称收集通道设置。

C 复制代码
static int imx_uart_dma_init(struct imx_port *sport)
{
	struct dma_slave_config slave_config = {};
	struct device *dev = sport->port.dev;
	int ret;
	sport->dma_chan_rx = dma_request_slave_channel(dev, "rx");
	if (!sport->dma_chan_rx) {
		/*cannot get the DMA channel. handle error*/
		[...]
	}
	[...]/*config slave channel*/
	ret = dmaengine_slave_config(sport->dma_chan_rx, &slave_config);
	[...]
	sport->dma_chan_tx = dma_request_slave_channel(dev, "tx");
	if (!sport->dma_chan_tx) {
		/*cannot get the DMA channel. handle error*/
		[...]
	}
	[...]/*configure the slave channel*/
	ret = dmaengine_slave_config(sport->dma_chan_tx, &slave_config);
	if (ret) {
		[...]/*handle error*/
	}
	[...]
}

关键调用dma_request_slave_channel()将用of_dma_request_slave_ channel()解析设备节点(在设备树中)​,并根据DMA通道名称收集通道设置。

相关推荐
wdfk_prog25 分钟前
[Linux]学习笔记系列 -- hashtable
linux·笔记·学习
仙俊红1 小时前
spring的IoC(控制反转)面试题
java·后端·spring
阿湯哥1 小时前
AgentScope Java 集成 Spring AI Alibaba Workflow 完整指南
java·人工智能·spring
廋到被风吹走1 小时前
【Spring】Spring Cloud 熔断降级深度解析:从 Hystrix 到 Resilience4j 的演进
spring·spring cloud·hystrix
CheungChunChiu1 小时前
Linux 内核动态打印机制详解
android·linux·服务器·前端·ubuntu
fenglllle2 小时前
spring-data-jpa saveall慢的原因
数据库·spring·hibernate
czlczl200209252 小时前
Guava Cache 原理与实战
java·后端·spring
yangminlei2 小时前
Spring 事务探秘:核心机制与应用场景解析
java·spring boot
BlueBirdssh2 小时前
linux 内核通过 dts 设备树 配置pcie 控制器 各种参数和中断等, 那freeRTOS 是通过直接设置PCIe寄存器吗
linux
小目标一个亿3 小时前
Windows平台Nginx配置web账号密码验证
linux·前端·nginx