[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框架是分阶段演进的,主要形成了两大核心组件:

  1. 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)。
  2. 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-mappingdmaengine两个层面协同工作。

1. DMA映射层 (dma-mapping)
这是所有DMA操作的基础。一个设备驱动使用它的典型流程是:

  1. 分配缓冲区:驱动调用 dma_alloc_coherent() 分配一个长期使用的、CPU和设备都能一致性访问的缓冲区;或者使用普通内存(如kmalloc),在需要进行DMA时通过 dma_map_single()dma_map_sg()(用于分散/汇集列表)进行临时映射
  2. 获取设备可用地址:映射函数会返回一个 dma_addr_t 类型的地址。这个地址是总线地址,可以直接编程到设备的DMA寄存器中。同时,内核会处理好必要的Cache刷新。
  3. 启动DMA:驱动将dma_addr_t地址和传输长度写入设备硬件,启动传输。
  4. 等待完成:设备完成传输后,通常会通过中断通知驱动。
  5. 同步数据:驱动在中断处理函数中,必须调用 dma_sync_*_for_cpu() 来确保CPU能看到设备写入内存的最新数据(例如,使相关的CPU Cache失效)。
  6. 解除映射:对于临时映射的缓冲区,驱动必须调用 dma_unmap_single()dma_unmap_sg() 来释放映射。

2. DMA引擎层 (dmaengine)
这是一个更上层的客户端-服务器模型:

  1. 请求通道:消费者驱动(如SPI驱动)调用 dma_request_chan()dmaengine请求一个DMA通道。
  2. 准备描述符:消费者驱动配置好一个或多个描述DMA传输的描述符dma_async_tx_descriptor),包括源地址、目标地址、长度以及完成后的回调函数。对于非连续内存,会使用分散/汇集列表(scatterlist)。
  3. 提交任务:驱动调用 dmaengine_submit() 将描述符提交给DMA通道。
  4. 启动传输:驱动调用 dma_async_issue_pending() 启动所有已提交的传输。dmaengine核心会找到对应的DMAC驱动,并调用其回调函数来对硬件进行编程。
  5. 完成通知: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
/**
* vchan_issue_pending - 将已提交的描述符移动到已发出列表
* @vc: 要更新的虚拟通道
*
* 调用者必须持有 vc->lock.
*/
static inline bool vchan_issue_pending(struct virt_dma_chan *vc)
{
/*
* lockdep_assert_held 是一个调试工具, 用于在运行时验证调用者是否确实持有了指定的锁.
* 这强制实施了正确的锁使用规则, 防止竞态条件.
*/
lockdep_assert_held(&vc->lock);

/*
* 这是核心操作: list_splice_tail_init.
* 它将 vc->desc_submitted 链表中的所有节点, 整体移动到 vc->desc_issued 链表的尾部.
* 操作完成后, vc->desc_submitted 链表会变为空.
* 这是一个 O(1) 操作, 无论链表有多长, 速度都非常快.
*/
list_splice_tail_init(&vc->desc_submitted, &vc->desc_issued);
/*
* 返回 vc->desc_issued 链表是否不为空.
* 如果成功移动了任何描述符, 此表达式为 true.
* 如果本来就没有待处理的描述符, 则为 false.
*/
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
/*
* vchan_find_desc - 在虚拟通道中查找一个描述符
* @vc: 要搜索的虚拟通道
* @cookie: 要查找的传输的唯一ID
* @return: 成功时返回指向 virt_dma_desc 的指针, 失败时返回 NULL.
*/
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 是一个标准的内核宏, 用于安全地遍历一个链表.
* 它会遍历 vc->desc_issued 链表中的每一个 virt_dma_desc 结构体.
*/
list_for_each_entry(vd, &vc->desc_issued, node)
/*
* 比较当前描述符的 cookie 是否与要查找的 cookie 相同.
*/
if (vd->tx.cookie == cookie)
/*
* 如果找到匹配项, 立即返回该描述符的指针.
*/
return vd;

/*
* 如果遍历完整个链表都没有找到匹配项, 返回 NULL.
*/
return NULL;
}
/*
* 将函数导出, 使其对其他内核模块可用.
* 这是一个供具体DMA驱动使用的通用辅助API.
*/
EXPORT_SYMBOL_GPL(vchan_find_desc);

vchan_tx_prep: DMA描述符的最终封装与注册

此函数是**virt-dma (虚拟DMA通道) 框架中的一个核心辅助函数, 它是任何prep系列回调函数(如stm32_dma_prep_slave_sg)在完成工作前的最后一步**。

它的核心原理是为一个已经由具体驱动程序(如STM32 DMA驱动)填充了硬件相关细节的描述符, “穿上”一层通用的、标准化的外衣, 并将其安全地注册到虚拟通道的内部管理队列中

可以把它理解为DMA传输准备流程中的**”质检与归档”**步骤:

  1. 质检: 它为描述符填充了所有符合Linux通用DMA引擎(DMA Engine)标准的接口和字段。
  2. 归档: 它将这个准备就绪的描述符放入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
/**
* vchan_tx_prep - 准备一个描述符
* @vc: 分配此描述符的虚拟通道
* @vd: 要准备的虚拟描述符
* @tx_flags: 传递给 prepare 函数的标志参数
*/
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;

/*
* 1. 初始化通用描述符部分.
* dma_async_tx_descriptor_init 会将描述符的通用部分(&vd->tx)与它所属的通道(&vc->chan)关联起来.
* 这是最基本的内务处理.
*/
dma_async_tx_descriptor_init(&vd->tx, &vc->chan);
/*
* 2. 填充通用接口.
* 这里是实现硬件抽象的关键.
*/
// 将使用者驱动传入的标志 (如 DMA_PREP_INTERRUPT) 保存到通用描述符中.
vd->tx.flags = tx_flags;
// 设置 tx_submit 回调. 当使用者驱动调用 dmaengine_submit() 时, DMA引擎核心最终会调用 vchan_tx_submit.
vd->tx.tx_submit = vchan_tx_submit;
// 设置 desc_free 回调. 当这个描述符完成其生命周期后, 这个函数会被调用来释放其内存.
vd->tx.desc_free = vchan_tx_desc_free;

/*
* 3. 初始化传输结果字段.
* 将结果预设为"无错误", 剩余字节数为0.
*/
vd->tx_result.result = DMA_TRANS_NOERROR;
vd->tx_result.residue = 0;

/*
* 4. 将描述符安全地加入队列.
* 获取自旋锁并禁用中断, 创建一个临界区.
* 这可以防止在向链表添加节点时, 被中断处理程序或其他任务打断, 从而保证链表的完整性.
* 在STM32H750这样的单核系统上, 它主要防止与中断处理程序的竞态条件.
*/
spin_lock_irqsave(&vc->lock, flags);
/*
* 将这个准备好的描述符节点(&vd->node)添加到虚拟通道的 "desc_allocated" (已分配) 链表的尾部.
* 此时, 描述符已准备就绪, 但尚未被提交去执行.
*/
list_add_tail(&vd->node, &vc->desc_allocated);
/*
* 释放自旋锁, 恢复之前的中断状态.
*/
spin_unlock_irqrestore(&vc->lock, flags);

