[TOC]
drivers/dma DMA引擎与映射(DMA Engine & Mapping) 内核统一的直接内存访问框架
历史与背景
这项技术是为了解决什么特定问题而诞生的?
DMA(Direct Memory Access,直接内存访问)框架的诞生是为了解决一个根本性的性能瓶颈问题,并为内核提供一个统一、可移植的解决方案。
- 解决CPU性能瓶颈:在没有DMA的系统中,当外设(如网卡、磁盘)需要与内存交换大量数据时,CPU必须亲自担当“搬运工”的角色。这种方式被称为PIO(Programmed I/O),CPU需要逐字节或逐字地从设备读取数据再写入内存,或者反之。对于高速设备,这会消耗大量的CPU周期,导致CPU无暇处理其他计算任务,严重影响系统整体性能。DMA技术通过引入一个专门的硬件控制器(DMAC),允许外设在没有CPU干预的情况下直接与内存进行数据传输,从而将CPU解放出来。
- 抽象硬件差异:不同的CPU架构和SoC平台,其DMA控制器的设计和编程接口千差万别。如果没有一个统一的软件框架,设备驱动程序的开发者将不得不为每一种不同的DMAC编写特定的、不可移植的代码。DMA框架就是为了提供一个标准的API,隐藏底层硬件的复杂性。
它的发展经历了哪些重要的里程碑或版本迭代?
Linux的DMA框架是分阶段演进的,主要形成了两大核心组件:
- DMA映射接口 (
dma-mapping) 的确立:这是最基础、最核心的一层。它主要解决了内存管理的问题,即如何为DMA操作提供安全、正确的内存缓冲区。它处理了三个关键问题:- 地址转换:CPU使用虚拟地址,而设备使用物理地址(或IO虚拟地址IOVA)。
dma-mappingAPI负责在这两者之间进行转换。 - 缓存一致性(Cache Coherency):当CPU和设备同时访问同一块内存时,可能会因为CPU Cache的存在而导致数据不一致(例如,设备直接写入内存,但CPU的Cache中仍然是旧数据)。
dma-mappingAPI提供了一系列函数(如dma_sync_*)来强制管理Cache的刷新和失效,确保数据同步。 - 连续内存:DMA操作通常需要物理上连续的内存块。
dma-mappingAPI提供了分配这种内存的接口(如dma_alloc_coherent)。
- 地址转换:CPU使用虚拟地址,而设备使用物理地址(或IO虚拟地址IOVA)。
- DMA引擎子系统 (
dmaengine) 的引入:在dma-mapping的基础上,dmaengine提供了一个更高层次的抽象,将DMAC本身服务化。它创建了一个生产者/消费者模型:- DMAC的驱动程序作为生产者(或提供者),注册自己能提供的DMA传输能力(如内存到内存、内存到设备等)。
- 需要进行DMA传输的设备驱动(如SPI、I2C控制器驱动)作为消费者,可以向
dmaengine请求一个DMA通道,并提交传输任务,而无需关心具体是哪个DMAC在为其服务。这极大地促进了驱动的解耦和代码复用。
目前该技术的社区活跃度和主流应用情况如何?
DMA框架是Linux内核中一个极其核心、稳定且至关重要的子系统。它是所有高性能设备驱动(网络、存储、图形、多媒体)正常工作的基础。
- 主流应用:
- 在嵌入式SoC中,
dmaengine被广泛使用,系统中的通用DMAC为SPI、I2C、UART、音频等各种外设提供DMA服务。 - 在PC和服务器中,像NVMe、SATA、千兆/万兆网卡等高性能PCIe设备通常有自己内置的DMA控制器(称为Bus-Mastering DMA),它们主要使用底层的
dma-mappingAPI来管理内存。
- 在嵌入式SoC中,
核心原理与设计
它的核心工作原理是什么?
DMA框架通过dma-mapping和dmaengine两个层面协同工作。
1. DMA映射层 (dma-mapping)
这是所有DMA操作的基础。一个设备驱动使用它的典型流程是:
- 分配缓冲区:驱动调用
dma_alloc_coherent()分配一个长期使用的、CPU和设备都能一致性访问的缓冲区;或者使用普通内存(如kmalloc),在需要进行DMA时通过dma_map_single()或dma_map_sg()(用于分散/汇集列表)进行临时映射。 - 获取设备可用地址:映射函数会返回一个
dma_addr_t类型的地址。这个地址是总线地址,可以直接编程到设备的DMA寄存器中。同时,内核会处理好必要的Cache刷新。 - 启动DMA:驱动将
dma_addr_t地址和传输长度写入设备硬件,启动传输。 - 等待完成:设备完成传输后,通常会通过中断通知驱动。
- 同步数据:驱动在中断处理函数中,必须调用
dma_sync_*_for_cpu()来确保CPU能看到设备写入内存的最新数据(例如,使相关的CPU Cache失效)。 - 解除映射:对于临时映射的缓冲区,驱动必须调用
dma_unmap_single()或dma_unmap_sg()来释放映射。
2. DMA引擎层 (dmaengine)
这是一个更上层的客户端-服务器模型:
- 请求通道:消费者驱动(如SPI驱动)调用
dma_request_chan()向dmaengine请求一个DMA通道。 - 准备描述符:消费者驱动配置好一个或多个描述DMA传输的描述符(
dma_async_tx_descriptor),包括源地址、目标地址、长度以及完成后的回调函数。对于非连续内存,会使用分散/汇集列表(scatterlist)。 - 提交任务:驱动调用
dmaengine_submit()将描述符提交给DMA通道。 - 启动传输:驱动调用
dma_async_issue_pending()启动所有已提交的传输。dmaengine核心会找到对应的DMAC驱动,并调用其回调函数来对硬件进行编程。 - 完成通知:DMAC硬件完成传输后,会触发中断。DMAC驱动处理中断,并最终调用消费者驱动在描述符中注册的回调函数,通知传输完成。
它的主要优势体现在哪些方面?
- 性能:极大地降低了CPU在数据传输中的开销,提升了系统吞吐量。
- 抽象与可移植性:隐藏了硬件细节,使得设备驱动可跨平台复用。
- 正确性:提供了处理缓存一致性、内存连续性等复杂问题的标准化方案,减少了驱动开发的错误。
- 解耦:
dmaengine使得设备驱动和DMAC驱动可以独立开发。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 延迟开销:设置一次DMA传输本身是有开销的(分配描述符、编程DMAC等)。对于非常小(比如几个字节)的数据传输,CPU直接拷贝(PIO)的延迟可能反而更低。
- 复杂性:DMA API,特别是涉及缓存一致性和异步回调的部分,比简单的PIO要复杂得多,容易出错。
- 硬件依赖:框架的有效性依赖于底层硬件DMAC的支持和正确性。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。
DMA是任何涉及大块数据在内存和外设之间传输场景的首选,甚至是唯一可行的方案。
- 网络接口卡(NIC):当接收数据包时,NIC通过DMA将数据包内容直接写入内存中的接收缓冲区,无需CPU干预。发送时同理。
- 存储控制器(SATA/NVMe):当读写文件时,存储控制器通过DMA在磁盘和内存的页缓存(Page Cache)之间传输整个数据块。
- 音频/视频流:声卡通过DMA持续地从内存缓冲区读取音频数据并发送到DAC(数模转换器)进行播放,实现流畅无卡顿的音频。
- 嵌入式SPI/I2C大传输:一个SPI驱动需要发送一个几KB的图像数据到LCD屏幕,它会使用
dmaengine来将数据从内存“喂”给SPI控制器的FIFO,而CPU可以去处理其他任务。
是否有不推荐使用该技术的场景?为什么?
- 小数据量的控制寄存器读写:例如,一个I2C驱动需要读取一个温度传感器的两位字节的温度值。这种操作使用PIO(CPU直接读写I2C控制器的数据寄存器)更简单高效,因为DMA的设置开销远大于CPU拷贝两个字节的开销。
- 需要CPU在传输过程中进行数据处理:DMA执行的是“内存到内存”或“内存到设备”的直接拷贝。如果数据在传输过程中需要被CPU进行复杂的实时处理(如加密、解压),那么数据路径就必须经过CPU,DMA无法适用。
对比分析
请将其 与 其他相似技术 进行详细对比。
在数据传输领域,DMA最直接的对比对象就是PIO(Programmed I/O)。
| 特性 | DMA (Direct Memory Access) | PIO (Programmed I/O) |
|---|---|---|
| CPU参与度 | 低。CPU仅在传输开始前进行设置,在传输结束后处理中断。 | 高。CPU全程参与数据的每一个字节/字的传输。 |
| 性能/吞吐量 | 高。传输速度受限于总线和内存带宽,适合大块数据传输。 | 低。传输速度受限于CPU的执行速度,会成为系统瓶颈。 |
| 延迟 | 设置延迟高。启动一次传输有固定开销。 | 设置延迟低。对于第一个字节的传输几乎没有额外延迟。 |
| 软件复杂性 | 高。需要处理异步回调、缓存一致性、内存映射等复杂问题。 | 低。通常只需要在一个循环中读写设备寄存器,逻辑简单。 |
| 硬件复杂性 | 高。需要一个专门的DMA控制器(DMAC)硬件。 | 低。只需要设备提供可供CPU读写的数据和状态寄存器。 |
| 适用场景 | 大数据块、流式数据传输(网络、存储、音视频)。 | 小数据量、低速率、控制/状态信息交互(如简单的I2C/SPI传感器读写)。 |
drivers/dma/virt-dma.h
virt-dma 框架: DMA描述符队列管理的核心
这两个函数是Linux内核**virt-dma (虚拟DMA通道) 框架的内部核心组件。virt-dma是一个通用的、与具体硬件无关的软件层, 旨在为各种DMA控制器驱动程序提供一套标准的、线程安全的描述符队列管理**机制。STM32 DMA驱动程序严重依赖这个框架来处理传输任务的排队和查找。
vchan_issue_pending: 将已提交的传输任务发布为”待启动”
此函数是连接**”准备好但未启动”和“即将启动”**两个状态的桥梁。当一个使用者驱动(如SPI)调用dmaengine_prep_slave_sg等函数时, virt-dma框架会创建描述符并将它们放入一个名为desc_submitted的”已提交”队列中。vchan_issue_pending的作用就是将这个队列中的所有任务一次性地移动到desc_issued(“已发出”)队列中。
核心原理: 它通过一个单一的、高度优化的链表操作
list_splice_tail_init来实现。这个操作原子地将desc_submitted链表中的所有节点”剪切”下来, 然后”粘贴”到desc_issued链表的末尾。操作完成后,desc_submitted链表会被自动重新初始化为空。这个函数本身并不启动硬件, 而是为硬件驱动程序(stm32_dma_start_transfer)提供一个明确的、需要被处理的任务列表。锁的重要性:
lockdep_assert_held(&vc->lock)这一行至关重要。它是一个调试断言, 强制要求调用此函数的地方(即stm32_dma_issue_pending)必须已经持有了虚拟通道的自旋锁。这确保了从检查队列到移动队列的整个过程不会被中断处理程序或其他任务打断, 从而保证了队列的完整性。
1 | /** |
vchan_find_desc: 在”已发出”的传输中查找特定任务
此函数用于在一个DMA通道的活动队列中查找一个特定的传输任务。使用者驱动通过一个唯一的dma_cookie_t来标识它想查询的传输。
核心原理: 它实现了一个简单的线性搜索。它使用
list_for_each_entry宏来遍历desc_issued链表中的每一个描述符。在循环中, 它比较每个描述符的cookie与传入的cookie是否匹配。如果找到匹配项, 就返回该描述符的指针; 如果遍历完整个链表都没有找到, 就返回NULL。使用场景: 这个函数是实现
tx_status回调的关键。当stm32_dma_tx_status需要计算一个尚未完成但已不在硬件上运行的传输的剩余字节数时, 它就会调用vchan_find_desc来找到这个传输的描述符, 从中读取总长度。注意: 此函数只在
desc_issued队列中查找。这意味着它只能找到那些已经被vchan_issue_pending处理过, 即正在硬件上传输或在队列中等待硬件启动的任务。它不会查找那些刚刚提交但尚未被”发出”的任务。
1 | /* |
vchan_tx_prep: DMA描述符的最终封装与注册
此函数是**virt-dma (虚拟DMA通道) 框架中的一个核心辅助函数, 它是任何prep系列回调函数(如stm32_dma_prep_slave_sg)在完成工作前的最后一步**。
它的核心原理是为一个已经由具体驱动程序(如STM32 DMA驱动)填充了硬件相关细节的描述符, “穿上”一层通用的、标准化的外衣, 并将其安全地注册到虚拟通道的内部管理队列中。
可以把它理解为DMA传输准备流程中的**”质检与归档”**步骤:
- 质检: 它为描述符填充了所有符合Linux通用DMA引擎(DMA Engine)标准的接口和字段。
- 归档: 它将这个准备就绪的描述符放入
desc_allocated(已分配)队列, 等待使用者驱动下一步的”提交”(submit)动作。
这个过程确保了无论底层DMA硬件有多大的差异, 所有提交给DMA引擎的描述符都具有统一的接口和行为, 这是实现硬件抽象的关键。
1 | /** |
virt-dma 框架: 描述符的同步、收集与批量清理
这一组函数是**virt-dma (虚拟DMA通道) 框架中负责高级生命周期管理和流程控制的API。它们协同工作, 为上层的具体DMA驱动(如STM32 DMA)提供了一套强大的工具, 用于实现强制终止 (terminate_all)** 和 同步 (synchronize) 等复杂操作。
vchan_terminate_vdesc: 终止描述符并停用其循环回调 (回顾)
- 核心原理: 这是一个原子操作, 用于将一个描述符隔离到
desc_terminated队列, 并切断其作为循环传输的软件反馈链接 (vc->cyclic = NULL)。这是终止操作的基础。
1 | static inline void vchan_terminate_vdesc(struct virt_dma_desc *vd) |
vchan_get_all_descriptors: “全员疏散” - 收集所有描述符
此函数是一个强大的内部工具, 用于原子地清空一个虚拟通道的所有内部描述符队列。
- 核心原理: 它通过一系列
list_splice_tail_init调用, 将allocated,submitted,issued,completed,terminated这五个状态队列中的所有描述符节点, 一次性地、高效地“剪切”并”粘贴”到一个外部的临时链表head中。操作完成后, 虚拟通道的所有内部队列都变为空。这是一个典型的**”先收集, 后处理”**模式, 它允许驱动在锁内快速地收集所有需要清理的资源, 然后在锁外执行耗时的清理操作。
1 | /** |
vchan_synchronize: 同步软件回调并清理”已终止”任务
此函数提供了一个阻塞式的同步点, 主要用于确保所有由软件触发的回调(特别是tasklet)都已执行完毕。
- 核心原理: 它的同步机制主要依赖于
tasklet_kill。virt-dma框架使用一个tasklet(一种软中断)来执行DMA完成回调。tasklet_kill(&vc->task)会阻塞当前线程, 直到该tasklet执行完成(如果它已被调度)。这确保了在函数返回时, 没有回调函数正在运行。作为一个附带的清理动作, 它还会安全地收集并释放所有先前被明确terminate的描述符, 防止内存泄漏。
1 | /** |
vchan_vdesc_fini: 描述符的最终裁决 - 重用或释放
此函数是**virt-dma (虚拟DMA通道) 框架中负责一个描述符生命周期终点**的关键函数。当一个DMA传输完成(或被终止)后, 它的描述符最终会来到这里接受”裁决”。
它的核心原理是根据使用者驱动的意图, 决定一个描述符的最终命运: 是将其“回收”以备下次使用(Re-use), 还是将其彻底“销毁”并释放其内存(Free)。
这个机制是DMA引擎框架中一项重要的性能优化。对于需要高频率、重复性地提交相同类型DMA传输的应用(例如, 音频流的每个数据包), 反复地分配和释放描述符内存会带来不小的开销。通过允许描述符重用, 系统可以显著减少内存管理开销, 提高吞吐量。
vchan_vdesc_fini 的工作流程
1 | /** |
vchan_cyclic_callback: 安全地调度循环DMA周期的完成回调
此函数是一个定义在头文件中的、高度优化的静态内联函数。它的核心作用是在硬件DMA中断处理程序(硬中断上下文)中, 以最快、最安全的方式, 安排对一次循环DMA传输周期的完成通知。
此函数是Linux内核中”中断下半部”(Bottom Half)或”延迟工作”(Deferred Work)设计哲学的典型体现。其原理是将可能耗时的工作从对时间要求极为苛刻的硬中断上下文中移出, 交给一个更宽松的软中断上下文(tasklet)来执行。
具体工作流程如下:
- 触发: 当一个配置为循环模式的DMA通道完成一个周期(例如, 传输完一个音频缓冲区)时, DMA硬件会产生一个中断。内核会调用该中断的硬中断处理程序(例如
stm32_dma_chan_irq)。 - 快速标记与调度: 硬中断处理程序在确认是循环周期完成后, 会立即调用
vchan_cyclic_callback。此函数只做两件非常快速的事情:vc->cyclic = vd;: 它将指向当前完成的DMA描述符(vd)的指针, 保存到虚拟通道(vc)的一个特殊字段cyclic中。这就像是在邮箱上插上一面旗帜, 标记“有一个循环周期的回调需要处理”。tasklet_schedule(&vc->task);: 它将与该虚拟通道关联的tasklet(vc->task)放入一个调度队列中。这个tasklet的处理函数是vchan_complete(在之前的分析中已出现)。这个调度动作本身是原子的, 并且执行得极快。
- 延迟执行: 在硬中断处理程序安全退出后, 内核会在稍后的一个”安全”时间点(通常是在下一次时钟节拍或者没有更高优先级任务时), 从队列中取出并执行
vchan_complete这个tasklet。vchan_complete会检查到vc->cyclic字段被设置, 从而知道它需要调用客户端驱动提供的循环回调函数, 并处理后续逻辑。
对于用户指定的STM32H750(单核)架构, 这种机制的价值丝毫没有减弱:
- 降低中断延迟: 即使只有一个核心, 硬中断也会抢占当前正在执行的任何代码。
vchan_cyclic_callback确保了硬中断处理程序本身能在微秒级别的时间内完成, 从而使得CPU可以极快地响应系统中其他可能更重要的中断(例如, 高速通信或精确的定时器中断)。 - 更宽松的执行上下文: 客户端驱动的回调函数将在
tasklet的上下文中执行。这个上下文虽然仍然不能休眠, 但它比硬中断上下文要宽松得多, 并且不会屏蔽其他硬件中断的发生, 从而保证了整个系统的响应性和稳定性。
1 | /** |
drivers/dma/virt-dma.c 描述符的提交与释放
这一组函数是**virt-dma (虚拟DMA通道) 框架的核心API, 它们构成了DMA传输描述符(descriptor)生命周期中状态转换**的关键节点。vchan_tx_submit将一个准备好的任务正式提交到待处理队列, 而vchan_tx_desc_free则负责将其从系统中彻底移除并释放内存。
这两个函数都体现了virt-dma框架的核心设计思想: 通过集中的、带锁的队列操作, 为上层的具体DMA驱动(如STM32 DMA)提供一个线程安全的、与硬件无关的描述符管理后端。
to_virt_desc (内部辅助宏)
这是一个静态内联函数, 实际上扮演了宏的角色。
- 核心原理: 它使用了Linux内核中一个非常基础且重要的设计模式:
container_of。通用DMA引擎的API(如回调函数)只处理指向dma_async_tx_descriptor的指针。然而,virt-dma框架需要管理一个更大的、包含更多信息的结构体virt_dma_desc, 而dma_async_tx_descriptor只是这个大结构体中的一个成员。to_virt_desc的作用就是根据成员的地址, 反向计算出整个容器结构体的起始地址。
1 | // to_virt_desc - 将一个通用的 tx 描述符指针转换为 virt_dma 描述符指针 |
vchan_tx_submit: 提交一个DMA传输请求
当一个使用者驱动(如SPI)调用dmaengine_submit()时, DMA引擎核心会通过vchan_tx_prep中设置的函数指针, 最终调用到此函数。
核心原理: 此函数执行两个关键动作, 将一个准备好的描述符(
allocated状态)原子地转换为”已提交, 等待启动”(submitted状态):- 分配票据(Cookie): 它调用
dma_cookie_assign为这次传输分配一个唯一的、顺序递增的ID。这个ID是之后追踪传输状态的唯一凭证。 - 移动队列: 它调用
list_move_tail, 将描述符从desc_allocated链表移动到desc_submitted链表的末尾。这是一个高效的O(1)操作。
整个过程都在自旋锁的保护下进行, 确保了在多任务或中断环境中, cookie的分配和队列的移动是一个不可分割的原子操作。
- 分配票据(Cookie): 它调用
1 | dma_cookie_t vchan_tx_submit(struct dma_async_tx_descriptor *tx) |
vchan_tx_desc_free: 释放一个DMA描述符
当一个描述符的生命周期结束时(无论是正常完成还是被终止), 需要调用此函数来释放其占用的内存。
核心原理: 此函数执行一个**”摘除并委托”**的操作:
- 摘除: 在自旋锁的保护下, 它调用
list_del将描述符从它当前所在的任何virt-dma链表中安全地移除。 - 委托:
virt-dma框架本身并不知道如何释放这个描述符的内存, 因为这个描述符可能是由具体的DMA驱动(如STM32 DMA)以一个更大的、自定义的结构体(如stm32_dma_desc)分配的。因此, 在锁释放之后, 它会调用一个名为vc->desc_free的回调函数。这个回调函数是由具体的DMA驱动在初始化时提供给virt-dma框架的。
这个设计是**控制反转(Inversion of Control)**的典型例子, 它使得
virt-dma框架可以管理它不了解具体实现的资源, 极大地提高了代码的模块化和复用性。- 摘除: 在自旋锁的保护下, 它调用
1 | /** |
vchan_dma_desc_free_list: “清理小队” - 批量释放描述符
此函数负责处理由vchan_get_all_descriptors等函数收集到的描述符列表, 安全地释放其中每一个描述符的内存。
- 核心原理: 它遍历传入的
head链表, 对其中的每一个描述符执行**控制反转(Inversion of Control)**的释放流程。virt-dma框架本身不知道如何释放一个具体的stm32_dma_desc结构, 因此它调用vchan_vdesc_fini, 而vchan_vdesc_fini内部会调用由具体驱动(如STM32 DMA驱动)在初始化时提供的vc->desc_free回调函数。这使得通用的框架可以管理和释放由特定驱动分配的、自定义的内存结构。
1 | void vchan_dma_desc_free_list(struct virt_dma_chan *vc, struct list_head *head) |
stm32_gpiolib_register_bank: 注册一个STM32的GPIO端口
此函数是stm32_pctl_probe函数的核心组成部分, 它的作用是将一个在设备树中描述的、独立的GPIO端口(例如GPIOA, GPIOB等, 称为一个”bank”)注册到Linux内核的gpiolib和irqchip框架中。完成注册后, 这个端口上的所有引脚就正式成为可被系统其他驱动程序使用的标准GPIO资源和中断源。
它的原理是一个多阶段的硬件抽象和软件注册过程:
- 硬件初始化: 它首先确保GPIO端口的硬件已经准备就緒, 包括将其”移出复位状态”(
reset_control_deassert), 以及通过内存映射(devm_ioremap_resource)获得访问其物理寄存器的虚拟地址。 - GPIO编号和Pinctrl集成: 这是关键的一步。它需要确定这个端口的引脚(本地编号0-15)如何映射到Linux内核的全局GPIO编号空间。
- 首选方式是解析设备树中的
gpio-ranges属性, 这个属性明确地定义了映射关系, 实现了最大的灵活性。 - 如果该属性不存在, 它会采用一种简单的、基于探测顺序的备用方案来计算全局编号。
- 无论采用哪种方式, 它都会调用
pinctrl_add_gpio_range, 将这个全局GPIO编号范围与当前的gpio_chip关联起来, 从而建立了pinctrl子系统和gpiolib子系统之间的桥梁。
- 首选方式是解析设备树中的
- 中断域层次结构: 它调用
irq_domain_create_hierarchy来创建一个新的中断域(bank->domain), 并将其设置为pinctrl主中断域(pctl->domain, 通常是EXTI)的子域。这精确地模拟了STM32的硬件结构: EXTI是中断的接收者, 而GPIO端口的配置决定了是将PA0, PB0还是PC0连接到EXTI0这条线上。这个层次结构使得中断管理既清晰又高效。 gpio_chip填充与注册: 它会填充一个struct gpio_chip结构体。这就像是向内核提交的一份”GPIO端口简历”, 里面包含了:- 指向实现具体硬件操作(如读、写、设置方向)的函数指针(这些通常在一个模板
stm32_gpio_template中预定义)。 - 端口的名称、父设备、引脚数量等信息。
- 一个包含所有引脚名称(如”PA0”, “PA1”…)的数组, 用于调试和sysfs。
- 指向实现具体硬件操作(如读、写、设置方向)的函数指针(这些通常在一个模板
- 最后, 它调用
gpiochip_add_data, 将这个完全配置好的gpio_chip正式提交给gpiolib核心。从这一刻起, 内核就完全接管了这个GPIO端口。
在STM32H750单核系统上, spin_lock_init(&bank->lock)依然至关重要, 它用于保护对该端口寄存器的访问, 防止在普通任务上下文中的访问被中断处理程序中的访问打断, 从而避免了竞态条件。
1 | /* |
vchan_complete: 处理虚拟DMA通道的完成任务
此函数是一个tasklet处理程序。Tasklet是Linux内核中的一种延迟工作机制, 它允许中断处理程序将耗时较长的工作推迟到”软中断上下文”中执行, 从而使硬中断处理程序本身能尽快完成。此函数的核心作用是在一个虚拟DMA通道上, 处理一批已经完成的DMA传输任务。
它的核心原理是将加锁时间最小化, 实现高效的生产者-消费者模型:
- 生产者(中断处理程序): 当DMA硬件完成一次传输并触发中断时, 中断处理程序(未在此处显示)会做最少的工作: 它将代表已完成传输的”描述符”(
virt_dma_desc)添加到一个共享的”已完成”链表(vc->desc_completed)中, 然后调度此vchan_completetasklet去执行。 - 消费者(本tasklet):
- 快速抓取: Tasklet开始执行后, 它首先获取通道的自旋锁
vc->lock。然后, 它不是在锁内逐个处理描述符, 而是调用list_splice_tail_init这个高效的链表操作, 一次性地将整个共享的”已完成”链表从未清空, 并移动到一个本地的、私有的head链表中。 - 快速释放: 抓取完成后, 它立即释放自旋锁。这是整个设计的精髓: 关键的共享数据访问(链表移动)在极短的时间内完成, 极大地降低了锁的争用, 提高了系统性能。
- 安全处理: 释放锁之后, tasklet现在可以从容地、安全地遍历它自己的私有
head链表。对于链表中的每一个描述符, 它会:- 调用
dmaengine_desc_get_callback获取该传输任务完成时需要通知的回调函数。 - 调用
dmaengine_desc_callback_invoke执行这个回调, 从而通知提交该DMA任务的驱动程序(例如,一个SPI驱动) “你的数据已经发送/接收完毕”。 - 调用
vchan_vdesc_fini释放该描述符占用的内存。
- 调用
- 快速抓取: Tasklet开始执行后, 它首先获取通道的自旋锁
1 | /* |
vchan_init: 初始化虚拟DMA通道
此函数是**virt-dma (虚拟DMA通道) 框架的构造函数**。它的核心原理是为一个DMA通道建立起一套完整的、与硬件无关的软件管理基础设施。当一个具体的DMA驱动(如STM32 DMA驱动)在其probe函数中初始化其通道时, 就会调用此函数。
vchan_init本身不与任何硬件寄存器交互。相反, 它精心构建了管理DMA传输描述符(descriptor)生命周期所需的所有软件数据结构和同步原语。可以把它看作是为一个DMA通道的”项目经理”(virt_dma_chan)配备好办公室、文件柜、待办事项列表和通信工具, 使其能够开始接收和管理任务。
这个初始化过程是后续所有virt-dma操作(如vchan_tx_prep, vchan_issue_pending)能够正确工作的基础。
vchan_init 的工作流程
1 | /* |
drivers/dma/of-dma.c
of_dma_find_controller: 查找已注册的DMA控制器
这是一个在Linux内核设备树(Device Tree) DMA辅助框架中扮演**”目录服务”**角色的核心内部函数。它的唯一作用是: 根据一个指向设备树节点的指针, 在一个全局的、已注册的DMA控制器/路由器列表中, 查找并返回与之匹配的软件抽象对象(struct of_dma)。
此函数的原理可以被理解为一个简单的、线性的数据库查询。内核维护着一个全局链表of_dma_list, 所有成功初始化的DMA控制器驱动(如STM32的DMA1, DMA2驱动)和DMA路由器驱动(如STM32的DMAMUX驱动)都会将代表自己的struct of_dma对象注册到这个链表中。当需要为一个DMA请求寻找服务提供者时, 内核会调用此函数, 传入从客户端驱动的dmas属性中解析出的硬件节点指针(dma_spec->np)。此函数会遍历全局链表, 通过直接比较节点指针的方式, 高效地找到与该硬件节点对应的、已经”在线”的软件服务实例。
这个查找过程是整个DMA路由和通道分配机制的基石。例如, 在of_dma_router_xlate函数中, 它被调用两次:
- 第一次, 用原始的
dma_spec(指向DMAMUX)来找到DMAMUX驱动的of_dma实例。 - 在DMAMUX驱动重写了
dma_spec使其指向真正的DMA控制器(如DMA1)后, 第二次调用此函数来找到DMA1驱动的of_dma实例, 从而将请求最终转发给正确的执行者。
1 | /** |
of_dma_router_xlate: 执行路由DMA请求的翻译
此函数是Linux内核设备树(Device Tree) DMA辅助框架中一个极其精妙的核心调度函数。当一个外设驱动请求的DMA通道指向一个DMA路由器(如STM32的DMAMUX)时, 此函数会被调用。它的核心作用是充当一个”中间人”或”业务流程编排器”, 负责协调客户端驱动、路由器驱动和最终的DMA控制器驱动这三方, 完成一次复杂的路由DMA请求。
它的原理可以被理解为一个三阶段的翻译与转发过程:
第一阶段: 调用路由器, 建立硬件路由并重写请求。
- 函数首先调用路由器驱动(如DMAMUX驱动)注册的
of_dma_route_allocate回调函数。 - 这个回调函数(如我们之前分析的
stm32_dmamux_route_allocate)会执行两项关键任务:
a. 物理连接: 它会找到一个空闲的DMAMUX硬件通道, 并通过写寄存器, 将发起请求的外设信号连接到这个通道上。
b. 请求重写: 它会修改传入的dma_spec_target参数, 将其phandle从指向DMAMUX节点重定向到指向一个真正的DMA控制器节点(如DMA1), 并将其参数修改为该DMA控制器能理解的流(stream)编号和配置。 - 执行完毕后,
dma_spec_target就不再是一个对路由器的请求, 而是一个全新的、对最终DMA控制器的有效请求。
- 函数首先调用路由器驱动(如DMAMUX驱动)注册的
第二阶段: 查找最终目标, 转发重写后的请求。
- 函数使用这个被重写过的
dma_spec_target, 再次在内核的全局DMA控制器列表中进行查找, 这次它会找到代表真正DMA控制器(如DMA1)的of_dma对象(ofdma_target)。 - 依赖处理: 如果找不到(因为真正的DMA控制器驱动尚未加载), 此函数会返回
-EPROBE_DEFER, 优雅地处理了驱动间的依赖关系。 - 接着, 它调用这个最终目标的
of_dma_xlate函数, 这就进入了标准的、非路由的DMA通道分配流程。
- 函数使用这个被重写过的
第三阶段: 链接与返回, 为未来清理做好准备。
- 如果最终的DMA通道被成功分配, 函数并不会立即返回。它会执行一个关键的”链接”操作:
a. 在返回的struct dma_chan中, 存入指向路由器驱动的指针(chan->router)。
b. 存入第一阶段中路由器驱动返回的私有路由数据(chan->route_data, 例如包含DMAMUX通道号的结构体)。 - 这个链接操作至关重要。当客户端驱动将来调用
dma_release_channel释放通道时, DMA框架看到这两个字段不为空, 就会知道这是一个路由通道, 从而会调用路由器驱动的route_free回调函数来断开硬件连接, 完美地完成了资源的自动清理。
- 如果最终的DMA通道被成功分配, 函数并不会立即返回。它会执行一个关键的”链接”操作:
错误处理: 这个函数包含了极为健壮的错误处理。如果在第二或第三阶段失败, 它会立即回头调用路由器驱动的route_free函数, 以确保在第一阶段已经建立的硬件连接被立即拆除, 防止了硬件资源泄漏。
1 | /** |
of_dma_router_register: 注册一个DMA路由器
此函数是Linux内核设备树(Device Tree) DMA辅助框架中的一个核心API。它的作用是将一个DMA路由器(DMA Router)设备, 例如STM32的DMAMUX, 作为一个合法的”DMA控制器”类型注册到内核中。
这个函数的核心原理是充当一个”登记员”。它并不执行任何实际的硬件操作, 而是创建一个代表DMA路由器的软件描述对象(struct of_dma), 并将驱动程序提供的、最关键的回调函数——of_dma_route_allocate(路由分配函数)——的地址保存在这个对象中。然后, 它将这个描述对象安全地添加到一个全局的DMA控制器链表(of_dma_list)中。
完成注册后, 当任何客户端驱动(如SPI驱动)根据其设备树中的dmas = <&dmamux ...>;属性请求DMA通道时:
- 内核的DMA框架会遍历全局链表, 找到与
&dmamux节点匹配的这个of_dma描述对象。 - 框架发现这是一个路由器, 于是它不会去查找最终的DMA通道, 而是转而调用我们在此处注册的
of_dma_route_allocate回调函数。 - 这个回调函数(如我们之前分析的
stm32_dmamux_route_allocate)将负责执行所有硬件路由操作, 并”伪造”一个新的DMA请求, 将其指向一个真正的DMA控制器。
通过这种方式, of_dma_router_register函数巧妙地将DMAMUX驱动”注入”到了标准的DMA请求流程中, 实现了对DMA请求的拦截和重定向, 从而完美地抽象了底层硬件的复杂性。
1 | /** |
of_dma_controller_register: 将DMA控制器注册为设备树DMA服务提供者
此函数是Linux内核中连接DMA控制器驱动和DMA使用者驱动(如SPI, I2C等)之间的核心桥梁。它的根本原理不是去操作硬件, 而是在一个全局的、可查询的注册表中, 发布一个DMA控制器”服务”。它向整个内核宣告: “我, 这个由设备树节点np所代表的DMA控制器, 现在已经准备就绪。如果任何其他设备需要在设备树中请求我的DMA服务, 请使用我提供的这个of_dma_xlate函数来翻译它们的请求。”
这个”翻译” (xlate = translate) 的概念是理解此函数的关键。在设备树中, 一个需要DMA的设备(如SPI控制器)会有一个dmas属性, 类似这样: dmas = <&dma1 5 0x400 0x80>;。这个条目对于SPI驱动来说是完全不透明的; 它只知道它需要一个DMA通道, 但它不知道5, 0x400, 0x80这些数字对于dma1控制器究竟意味着什么(例如, 可能是”使用第5个流, 配置为内存到外设, 优先级高, 采用FIFO模式”)。
of_dma_controller_register所注册的of_dma_xlate函数, 正是负责将这些抽象的、特定于硬件的数字翻译成一个标准的、可供SPI驱动使用的struct dma_chan(DMA通道)句柄的逻辑所在。
其工作流程非常直接:
- 创建注册记录: 函数首先分配一个
struct of_dma结构体。这个结构体就像一张”服务提供商名片”。 - 填充记录: 它将三项关键信息填入这张名片:
of_node: DMA控制器自身的设备树节点。这是服务提供商的唯一标识。of_dma_xlate: 指向DMA控制器驱动自己实现的翻译函数的指针。这是服务的核心逻辑。of_dma_data: 一个私有数据指针, 会在调用of_dma_xlate时传回, 用于提供上下文。
- 发布到全局列表: 函数会获取一个全局互斥锁
of_dma_lock, 然后将这张填好的”名片”(ofdma)添加到全局的DMA控制器链表of_dma_list的末尾。
当SPI驱动调用of_dma_request_slave_channel()时, 内核的DMA辅助代码就会:
- 遍历
of_dma_list这个全局链表。 - 根据SPI设备
dmas属性中的phandle(&dma1), 在链表中找到of_node与之匹配的ofdma“名片”。 - 调用这张名片上记录的
of_dma_xlate函数, 并将dmas属性中剩余的数字(5,0x400,0x80)作为参数传递给它。 of_dma_xlate函数执行其内部逻辑, 返回一个配置好的dma_chan指针。- SPI驱动拿到这个指针, 就可以开始准备DMA传输了。
在STM32H750这样的单核系统中, mutex_lock依然是必需的, 它可以防止在驱动注册过程中, 由于任务抢占或中断, 导致对全局of_dma_list链表的并发访问而破坏其完整性。
1 | /** |
of_dma_request_slave_channel: 从设备树解析和请求DMA从通道
本代码片段展示了Linux内核中基于设备树(Device Tree)的DMA从通道请求机制的核心实现。其主要功能是为设备驱动提供一个API (of_dma_request_slave_channel),允许驱动仅通过其设备树节点(np)和一个字符串名称(如"rx"或"tx"),就能自动解析出对应的DMA控制器、通道信息,并向正确的DMA控制器驱动请求分配一个dma_chan实例。这是现代嵌入式Linux驱动中实现DMA功能、处理驱动依赖的标准且核心的流程。
实现原理分析
该机制的核心是设备树中的标准化绑定。它依赖于设备树(DTS)文件中dmas和dma-names这两个属性的成对出现,来描述一个设备(如USART)与一个DMA通道之间的物理连接。
设备树绑定规范:
dmas: 这是一个属性,其内容是一个或多个**DMA说明符(specifier)**的列表。每个说明符通常由一个指向DMA控制器节点的phandle和一系列参数(由#dma-cells定义,如通道号、流号、优先级等)组成。dma-names: 这是一个字符串列表,与dmas列表中的条目一一对应。它为每个DMA说明符提供了一个人类可读的、功能性的名称,如"rx"或"tx"。
匹配与解析 (
of_dma_match_channel):- 这是一个辅助函数,其职责是在设备节点的
dmas/dma-names属性列表中,查找指定索引(index)处的条目是否与给定的名称(name)匹配。 - 步骤:
of_property_read_string_index: 读取dma-names列表中第index个字符串。strcmp: 比较读取到的字符串与请求的name是否相同。of_parse_phandle_with_args: 如果名称匹配,则解析dmas列表中同样位于第index个的DMA说明符,将其中的phandle和参数提取到dma_spec结构体中。
- 只有当这三步全部成功时,该函数才返回0,表示在指定索引处找到了一个完全匹配的DMA通道描述。
- 这是一个辅助函数,其职责是在设备节点的
请求与翻译 (
of_dma_request_slave_channel):- 这是主API函数,它 orchestrates 整个过程。
- 遍历与查找: 它会遍历设备节点中所有的
dma-names条目。 - 简单负载均衡:
start = atomic_inc_return(&last_index); ... (i + start) % count这是一个简单的轮询机制。如果设备树中为同一个名称(如”rx”)提供了多个可选的DMA通道,这个原子计数器会使得每次请求都从一个不同的起始点开始搜索,从而在多个请求者之间大致平均地分配这些通道。 - 寻找控制器: 当通过
of_dma_match_channel找到一个匹配的dma_spec后,它会调用of_dma_find_controller。这个函数会拿着dma_spec中的phandle,去一个全局的、已注册的DMA控制器列表中查找,看是否有哪个DMA控制器驱动已经“认领”了这个设备树节点。 - 翻译 (
.of_dma_xlate):- 如果找到了对应的DMA控制器驱动(
ofdma),就会调用该驱动提供的**of_dma_xlate回调函数。这是最关键的一步。.of_dma_xlate(translate,翻译)函数是DMA控制器驱动的一部分,它的职责是接收通用的、来自设备树的dma_spec参数,并将其翻译**成一个具体的、可用的struct dma_chan实例。
- 如果找到了对应的DMA控制器驱动(
- 依赖处理 (
-EPROBE_DEFER):- 如果在
of_dma_find_controller中没有找到匹配的DMA控制器驱动,这意味着该DMA驱动尚未被探测(probe)或初始化。在这种情况下,函数不会立即失败,而是将最终的返回值标记为-EPROBE_DEFER。这个错误码会沿着调用栈向上传递,最终告诉内核驱动核心:“我的依赖项(DMA控制器)还没准备好,请稍后重试我的probe”。
- 如果在
特定场景分析:单核、无MMU的STM32H750平台
硬件交互
- 设备树实例: STM32H750的
usart1设备树节点会包含:1
2
3
4
5usart1: serial@40010000 {
/* ... */
dmas = <&dma1_stream0 0x400401>, <&dma1_stream1 0x400402>;
dma-names = "rx", "tx";
}; - 执行流程: 当
stm32_usart_serial_probe调用of_dma_request_slave_channel(np, "rx")时:- 函数会遍历
dma-names={"rx", "tx"}。 - 当它检查到索引
0时,of_dma_match_channel成功,因为"rx"匹配。 of_parse_phandle_with_args会解析dmas列表的第0个条目,得到dma_spec,其中包含:- 一个指向
dma1节点的phandle。 - 参数
0x400401(这通常编码了DMA流的配置,如请求线ID、方向、优先级等,具体格式由dma1节点的#dma-cells定义)。
- 一个指向
of_dma_find_controller会找到已经注册的STM32 DMA驱动。- STM32 DMA驱动的
.of_dma_xlate函数被调用。它会解析0x400401这个参数,知道客户端需要DMA1的Stream 0,并且需要将DMAMUX配置为将USART1_RX的请求路由到Stream 0。然后,它返回一个代表DMA1_Stream0的dma_chan。
- 函数会遍历
单核与无MMU影响
- 并发:
atomic_inc_return在单核上仍然是原子的,可以防止中断或抢占。mutex_lock(&of_dma_lock)则保护了全局DMA控制器列表在被遍历和查找时的一致性,防止了任务间的并发访问。 - MMU: 此代码是纯粹的设备树解析和内核数据结构操作。它不涉及任何内存地址的直接使用,因此与MMU完全无关,可以在无MMU的STM32H750上正确运行。
代码分析
1 | /** |
kernel/dma/mapping.c
dma_alloc_coherent: 统一的DMA一致性内存分配接口
本代码片段展示了Linux内核中用于分配DMA一致性内存的核心API——dma_alloc_coherent及其底层实现dma_alloc_attrs。其核心功能是提供一个统一的、与架构无关的接口,允许设备驱动程序申请一块特殊的内存区域。这块内存保证了CPU的缓存视图和DMA控制器的物理内存视图始终保持一致,从而免除了驱动程序手动进行复杂的缓存刷新(flushing)和失效(invalidating)操作。该函数会返回两个地址:一个供CPU使用的虚拟地址,另一个供DMA控制器使用的总线地址。
实现原理分析
该机制是DMA引擎框架中为了解决“缓存一致性”问题而设计的核心抽象。它通过dma_map_ops结构体将具体的实现委托给与架构和总线相关的后端,并根据系统配置(如是否有IOMMU)选择最优的分配路径。
问题的根源——缓存一致性:
- 在现代CPU(如带有L1缓存的ARM Cortex-M7)上,当CPU向内存写入数据时,数据可能只存在于CPU的缓存(Cache)中,尚未写回主内存(RAM)。此时,如果DMA控制器直接去读取主内存,会读到过时的数据。
- 反之,当DMA控制器向主内存写入数据后,CPU的缓存中可能仍然保留着该内存地址的旧数据。如果CPU直接从缓存读取,就会读到过时的数据。
- “一致性内存”就是为了解决这个问题,确保CPU和DMA控制器对这块内存的读写总是能看到最新的结果。
多路径分配策略 (
dma_alloc_attrs):- 该函数是一个分发器,它按照优先级尝试多种分配策略:
- a) 设备特定一致性内存池 (
dma_alloc_from_dev_coherent): 这是最高优先级的尝试。它会检查设备是否有关联的、预留的“一致性内存池”(通常通过CMA - Contiguous Memory Allocator实现,在设备树中定义)。如果设备可以从这个池中分配,就直接使用。这是一种高效的优化。 - b) 直接映射 (
dma_direct_alloc): 在没有IOMMU的系统上(如STM32H750),这是最常见的路径。dma_alloc_direct函数会返回true。dma_direct_alloc会分配一块物理上连续的内存,并将其属性设置为“非缓存的”(non-cacheable)或“写通的”(write-through)。这样,CPU对这块内存的任何读写都会绕过缓存或立即写回主存,从而在硬件层面保证了与DMA控制器的一致性。 - c) IOMMU路径 (
iommu_dma_alloc): 在有IOMMU(输入/输出内存管理单元)的系统上,IOMMU负责处理DMA的地址翻译和隔离。此路径会分配普通的可缓存内存,然后通过IOMMU为这块内存创建一个对DMA控制器可见的、一致的映射。 - d) 架构特定回调 (
ops->alloc): 这是一个最终的回退路径,允许特定的CPU架构或平台提供自己的、特殊的DMA一致性内存分配实现。
两个地址的返回:
cpu_addr(void *): 这是分配的内存的CPU虚拟地址。驱动程序应该使用这个地址来读写缓冲区。dma_handle(dma_addr_t *): 这是一个输出参数,函数会通过它返回DMA控制器应该使用的总线地址。DMA控制器硬件的源/目的地址寄存器必须被配置为这个值。
释放流程 (
dma_free_attrs):- 释放函数
dma_free_attrs严格镜像了分配函数的决策树。它会按照相同的逻辑顺序检查系统配置,并调用与分配时所用路径相对应的释放函数(dma_direct_free,iommu_dma_free,ops->free等),确保资源被正确地回收。
- 释放函数
代码分析
1 | /** |









