[toc]
drivers/dma/dmaengine.h
struct dma_chan / struct dma_chan_dev: DMA 通道对象与其 sysfs 设备包装对象的语义建模
struct dma_chan:DMAengine 框架中“可被客户端申请与提交事务”的通道抽象,承载运行期状态(cookie、完成度、客户端引用计数、路由信息等)以及与上层可见性的关联(sysfs/debugfs 相关字段)。struct dma_chan_dev:把通道映射为 Linux 设备模型中的一个struct device,用于 sysfs 节点呈现与生命周期管理;同时保存dma_chan *的反向指针(也就是你前面讨论的chan->dev->chan这一条关键可见性路径)。
这两个结构体之所以分开,是因为 DMAengine 需要同时满足:
- 运行期快速访问与调度(
dma_chan) - 设备模型可见性、热插拔/解绑生命周期、sysfs 目录树一致性(
dma_chan_dev+struct device)
1 | /** |
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 | /** |
dma_cookie_assign: 为传输任务分配唯一票据(Cookie)
1 | /** |
dma_cookie_complete: 标记一个传输任务已完成
1 | /** |
dma_cookie_status: 无锁查询传输状态
1 | /** |
dma_set_residue 和 dma_set_in_flight_bytes: 状态设置辅助函数
1 | /* |
DMA引擎回调辅助函数集
此代码片段定义了一组静态内联函数, 它们是Linux内核DMA引擎(DMA Engine)框架的通用辅助工具。它们的核心原理是提供一套标准化的、安全的、并且向后兼容的机制, 用于处理DMA传输完成后的回调操作。
当一个设备驱动(例如SPI驱动)请求一次DMA传输后, 它会提供一个”回调函数”。当DMA控制器硬件完成传输并触发中断时, DMA控制器驱动需要调用这个回调函数来通知原始的设备驱动”你的数据传输已完成”。这组辅助函数就是为了让这个通知过程变得更加简洁、健壮和统一。
struct dmaengine_desc_callback: DMA引擎描述符回调信息结构体
这是一个纯粹的数据结构, 作为一个临时容器来存储一次回调所需的所有信息。
1 | /* |
dmaengine_desc_get_callback: 获取DMA描述符中的回调信息
此函数用于从一个DMA传输描述符中安全地复制出回调信息。
1 | /** |
dmaengine_desc_callback_invoke: 调用回调函数
此函数负责执行存储在临时结构体中的回调函数, 并处理了新旧两种回调类型的兼容性问题。
1 | /** |
dmaengine_desc_get_callback_invoke: 获取并立即调用DMA回调
这是一个便利的封装函数, 将获取和调用两个步骤合二为一。
1 | /** |
dmaengine_desc_callback_valid: 检查回调是否有效
这是一个简单的检查函数, 用于判断一个描述符是否有关联的完成回调。
1 | /** |
1 | static inline int dmaengine_pause(struct dma_chan *chan) |
drivers/dma/dmaengine.c
1 | 这段注释的中文翻译如下(保持技术语义,不增加引申): |
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 | /* dma_transaction_type - DMA事务类型/索引的枚举 */ |
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 | /* dmaengine_unmap_pool: 描述一个用于DMA unmap操作的内存池 */ |
dma_async_tx_descriptor_init: 初始化DMA传输描述符
此函数是通用DMA引擎(DMA Engine)框架中的一个基础”构造函数”。它的核心原理是为一个新创建的DMA传输描述符 (dma_async_tx_descriptor) 执行最基本、最必要的初始化步骤, 主要是将其与它所属的DMA通道 (dma_chan) 牢固地关联起来。
可以把它看作是给一张空白的DMA任务单盖上”所属部门”的印章。在执行这个操作之前, 描述符只是一块内存; 执行之后, 它就正式成为了某个特定DMA通道的一个待处理任务。
这是一个非常轻量级的函数, 但它的作用至关重要, 因为描述符与通道的关联是后续所有DMA操作(提交、启动、查询状态)的基础。
1 | /* |
dma_list_mutex 与 dma_device/dma_chan/dma_device_list: 全局可见性与注册注销一致性总结
这次讨论的核心结论是:dma_list_mutex 在 DMAengine 中并不是“只保护 dma_device_list 这个链表”,而是用于维护DMAengine 核心全局状态与其不变量的一把统一互斥锁。它保障注册/注销/重平衡等路径对外呈现的状态切换是原子且一致的。
dma_device_list 的语义
dma_device_list表示 DMAengine 框架中全局已注册 DMA 控制器集合。- 一旦某个
struct dma_device进入该集合,它及其下属资源就进入“框架全局可见、可枚举、可分配”的状态。
struct dma_device 与 struct dma_chan 的关系与“全局性”
struct dma_device是 DMAengine 对一个 DMA 控制器的抽象,它包含一个channels链表,链表元素是struct dma_chan。struct dma_chan虽然从属某个dma_device,但在框架语义上属于全局可见资源的一部分,因为:- 框架会全局枚举通道用于分配与匹配;
- 框架维护全局索引结构(如通道表/重平衡逻辑)会读取并依赖通道状态;
- sysfs/debugfs 通过
dma_chan_dev将通道暴露到设备模型,外部路径可通过该入口间接触达通道。
因此,“通道不是独立的全局链表节点”不代表它不是全局可见状态;它的可见性与分配资格仍是 DMAengine 核心必须一致管理的全局属性。
dma_list_mutex 保护的对象
dma_list_mutex 保护的不仅是某一个变量,而是以下共享状态及其一致性关系:
全局设备集合与全局索引
dma_device_listdma_ida(设备 dev_id 分配器)- 与通道分配相关的全局表/索引(例如重平衡涉及的结构)
设备/通道的“可见性切换点”
device->chancnt等与通道数量相关的框架判断依据chan->dev->chan等“通过 device model 入口能否访问到通道”的关键指针
锁的本质目的是:保证注册/注销/重平衡路径看到的状态是自洽的,而不是看到“半注册/半注销”的中间态。
注册 vs 注销:为什么有的地方你看到没加锁、有的地方加了锁
DMAengine 的典型组织方式是把生命周期分成两段:
构建阶段(construction)
- 仅初始化对象内部字段
- 对外不可见或不会被全局分配/枚举路径触达
- 因此某些字段赋值(如
chan->dev->chan = chan)可能不需要立刻拿dma_list_mutex
发布/撤销发布阶段(publish/unpublish)
- 把对象加入/移出全局集合或全局索引
- 切换可见性入口(例如断开
chan->dev->chan) - 必须在
dma_list_mutex下完成关键切换,以防并发路径获取新引用或观察到中间态
注销路径里,__dma_async_device_channel_unregister() 的核心就是在锁下完成“撤销发布”,然后把 device_unregister()、free_percpu() 这类重操作放到锁外,降低锁持有时间并避免锁顺序问题。
channel_table[cap][cpu]->chan: DMAengine 通道选择缓存、硬件通道注册与“置 NULL 是否影响正在 DMA”
1) channel_table[cap][cpu]->chan 是什么,有什么作用
对象类型与语义:
channel_table[cap][cpu]->chan保存的是一个struct dma_chan *指针,其语义是:
针对能力cap、CPUcpu的“推荐通道缓存指针”。
它不是数量,不是硬件寄存器配置,也不是“CPU 固定绑定某硬件通道”的规则表。为什么是 per-CPU:
per_cpu_ptr(channel_table[cap], cpu)取得的是该能力维度表在某个 CPU 上的实例,目的是在多 CPU 场景下减少共享争用并引入“邻近性偏好”。单核系统仍沿用该抽象接口,但语义不变。它的作用范围:
channel_table的作用仅限于 “通道选择/分配阶段”:- 快速给出一个候选
dma_chan - 避免每次分配都遍历
dma_device_list与device->channels做全量扫描
因此它是性能优化缓存,不是执行路径的必需结构。
- 快速给出一个候选
2) 为什么要先清空再重新平衡(重建推荐通道)
你看到的两行代码体现了一个明确的缓存维护模式:失效(invalidate)→ 重建(rebuild)。
先清空的目的:
per_cpu_ptr(channel_table[cap], cpu)->chan = NULL;的目的不是“停止 DMA”,而是:- 宣告旧推荐结果无效
- 避免重平衡过程中旧指针被继续当作可靠候选
- 让并发路径在看到
NULL时走慢路径重新选择,而不是继续使用旧指针
再写回新值的目的:
per_cpu_ptr(channel_table[cap], cpu)->chan = chan;将重平衡算法选出的“当前最优”通道写回缓存,使之后同类请求回到快路径。
1 | /* |
3) 清空 channel_table 会不会把正在 DMA 的通道“断掉”
不会。
正在执行的 DMA 不依赖
channel_table:
正在执行的 DMA 事务由以下要素维持:- 客户端已持有的
struct dma_chan *chan(通道句柄) - 已提交的描述符(
dma_async_tx_descriptor)与驱动回调(如device_issue_pending) - 硬件寄存器与 DMA 状态机
- 客户端已持有的
channel_table只影响未来“选通道”:
将缓存置为NULL只会导致:- 下次通道选择时缓存未命中
- 走慢路径重新扫描/重新选择
它不会撤销通道引用,不会改硬件寄存器,也不会终止当前搬运。
补充的严谨点:
硬件负责搬运本身;软件仍负责完成通知、cookie 更新、错误处理、链式推进等,但这些完成路径走chan的中断/回调链路,而不是channel_table。
4) DMA 硬件通道是如何在 DMAengine 中“注册并被使用”的
硬件规模的输入:
设备树中的dma-channels(或等价属性)描述的是该 DMA 控制器实例的物理通道数量上限。驱动 probe 时创建软件通道对象:
控制器驱动在probe中会根据nr_channels初始化对应数量的struct dma_chan(通常组织在device->channels链表中,并且注册后该链表结构一般不再修改)。注册到 DMAengine:
驱动通过注册接口把struct dma_device暴露给 DMAengine 核心,使得:dma_device进入全局可见集合(如dma_device_list)- 其
channels进入全局可枚举/可选择资源图
客户端使用方式:
客户端不是通过channel_table来“执行 DMA”,而是通过框架 API:- 先“申请/获得”一个
dma_chan *(可能命中缓存,也可能全量扫描) - 然后在该
chan上准备描述符、提交、issue_pending - 完成由中断/轮询推进
- 先“申请/获得”一个
channel_table在其中的定位:
它仅用于“获得通道时的快速候选”,并不参与“已获得通道后的传输执行”。
__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 | /* |
min_chan: 在指定能力与 CPU 邻近性约束下,选择 table_count 最小的通道并更新其计数
它的算法语义可以精确概括为:
在
dma_device_list的所有dma_device中,筛选出:- 具备目标能力
cap - 且不带
DMA_PRIVATE(表示不可用于公共分配/正在拆除或仅私用)
- 具备目标能力
在每个
device->channels中,筛选出client_count != 0的通道(即“已被至少一个客户端引用/占用”的通道)从这些候选通道中选出
table_count最小者作为min若存在“NUMA/CPU 邻近”的候选通道,则优先在邻近集合中选
table_count最小者作为localmin最终选
localmin(若存在)否则选min,并对其table_count++
1 | /** |
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]这个槽位中。
- 它遍历所有可能的DMA能力(
对于用户指定的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 | /** |
dma_async_device_register: 注册一个DMA控制器设备
此函数是Linux内核DMA引擎(DMA Engine)框架的核心入口点。当一个DMA控制器的硬件驱动(例如STM32H750的DMA或MDMA驱动)在探测(probe)过程中完成了自身的初始化后, 就会调用此函数, 将其所代表的DMA控制器及其所有的通道(channels)正式地、完整地注册到内核中。完成此调用后, 该DMA控制器就对系统的其他部分(即”客户端”驱动, 如SPI, I2C, Crypto等)变得可见、可发现和可使用。
其核心原理是一个全面的验证、初始化和集成过程, 确保只有功能完整、行为正确的DMA驱动才能被系统接受:
1 | /** |
__dma_async_device_channel_unregister: 从 DMAengine 注销单个通道并回收其内核对象
此函数是DMAengine 核心在注销 DMA 设备时,用于逐个撤销通道对外可见性与回收通道对象资源的内部函数。它由 dma_async_device_unregister() 在遍历 device->channels 时调用。
其核心原理是:把一个已注册的 dma_chan 从“框架可分配/可引用”的状态转换为“不可见/不可再引用”,并释放与该通道绑定的 ID、device model 对象以及 percpu 私有数据。
- 快速判定是否已注册:以
chan->local是否为NULL作为“该通道是否已经完成注册”的判据;若未注册则直接返回,避免重复注销。 - 引用安全性告警:若通道仍存在
client_count引用且驱动未提供device_release路径,则触发一次告警,提示存在“注销时仍被客户端持有引用”的风险。 - 受互斥保护的状态更新:在
dma_list_mutex保护下更新通道计数与通道指针,使框架视角下该通道立刻不可再被访问到。 - 回收全局/设备级 ID 与内核对象:释放
chan_ida分配的chan_id,注销device对象,并释放percpu的chan->local。
1 | /** |
为什么这里需要 mutex_lock(&dma_list_mutex);?
原因不是“多核才需要锁”,而是存在并发访问共享状态的可能性,即使在单核系统也一样成立(抢占、软中断、工作队列、线程调度都会造成并发交错)。
这把锁在这里至少同时保护了两类“必须一致更新”的共享状态:
device->chancnt的一致性
这个计数会被其他路径读取/依赖(例如注册/注销、通道表重构、甚至调试/枚举路径)。若不加锁,可能出现:- 并发注销导致计数丢更新(lost update)
- 并发注册/注销导致计数短暂错误,进而影响框架逻辑判断
chan->dev->chan的可见性切换
这是把通道从“可通过 device model 找到”切换为“不可找到”的关键动作。若不加锁,可能出现:- 另一路径正通过
chan->dev->chan获取通道指针并继续使用(在你置空之前/之后交错) - 造成“拿到半注销对象”的竞态窗口,进而引发 use-after-free 风险
- 另一路径正通过
这也是为什么函数在锁内只做“最关键的共享状态切换”,然后把 device_unregister()、free_percpu() 放到锁外:
锁用来建立一致性边界;重资源回收放到边界外执行,减少锁占用并避免锁嵌套风险。
dma_async_device_unregister / dmaenginem_async_device_register: 注销 DMA 设备与 devm 管理式注册回滚
你贴的这段代码来自 DMAengine 核心层,语义很集中:
dma_async_device_unregister:将一个struct dma_device从 DMAengine 全局视角彻底移除,并确保在移除过程中不会再被通道分配路径使用。dmaenginem_async_device_register:在完成注册后,挂一个 devm action,保证驱动解绑(detach)时自动触发上面的 unregister,实现“注册/回滚”的资源生命周期绑定。
关键点在于 DMAengine 框架内部的并发一致性与对象生命周期管理。即使单核,也必须用 mutex / 引用计数,因为内核上下文切换与中断上下文会带来并发可见性问题。
核心原理概述
先拆通道、再拆设备:先把
device->channels链表里每个通道从框架注销(调用__dma_async_device_channel_unregister),避免残留通道入口被上层继续引用。全局互斥保护关键全局结构:
dma_list_mutex保护 DMAengine 的全局设备列表、通道表(channel_table)与 IDA 分配等共享资源。用
DMA_PRIVATE强制“不可再被分配”:注销时先设置DMA_PRIVATE到cap_mask,其目的不是表达硬件能力,而是 让通道选择/重平衡逻辑把该设备视为不可供一般客户端使用,避免被加入channel_table。重平衡与回收:调用
dma_channel_rebalance()刷新全局通道表,然后释放dev_id(ida_free),最后dma_device_put()递减引用,允许对象最终释放。devm action 的语义:
dmaenginem_async_device_register在注册成功后,通过devm_add_action_or_reset()把“反注册动作”绑定到device->dev的生命周期:- 后续驱动 detach 时自动执行 unregister
- 若 action 添加失败,会立即执行回滚(reset),避免半注册状态
1 | /** |
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 | /** |
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 | /** |
dma_request_chan: 统一的DMA从通道请求接口
本代码片段展示了Linux内核中一个关键且通用的API——dma_request_chan。这是设备驱动程序(“客户端”)用来向DMA子系统请求一个专用的、用于外设数据传输的DMA通道(“从通道”)的标准入口点。其核心功能是提供一个统一的接口,它能自动通过现代的固件描述(如设备树)或旧的过滤机制来查找并分配一个合适的DMA通道,同时优雅地处理驱动间的加载顺序依赖问题(即“探测延迟”,Probe Deferral)。
实现原理分析
该函数是连接设备驱动与具体DMA控制器驱动的核心枢纽。它将复杂的DMA通道匹配和分配过程抽象化,其实现策略是“固件优先,过滤为辅”。
统一的固件优先方法:
- 函数首先获取设备的固件节点(
fwnode),这是一个通用的句柄,可以是设备树(Device Tree)节点或ACPI节点。 - 设备树路径: 如果是设备树系统(
is_of_node),它会调用of_dma_request_slave_channel。这个函数会解析设备驱动对应的设备树节点中的dmas和dma-names属性。例如,一个UART的设备树节点可能会有dmas = <&mdma_channel_x ...>和dma-names = "rx",dma_request_chan(dev, "rx")就会精确地请求到mdma_channel_x这个物理通道。这是最现代、最精确的匹配方式。 - ACPI路径: 如果是ACPI系统,则调用
acpi_dma_request_slave_chan_by_name,执行类似的功能。
- 函数首先获取设备的固件节点(
优雅的依赖处理 (
EPROBE_DEFER):if (PTR_ERR(chan) == -EPROBE_DEFER): 这是一个至关重要的机制。当of_dma_request_slave_channel返回-EPROBE_DEFER时,意味着设备树中虽然描述了DMA通道,但对应的DMA控制器驱动尚未初始化完成。dma_request_chan会立即将这个错误码返回给调用者(如stm32_usart_serial_probe)。调用者看到这个错误后,会中止自己的初始化并同样返回-EPROBE_DEFER。这会通知内核驱动核心:“我的依赖(DMA控制器)还没准备好,请稍后**重新探测(re-probe)**我”。这完美地解决了驱动加载顺序的问题。
旧式过滤回退机制:
- 如果基于固件的查找失败(但不是因为
-EPROBE_DEFER),代码会进入一个回退路径。 mutex_lock(&dma_list_mutex): 锁定一个全局列表。list_for_each_entry_safe(d, _d, &dma_device_list, global_node): 它会遍历系统中所有已注册的DMA控制器设备。dma_filter_match: 对于每一个DMA控制器,它会使用一个filter函数和name来判断这个控制器是否能为当前设备dev服务。这是一种比较旧的、基于平台数据或硬件特定逻辑的匹配方法。- 如果回退机制也找不到任何匹配的通道,它会返回
-EPROBE_DEFER,寄希望于一个合适的DMA控制器可能在未来才被注册。
- 如果基于固件的查找失败(但不是因为
通道分配后的处理:
- 一旦成功获取到一个
dma_chan,函数会执行一系列“收尾”工作: chan->slave = dev;: 建立从DMA通道到客户端设备的反向链接,正式确立主从关系。sysfs_create_link: 在sysfs中创建符号链接。这极大地增强了系统的可观测性。例如,它会在DMA通道的sysfs目录下创建一个名为slave的符号链接,指向使用它的设备(如ttySTM0);同时,在设备的目录下也会创建一个指向DMA通道的链接。这使得系统管理员可以轻松地从命令行查看到设备与DMA通道的绑定关系。
- 一旦成功获取到一个
特定场景分析:单核、无MMU的STM32H750平台
硬件交互
- 设备树是关键: 在STM32H750平台上,
dma_request_chan的执行完全依赖于设备树(DTS)的正确配置。当stm32_usart_serial_probe调用dma_request_chan(&pdev->dev, "rx")时:dev就是usart1的platform_device。of_dma_request_slave_channel函数会被调用。- 它会查找
usart1节点下的dmas = <&dma1_stream0 ...>和dma-names = "rx", ...属性。 - 它发现”rx”对应的是
dma1_stream0。于是,它会向dma1(STM32的DMA控制器之一)的驱动请求分配第0个流(stream)。 dma1的驱动会返回一个代表DMA1_Stream0这个物理硬件的struct dma_chan实例。
- 硬件连接的软件体现: 这个返回的
dma_chan不仅仅是一个数据结构,它内部包含了操作DMA1_Stream0所有相关寄存器(如配置寄存器CR、源地址PAR、目标地址M0AR等)所需的信息和函数指针。dma_request_chan的成功调用,标志着STM32 USART的硬件DMA请求信号(USART_DR寄存器有数据)与STM32 DMA控制器的物理通道之间的软件链路已经建立。
单核环境影响
- DMA的主要目的是将数据搬运工作从CPU卸载,这对于只有一个核心的STM32H750来说,可以极大地提升性能,使其在DMA传输数据的同时能够执行其他计算任务。
- 函数中的
mutex_lock(&dma_list_mutex)在单核系统上用于防止在遍历全局DMA设备列表时被其他任务抢占,从而保证了列表的完整性。
无MMU影响
dma_request_chan函数本身不处理内存地址,所以与MMU无关。- 然而,它所属的DMA引擎框架是MMU/IOMMU感知的。在无MMU的系统上,后续的DMA操作(如
dma_map_single或使用dma_alloc_coherent返回的地址)会直接使用物理地址。DMA引擎框架的抽象使得设备驱动代码(如STM32 USART驱动)几乎无需关心底层是物理地址还是DMA虚拟地址,从而具有很好的可移植性。
代码分析
1 | static const struct dma_slave_map *dma_filter_match(struct dma_device *device, |
find_candidate: DMA私有通道的搜索与获取
本代码片段展示了dma_request_chan内部使用的一个核心辅助函数——find_candidate。其核心功能是根据指定的匹配条件(能力掩码和过滤函数),在一个DMA控制器设备上查找一个合适的、可用的私有通道,并立即尝试获取(acquire)它。这是一个“查找并锁定”的原子性操作,它通过管理私有通道计数和状态,确保了找到的通道被独占式地分配给请求者,并且它在找不到可用通道时,能够正确地返回-EPROBE_DEFER以支持驱动探测延迟机制。
实现原理分析
此函数是DMA引擎为“私有”或“专用”通道(即不通过通用池分配,而是由特定驱动直接请求的通道)设计的分配逻辑。它将“寻找”和“获取”两个步骤紧密结合,以处理并发和资源可用性问题。
第一步:寻找候选者 (
private_candidate):- 函数的第一步是调用
private_candidate(代码未显示)。这个内部函数负责实际的搜索工作。 - 它会遍历
device(一个DMA控制器)上的所有物理通道。 - 对于每个通道,它会使用
mask(例如,必须支持DMA_SLAVE能力)和fn(一个驱动提供的、硬件相关的过滤函数)来进行匹配。fn是关键,它允许进行精确的匹配,例如,fn可能会检查一个DMA通道是否连接到了指定的UART TX请求线上。 - 如果
private_candidate找到一个满足所有条件的空闲通道,它会返回该通道的指针;否则返回NULL。
- 函数的第一步是调用
第二步:尝试获取与状态管理:
- 如果
private_candidate成功找到了一个候选通道(if (chan)),函数会立即尝试正式地获取它。 dma_cap_set(DMA_PRIVATE, device->cap_mask): 在DMA控制器设备上设置DMA_PRIVATE标志。这是一个状态标记,告知DMA引擎,该控制器至少有一个通道正在被私有(独占)使用。如注释所言,这会改变某些内部管理逻辑,如引用计数的平衡。device->privatecnt++: 递增该控制器的私有通道使用计数器。err = dma_chan_get(chan): 这是关键的获取操作。它会去真正地“锁定”这个通道,通常是增加通道自身的引用计数,使其不能再被分配给其他请求者。- 获取失败处理:
- 如果
dma_chan_get失败,说明在找到候选者和尝试获取它的极短时间窗口内,通道的状态发生了变化(例如,被另一个任务或中断获取)。 - 代码会执行对称的清理操作:递减
privatecnt计数器,并在计数器归零时清除DMA_PRIVATE标志。 - 一个特殊情况是
err == -ENODEV,这表示底层硬件设备或其模块已经被移除。这是一个罕见的竞态条件,函数的响应是果断地将整个DMA控制器设备从全局列表中移除。
- 如果
- 如果
返回值的意义:
- 函数的返回值设计得非常精妙,它能向调用者传达三种不同的状态:
- 成功:
private_candidate找到通道,且dma_chan_get成功。函数返回一个有效的struct dma_chan *指针。 - 需要延迟探测:
private_candidate没有找到任何满足条件的空闲通道。函数返回ERR_PTR(-EPROBE_DEFER)。这告诉调用者(如dma_request_chan),现在没有可用的资源,但未来可能会有(例如,当另一个驱动释放了它占用的通道时),所以应该推迟当前驱动的初始化,稍后再试。 - 其他错误:
private_candidate找到了通道,但dma_chan_get失败。函数返回具体的错误码(如-EBUSY)。
- 成功:
- 函数的返回值设计得非常精妙,它能向调用者传达三种不同的状态:
代码分析
1 | /** |
__dma_request_channel: 按能力掩码与过滤器申请一个独占 DMA 通道
这个函数是 DMAengine “通道分配器”的核心入口之一:在全局 DMA 设备链表中遍历所有已注册的 struct dma_device,对每个控制器调用 find_candidate() 尝试找到一个满足 mask(能力集合)与 fn(可选过滤器)的空闲通道,并以“独占/私有(DMA_PRIVATE)”语义把该通道占用起来;成功则返回该 struct dma_chan *,失败则返回 NULL。
STM32H750(单核、无 MMU)场景下的意义
- 单核不会改变此函数的并发模型:Linux 仍可能在同一核上发生抢占/中断导致的并发交错,因此
dma_list_mutex仍是必要的全局互斥。 - 无 MMU不是主线 Linux 的常规形态;若你是在特定移植环境中复用该逻辑,则这里的“全局链表扫描 + 错误指针语义(如
-EPROBE_DEFER)”对驱动探测顺序更敏感:依赖未就绪时必须允许延迟探测,否则会把“暂时不可用”误判为“永久无通道”。
1 | /** |
DMA_PRIVATE: 控制器私有模式标记的作用、设计原因与“通道预留”实现方式
1) DMA_PRIVATE 的作用与含义
DMA_PRIVATE 在 DMAengine 里不是“这个通道私有”,而是更接近:
- 对
struct dma_device的策略标记:表示该 DMA 控制器处于“私有分配模式”,框架会倾向于把它从公共的通道分配池/重平衡视图里剔除,避免公共分配路径继续把它当成候选资源。 - 与
privatecnt配套:privatecnt表示该控制器当前有多少“私有持有者”。只有当privatecnt归零,才允许清除DMA_PRIVATE,让控制器回到公共可分配状态。 - 典型使用场景:某些驱动希望“我拿到这个控制器的一个通道之后,后续也不要让公共分配器再把这个控制器分给其他不受控的客户端”,从而获得更强的控制器级可预测性(例如对 runtime PM、带宽、优先级策略的整体控制)。
重要结论:
DMA_PRIVATE表达的是“控制器退出公共分配体系”的语义,而不是“预留几个通道”。
2) 为什么 DMAengine 把 DMA_PRIVATE 设计成“控制器级别开关”?
主要是为了保持 DMAengine 的全局分配模型简单且一致,具体原因可分为三点:
框架能力模型是 device 粒度
DMAengine 的能力描述(cap_mask、方向、位宽、burst、残余粒度)都在struct dma_device上,通道选择本来就先按 device 粗筛再选 chan。把DMA_PRIVATE放在device->cap_mask,能用最低成本让控制器整体改变“是否参与公共池”的状态。DMA_PRIVATE的目标是影响全局分配视图
DMAengine 维护全局通道表/重平衡逻辑时,需要一个粗粒度的“这个控制器要不要出现在公共候选里”的开关。若做成通道级私有,就意味着全局表必须维护额外维度(每个通道的公/私),并引入更复杂的规则(私有优先级、冲突回滚、状态同步),会显著增加维护与热路径开销。控制器内共享状态普遍存在
很多 DMA 控制器即使通道寄存器独立,也会共享:时钟/电源域、全局中断状态、仲裁/优先级策略、QoS 配置等。
因此把“私有”定义为 device 级,能减少“控制器级策略被多个互不知情的客户端同时改变”的风险。
结论:
这是一个“用更强的不变量换简单实现与可维护性”的设计选择。
3) 怎么实现“一个控制器里有些公有有些私有”(你的目标:固定预留几条通道给某外设/驱动)
你要的其实是:通道预留(channel reservation),推荐的做法是“让公共分配器永远选不到那几条预留通道”,而不是把设备设为 DMA_PRIVATE。
常见实现路径(从框架语义到驱动落地)如下:
3.1 预留策略 A:通过过滤器让公共分配永远跳过预留通道
思路:
- 让“公共分配路径”的过滤器拒绝预留通道
- 让“特定外设/驱动”的过滤器只接受预留通道
效果:
- 同一控制器里,预留通道只会被特定客户端拿到
- 其他客户端仍能通过公共路径拿到剩余通道
- 不需要
DMA_PRIVATE,也不会触发控制器级“退出公共池”
在 STM32 MDMA 场景对应落地通常是:
- 设备树 xlate(比如
stm32_mdma_of_xlate)传入fn_param,过滤器根据request/priority/…以及你定义的“预留通道集合”来判定是否允许。 - 或者在
filter_fn里加入“如果通道 id 属于 reserved_set 且请求不是目标外设,则返回 false”。
3.2 预留策略 B:驱动内部维护 chan_reserved 位图并在过滤器中强制排除
这是一种更“硬”的预留:
- 驱动维护一个
chan_reserved位图(你在 STM32 MDMA 驱动里也能看到类似思想:secure 通道会被标记并排除) - 公共分配过滤器直接拒绝
chan_reserved的通道 - 特定外设路径可以走“专用 xlate/专用过滤器”去允许这些通道(或在它申请前动态切换预留位)
优点:行为稳定、成本低
缺点:需要你明确管理“谁有权用预留通道”,否则容易变成“永远不可用”的资源浪费。
3.3 预留策略 C:让预留通道在系统启动时就被目标驱动长期持有
思路:
- 目标驱动在 probe 时提前申请并持有通道,不释放(
client_count一直非 0) - 公共分配扫描时会因
client_count != 0把它当 busy 跳过
优点:实现最简单,几乎不改 DMAengine/过滤器
缺点:需要目标驱动生命周期与通道绑定,且对热插拔/错误恢复不友好
private_candidate: 在单个 DMA 控制器内筛选一个“可私有化分配”的空闲通道
private_candidate() 的核心功能是:在给定的 struct dma_device *dev(一个 DMA 控制器)内部,按顺序执行三类约束筛选,最终返回一个“当前空闲且满足条件”的 struct dma_chan *:
- 能力匹配:如果传入了
mask,则必须通过dma_device_satisfies_mask(dev, mask);否则该控制器直接不参与候选。([Linux Git Repositories][1]) - 多通道控制器的一致性约束:若该控制器
chancnt > 1且当前还不是DMA_PRIVATE,则要求 该控制器的所有通道都必须处于“未被公共分配”的状态(即所有通道client_count == 0),否则拒绝返回任何通道。其目的在于:避免同一控制器出现“部分通道公共分配、部分通道私有分配”的混合状态,因为后续find_candidate()会把DMA_PRIVATE置位,语义上要把整个控制器从通用分配器里摘除。([Linux Git Repositories][1]) - 逐通道筛选:遍历
dev->channels,跳过 busy 通道(client_count != 0),再用可选过滤器fn(chan, fn_param)做驱动自定义约束筛选(例如 STM32 MDMA 里可排除 reserved/secure 通道),找到第一个满足条件的通道就返回。([Linux Git Repositories][1])
注意:private_candidate() 不做占用(不增加引用、不改 DMA_PRIVATE、不改 privatecnt),它只负责“在本控制器内找一个可行通道”。真正的“私有化占用 + 获取通道”是在上一层 find_candidate() 里完成。([Linux Git Repositories][1])
1 | /** |