/*
* 5. 返回通用描述符的指针.
* 返回的是内嵌的 dma_async_tx_descriptor 结构体(&vd->tx)的地址,
* 而不是整个 virt_dma_desc 的地址. 这是为了向使用者驱动隐藏 virt-dma 框架的内部细节,
* 只暴露标准的接口.
*/
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); // 强制要求调用者持有锁

// 1. 隔离: 将描述符移动到"已终止"队列.
list_add_tail(&vd->node, &vc->desc_terminated);

// 2. 断链: 如果是活动的循环描述符, 则解除其循环资格.
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
/**
* vchan_get_all_descriptors - 获取所有已提交和已发出的描述符
* @vc: 从中获取描述符的虚拟通道
* @head: 存放找到的描述符的链表
*
* 调用者必须持有 vc.lock.
* 从内部链表中移除所有已分配/提交/发出/完成/终止的描述符, 并提供一个包含所有找到的描述符的链表.
*/
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 将每个内部队列都合并到 head 链表的末尾.
// 这是一个 O(1) 的批量移动操作.
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_killvirt-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
/**
* vchan_synchronize() - 将回调执行同步到当前上下文
* @vc: 要同步的虚拟通道
*/
static inline void vchan_synchronize(struct virt_dma_chan *vc)
{
LIST_HEAD(head);
unsigned long flags;

/*
* 1. 同步软件回调:
* 杀死 tasklet. 这会等待当前正在运行的 tasklet (如果有的话) 执行完毕.
* 这确保了所有软件层面的完成回调都已处理完毕.
*/
tasklet_kill(&vc->task);

/*
* 2. 清理已终止的描述符:
* 这是一个内务操作, 用于清理那些被 terminate 但尚未释放的描述符.
*/
spin_lock_irqsave(&vc->lock, flags);
// 将 desc_terminated 队列中的所有描述符移动到临时 head 链表.
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
/**
* vchan_vdesc_fini - 释放或重用一个描述符
* @vd: 要被释放或重用的虚拟描述符
*/
static inline void vchan_vdesc_fini(struct virt_dma_desc *vd)
{
/*
* 从通用的描述符指针 vd->tx.chan 反向找到其所属的虚拟通道 vc.
*/
struct virt_dma_chan *vc = to_virt_chan(vd->tx.chan);

/*
* *** 核心裁决逻辑 ***
* dmaengine_desc_test_reuse() 是一个辅助宏, 它会检查描述符的通用部分 (vd->tx)
* 中是否设置了 DMA_CTRL_REUSE 标志. 这个标志是由使用者驱动在调用 prep 函数时设置的.
*/
if (dmaengine_desc_test_reuse(&vd->tx)) {
/*
* --- 路径 A: 重用 (Re-use) ---
* 如果描述符被标记为可重用.
*/
unsigned long flags;

/*
* 获取自旋锁并禁用本地中断. 这是至关重要的, 因为 `desc_allocated` 链表
* 是一个共享资源, 可能有其他任务正在尝试从中获取描述符来准备新的传输.
* 这个锁确保了链表操作的原子性.
*/
spin_lock_irqsave(&vc->lock, flags);
/*
* 将描述符节点重新添加回 'desc_allocated' (已分配) 链表的头部.
* 这使得描述符立即可以被下一次的 `prep` 函数调用所获取和填充.
* 描述符的生命周期在此形成了一个闭环, 实现了回收.
*/
list_add(&vd->node, &vc->desc_allocated);
/*
* 释放自旋锁, 恢复之前的中断状态.
*/
spin_unlock_irqrestore(&vc->lock, flags);
} else {
/*
* --- 路径 B: 释放 (Free) ---
* 如果描述符未被标记为可重用 (这是默认情况).
*/
/*
* 调用由具体DMA驱动(如STM32 DMA)提供的回调函数 vc->desc_free.
* 这是"控制反转"(Inversion of Control)的体现: virt-dma 框架本身不知道如何释放
* 一个具体的 `stm32_dma_desc` 结构, 所以它将这个任务委托给当初分配
* 这个描述符的驱动程序来完成. `vc->desc_free` 最终会调用 kfree().
*/
vc->desc_free(vd);
}
}

vchan_cyclic_callback: 安全地调度循环DMA周期的完成回调

此函数是一个定义在头文件中的、高度优化的静态内联函数。它的核心作用是在硬件DMA中断处理程序(硬中断上下文)中, 以最快、最安全的方式, 安排对一次循环DMA传输周期的完成通知

此函数是Linux内核中”中断下半部”(Bottom Half)或”延迟工作”(Deferred Work)设计哲学的典型体现。其原理是将可能耗时的工作从对时间要求极为苛刻的硬中断上下文中移出, 交给一个更宽松的软中断上下文(tasklet)来执行

具体工作流程如下:

  1. 触发: 当一个配置为循环模式的DMA通道完成一个周期(例如, 传输完一个音频缓冲区)时, DMA硬件会产生一个中断。内核会调用该中断的硬中断处理程序(例如stm32_dma_chan_irq)。
  2. 快速标记与调度: 硬中断处理程序在确认是循环周期完成后, 会立即调用vchan_cyclic_callback。此函数只做两件非常快速的事情:
    • vc->cyclic = vd;: 它将指向当前完成的DMA描述符(vd)的指针, 保存到虚拟通道(vc)的一个特殊字段cyclic中。这就像是在邮箱上插上一面旗帜, 标记“有一个循环周期的回调需要处理”。
    • tasklet_schedule(&vc->task);: 它将与该虚拟通道关联的tasklet(vc->task)放入一个调度队列中。这个tasklet的处理函数是vchan_complete(在之前的分析中已出现)。这个调度动作本身是原子的, 并且执行得极快。
  3. 延迟执行: 在硬中断处理程序安全退出后, 内核会在稍后的一个”安全”时间点(通常是在下一次时钟节拍或者没有更高优先级任务时), 从队列中取出并执行vchan_complete这个taskletvchan_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
/**
* vchan_cyclic_callback - 报告一个周期的完成
* @vd: 虚拟描述符
*/
static inline void vchan_cyclic_callback(struct virt_dma_desc *vd)
{
/*
* to_virt_chan 是一个宏 (类似于 container_of),
* 它从描述符 'vd' 中包含的通用 dma_chan 指针,
* 反向计算出其容器结构体 virt_dma_chan 的地址 'vc'.
*/
struct virt_dma_chan *vc = to_virt_chan(vd->tx.chan);

/*
* 在虚拟通道 'vc' 中, 将 'cyclic' 字段设置为指向当前完成的描述符 'vd'.
* 这是一个"信使"或"标志", 用于通知即将被调度的 tasklet,
* 刚刚发生的是一次"循环周期完成"事件, 并且与这个描述符'vd'相关.
*/
vc->cyclic = vd;
/*
* 调用 tasklet_schedule(), 将与此虚拟通道关联的 tasklet ('vc->task')
* 放入内核的 tasklet 调度队列中.
* 内核将在稍后的安全时间点, 在软中断上下文中执行这个 tasklet 的处理函数 (vchan_complete).
* 这个操作本身非常快速, 使得硬中断处理程序可以立即返回.
*/
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
// to_virt_desc - 将一个通用的 tx 描述符指针转换为 virt_dma 描述符指针
static struct virt_dma_desc *to_virt_desc(struct dma_async_tx_descriptor *tx)
{
// 使用 container_of 宏:
// "tx" 是成员指针,
// "struct virt_dma_desc" 是容器的类型,
// "tx" 是容器内成员的名称.
return container_of(tx, struct virt_dma_desc, tx);
}

vchan_tx_submit: 提交一个DMA传输请求

当一个使用者驱动(如SPI)调用dmaengine_submit()时, DMA引擎核心会通过vchan_tx_prep中设置的函数指针, 最终调用到此函数。

  • 核心原理: 此函数执行两个关键动作, 将一个准备好的描述符(allocated状态)原子地转换为”已提交, 等待启动”(submitted状态):

    1. 分配票据(Cookie): 它调用dma_cookie_assign为这次传输分配一个唯一的、顺序递增的ID。这个ID是之后追踪传输状态的唯一凭证。
    2. 移动队列: 它调用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);

// 1. 为这个传输分配一个唯一的 cookie.
cookie = dma_cookie_assign(tx);

// 2. 将描述符节点从它当前的链表(应该是 desc_allocated), 移动到 desc_submitted 链表的尾部.
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);

