[toc]
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 | /** |
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
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_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 | /* |
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_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 | /** |








