[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-mapping
API负责在这两者之间进行转换。
缓存一致性(Cache Coherency) :当CPU和设备同时访问同一块内存时,可能会因为CPU Cache的存在而导致数据不一致(例如,设备直接写入内存,但CPU的Cache中仍然是旧数据)。dma-mapping
API提供了一系列函数(如dma_sync_*
)来强制管理Cache的刷新和失效,确保数据同步。
连续内存 :DMA操作通常需要物理上连续的内存块。dma-mapping
API提供了分配这种内存的接口(如dma_alloc_coherent
)。
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-mapping
API来管理内存。
核心原理与设计 它的核心工作原理是什么? 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 static inline bool vchan_issue_pending (struct virt_dma_chan *vc) { lockdep_assert_held(&vc->lock); list_splice_tail_init(&vc->desc_submitted, &vc->desc_issued); return !list_empty(&vc->desc_issued); }
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 struct virt_dma_desc *vchan_find_desc (struct virt_dma_chan *vc, dma_cookie_t cookie) { struct virt_dma_desc *vd ; list_for_each_entry(vd, &vc->desc_issued, node) if (vd->tx.cookie == cookie) return vd; return NULL ; } EXPORT_SYMBOL_GPL(vchan_find_desc);
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 static inline struct dma_async_tx_descriptor *vchan_tx_prep (struct virt_dma_chan *vc, struct virt_dma_desc *vd, unsigned long tx_flags) { unsigned long flags; dma_async_tx_descriptor_init(&vd->tx, &vc->chan); vd->tx.flags = tx_flags; vd->tx.tx_submit = vchan_tx_submit; vd->tx.desc_free = vchan_tx_desc_free; vd->tx_result.result = DMA_TRANS_NOERROR; vd->tx_result.residue = 0 ; spin_lock_irqsave(&vc->lock, flags); list_add_tail(&vd->node, &vc->desc_allocated); spin_unlock_irqrestore(&vc->lock, flags); return &vd->tx; }
virt-dma
框架: 描述符的同步、收集与批量清理这一组函数是**virt-dma
(虚拟DMA通道) 框架中负责高级 生命周期管理和 流程控制的API。它们协同工作, 为上层的具体DMA驱动(如STM32 DMA)提供了一套强大的工具, 用于实现 强制终止 (terminate_all
)** 和 同步 (synchronize
) 等复杂操作。
vchan_terminate_vdesc
: 终止描述符并停用其循环回调 (回顾)
核心原理 : 这是一个原子操作, 用于将一个描述符隔离 到desc_terminated
队列, 并切断 其作为循环传输的软件反馈链接 (vc->cyclic = NULL
)。这是终止操作的基础。
1 2 3 4 5 6 7 8 9 10 11 12 13 static inline void vchan_terminate_vdesc (struct virt_dma_desc *vd) { struct virt_dma_chan *vc = to_virt_chan(vd->tx.chan); lockdep_assert_held(&vc->lock); list_add_tail(&vd->node, &vc->desc_terminated); if (vc->cyclic == vd) vc->cyclic = NULL ; }
vchan_get_all_descriptors
: “全员疏散” - 收集所有描述符此函数是一个强大的内部工具, 用于原子地清空一个虚拟通道的所有内部描述符队列 。
核心原理 : 它通过一系列list_splice_tail_init
调用, 将allocated
, submitted
, issued
, completed
, terminated
这五个状态队列中的所有 描述符节点, 一次性地、高效地 “剪切”并”粘贴”到一个外部的临时链表head
中。操作完成后, 虚拟通道的所有内部队列都变为空。这是一个典型的**”先收集, 后处理”**模式, 它允许驱动在锁内快速地收集所有需要清理的资源, 然后在锁外执行耗时的清理操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 static inline void vchan_get_all_descriptors (struct virt_dma_chan *vc, struct list_head *head) { lockdep_assert_held(&vc->lock); list_splice_tail_init(&vc->desc_allocated, head); list_splice_tail_init(&vc->desc_submitted, head); list_splice_tail_init(&vc->desc_issued, head); list_splice_tail_init(&vc->desc_completed, head); list_splice_tail_init(&vc->desc_terminated, head); }
vchan_synchronize
: 同步软件回调并清理”已终止”任务此函数提供了一个阻塞式的同步点 , 主要用于确保所有由软件触发的回调(特别是tasklet
)都已执行完毕 。
核心原理 : 它的同步机制主要依赖于tasklet_kill
。virt-dma
框架使用一个tasklet
(一种软中断)来执行DMA完成回调。tasklet_kill(&vc->task)
会阻塞当前线程, 直到该tasklet执行完成(如果它已被调度) 。这确保了在函数返回时, 没有回调函数正在运行。作为一个附带的清理动作, 它还会安全地收集并释放所有先前被明确terminate
的描述符, 防止内存泄漏。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 static inline void vchan_synchronize (struct virt_dma_chan *vc) { LIST_HEAD(head); unsigned long flags; tasklet_kill(&vc->task); spin_lock_irqsave(&vc->lock, flags); list_splice_tail_init(&vc->desc_terminated, &head); spin_unlock_irqrestore(&vc->lock, flags); vchan_dma_desc_free_list(vc, &head); }
vchan_vdesc_fini
: 描述符的最终裁决 - 重用或释放此函数是**virt-dma
(虚拟DMA通道) 框架中负责一个 描述符生命周期终点**的关键函数。当一个DMA传输完成(或被终止)后, 它的描述符最终会来到这里接受”裁决”。
它的核心原理是根据使用者驱动的意图, 决定一个描述符的最终命运: 是将其“回收”以备下次使用(Re-use), 还是将其彻底“销毁”并释放其内存(Free) 。
这个机制是DMA引擎框架中一项重要的性能优化 。对于需要高频率、重复性地提交相同类型DMA传输的应用(例如, 音频流的每个数据包), 反复地分配和释放描述符内存会带来不小的开销。通过允许描述符重用, 系统可以显著减少内存管理开销, 提高吞吐量。
vchan_vdesc_fini
的工作流程1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 static inline void vchan_vdesc_fini (struct virt_dma_desc *vd) { struct virt_dma_chan *vc = to_virt_chan(vd->tx.chan); if (dmaengine_desc_test_reuse(&vd->tx)) { unsigned long flags; spin_lock_irqsave(&vc->lock, flags); list_add(&vd->node, &vc->desc_allocated); spin_unlock_irqrestore(&vc->lock, flags); } else { vc->desc_free(vd); } }
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 static inline void vchan_cyclic_callback (struct virt_dma_desc *vd) { struct virt_dma_chan *vc = to_virt_chan(vd->tx.chan); vc->cyclic = vd; tasklet_schedule(&vc->task); }
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 2 3 4 5 6 7 8 9 static struct virt_dma_desc *to_virt_desc (struct dma_async_tx_descriptor *tx) { return container_of(tx, struct virt_dma_desc, tx); }
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的分配和队列的移动是一个不可分割的原子操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 dma_cookie_t vchan_tx_submit (struct dma_async_tx_descriptor *tx) { struct virt_dma_chan *vc = to_virt_chan(tx->chan); struct virt_dma_desc *vd = to_virt_desc(tx); unsigned long flags; dma_cookie_t cookie; spin_lock_irqsave(&vc->lock, flags); cookie = dma_cookie_assign(tx); list_move_tail(&vd->node, &vc->desc_submitted); spin_unlock_irqrestore(&vc->lock, flags); dev_dbg(vc->chan.device->dev, "vchan %p: txd %p[%x]: submitted\n" , vc, vd, cookie); return cookie; } EXPORT_SYMBOL_GPL(vchan_tx_submit);
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 int vchan_tx_desc_free (struct dma_async_tx_descriptor *tx) { struct virt_dma_chan *vc = to_virt_chan(tx->chan); struct virt_dma_desc *vd = to_virt_desc(tx); unsigned long flags; spin_lock_irqsave(&vc->lock, flags); list_del(&vd->node); spin_unlock_irqrestore(&vc->lock, flags); dev_dbg(vc->chan.device->dev, "vchan %p: txd %p[%x]: freeing\n" , vc, vd, vd->tx.cookie); vc->desc_free(vd); return 0 ; } EXPORT_SYMBOL_GPL(vchan_tx_desc_free);
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void vchan_dma_desc_free_list (struct virt_dma_chan *vc, struct list_head *head) { struct virt_dma_desc *vd , *_vd ; list_for_each_entry_safe(vd, _vd, head, node) { list_del(&vd->node); vchan_vdesc_fini(vd); } } EXPORT_SYMBOL_GPL(vchan_dma_desc_free_list);
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 static int stm32_gpiolib_register_bank (struct stm32_pinctrl *pctl, struct fwnode_handle *fwnode) { struct stm32_gpio_bank *bank = &pctl->banks[pctl->nbanks]; int bank_ioport_nr; struct pinctrl_gpio_range *range = &bank->range; struct fwnode_reference_args args ; struct device *dev = pctl->dev; struct resource res ; int npins = STM32_GPIO_PINS_PER_BANK; int bank_nr, err, i = 0 ; struct stm32_desc_pin *stm32_pin ; char **names; if (!IS_ERR(bank->rstc)) reset_control_deassert(bank->rstc); if (of_address_to_resource(to_of_node(fwnode), 0 , &res)) return -ENODEV; bank->base = devm_ioremap_resource(dev, &res); if (IS_ERR(bank->base)) return PTR_ERR(bank->base); bank->gpio_chip = stm32_gpio_template; fwnode_property_read_string(fwnode, "st,bank-name" , &bank->gpio_chip.label); if (!fwnode_property_get_reference_args(fwnode, "gpio-ranges" , NULL , 3 , i, &args)) { bank_nr = args.args[1 ] / STM32_GPIO_PINS_PER_BANK; bank->gpio_chip.base = args.args[1 ]; npins = args.args[0 ] + args.args[2 ]; while (!fwnode_property_get_reference_args(fwnode, "gpio-ranges" , NULL , 3 , ++i, &args)) npins = max(npins, (int )(args.args[0 ] + args.args[2 ])); } else { bank_nr = pctl->nbanks; bank->gpio_chip.base = bank_nr * STM32_GPIO_PINS_PER_BANK; range->name = bank->gpio_chip.label; range->id = bank_nr; range->pin_base = range->id * STM32_GPIO_PINS_PER_BANK; range->base = range->id * STM32_GPIO_PINS_PER_BANK; range->npins = npins; range->gc = &bank->gpio_chip; pinctrl_add_gpio_range(pctl->pctl_dev, &pctl->banks[bank_nr].range); } if (fwnode_property_read_u32(fwnode, "st,bank-ioport" , &bank_ioport_nr)) bank_ioport_nr = bank_nr; bank->gpio_chip.base = -1 ; bank->gpio_chip.ngpio = npins; bank->gpio_chip.fwnode = fwnode; bank->gpio_chip.parent = dev; bank->bank_nr = bank_nr; bank->bank_ioport_nr = bank_ioport_nr; bank->secure_control = pctl->match_data->secure_control; bank->rif_control = pctl->match_data->rif_control; spin_lock_init(&bank->lock); if (pctl->domain) { bank->fwnode = fwnode; bank->domain = irq_domain_create_hierarchy(pctl->domain, 0 , STM32_GPIO_IRQ_LINE, bank->fwnode, &stm32_gpio_domain_ops, bank); if (!bank->domain) return -ENODEV; } names = devm_kcalloc(dev, npins, sizeof (char *), GFP_KERNEL); if (!names) return -ENOMEM; for (i = 0 ; i < npins; i++) { stm32_pin = stm32_pctrl_get_desc_pin_from_gpio(pctl, bank, i); if (stm32_pin && stm32_pin->pin.name) { names[i] = devm_kasprintf(dev, GFP_KERNEL, "%s" , stm32_pin->pin.name); if (!names[i]) return -ENOMEM; } else { names[i] = NULL ; } } bank->gpio_chip.names = (const char * const *)names; err = gpiochip_add_data(&bank->gpio_chip, bank); if (err) { dev_err(dev, "Failed to add gpiochip(%d)!\n" , bank_nr); return err; } dev_info(dev, "%s bank added\n" , bank->gpio_chip.label); return 0 ; }
vchan_complete
: 处理虚拟DMA通道的完成任务此函数是一个tasklet
处理程序。Tasklet是Linux内核中的一种延迟工作 机制, 它允许中断处理程序将耗时较长的工作推迟到”软中断上下文”中执行, 从而使硬中断处理程序本身能尽快完成。此函数的核心作用是在一个虚拟DMA通道上, 处理一批已经完成的DMA传输任务 。
它的核心原理是将加锁时间最小化, 实现高效的生产者-消费者模型 :
生产者(中断处理程序) : 当DMA硬件完成一次传输并触发中断时, 中断处理程序(未在此处显示)会做最少的工作: 它将代表已完成传输的”描述符”(virt_dma_desc
)添加到一个共享的”已完成”链表(vc->desc_completed
)中, 然后调度此vchan_complete
tasklet去执行。
消费者(本tasklet) :
快速抓取 : Tasklet开始执行后, 它首先获取通道的自旋锁vc->lock
。然后, 它不是 在锁内逐个处理描述符, 而是调用list_splice_tail_init
这个高效的链表操作, 一次性地将整个共享的”已完成”链表从未清空, 并移动到一个本地的、私有的head
链表中 。
快速释放 : 抓取完成后, 它立即释放自旋锁。这是整个设计的精髓 : 关键的共享数据访问(链表移动)在极短的时间内完成, 极大地降低了锁的争用, 提高了系统性能。
安全处理 : 释放锁之后, tasklet现在可以从容地、安全地遍历它自己的私有head
链表。对于链表中的每一个描述符, 它会:
调用dmaengine_desc_get_callback
获取该传输任务完成时需要通知的回调函数。
调用dmaengine_desc_callback_invoke
执行这个回调, 从而通知提交该DMA任务的驱动程序(例如,一个SPI驱动) “你的数据已经发送/接收完毕”。
调用vchan_vdesc_fini
释放该描述符占用的内存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 static void vchan_complete (struct tasklet_struct *t) { struct virt_dma_chan *vc = from_tasklet(vc, t, task); struct virt_dma_desc *vd , *_vd ; struct dmaengine_desc_callback cb ; LIST_HEAD(head); spin_lock_irq(&vc->lock); list_splice_tail_init(&vc->desc_completed, &head); vd = vc->cyclic; if (vd) { vc->cyclic = NULL ; dmaengine_desc_get_callback(&vd->tx, &cb); } else { memset (&cb, 0 , sizeof (cb)); } spin_unlock_irq(&vc->lock); dmaengine_desc_callback_invoke(&cb, &vd->tx_result); list_for_each_entry_safe(vd, _vd, &head, node) { dmaengine_desc_get_callback(&vd->tx, &cb); list_del(&vd->node); dmaengine_desc_callback_invoke(&cb, &vd->tx_result); vchan_vdesc_fini(vd); } }
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 void vchan_init (struct virt_dma_chan *vc, struct dma_device *dmadev) { dma_cookie_init(&vc->chan); spin_lock_init(&vc->lock); INIT_LIST_HEAD(&vc->desc_allocated); INIT_LIST_HEAD(&vc->desc_submitted); INIT_LIST_HEAD(&vc->desc_issued); INIT_LIST_HEAD(&vc->desc_completed); INIT_LIST_HEAD(&vc->desc_terminated); tasklet_setup(&vc->task, vchan_complete); vc->chan.device = dmadev; list_add_tail(&vc->chan.device_node, &dmadev->channels); } EXPORT_SYMBOL_GPL(vchan_init);
drivers/dma/dmaengine.h DMA Cookie 函数族: DMA传输生命周期的核心追踪机制 这一组定义在头文件中的静态内联函数共同构成了一个高效、轻量级且线程安全的框架, 用于追踪Linux内核中异步DMA传输的生命周期 。它们是通用DMA引擎(DMA Engine)子系统的基石, STM32 DMA驱动等具体实现都依赖这个框架来管理传输任务。
其核心原理是实现了一个**”票据系统” (ticket system)**:
初始化 (dma_cookie_init
) : DMA通道像一个票据分发机, 初始化时将”已分发”和”已完成”的票号都重置为起始值。
分配 (dma_cookie_assign
) : 每当一个新的DMA传输任务(由dma_async_tx_descriptor
表示)被提交时, 就会从分发机取一张新的、唯一的、顺序递增的票(cookie), 并将这张票贴在任务上。同时, 分发机记下”最后分发的票号”。
完成 (dma_cookie_complete
) : 当DMA硬件完成一个任务并触发中断时, 中断处理程序会拿出完成任务上的票, 并更新一个公告板, 上面写着”最后完成的票号”。
查询 (dma_cookie_status
) : 任何时候, 任何人都可以拿着自己手里的票号去和公告板上的”最后完成票号”以及分发机的”最后分发票号”做比较。通过比较, 就可以确定这个任务是已经完成、正在处理、还是仍在排队。
这个系统巧妙地利用了单调递增的整数, 使得状态查询可以无锁 (lock-free) 执行, 极大地提高了性能。
dma_cookie_init
: 初始化通道票据系统1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static inline void dma_cookie_init (struct dma_chan *chan) { chan->cookie = DMA_MIN_COOKIE; chan->completed_cookie = DMA_MIN_COOKIE; }
dma_cookie_assign
: 为传输任务分配唯一票据(Cookie)1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static inline dma_cookie_t dma_cookie_assign (struct dma_async_tx_descriptor *tx) { struct dma_chan *chan = tx->chan; dma_cookie_t cookie; cookie = chan->cookie + 1 ; if (cookie < DMA_MIN_COOKIE) cookie = DMA_MIN_COOKIE; tx->cookie = cookie; chan->cookie = cookie; return cookie; }
dma_cookie_complete
: 标记一个传输任务已完成1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static inline void dma_cookie_complete (struct dma_async_tx_descriptor *tx) { BUG_ON(tx->cookie < DMA_MIN_COOKIE); tx->chan->completed_cookie = tx->cookie; tx->cookie = 0 ; }
dma_cookie_status
: 无锁查询传输状态1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 static inline enum dma_status dma_cookie_status (struct dma_chan *chan, dma_cookie_t cookie, struct dma_tx_state *state) { dma_cookie_t used, complete; used = chan->cookie; complete = chan->completed_cookie; barrier(); if (state) { state->last = complete; state->used = used; state->residue = 0 ; state->in_flight_bytes = 0 ; } return dma_async_is_complete(cookie, complete, used); }
dma_set_residue
和 dma_set_in_flight_bytes
: 状态设置辅助函数1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static inline void dma_set_residue (struct dma_tx_state *state, u32 residue) { if (state) state->residue = residue; } static inline void dma_set_in_flight_bytes (struct dma_tx_state *state, u32 in_flight_bytes) { if (state) state->in_flight_bytes = in_flight_bytes; }
DMA引擎回调辅助函数集 此代码片段定义了一组静态内联函数, 它们是Linux内核DMA引擎(DMA Engine)框架的通用辅助工具。它们的核心原理是提供一套标准化的、安全的、并且向后兼容的机制, 用于处理DMA传输完成后的回调操作 。
当一个设备驱动(例如SPI驱动)请求一次DMA传输后, 它会提供一个”回调函数”。当DMA控制器硬件完成传输并触发中断时, DMA控制器驱动需要调用这个回调函数来通知原始的设备驱动”你的数据传输已完成”。这组辅助函数就是为了让这个通知过程变得更加简洁、健壮和统一。
struct dmaengine_desc_callback
: DMA引擎描述符回调信息结构体这是一个纯粹的数据结构, 作为一个临时容器来存储一次回调所需的所有信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 struct dmaengine_desc_callback { dma_async_tx_callback callback; dma_async_tx_callback_result callback_result; void *callback_param; };
dmaengine_desc_get_callback
: 获取DMA描述符中的回调信息此函数用于从一个DMA传输描述符中安全地复制出回调信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 static inline void dmaengine_desc_get_callback (struct dma_async_tx_descriptor *tx, struct dmaengine_desc_callback *cb) { cb->callback = tx->callback; cb->callback_result = tx->callback_result; cb->callback_param = tx->callback_param; }
dmaengine_desc_callback_invoke
: 调用回调函数此函数负责执行存储在临时结构体中的回调函数, 并处理了新旧两种回调类型的兼容性问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 static inline void dmaengine_desc_callback_invoke (struct dmaengine_desc_callback *cb, const struct dmaengine_result *result) { struct dmaengine_result dummy_result = { .result = DMA_TRANS_NOERROR, .residue = 0 }; if (cb->callback_result) { if (!result) result = &dummy_result; cb->callback_result(cb->callback_param, result); } else if (cb->callback) { cb->callback(cb->callback_param); } }
dmaengine_desc_get_callback_invoke
: 获取并立即调用DMA回调这是一个便利的封装函数, 将获取和调用两个步骤合二为一。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 static inline void dmaengine_desc_get_callback_invoke (struct dma_async_tx_descriptor *tx, const struct dmaengine_result *result) { struct dmaengine_desc_callback cb ; dmaengine_desc_get_callback(tx, &cb); dmaengine_desc_callback_invoke(&cb, result); }
dmaengine_desc_callback_valid
: 检查回调是否有效这是一个简单的检查函数, 用于判断一个描述符是否有关联的完成回调。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 static inline bool dmaengine_desc_callback_valid (struct dmaengine_desc_callback *cb) { return cb->callback || cb->callback_result; }
drivers/dma/dmaengine.c DMA引擎 事务类型与通道查找表 此代码片段属于Linux内核DMA引擎(dmaengine
)子系统 的核心部分。它的核心作用是定义DMA引擎支持的所有操作类型, 并创建一个全局的、高性能的查找表(channel_table
), 以便内核可以快速地为特定的DMA任务(如内存拷贝、异或计算)找到一个最合适的DMA通道 。
该机制最关键的特性是它为每个CPU核心都维护一个独立的查找表副本 。这一原理是通过__percpu
关键字实现的, 它的主要目的是在多核系统中避免锁竞争 , 从而极大地提升性能。当一个CPU上的驱动程序需要一个DMA通道时, 它可以直接访问自己私有的查找表, 无需与其它CPU同步。
原理与工作流程:
enum dma_transaction_type
(DMA事务类型) : 这个枚举定义了dmaengine
框架所能理解的所有标准DMA操作。这包括:
简单的内存操作: DMA_MEMCPY
(内存拷贝), DMA_MEMSET
(内存填充)。
复杂的计算卸载: DMA_XOR
, DMA_PQ
(常用于RAID加速)。
外设相关的传输类型: DMA_SLAVE
(例如, 从SPI外设到内存), DMA_CYCLIC
(用于音频等循环缓冲)。
这些枚举值是DMA控制器驱动和DMA客户端驱动之间沟通能力的”通用语言”。
channel_table
(通道查找表) : 这是一个全局数组, 但被__percpu
修饰。
channel_table[DMA_MEMCPY]
这一项专门用来存放能执行”内存拷贝”的DMA通道。
__percpu
意味着每个CPU核心都有自己独立的channel_table
副本。CPU 0写入channel_table
的数据不会影响CPU 1的副本。
dma_channel_table_init()
(初始化函数) :
这是一个在内核启动早期通过arch_initcall
调用的初始化函数。
它首先初始化一个dma_cap_mask_all
位掩码, 初始时包含所有定义的操作类型。
然后, 它从这个掩码中移除 了一些不代表具体”内存到内存”操作的类型, 如DMA_SLAVE
。因为这个查找表专门用于优化memcpy
, xor
等”计算卸载”类操作的通道查找, 而DMA_SLAVE
有其自己的查找机制。
最关键的一步是循环, 它遍历所有有效的操作类型(cap
), 并为每一种类型调用alloc_percpu()
。这个调用为channel_table[cap]
在每一个CPU核心上 都分配了内存。
函数包含了完善的错误处理, 如果在初始化过程中内存不足, 它会释放所有已分配的资源并报错。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 enum dma_transaction_type { DMA_MEMCPY, DMA_XOR, DMA_PQ, DMA_SLAVE, DMA_CYCLIC, DMA_TX_TYPE_END, }; static dma_cap_mask_t dma_cap_mask_all;struct dma_chan_tbl_ent { struct dma_chan *chan ; }; static struct dma_chan_tbl_ent __percpu *channel_table [DMA_TX_TYPE_END ];static int __init dma_channel_table_init (void ) { enum dma_transaction_type cap ; int err = 0 ; bitmap_fill(dma_cap_mask_all.bits, DMA_TX_TYPE_END); clear_bit(DMA_INTERRUPT, dma_cap_mask_all.bits); clear_bit(DMA_PRIVATE, dma_cap_mask_all.bits); clear_bit(DMA_SLAVE, dma_cap_mask_all.bits); for_each_dma_cap_mask(cap, dma_cap_mask_all) { channel_table[cap] = alloc_percpu(struct dma_chan_tbl_ent); if (!channel_table[cap]) { err = -ENOMEM; break ; } } if (err) { pr_err("dmaengine dma_channel_table_init failure: %d\n" , err); for_each_dma_cap_mask(cap, dma_cap_mask_all) free_percpu(channel_table[cap]); } return err; } arch_initcall(dma_channel_table_init);
DMA引擎 Unmap内存池与总线初始化 此代码片段展示了Linux内核DMA引擎(dmaengine
)子系统 在启动时进行的两个关键初始化步骤: 1) 创建一组用于管理DMA”unmap”操作元数据的内存池(Memory Pools) , 2) 注册dma
设备类, 为后续的DMA控制器设备创建/sys/class/dma/
目录。
这部分代码的核心原理是通过预分配机制来解决在中断上下文中进行内存分配的难题, 并为不同复杂度的DMA传输提供内存优化的解决方案 。
dmaengine_init_unmap_pool
: 初始化DMA Unmap内存池1. 问题背景: 为什么需要内存池? 当一个驱动程序提交一个DMA传输任务时, dmaengine
框架需要一个数据结构来存储这次传输的相关信息, 特别是所有需要被DMA控制器访问的内存缓冲区的地址。这些缓冲区在传输完成后必须被”unmap”, 以便CPU可以再次安全地访问它们。这个”unmap”操作通常是在DMA传输完成中断的处理程序中触发的。
关键约束 : 内核的中断处理程序绝对不能睡眠 。而常规的内存分配函数kmalloc(..., GFP_KERNEL)
在系统内存不足时可能会睡眠等待。因此, 在中断上下文中直接调用kmalloc
是禁止的, 否则会导致系统死锁或崩溃。
2. 解决方案: mempool_t
(内存池) 内存池是解决这个问题的标准内核机制。它的原理是:
在系统启动时(此时可以安全地睡眠), 就预先分配 一批固定大小的内存对象。
将这些预分配的对象存放在一个”池子”里。
当驱动程序在不能睡眠的上下文(如中断处理)中需要一个对象时, 它可以从池子中无阻塞地、保证成功地 获取一个。
当对象使用完毕后, 再将其归还到池子中, 而不是立即释放给系统。
3. dmaengine_init_unmap_pool
的工作流程: 此函数就是建立这一套内存池系统的过程。
定义多种大小的池 : unmap_pool[]
数组定义了多个不同大小的池。例如, __UNMAP_POOL(2)
用于存储需要unmap两个地址的简单传输(如一个源, 一个目的)。而__UNMAP_POOL(128)
则用于需要unmap 128个地址的复杂”分散-聚集”(scatter-gather)传输, 这在RAID计算中很常见。这种分池策略避免了为简单任务分配过大的元数据结构, 从而节省了内存。
创建SLAB缓存(kmem_cache
) : 在循环中, 对于每一种池大小, 它首先调用kmem_cache_create
。kmem_cache
是Linux SLAB/SLUB分配器的核心, 它是一个专门用来高效分配和释放大量同尺寸对象的高速缓存。这本身就是一种性能优化。
创建内存池(mempool_t
) : 然后, 它在创建好的kmem_cache
之上, 调用mempool_create_slab_pool
来创建真正的内存池。这个内存池会从SLAB缓存中预取一批对象备用。
错误处理 : 函数包含了健壮的错误处理。如果在创建任何一个池的过程中失败, 它会调用dmaengine_destroy_unmap_pool()
(未在此片段中显示)来清理所有已经成功创建的池, 确保系统不会处于一个不一致的状态。
dma_bus_init
: DMA总线初始化这是一个更高层的初始化函数, 它编排了dmaengine
子系统的基础设置。
它首先调用dmaengine_init_unmap_pool()
来确保内存池被优先创建, 因为它们是dmaengine
后续所有操作的基础。
如果内存池创建成功, 它会调用class_register(&dma_devclass)
。这个函数会在/sys
文件系统中创建/sys/class/dma/
。之后, 当具体的DMA控制器驱动(如STM32的DMA驱动)注册其设备时, 对应的设备节点就会出现在这个目录下, 例如/sys/class/dma/dma0chan1
。
最后, 它初始化dmaengine
的debugfs
接口, 为调试提供便利。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 struct dmaengine_unmap_pool { struct kmem_cache *cache ; const char *name; mempool_t *pool; size_t size; }; #define __UNMAP_POOL(x) { .size = x, .name = "dmaengine-unmap-" __stringify(x) } static struct dmaengine_unmap_pool unmap_pool [] = { __UNMAP_POOL(2 ), #if IS_ENABLED(CONFIG_DMA_ENGINE_RAID) __UNMAP_POOL(16 ), __UNMAP_POOL(128 ), __UNMAP_POOL(256 ), #endif }; static int __init dmaengine_init_unmap_pool (void ) { int i; for (i = 0 ; i < ARRAY_SIZE(unmap_pool); i++) { struct dmaengine_unmap_pool *p = &unmap_pool[i]; size_t size; size = sizeof (struct dmaengine_unmap_data) + sizeof (dma_addr_t ) * p->size; p->cache = kmem_cache_create(p->name, size, 0 , SLAB_HWCACHE_ALIGN, NULL ); if (!p->cache) break ; p->pool = mempool_create_slab_pool(1 , p->cache); if (!p->pool) break ; } if (i == ARRAY_SIZE(unmap_pool)) return 0 ; dmaengine_destroy_unmap_pool(); return -ENOMEM; } static int __init dma_bus_init (void ) { int err = dmaengine_init_unmap_pool(); if (err) return err; err = class_register(&dma_devclass); if (!err) dmaengine_debugfs_init(); return err; } arch_initcall(dma_bus_init);
dma_async_tx_descriptor_init
: 初始化DMA传输描述符此函数是通用DMA引擎(DMA Engine)框架中的一个基础”构造函数”。它的核心原理是为一个新创建的DMA传输描述符 (dma_async_tx_descriptor
) 执行最基本、最必要的初始化步骤, 主要是将其与它所属的DMA通道 (dma_chan
) 牢固地关联起来 。
可以把它看作是给一张空白的DMA任务单盖上”所属部门”的印章。在执行这个操作之前, 描述符只是一块内存; 执行之后, 它就正式成为了某个特定DMA通道的一个待处理任务。
这是一个非常轻量级的函数, 但它的作用至关重要, 因为描述符与通道的关联是后续所有DMA操作(提交、启动、查询状态)的基础。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 void dma_async_tx_descriptor_init (struct dma_async_tx_descriptor *tx, struct dma_chan *chan) { tx->chan = chan; #ifdef CONFIG_ASYNC_TX_ENABLE_CHANNEL_SWITCH spin_lock_init(&tx->lock); #endif } EXPORT_SYMBOL(dma_async_tx_descriptor_init);
__dma_async_device_channel_register: 注册单个DMA通道并创建其sysfs接口 此函数是dma_async_device_register
内部使用的一个核心辅助函数。它的作用是将一个由DMA控制器驱动程序定义的、代表硬件DMA通道的struct dma_chan
实例, 完全地、正式地注册到内核中 。
其核心原理是为这个抽象的DMA通道创建一个具体的软件实体, 使其对内核的其他部分(特别是设备模型和sysfs)可见。这个过程包括三个关键步骤:
资源分配 : 为通道分配必要的软件资源, 包括用于存储运行时状态的per-CPU数据(chan->local
)和一个用于在sysfs中表示该通道的struct dma_channel_dev
结构(chan->dev
)。
身份分配 : 调用IDA(ID Allocator)机制, 从其父DMA设备的ID池中为该通道分配一个唯一的、局部的通道ID号。
Sysfs注册 : 填充一个标准的struct device
结构, 设置其类别为dma
, 父设备为DMA控制器设备, 并根据设备ID和通道ID构建一个唯一的名称(例如dma0chan1
)。最后, 调用device_register
将这个设备正式注册到内核的设备模型中, 这会在/sys/class/dma/
目录下创建一个对应的条目。
这个函数通过goto
语句实现了非常健壮的错误处理流程。如果在注册过程中的任何一步失败, 它都会精确地回滚(undo)所有已经成功执行的步骤, 例如释放已分配的ID和内存, 确保系统不会因部分失败的注册而遗留任何悬空资源。在STM32H750这样的单核系统上, alloc_percpu
虽然是为多核设计的, 但它会优雅地退化为只分配一个实例, 保持了代码的可移植性和一致性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 static int __dma_async_device_channel_register(struct dma_device *device, struct dma_chan *chan, const char *name) { int rc; chan->local = alloc_percpu(typeof (*chan->local)); if (!chan->local) return -ENOMEM; chan->dev = kzalloc(sizeof (*chan->dev), GFP_KERNEL); if (!chan->dev) { rc = -ENOMEM; goto err_free_local; } chan->chan_id = ida_alloc(&device->chan_ida, GFP_KERNEL); if (chan->chan_id < 0 ) { pr_err("%s: unable to alloc ida for chan: %d\n" , __func__, chan->chan_id); rc = chan->chan_id; goto err_free_dev; } chan->dev->device.class = &dma_devclass; chan->dev->device.parent = device->dev; chan->dev->chan = chan; chan->dev->dev_id = device->dev_id; if (!name) dev_set_name(&chan->dev->device, "dma%dchan%d" , device->dev_id, chan->chan_id); else dev_set_name(&chan->dev->device, "%s" , name); rc = device_register(&chan->dev->device); if (rc) goto err_out_ida; chan->client_count = 0 ; device->chancnt++; return 0 ; err_out_ida: ida_free(&device->chan_ida, chan->chan_id); err_free_dev: kfree(chan->dev); err_free_local: free_percpu(chan->local); chan->local = NULL ; return rc; }
dma_channel_rebalance: 重新平衡并分配全局DMA通道 此函数是Linux DMA引擎框架的核心调度器和负载均衡器 。它的主要作用是构建和更新一个名为channel_table
的全局快速查找表。这个表的存在, 使得当一个客户端驱动请求一个特定功能(如内存拷贝)的DMA通道时, 内核可以立即 为其提供一个当前最优的选择, 而无需在每次请求时都去遍历系统中所有已注册的DMA控制器和通道。
该函数的原理可以概括为**”先拆毁, 再重建”**的负载均衡策略:
完全重置 (Teardown) : 函数首先进入一个”拆毁”阶段, 为即将进行的重新分配清理环境。
它遍历整个channel_table
, 将其中每一个条目都清空为NULL
。这确保了旧的、可能已过时的分配关系被完全抹除。
它遍历系统中所有公共的 (非DMA_PRIVATE
)DMA通道, 将它们各自的table_count
(一个用于衡量该通道被分配了多少次任务的”负载计数器”)清零。
优化检查 : 在重建之前, 它会检查一个全局引用计数dmaengine_ref_count
。如果这个计数为零, 意味着当前系统中没有任何客户端驱动正在使用或等待DMA通道。在这种情况下, 填充查找表是毫无意义的, 函数会提前退出, 这是一个重要的性能优化。
重新分配 (Rebuild) : 这是函数的核心逻辑。它会系统地、从头开始地重建channel_table
。
它遍历所有可能的DMA能力(cap
, 如DMA_MEMCPY
, DMA_XOR
, DMA_CYCLIC
等)。
对于每一种能力, 它会遍历所有当前在线的 CPU核心(cpu
)。
在循环的内部, 它调用一个关键的辅助函数min_chan(cap, cpu)
。这个函数是负载均衡算法的实现者, 它会搜索整个系统中所有可用的公共DMA通道, 找出那个**支持当前能力(cap
)并且 当前负载最低(table_count
最小)**的通道。
最后, 它将min_chan
找到的最优通道, 填入channel_table[cap][cpu]
这个槽位中。
对于用户指定的STM32H750(单核, 非SMP)架构, 此函数的行为会相应地简化和调整 :
所有for_each_*_cpu
循环只会迭代一次, cpu
的值始终为0。
函数的目标从文档注释中描述的”CPU隔离”转变为**”操作隔离”。这意味着, dma_channel_rebalance
会尝试为 每一种不同类型的DMA操作分配一个 不同的、专用的DMA通道**(如果硬件资源允许的话)。例如, 它可能会将dma0chan1
分配给DMA_MEMCPY
操作(channel_table[DMA_MEMCPY][0] = &dma0chan1
), 同时将dma0chan2
分配给DMA_CYCLIC
操作(channel_table[DMA_CYCLIC][0] = &dma0chan2
)。这样做可以避免让同一个硬件通道在不同类型的任务之间频繁切换上下文, 从而简化驱动逻辑并可能提高性能。
此函数必须在dma_list_mutex
锁的保护下调用, 因为它读取和修改了多个全局共享的数据结构(channel_table
, dma_device_list
等), 这个锁确保了整个”再平衡”操作的原子性和线程安全性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 static void dma_channel_rebalance (void ) { struct dma_chan *chan ; struct dma_device *device ; int cpu; int cap; for_each_dma_cap_mask(cap, dma_cap_mask_all) for_each_possible_cpu(cpu) per_cpu_ptr(channel_table[cap], cpu)->chan = NULL ; list_for_each_entry(device, &dma_device_list, global_node) { if (dma_has_cap(DMA_PRIVATE, device->cap_mask)) continue ; list_for_each_entry(chan, &device->channels, device_node) chan->table_count = 0 ; } if (!dmaengine_ref_count) return ; for_each_dma_cap_mask(cap, dma_cap_mask_all) for_each_online_cpu(cpu) { chan = min_chan(cap, cpu); per_cpu_ptr(channel_table[cap], cpu)->chan = chan; } }
dma_async_device_register: 注册一个DMA控制器设备 此函数是Linux内核DMA引擎(DMA Engine)框架的核心入口点 。当一个DMA控制器的硬件驱动(例如STM32H750的DMA或MDMA驱动)在探测(probe
)过程中完成了自身的初始化后, 就会调用此函数, 将其所代表的DMA控制器及其所有的通道(channels)正式地、完整地注册到内核中。完成此调用后, 该DMA控制器就对系统的其他部分(即”客户端”驱动, 如SPI, I2C, Crypto等)变得可见、可发现和可使用 。
其核心原理是一个全面的验证、初始化和集成过程 , 确保只有功能完整、行为正确的DMA驱动才能被系统接受:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 int dma_async_device_register (struct dma_device *device) { int rc; struct dma_chan * chan ; if (!device) return -ENODEV; if (!device->dev) { pr_err("DMAdevice must have dev\n" ); return -EIO; } device->owner = device->dev->driver->owner; #define CHECK_CAP(_name, _type) \ { \ if (dma_has_cap(_type, device->cap_mask) && !device->device_prep_##_name) { \ dev_err(device->dev, \ "Device claims capability %s, but op is not defined\n" , \ __stringify(_type)); \ return -EIO; \ } \ } CHECK_CAP(dma_memcpy, DMA_MEMCPY); CHECK_CAP(dma_xor, DMA_XOR); CHECK_CAP(dma_xor_val, DMA_XOR_VAL); CHECK_CAP(dma_pq, DMA_PQ); CHECK_CAP(dma_pq_val, DMA_PQ_VAL); CHECK_CAP(dma_memset, DMA_MEMSET); CHECK_CAP(dma_interrupt, DMA_INTERRUPT); CHECK_CAP(dma_cyclic, DMA_CYCLIC); CHECK_CAP(interleaved_dma, DMA_INTERLEAVE); #undef CHECK_CAP if (!device->device_tx_status) { dev_err(device->dev, "Device tx_status is not defined\n" ); return -EIO; } if (!device->device_issue_pending) { dev_err(device->dev, "Device issue_pending is not defined\n" ); return -EIO; } if (!device->device_release) dev_dbg(device->dev, "WARN: Device release is not defined so it is not safe to unbind this driver while in use\n" ); kref_init(&device->ref); if (device_has_all_tx_types(device)) dma_cap_set(DMA_ASYNC_TX, device->cap_mask); rc = get_dma_id(device); if (rc != 0 ) return rc; ida_init(&device->chan_ida); list_for_each_entry(chan, &device->channels, device_node) { rc = __dma_async_device_channel_register(device, chan, NULL ); if (rc < 0 ) goto err_out; } mutex_lock(&dma_list_mutex); if (dmaengine_ref_count && !dma_has_cap(DMA_PRIVATE, device->cap_mask)) list_for_each_entry(chan, &device->channels, device_node) { if (dma_chan_get(chan) == -ENODEV) { rc = -ENODEV; mutex_unlock(&dma_list_mutex); goto err_out; } } list_add_tail_rcu(&device->global_node, &dma_device_list); if (dma_has_cap(DMA_PRIVATE, device->cap_mask)) device->privatecnt++; dma_channel_rebalance(); mutex_unlock(&dma_list_mutex); dmaengine_debug_register(device); return 0 ; err_out: if (!device->chancnt) { ida_free(&dma_ida, device->dev_id); return rc; } list_for_each_entry(chan, &device->channels, device_node) { if (chan->local == NULL ) continue ; mutex_lock(&dma_list_mutex); chan->dev->chan = NULL ; mutex_unlock(&dma_list_mutex); device_unregister(&chan->dev->device); free_percpu(chan->local); } return rc; } EXPORT_SYMBOL(dma_async_device_register);
dma_chan_get: 安全地获取并声明一个DMA通道的所有权 此函数是Linux DMA引擎框架内部的一个核心资源管理函数 。它的主要作用是在一个客户端驱动程序正式开始使用一个DMA通道之前, 执行一个多层次的、健壮的资源获取和引用计数流程 。它不仅仅是返回一个指针, 而是作为一个”最终的守门员”, 确保与该通道关联的所有底层资源(内核模块、DMA设备、通道特定内存)都已准备就绪, 并且其生命周期已被正确管理。
此函数被设计为在dma_list_mutex
锁的保护下调用, 其核心原理是为一个即将被激活的DMA通道, 安全地”上线”其所有依赖资源, 并更新其使用状态 。
这个过程分为两种主要路径:
通道已被占用 (共享路径) :
如果chan->client_count
大于0, 意味着这个通道已经被一个或多个客户端驱动占用。在这种情况下, 它只执行两个简单的引用计数操作: __module_get
增加DMA控制器驱动模块的引用计数, chan->client_count++
增加本通道的客户端计数。这允许多个客户端共享同一个通道(如果硬件和驱动支持)。
通道首次被使用 (独占或首次获取路径) :
如果chan->client_count
为0, 函数会执行一个完整、严谨的”上线”流程:
模块存活检查 : 它首先调用try_module_get
。这是第一道、也是最关键的一道防线。它尝试增加DMA控制器驱动模块的引用计数, 但如果该模块正在被卸载(rmmod
), 这个操作会失败。这从根本上杜绝了在驱动卸载过程中仍能成功获取其硬件资源的竞态条件, 是保证系统稳定性的关键。
设备存活检查 : 接下来, 它调用kref_get_unless_zero
来增加DMA控制器设备 的引用计数。这确保了DMA设备本身不是正在被释放的过程中, 防止了”use-after-free”类型的错误。
按需资源分配 : 它会检查并调用DMA驱动提供的可选回调函数device_alloc_chan_resources
。这是一个重要的内存优化 机制。它允许DMA驱动将那些只有在通道被实际使用时才需要的、可能很消耗内存的资源(如DMA描述符池)的分配工作, 推迟 到通道首次被获取时才执行, 而不是在驱动初始化时就为所有通道预分配。
状态更新 : 在所有检查和分配都成功后, 它才将chan->client_count
从0增加到1, 正式将该通道标记为”使用中”。
全局平衡通知 : 最后, 对于非私有通道, 它调用balance_ref_count
。这个函数会通知DMA引擎的全局调度器(dma_channel_rebalance
), 系统中现在有了一个活跃的客户端, 这可能会触发一次全局的DMA通道负载均衡计算。
在STM32H750这样的单核系统上, 即使不存在多核并发, 此函数及其锁机制依然至关重要。dma_list_mutex
可以防止在多个设备驱动的探测(probe
)函数并发执行时(由于任务抢占)对全局DMA状态产生竞争。而按需的资源分配机制对于资源相对有限的嵌入式系统来说, 是一个非常有价值的特性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 static int dma_chan_get (struct dma_chan *chan) { struct module *owner = dma_chan_to_owner(chan); int ret; if (chan->client_count) { __module_get(owner); chan->client_count++; return 0 ; } if (!try_module_get(owner)) return -ENODEV; ret = kref_get_unless_zero(&chan->device->ref); if (!ret) { ret = -ENODEV; goto module_put_out; } if (chan->device->device_alloc_chan_resources) { ret = chan->device->device_alloc_chan_resources(chan); if (ret < 0 ) goto err_out; } chan->client_count++; if (!dma_has_cap(DMA_PRIVATE, chan->device->cap_mask)) balance_ref_count(chan); return 0 ; err_out: dma_device_put(chan->device); module_put_out: module_put(owner); return ret; }
dma_get_slave_channel: 独占性地获取一个指定的DMA通道 此函数是Linux DMA引擎框架中一个非常特殊且重要 的API。它的核心作用是允许一个驱动程序尝试以独占的方式, 获取一个它已经通过其他方式(例如, of_xlate
函数)识别出的、确切的物理DMA通道 。
与通用的dma_request_channel
(它会根据能力自动选择一个可用的通道)不同, 此函数的目标是”我需要这一个 通道, 而且在我使用期间, 不希望通用的分配系统再把它分配给别人”。
其核心原理是通过一个巧妙的”私有化”机制来确保独占性 :
锁定全局状态 : 函数首先获取dma_list_mutex
全局互斥锁。这是至关重要的, 因为它即将修改一个DMA控制器设备的全局可见状态。
检查可用性 : 它做的第一件事, 也是最关键的检查, 是chan->client_count == 0
。这个计数器记录了当前有多少个”客户端”正在使用这个通道。如果计数不为0, 意味着该通道已经被占用了, 独占请求立即失败。
执行”私有化” : 如果通道当前是空闲的, 函数会执行一系列原子操作来”保留”它:
dma_cap_set(DMA_PRIVATE, device->cap_mask);
: 这是整个机制的核心。它会给该通道所属的整个DMA控制器设备 打上DMA_PRIVATE
(私有)的标志。这个标志就像一个”请勿打扰”的牌子, 它会告诉通用的dma_request_channel
函数:”这个DMA控制器当前处于私有模式, 不要再从中自动分配任何通道给新的通用请求”。
device->privatecnt++;
: 它会增加该DMA控制器设备上的私有通道计数器。这允许多个通道被同一个或不同的驱动以这种方式独占。
err = dma_chan_get(chan);
: 它调用一个内部函数来正式地”获取”这个通道, 这通常会使chan->client_count
从0变为1。
健壮的错误回滚 : 如果在正式获取通道时(虽然不太可能)发生错误, 它会执行一个精确的回滚操作 : 它会递减privatecnt
计数器。如果这个计数器减到0, 意味着这是该设备上最后一个被独占的通道, 那么它就会调用dma_cap_clear(DMA_PRIVATE, ...)
来移除 整个设备的”私有”标志, 使其重新对通用分配系统开放。
释放锁并返回 : 完成所有操作后, 它会释放全局锁, 并返回结果——成功时是原始的通道指针, 失败时是NULL
。
在STM32H750这样的系统中, 这个函数通常是在stm32_dma_of_xlate
函数的内部被调用的。of_xlate
从设备树中精确地识别出了客户端驱动需要的硬件通道(例如dma1, stream 7
), 然后它就会调用dma_get_slave_channel
来独占性地获取这个通道的软件句柄, 并返回给客户端驱动。即使是在单核系统中, dma_list_mutex
锁也是必不可少的, 因为它能防止在多个设备驱动的探测(probe
)函数并发执行时(由于任务抢占)对全局DMA状态产生竞争。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 struct dma_chan *dma_get_slave_channel (struct dma_chan *chan) { mutex_lock(&dma_list_mutex); if (chan->client_count == 0 ) { struct dma_device *device = chan->device; int err; dma_cap_set(DMA_PRIVATE, device->cap_mask); device->privatecnt++; err = dma_chan_get(chan); if (err) { dev_dbg(chan->device->dev, "%s: failed to get %s: (%d)\n" , __func__, dma_chan_name(chan), err); chan = NULL ; if (--device->privatecnt == 0 ) dma_cap_clear(DMA_PRIVATE, device->cap_mask); } } else { chan = NULL ; } mutex_unlock(&dma_list_mutex); return chan; } EXPORT_SYMBOL_GPL(dma_get_slave_channel);
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 static struct of_dma *of_dma_find_controller (const struct of_phandle_args *dma_spec) { struct of_dma *ofdma ; list_for_each_entry(ofdma, &of_dma_list, of_dma_controllers) if (ofdma->of_node == dma_spec->np) return ofdma; pr_debug("%s: can't find DMA controller %pOF\n" , __func__, dma_spec->np); return NULL ; }
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控制器的有效请求。
第二阶段: 查找最终目标, 转发重写后的请求 。
函数使用这个被重写过的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
回调函数来断开硬件连接, 完美地完成了资源的自动清理。
错误处理 : 这个函数包含了极为健壮的错误处理。如果在第二或第三阶段失败, 它会立即回头调用路由器驱动的route_free
函数, 以确保在第一阶段已经建立的硬件连接被立即拆除, 防止了硬件资源泄漏。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 static struct dma_chan *of_dma_router_xlate (struct of_phandle_args *dma_spec, struct of_dma *ofdma) { struct dma_chan *chan ; struct of_dma *ofdma_target ; struct of_phandle_args dma_spec_target ; void *route_data; memcpy (&dma_spec_target, dma_spec, sizeof (dma_spec_target)); route_data = ofdma->of_dma_route_allocate(&dma_spec_target, ofdma); if (IS_ERR(route_data)) return NULL ; ofdma_target = of_dma_find_controller(&dma_spec_target); if (!ofdma_target) { ofdma->dma_router->route_free(ofdma->dma_router->dev, route_data); chan = ERR_PTR(-EPROBE_DEFER); goto err; } chan = ofdma_target->of_dma_xlate(&dma_spec_target, ofdma_target); if (IS_ERR_OR_NULL(chan)) { ofdma->dma_router->route_free(ofdma->dma_router->dev, route_data); } else { int ret = 0 ; chan->router = ofdma->dma_router; chan->route_data = route_data; if (chan->device->device_router_config) ret = chan->device->device_router_config(chan); if (ret) { dma_release_channel(chan); chan = ERR_PTR(ret); } } err: of_node_put(dma_spec_target.np); return chan; }
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 int of_dma_router_register (struct device_node *np, void *(*of_dma_route_allocate) (struct of_phandle_args *, struct of_dma *), struct dma_router *dma_router) { struct of_dma *ofdma ; if (!np || !of_dma_route_allocate || !dma_router) { pr_err("%s: not enough information provided\n" , __func__); return -EINVAL; } ofdma = kzalloc(sizeof (*ofdma), GFP_KERNEL); if (!ofdma) return -ENOMEM; ofdma->of_node = np; ofdma->of_dma_xlate = of_dma_router_xlate; ofdma->of_dma_route_allocate = of_dma_route_allocate; ofdma->dma_router = dma_router; mutex_lock(&of_dma_lock); list_add_tail(&ofdma->of_dma_controllers, &of_dma_list); mutex_unlock(&of_dma_lock); return 0 ; } EXPORT_SYMBOL_GPL(of_dma_router_register);
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 int of_dma_controller_register (struct device_node *np, struct dma_chan *(*of_dma_xlate) (struct of_phandle_args *, struct of_dma *), void *data) { struct of_dma *ofdma ; if (!np || !of_dma_xlate) { pr_err("%s: not enough information provided\n" , __func__); return -EINVAL; } ofdma = kzalloc(sizeof (*ofdma), GFP_KERNEL); if (!ofdma) return -ENOMEM; ofdma->of_node = np; ofdma->of_dma_xlate = of_dma_xlate; ofdma->of_dma_data = data; mutex_lock(&of_dma_lock); list_add_tail(&ofdma->of_dma_controllers, &of_dma_list); mutex_unlock(&of_dma_lock); return 0 ; } EXPORT_SYMBOL_GPL(of_dma_controller_register);
drivers/dma/stm32/stm32-dmamux.c STM32 DMAMUX (DMA多路复用器) 驱动 此代码片段是STM32 DMAMUX的完整平台驱动程序。它的核心作用是充当一个硬件”路由交换机”的软件抽象层 。在复杂的STM32微控制器(如STM32H7系列)中, 有大量的外设(如SPI, I2C, UART)可以发起DMA请求, 但实际的DMA控制器(DMA1, DMA2)通道数量是有限的。DMAMUX就是处在这两者之间的一个硬件单元, 它能将任意一个外设的请求信号连接到任意一个空闲的DMA通道上。
这个驱动的根本原理是实现内核DMA框架中的dma_router
(DMA路由)模式 。当一个外设驱动(如SPI驱动)请求一个DMA通道时, 它并不知道DMAMUX的存在。它在设备树中的dmas
属性指向的是DMAMUX设备, 而不是最终的DMA控制器。内核的DMA框架看到这个dmas
属性后, 会识别出这是一个DMA路由, 于是它不会直接与DMA控制器驱动交互, 而是调用这个DMAMUX驱动注册的route_allocate
回调函数 。
stm32_dmamux_route_allocate
函数就是这个驱动的”大脑”, 它负责:
从大量可用的DMAMUX输出通道中, 找到一个空闲的通道 。
通过写硬件寄存器, 将发起请求的外设信号连接到这个空闲的DMAMUX通道上 。
巧妙地修改(rewrite)原始的DMA请求规格(dma_spec
) , 将其目标从DMAMUX设备重定向 到实际的、承载这个通道的DMA控制器(如DMA1)以及该控制器上的具体流(stream)编号。
将修改后的请求规格返回给DMA框架, 框架再用这个新的规格去与最终的DMA控制器驱动交互, 完成后续的DMA传输设置。
通过这种方式, 该驱动完美地将DMAMUX的复杂性对上层驱动隐藏了起来, 实现了硬件的解耦和抽象。
数据结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #define STM32_DMAMUX_CCR(x) (0x4 * (x)) #define STM32_DMAMUX_MAX_DMA_REQUESTS 32 #define STM32_DMAMUX_MAX_REQUESTS 255 struct stm32_dmamux { u32 master; u32 request; u32 chan_id; }; struct stm32_dmamux_data { struct dma_router dmarouter ; struct clk *clk ; void __iomem *iomem; u32 dma_requests; u32 dmamux_requests; spinlock_t lock; DECLARE_BITMAP(dma_inuse, STM32_DMAMUX_MAX_DMA_REQUESTS); u32 ccr[STM32_DMAMUX_MAX_DMA_REQUESTS]; u32 dma_reqs[]; };
stm32_dmamux_probe
: 驱动探测/初始化函数1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 static void stm32_dmamux_free (struct device *dev, void *route_data) { struct stm32_dmamux_data *dmamux = dev_get_drvdata(dev); struct stm32_dmamux *mux = route_data; unsigned long flags; spin_lock_irqsave(&dmamux->lock, flags); stm32_dmamux_write(dmamux->iomem, STM32_DMAMUX_CCR(mux->chan_id), 0 ); clear_bit(mux->chan_id, dmamux->dma_inuse); pm_runtime_put_sync(dev); spin_unlock_irqrestore(&dmamux->lock, flags); dev_dbg(dev, "Unmapping DMAMUX(%u) to DMA%u(%u)\n" , mux->request, mux->master, mux->chan_id); kfree(mux); } static int stm32_dmamux_probe (struct platform_device *pdev) { struct device_node *node = pdev->dev.of_node; const struct of_device_id *match ; struct device_node *dma_node ; struct stm32_dmamux_data *stm32_dmamux ; void __iomem *iomem; struct reset_control *rst ; int i, count, ret; u32 dma_req; if (!node) return -ENODEV; count = device_property_count_u32(&pdev->dev, "dma-masters" ); if (count < 0 ) { dev_err(&pdev->dev, "Can't get DMA master(s) node\n" ); return -ENODEV; } stm32_dmamux = devm_kzalloc(&pdev->dev, sizeof (*stm32_dmamux) + sizeof (u32) * (count + 1 ), GFP_KERNEL); if (!stm32_dmamux) return -ENOMEM; dma_req = 0 ; for (i = 1 ; i <= count; i++) { dma_node = of_parse_phandle(node, "dma-masters" , i - 1 ); match = of_match_node(stm32_stm32dma_master_match, dma_node); if (!match) { dev_err(&pdev->dev, "DMA master is not supported\n" ); of_node_put(dma_node); return -EINVAL; } if (of_property_read_u32(dma_node, "dma-requests" , &stm32_dmamux->dma_reqs[i])) { stm32_dmamux->dma_reqs[i] = STM32_DMAMUX_MAX_DMA_REQUESTS; } dma_req += stm32_dmamux->dma_reqs[i]; of_node_put(dma_node); } if (dma_req > STM32_DMAMUX_MAX_DMA_REQUESTS) { dev_err(&pdev->dev, "Too many DMA Master Requests to manage\n" ); return -ENODEV; } stm32_dmamux->dma_requests = dma_req; stm32_dmamux->dma_reqs[0 ] = count; if (device_property_read_u32(&pdev->dev, "dma-requests" , &stm32_dmamux->dmamux_requests)) { stm32_dmamux->dmamux_requests = STM32_DMAMUX_MAX_REQUESTS; } pm_runtime_get_noresume(&pdev->dev); iomem = devm_platform_ioremap_resource(pdev, 0 ); if (IS_ERR(iomem)) return PTR_ERR(iomem); spin_lock_init(&stm32_dmamux->lock); stm32_dmamux->clk = devm_clk_get(&pdev->dev, NULL ); if (IS_ERR(stm32_dmamux->clk)) return dev_err_probe(&pdev->dev, PTR_ERR(stm32_dmamux->clk), "Missing clock controller\n" ); ret = clk_prepare_enable(stm32_dmamux->clk); if (ret < 0 ) { dev_err(&pdev->dev, "clk_prep_enable error: %d\n" , ret); return ret; } rst = devm_reset_control_get(&pdev->dev, NULL ); if (IS_ERR(rst)) { ret = PTR_ERR(rst); if (ret == -EPROBE_DEFER) goto err_clk; } else if (count > 1 ) { reset_control_assert(rst); udelay(2 ); reset_control_deassert(rst); } stm32_dmamux->iomem = iomem; stm32_dmamux->dmarouter.dev = &pdev->dev; stm32_dmamux->dmarouter.route_free = stm32_dmamux_free; platform_set_drvdata(pdev, stm32_dmamux); pm_runtime_set_active(&pdev->dev); pm_runtime_enable(&pdev->dev); pm_runtime_get_noresume(&pdev->dev); for (i = 0 ; i < stm32_dmamux->dma_requests; i++) stm32_dmamux_write(stm32_dmamux->iomem, STM32_DMAMUX_CCR(i), 0 ); pm_runtime_put(&pdev->dev); ret = of_dma_router_register(node, stm32_dmamux_route_allocate, &stm32_dmamux->dmarouter); if (ret) goto pm_disable; return 0 ; pm_disable: pm_runtime_disable(&pdev->dev); err_clk: clk_disable_unprepare(stm32_dmamux->clk); return ret; }
stm32_dmamux_route_allocate
: 核心路由分配函数1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 static inline u32 stm32_dmamux_read (void __iomem *iomem, u32 reg) { return readl_relaxed(iomem + reg); } static inline void stm32_dmamux_write (void __iomem *iomem, u32 reg, u32 val) { writel_relaxed(val, iomem + reg); } static void *stm32_dmamux_route_allocate (struct of_phandle_args *dma_spec, struct of_dma *ofdma) { struct platform_device *pdev = of_find_device_by_node(ofdma->of_node); struct stm32_dmamux_data *dmamux = platform_get_drvdata(pdev); struct stm32_dmamux *mux ; u32 i, min, max; int ret; unsigned long flags; if (dma_spec->args_count != 3 ) { dev_err(&pdev->dev, "invalid number of dma mux args\n" ); return ERR_PTR(-EINVAL); } if (dma_spec->args[0 ] > dmamux->dmamux_requests) { dev_err(&pdev->dev, "invalid mux request number: %d\n" , dma_spec->args[0 ]); return ERR_PTR(-EINVAL); } mux = kzalloc(sizeof (*mux), GFP_KERNEL); if (!mux) return ERR_PTR(-ENOMEM); spin_lock_irqsave(&dmamux->lock, flags); mux->chan_id = find_first_zero_bit(dmamux->dma_inuse, dmamux->dma_requests); if (mux->chan_id == dmamux->dma_requests) { spin_unlock_irqrestore(&dmamux->lock, flags); dev_err(&pdev->dev, "Run out of free DMA requests\n" ); ret = -ENOMEM; goto error_chan_id; } set_bit(mux->chan_id, dmamux->dma_inuse); spin_unlock_irqrestore(&dmamux->lock, flags); for (i = 1 , min = 0 , max = dmamux->dma_reqs[i]; i <= dmamux->dma_reqs[0 ]; min += dmamux->dma_reqs[i], max += dmamux->dma_reqs[++i]) if (mux->chan_id < max) break ; mux->master = i - 1 ; dma_spec->np = of_parse_phandle(ofdma->of_node, "dma-masters" , i - 1 ); if (!dma_spec->np) { dev_err(&pdev->dev, "can't get dma master\n" ); ret = -EINVAL; goto error; } spin_lock_irqsave(&dmamux->lock, flags); ret = pm_runtime_resume_and_get(&pdev->dev); if (ret < 0 ) { spin_unlock_irqrestore(&dmamux->lock, flags); goto error; } spin_unlock_irqrestore(&dmamux->lock, flags); mux->request = dma_spec->args[0 ]; dma_spec->args[3 ] = dma_spec->args[2 ] | mux->chan_id << 16 ; dma_spec->args[2 ] = dma_spec->args[1 ]; dma_spec->args[1 ] = 0 ; dma_spec->args[0 ] = mux->chan_id - min; dma_spec->args_count = 4 ; stm32_dmamux_write(dmamux->iomem, STM32_DMAMUX_CCR(mux->chan_id), mux->request); dev_dbg(&pdev->dev, "Mapping DMAMUX(%u) to DMA%u(%u)\n" , mux->request, mux->master, mux->chan_id); return mux; error: clear_bit(mux->chan_id, dmamux->dma_inuse); error_chan_id: kfree(mux); return ERR_PTR(ret); }
STM32 DMAMUX 平台驱动程序注册 此代码片段的核心作用是定义并向Linux内核注册一个平台驱动程序(platform_driver
) , 这个驱动专门用于管理ST微控制器(特别是STM32H7系列)上的DMAMUX 外设。DMAMUX是”DMA多路复用器”(DMA Multiplexer)的缩写, 它是一个硬件路由单元, 允许将数量众多的外设DMA请求灵活地连接到数量有限的实际DMA控制器通道上。
该驱动程序的结构是Linux内核中用于处理SoC片上外设的标准范例。它的原理如下:
定义匹配规则 : 通过of_device_id
表声明一个compatible
字符串 (“st,stm32h7-dmamux”)。当内核在解析设备树(Device Tree)时, 如果发现一个硬件节点的compatible
属性与这个字符串完全匹配, 内核就确信stm32_dmamux_driver
是管理该硬件的正确驱动。
定义核心操作 : 它将驱动的核心逻辑函数指针(如probe
函数stm32_dmamux_probe
)和电源管理回调函数集(stm32_dmamux_pm_ops
)打包进一个platform_driver
结构体中。probe
函数会在匹配成功后被内核调用, 负责初始化DMAMUX硬件; 电源管理函数则负责在系统挂起/恢复或运行时空闲时关闭/打开DMAMUX的时钟以节省功耗。
注册与初始化 : 在内核启动的早期阶段(由arch_initcall
指定), stm32_dmamux_init
函数会被调用。它唯一的工作就是调用platform_driver_register
, 将整个stm32_dmamux_driver
结构体提交给内核的平台总线核心。从这一刻起, 该驱动就进入了”待命”状态, 等待内核为其派发匹配的设备。
对于STM32H750这样的单核系统, 这个驱动依然至关重要。它使得其他外设驱动(如SPI, I2C)无需关心DMAMUX的底层寄存器操作, 只需通过内核提供的标准DMA API来请求一个DMA通道, 而DMAMUX驱动和DMA引擎框架会自动处理复杂的请求路由和资源分配, 实现了硬件的抽象和驱动的解耦。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 static const struct dev_pm_ops stm32_dmamux_pm_ops = { SET_SYSTEM_SLEEP_PM_OPS(stm32_dmamux_suspend, stm32_dmamux_resume) SET_RUNTIME_PM_OPS(stm32_dmamux_runtime_suspend, stm32_dmamux_runtime_resume, NULL ) }; static const struct of_device_id stm32_dmamux_match [] = { { .compatible = "st,stm32h7-dmamux" }, {}, }; static struct platform_driver stm32_dmamux_driver = { .probe = stm32_dmamux_probe, .driver = { .name = "stm32-dmamux" , .of_match_table = stm32_dmamux_match, .pm = &stm32_dmamux_pm_ops, }, }; static int __init stm32_dmamux_init (void ) { return platform_driver_register(&stm32_dmamux_driver); } arch_initcall(stm32_dmamux_init); MODULE_DESCRIPTION("DMA Router driver for STM32 DMA MUX" ); MODULE_AUTHOR("M'boumba Cedric Madianga <cedric.madianga@gmail.com>" ); MODULE_AUTHOR("Pierre-Yves Mordret <pierre-yves.mordret@st.com>" );
drivers/dma/stm32/stm32-dma.c STM32 DMA 驱动寄存器与常量定义 底层寄存器访问函数 这两个静态内联函数是所有硬件交互的基础。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 static u32 stm32_dma_read (struct stm32_dma_device *dmadev, u32 reg) { return readl_relaxed(dmadev->base + reg); } static void stm32_dma_write (struct stm32_dma_device *dmadev, u32 reg, u32 val) { writel_relaxed(val, dmadev->base + reg); }
中断状态与清除寄存器定义 这组宏用于定位与特定DMA流(Stream, 在驱动中称为chan
或channel
)关联的中断标志。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #define STM32_DMA_LISR 0x0000 #define STM32_DMA_HISR 0x0004 #define STM32_DMA_ISR(n) (((n) & 4) ? STM32_DMA_HISR : STM32_DMA_LISR) #define STM32_DMA_LIFCR 0x0008 #define STM32_DMA_HIFCR 0x000c #define STM32_DMA_IFCR(n) (((n) & 4) ? STM32_DMA_HIFCR : STM32_DMA_LIFCR) #define STM32_DMA_TCI BIT(5) #define STM32_DMA_HTI BIT(4) #define STM32_DMA_TEI BIT(3) #define STM32_DMA_DMEI BIT(2) #define STM32_DMA_FEI BIT(0) #define STM32_DMA_MASKI (STM32_DMA_TCI \ | STM32_DMA_TEI \ | STM32_DMA_DMEI \ | STM32_DMA_FEI) #define STM32_DMA_FLAGS_SHIFT(n) ({ typeof(n) (_n) = (n); \ (((_n) & 2) << 3) | (((_n) & 1) * 6); })
DMA流 (Stream) 寄存器定义 这组宏定义了每个DMA流的独立寄存器组的地址和位域。STM32 DMA控制器有多个流(例如8个), 每个流都有一套独立的配置、地址和数据计数寄存器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #define STM32_DMA_SCR(x) (0x0010 + 0x18 * (x)) #define STM32_DMA_SCR_REQ_MASK GENMASK(27, 25) #define STM32_DMA_SCR_MBURST_MASK GENMASK(24, 23) #define STM32_DMA_SCR_PBURST_MASK GENMASK(22, 21) #define STM32_DMA_SCR_PL_MASK GENMASK(17, 16) #define STM32_DMA_SCR_MSIZE_MASK GENMASK(14, 13) #define STM32_DMA_SCR_PSIZE_MASK GENMASK(12, 11) #define STM32_DMA_SCR_DIR_MASK GENMASK(7, 6) #define STM32_DMA_SCR_MINC BIT(10) #define STM32_DMA_SCR_PINC BIT(9) #define STM32_DMA_SCR_CIRC BIT(8) #define STM32_DMA_SCR_PFCTRL BIT(5) #define STM32_DMA_SCR_TCIE BIT(4) #define STM32_DMA_SCR_TEIE BIT(2) #define STM32_DMA_SCR_DMEIE BIT(1) #define STM32_DMA_SCR_EN BIT(0) #define STM32_DMA_SCR_IRQ_MASK (STM32_DMA_SCR_TCIE \ | STM32_DMA_SCR_TEIE \ | STM32_DMA_SCR_DMEIE) #define STM32_DMA_SNDTR(x) (0x0014 + 0x18 * (x)) #define STM32_DMA_SPAR(x) (0x0018 + 0x18 * (x)) #define STM32_DMA_SM0AR(x) (0x001c + 0x18 * (x)) #define STM32_DMA_SM1AR(x) (0x0020 + 0x18 * (x)) #define STM32_DMA_SFCR(x) (0x0024 + 0x18 * (x)) #define STM32_DMA_SFCR_FTH_MASK GENMASK(1, 0) #define STM32_DMA_SFCR_FEIE BIT(7) #define STM32_DMA_SFCR_DMDIS BIT(2)
DMA 行为和硬件参数常量 这组宏和枚举定义了DMA的逻辑行为(如方向、优先级)和固有的硬件限制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #define STM32_DMA_DEV_TO_MEM 0x00 #define STM32_DMA_MEM_TO_DEV 0x01 #define STM32_DMA_MEM_TO_MEM 0x02 #define STM32_DMA_PRIORITY_LOW 0x00 #define STM32_DMA_PRIORITY_MEDIUM 0x01 #define STM32_DMA_PRIORITY_HIGH 0x02 #define STM32_DMA_PRIORITY_VERY_HIGH 0x03 #define STM32_DMA_FIFO_THRESHOLD_1QUARTERFULL 0x00 #define STM32_DMA_FIFO_THRESHOLD_HALFFULL 0x01 #define STM32_DMA_FIFO_THRESHOLD_3QUARTERSFULL 0x02 #define STM32_DMA_FIFO_THRESHOLD_FULL 0x03 #define STM32_DMA_MAX_DATA_ITEMS 0xffff #define STM32_DMA_ALIGNED_MAX_DATA_ITEMS \ ALIGN_DOWN(STM32_DMA_MAX_DATA_ITEMS, 16) #define STM32_DMA_MAX_CHANNELS 0x08 #define STM32_DMA_FIFO_SIZE 16 #define STM32_DMA_MIN_BURST 4 #define STM32_DMA_MAX_BURST 16 enum stm32_dma_width { STM32_DMA_BYTE, STM32_DMA_HALF_WORD, STM32_DMA_WORD, }; enum stm32_dma_burst_size { STM32_DMA_BURST_SINGLE, STM32_DMA_BURST_INCR4, STM32_DMA_BURST_INCR8, STM32_DMA_BURST_INCR16, };
STM32 DMA通道硬件操作底层函数 这一组静态函数构成了STM32 DMA驱动程序中直接与硬件寄存器交互的最底层 。它们将对DMA通道的抽象操作(如”获取中断状态”、”停止传输”)转换为精确的寄存器读写序列。这些函数是驱动中所有更高级别逻辑(如中断处理、传输准备)的基础。
stm32_dma_irq_status
: 读取通道的中断状态此函数的核心作用是查询一个DMA通道的硬件中断状态寄存器(DMA_ISR
), 并返回与该通道相关的中断标志 。
原理 : STM32的DMA控制器通常将多个通道(流)的中断标志打包存放在同一个32位ISR
(Interrupt Status Register)中。例如, Stream 0-3的标志在LISR
, Stream 4-7的在HISR
。此函数首先通过STM32_DMA_ISR(chan->id)
宏计算出当前通道属于哪个ISR
寄存器, 然后读取该寄存器的完整值。接着, 它使用STM32_DMA_FLAGS_SHIFT(chan->id)
宏计算出该通道标志位在此寄存器内的偏移量, 并通过右移 操作将这些标志位对齐到寄存器的最低位。最后, 它与一个掩码(STM32_DMA_MASKI
)进行按位与 , 以过滤掉不相关的位, 仅返回有效的中断标志(如传输完成、半传输、传输错误等)。
1 2 3 4 5 6 7 8 9 10 11 12 13 static u32 stm32_dma_irq_status (struct stm32_dma_chan *chan) { struct stm32_dma_device *dmadev = stm32_dma_get_dev(chan); u32 flags, dma_isr; dma_isr = stm32_dma_read(dmadev, STM32_DMA_ISR(chan->id)); flags = dma_isr >> STM32_DMA_FLAGS_SHIFT(chan->id); return flags & STM32_DMA_MASKI; }
stm32_dma_irq_clear
: 清除通道的中断标志此函数的核心作用是向DMA通道的硬件中断标志清除寄存器(DMA_IFCR
)写入相应的值, 以清除一个或多个已触发的中断标志 。
原理 : 在STM32中, 中断标志的清除是通过向IFCR
(Interrupt Flag Clear Register)的特定位写入’1’来完成的。此函数的逻辑与irq_status
相反。它首先获取要清除的标志flags
, 通过STM32_DMA_FLAGS_SHIFT
宏左移 到它们在IFCR
寄存器中正确的位置, 然后通过stm32_dma_write
将这个计算出的值写入由STM32_DMA_IFCR
宏确定的正确寄存器地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 static void stm32_dma_irq_clear (struct stm32_dma_chan *chan, u32 flags) { struct stm32_dma_device *dmadev = stm32_dma_get_dev(chan); u32 dma_ifcr; flags &= STM32_DMA_MASKI; dma_ifcr = flags << STM32_DMA_FLAGS_SHIFT(chan->id); stm32_dma_write(dmadev, STM32_DMA_IFCR(chan->id), dma_ifcr); }
stm32_dma_disable_chan
: 安全地禁用一个DMA通道此函数的核心作用是关闭一个正在运行的DMA通道(流), 并等待硬件确认该通道确实已经停止 。
原理 : 简单地向DMA流控制寄存器(DMA_SxCR
)的EN
(Enable)位写入’0’可能不会立即生效, 硬件可能需要几个时钟周期来完成当前的总线事务。直接返回可能会导致竞态条件。因此, 此函数实现了一个”写后轮询 “的安全序列:
读取DMA_SxCR
寄存器。
如果EN
位已经是’0’, 说明通道已禁用, 直接返回。
如果EN
位是’1’, 则将该位置’0’后写回寄存器。
关键步骤 : 它调用readl_relaxed_poll_timeout_atomic
函数, 持续轮询 DMA_SxCR
寄存器, 直到EN
位变为’0’, 或者超时发生。这确保了当函数返回时, 硬件传输流是确定性地停止 了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 static int stm32_dma_disable_chan (struct stm32_dma_chan *chan) { struct stm32_dma_device *dmadev = stm32_dma_get_dev(chan); u32 dma_scr, id, reg; id = chan->id; reg = STM32_DMA_SCR(id); dma_scr = stm32_dma_read(dmadev, reg); if (dma_scr & STM32_DMA_SCR_EN) { dma_scr &= ~STM32_DMA_SCR_EN; stm32_dma_write(dmadev, reg, dma_scr); return readl_relaxed_poll_timeout_atomic(dmadev->base + reg, dma_scr, !(dma_scr & STM32_DMA_SCR_EN), 10 , 1000000 ); } return 0 ; }
stm32_dma_stop
: 完整地停止并清理一个通道这是一个更高级别的封装, 其核心作用是执行一个完整的、安全的通道停止序列 , 包括禁用中断、停止硬件传输、清除悬挂的中断标志以及更新软件状态。
原理 : 它按照一个严格的顺序执行清理操作:
禁用中断 : 首先, 它修改DMA_SxCR
和DMA_SxFCR
寄存器, 清除所有中断使能位(如TCIE
, TEIE
, FEIE
等)。这是为了防止在停止过程中产生任何新的中断。
禁用DMA : 调用stm32_dma_disable_chan
来安全地停止硬件传输流。
清除状态 : 调用stm32_dma_irq_status
检查是否有在禁用中断之前就已经触发并悬挂的中断标志。如果有, 就调用stm32_dma_irq_clear
来清除它们, 确保通道处于一个干净的状态。
更新软件状态 : 最后, 它更新驱动内部的软件标志, 将chan->busy
置为false
, chan->status
置为DMA_COMPLETE
, 以向上层表明该通道已空闲。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 static void stm32_dma_stop (struct stm32_dma_chan *chan) { struct stm32_dma_device *dmadev = stm32_dma_get_dev(chan); u32 dma_scr, dma_sfcr, status; int ret; dma_scr = stm32_dma_read(dmadev, STM32_DMA_SCR(chan->id)); dma_scr &= ~STM32_DMA_SCR_IRQ_MASK; stm32_dma_write(dmadev, STM32_DMA_SCR(chan->id), dma_scr); dma_sfcr = stm32_dma_read(dmadev, STM32_DMA_SFCR(chan->id)); dma_sfcr &= ~STM32_DMA_SFCR_FEIE; stm32_dma_write(dmadev, STM32_DMA_SFCR(chan->id), dma_sfcr); ret = stm32_dma_disable_chan(chan); if (ret < 0 ) return ; status = stm32_dma_irq_status(chan); if (status) { dev_dbg(chan2dev(chan), "%s(): clearing interrupt: 0x%08x\n" , __func__, status); stm32_dma_irq_clear(chan, status); } chan->busy = false ; chan->status = DMA_COMPLETE; }
stm32_dma_alloc_chan_resources
和 stm32_dma_free_chan_resources
: DMA通道资源的分配与释放这两个函数是Linux DMA引擎框架中通道生命周期管理 的核心回调。它们分别在使用DMA通道之前 和之后 被调用, 负责准备和清理与一个特定DMA通道相关的所有软硬件资源。
stm32_dma_alloc_chan_resources
: 分配并准备DMA通道资源此函数在DMA通道被一个”使用者”驱动(如SPI驱动)首次请求并获得时被调用。它的核心原理是将一个空闲的DMA通道转换到一个准备就绪、可以被安全编程的初始状态 。
这个准备过程包括两个关键的动作:
唤醒硬件 : 它首先通过pm_runtime_resume_and_get
来确保DMA控制器本身是上电并工作的。如果DMA控制器因为长时间未使用而进入了低功耗的”运行时挂起”状态, 这个调用会唤醒它并增加其使用计数, 防止其再次休眠。
确保硬件空闲 : 接着, 它调用stm32_dma_disable_chan
来强制禁用 该通道对应的硬件单元(在STM32中是”流”Stream)。这是一个至关重要的安全步骤, 它可以确保无论该通道之前处于何种状态(可能是上一个使用者异常退出留下的), 它现在都处于一个已知的、停止的、可以安全写入新配置的状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 static int stm32_dma_alloc_chan_resources (struct dma_chan *c) { struct stm32_dma_chan *chan = to_stm32_dma_chan(c); struct stm32_dma_device *dmadev = stm32_dma_get_dev(chan); int ret; chan->config_init = false ; ret = pm_runtime_resume_and_get(dmadev->ddev.dev); if (ret < 0 ) return ret; ret = stm32_dma_disable_chan(chan); if (ret < 0 ) pm_runtime_put(dmadev->ddev.dev); return ret; }
stm32_dma_free_chan_resources
: 释放DMA通道资源当使用者驱动不再需要DMA通道并调用dma_release_channel
时, 此函数被调用。它的核心原理是安全地终止该通道上任何可能正在进行的活动, 清理所有相关的软硬件状态, 并通知电源管理框架该通道已变为空闲 。
其清理流程如下:
终止硬件传输 : 它首先检查通道是否处于busy
状态。如果是, 它会进入一个由自旋锁保护的临界区, 调用stm32_dma_stop
来强制停止硬件传输, 防止在清理过程中发生中断等竞态条件。
清理软件状态 : 它会清理与该通道相关的所有软件状态, 包括释放虚拟通道(vchan
)的资源(如待处理的描述符列表)和重置驱动内部的缓存及标志位。
释放电源锁 : 最后, 它调用pm_runtime_put
来减少DMA控制器的使用计数。如果这是最后一个被释放的活动通道, 那么DMA控制器的使用计数可能会降为0, 使得运行时电源管理框架可以在稍后将其置于低功耗状态, 以节省能源。
在STM32H750这样的单核抢占式系统上, spin_lock_irqsave
至关重要, 它通过禁用中断来防止在清理通道时, 该通道的DMA完成中断突然触发, 从而避免了对通道状态和描述符链表的并发访问。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 static void stm32_dma_free_chan_resources (struct dma_chan *c) { struct stm32_dma_chan *chan = to_stm32_dma_chan(c); struct stm32_dma_device *dmadev = stm32_dma_get_dev(chan); unsigned long flags; dev_dbg(chan2dev(chan), "Freeing channel %d\n" , chan->id); if (chan->busy) { spin_lock_irqsave(&chan->vchan.lock, flags); stm32_dma_stop(chan); chan->desc = NULL ; spin_unlock_irqrestore(&chan->vchan.lock, flags); } pm_runtime_put(dmadev->ddev.dev); vchan_free_chan_resources(to_virt_chan(c)); stm32_dma_clear_reg(&chan->chan_reg); chan->threshold = 0 ; }
stm32_dma_issue_pending
和 stm32_dma_tx_status
: 传输的启动与状态查询这两个函数是DMA驱动程序中负责运行时传输管理 的核心回调。issue_pending
是启动 操作的入口, 而tx_status
是查询 操作的入口。它们都严重依赖于virt-dma
(虚拟DMA通道)这个通用框架来管理传输描述符队列, 同时通过自旋锁来确保操作的原子性, 防止与中断处理程序发生竞态条件。
stm32_dma_issue_pending
: 启动待处理的DMA传输当一个使用者驱动(如SPI)调用dmaengine_submit()
提交一个或多个传输请求后, DMA引擎核心会调用此函数。它的核心原理是作为一个看门人, 检查DMA通道当前是否空闲, 以及是否有新的传输任务在排队。如果两个条件都满足, 它就从队列中取出下一个任务并启动硬件传输 。
这个过程是异步DMA操作的关键: submit
只是把任务放入队列, 而issue_pending
才是真正”按下启动按钮”的动作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 static void stm32_dma_issue_pending (struct dma_chan *c) { struct stm32_dma_chan *chan = to_stm32_dma_chan(c); unsigned long flags; spin_lock_irqsave(&chan->vchan.lock, flags); if (vchan_issue_pending(&chan->vchan) && !chan->desc && !chan->busy) { dev_dbg(chan2dev(chan), "vchan %p: issued\n" , &chan->vchan); stm32_dma_start_transfer(chan); } spin_unlock_irqrestore(&chan->vchan.lock, flags); }
stm32_dma_tx_status
: 查询一个DMA传输的状态此函数用于查询一个特定DMA传输的当前状态。使用者驱动通过一个dma_cookie_t
(唯一的传输ID)来指定要查询的传输。它的核心原理是首先利用通用DMA框架快速检查传输是否已经完成, 如果没有, 则通过查询硬件寄存器或软件队列来计算剩余未传输的数据量(residue
) 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 static enum dma_status stm32_dma_tx_status (struct dma_chan *c, dma_cookie_t cookie, struct dma_tx_state *state) { struct stm32_dma_chan *chan = to_stm32_dma_chan(c); struct virt_dma_desc *vdesc ; enum dma_status status ; unsigned long flags; u32 residue = 0 ; status = dma_cookie_status(c, cookie, state); if (status == DMA_COMPLETE) return status; status = chan->status; if (!state) return status; spin_lock_irqsave(&chan->vchan.lock, flags); vdesc = vchan_find_desc(&chan->vchan, cookie); if (chan->desc && cookie == chan->desc->vdesc.tx.cookie) residue = stm32_dma_desc_residue(chan, chan->desc, chan->next_sg); else if (vdesc) residue = stm32_dma_desc_residue(chan, to_stm32_dma_desc(vdesc), 0 ); dma_set_residue(state, residue); spin_unlock_irqrestore(&chan->vchan.lock, flags); return status; }
stm32_dma_set_xfer_param
: DMA传输参数的智能优化与配置此函数是STM32 DMA驱动中一个至关重要的内部辅助函数 , 它的核心作用是在prep
函数(如stm32_dma_prep_slave_sg
)内部, 为一个具体的传输缓冲区(scatterlist中的一个sg条目)计算出最优的硬件传输参数, 并将这些参数更新到通道的配置缓存中 。
可以把它理解为一个智能配置引擎 。它接收高级的、与硬件无关的请求(如传输方向、缓冲区长度), 并结合之前由使用者驱动通过dma_slave_config
设置的约束(如外设的数据宽度), 最终生成一组能够最高效地利用STM32 DMA硬件特性(如突发传输、FIFO)的底层寄存器值。
其工作原理是根据传输方向 (direction
)执行两套不同的优化逻辑:
**2. 内存到外设 (`DMA_MEM_TO_DEV`) 逻辑:**
**3. 外设到内存 (`DMA_DEV_TO_MEM`) 逻辑:**
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 static int stm32_dma_set_xfer_param (struct stm32_dma_chan *chan, enum dma_transfer_direction direction, enum dma_slave_buswidth *buswidth, u32 buf_len, dma_addr_t buf_addr) { src_addr_width = chan->dma_sconfig.src_addr_width; dst_addr_width = chan->dma_sconfig.dst_addr_width; src_maxburst = chan->dma_sconfig.src_maxburst; dst_maxburst = chan->dma_sconfig.dst_maxburst; fifoth = chan->threshold; switch (direction) { case DMA_MEM_TO_DEV: dst_bus_width = stm32_dma_get_width(chan, dst_addr_width); if (dst_bus_width < 0 ) return dst_bus_width; dst_best_burst = stm32_dma_get_best_burst(buf_len, dst_maxburst, fifoth, dst_addr_width); dst_burst_size = stm32_dma_get_burst(chan, dst_best_burst); if (dst_burst_size < 0 ) return dst_burst_size; src_addr_width = stm32_dma_get_max_width(buf_len, buf_addr, fifoth); chan->mem_width = src_addr_width; src_bus_width = stm32_dma_get_width(chan, src_addr_width); if (src_bus_width < 0 ) return src_bus_width; if (buf_addr & (buf_len - 1 )) src_maxburst = 1 ; else src_maxburst = STM32_DMA_MAX_BURST; src_best_burst = stm32_dma_get_best_burst(buf_len, src_maxburst, fifoth, src_addr_width); src_burst_size = stm32_dma_get_burst(chan, src_best_burst); if (src_burst_size < 0 ) return src_burst_size; dma_scr = FIELD_PREP(STM32_DMA_SCR_DIR_MASK, STM32_DMA_MEM_TO_DEV) | FIELD_PREP(STM32_DMA_SCR_PSIZE_MASK, dst_bus_width) | FIELD_PREP(STM32_DMA_SCR_MSIZE_MASK, src_bus_width) | FIELD_PREP(STM32_DMA_SCR_PBURST_MASK, dst_burst_size) | FIELD_PREP(STM32_DMA_SCR_MBURST_MASK, src_burst_size); chan->chan_reg.dma_sfcr &= ~STM32_DMA_SFCR_FTH_MASK; if (fifoth != STM32_DMA_FIFO_THRESHOLD_NONE) chan->chan_reg.dma_sfcr |= FIELD_PREP(STM32_DMA_SFCR_FTH_MASK, fifoth); chan->chan_reg.dma_spar = chan->dma_sconfig.dst_addr; *buswidth = dst_addr_width; break ; case DMA_DEV_TO_MEM: break ; default : dev_err(chan2dev(chan), "Dma direction is not supported\n" ); return -EINVAL; } stm32_dma_set_fifo_config(chan, src_best_burst, dst_best_burst); chan->chan_reg.dma_scr &= ~(STM32_DMA_SCR_DIR_MASK | STM32_DMA_SCR_PSIZE_MASK | STM32_DMA_SCR_MSIZE_MASK | STM32_DMA_SCR_PBURST_MASK | STM32_DMA_SCR_MBURST_MASK); chan->chan_reg.dma_scr |= dma_scr; return 0 ; }
stm32_dma_prep_slave_sg
和 stm32_dma_prep_dma_cyclic
: DMA传输的”蓝图”构建器这两个函数是STM32 DMA驱动程序中负责将抽象的传输请求转化为具体的硬件编程指令集 的核心回调函数。当一个”使用者”驱动(如SPI)需要进行DMA传输时, 它会调用通用DMA引擎的API, 最终会路由到这两个函数之一。
它们的核心原理是充当一个”蓝图绘制师” : 接收使用者驱动提供的高层信息(如内存地址列表、长度、方向), 然后为硬件执行器(即DMA控制器)精心准备一个或多个详细的、可执行的描述符 (descriptor) 。这个描述符本质上是一个软件结构体, 它缓存了在启动传输时需要被写入到DMA硬件寄存器(如地址寄存器、数据计数器、控制寄存器)的所有数值。
关键点是, prep
函数本身并不启动硬件传输 , 它只是创建和准备这些”蓝图”, 然后将它们提交到virt-dma
框架的队列中。真正的硬件启动由stm32_dma_issue_pending
在稍后完成。
stm32_dma_prep_slave_sg
: 准备散列表(Scatter-Gather)传输此函数用于处理最常见的DMA传输类型: 在一个外设和多个不连续的内存块 之间传输数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 static struct dma_async_tx_descriptor *stm32_dma_prep_slave_sg ( struct dma_chan *c, struct scatterlist *sgl, u32 sg_len, enum dma_transfer_direction direction, unsigned long flags, void *context) { struct stm32_dma_chan *chan = to_stm32_dma_chan(c); struct stm32_dma_desc *desc ; struct scatterlist *sg ; enum dma_slave_buswidth buswidth ; u32 nb_data_items; int i, ret; if (!chan->config_init) { dev_err(chan2dev(chan), "dma channel is not configured\n" ); return NULL ; } if (sg_len < 1 ) { dev_err(chan2dev(chan), "Invalid segment length %d\n" , sg_len); return NULL ; } desc = kzalloc(struct_size(desc, sg_req, sg_len), GFP_NOWAIT); if (!desc) return NULL ; desc->num_sgs = sg_len; if (chan->dma_sconfig.device_fc) chan->chan_reg.dma_scr |= STM32_DMA_SCR_PFCTRL; else chan->chan_reg.dma_scr &= ~STM32_DMA_SCR_PFCTRL; if (chan->trig_mdma && sg_len > 1 ) { chan->chan_reg.dma_scr |= STM32_DMA_SCR_DBM; chan->chan_reg.dma_scr &= ~STM32_DMA_SCR_CT; } for_each_sg(sgl, sg, sg_len, i) { ret = stm32_dma_set_xfer_param(chan, direction, &buswidth, sg_dma_len(sg), sg_dma_address(sg)); if (ret < 0 ) goto err; desc->sg_req[i].len = sg_dma_len(sg); nb_data_items = desc->sg_req[i].len / buswidth; if (nb_data_items > STM32_DMA_ALIGNED_MAX_DATA_ITEMS) { goto err; } stm32_dma_clear_reg(&desc->sg_req[i].chan_reg); desc->sg_req[i].chan_reg.dma_scr = chan->chan_reg.dma_scr; desc->sg_req[i].chan_reg.dma_sfcr = chan->chan_reg.dma_sfcr; desc->sg_req[i].chan_reg.dma_spar = chan->chan_reg.dma_spar; desc->sg_req[i].chan_reg.dma_sm0ar = sg_dma_address(sg); desc->sg_req[i].chan_reg.dma_sm1ar = sg_dma_address(sg); if (chan->trig_mdma) desc->sg_req[i].chan_reg.dma_sm1ar += sg_dma_len(sg); desc->sg_req[i].chan_reg.dma_sndtr = nb_data_items; } desc->cyclic = false ; return vchan_tx_prep(&chan->vchan, &desc->vdesc, flags); err: kfree(desc); return NULL ; }
stm32_dma_prep_dma_cyclic
: 准备循环(Cyclic)传输此函数用于准备一个特殊的DMA传输, 数据会从一个缓冲区中被周期性地、无限地传输。这对于音频(I2S)或连续ADC采样等应用是必不可少的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 static struct dma_async_tx_descriptor *stm32_dma_prep_dma_cyclic ( struct dma_chan *c, dma_addr_t buf_addr, size_t buf_len, size_t period_len, enum dma_transfer_direction direction, unsigned long flags) { if (buf_len == period_len) { chan->chan_reg.dma_scr |= STM32_DMA_SCR_CIRC; } else { chan->chan_reg.dma_scr |= STM32_DMA_SCR_DBM; chan->chan_reg.dma_scr &= ~STM32_DMA_SCR_CT; } num_periods = buf_len / period_len; desc->num_sgs = num_periods; for (i = 0 ; i < num_periods; i++) { desc->sg_req[i].len = period_len; stm32_dma_clear_reg(&desc->sg_req[i].chan_reg); desc->sg_req[i].chan_reg.dma_scr = chan->chan_reg.dma_scr; desc->sg_req[i].chan_reg.dma_sfcr = chan->chan_reg.dma_sfcr; desc->sg_req[i].chan_reg.dma_spar = chan->chan_reg.dma_spar; desc->sg_req[i].chan_reg.dma_sm0ar = buf_addr; desc->sg_req[i].chan_reg.dma_sm1ar = buf_addr; if (chan->trig_mdma) desc->sg_req[i].chan_reg.dma_sm1ar += period_len; desc->sg_req[i].chan_reg.dma_sndtr = nb_data_items; if (!chan->trig_mdma) buf_addr += period_len; } desc->cyclic = true ; return vchan_tx_prep(&chan->vchan, &desc->vdesc, flags); }
stm32_dma_handle_chan_paused
: 捕获DMA暂停状态并为恢复做准备此函数是一个内部状态处理函数 , 它在DMA硬件流被stm32_dma_disable_chan
安全地停止后立即被调用。它的核心原理是为一个已暂停的DMA传输执行”状态快照”, 精确地捕获硬件在停止瞬间的状态, 并对硬件进行微调, 以确保后续的resume
(恢复)操作能够无缝、正确地继续传输 。
可以把它看作是DMA暂停操作的”收尾工作”。stm32_dma_disable_chan
负责”踩刹车”, 而stm32_dma_handle_chan_paused
负责”记录停车位置和车辆状态”。
这个过程包含两个关键且精妙的步骤:
捕获剩余工作量 :
它首先读取硬件的DMA_SNDTR
(Stream Number of Data to Transfer)寄存器。这个寄存器中的值是在硬件停止时精确剩余的、尚未传输的数据项数量 。这个值被保存在软件的通道配置缓存(chan->chan_reg.dma_sndtr
)中, 这是resume
函数能够计算出从哪里继续传输的最关键信息 。
处理循环/双缓冲模式的特殊逻辑 :
这是此函数最复杂也是最重要的部分。直接暂停一个处于循环(CIRC
)或双缓冲(DBM
)模式的传输, 然后再恢复, 会有一个问题: DMA硬件的自动重载机制可能会在恢复时使用一个不正确(部分)的数据计数值, 从而破坏后续的循环。
为了解决这个问题, 函数执行了一个巧妙的**”软件保存意图, 硬件简化状态”**的操作:
保存意图 : 它先读取SCR
(Stream Configuration Register), 并在软件备份(chan->chan_reg.dma_scr
)中重新确保 CIRC
或DBM
标志是设置的。这可以防止因某些临时状态(如上次恢复后)导致硬件SCR
中的这些位被清除, 从而”忘记”了这次传输本应是循环的。
简化状态 : 然后, 它在硬件的SCR
寄存器中, 故意清除CIRC
和DBM
位 。这暂时将当前被中断的传输片段变成了一个普通的”一次性”传输。这样, resume
函数就可以简单地恢复这个一次性传输, 而不必担心硬件的自动重载机制会出错。当中断处理程序在这次恢复的传输完成后被触发时, 它会负责检查原始意图(从软件备份中读取), 并为下一个完整的周期重新启用CIRC
或DBM
模式。
最后, 它将通道的软件状态正式设置为DMA_PAUSED
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 static void stm32_dma_handle_chan_paused (struct stm32_dma_chan *chan) { struct stm32_dma_device *dmadev = stm32_dma_get_dev(chan); u32 dma_scr; dma_scr = stm32_dma_read(dmadev, STM32_DMA_SCR(chan->id)); if (chan->desc && chan->desc->cyclic) { if (chan->desc->num_sgs == 1 ) dma_scr |= STM32_DMA_SCR_CIRC; else dma_scr |= STM32_DMA_SCR_DBM; } chan->chan_reg.dma_scr = dma_scr; if (chan->desc && chan->desc->cyclic) { dma_scr &= ~(STM32_DMA_SCR_DBM | STM32_DMA_SCR_CIRC); stm32_dma_write(dmadev, STM32_DMA_SCR(chan->id), dma_scr); } chan->chan_reg.dma_sndtr = stm32_dma_read(dmadev, STM32_DMA_SNDTR(chan->id)); chan->status = DMA_PAUSED; dev_dbg(chan2dev(chan), "vchan %p: paused\n" , &chan->vchan); }
stm32_dma_pause
和 stm32_dma_resume
: DMA传输的暂停与恢复这两个函数实现了DMA引擎框架中对正在进行的传输进行临时暂停 和精确续传 的功能。这是一项高级功能, 要求驱动程序能够精确地记录硬件在被暂停时的状态, 并在恢复时正确地重构这个状态。
stm32_dma_pause
: 暂停DMA传输此函数的核心原理是安全地停止硬件传输, 并记录下被暂停时的状态 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 static int stm32_dma_pause (struct dma_chan *c) { struct stm32_dma_chan *chan = to_stm32_dma_chan(c); unsigned long flags; int ret; if (chan->status != DMA_IN_PROGRESS) return -EPERM; spin_lock_irqsave(&chan->vchan.lock, flags); ret = stm32_dma_disable_chan(chan); if (!ret) stm32_dma_handle_chan_paused(chan); spin_unlock_irqrestore(&chan->vchan.lock, flags); return ret; }
stm32_dma_resume
: 从暂停点恢复DMA传输此函数是整个机制中最复杂的部分。它的核心原理是利用暂停时保存的状态, 精确地重新计算并配置硬件寄存器, 使得DMA传输能够从中断点无缝地继续, 而不是从头开始 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 static int stm32_dma_resume (struct dma_chan *c) { if (chan->status != DMA_PAUSED) return -EPERM; spin_lock_irqsave(&chan->vchan.lock, flags); if (!chan->next_sg) sg_req = &chan->desc->sg_req[chan->desc->num_sgs - 1 ]; else sg_req = &chan->desc->sg_req[chan->next_sg - 1 ]; ndtr = sg_req->chan_reg.dma_sndtr; offset = (ndtr - chan_reg.dma_sndtr); offset <<= FIELD_GET(STM32_DMA_SCR_PSIZE_MASK, chan_reg.dma_scr); spar = sg_req->chan_reg.dma_spar; sm0ar = sg_req->chan_reg.dma_sm0ar; sm1ar = sg_req->chan_reg.dma_sm1ar; if (chan_reg.dma_scr & STM32_DMA_SCR_PINC) stm32_dma_write(dmadev, STM32_DMA_SPAR(id), spar + offset); else stm32_dma_write(dmadev, STM32_DMA_SPAR(id), spar); if (!(chan_reg.dma_scr & STM32_DMA_SCR_MINC)) offset = 0 ; stm32_dma_write(dmadev, STM32_DMA_SNDTR(id), chan_reg.dma_sndtr); if (chan_reg.dma_scr & (STM32_DMA_SCR_CIRC | STM32_DMA_SCR_DBM)) chan_reg.dma_scr &= ~(STM32_DMA_SCR_CIRC | STM32_DMA_SCR_DBM); chan->status = DMA_IN_PROGRESS; chan_reg.dma_scr |= STM32_DMA_SCR_EN; stm32_dma_write(dmadev, STM32_DMA_SCR(id), chan_reg.dma_scr); spin_unlock_irqrestore(&chan->vchan.lock, flags); dev_dbg(chan2dev(chan), "vchan %p: resumed\n" , &chan->vchan); return 0 ; }
stm32_dma_terminate_all
和 stm32_dma_synchronize
: 传输的终止与同步这两个函数是DMA引擎框架中负责流程控制 的两个关键回调。terminate_all
提供了一种强制、立即中止 所有传输的机制, 主要用于错误恢复或驱动卸载。而synchronize
则提供了一种阻塞式等待 机制, 确保在CPU继续执行之前, 所有已启动的DMA传输都已完成。
stm32_dma_terminate_all
: 强制中止所有传输此函数的核心原理是执行一个**”焦土式”的清理操作**。它会立即停止硬件, 并清理掉该通道上所有正在进行和排队等待的传输任务, 包括软件描述符和硬件状态。
这个过程必须是原子的, 以防止在清理过程中有新的中断或任务提交发生, 因此它在自旋锁 的保护下执行关键步骤:
停止当前传输 : 如果有一个传输正在硬件上运行(chan->desc
有效), 它会首先在软件层面将其标记为完成(即使是被中止的), 并调用vchan_terminate_vdesc
来通知使用者驱动该任务已终止。然后, 它调用stm32_dma_stop
来强制停止硬件。
清空队列 : 它调用vchan_get_all_descriptors
将virt-dma
框架中该通道的所有队列(submitted
和issued
)中的描述符全部 移动到一个临时的本地链表中。
释放资源 : 在释放自旋锁之后, 它调用vchan_dma_desc_free_list
来安全地释放从队列中取出的所有描述符的内存。将内存释放操作放在锁之外是一个好的实践, 可以让临界区尽可能短。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 static int stm32_dma_terminate_all (struct dma_chan *c) { struct stm32_dma_chan *chan = to_stm32_dma_chan(c); unsigned long flags; LIST_HEAD(head); spin_lock_irqsave(&chan->vchan.lock, flags); if (chan->desc) { dma_cookie_complete(&chan->desc->vdesc.tx); vchan_terminate_vdesc(&chan->desc->vdesc); if (chan->busy) stm32_dma_stop(chan); chan->desc = NULL ; } vchan_get_all_descriptors(&chan->vchan, &head); spin_unlock_irqrestore(&chan->vchan.lock, flags); vchan_dma_desc_free_list(&chan->vchan, &head); return 0 ; }
stm32_dma_synchronize
: 同步CPU执行与DMA完成此函数的核心原理是充当一个阻塞式的同步点或”栅栏” 。当一个使用者驱动调用dmaengine_synchronize()
时, 调用线程会在此函数中暂停执行, 直到该DMA通道上所有先前已提交并启动的传输都全部被硬件完成 。
实现方式 : STM32 DMA驱动程序完全委托 virt-dma
框架来实现这个功能。vchan_synchronize
是一个通用的辅助函数, 它内部实现了一个等待循环, 检查虚拟通道的desc_issued
队列是否为空。只有当中断处理程序将所有已发出的描述符都处理完毕(即从队列中移除)后, 这个等待才会结束, 函数才会返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 static void stm32_dma_synchronize (struct dma_chan *c) { struct stm32_dma_chan *chan = to_stm32_dma_chan(c); vchan_synchronize(&chan->vchan); }
‘stm32_dma_prep_dma_memcpy’: 准备内存到内存的DMA传输 此函数是STM32 DMA驱动程序中负责内存到内存 (’memcpy’) 类型传输的回调函数。 它的核心原理是将一个用户请求的大块内存复制作, 分解(segmentation) 成一个或多个符合STM32 DMA硬件单次传输能力上限的小块传输任务 , 并为每一个小块任务精心准备一套完整的硬件寄存器配置, 最终将这些配置打包成一个描述符(descriptor) , 提交给’virt-dma’框架排队等待执行。
这个函数是实现高效内存复制的关键, 它通过以下步骤将一个抽象的’memcpy’请求转化为具体的硬件指令集:
分片计算 : 由于STM32 DMA的’NDTR’(数据传输数量)寄存器有其最大值(’STM32_DMA_ALIGNED_MAX_DATA_ITEMS’), 一个大的内存复制请求必须被拆分成多个DMA传输。 函数首先通过’DIV_ROUND_UP’计算出总共需要多少个这样的小块传输, 这决定了需要分配多大的描述符。
描述符分配 : 它动态地分配一个’stm32_dma_desc’结构体, 该结构体尾部包含一个足够容纳所有小块传输配置(’sg_req’)的弹性数组。
循环准备 : 函数进入一个循环, 为每一个小块传输(chunk)进行配置:
它计算出当前小块的长度 (’xfer_count’)。
它调用辅助函数 (’stm32_dma_get_best_burst’) 来为当前传输块智能地选择最优的突发传输尺寸(burst size) , 以最大限度地提高总线利用率。
绘制蓝图 : 最关键的一步, 它将启动这个小块传输所需的所有硬件寄存器值, 预先计算 并缓存到描述符对应的’sg_req[i].chan_reg’中。 这包括:
方向(’DIR’) : 明确设置为’STM32_DMA_MEM_TO_MEM’。
地址 : 将源地址(’src offset’)写入’SPAR’(外设地址寄存器), 将目标地址(’dest offset’)写入’SM0AR’(内存地址寄存器)。 在M2M模式下, ‘SPAR’被复用为源地址。
地址增量(’PINC’, ‘MINC’) : 同时使能 外设和内存地址的自增, 这是’memcpy’的标准行为。
数据计数(’SNDTR’) : 设置为当前小块的长度。
突发尺寸(’PBURST’, ‘MBURST’) : 设置为前面计算出的最优值。
中断使能(’TCIE’, ‘TEIE’) : 使能传输完成和传输错误中断。
提交给框架 : 当所有小块的“蓝图”都绘制完成后, 整个描述符通过’vchan_tx_prep’提交给’virt-dma’框架。 ‘vchan_tx_prep’会为其添加通用的接口, 并将其放入“已分配”队列, 等待用户驱动的最终提交。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 static struct dma_async_tx_descriptor *stm32_dma_prep_dma_memcpy ( struct dma_chan *c, dma_addr_t dest, dma_addr_t src, size_t len, unsigned long flags) { struct stm32_dma_chan *chan = to_stm32_dma_chan(c); enum dma_slave_buswidth max_width ; struct stm32_dma_desc *desc ; size_t xfer_count, offset; u32 num_sgs, best_burst, threshold; int dma_burst, i; num_sgs = DIV_ROUND_UP(len, STM32_DMA_ALIGNED_MAX_DATA_ITEMS); desc = kzalloc(struct_size(desc, sg_req, num_sgs), GFP_NOWAIT); if (!desc) return NULL ; desc->num_sgs = num_sgs; threshold = chan->threshold; for (offset = 0 , i = 0 ; offset < len; offset = xfer_count, i ) { xfer_count = min_t (size_t , len - offset, STM32_DMA_ALIGNED_MAX_DATA_ITEMS); max_width = DMA_SLAVE_BUSWIDTH_1_BYTE; best_burst = stm32_dma_get_best_burst(len, STM32_DMA_MAX_BURST, threshold, max_width); dma_burst = stm32_dma_get_burst(chan, best_burst); if (dma_burst < 0 ) { kfree(desc); return NULL ; } stm32_dma_clear_reg(&desc->sg_req[i].chan_reg); desc->sg_req[i].chan_reg.dma_scr = FIELD_PREP(STM32_DMA_SCR_DIR_MASK, STM32_DMA_MEM_TO_MEM) | FIELD_PREP(STM32_DMA_SCR_PBURST_MASK, dma_burst) | FIELD_PREP(STM32_DMA_SCR_MBURST_MASK, dma_burst) | STM32_DMA_SCR_MINC | STM32_DMA_SCR_PINC | STM32_DMA_SCR_TCIE | STM32_DMA_SCR_TEIE; desc->sg_req[i].chan_reg.dma_sfcr |= STM32_DMA_SFCR_MASK; desc->sg_req[i].chan_reg.dma_sfcr |= FIELD_PREP(STM32_DMA_SFCR_FTH_MASK, threshold); desc->sg_req[i].chan_reg.dma_spar = src + offset; desc->sg_req[i].chan_reg.dma_sm0ar = dest + offset; desc->sg_req[i].chan_reg.dma_sndtr = xfer_count; desc->sg_req[i].len = xfer_count; } desc->cyclic = false ; return vchan_tx_prep(&chan->vchan, &desc->vdesc, flags); }
stm32_dma_chan_irq
: DMA通道硬件中断的主处理程序 (顶层)此函数是硬件中断的直接入口点 。当一个STM32 DMA通道完成传输、遇到错误或达到半程点时, 硬件会触发一个中断, 内核的中断子系统最终会调用这个函数来响应该事件。它是中断处理的”上半部”(Top Half), 运行在**硬中断上下文(hardirq context)**中。
其核心原理是快速响应、分类处理、最小化延迟 。它必须在尽可能短的时间内完成, 以便让CPU可以尽快响应其他可能更重要的中断。
获取状态快照 : 它首先获取保护通道状态的自旋锁, 然后立即读取所有相关的硬件状态寄存器(中断状态、流控制、FIFO控制)。这确保了它处理的是触发中断那一刻的精确硬件状态。
错误优先处理 : 它会优先检查并处理各种错误标志(如FIFO错误FEI
、直接模式错误DMEI
)。对于每个错误, 它会清除硬件中的中断标志位, 并检查该错误中断是否被使能。这是一种严谨的做法, 确保驱动只对它明确要求关注的事件做出反应, 并将错误信息记录到内核日志中。
成功路径处理 : 接下来, 它处理最重要的成功事件——传输完成(TCI
)。如果传输完成中断发生并且被使能, 且通道不处于软件暂停状态, 它不会在此函数中执行所有后续逻辑, 而是将控制权转交给stm32_dma_handle_chan_done
函数, 由该函数执行更复杂的”下半部”逻辑。
其他事件处理 : 它会检查并清除其他类型的中断标志(如半传输完成HTI
), 即使当前驱动逻辑没有为它们附加特殊操作, 清除标志也是必需的, 以防止中断风暴。
兜底与清理 : 最后, 它会检查是否有任何未识别的中断标志被置位, 如果有, 则报告一个通用错误。完成所有操作后, 它释放自旋锁, 并返回IRQ_HANDLED
告知内核该中断已成功处理。
在STM32H750这样的单核系统上, spin_lock_irq
依然至关重要。它能防止在硬中断上下文中处理通道状态时, 被另一个中断(甚至是同一个中断的再次触发, 尽管不太可能)或被抢占的任务上下文中的代码(如dma_issue_pending
)同时访问和修改chan
结构体, 从而保证了数据的一致性和完整性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 static irqreturn_t stm32_dma_chan_irq (int irq, void *devid) { struct stm32_dma_chan *chan = devid; struct stm32_dma_device *dmadev = stm32_dma_get_dev(chan); u32 status, scr, sfcr; spin_lock(&chan->vchan.lock); status = stm32_dma_irq_status(chan); scr = stm32_dma_read(dmadev, STM32_DMA_SCR(chan->id)); sfcr = stm32_dma_read(dmadev, STM32_DMA_SFCR(chan->id)); if (status & STM32_DMA_FEI) { stm32_dma_irq_clear(chan, STM32_DMA_FEI); status &= ~STM32_DMA_FEI; if (sfcr & STM32_DMA_SFCR_FEIE) { if (!(scr & STM32_DMA_SCR_EN) && !(status & STM32_DMA_TCI)) dev_err(chan2dev(chan), "FIFO Error\n" ); else dev_dbg(chan2dev(chan), "FIFO over/underrun\n" ); } } if (status & STM32_DMA_DMEI) { stm32_dma_irq_clear(chan, STM32_DMA_DMEI); status &= ~STM32_DMA_DMEI; if (sfcr & STM32_DMA_SCR_DMEIE) dev_dbg(chan2dev(chan), "Direct mode overrun\n" ); } if (status & STM32_DMA_TCI) { stm32_dma_irq_clear(chan, STM32_DMA_TCI); if (scr & STM32_DMA_SCR_TCIE) { if (chan->status != DMA_PAUSED) stm32_dma_handle_chan_done(chan, scr); } status &= ~STM32_DMA_TCI; } if (status & STM32_DMA_HTI) { stm32_dma_irq_clear(chan, STM32_DMA_HTI); status &= ~STM32_DMA_HTI; } if (status) { stm32_dma_irq_clear(chan, status); dev_err(chan2dev(chan), "DMA error: status=0x%08x\n" , status); if (!(scr & STM32_DMA_SCR_EN)) dev_err(chan2dev(chan), "chan disabled by HW\n" ); } spin_unlock(&chan->vchan.lock); return IRQ_HANDLED; }
stm32_dma_handle_chan_done
: DMA传输完成事件的逻辑处理器 (中层)此函数是中断处理的”下半部”逻辑的起点 。它由stm32_dma_chan_irq
在确认一次成功的传输完成后调用, 负责根据DMA的**工作模式(循环模式或单次模式)**来执行不同的状态更新和后续操作。
其核心原理是区分不同的DMA工作流, 并精确地管理描述符和硬件状态 :
循环模式 (cyclic
) :
通知客户端 : 它立即调用vchan_cyclic_callback
。这个函数通常会调度一个tasklet, 由该tasklet在软中断上下文中去执行客户端驱动提供的回调函数。这遵循了将耗时工作移出硬中断上下文的最佳实践。
硬件管理 : 它接着检查硬件是否处于自动循环模式(CIRC
或DBM
)。
如果不是 , 意味着驱动正在”手动”模拟循环传输。在这种情况下, 硬件在完成一轮后已经停止, 必须调用stm32_dma_post_resume_reconfigure
来完全重新编程 DMA寄存器并手动重启 下一次传输。
如果是 , 硬件会自动处理循环。驱动只需为硬件的双缓冲模式准备好下一个数据段(stm32_dma_configure_next_sg
)即可。
单次/Scatter-Gather模式 :
状态更新 : 它将通道标记为空闲(busy = false
), 状态为完成(DMA_COMPLETE
)。
描述符处理 : 它检查当前完成的是否是整个传输请求的最后一个数据段 (next_sg == num_sgs
)。
如果是, 意味着整个DMA任务(cookie)已完成。它调用vchan_cookie_complete
来最终通知客户端, 并将chan->desc
清空, 表示通道现在完全空闲。
如果不是, 意味着这只是Scatter-Gather列表中的一个中间段, 整个任务尚未完成。
启动下一次传输 : 无论当前任务是否完成, 它都会调用stm32_dma_start_transfer
。这个函数会检查: 如果当前任务还有剩余的数据段, 它会立即启动下一个段的传输; 如果当前任务已完成, 它会检查是否有新的DMA任务在排队, 如果有则启动它。这实现了DMA传输的无缝衔接, 保持硬件的最高利用率。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 static void stm32_dma_handle_chan_done (struct stm32_dma_chan *chan, u32 scr) { if (!chan->desc) return ; if (chan->desc->cyclic) { vchan_cyclic_callback(&chan->desc->vdesc); if (chan->trig_mdma) return ; stm32_dma_sg_inc(chan); if (!(scr & (STM32_DMA_SCR_CIRC | STM32_DMA_SCR_DBM))) stm32_dma_post_resume_reconfigure(chan); else if (scr & STM32_DMA_SCR_DBM && chan->desc->num_sgs > 2 ) stm32_dma_configure_next_sg(chan); } else { chan->busy = false ; chan->status = DMA_COMPLETE; if (chan->next_sg == chan->desc->num_sgs) { vchan_cookie_complete(&chan->desc->vdesc); chan->desc = NULL ; } stm32_dma_start_transfer(chan); } }
stm32_dma_post_resume_reconfigure
: 在暂停/恢复后重新配置并启动DMA (底层)此函数是一个非常具体的、用于恢复和重启DMA硬件状态的底层操作函数 。它主要被stm32_dma_handle_chan_done
在”软件模拟循环传输”的特殊场景下调用。
其核心原理是将DMA通道的所有关键寄存器恢复到一次传输的初始状态, 然后重新使能通道 。这模拟了硬件循环模式的行为, 但完全由软件控制。
清理和定位 : 它首先清除任何可能残留的中断状态, 然后根据当前scatter-gather索引, 精确地定位到本轮传输应该使用的那个数据段描述符(sg_req
)。
寄存器恢复 : 它从sg_req
中读取预先保存好的初始值, 并依次写回到DMA硬件寄存器中, 包括:
SNDTR
: 传输数据项的数量。
SPAR
: 外设地址。
SM0AR
/SM1AR
: 内存地址(在双缓冲模式下有两个)。
模式恢复 : 它检查原始配置, 如果需要, 重新在流控制寄存器(SCR
)中设置CIRC
(循环)或DBM
(双缓冲)标志。
重启 : 最后, 它在SCR
中设置EN
(使能)位, 重新启动DMA硬件 , 开始新一轮的传输。
这个函数体现了驱动程序如何通过精细的软件控制, 扩展硬件的功能, 实现硬件本身不支持或在特定条件下无法使用的操作模式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 static void stm32_dma_post_resume_reconfigure (struct stm32_dma_chan *chan) { struct stm32_dma_device *dmadev = stm32_dma_get_dev(chan); struct stm32_dma_sg_req *sg_req ; u32 dma_scr, status, id; id = chan->id; dma_scr = stm32_dma_read(dmadev, STM32_DMA_SCR(id)); status = stm32_dma_irq_status(chan); if (status) stm32_dma_irq_clear(chan, status); if (!chan->next_sg) sg_req = &chan->desc->sg_req[chan->desc->num_sgs - 1 ]; else sg_req = &chan->desc->sg_req[chan->next_sg - 1 ]; stm32_dma_write(dmadev, STM32_DMA_SNDTR(chan->id), sg_req->chan_reg.dma_sndtr); stm32_dma_write(dmadev, STM32_DMA_SPAR(id), sg_req->chan_reg.dma_spar); stm32_dma_write(dmadev, STM32_DMA_SM0AR(id), sg_req->chan_reg.dma_sm0ar); stm32_dma_write(dmadev, STM32_DMA_SM1AR(id), sg_req->chan_reg.dma_sm1ar); if (chan->chan_reg.dma_scr & STM32_DMA_SCR_DBM) { dma_scr |= STM32_DMA_SCR_DBM; if (chan->chan_reg.dma_scr & STM32_DMA_SCR_CT) dma_scr &= ~STM32_DMA_SCR_CT; else dma_scr |= STM32_DMA_SCR_CT; } else if (chan->chan_reg.dma_scr & STM32_DMA_SCR_CIRC) { dma_scr |= STM32_DMA_SCR_CIRC; } stm32_dma_write(dmadev, STM32_DMA_SCR(chan->id), dma_scr); stm32_dma_configure_next_sg(chan); stm32_dma_dump_reg(chan); dma_scr |= STM32_DMA_SCR_EN; stm32_dma_write(dmadev, STM32_DMA_SCR(chan->id), dma_scr); dev_dbg(chan2dev(chan), "vchan %p: reconfigured after pause/resume\n" , &chan->vchan); }
stm32_dma_of_xlate: 从设备树”翻译”DMA请求 此函数是STM32 DMA驱动程序中一个至关重要的回调函数 。它在DMA引擎框架中的角色是一个**”翻译器” (translator)**。当一个外设的客户端驱动(例如SPI, I2C, UART驱动)需要使用DMA时, 内核的DMA引擎框架会调用这个函数, 将客户端设备树中描述的、高度硬件相关的DMA请求信息, “翻译”成一个标准的、可供客户端驱动使用的struct dma_chan
句柄。
此函数是连接客户端驱动 和DMA控制器驱动 之间的桥梁, 其核心原理如下:
解析硬件描述 : 函数的输入dma_spec
包含了从客户端设备树的dmas
属性中解析出的原始数据。例如, SPI驱动的设备树节点中可能会有一行dmas = <&dma1 7 0x420 0x800>;
。dma_spec->args
就包含了{7, 0x420, 0x800, ...}
这些原始的整数。
翻译为有意义的配置 : 函数做的第一件事就是将这些匿名的数字翻译成一个有意义的、驱动内部使用的stm32_dma_cfg
结构。这正是”xlate”的含义:
args[0]
(7) -> cfg.channel_id
: 硬件DMA流(Stream)的编号。
args[1]
-> cfg.request_line
: 外设的请求线。这是配置STM32特有的**DMAMUX(DMA多路复用器)**的关键, 它将特定的外设请求(如SPI1_TX)路由到指定的DMA流。
args[2]
-> cfg.stream_config
: DMA流的静态配置, 如传输方向、数据宽度、优先级等, 这些值可以直接写入硬件寄存器。
args[3]
-> cfg.features
: 特殊功能标志, 如是否使用FIFO。
验证与资源定位 : 函数会进行严格的有效性检查, 确保设备树中提供的值在硬件支持的范围内。然后, 它使用channel_id
作为索引, 直接定位到DMA控制器驱动内部代表该硬件流的stm32_dma_chan
结构。
通道申请与锁定 : 最关键的一步是调用dma_get_slave_channel
。这是一个向通用DMA引擎框架发出的请求, 意图**”申请并独占”**这个物理DMA通道。如果该通道已经被其他驱动占用, 此调用将失败, 从而正确地管理了共享硬件资源的访问。
应用静态配置 : 一旦成功获得通道的独占使用权, 它就会调用stm32_dma_set_config
, 将从设备树中解析出的静态配置(特别是DMAMUX的请求线)应用到硬件或软件状态中。这完成了通道的预配置, 使其准备好为该特定外设服务。
返回通用句柄 : 最后, 它返回一个标准的struct dma_chan
句柄。客户端驱动接收到这个句柄后, 就可以使用硬件无关 的、标准的DMA引擎API(如dmaengine_prep_slave_sg
, dmaengine_submit
等)来发起传输, 而无需关心任何STM32特有的寄存器细节。
在STM32H750系统上, 这个函数是整个DMA子系统正常工作的基石。它完美地体现了设备树的设计哲学: 将板级的、不可变的硬件连接信息(哪个SPI连接到哪个DMA流的哪个请求线)保留在设备树中, 而驱动程序则负责在运行时解析这些信息, 从而实现了内核代码与具体硬件配置的解耦。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 static struct dma_chan *stm32_dma_of_xlate (struct of_phandle_args *dma_spec, struct of_dma *ofdma) { struct stm32_dma_device *dmadev = ofdma->of_dma_data; struct device *dev = dmadev->ddev.dev; struct stm32_dma_cfg cfg ; struct stm32_dma_chan *chan ; struct dma_chan *c ; if (dma_spec->args_count < 4 ) { dev_err(dev, "Bad number of cells\n" ); return NULL ; } cfg.channel_id = dma_spec->args[0 ]; cfg.request_line = dma_spec->args[1 ]; cfg.stream_config = dma_spec->args[2 ]; cfg.features = dma_spec->args[3 ]; if (cfg.channel_id >= STM32_DMA_MAX_CHANNELS || cfg.request_line >= STM32_DMA_MAX_REQUEST_ID) { dev_err(dev, "Bad channel and/or request id\n" ); return NULL ; } chan = &dmadev->chan[cfg.channel_id]; c = dma_get_slave_channel(&chan->vchan.chan); if (!c) { dev_err(dev, "No more channels available\n" ); return NULL ; } stm32_dma_set_config(chan, &cfg); return c; }
stm32_dma_probe: STM32 DMA控制器探测与初始化 此函数是STM32 DMA驱动程序的核心, 是驱动与硬件交互的起点。当内核根据设备树匹配到DMA控制器设备时, 就会调用此函数。它的核心原理是执行一个全面的初始化序列, 将原始的DMA硬件资源(寄存器、时钟、中断)进行配置, 并将其封装成一个标准的Linux dma_device
对象, 然后将这个对象注册到内核的DMA引擎(DMA Engine)子系统中 。完成此过程后, 系统中其他需要DMA服务的设备驱动(如SPI, I2C, UART)就可以通过标准的DMA Engine API来请求和使用DMA通道了。
整个初始化过程可以分为以下几个关键阶段:
资源获取与硬件复位 :
函数首先为驱动的核心数据结构(stm32_dma_device
)分配内存。
它从设备树中获取DMA控制器的寄存器基地址, 并通过ioremap
将其映射到内核可访问的地址空间。
它获取并使能DMA控制器所需的时钟。
它获取复位控制器句柄, 并对DMA硬件执行一次”断言-延时-解除断言”的复位序列, 确保硬件处于一个已知的初始状态。
向DMA引擎描述硬件能力 :
函数会填充dma_device
结构体。这部分至关重要, 它相当于驱动向通用的DMA引擎框架提交的一份”硬件能力说明书”。
通过dma_cap_set
设置能力位掩码, 声明此DMA支持从设备模式(DMA_SLAVE
)、循环模式(DMA_CYCLIC
)以及可能的内存到内存模式(DMA_MEMCPY
)。
它为dma_device
结构体中的一系列函数指针(device_alloc_chan_resources
, device_prep_slave_sg
等)赋值。这些指针指向本驱动内部实现的、针对STM32 DMA硬件的特定操作函数。当上层驱动调用一个通用的DMA API时, DMA引擎核心会通过这些函数指针来调用STM32的特定实现, 这正是硬件抽象的核心。
它还定义了支持的数据宽度、传输方向、对齐要求等硬件特性。
初始化DMA通道 :
DMA控制器有多个通道, 函数会遍历所有通道, 为每一个通道初始化其软件表示(stm32_dma_chan
)。
它会初始化vchan
(虚拟通道)系统, 这是DMA引擎用于管理多个客户端对物理通道请求的机制。
为了提高效率, 它会预先计算好每个通道的中断清除和状态标志寄存器的地址和位掩码。
注册与中断设置 :
通过dma_async_device_register
, 将完全配置好的dma_device
对象正式注册到DMA引擎中。
它为每个DMA通道获取对应的中断号(IRQ), 并调用devm_request_irq
为每个中断注册一个处理函数(stm32_dma_chan_irq
)。
通过of_dma_controller_register
, 将此DMA控制器注册为设备树的DMA提供者。这一步会注册一个xlate
(翻译)函数, 该函数负责解析其他设备在设备树中定义的DMA请求(例如, dmas = <&dma1 5 ...>
), 并将其转换为本驱动可以理解的通道和配置信息。
最后, 它会启用运行时电源管理(Runtime PM), 允许DMA控制器在不使用时自动进入低功耗状态。
对于STM32H750这样的单核系统, devm_*
系列函数的使用确保了即使在初始化中途出错, 所有已分配的资源(内存、中断、时钟等)也能够被自动安全地释放, 极大地增强了驱动的健壮性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 static int stm32_dma_slave_config (struct dma_chan *c, struct dma_slave_config *config) { struct stm32_dma_chan *chan = to_stm32_dma_chan(c); memcpy (&chan->dma_sconfig, config, sizeof (*config)); if (config->peripheral_size) { config->peripheral_config = &chan->mdma_config; config->peripheral_size = sizeof (chan->mdma_config); chan->trig_mdma = true ; } chan->config_init = true ; return 0 ; } static int stm32_dma_probe (struct platform_device *pdev) { struct stm32_dma_chan *chan ; struct stm32_dma_device *dmadev ; struct dma_device *dd ; struct resource *res ; struct reset_control *rst ; int i, ret; dmadev = devm_kzalloc(&pdev->dev, sizeof (*dmadev), GFP_KERNEL); if (!dmadev) return -ENOMEM; dd = &dmadev->ddev; dmadev->base = devm_platform_get_and_ioremap_resource(pdev, 0 , &res); if (IS_ERR(dmadev->base)) return PTR_ERR(dmadev->base); dmadev->clk = devm_clk_get(&pdev->dev, NULL ); if (IS_ERR(dmadev->clk)) return dev_err_probe(&pdev->dev, PTR_ERR(dmadev->clk), "Can't get clock\n" ); ret = clk_prepare_enable(dmadev->clk); if (ret < 0 ) { dev_err(&pdev->dev, "clk_prep_enable error: %d\n" , ret); return ret; } dmadev->mem2mem = of_property_read_bool(pdev->dev.of_node, "st,mem2mem" ); rst = devm_reset_control_get(&pdev->dev, NULL ); if (IS_ERR(rst)) { ret = PTR_ERR(rst); if (ret == -EPROBE_DEFER) goto clk_free; } else { reset_control_assert(rst); udelay(2 ); reset_control_deassert(rst); } dma_set_max_seg_size(&pdev->dev, STM32_DMA_ALIGNED_MAX_DATA_ITEMS); dma_cap_set(DMA_SLAVE, dd->cap_mask); dma_cap_set(DMA_PRIVATE, dd->cap_mask); dma_cap_set(DMA_CYCLIC, dd->cap_mask); dd->device_alloc_chan_resources = stm32_dma_alloc_chan_resources; dd->device_free_chan_resources = stm32_dma_free_chan_resources; dd->device_tx_status = stm32_dma_tx_status; dd->device_issue_pending = stm32_dma_issue_pending; dd->device_prep_slave_sg = stm32_dma_prep_slave_sg; dd->device_prep_dma_cyclic = stm32_dma_prep_dma_cyclic; dd->device_config = stm32_dma_slave_config; dd->device_pause = stm32_dma_pause; dd->device_resume = stm32_dma_resume; dd->device_terminate_all = stm32_dma_terminate_all; dd->device_synchronize = stm32_dma_synchronize; dd->src_addr_widths = BIT(DMA_SLAVE_BUSWIDTH_1_BYTE) | BIT(DMA_SLAVE_BUSWIDTH_2_BYTES) | BIT(DMA_SLAVE_BUSWIDTH_4_BYTES); dd->dst_addr_widths = BIT(DMA_SLAVE_BUSWIDTH_1_BYTE) | BIT(DMA_SLAVE_BUSWIDTH_2_BYTES) | BIT(DMA_SLAVE_BUSWIDTH_4_BYTES); dd->directions = BIT(DMA_DEV_TO_MEM) | BIT(DMA_MEM_TO_DEV); dd->residue_granularity = DMA_RESIDUE_GRANULARITY_BURST; dd->copy_align = DMAENGINE_ALIGN_32_BYTES; dd->max_burst = STM32_DMA_MAX_BURST; dd->max_sg_burst = STM32_DMA_ALIGNED_MAX_DATA_ITEMS; dd->descriptor_reuse = true ; dd->dev = &pdev->dev; INIT_LIST_HEAD(&dd->channels); if (dmadev->mem2mem) { dma_cap_set(DMA_MEMCPY, dd->cap_mask); dd->device_prep_dma_memcpy = stm32_dma_prep_dma_memcpy; dd->directions |= BIT(DMA_MEM_TO_MEM); } for (i = 0 ; i < STM32_DMA_MAX_CHANNELS; i++) { chan = &dmadev->chan[i]; chan->id = i; chan->vchan.desc_free = stm32_dma_desc_free; vchan_init(&chan->vchan, dd); chan->mdma_config.ifcr = res->start; chan->mdma_config.ifcr += STM32_DMA_IFCR(chan->id); chan->mdma_config.tcf = STM32_DMA_TCI; chan->mdma_config.tcf <<= STM32_DMA_FLAGS_SHIFT(chan->id); } ret = dma_async_device_register(dd); if (ret) goto clk_free; for (i = 0 ; i < STM32_DMA_MAX_CHANNELS; i++) { chan = &dmadev->chan[i]; ret = platform_get_irq(pdev, i); if (ret < 0 ) goto err_unregister; chan->irq = ret; ret = devm_request_irq(&pdev->dev, chan->irq, stm32_dma_chan_irq, 0 , dev_name(chan2dev(chan)), chan); if (ret) { dev_err(&pdev->dev, "request_irq failed with err %d channel %d\n" , ret, i); goto err_unregister; } } ret = of_dma_controller_register(pdev->dev.of_node, stm32_dma_of_xlate, dmadev); if (ret < 0 ) { dev_err(&pdev->dev, "STM32 DMA DMA OF registration failed %d\n" , ret); goto err_unregister; } platform_set_drvdata(pdev, dmadev); pm_runtime_set_active(&pdev->dev); pm_runtime_enable(&pdev->dev); pm_runtime_get_noresume(&pdev->dev); pm_runtime_put(&pdev->dev); dev_info(&pdev->dev, "STM32 DMA driver registered\n" ); return 0 ; err_unregister: dma_async_device_unregister(dd); clk_free: clk_disable_unprepare(dmadev->clk); return ret; }
STM32 DMA 驱动的注册与初始化 此代码片段是STM32 DMA驱动程序的入口点。它的核心作用不是执行DMA传输, 而是将整个DMA驱动程序作为一个platform_driver
注册到Linux内核的设备模型中 。这使得内核能够识别并管理STM32的DMA控制器硬件。
其工作原理如下:
定义电源管理操作 : 首先, 它定义了一个dev_pm_ops
结构体, stm32_dma_pm_ops
。这个结构体包含了一系列函数指针, 用于响应内核的电源管理事件。通过使用SET_SYSTEM_SLEEP_PM_OPS
和SET_RUNTIME_PM_OPS
宏, 它将驱动内部的挂起/恢复函数(如stm32_dma_pm_suspend
)与标准的系统睡眠和运行时电源管理(Runtime PM)钩子关联起来。这使得DMA控制器可以在系统进入低功耗状态或自身空闲时被安全地关闭, 并在需要时被唤醒。
定义平台驱动主体 : 接着, 它定义了platform_driver
的核心结构体stm32_dma_driver
。这个结构体是驱动的”身份证”, 它告诉内核:
名称(name
) : 驱动的名字是 “stm32-dma”。
匹配方式(of_match_table
) : 驱动通过一个名为stm32_dma_of_match
的表来与设备树中的节点进行匹配。当内核在设备树中找到一个节点的compatible
属性与此表中的条目匹配时, 就认为找到了一个该驱动可以管理的设备。
电源管理(pm
) : 将上一步定义的电源管理操作挂接到驱动上。
探测函数(probe
) : 当匹配成功时, 内核应该调用的核心初始化函数是stm32_dma_probe
。这个probe
函数(未在此代码段中显示)才是真正负责初始化DMA硬件、分配通道等工作的函数。
注册驱动 : 最后, stm32_dma_init
函数作为驱动的初始化入口, 调用platform_driver_register
将stm32_dma_driver
结构体提交给内核。subsys_initcall
宏则是一个链接器指令, 它告诉内核在系统启动过程中的一个特定阶段(在核心子系统初始化之后)来调用stm32_dma_init
函数, 从而完成驱动的注册。
在STM32H750系统上, __init
宏会将stm32_dma_init
函数放入一个特殊的内存段, 这段内存在内核启动完成后可以被释放, 从而为内存资源有限的MCU节省宝贵的RAM。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 static const struct of_device_id stm32_dma_of_match [] = { { .compatible = "st,stm32-dma" , }, { }, }; MODULE_DEVICE_TABLE(of, stm32_dma_of_match); static const struct dev_pm_ops stm32_dma_pm_ops = { SET_SYSTEM_SLEEP_PM_OPS(stm32_dma_pm_suspend, stm32_dma_pm_resume) SET_RUNTIME_PM_OPS(stm32_dma_runtime_suspend, stm32_dma_runtime_resume, NULL ) }; static struct platform_driver stm32_dma_driver = { .driver = { .name = "stm32-dma" , .of_match_table = stm32_dma_of_match, .pm = &stm32_dma_pm_ops, }, .probe = stm32_dma_probe, }; static int __init stm32_dma_init (void ) { return platform_driver_register(&stm32_dma_driver); } subsys_initcall(stm32_dma_init);
drivers/dma/stm32/stm32-mdma.c STM32高性能DMA架构解析:MDMA与DMA的核心差异及最佳实践 在STM32的高性能产品线中(如STM32H7和STM32U5系列), STMicroelectronics引入了一种强大的分层DMA架构, 同时包含了传统的DMA/BDMA控制器和一颗性能更强的MDMA(主DMA)控制器。对于开发者而言, 理解这两者的工作原理、设计哲学、优势和使用场景, 是充分发挥MCU性能、优化系统功耗的关键。
核心比喻:物流中心 vs. 专属快递员
DMA/BDMA (普通/基本DMA) : 可以看作是每个外设(如SPI、UART)的“专属快递员 ”。它的任务明确、路径固定, 主要负责将指定外设的数据传入或传出内存, 响应速度快, 功耗较低, 但灵活性和吞吐量有限。
MDMA (主DMA) : 则像是整个系统的“中央物流中心 ”。它是一个独立的、高性能的数据搬运处理器, 拥有访问所有系统总线和内存的最高权限(Master地位)。它可以处理来自任何地方、发往任何地方的大批量数据请求, 吞吐量巨大, 调度灵活, 但启动和运行的开销也相对更高。
一、 工作原理与设计哲学 DMA/BDMA 的原理与作用
原理 : DMA/BDMA是一个从设备 (Slave) , 紧密耦合于外设。它的工作通常由外设触发。例如, 当UART接收到一个字节后, 会向DMA控制器发出一个请求。DMA控制器在获得总线访问权后, 自动将UART数据寄存器的内容搬运到预先配置好的内存地址, 然后释放总线, 等待下一次请求。这个过程完全无需CPU干预。
设计 : 它的设计目标是解放CPU, 处理低速、持续、可预测的外设数据流 。其请求映射通常是固定的, 例如, SPI1的发送请求只能连接到DMA1的特定通道/流。它主要连接在性能较低的AHB/APB总线上。
MDMA 的原理与作用
原理 : MDMA是一个主设备 (Master) , 与CPU处于同等地位, 可以主动发起对总线矩阵的访问。它连接在MCU内部最高速的64位AXI总线 上, 能够以极高的速度在内存(内部SRAM、外部SDRAM)和外设之间传输数据。
设计 : 它的设计目标是处理高速、大批量的数据传输, 尤其是复杂的内存到内存操作 。其最关键的特性是拥有一个DMA请求路由器 (DMA Router) , 允许将来自任何外设的DMA请求灵活地路由到任意一个空闲的MDMA通道进行处理, 提供了极大的系统设计灵活性。
二、 核心异同点对比
特性
DMA / BDMA (专属快递员)
MDMA (物流中心)
硬件角色
从设备 (Slave), 由外设触发
主设备 (Master), 可主动发起传输
总线连接
32位 AHB / APB 总线
64位 AXI 总线
性能吞吐量
中低
非常高
请求映射
固定或有限映射 (外设 -> 特定通道)
灵活路由 (外设 -> 任意通道)
中断机制
每个通道/流通常有独立的中断
所有通道共享一个全局中断
数据宽度
最高支持32位 (4字节)
最高支持64位 (8字节)
主要任务
外设到内存 (P2M), 内存到外设 (M2P)
内存到内存 (M2M), 高速P2M/M2P
相同点 :
核心目标相同 : 两者都是为了在没有CPU干预的情况下传输数据, 降低CPU负载, 提升系统并行处理能力。
基本配置相似 : 都需要配置源地址、目标地址、传输数据量、传输方向、数据宽度等基本参数。
均可被内核框架管理 : 在Linux/RTOS中, 它们都可以被统一的DMA引擎框架管理, 为上层驱动提供标准化的API。
三、 各自优势与最佳使用场景 DMA/BDMA 的优势与场景
优势 :
低延迟 : 专为外设服务, 响应外设请求的延迟较低。
低功耗 : 处理简单任务时, 无需唤醒和驱动强大的AXI总线和MDMA控制器, 系统整体功耗更低。
配置简单 : 请求映射固定, 软件配置相对直接。
最佳使用场景 :
串行通信 : UART、SPI、I2C 的连续数据收发。
数据采集 : ADC 连续模式下, 将转换结果自动存入内存。
音频/信号生成 : I2S、SAI、DAC 的数据流传输。
定时器触发 : 由定时器更新事件触发, 向GPIO或内存块进行固定模式的数据传输。
MDMA 的优势与场景
优势 :
极高吞吐量 : 64位总线宽度使其在处理大块数据时效率无与伦比。
极高灵活性 : 请求路由器允许动态分配通道, 优化系统负载。
强大的内存操作 : 为内存到内存的复制、移动、格式转换等操作提供了硬件级加速。
支持高级特性 : 如链表模式, 可实现复杂的、无CPU干预的数据处理链。
最佳使用场景 :
图形图像处理 : 在内存中搬运LTDC的帧缓冲区, 或者配合DMA2D进行图层混合。
大数据块搬运 : 在内部RAM、外部SDRAM、QuadSPI Flash之间高速传输固件、资源文件等。
复杂数据处理 : 利用链表模式对数据包进行预处理, 如添加/剥离包头, 将分散的数据包拼接成连续的缓冲区。
CPU算法卸载 : 将CPU密集型的memcpy
, memset
等操作完全交给MDMA完成。
四、 补充要点:协同工作与高级特性 1. 协同工作:DMA链式操作 (DMA Chaining) MDMA和DMA并非孤立工作, 它们可以形成强大的DMA链 。这是一个非常高效的工作模式:
场景 : 一个传感器通过SPI接口将数据传输进来。
流程 :
DMA工作 : 普通DMA负责处理SPI的外设请求, 将一小块数据(例如1KB)从SPI数据寄存器搬运到内存中的一个Ping-Pong缓冲区A。
触发MDMA : 当DMA完成对缓冲区A的填充后, 它不产生CPU中断, 而是自动触发MDMA的一个通道 。
MDMA接力 : MDMA被触发后, 开始将缓冲区A中的1KB数据搬运到外部SDRAM的一个巨大缓冲区中进行存储, 或者对这1KB数据进行格式转换后存入另一个处理缓冲区。
并行处理 : 在MDMA处理缓冲区A数据的同时 , 普通DMA已经开始向另一个Ping-Pong缓冲区B中填充下一块SPI数据。
优势 : 整个数据流(从外设到最终内存)完全自动化, CPU只在需要时(例如整个大缓冲区都满了)才介入一次, 极大地提升了实时性能和系统效率。
2. MDMA的高级特性:链表模式 (Linked-List Mode) MDMA支持一种强大的“链表”模式, 这本质上是一种硬件级的分散-聚集(Scatter-Gather)DMA 。
原理 : 开发者可以在内存中构建一个描述符链表, 每个描述符包含了一次独立传输的所有信息(源地址、目标地址、长度等)以及指向下一个描述符的指针。
工作方式 : 启动MDMA时, 只需告诉它第一个描述符的地址。MDMA会自动完成第一个传输, 然后根据指针加载并执行第二个、第三个…直到遇到链表末尾。
优势 : 这允许MDMA在完全没有CPU干预的情况下, 执行一系列复杂且非连续的传输任务, 是实现高性能网络协议栈、文件系统数据读写等应用的神器。
3. 资源规划与功耗考量
“杀鸡焉用牛刀” : 永远不要用MDMA去处理一个低速的UART数据流。这样做不仅配置更复杂, 而且会不必要地激活AXI总线和MDMA的时钟, 增加系统功耗。
按需分配 : 在设计系统时, 应该将持续的、低带宽的外设任务分配给DMA/BDMA, 将突发的、高带宽的内存操作任务预留给MDMA。
功耗优化 : 充分利用普通DMA可以让MDMA和AXI总线矩阵长时间处于时钟门控关闭的低功耗状态, 这对于电池供电的设备至关重要。
结论 STM32的MDMA和DMA/BDMA共同构成了一个功能互补、性能分层的强大数据传输架构。它们不是竞争关系, 而是协同工作的伙伴 。掌握“简单持续用DMA, 复杂批量用MDMA, 高效联动靠链式触发 ”的核心思想, 并根据具体应用场景进行合理规划, 才能将STM32H7/U5系列MCU的潜力发挥到极致, 设计出真正高性能、低功耗的嵌入式系统。