// 将新分配的 cookie 返回给使用者驱动, 以便其之后可以查询状态.
return cookie;
}
EXPORT_SYMBOL_GPL(vchan_tx_submit);

vchan_tx_desc_free: 释放一个DMA描述符

当一个描述符的生命周期结束时(无论是正常完成还是被终止), 需要调用此函数来释放其占用的内存。

  • 核心原理: 此函数执行一个**”摘除并委托”**的操作:

    1. 摘除: 在自旋锁的保护下, 它调用list_del将描述符从它当前所在的任何virt-dma链表中安全地移除。
    2. 委托: 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
/**
* vchan_tx_desc_free - 释放一个可重用的描述符
* @tx: 传输描述符
*/
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);
// 1. 从当前所在的链表中安全地移除描述符节点.
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);
// 2. 调用由具体DMA驱动提供的回调函数, 来执行实际的内存释放操作.
// vc->desc_free 指向的通常是像 stm32_dma_desc_free 这样的函数.
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 安全地遍历链表.
* "_safe" 版本是必需的, 因为我们在循环体内会删除当前节点.
*/
list_for_each_entry_safe(vd, _vd, head, node) {
list_del(&vd->node); // 从临时链表中移除节点 (可选但良好实践)
/*
* 调用 vchan_vdesc_fini, 它最终会调用由具体驱动提供的 vc->desc_free 回调,
* 来执行实际的 kfree(to_stm32_dma_desc(vd)) 操作.
*/
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内核的gpiolibirqchip框架中。完成注册后, 这个端口上的所有引脚就正式成为可被系统其他驱动程序使用的标准GPIO资源和中断源。

它的原理是一个多阶段的硬件抽象和软件注册过程:

  1. 硬件初始化: 它首先确保GPIO端口的硬件已经准备就緒, 包括将其”移出复位状态”(reset_control_deassert), 以及通过内存映射(devm_ioremap_resource)获得访问其物理寄存器的虚拟地址。
  2. GPIO编号和Pinctrl集成: 这是关键的一步。它需要确定这个端口的引脚(本地编号0-15)如何映射到Linux内核的全局GPIO编号空间。
    • 首选方式是解析设备树中的gpio-ranges属性, 这个属性明确地定义了映射关系, 实现了最大的灵活性。
    • 如果该属性不存在, 它会采用一种简单的、基于探测顺序的备用方案来计算全局编号。
    • 无论采用哪种方式, 它都会调用pinctrl_add_gpio_range, 将这个全局GPIO编号范围与当前的gpio_chip关联起来, 从而建立了pinctrl子系统和gpiolib子系统之间的桥梁
  3. 中断域层次结构: 它调用irq_domain_create_hierarchy来创建一个新的中断域(bank->domain), 并将其设置为pinctrl主中断域(pctl->domain, 通常是EXTI)的子域。这精确地模拟了STM32的硬件结构: EXTI是中断的接收者, 而GPIO端口的配置决定了是将PA0, PB0还是PC0连接到EXTI0这条线上。这个层次结构使得中断管理既清晰又高效。
  4. gpio_chip填充与注册: 它会填充一个struct gpio_chip结构体。这就像是向内核提交的一份”GPIO端口简历”, 里面包含了:
    • 指向实现具体硬件操作(如读、写、设置方向)的函数指针(这些通常在一个模板stm32_gpio_template中预定义)。
    • 端口的名称、父设备、引脚数量等信息。
    • 一个包含所有引脚名称(如”PA0”, “PA1”…)的数组, 用于调试和sysfs。
  5. 最后, 它调用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
/*
* 静态函数声明: stm32_gpiolib_register_bank
* @pctl: 指向驱动核心数据结构的指针.
* @fwnode: 指向当前正在处理的GPIO bank的设备树节点句柄.
* @return: 成功时返回 0, 失败时返回负的错误码.
*/
static int stm32_gpiolib_register_bank(struct stm32_pinctrl *pctl, struct fwnode_handle *fwnode)
{
// 获取当前bank的预分配结构体
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;

// 如果有复位控制器, 则将其 deassert (即让其停止复位)
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);

// 使用预定义的模板初始化 gpio_chip
bank->gpio_chip = stm32_gpio_template;

// 从设备树读取 "st,bank-name" 属性作为 gpio_chip 的标签
fwnode_property_read_string(fwnode, "st,bank-name", &bank->gpio_chip.label);

// 尝试解析 "gpio-ranges" 属性来确定GPIO编号
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];

// 计算此 bank 覆盖的引脚数量
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 {
// 如果没有 "gpio-ranges", 使用备用方案计算GPIO基地址
bank_nr = pctl->nbanks;
bank->gpio_chip.base = bank_nr * STM32_GPIO_PINS_PER_BANK;
// 填充 pinctrl_gpio_range 结构, 并将其添加到 pinctrl 设备
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);
}

// 读取可选的 "st,bank-ioport" 属性
if (fwnode_property_read_u32(fwnode, "st,bank-ioport", &bank_ioport_nr))
bank_ioport_nr = bank_nr;

// gpiolib 推荐将 base 设置为-1, 由核心自动分配
bank->gpio_chip.base = -1;

// 填充 gpio_chip 的其余成员
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); // 初始化自旋锁

// 如果pinctrl支持中断, 为此bank创建层次化的中断域
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;

// 正式将 gpio_chip 添加到 gpiolib 核心
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传输任务

它的核心原理是将加锁时间最小化, 实现高效的生产者-消费者模型:

  1. 生产者(中断处理程序): 当DMA硬件完成一次传输并触发中断时, 中断处理程序(未在此处显示)会做最少的工作: 它将代表已完成传输的”描述符”(virt_dma_desc)添加到一个共享的”已完成”链表(vc->desc_completed)中, 然后调度此vchan_complete tasklet去执行。
  2. 消费者(本tasklet):
    • 快速抓取: Tasklet开始执行后, 它首先获取通道的自旋锁vc->lock。然后, 它不是在锁内逐个处理描述符, 而是调用list_splice_tail_init这个高效的链表操作, 一次性地将整个共享的”已完成”链表从未清空, 并移动到一个本地的、私有的head链表中
    • 快速释放: 抓取完成后, 它立即释放自旋锁。这是整个设计的精髓: 关键的共享数据访问(链表移动)在极短的时间内完成, 极大地降低了锁的争用, 提高了系统性能。
    • 安全处理: 释放锁之后, tasklet现在可以从容地、安全地遍历它自己的私有head链表。对于链表中的每一个描述符, 它会:
      1. 调用dmaengine_desc_get_callback获取该传输任务完成时需要通知的回调函数。
      2. 调用dmaengine_desc_callback_invoke执行这个回调, 从而通知提交该DMA任务的驱动程序(例如,一个SPI驱动) “你的数据已经发送/接收完毕”。
      3. 调用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
/*
* vchan_complete (Tasklet处理函数)
* 这个tasklet处理DMA描述符的完成事件, 它会调用
* 它的回调函数并释放它.
*/
static void vchan_complete(struct tasklet_struct *t)
{
// 从 tasklet_struct 指针 t, 获取其容器结构 virt_dma_chan 的指针 vc
struct virt_dma_chan *vc = from_tasklet(vc, t, task);
// 定义用于遍历描述符链表的指针
struct virt_dma_desc *vd, *_vd;
// 定义用于存储回调信息的结构体
struct dmaengine_desc_callback cb;
// 在栈上创建一个临时的本地链表头
LIST_HEAD(head);

// 获取通道的自旋锁, 并禁用本地中断, 保护共享的 desc_completed 链表
spin_lock_irq(&vc->lock);
// 将 vc->desc_completed 链表中的所有节点一次性移动到本地的 head 链表
// 移动后, vc->desc_completed 会被重新初始化为空链表. 这是一个原子且高效的操作.
list_splice_tail_init(&vc->desc_completed, &head);
// 检查是否有循环模式的DMA传输完成了
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);

// 安全地遍历本地 head 链表中的每一个已完成的(非循环)描述符
list_for_each_entry_safe(vd, _vd, &head, node) {
// 获取当前描述符的回调信息
dmaengine_desc_get_callback(&vd->tx, &cb);

// 从本地链表中删除该节点
list_del(&vd->node);
// 调用回调函数, 通知上层驱动DMA操作已完成
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
/*
* vchan_init - 初始化一个虚拟DMA通道
* @vc: 要初始化的 virt_dma_chan 结构体
* @dmadev: 该通道所属的 dma_device (物理DMA控制器)
*/
void vchan_init(struct virt_dma_chan *vc, struct dma_device *dmadev)
{
/*
* 1. 初始化 Cookie 系统:
* 调用 dma_cookie_init 来重置该通道的 "票据系统".
* 这将 'cookie' (最后分配的) 和 'completed_cookie' (最后完成的) 都设置为起始值.
* 这是追踪传输生命周期的基础.
*/
dma_cookie_init(&vc->chan);

/*
* 2. 初始化同步原语:
* 初始化自旋锁 vc->lock. 这个锁是保护所有描述符链表和通道关键状态的
* 核心同步机制, 用于防止任务上下文和中断上下文之间的竞态条件.
* 在单核STM32H750上, 它主要通过禁用中断来实现原子性.
*/
spin_lock_init(&vc->lock);
/*
* 3. 初始化描述符生命周期队列:
* 使用 INIT_LIST_HEAD 将五个链表头都初始化为空链表.
* 这五个链表代表了一个DMA描述符可能处于的所有软件状态, 构成了其完整的生命周期:
* - desc_allocated: 已分配, 由 prep 函数填充, 等待 submit.
* - desc_submitted: 已提交, 等待 issue_pending 来启动.
* - desc_issued: 已发出, 已被传递给硬件驱动, 正在运行或等待运行.
* - desc_completed: 已完成, 等待 tasklet 回调. (较少使用)
* - desc_terminated: 已终止, 等待被释放.
*/
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);

/*
* 4. 设置异步完成机制 (Bottom Half):
* 初始化一个 tasklet. 当DMA传输完成中断(硬中断/IRQ)发生时,
* 中断处理程序(ISR)会调度这个 tasklet. 实际的完成回调(如通知使用者驱动)
* 将在 vchan_complete 这个 tasklet 函数中执行.
* 这遵循了中断处理"上半部/下半部"的设计模式, 保证了硬中断处理程序尽可能快地退出.
*/
tasklet_setup(&vc->task, vchan_complete);

/*
* 5. 建立父子关系并注册:
* 将虚拟通道内嵌的通用 dma_chan 结构的 device 成员指向其父设备 dmadev.
*/
vc->chan.device = dmadev;
/*
* 将这个新初始化的通道(通过其内嵌的 device_node 链表头)添加到其父设备
* dmadev 的 channels 链表的末尾.
* 这样, 该通道就正式成为了其父DMA控制器的一部分, 对内核可见.
*/
list_add_tail(&vc->chan.device_node, &dmadev->channels);
}
/*
* 将函数导出, 使其对其他内核模块可用. 这是 virt-dma 框架的核心API之一.
*/
EXPORT_SYMBOL_GPL(vchan_init);

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函数中, 它被调用两次:

  1. 第一次, 用原始的dma_spec(指向DMAMUX)来找到DMAMUX驱动的of_dma实例。
  2. 在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
/**
* of_dma_find_controller - 在DT DMA辅助列表中获取一个DMA控制器
* @dma_spec: 指向在设备树中找到的DMA描述符的指针
*
* 在一个已注册的DMA控制器列表中, 查找一个具有匹配的设备节点和dma单元数量
* 的DMA控制器. 如果找到匹配, 将返回一个指向存储的DMA数据的有效指针.
* 如果未找到匹配, 则返回NULL指针.
*/
static struct of_dma *of_dma_find_controller(const struct of_phandle_args *dma_spec)
{
/* 定义一个指向 of_dma 结构体的指针 ofdma, 用作循环的遍历器. */
struct of_dma *ofdma;

/*
* 使用内核标准的 list_for_each_entry 宏来遍历全局的 of_dma_list 链表.
* of_dma_list 包含了所有已通过 of_dma_controller_register 或
* of_dma_router_register 注册的DMA控制器和路由器.
*/
list_for_each_entry(ofdma, &of_dma_list, of_dma_controllers)
/*
* 这是核心的匹配逻辑:
* 直接比较当前遍历到的 ofdma 对象的设备树节点指针 (ofdma->of_node)
* 与传入的 dma_spec 中的设备树节点指针 (dma_spec->np) 是否相同.
* 这是一个非常快速高效的指针比较.
*/
if (ofdma->of_node == dma_spec->np)
/* 如果指针相同, 说明找到了匹配的控制器/路由器, 立即返回指向其软件抽象的指针. */
return ofdma;

/*
* 如果遍历完整个链表都没有找到匹配项, 打印一条调试信息.
* pr_debug 只在内核开启了动态调试时才会输出, 在生产环境中通常是无操作的.
* %pOF 是一个特殊的格式说明符, 用于打印设备树节点的完整路径.
*/
pr_debug("%s: can't find DMA controller %pOF\n", __func__,
dma_spec->np);

/* 返回NULL, 表示未找到匹配的DMA控制器/路由器. */
return NULL;
}

of_dma_router_xlate: 执行路由DMA请求的翻译

此函数是Linux内核设备树(Device Tree) DMA辅助框架中一个极其精妙的核心调度函数。当一个外设驱动请求的DMA通道指向一个DMA路由器(如STM32的DMAMUX)时, 此函数会被调用。它的核心作用是充当一个”中间人”或”业务流程编排器”, 负责协调客户端驱动、路由器驱动和最终的DMA控制器驱动这三方, 完成一次复杂的路由DMA请求

它的原理可以被理解为一个三阶段的翻译与转发过程:

  1. 第一阶段: 调用路由器, 建立硬件路由并重写请求

    • 函数首先调用路由器驱动(如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控制器的有效请求。
  2. 第二阶段: 查找最终目标, 转发重写后的请求

    • 函数使用这个被重写过的dma_spec_target, 再次在内核的全局DMA控制器列表中进行查找, 这次它会找到代表真正DMA控制器(如DMA1)的of_dma对象(ofdma_target)。
    • 依赖处理: 如果找不到(因为真正的DMA控制器驱动尚未加载), 此函数会返回-EPROBE_DEFER, 优雅地处理了驱动间的依赖关系。
    • 接着, 它调用这个最终目标of_dma_xlate函数, 这就进入了标准的、非路由的DMA通道分配流程。
  3. 第三阶段: 链接与返回, 为未来清理做好准备

    • 如果最终的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
/**
* of_dma_router_xlate - 路由器设备的翻译函数
* @dma_spec: 指向在设备树中找到的DMA描述符的指针 (原始请求)
* @ofdma: 指向DMA控制器数据的指针 (此处是路由器的信息)
*
* 此函数创建一个新的dma_spec, 传递给路由器驱动的of_dma_route_allocate()
* 函数, 以准备一个将用于从真正的DMA控制器请求通道的dma_spec.
*/
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;

/* 复制原始的DMA请求规格, 因为路由分配函数会修改它. */
memcpy(&dma_spec_target, dma_spec, sizeof(dma_spec_target));

/*
* 阶段1: 调用路由器驱动的回调函数.
* 这个函数会建立硬件路由, 并重写 dma_spec_target 指向真正的DMA控制器.
*/
route_data = ofdma->of_dma_route_allocate(&dma_spec_target, ofdma);
if (IS_ERR(route_data))
return NULL; /* 如果路由分配失败, 直接返回. */

/*
* 阶段2: 使用被重写后的 dma_spec_target, 查找最终的DMA控制器.
*/
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; /* 跳转到统一的清理点. */
}

/*
* 阶段3: 调用最终目标DMA控制器的 xlate 函数, 获取实际的DMA通道.
*/
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; /* 将路由私有数据存入通道. */

/* 调用一个可选的回调, 让最终的DMA控制器知道它正在被路由. */
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_dma_route_allocate 内部调用了 of_parse_phandle,
* 增加了 dma_spec_target.np 的引用计数, 这里必须释放它以防内存泄漏.
*/
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通道时:

  1. 内核的DMA框架会遍历全局链表, 找到与&dmamux节点匹配的这个of_dma描述对象。
  2. 框架发现这是一个路由器, 于是它不会去查找最终的DMA通道, 而是转而调用我们在此处注册的of_dma_route_allocate回调函数
  3. 这个回调函数(如我们之前分析的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
/**
* of_dma_router_register - 将一个DMA路由器作为控制器注册到DT DMA辅助框架中
* @np: DMA路由器的设备节点
* @of_dma_route_allocate: 路由器的设置函数, 它需要修改dma_spec以供
* 真正的DMA控制器使用, 并建立请求的路由.
* @dma_router: 指向dma_router结构体的指针, 在需要释放路由时使用.
*
* 成功时返回0, 失败时返回相应的errno值.
*
* 分配的内存应通过相应的of_dma_controller_free()调用来释放.
*/
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)
{
/* 定义一个指向 of_dma 结构体的指针 ofdma. 这个结构体是DMA控制器/路由器的软件抽象. */
struct of_dma *ofdma;

/*
* 对传入的参数进行合法性检查.
* 必须提供设备节点(np), 路由分配回调函数(of_dma_route_allocate), 和dma_router结构体.
*/
if (!np || !of_dma_route_allocate || !dma_router) {
pr_err("%s: not enough information provided\n", __func__);
return -EINVAL; /* 返回"无效参数"错误. */
}

/*
* 为 ofdma 结构体分配内存. kzalloc会分配内存并将其清零.
* GFP_KERNEL 允许在内存不足时睡眠等待.
*/
ofdma = kzalloc(sizeof(*ofdma), GFP_KERNEL);
if (!ofdma)
return -ENOMEM; /* 内存分配失败, 返回"内存不足"错误. */

/* 将此软件抽象与硬件的设备树节点关联起来. */
ofdma->of_node = np;
/*
* 设置 "xlate" (翻译) 函数指针.
* 当DMA框架处理指向此节点的dmas属性时, of_dma_router_xlate 将是第一个被调用的函数,
* 它会接着调用下面的 of_dma_route_allocate.
*/
ofdma->of_dma_xlate = of_dma_router_xlate;
/*
* 核心步骤: 将DMAMUX驱动提供的路由分配函数(例如 stm32_dmamux_route_allocate)
* 的地址保存到 ofdma 结构体中.
*/
ofdma->of_dma_route_allocate = of_dma_route_allocate;
/*
* 保存指向 dma_router 私有数据结构的指针, 其中包含了 route_free 等信息.
*/
ofdma->dma_router = dma_router;

/*
* 获取一个全局互斥锁 of_dma_lock.
* 在单核抢占式系统上, 这能防止在修改全局链表时被其他任务抢占, 保证操作的原子性.
*/
mutex_lock(&of_dma_lock);
/*
* 将新创建的 ofdma 描述对象添加到全局的 of_dma_list 链表的尾部.
* 从这一刻起, 这个DMAMUX路由器就对内核的DMA框架可见并可用了.
*/
list_add_tail(&ofdma->of_dma_controllers, &of_dma_list);
/* 释放互斥锁. */
mutex_unlock(&of_dma_lock);

/* 注册成功, 返回0. */
return 0;
}
/*
* 将 of_dma_router_register 函数导出, 使其对其他遵循GPL许可证的内核模块(如DMAMUX驱动)可用.
*/
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通道)句柄的逻辑所在。

其工作流程非常直接:

  1. 创建注册记录: 函数首先分配一个struct of_dma结构体。这个结构体就像一张”服务提供商名片”。
  2. 填充记录: 它将三项关键信息填入这张名片:
    • of_node: DMA控制器自身的设备树节点。这是服务提供商的唯一标识。
    • of_dma_xlate: 指向DMA控制器驱动自己实现的翻译函数的指针。这是服务的核心逻辑。
    • of_dma_data: 一个私有数据指针, 会在调用of_dma_xlate时传回, 用于提供上下文。
  3. 发布到全局列表: 函数会获取一个全局互斥锁of_dma_lock, 然后将这张填好的”名片”(ofdma)添加到全局的DMA控制器链表of_dma_list的末尾。

当SPI驱动调用of_dma_request_slave_channel()时, 内核的DMA辅助代码就会:

  1. 遍历of_dma_list这个全局链表。
  2. 根据SPI设备dmas属性中的phandle(&dma1), 在链表中找到of_node与之匹配的ofdma“名片”。
  3. 调用这张名片上记录的of_dma_xlate函数, 并将dmas属性中剩余的数字(5, 0x400, 0x80)作为参数传递给它。
  4. of_dma_xlate函数执行其内部逻辑, 返回一个配置好的dma_chan指针。
  5. 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
/**
* of_dma_controller_register - 将一个DMA控制器注册到设备树DMA辅助系统中
* @np: DMA控制器的设备节点
* @of_dma_xlate: 一个翻译函数, 它将一个 phandle 参数列表转换为一个 dma_chan 结构体
* @data: 一个指向控制器特定数据的指针, 将被翻译函数使用
*
* 成功时返回0, 失败时返回相应的errno值.
*
* 分配的内存应通过对应的 of_dma_controller_free() 调用来释放.
*/
int of_dma_controller_register(struct device_node *np,
struct dma_chan *(*of_dma_xlate)
(struct of_phandle_args *, struct of_dma *),
void *data)
{
/*
* 定义一个指向 of_dma 结构体的指针 ofdma.
* of_dma 结构体是用于在全局链表中代表一个DMA控制器的"注册记录".
*/
struct of_dma *ofdma;

/*
* 参数健全性检查. 设备节点(np)和翻译函数(of_dma_xlate)都是必需的.
*/
if (!np || !of_dma_xlate) {
pr_err("%s: not enough information provided\n", __func__);
return -EINVAL; // 返回无效参数错误
}

/*
* 使用 kzalloc 分配并清零 ofdma 结构体的内存.
*/
ofdma = kzalloc(sizeof(*ofdma), GFP_KERNEL);
if (!ofdma)
return -ENOMEM; // 返回内存不足错误

/*
* 填充 ofdma 结构体 (即"注册记录") 的内容.
*/
ofdma->of_node = np; // 记录DMA控制器的设备树节点 (身份标识)
ofdma->of_dma_xlate = of_dma_xlate; // 记录驱动提供的翻译函数 (核心逻辑)
ofdma->of_dma_data = data; // 记录驱动的私有数据 (上下文)

/*
* 将这个 of_dma 控制器结构体加入到全局链表中.
* 获取全局互斥锁 of_dma_lock, 以保护 of_dma_list 链表的完整性, 防止并发访问.
*/
mutex_lock(&of_dma_lock);
/*
* 调用 list_add_tail 将 ofdma 结构体内嵌的链表头, 添加到全局 of_dma_list 链表的尾部.
*/
list_add_tail(&ofdma->of_dma_controllers, &of_dma_list);
/*
* 释放互斥锁.
*/
mutex_unlock(&of_dma_lock);

/*
* 注册成功, 返回 0.
*/
return 0;
}
/*
* 将函数导出, 使其对其他遵循GPL许可证的内核模块可用.
* 这是一个核心的设备树辅助API.
*/
EXPORT_SYMBOL_GPL(of_dma_controller_register);

of_dma_request_slave_channel: 从设备树解析和请求DMA从通道

本代码片段展示了Linux内核中基于设备树(Device Tree)的DMA从通道请求机制的核心实现。其主要功能是为设备驱动提供一个API (of_dma_request_slave_channel),允许驱动仅通过其设备树节点(np)和一个字符串名称(如"rx""tx"),就能自动解析出对应的DMA控制器、通道信息,并向正确的DMA控制器驱动请求分配一个dma_chan实例。这是现代嵌入式Linux驱动中实现DMA功能、处理驱动依赖的标准且核心的流程。

实现原理分析

该机制的核心是设备树中的标准化绑定。它依赖于设备树(DTS)文件中dmasdma-names这两个属性的成对出现,来描述一个设备(如USART)与一个DMA通道之间的物理连接。

  1. 设备树绑定规范:

    • dmas: 这是一个属性,其内容是一个或多个**DMA说明符(specifier)**的列表。每个说明符通常由一个指向DMA控制器节点的phandle和一系列参数(由#dma-cells定义,如通道号、流号、优先级等)组成。
    • dma-names: 这是一个字符串列表,与dmas列表中的条目一一对应。它为每个DMA说明符提供了一个人类可读的、功能性的名称,如"rx""tx"
  2. 匹配与解析 (of_dma_match_channel):

    • 这是一个辅助函数,其职责是在设备节点的dmas/dma-names属性列表中,查找指定索引(index)处的条目是否与给定的名称(name)匹配
    • 步骤:
      1. of_property_read_string_index: 读取dma-names列表中第index个字符串。
      2. strcmp: 比较读取到的字符串与请求的name是否相同。
      3. of_parse_phandle_with_args: 如果名称匹配,则解析dmas列表中同样位于第index的DMA说明符,将其中的phandle和参数提取到dma_spec结构体中。
    • 只有当这三步全部成功时,该函数才返回0,表示在指定索引处找到了一个完全匹配的DMA通道描述。
  3. 请求与翻译 (of_dma_request_slave_channel):

    • 这是主API函数,它 orchestrates 整个过程。
    • 遍历与查找: 它会遍历设备节点中所有的dma-names条目。
    • 简单负载均衡: start = atomic_inc_return(&last_index); ... (i + start) % count 这是一个简单的轮询机制。如果设备树中为同一个名称(如”rx”)提供了多个可选的DMA通道,这个原子计数器会使得每次请求都从一个不同的起始点开始搜索,从而在多个请求者之间大致平均地分配这些通道。
    • 寻找控制器: 当通过of_dma_match_channel找到一个匹配的dma_spec后,它会调用of_dma_find_controller。这个函数会拿着dma_spec中的phandle,去一个全局的、已注册的DMA控制器列表中查找,看是否有哪个DMA控制器驱动已经“认领”了这个设备树节点。
    • 翻译 (.of_dma_xlate):
      • 如果找到了对应的DMA控制器驱动(ofdma),就会调用该驱动提供的**of_dma_xlate回调函数。这是最关键的一步.of_dma_xlate(translate,翻译)函数是DMA控制器驱动的一部分,它的职责是接收通用的、来自设备树的dma_spec参数,并将其翻译**成一个具体的、可用的struct dma_chan实例。
    • 依赖处理 (-EPROBE_DEFER):
      • 如果在of_dma_find_controller没有找到匹配的DMA控制器驱动,这意味着该DMA驱动尚未被探测(probe)或初始化。在这种情况下,函数不会立即失败,而是将最终的返回值标记为-EPROBE_DEFER。这个错误码会沿着调用栈向上传递,最终告诉内核驱动核心:“我的依赖项(DMA控制器)还没准备好,请稍后重试我的probe”。

特定场景分析:单核、无MMU的STM32H750平台

硬件交互

  1. 设备树实例: STM32H750的usart1设备树节点会包含:
    1
    2
    3
    4
    5
    usart1: serial@40010000 {
    /* ... */
    dmas = <&dma1_stream0 0x400401>, <&dma1_stream1 0x400402>;
    dma-names = "rx", "tx";
    };
  2. 执行流程: 当stm32_usart_serial_probe调用of_dma_request_slave_channel(np, "rx")时:
    • 函数会遍历dma-names = {"rx", "tx"}
    • 当它检查到索引0时,of_dma_match_channel成功,因为"rx"匹配。
    • of_parse_phandle_with_args会解析dmas列表的第0个条目,得到dma_spec,其中包含:
      • 一个指向dma1节点的phandle
      • 参数 0x400401(这通常编码了DMA流的配置,如请求线ID、方向、优先级等,具体格式由dma1节点的#dma-cells定义)。
    • of_dma_find_controller会找到已经注册的STM32 DMA驱动。
    • STM32 DMA驱动的.of_dma_xlate函数被调用。它会解析0x400401这个参数,知道客户端需要DMA1Stream 0,并且需要将DMAMUX配置为将USART1_RX的请求路由到Stream 0。然后,它返回一个代表DMA1_Stream0dma_chan

单核与无MMU影响

  • 并发: atomic_inc_return在单核上仍然是原子的,可以防止中断或抢占。mutex_lock(&of_dma_lock)则保护了全局DMA控制器列表在被遍历和查找时的一致性,防止了任务间的并发访问。
  • MMU: 此代码是纯粹的设备树解析和内核数据结构操作。它不涉及任何内存地址的直接使用,因此与MMU完全无关,可以在无MMU的STM32H750上正确运行。

代码分析

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
/**
* @brief 检查一个DMA说明符是否与给定的名称匹配。
* @param np 要查找DMA通道的设备节点。
* @param name 要匹配的通道名称。
* @param index dma-names/dmas列表中的索引。
* @param dma_spec 用于存储找到的DMA说明符的指针(输出)。
* @return int 如果名称匹配且找到有效的说明符,返回0;否则返回-ENODEV。
*/
static int of_dma_match_channel(struct device_node *np, const char *name,
int index, struct of_phandle_args *dma_spec)
{
const char *s;

/* 读取dma-names属性中第index个字符串。 */
if (of_property_read_string_index(np, "dma-names", index, &s))
return -ENODEV;

/* 比较读取到的名称与请求的名称是否一致。 */
if (strcmp(name, s))
return -ENODEV;

/* 如果名称匹配,则解析dmas属性中同样位于index处的DMA说明符。 */
if (of_parse_phandle_with_args(np, "dmas", "#dma-cells", index,
dma_spec))
return -ENODEV;

return 0;
}

/**
* @brief 从设备树节点获取DMA从通道。
* @param np 要获取DMA请求的设备节点。
* @param name 期望的通道名称。
* @return struct dma_chan* 成功则返回合适的DMA通道指针,失败则返回错误指针。
*/
struct dma_chan *of_dma_request_slave_channel(struct device_node *np,
const char *name)
{
struct of_phandle_args dma_spec;
struct of_dma *ofdma;
struct dma_chan *chan;
int count, i, start;
int ret_no_channel = -ENODEV; /* 默认返回“设备未找到” */
static atomic_t last_index;

/* ... 基本的参数有效性检查 ... */

/* 如果连"dmas"属性都没有,则静默失败。 */
if (!of_property_present(np, "dmas"))
return ERR_PTR(-ENODEV);

/* 获取dma-names属性中的字符串数量。 */
count = of_property_count_strings(np, "dma-names");
if (count < 0) {
/* ... 错误处理 ... */
return ERR_PTR(-ENODEV);
}

/*
* 使用一个全局原子变量来轮转搜索的起始点,
* 以在多个同名通道间实现简单的负载均衡。
*/
start = atomic_inc_return(&last_index);
/* 遍历设备节点中所有的DMA说明符。 */
for (i = 0; i < count; i++) {
/* 检查当前索引处的条目是否与请求的名称匹配。 */
if (of_dma_match_channel(np, name,
(i + start) % count,
&dma_spec))
continue; /* 不匹配,继续下一个。 */

/* 锁定全局的of_dma控制器列表。 */
mutex_lock(&of_dma_lock);
/* 根据解析出的dma_spec(主要是phandle),查找已注册的DMA控制器驱动。 */
ofdma = of_dma_find_controller(&dma_spec);

if (ofdma) {
/*
* 找到了控制器驱动,调用其.of_dma_xlate回调函数,
* 将设备树中的说明符“翻译”成一个具体的dma_chan。
*/
chan = ofdma->of_dma_xlate(&dma_spec, ofdma);
} else {
/*
* 未找到控制器驱动,意味着它尚未被探测。
* 将返回值设为-EPROBE_DEFER,以便稍后重试。
*/
ret_no_channel = -EPROBE_DEFER;
chan = NULL;
}

mutex_unlock(&of_dma_lock);

/* 释放对DMA控制器设备树节点的引用。 */
of_node_put(dma_spec.np);

/* 如果成功翻译出dma_chan,则直接返回。 */
if (chan)
return chan;
}

/* 如果循环结束仍未找到,则返回最终的错误码。 */
return ERR_PTR(ret_no_channel);
}
EXPORT_SYMBOL_GPL(of_dma_request_slave_channel);

kernel/dma/mapping.c

dma_alloc_coherent: 统一的DMA一致性内存分配接口

本代码片段展示了Linux内核中用于分配DMA一致性内存的核心API——dma_alloc_coherent及其底层实现dma_alloc_attrs。其核心功能是提供一个统一的、与架构无关的接口,允许设备驱动程序申请一块特殊的内存区域。这块内存保证了CPU的缓存视图和DMA控制器的物理内存视图始终保持一致,从而免除了驱动程序手动进行复杂的缓存刷新(flushing)和失效(invalidating)操作。该函数会返回两个地址:一个供CPU使用的虚拟地址,另一个供DMA控制器使用的总线地址。

实现原理分析

该机制是DMA引擎框架中为了解决“缓存一致性”问题而设计的核心抽象。它通过dma_map_ops结构体将具体的实现委托给与架构和总线相关的后端,并根据系统配置(如是否有IOMMU)选择最优的分配路径。

  1. 问题的根源——缓存一致性:

    • 在现代CPU(如带有L1缓存的ARM Cortex-M7)上,当CPU向内存写入数据时,数据可能只存在于CPU的缓存(Cache)中,尚未写回主内存(RAM)。此时,如果DMA控制器直接去读取主内存,会读到过时的数据。
    • 反之,当DMA控制器向主内存写入数据后,CPU的缓存中可能仍然保留着该内存地址的旧数据。如果CPU直接从缓存读取,就会读到过时的数据。
    • “一致性内存”就是为了解决这个问题,确保CPU和DMA控制器对这块内存的读写总是能看到最新的结果。
  2. 多路径分配策略 (dma_alloc_attrs):

    • 该函数是一个分发器,它按照优先级尝试多种分配策略:
    • a) 设备特定一致性内存池 (dma_alloc_from_dev_coherent): 这是最高优先级的尝试。它会检查设备是否有关联的、预留的“一致性内存池”(通常通过CMA - Contiguous Memory Allocator实现,在设备树中定义)。如果设备可以从这个池中分配,就直接使用。这是一种高效的优化。
    • b) 直接映射 (dma_direct_alloc): 在没有IOMMU的系统上(如STM32H750),这是最常见的路径。dma_alloc_direct函数会返回truedma_direct_alloc会分配一块物理上连续的内存,并将其属性设置为“非缓存的”(non-cacheable)或“写通的”(write-through)。这样,CPU对这块内存的任何读写都会绕过缓存或立即写回主存,从而在硬件层面保证了与DMA控制器的一致性。
    • c) IOMMU路径 (iommu_dma_alloc): 在有IOMMU(输入/输出内存管理单元)的系统上,IOMMU负责处理DMA的地址翻译和隔离。此路径会分配普通的可缓存内存,然后通过IOMMU为这块内存创建一个对DMA控制器可见的、一致的映射。
    • d) 架构特定回调 (ops->alloc): 这是一个最终的回退路径,允许特定的CPU架构或平台提供自己的、特殊的DMA一致性内存分配实现。
  3. 两个地址的返回:

    • cpu_addr (void *): 这是分配的内存的CPU虚拟地址。驱动程序应该使用这个地址来读写缓冲区。
    • dma_handle (dma_addr_t *): 这是一个输出参数,函数会通过它返回DMA控制器应该使用的总线地址。DMA控制器硬件的源/目的地址寄存器必须被配置为这个值。
  4. 释放流程 (dma_free_attrs):

    • 释放函数dma_free_attrs严格镜像了分配函数的决策树。它会按照相同的逻辑顺序检查系统配置,并调用与分配时所用路径相对应的释放函数(dma_direct_free, iommu_dma_free, ops->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
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
/**
* @brief 分配一块DMA一致性内存(简化版API)。
* @param dev 请求内存的设备。
* @param size 请求的字节数。
* @param dma_handle 指向dma_addr_t的指针,用于返回DMA总线地址。
* @param gfp 内核内存分配标志。
* @return void* 成功则返回CPU可用的虚拟地址,失败返回NULL。
*/
static inline void *dma_alloc_coherent(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t gfp)
{
/* 调用功能更全的、带属性的版本,属性为0。 */
return dma_alloc_attrs(dev, size, dma_handle, gfp,
(gfp & __GFP_NOWARN) ? DMA_ATTR_NO_WARN : 0);
}

/**
* @brief 释放一块DMA一致性内存(简化版API)。
* @param dev 分配内存的设备。
* @param size 内存大小。
* @param cpu_addr CPU虚拟地址。
* @param dma_handle DMA总线地址。
*/
static inline void dma_free_coherent(struct device *dev, size_t size,
void *cpu_addr, dma_addr_t dma_handle)
{
return dma_free_attrs(dev, size, cpu_addr, dma_handle, 0);
}

/**
* @brief 分配一块带属性的DMA一致性内存(核心实现)。
* @param dev 请求内存的设备。
* @param size 请求的字节数。
* @param dma_handle 指向dma_addr_t的指针,用于返回DMA总线地址。
* @param flag 内核内存分配标志。
* @param attrs DMA分配属性。
* @return void* 成功则返回CPU可用的虚拟地址,失败返回NULL。
*/
void *dma_alloc_attrs(struct device *dev, size_t size, dma_addr_t *dma_handle,
gfp_t flag, unsigned long attrs)
{
const struct dma_map_ops *ops = get_dma_ops(dev);
void *cpu_addr;

/* 警告:设备必须有一个有效的一致性DMA掩码。 */
WARN_ON_ONCE(!dev->coherent_dma_mask);
/* ... */

/* 优先级1:尝试从设备专属的、预留的一致性内存池中分配。 */
if (dma_alloc_from_dev_coherent(dev, size, dma_handle, &cpu_addr)) {
/* ... 追踪 ... */
return cpu_addr;
}

/* 清理掉通用的区域标志,让DMA子系统根据设备掩码自行决定。 */
flag &= ~(__GFP_DMA | __GFP_DMA32 | __GFP_HIGHMEM);

/* 优先级2:如果系统使用直接映射(无IOMMU),则调用直接分配函数。 */
if (dma_alloc_direct(dev, ops)) {
cpu_addr = dma_direct_alloc(dev, size, dma_handle, flag, attrs);
/* 优先级3:如果系统使用IOMMU,则调用IOMMU的分配函数。 */
} else if (use_dma_iommu(dev)) {
cpu_addr = iommu_dma_alloc(dev, size, dma_handle, flag, attrs);
/* 优先级4:调用架构特定的、通过ops注册的回调函数。 */
} else if (ops->alloc) {
cpu_addr = ops->alloc(dev, size, dma_handle, flag, attrs);
} else {
/* ... 追踪并返回NULL ... */
return NULL;
}

/* ... 追踪与调试 ... */
return cpu_addr;
}
EXPORT_SYMBOL(dma_alloc_attrs);

/**
* @brief 释放一块带属性的DMA一致性内存(核心实现)。
* @param dev 分配内存的设备。
* @param size 内存大小。
* @param cpu_addr CPU虚拟地址。
* @param dma_handle DMA总线地址。
* @param attrs DMA分配属性。
*/
void dma_free_attrs(struct device *dev, size_t size, void *cpu_addr,
dma_addr_t dma_handle, unsigned long attrs)
{
const struct dma_map_ops *ops = get_dma_ops(dev);

/* 对应优先级1的释放。 */
if (dma_release_from_dev_coherent(dev, get_order(size), cpu_addr))
return;

/*
* 警告:在中断禁用的上下文中调用此函数是危险的,
* 因为某些平台的释放操作可能会睡眠。
*/
WARN_ON(irqs_disabled());
/* ... */
if (!cpu_addr)
return;
/* ... */
/* 按照与分配时相同的逻辑,选择正确的释放函数。 */
if (dma_alloc_direct(dev, ops))
dma_direct_free(dev, size, cpu_addr, dma_handle, attrs);
else if (use_dma_iommu(dev))
iommu_dma_free(dev, size, cpu_addr, dma_handle, attrs);
else if (ops->free)
ops->free(dev, size, cpu_addr, dma_handle, attrs);
}
EXPORT_SYMBOL(dma_free_attrs);