[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/dmaengine.h

这一组定义在头文件中的静态内联函数共同构成了一个高效、轻量级且线程安全的框架, 用于追踪Linux内核中异步DMA传输的生命周期。它们是通用DMA引擎(DMA Engine)子系统的基石, STM32 DMA驱动等具体实现都依赖这个框架来管理传输任务。

其核心原理是实现了一个**”票据系统” (ticket system)**:

  1. 初始化 (dma_cookie_init): DMA通道像一个票据分发机, 初始化时将”已分发”和”已完成”的票号都重置为起始值。
  2. 分配 (dma_cookie_assign): 每当一个新的DMA传输任务(由dma_async_tx_descriptor表示)被提交时, 就会从分发机取一张新的、唯一的、顺序递增的票(cookie), 并将这张票贴在任务上。同时, 分发机记下”最后分发的票号”。
  3. 完成 (dma_cookie_complete): 当DMA硬件完成一个任务并触发中断时, 中断处理程序会拿出完成任务上的票, 并更新一个公告板, 上面写着”最后完成的票号”。
  4. 查询 (dma_cookie_status): 任何时候, 任何人都可以拿着自己手里的票号去和公告板上的”最后完成票号”以及分发机的”最后分发票号”做比较。通过比较, 就可以确定这个任务是已经完成、正在处理、还是仍在排队。

这个系统巧妙地利用了单调递增的整数, 使得状态查询可以无锁 (lock-free) 执行, 极大地提高了性能。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* dma_cookie_init - 初始化一个DMA通道的cookie
* @chan: 要初始化的dma通道
*/
static inline void dma_cookie_init(struct dma_chan *chan)
{
/*
* chan->cookie: 代表最后分配出去的cookie.
* chan->completed_cookie: 代表最后一个确认完成的cookie.
* DMA_MIN_COOKIE: cookie的起始值, 通常是1, 因为0被用作无效值.
* 原理: 将通道的两个核心追踪器都重置为起始状态, 为新的传输序列做准备.
* 这通常在DMA驱动的probe函数中为每个通道调用一次.
*/
chan->cookie = DMA_MIN_COOKIE;
chan->completed_cookie = DMA_MIN_COOKIE;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* dma_cookie_assign - 为描述符分配一个DMA引擎cookie
* @tx: 需要cookie的描述符
*
* 原理: 这是一个票据分发器. 它原子地(在调用者持有的锁的保护下)递增通道的cookie计数器,
* 并将这个新的、唯一的ID同时赋给描述符和通道的"最后分配"追踪器.
*/
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; // <-- 计算下一个cookie值.
if (cookie < DMA_MIN_COOKIE) // <-- 处理32位整数溢出回绕的情况.
cookie = DMA_MIN_COOKIE;
tx->cookie = cookie; // <-- 将新cookie赋给描述符.
chan->cookie = cookie; // <-- 更新通道的"最后分配"cookie.

return cookie;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* dma_cookie_complete - 完成一个描述符
* @tx: 要完成的描述符
*
* 原理: 这是中断处理程序的核心动作. 它更新通道的"公告板", 宣告此cookie代表的任务已经完成.
* 将描述符自身的cookie清零是一个重要的安全措施, 防止因代码逻辑错误导致同一个任务被意外地"完成"两次.
*/
static inline void dma_cookie_complete(struct dma_async_tx_descriptor *tx)
{
// BUG_ON确保我们不会去完成一个从未被分配cookie的任务.
BUG_ON(tx->cookie < DMA_MIN_COOKIE);
// <-- 核心操作: 更新通道的"最后完成"cookie.
tx->chan->completed_cookie = tx->cookie;
// <-- 安全措施: 作废描述符上的cookie.
tx->cookie = 0;
}

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
/**
* dma_cookie_status - 报告cookie的状态
* @chan: dma通道
* @cookie: 我们感兴趣的cookie
* @state: 用于返回详细状态的结构体
*
* 原理: 这是一个无锁查询函数. 它首先原子地读取"最后分配"和"最后完成"的快照,
* 然后调用 dma_async_is_complete (一个处理整数回绕的比较函数) 来判断给定的cookie
* 是否小于等于"最后完成"的cookie.
*/
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; // <-- 读取"最后分配"的cookie.
complete = chan->completed_cookie; // <-- 读取"最后完成"的cookie.
/*
* barrier(): 这是一个内存屏障. 在无锁的上下文中, 它至关重要.
* 它确保了编译器和CPU不会重排上面两个读操作和下面 dma_async_is_complete 中的读操作.
* 这保证了我们总是基于一个一致的(尽管可能略微过时)状态快照来进行判断.
*/
barrier();
if (state) { // <-- 如果调用者需要详细信息, 填充state结构.
state->last = complete;
state->used = used;
state->residue = 0; // 剩余字节数默认为0, 具体驱动会更新它.
state->in_flight_bytes = 0; // 同上
}
// 调用核心比较逻辑, 返回 DMA_COMPLETE 或 DMA_IN_PROGRESS.
return dma_async_is_complete(cookie, complete, used);
}

dma_set_residuedma_set_in_flight_bytes: 状态设置辅助函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
* dma_set_residue - 设置状态中的剩余字节数
* 原理: 这是一个简单的、安全的设置器. 它避免了调用者每次都需要检查 state 指针是否为NULL.
*/
static inline void dma_set_residue(struct dma_tx_state *state, u32 residue)
{
if (state)
state->residue = residue;
}

/*
* dma_set_in_flight_bytes - 设置状态中正在传输的字节数
* 原理: 同上, 一个安全的设置器.
*/
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
/*
* 结构体定义: dmaengine_desc_callback
* 作用: 临时存储从一个DMA描述符中提取出的回调函数信息.
*/
struct dmaengine_desc_callback {
/*
* .callback: 一个函数指针, 指向一个简单的回调函数.
* dma_async_tx_callback 的类型定义为: void (*)(void *param);
* 这是旧版的回调类型.
*/
dma_async_tx_callback callback;
/*
* .callback_result: 一个函数指针, 指向一个更详细的回调函数.
* dma_async_tx_callback_result 的类型定义为: void (*)(void *param, const struct dmaengine_result *result);
* 这是新版的回调类型, 能够传递一个包含传输结果(如成功/失败, 剩余字节数)的结构体.
*/
dma_async_tx_callback_result callback_result;
/*
* .callback_param: 一个 void 指针, 用于传递给回调函数的参数.
* 通常, 提交DMA请求的驱动会把自身的上下文指针(如指向其设备结构体的指针)放在这里.
*/
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
/**
* dmaengine_desc_get_callback - 获取传入的回调函数
* @tx: 传输描述符
* @cb: 用于保存回调信息的临时结构体
*
* 将传入的tx描述符结构体中的回调信息填充到传入的cb结构体中.
* 这个操作不需要加锁.
*/
static inline void
dmaengine_desc_get_callback(struct dma_async_tx_descriptor *tx,
struct dmaengine_desc_callback *cb)
{
/*
* 将 tx 描述符中的 callback 成员 (函数指针) 复制到 cb 结构体中.
*/
cb->callback = tx->callback;
/*
* 将 tx 描述符中的 callback_result 成员 (函数指针) 复制到 cb 结构体中.
*/
cb->callback_result = tx->callback_result;
/*
* 将 tx 描述符中的 callback_param 成员 (void 指针) 复制到 cb 结构体中.
*/
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
/**
* dmaengine_desc_callback_invoke - 调用cb结构体中的回调函数
* @cb: 保存着回调信息的临时结构体
* @result: 传输结果
*
* 使用cb结构体中的参数来调用cb结构体中提供的回调函数.
* 是否需要加锁取决于驱动程序的具体实现.
*/
static inline void
dmaengine_desc_callback_invoke(struct dmaengine_desc_callback *cb,
const struct dmaengine_result *result)
{
/*
* 在栈上定义并初始化一个"虚拟的"成功结果.
* 如果调用者没有提供有效的 'result', 并且需要调用新版的回调函数时, 将使用这个默认结果.
*/
struct dmaengine_result dummy_result = {
.result = DMA_TRANS_NOERROR, // 结果: 没有错误
.residue = 0 // 剩余字节数: 0
};

/*
* 优先检查新版的回调函数指针是否存在.
*/
if (cb->callback_result) {
/*
* 如果调用者没有提供结果 (result == NULL), 则使用我们创建的虚拟成功结果.
* 这确保了新版回调函数总是能接收到一个有效的 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
/**
* dmaengine_desc_get_callback_invoke - 获取tx描述符中的回调并立即调用它.
* @tx: DMA异步传输描述符
* @result: 传输结果
*
* 在一个函数内调用 dmaengine_desc_get_callback() 和 dmaengine_desc_callback_invoke(),
* 因为对于驱动来说, 这两步之间通常不需要做额外的工作.
* 是否需要加锁取决于驱动程序的具体实现.
*/
static inline void
dmaengine_desc_get_callback_invoke(struct dma_async_tx_descriptor *tx,
const struct dmaengine_result *result)
{
/*
* 在栈上定义一个临时的回调信息结构体.
*/
struct dmaengine_desc_callback cb;

/*
* 第一步: 从 tx 描述符中获取回调信息到 cb.
*/
dmaengine_desc_get_callback(tx, &cb);
/*
* 第二步: 立即调用存储在 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
/**
* dmaengine_desc_callback_valid - 验证cb中的回调是否有效
* @cb: 回调信息结构体
*
* 返回一个布尔值, 验证cb中的回调是否有效.
* 这个操作不需要加锁.
*/
static inline bool
dmaengine_desc_callback_valid(struct dmaengine_desc_callback *cb)
{
/*
* 如果 cb->callback 指针不为NULL, 或者 cb->callback_result 指针不为NULL,
* 表达式为真, 函数返回 true.
* 只有当两个指针都为NULL时, 才返回 false.
*/
return cb->callback || cb->callback_result;
}

drivers/dma/dmaengine.c

DMA引擎 事务类型与通道查找表

此代码片段属于Linux内核DMA引擎(dmaengine)子系统的核心部分。它的核心作用是定义DMA引擎支持的所有操作类型, 并创建一个全局的、高性能的查找表(channel_table), 以便内核可以快速地为特定的DMA任务(如内存拷贝、异或计算)找到一个最合适的DMA通道

该机制最关键的特性是它为每个CPU核心都维护一个独立的查找表副本。这一原理是通过__percpu关键字实现的, 它的主要目的是在多核系统中避免锁竞争, 从而极大地提升性能。当一个CPU上的驱动程序需要一个DMA通道时, 它可以直接访问自己私有的查找表, 无需与其它CPU同步。

原理与工作流程:

  1. enum dma_transaction_type (DMA事务类型): 这个枚举定义了dmaengine框架所能理解的所有标准DMA操作。这包括:

    • 简单的内存操作: DMA_MEMCPY (内存拷贝), DMA_MEMSET (内存填充)。
    • 复杂的计算卸载: DMA_XOR, DMA_PQ (常用于RAID加速)。
    • 外设相关的传输类型: DMA_SLAVE (例如, 从SPI外设到内存), DMA_CYCLIC (用于音频等循环缓冲)。
    • 这些枚举值是DMA控制器驱动和DMA客户端驱动之间沟通能力的”通用语言”。
  2. channel_table (通道查找表): 这是一个全局数组, 但被__percpu修饰。

    • channel_table[DMA_MEMCPY] 这一项专门用来存放能执行”内存拷贝”的DMA通道。
    • __percpu意味着每个CPU核心都有自己独立的channel_table副本。CPU 0写入channel_table的数据不会影响CPU 1的副本。
  3. 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
/* dma_transaction_type - DMA事务类型/索引的枚举 */
enum dma_transaction_type {
DMA_MEMCPY, // 内存到内存拷贝
DMA_XOR, // 对多个源进行异或操作, 存入一个目标
DMA_PQ, // P+Q校验码计算 (RAID6)
// ... 其他类型
DMA_SLAVE, // 外设到内存或内存到外设的传输
DMA_CYCLIC, // 循环模式DMA, 用于音频等
// ...
DMA_TX_TYPE_END, // 标志事务类型的结束, 用于定义数组大小
};

/* --- 客户端和设备注册 --- */

/* 定义一个dma能力位掩码, 用于遍历所有操作类型. */
static dma_cap_mask_t dma_cap_mask_all;

/* dma_chan_tbl_ent - 用于追踪每个核心/每种操作的通道分配情况的条目 */
struct dma_chan_tbl_ent {
struct dma_chan *chan; // 指向与此条目关联的DMA通道
};

/*
* 为内存到内存卸载操作提供者的per-cpu查找表.
* 这是一个数组, 数组的每个元素都是一个指向 per-cpu 数据的指针.
*/
static struct dma_chan_tbl_ent __percpu *channel_table[DMA_TX_TYPE_END];

/* dma_channel_table_init - 初始化DMA通道表 */
static int __init dma_channel_table_init(void)
{
enum dma_transaction_type cap;
int err = 0;

/* 将 dma_cap_mask_all 的所有位都设置为1. */
bitmap_fill(dma_cap_mask_all.bits, DMA_TX_TYPE_END);

/*
* 'interrupt', 'private', 'slave' 是通道的能力, 但不与具体
* 的内存操作关联, 因此它们不需要在channel_table中占有条目.
* 这个表专门用于 memcpy, xor 等 "计算/卸载" 操作.
*/
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);

/* 遍历所有有效的DMA能力掩码位. */
for_each_dma_cap_mask(cap, dma_cap_mask_all) {
/*
* 为每种能力类型, 分配一个 per-cpu 的 dma_chan_tbl_ent 实例.
* 这意味着每个CPU核心都会有自己的 dma_chan_tbl_ent 实例.
*/
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);
/* 清理所有已经成功分配的per-cpu内存. */
for_each_dma_cap_mask(cap, dma_cap_mask_all)
free_percpu(channel_table[cap]);
}

return err;
}
/*
* arch_initcall - 这是一个内核初始化宏, 确保此函数在系统启动的早期阶段被调用,
* 在任何可能使用DMA的驱动程序初始化之前.
*/
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_createkmem_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
  • 最后, 它初始化dmaenginedebugfs接口, 为调试提供便利。
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
/* dmaengine_unmap_pool: 描述一个用于DMA unmap操作的内存池 */
struct dmaengine_unmap_pool {
struct kmem_cache *cache; // 指向底层的SLAB缓存
const char *name; // 池的名称, 用于调试
mempool_t *pool; // 指向内存池的指针
size_t size; // 池中对象能容纳的dma_addr_t的数量
};

/* __UNMAP_POOL: 一个便捷宏, 用于在数组中初始化一个池的定义. */
#define __UNMAP_POOL(x) { .size = x, .name = "dmaengine-unmap-" __stringify(x) }

/* unmap_pool: 静态定义的内存池数组. */
static struct dmaengine_unmap_pool unmap_pool[] = {
__UNMAP_POOL(2), // 一个用于简单传输(最多2个地址)的池
#if IS_ENABLED(CONFIG_DMA_ENGINE_RAID) // 仅当内核配置了RAID支持时
__UNMAP_POOL(16), // 用于更复杂传输的池
__UNMAP_POOL(128),
__UNMAP_POOL(256),
#endif
};

/* dmaengine_init_unmap_pool: 初始化unmap内存池 */
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;

/* 步骤1: 创建一个SLAB缓存, 用于高效分配/释放这种大小的对象. */
p->cache = kmem_cache_create(p->name, size, 0,
SLAB_HWCACHE_ALIGN, NULL);
if (!p->cache)
break; // 如果创建失败, 退出循环.

/* 步骤2: 在SLAB缓存之上创建一个内存池, 预分配至少1个对象. */
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; // 返回内存不足错误.
}

/* dma_bus_init: DMA总线初始化函数 */
static int __init dma_bus_init(void)
{
int err = dmaengine_init_unmap_pool(); // 首先初始化内存池.

if (err)
return err;

/* 注册 "dma" 设备类, 在 /sys/class/ 下创建 "dma" 目录. */
err = class_register(&dma_devclass);
if (!err)
dmaengine_debugfs_init(); // 如果类注册成功, 初始化debugfs接口.

return err;
}
/* 使用 arch_initcall 确保此函数在内核启动早期被调用. */
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
/*
* dma_async_tx_descriptor_init - 初始化一个 dma_async_tx_descriptor
* @tx: 要初始化的传输描述符
* @chan: 该描述符所属的DMA通道
*/
void dma_async_tx_descriptor_init(struct dma_async_tx_descriptor *tx,
struct dma_chan *chan)
{
/*
* 1. 核心操作: 将描述符的 `chan` 成员指向它所属的通道.
* 这就在"任务单"(tx)和"执行者"(chan)之间建立了一个直接的链接.
* 之后, 任何拿到这个描述符的代码都可以通过 `tx->chan` 快速找到控制它的DMA通道.
*/
tx->chan = chan;
/*
* 2. 条件性初始化锁.
* #ifdef CONFIG_ASYNC_TX_ENABLE_CHANNEL_SWITCH 是一个预处理指令,
* 意味着下面的代码只有在内核编译时配置了 CONFIG_ASYNC_TX_ENABLE_CHANNEL_SWITCH 选项时才会被包含.
* 这个配置用于支持一种高级功能, 即一个准备好的DMA任务可以在不同的通道之间切换.
* 为了在多核系统上安全地进行这种切换, 描述符本身需要一个锁来保护其内部状态.
* 对于大多数嵌入式系统(包括典型的STM32H750应用场景), 这个功能通常是关闭的,
* 因此这部分代码通常不会被编译, 函数的执行体就只有上面的一行赋值语句.
*/
#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)可见。这个过程包括三个关键步骤:

  1. 资源分配: 为通道分配必要的软件资源, 包括用于存储运行时状态的per-CPU数据(chan->local)和一个用于在sysfs中表示该通道的struct dma_channel_dev结构(chan->dev)。
  2. 身份分配: 调用IDA(ID Allocator)机制, 从其父DMA设备的ID池中为该通道分配一个唯一的、局部的通道ID号。
  3. 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
/*
* 静态函数声明: __dma_async_device_channel_register
* 这是 dma_async_device_register 的一个内部辅助函数.
* @device: 指向父 dma_device 设备的指针.
* @chan: 指向需要被注册的 dma_chan 结构体的指针.
* @name: 一个可选的、用于sysfs的自定义名称.
* @return: 成功时返回 0, 失败时返回负的错误码.
*/
static int __dma_async_device_channel_register(struct dma_device *device,
struct dma_chan *chan,
const char *name)
{
int rc;

/*
* 为通道的 'local' 成员分配 per-CPU 内存.
* per-CPU 变量用于存储每个CPU核心私有的数据, 以避免在多核系统上加锁.
* 在 STM32H750 这样的单核系统上, 这等效于分配一个单独的实例.
*/
chan->local = alloc_percpu(typeof(*chan->local));
if (!chan->local)
return -ENOMEM;
/*
* 为 chan->dev 分配内存, chan->dev (类型为 dma_channel_dev) 主要用于在sysfs中表示这个通道.
* kzalloc 会分配内存并将其内容清零.
*/
chan->dev = kzalloc(sizeof(*chan->dev), GFP_KERNEL);
if (!chan->dev) {
rc = -ENOMEM;
goto err_free_local; // 如果失败, 跳转到清理 per-CPU 内存的标签.
}

/*
* 调用 ida_alloc 从父设备 'device' 的通道ID分配器 (chan_ida) 中获取一个唯一的ID号.
* IDA (ID Allocator) 是内核中用于动态分配0到INT_MAX范围内唯一ID的机制.
*/
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 的标签.
}

/*
* 开始填充用于在sysfs中注册的 device 结构体.
* 设置其设备类别为 dma_devclass, 这会使它出现在 /sys/class/dma/ 目录下.
*/
chan->dev->device.class = &dma_devclass;
/*
* 设置其父设备为 DMA 控制器设备本身.
*/
chan->dev->device.parent = device->dev;
/*
* 在 sysfs 设备和 dma_chan 结构之间建立一个反向链接.
*/
chan->dev->chan = chan;
/*
* 记录父DMA设备的ID.
*/
chan->dev->dev_id = device->dev_id;
/*
* 如果调用者没有提供自定义名称.
*/
if (!name)
/*
* 使用父设备ID和通道ID构建一个标准的名称, 例如 "dma0chan1".
*/
dev_set_name(&chan->dev->device, "dma%dchan%d", device->dev_id, chan->chan_id);
else
/*
* 否则, 使用提供的自定义名称.
*/
dev_set_name(&chan->dev->device, "%s", name);
/*
* 调用 device_register() 将这个新构建的设备正式注册到内核的设备模型中.
* 执行成功后, 在 sysfs 中就会出现对应的条目.
*/
rc = device_register(&chan->dev->device);
if (rc)
goto err_out_ida; // 如果注册失败, 跳转到释放ID的标签.

/*
* 初始化客户端计数器.
*/
chan->client_count = 0;
/*
* 父设备的总通道数加一.
*/
device->chancnt++;

/*
* 成功返回.
*/
return 0;

/*
* 错误处理回滚路径.
*/
err_out_ida:
/*
* 释放刚刚从IDA中分配的ID, 将其归还到池中.
*/
ida_free(&device->chan_ida, chan->chan_id);
err_free_dev:
/*
* 释放为 chan->dev 分配的内存.
*/
kfree(chan->dev);
err_free_local:
/*
* 释放为 chan->local 分配的 per-CPU 内存.
*/
free_percpu(chan->local);
/*
* 将 local 指针置为 NULL, 防止悬空指针.
*/
chan->local = NULL;
/*
* 返回导致失败的错误码.
*/
return rc;
}

dma_channel_rebalance: 重新平衡并分配全局DMA通道

此函数是Linux DMA引擎框架的核心调度器和负载均衡器。它的主要作用是构建和更新一个名为channel_table的全局快速查找表。这个表的存在, 使得当一个客户端驱动请求一个特定功能(如内存拷贝)的DMA通道时, 内核可以立即为其提供一个当前最优的选择, 而无需在每次请求时都去遍历系统中所有已注册的DMA控制器和通道。

该函数的原理可以概括为**”先拆毁, 再重建”**的负载均衡策略:

  1. 完全重置 (Teardown): 函数首先进入一个”拆毁”阶段, 为即将进行的重新分配清理环境。

    • 它遍历整个channel_table, 将其中每一个条目都清空为NULL。这确保了旧的、可能已过时的分配关系被完全抹除。
    • 它遍历系统中所有公共的(非DMA_PRIVATE)DMA通道, 将它们各自的table_count(一个用于衡量该通道被分配了多少次任务的”负载计数器”)清零。
  2. 优化检查: 在重建之前, 它会检查一个全局引用计数dmaengine_ref_count。如果这个计数为零, 意味着当前系统中没有任何客户端驱动正在使用或等待DMA通道。在这种情况下, 填充查找表是毫无意义的, 函数会提前退出, 这是一个重要的性能优化。

  3. 重新分配 (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
/**
* dma_channel_rebalance - 重新分配可用的通道
*
* 在SMP(多核)情况下, 优化CPU隔离(每个CPU为一种操作类型获取一个专用通道).
* 在非SMP(单核)情况下, 优化操作隔离(避免让通道执行多种任务).
*
* 必须在 dma_list_mutex 锁的保护下调用.
*/
static void dma_channel_rebalance(void)
{
struct dma_chan *chan;
struct dma_device *device;
int cpu;
int cap;

/*
* 第一阶段: 撤销上一次的分配结果 (拆毁)
*/

/* 遍历所有可能的DMA能力(capability) */
for_each_dma_cap_mask(cap, dma_cap_mask_all)
/* 遍历所有可能的CPU核心 */
for_each_possible_cpu(cpu)
/*
* 将全局查找表 channel_table 中对应 [能力][CPU] 的条目清空.
* per_cpu_ptr 用于获取 per-CPU 变量的指针.
*/
per_cpu_ptr(channel_table[cap], cpu)->chan = NULL;

/* 遍历全局 dma_device_list 链表中的每一个已注册的DMA设备 */
list_for_each_entry(device, &dma_device_list, global_node) {
/* 跳过标记为私有的DMA设备, 它们不参与全局平衡 */
if (dma_has_cap(DMA_PRIVATE, device->cap_mask))
continue;
/* 遍历该设备的每一个通道 */
list_for_each_entry(chan, &device->channels, device_node)
/* 将每个通道的"负载计数器" (table_count) 清零 */
chan->table_count = 0;
}

/*
* 第二阶段: 优化检查
*/

/* 如果没有任何客户端(驱动)正在使用或等待DMA引擎, 就没有必要填充查找表 */
if (!dmaengine_ref_count)
return;

/*
* 第三阶段: 重新分配可用的通道 (重建)
*/

/* 遍历所有可能的DMA能力 */
for_each_dma_cap_mask(cap, dma_cap_mask_all)
/* 遍历所有当前在线的CPU核心 (对于单核系统, 这个循环只会执行一次, cpu=0) */
for_each_online_cpu(cpu) {
/*
* 调用 min_chan, 这是负载均衡算法的核心.
* 它会查找全系统中支持 'cap' 能力且当前负载最低的通道.
*/
chan = min_chan(cap, cpu);
/*
* 将找到的最优通道 'chan' 填充到全局查找表的 [能力][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
/**
* dma_async_device_register - 注册发现的DMA设备
* @device: 指向 &struct dma_device 的指针
*
* 调用此例程后, 除了在 device_release() 回调中, 不应释放该结构体.
* device_release() 将在 dma_async_device_unregister() 被调用且没有其他引用后被调用.
*/
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"); // 设备必须有关联的 struct device
return -EIO;
}

// 从驱动程序中获取模块所有者信息, 用于引用计数
device->owner = device->dev->driver->owner;

/*
* 定义一个宏, 用于检查: 如果设备声明了某个能力(capability),
* 那么它必须提供实现该能力的函数指针.
*/
#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)); /* __stringify将宏参数转为字符串 */ \
return -EIO; \
} \
}

// 使用宏对各种DMA能力进行检查
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;
}

// device_release 是可选的, 但没有它, 热拔插可能不安全
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);

// 如果设备支持所有传输类型, 则给它打上通用的 ASYNC_TX 标志
if (device_has_all_tx_types(device))
dma_cap_set(DMA_ASYNC_TX, device->cap_mask);

// 为设备获取一个全局唯一的ID号
rc = get_dma_id(device);
if (rc != 0)
return rc;

// 初始化用于分配通道ID的IDA (ID Allocator)
ida_init(&device->chan_ida);

// 遍历设备的所有通道, 并为它们在sysfs中创建条目
list_for_each_entry(chan, &device->channels, device_node) {
rc = __dma_async_device_channel_register(device, chan, NULL);
if (rc < 0)
goto err_out; // 如果失败, 跳转到错误清理
}

// 锁定全局DMA列表互斥锁, 准备修改全局列表
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; // 如果获取失败, 说明有问题, 清理退出
}
}
// 使用RCU安全地将设备添加到全局DMA设备链表的尾部
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);

// 为设备注册DebugFS接口
dmaengine_debug_register(device);

return 0; // 成功

err_out: // 错误清理路径
// 如果没有任何通道被成功注册, 只需释放设备ID
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; // 断开sysfs设备与通道的连接
mutex_unlock(&dma_list_mutex);
device_unregister(&chan->dev->device); // 注销sysfs设备
free_percpu(chan->local); // 释放per-cpu数据
}
return rc; // 返回错误码
}
EXPORT_SYMBOL(dma_async_device_register);

dma_chan_get: 安全地获取并声明一个DMA通道的所有权

此函数是Linux DMA引擎框架内部的一个核心资源管理函数。它的主要作用是在一个客户端驱动程序正式开始使用一个DMA通道之前, 执行一个多层次的、健壮的资源获取和引用计数流程。它不仅仅是返回一个指针, 而是作为一个”最终的守门员”, 确保与该通道关联的所有底层资源(内核模块、DMA设备、通道特定内存)都已准备就绪, 并且其生命周期已被正确管理。

此函数被设计为在dma_list_mutex锁的保护下调用, 其核心原理是为一个即将被激活的DMA通道, 安全地”上线”其所有依赖资源, 并更新其使用状态

这个过程分为两种主要路径:

  1. 通道已被占用 (共享路径):

    • 如果chan->client_count大于0, 意味着这个通道已经被一个或多个客户端驱动占用。在这种情况下, 它只执行两个简单的引用计数操作: __module_get增加DMA控制器驱动模块的引用计数, chan->client_count++增加本通道的客户端计数。这允许多个客户端共享同一个通道(如果硬件和驱动支持)。
  2. 通道首次被使用 (独占或首次获取路径):

    • 如果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
/**
* dma_chan_get - 尝试获取一个DMA通道的父驱动模块
* @chan: 要获取的通道
*
* 必须在 dma_list_mutex 锁的保护下调用.
*/
static int dma_chan_get(struct dma_chan *chan)
{
/*
* 获取拥有此通道的内核模块的指针.
*/
struct module *owner = dma_chan_to_owner(chan);
int ret;

/*
* 路径1: 通道已被一个或多个客户端使用 (共享情况).
*/
if (chan->client_count) {
/*
* 已经有客户端了, 我们只需要简单地增加引用计数.
* __module_get: 强制增加模块引用计数.
*/
__module_get(owner);
/*
* 增加本通道的客户端计数.
*/
chan->client_count++;
return 0; // 成功
}

/*
* 路径2: 通道首次被使用 (client_count 为 0).
*/

/*
* 步骤 1: 模块存活检查.
* 尝试获取模块引用. 如果模块正在被卸载, 此函数会失败.
*/
if (!try_module_get(owner))
return -ENODEV; // 模块不可用, 获取失败.

/*
* 步骤 2: 设备存活检查.
* 安全地获取DMA设备(device)的引用. 如果设备正在被释放(ref为0), 此函数会失败.
*/
ret = kref_get_unless_zero(&chan->device->ref);
if (!ret) {
ret = -ENODEV; // 设备不可用, 获取失败.
goto module_put_out; // 跳转到清理步骤1(释放模块引用).
}

/*
* 步骤 3: 按需分配通道资源.
* 如果DMA驱动提供了这个回调函数, 就在这里调用它.
*/
if (chan->device->device_alloc_chan_resources) {
ret = chan->device->device_alloc_chan_resources(chan);
if (ret < 0)
goto err_out; // 如果资源分配失败, 跳转到清理步骤1和2.
}

/*
* 步骤 4: 更新状态.
* 所有检查和分配都已成功, 将客户端计数从0增加到1.
*/
chan->client_count++;

/*
* 步骤 5: 通知全局调度器.
* 如果这不是一个私有通道, 调用 balance_ref_count 来更新全局客户端计数,
* 这可能会触发 dma_channel_rebalance.
*/
if (!dma_has_cap(DMA_PRIVATE, chan->device->cap_mask))
balance_ref_count(chan);

return 0; // 成功

/*
* 健壮的错误回滚路径
*/
err_out:
/*
* 清理步骤2: 释放对DMA设备的引用.
*/
dma_device_put(chan->device);
module_put_out:
/*
* 清理步骤1: 释放对模块的引用.
*/
module_put(owner);
return ret; // 返回错误码.
}

dma_get_slave_channel: 独占性地获取一个指定的DMA通道

此函数是Linux DMA引擎框架中一个非常特殊且重要的API。它的核心作用是允许一个驱动程序尝试以独占的方式, 获取一个它已经通过其他方式(例如, of_xlate函数)识别出的、确切的物理DMA通道

与通用的dma_request_channel(它会根据能力自动选择一个可用的通道)不同, 此函数的目标是”我需要这一个通道, 而且在我使用期间, 不希望通用的分配系统再把它分配给别人”。

其核心原理是通过一个巧妙的”私有化”机制来确保独占性:

  1. 锁定全局状态: 函数首先获取dma_list_mutex全局互斥锁。这是至关重要的, 因为它即将修改一个DMA控制器设备的全局可见状态。
  2. 检查可用性: 它做的第一件事, 也是最关键的检查, 是chan->client_count == 0。这个计数器记录了当前有多少个”客户端”正在使用这个通道。如果计数不为0, 意味着该通道已经被占用了, 独占请求立即失败。
  3. 执行”私有化”: 如果通道当前是空闲的, 函数会执行一系列原子操作来”保留”它:
    • 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。
  4. 健壮的错误回滚: 如果在正式获取通道时(虽然不太可能)发生错误, 它会执行一个精确的回滚操作: 它会递减privatecnt计数器。如果这个计数器减到0, 意味着这是该设备上最后一个被独占的通道, 那么它就会调用dma_cap_clear(DMA_PRIVATE, ...)移除整个设备的”私有”标志, 使其重新对通用分配系统开放。
  5. 释放锁并返回: 完成所有操作后, 它会释放全局锁, 并返回结果——成功时是原始的通道指针, 失败时是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
/**
* dma_get_slave_channel - 尝试独占性地获取一个指定的通道
* @chan: 目标通道
*/
struct dma_chan *dma_get_slave_channel(struct dma_chan *chan)
{
/*
* 获取全局DMA列表互斥锁, 以保护对通道客户端计数和设备私有状态的并发访问.
* __dma_request_channel (通用请求函数)也会获取这个锁.
*/
mutex_lock(&dma_list_mutex);

/*
* 核心检查: 只有当没有任何客户端正在使用这个通道时 (client_count == 0),
* 我们才有可能独占性地获取它.
*/
if (chan->client_count == 0) {
struct dma_device *device = chan->device;
int err;

/*
* "私有化"步骤 1:
* 给该通道所属的整个DMA控制器设备打上 DMA_PRIVATE 标志.
* 这会阻止 dma_request_channel 再从这个设备自动分配通道.
*/
dma_cap_set(DMA_PRIVATE, device->cap_mask);
/*
* "私有化"步骤 2:
* 增加该设备的私有通道计数器.
*/
device->privatecnt++;
/*
* "私有化"步骤 3:
* 调用 dma_chan_get 来正式获取对该通道的引用 (这会增加 client_count).
*/
err = dma_chan_get(chan);
if (err) {
/*
* 如果 dma_chan_get 失败 (理论上不应发生, 但作为保护).
*/
dev_dbg(chan->device->dev,
"%s: failed to get %s: (%d)\n",
__func__, dma_chan_name(chan), err);
chan = NULL; // 将返回值设为NULL, 表示失败.

/*
* 错误回滚: 递减私有计数器. 如果计数器归零,
* 意味着这是最后一个私有通道, 必须清除整个设备的 DMA_PRIVATE 标志,
* 使其恢复为公共可用状态.
*/
if (--device->privatecnt == 0)
dma_cap_clear(DMA_PRIVATE, device->cap_mask);
}
} else {
/*
* 如果 client_count 不为0, 说明通道已被占用, 独占请求失败.
*/
chan = NULL;
}

/*
* 释放全局互斥锁.
*/
mutex_unlock(&dma_list_mutex);


/*
* 返回结果: 成功时是有效的chan指针, 失败时是NULL.
*/
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函数中, 它被调用两次:

  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);

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函数就是这个驱动的”大脑”, 它负责:

  1. 从大量可用的DMAMUX输出通道中, 找到一个空闲的通道
  2. 通过写硬件寄存器, 将发起请求的外设信号连接到这个空闲的DMAMUX通道上
  3. 巧妙地修改(rewrite)原始的DMA请求规格(dma_spec), 将其目标从DMAMUX设备重定向到实际的、承载这个通道的DMA控制器(如DMA1)以及该控制器上的具体流(stream)编号。
  4. 将修改后的请求规格返回给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
/*
* 宏定义: STM32_DMAMUX_CCR(x)
* 计算第 x 个DMAMUX通道配置寄存器(Channel Configuration Register)的偏移地址.
* 每个寄存器占4个字节.
*/
#define STM32_DMAMUX_CCR(x) (0x4 * (x))
/*
* 宏定义: STM32_DMAMUX_MAX_DMA_REQUESTS
* 定义了DMAMUX可以输出到的DMA通道/流的最大数量.
*/
#define STM32_DMAMUX_MAX_DMA_REQUESTS 32
/*
* 宏定义: STM32_DMAMUX_MAX_REQUESTS
* 定义了可以连接到DMAMUX的外设请求源的最大数量.
*/
#define STM32_DMAMUX_MAX_REQUESTS 255

/*
* stm32_dmamux: 用于描述一个已建立的DMAMUX路由连接.
*/
struct stm32_dmamux {
u32 master; // 该连接最终通向的DMA主控制器编号 (例如 0 代表DMA1, 1 代表DMA2).
u32 request; // 来自外设的原始请求ID号 (例如 SPI1_TX 的请求号).
u32 chan_id; // 分配给此次连接的DMAMUX输出通道编号.
};

/*
* stm32_dmamux_data: DMAMUX驱动的核心私有数据结构.
*/
struct stm32_dmamux_data {
struct dma_router dmarouter; // 内核标准的DMA路由器结构体.
struct clk *clk; // 指向DMAMUX外设时钟的指针.
void __iomem *iomem; // 映射后的DMAMUX寄存器基地址.
u32 dma_requests; // DMAMUX可输出的总通道数.
u32 dmamux_requests; // DMAMUX可接受的总外设请求源数.
spinlock_t lock; // 用于保护寄存器和共享数据访问的自旋锁.
DECLARE_BITMAP(dma_inuse, STM32_DMAMUX_MAX_DMA_REQUESTS); // 位图, 用于追踪哪些DMAMUX输出通道已被占用.
u32 ccr[STM32_DMAMUX_MAX_DMA_REQUESTS]; // 数组, 用于在系统挂起时备份所有通道的配置.
u32 dma_reqs[]; // 柔性数组成员, 用于存储每个DMA主控制器各有多少个通道.
};

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;

/* Clear dma request */
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);
}

/* stm32_dmamux_probe: DMAMUX驱动的探测函数, 在内核找到匹配的设备树节点时调用. */
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;

/* 获取设备树中 "dma-masters" 属性的数量, 即DMAMUX连接了多少个DMA控制器. */
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;
/* 遍历所有dma-masters, 获取每个DMA控制器有多少个通道(request). */
for (i = 1; i <= count; i++) {
dma_node = of_parse_phandle(node, "dma-masters", i - 1);

/* 检查DMA控制器是否是受支持的类型. */
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;
}

/* 从DMA控制器节点读取 "dma-requests" 属性, 得到其通道数. */
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; // 将master数量存在数组第0个元素.

/* 读取本DMAMUX支持多少个外设请求源. */
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); // 增加运行时电源管理的引用计数.

/* 映射DMAMUX的寄存器地址空间. */
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) { // 对DMAMUX进行复位操作.
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);

/* 将所有DMAMUX通道的配置清零, 恢复到默认状态. */
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);

/*
* 关键: 将自己注册为DMA路由器.
* 传入 stm32_dmamux_route_allocate 作为分配路由时的回调函数.
*/
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);
}

/* stm32_dmamux_route_allocate: 当客户端驱动请求DMA通道时, 由DMA框架调用的回调函数. */
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;

/* 检查客户端驱动在设备树dmas属性中提供的参数数量是否正确. */
if (dma_spec->args_count != 3) {
dev_err(&pdev->dev, "invalid number of dma mux args\n");
return ERR_PTR(-EINVAL);
}
/* 检查请求的外设ID是否有效. */
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); // 加锁保护.
/* 查找第一个空闲的DMAMUX输出通道. */
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); // 解锁.

/* 根据分配到的 chan_id, 查找它属于哪个DMA主控制器. */
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: 将其dma_spec->np(节点指针)修改为指向真正的DMA主控制器.
*/
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]; // 保存原始的外设请求ID.

/*
* 重写dma_spec: 修改参数, 使其符合最终DMA控制器驱动的要求.
* 这通常包括DMA流/通道号, 以及DMA配置字等.
*/
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控制器上的本地通道号.
dma_spec->args_count = 4;

/* 编程硬件: 将外设请求ID写入DMAMUX通道配置寄存器, 建立物理连接. */
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片上外设的标准范例。它的原理如下:

  1. 定义匹配规则: 通过of_device_id表声明一个compatible字符串 (“st,stm32h7-dmamux”)。当内核在解析设备树(Device Tree)时, 如果发现一个硬件节点的compatible属性与这个字符串完全匹配, 内核就确信stm32_dmamux_driver是管理该硬件的正确驱动。
  2. 定义核心操作: 它将驱动的核心逻辑函数指针(如probe函数stm32_dmamux_probe)和电源管理回调函数集(stm32_dmamux_pm_ops)打包进一个platform_driver结构体中。probe函数会在匹配成功后被内核调用, 负责初始化DMAMUX硬件; 电源管理函数则负责在系统挂起/恢复或运行时空闲时关闭/打开DMAMUX的时钟以节省功耗。
  3. 注册与初始化: 在内核启动的早期阶段(由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
/*
* 定义一个静态的、常量类型的 dev_pm_ops 结构体.
* 这个结构体包含了此设备驱动的电源管理(Power Management)回调函数.
*/
static const struct dev_pm_ops stm32_dmamux_pm_ops = {
/*
* SET_SYSTEM_SLEEP_PM_OPS 是一个宏, 用于设置系统级的睡眠/唤醒回调.
* 当整个系统进入挂起(suspend)状态时, stm32_dmamux_suspend 会被调用.
* 当系统从挂起状态恢复(resume)时, stm32_dmamux_resume 会被调用.
*/
SET_SYSTEM_SLEEP_PM_OPS(stm32_dmamux_suspend, stm32_dmamux_resume)
/*
* SET_RUNTIME_PM_OPS 是一个宏, 用于设置运行时电源管理回调.
* 即使系统正在运行, 如果内核发现DMAMUX外设在一段时间内处于空闲状态,
* 就会调用 stm32_dmamux_runtime_suspend 来关闭它以省电.
* 当有驱动再次需要使用它时, stm32_dmamux_runtime_resume 会被调用来重新唤醒它.
*/
SET_RUNTIME_PM_OPS(stm32_dmamux_runtime_suspend,
stm32_dmamux_runtime_resume, NULL)
};

/*
* 定义一个 of_device_id 结构体数组, 用于设备树(Open Firmware)的设备匹配.
*/
static const struct of_device_id stm32_dmamux_match[] = {
{
/*
* .compatible: 兼容性字符串.
* 这是驱动与设备绑定的关键. 内核会寻找设备树中 'compatible' 属性与之完全相同的节点.
*/
.compatible = "st,stm32h7-dmamux"
},
{}, /* 一个空的条目, 标志着数组的结束. */
};

/*
* 定义一个静态的 platform_driver 结构体, 这是平台驱动程序的核心.
*/
static struct platform_driver stm32_dmamux_driver = {
/*
* .probe: 一个函数指针, 指向驱动的探测函数 stm32_dmamux_probe (此文件中未提供其实现).
* 当内核找到一个匹配的设备时, 就会调用这个函数来初始化硬件.
*/
.probe = stm32_dmamux_probe,
/*
* .driver: 一个内嵌的 device_driver 结构体, 包含了驱动的通用属性.
*/
.driver = {
/* .name: 驱动的名称, 会出现在/sys/bus/platform/drivers/目录下. */
.name = "stm32-dmamux",
/* .of_match_table: 指向上面定义的OF匹配表. */
.of_match_table = stm32_dmamux_match,
/* .pm: 指向上面定义的电源管理操作函数集. */
.pm = &stm32_dmamux_pm_ops,
},
};

/*
* 驱动的初始化函数.
*/
static int __init stm32_dmamux_init(void)
{
/*
* 调用 platform_driver_register 将我们的驱动注册到内核的平台总线系统中.
* 这是驱动程序能够被内核识别和使用的第一步.
*/
return platform_driver_register(&stm32_dmamux_driver);
}
/*
* arch_initcall 是一个内核初始化宏.
* 它确保 stm32_dmamux_init 函数在内核启动过程中的一个特定早期阶段被调用,
* 保证在任何可能需要DMAMUX的设备驱动被探测之前, DMAMUX驱动本身已经注册.
*/
arch_initcall(stm32_dmamux_init);

/*
* 以下是标准的模块信息宏, 用于提供关于这个内核模块的元数据.
* 这些信息可以通过 "modinfo" 等工具查看.
*/
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
/*
* stm32_dma_read - 从DMA寄存器读取一个32位值
* @dmadev: 指向DMA设备结构的指针
* @reg: 要读取的寄存器相对于基地址的偏移量 (由下面的宏提供)
* @return: 读取到的32位值
*
* 原理: 这是一个封装函数, 它调用`readl_relaxed`来执行内存映射I/O(MMIO)的读取操作.
* `readl_relaxed`用于读取内存映射的外设寄存器, `_relaxed`版本表示编译器不需要设置内存屏障,
* 适用于驱动程序内部已经保证了访问顺序的场合.
*/
static u32 stm32_dma_read(struct stm32_dma_device *dmadev, u32 reg)
{
return readl_relaxed(dmadev->base + reg);
}

/*
* stm32_dma_write - 向DMA寄存器写入一个32位值
* @dmadev: 指向DMA设备结构的指针
* @reg: 要写入的寄存器偏移量
* @val: 要写入的32位值
*
* 原理: 封装`writel_relaxed`, 执行MMIO的写入操作.
*/
static void stm32_dma_write(struct stm32_dma_device *dmadev, u32 reg, u32 val)
{
writel_relaxed(val, dmadev->base + reg);
}

中断状态与清除寄存器定义

这组宏用于定位与特定DMA流(Stream, 在驱动中称为chanchannel)关联的中断标志。

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 /* DMA 低位中断状态寄存器 (用于流 0-3) */
#define STM32_DMA_HISR 0x0004 /* DMA 高位中断状态寄存器 (用于流 4-7) */
/*
* STM32_DMA_ISR(n): 根据流编号 n (0-7) 计算出对应的中断状态寄存器(ISR)地址.
* 原理: `(n) & 4` 是一个巧妙的位技巧. 如果 n 是 4, 5, 6, 7, 那么 `n` 的二进制表示中第2位(值为4)必然是1,
* 表达式为真, 返回 HISR. 否则返回 LISR.
*/
#define STM32_DMA_ISR(n) (((n) & 4) ? STM32_DMA_HISR : STM32_DMA_LISR)
#define STM32_DMA_LIFCR 0x0008 /* DMA 低位中断标志清除寄存器 (用于流 0-3) */
#define STM32_DMA_HIFCR 0x000c /* DMA 高位中断标志清除寄存器 (用于流 4-7) */
/*
* STM32_DMA_IFCR(n): 根据流编号 n (0-7) 计算出对应的中断标志清除寄存器(IFCR)地址. 原理同上.
*/
#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) /* FIFO 错误中断 */
/*
* STM32_DMA_MASKI: 一个掩码, 包含了驱动程序关心的所有中断标志. (注意: 半传输中断HTI通常不用于错误检查)
*/
#define STM32_DMA_MASKI (STM32_DMA_TCI \
| STM32_DMA_TEI \
| STM32_DMA_DMEI \
| STM32_DMA_FEI)
/*
* STM32_DMA_FLAGS_SHIFT(n): 计算流 n 的中断标志在其所属的ISR/IFCR寄存器中的起始比特位偏移量.
* 原理: STM32硬件手册规定, 中断标志在32位寄存器中的布局不是线性的.
* 例如, 流0在位0, 流1在位6, 流2在位16, 流3在位22.
* 这个宏通过对流编号 n (模4之后的值) 进行位操作, 精确地计算出这些非线性的偏移量.
* - `(((_n) & 2) << 3)`: 如果流号是2或3, 产生一个8位的偏移.
* - `(((_n) & 1) * 6)`: 如果流号是1或3, 产生一个6位的偏移.
* - 两者相加得到最终的偏移: n=0 -> 0; n=1 -> 6; n=2 -> 8; n=3 -> 14. (注意: 此处与注释有出入, 宏的实现是最终依据)
*/
#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
/* DMA 流 x 配置寄存器 (DMA_SxCR) */
#define STM32_DMA_SCR(x) (0x0010 + 0x18 * (x)) /* x = 0..7 */
/* 宏原理: 基地址(0x10) + 流编号(x) * 寄存器组步长(0x18). */

/* --- DMA_SxCR 寄存器内的位域定义 --- */
#define STM32_DMA_SCR_REQ_MASK GENMASK(27, 25) /* DMA请求选择掩码 */
#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) /* 外设流控制器 (DMA是流控者) */
#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) /* 流使能 */
/*
* STM32_DMA_SCR_IRQ_MASK: 一个方便的掩码, 用于一次性操作所有中断使能位.
*/
#define STM32_DMA_SCR_IRQ_MASK (STM32_DMA_SCR_TCIE \
| STM32_DMA_SCR_TEIE \
| STM32_DMA_SCR_DMEIE)

/* DMA 流 x 数据项数量寄存器 (DMA_SxNDTR) */
#define STM32_DMA_SNDTR(x) (0x0014 + 0x18 * (x))

/* DMA 流 x 外设地址寄存器 (DMA_SxPAR) */
#define STM32_DMA_SPAR(x) (0x0018 + 0x18 * (x))

/* DMA 流 x 内存地址寄存器0 (DMA_SxM0AR) */
#define STM32_DMA_SM0AR(x) (0x001c + 0x18 * (x))

/* DMA 流 x 内存地址寄存器1 (DMA_SxM1AR, 用于双缓冲模式) */
#define STM32_DMA_SM1AR(x) (0x0020 + 0x18 * (x))

/* DMA 流 x FIFO 控制寄存器 (DMA_SxFCR) */
#define STM32_DMA_SFCR(x) (0x0024 + 0x18 * (x))
#define STM32_DMA_SFCR_FTH_MASK GENMASK(1, 0) /* FIFO 阈值选择掩码 */
#define STM32_DMA_SFCR_FEIE BIT(7) /* FIFO 错误中断使能 */
#define STM32_DMA_SFCR_DMDIS BIT(2) /* 直接模式禁用 (即启用FIFO) */

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
/* DMA 传输方向 */
#define STM32_DMA_DEV_TO_MEM 0x00 /* 外设到内存 */
#define STM32_DMA_MEM_TO_DEV 0x01 /* 内存到外设 */
#define STM32_DMA_MEM_TO_MEM 0x02 /* 内存到内存 */

/* DMA 优先级 */
#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

/* DMA FIFO 阈值选择 */
#define STM32_DMA_FIFO_THRESHOLD_1QUARTERFULL 0x00 /* 1/4 满 */
#define STM32_DMA_FIFO_THRESHOLD_HALFFULL 0x01 /* 1/2 满 */
#define STM32_DMA_FIFO_THRESHOLD_3QUARTERSFULL 0x02 /* 3/4 满 */
#define STM32_DMA_FIFO_THRESHOLD_FULL 0x03 /* 全满 */

/* 硬件限制和特性 */
#define STM32_DMA_MAX_DATA_ITEMS 0xffff /* NDTR 寄存器最大值 (65535) */
/*
* STM32_DMA_ALIGNED_MAX_DATA_ITEMS: 对齐后的最大数据项.
* 原理: 为防止在边界处发生非对齐的 scatter-gather 操作,
* 将最大值向下对齐到FIFO大小(16字节)的倍数.
*/
#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 /* FIFO 深度为16字节 */
#define STM32_DMA_MIN_BURST 4 /* 最小突发大小 */
#define STM32_DMA_MAX_BURST 16 /* 最大突发大小 */

/* 枚举: 数据宽度, 用于将逻辑宽度映射到寄存器 MSIZE/PSIZE 位域的值 */
enum stm32_dma_width {
STM32_DMA_BYTE, // 8位
STM32_DMA_HALF_WORD, // 16位
STM32_DMA_WORD, // 32位
};

/* 枚举: 突发大小, 用于将逻辑突发大小映射到寄存器 MBURST/PBURST 位域的值 */
enum stm32_dma_burst_size {
STM32_DMA_BURST_SINGLE, // 单次传输
STM32_DMA_BURST_INCR4, // 4拍突发
STM32_DMA_BURST_INCR8, // 8拍突发
STM32_DMA_BURST_INCR16, // 16拍突发
};

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;

// 通过 STM32_DMA_ISR 宏计算出此通道对应的中断状态寄存器(ISR)的地址.
dma_isr = stm32_dma_read(dmadev, STM32_DMA_ISR(chan->id));
// 通过 STM32_DMA_FLAGS_SHIFT 宏计算出此通道标志位在 ISR 中的偏移量, 并通过右移将其对齐.
flags = dma_isr >> STM32_DMA_FLAGS_SHIFT(chan->id);

// 与 STM32_DMA_MASKI (一个包含所有可能中断标志的掩码)进行按位与, 过滤掉无关位.
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, 只保留有效的中断标志位.
flags &= STM32_DMA_MASKI;
// 将要清除的标志位左移到它们在 IFCR 寄存器中正确的位置.
dma_ifcr = flags << STM32_DMA_FLAGS_SHIFT(chan->id);

// 通过 STM32_DMA_IFCR 宏计算出正确的 IFCR 寄存器地址, 并写入计算出的值.
stm32_dma_write(dmadev, STM32_DMA_IFCR(chan->id), dma_ifcr);
}

stm32_dma_disable_chan: 安全地禁用一个DMA通道

此函数的核心作用是关闭一个正在运行的DMA通道(流), 并等待硬件确认该通道确实已经停止

  • 原理: 简单地向DMA流控制寄存器(DMA_SxCR)的EN(Enable)位写入’0’可能不会立即生效, 硬件可能需要几个时钟周期来完成当前的总线事务。直接返回可能会导致竞态条件。因此, 此函数实现了一个”写后轮询“的安全序列:
    1. 读取DMA_SxCR寄存器。
    2. 如果EN位已经是’0’, 说明通道已禁用, 直接返回。
    3. 如果EN位是’1’, 则将该位置’0’后写回寄存器。
    4. 关键步骤: 它调用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); // 获取 Stream x Configuration Register (SCR) 地址
dma_scr = stm32_dma_read(dmadev, reg);

// 检查 EN (Enable) 位是否为 1
if (dma_scr & STM32_DMA_SCR_EN) {
dma_scr &= ~STM32_DMA_SCR_EN; // 清除 EN 位
stm32_dma_write(dmadev, reg, dma_scr);

// 轮询 SCR 寄存器, 等待 EN 位变为0, 或者超时.
return readl_relaxed_poll_timeout_atomic(dmadev->base + reg,
dma_scr, !(dma_scr & STM32_DMA_SCR_EN),
10, 1000000);
}

return 0; // 如果本来就是禁用的, 直接返回成功
}

stm32_dma_stop: 完整地停止并清理一个通道

这是一个更高级别的封装, 其核心作用是执行一个完整的、安全的通道停止序列, 包括禁用中断、停止硬件传输、清除悬挂的中断标志以及更新软件状态。

  • 原理: 它按照一个严格的顺序执行清理操作:
    1. 禁用中断: 首先, 它修改DMA_SxCRDMA_SxFCR寄存器, 清除所有中断使能位(如TCIE, TEIE, FEIE等)。这是为了防止在停止过程中产生任何新的中断。
    2. 禁用DMA: 调用stm32_dma_disable_chan来安全地停止硬件传输流。
    3. 清除状态: 调用stm32_dma_irq_status检查是否有在禁用中断之前就已经触发并悬挂的中断标志。如果有, 就调用stm32_dma_irq_clear来清除它们, 确保通道处于一个干净的状态。
    4. 更新软件状态: 最后, 它更新驱动内部的软件标志, 将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;

// 1. 禁用所有中断: 从 Stream x Configuration Register 中清除中断使能位
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);
// 从 Stream x FIFO Control Register 中清除 FIFO 错误中断使能位
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);

// 2. 禁用DMA硬件流, 并等待其停止
ret = stm32_dma_disable_chan(chan);
if (ret < 0)
return;

// 3. 清除任何可能悬挂的中断状态标志
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);
}

// 4. 更新软件状态, 标记通道为空闲
chan->busy = false;
chan->status = DMA_COMPLETE;
}

stm32_dma_alloc_chan_resourcesstm32_dma_free_chan_resources: DMA通道资源的分配与释放

这两个函数是Linux DMA引擎框架中通道生命周期管理的核心回调。它们分别在使用DMA通道之前之后被调用, 负责准备和清理与一个特定DMA通道相关的所有软硬件资源。


stm32_dma_alloc_chan_resources: 分配并准备DMA通道资源

此函数在DMA通道被一个”使用者”驱动(如SPI驱动)首次请求并获得时被调用。它的核心原理是将一个空闲的DMA通道转换到一个准备就绪、可以被安全编程的初始状态

这个准备过程包括两个关键的动作:

  1. 唤醒硬件: 它首先通过pm_runtime_resume_and_get来确保DMA控制器本身是上电并工作的。如果DMA控制器因为长时间未使用而进入了低功耗的”运行时挂起”状态, 这个调用会唤醒它并增加其使用计数, 防止其再次休眠。
  2. 确保硬件空闲: 接着, 它调用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
/*
* stm32_dma_alloc_chan_resources - 为一个 dma_chan 分配资源
* @c: 指向通用 dma_chan 结构体的指针
* @return: 成功时返回0, 失败时返回负错误码.
*/
static int stm32_dma_alloc_chan_resources(struct dma_chan *c)
{
/*
* to_stm32_dma_chan 是一个宏, 用于从通用的 dma_chan 指针获取包含它的、驱动特定的 stm32_dma_chan 结构体的指针.
*/
struct stm32_dma_chan *chan = to_stm32_dma_chan(c);
/*
* stm32_dma_get_dev 是一个宏, 用于从通道结构体获取其所属的DMA设备(dmadev)的指针.
*/
struct stm32_dma_device *dmadev = stm32_dma_get_dev(chan);
int ret;

/*
* 将 config_init 标志置为 false. 这个标志用于表示通道的配置是否已初始化.
* 在分配资源时将其重置, 确保使用者必须重新配置通道.
*/
chan->config_init = false;

/*
* 调用运行时电源管理接口, 增加设备的使用计数, 并在必要时将其从低功耗状态唤醒.
* 这是在访问任何硬件寄存器之前的必要步骤.
*/
ret = pm_runtime_resume_and_get(dmadev->ddev.dev);
if (ret < 0)
return ret;

/*
* 调用 stm32_dma_disable_chan 来确保该通道的硬件单元(DMA流)被禁用.
* 这是一个安全措施, 保证通道处于一个已知的、可编程的停止状态.
*/
ret = stm32_dma_disable_chan(chan);
if (ret < 0)
/*
* 如果禁用通道失败, 必须撤销之前的 pm_runtime_get 操作,
* 即减少设备的使用计数, 以保持计数平衡.
*/
pm_runtime_put(dmadev->ddev.dev);

return ret;
}

stm32_dma_free_chan_resources: 释放DMA通道资源

当使用者驱动不再需要DMA通道并调用dma_release_channel时, 此函数被调用。它的核心原理是安全地终止该通道上任何可能正在进行的活动, 清理所有相关的软硬件状态, 并通知电源管理框架该通道已变为空闲

其清理流程如下:

  1. 终止硬件传输: 它首先检查通道是否处于busy状态。如果是, 它会进入一个由自旋锁保护的临界区, 调用stm32_dma_stop来强制停止硬件传输, 防止在清理过程中发生中断等竞态条件。
  2. 清理软件状态: 它会清理与该通道相关的所有软件状态, 包括释放虚拟通道(vchan)的资源(如待处理的描述符列表)和重置驱动内部的缓存及标志位。
  3. 释放电源锁: 最后, 它调用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
/*
* stm32_dma_free_chan_resources - 释放一个 dma_chan 的资源
* @c: 指向通用 dma_chan 结构体的指针
*/
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) {
/*
* 获取自旋锁并禁用本地中断. 这创建了一个临界区, 防止在停止硬件时
* 被该通道自身的DMA完成中断或其他任务抢占, 从而避免竞态条件.
*/
spin_lock_irqsave(&chan->vchan.lock, flags);
/*
* 调用 stm32_dma_stop 来强制停止硬件传输并清理中断状态.
*/
stm32_dma_stop(chan);
/*
* 清除指向当前活动描述符的指针.
*/
chan->desc = NULL;
/*
* 释放自旋锁并恢复之前的中断状态.
*/
spin_unlock_irqrestore(&chan->vchan.lock, flags);
}

/*
* 减少设备的使用计数. 如果计数降为0, 运行时PM框架可能会在稍后关闭设备电源.
* 这是与 alloc 函数中的 pm_runtime_resume_and_get 相对应的操作.
*/
pm_runtime_put(dmadev->ddev.dev);

/*
* 调用虚拟通道(virt-dma)框架的辅助函数来释放通用资源, 主要是描述符列表.
*/
vchan_free_chan_resources(to_virt_chan(c));
/*
* 清理驱动内部缓存的寄存器值.
*/
stm32_dma_clear_reg(&chan->chan_reg);
/*
* 重置FIFO阈值配置.
*/
chan->threshold = 0;
}

stm32_dma_issue_pendingstm32_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
/*
* stm32_dma_issue_pending - 提交一个待处理的传输
* @c: 指向通用 dma_chan 结构体的指针
*/
static void stm32_dma_issue_pending(struct dma_chan *c)
{
struct stm32_dma_chan *chan = to_stm32_dma_chan(c);
unsigned long flags;

/*
* 获取自旋锁并禁用本地中断. 这是至关重要的, 因为中断处理程序(ISR)在完成一个传输后
* 也可能尝试启动下一个传输. 这个锁确保了 "检查是否空闲" 和 "启动传输" 这两个动作
* 是一个不可分割的原子操作, 防止与ISR发生竞态条件.
*/
spin_lock_irqsave(&chan->vchan.lock, flags);
/*
* 这里的条件判断是核心逻辑:
* 1. vchan_issue_pending(&chan->vchan): 调用 virt-dma 框架的函数.
* 这个函数会检查是否有已提交(prepared)的描述符, 如果有, 它会将其从"待处理"队列
* 移动到"已发出"(issued)队列, 并返回 true.
* 2. !chan->desc && !chan->busy: 这是驱动自身的狀態檢查. 只有在当前通道没有
* 正在处理的描述符 (chan->desc == NULL) 并且通道不忙 (chan->busy == false) 的情况下,
* 我们才能启动一个新的传输.
*/
if (vchan_issue_pending(&chan->vchan) && !chan->desc && !chan->busy) {
dev_dbg(chan2dev(chan), "vchan %p: issued\n", &chan->vchan);
/*
* 如果条件满足, 调用 stm32_dma_start_transfer. 这个内部函数(未显示)
* 将会从 "已发出" 队列中获取描述符, 将其内容(地址, 长度等)编程到
* STM32 DMA 的硬件寄存器中, 并最终设置 EN (使能) 位来启动硬件传输.
*/
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
/*
* stm32_dma_tx_status - 获取一个传输的状态
* @c: 指向通用 dma_chan 结构体的指针
* @cookie: 要查询的传输的唯一ID
* @state: 一个可选的指针, 用于返回详细状态 (如剩余字节数)
* @return: dma_status 枚举值 (DMA_COMPLETE, DMA_IN_PROGRESS, etc.)
*/
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; // 剩余未传输的数据量

/*
* 第一步: 调用通用DMA框架的 dma_cookie_status.
* 这个函数会检查 cookie 是否已经被中断处理程序标记为"已完成".
* 如果是, 就可以直接返回 DMA_COMPLETE, 这是一个快速路径优化.
*/
status = dma_cookie_status(c, cookie, state);
if (status == DMA_COMPLETE)
return status;

/*
* 如果未完成, 则获取通道的当前软件状态 (例如 DMA_IN_PROGRESS 或 DMA_PAUSED).
*/
status = chan->status;

/*
* 如果调用者不关心详细状态 (state == NULL), 直接返回通用状态即可.
*/
if (!state)
return status;

/*
* 获取自旋锁以确保在查询期间, 中断处理程序不会改变正在运行的描述符(chan->desc)
* 或描述符队列(chan->vchan).
*/
spin_lock_irqsave(&chan->vchan.lock, flags);
/*
* 在 virt-dma 的队列中查找与 cookie 对应的描述符.
*/
vdesc = vchan_find_desc(&chan->vchan, cookie);
/*
* 核心逻辑: 计算剩余字节数 (residue)
*/
if (chan->desc && cookie == chan->desc->vdesc.tx.cookie)
// 情况A: 要查询的传输就是当前正在硬件上运行的传输.
// 调用 stm32_dma_desc_residue, 这个函数会读取STM32 DMA的 NDTR
// (Number of Data to Transfer) 硬件寄存器来获取精确的剩余数据量.
residue = stm32_dma_desc_residue(chan, chan->desc,
chan->next_sg);
else if (vdesc)
// 情况B: 传输不在硬件上运行, 但仍在软件队列中等待.
// 这种情况下, 还没有数据被传输, 所以剩余量就是这个描述符的总数据量.
residue = stm32_dma_desc_residue(chan,
to_stm32_dma_desc(vdesc), 0);
/*
* 通过 dma_set_residue 将计算出的剩余量报告给调用者.
*/
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)
{
// ... (定义局部变量)

// --- 1. 从通道配置中加载由 dma_slave_config 设置的参数 ---
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: // --- 2. 处理"内存到外设"方向 ---
/* --- 2a. 配置目标(外设)端 --- */
// 将逻辑总线宽度转换为寄存器位域值
dst_bus_width = stm32_dma_get_width(chan, dst_addr_width);
if (dst_bus_width < 0)
return dst_bus_width;

// 根据缓冲区长度、外设最大突发和FIFO阈值, 计算最优的突发大小
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;

/* --- 2b. 动态优化源(内存)端 --- */
// 根据内存地址对齐和长度, 动态选择最大可能的内存读取宽度
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;

/* --- 2c. 准备寄存器值 --- */
// 使用 FIELD_PREP 安全地构建 Stream Configuration Register (SCR) 的值
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);

// 准备 FIFO Control Register (SFCR) 的值
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);

// 准备 Peripheral Address Register (SPAR) 的值
chan->chan_reg.dma_spar = chan->dma_sconfig.dst_addr;
// 通过指针返回最终决定的总线宽度
*buswidth = dst_addr_width;
break;

case DMA_DEV_TO_MEM: // --- 3. 处理"外设到内存"方向 ---
// 逻辑与上面类似, 只是源和目标的角色互换.
// 外设端参数固定, 内存端参数被动态优化.
// ... (代码省略)
break;

default:
dev_err(chan2dev(chan), "Dma direction is not supported\n");
return -EINVAL;
}

// 4. 根据计算出的突发大小, 进一步配置FIFO模式 (直接模式或FIFO模式)
stm32_dma_set_fifo_config(chan, src_best_burst, dst_best_burst);

/* --- 5. 更新通道的配置缓存 --- */
// 首先清除所有将被修改的位域
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_sgstm32_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;

// ... (根据通道配置设置硬件模式, 如外设流控, 双缓冲等)
/* Set peripheral flow controller */
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;

/* Activate Double Buffer Mode if DMA triggers STM32 MDMA and more than 1 sg */
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;
}

/*
* 核心循环: 遍历 scatterlist 中的每一个内存块 (sg)。
*/
for_each_sg(sgl, sg, sg_len, i) {
// 1. 调用内部辅助函数, 根据传输方向和长度, 计算并缓存总线宽度等参数.
ret = stm32_dma_set_xfer_param(chan, direction, &buswidth,
sg_dma_len(sg),
sg_dma_address(sg));
if (ret < 0)
goto err;

// 2. 将此内存块的长度保存到描述符的 sg_req[i] 中.
desc->sg_req[i].len = sg_dma_len(sg);

// 3. 计算需要传输的数据项数量 (总字节数 / 每个数据项的字节数).
nb_data_items = desc->sg_req[i].len / buswidth;
if (nb_data_items > STM32_DMA_ALIGNED_MAX_DATA_ITEMS) {
goto err; // 检查是否超过硬件限制
}

/*
* 4. *** 绘制蓝图 ***
* 将启动此 sg 块传输所需的所有寄存器值, 预先计算并缓存到
* 描述符的第 i 个条目 (desc->sg_req[i].chan_reg) 中.
*/
stm32_dma_clear_reg(&desc->sg_req[i].chan_reg);
// 复制通用的控制寄存器 SCR 和 SFCR 配置
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;
// 复制外设地址 SPAR (因为对于整个sg传输, 外设地址通常不变)
desc->sg_req[i].chan_reg.dma_spar = chan->chan_reg.dma_spar;
// 设置内存地址 SM0AR 和 SM1AR (用于双缓冲)
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);
// 设置数据传输数量 SNDTR
desc->sg_req[i].chan_reg.dma_sndtr = nb_data_items;
}
desc->cyclic = false; // 标记这不是一个循环传输

/*
* 5. 将准备好的描述符提交给 virt-dma 框架的"已提交"队列.
*/
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) {
// 如果总长度等于周期长度, 说明只有一个周期, 使用硬件的"循环模式".
// DMA硬件在传输完一个周期后会自动将地址和计数器重置.
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;

/*
* 核心循环: 为每个周期(period)创建一个描述符条目.
*/
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; // 标记这是一个循环传输

/*
* 将准备好的描述符提交给 virt-dma 框架.
*/
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负责”记录停车位置和车辆状态”。

这个过程包含两个关键且精妙的步骤:

  1. 捕获剩余工作量:

    • 它首先读取硬件的DMA_SNDTR(Stream Number of Data to Transfer)寄存器。这个寄存器中的值是在硬件停止时精确剩余的、尚未传输的数据项数量。这个值被保存在软件的通道配置缓存(chan->chan_reg.dma_sndtr)中, 这是resume函数能够计算出从哪里继续传输的最关键信息
  2. 处理循环/双缓冲模式的特殊逻辑:

    • 这是此函数最复杂也是最重要的部分。直接暂停一个处于循环(CIRC)或双缓冲(DBM)模式的传输, 然后再恢复, 会有一个问题: DMA硬件的自动重载机制可能会在恢复时使用一个不正确(部分)的数据计数值, 从而破坏后续的循环。
    • 为了解决这个问题, 函数执行了一个巧妙的**”软件保存意图, 硬件简化状态”**的操作:
      • 保存意图: 它先读取SCR(Stream Configuration Register), 并在软件备份(chan->chan_reg.dma_scr)中重新确保CIRCDBM标志是设置的。这可以防止因某些临时状态(如上次恢复后)导致硬件SCR中的这些位被清除, 从而”忘记”了这次传输本应是循环的。
      • 简化状态: 然后, 它在硬件的SCR寄存器中, 故意清除CIRCDBM。这暂时将当前被中断的传输片段变成了一个普通的”一次性”传输。这样, resume函数就可以简单地恢复这个一次性传输, 而不必担心硬件的自动重载机制会出错。当中断处理程序在这次恢复的传输完成后被触发时, 它会负责检查原始意图(从软件备份中读取), 并为下一个完整的周期重新启用CIRCDBM模式。

最后, 它将通道的软件状态正式设置为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
/*
* stm32_dma_handle_chan_paused - 处理一个通道被暂停后的状态
* @chan: 指向被暂停的 stm32_dma_chan 结构体的指针
*
* 此函数在硬件流已被禁用的前提下被调用.
*/
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;

/*
* 1. 读取并保存硬件的当前状态, 以便 resume 时可以更新.
*/
dma_scr = stm32_dma_read(dmadev, STM32_DMA_SCR(chan->id));

/*
* 2a. *** 关键逻辑: 在软件备份中保存传输的"原始意图" ***
* 检查当前传输是否为循环/双缓冲模式.
* 这部分代码是为了处理一种边界情况: 如果一个循环传输在 resume 和下一次
* 中断触发之间被再次 pause, 此时硬件 SCR 中的 CIRC/DBM 位可能已被
* resume 函数临时清除. 为了防止"忘记"这是一个循环传输,
* 我们在这里强制在软件备份 dma_scr 中重新设置这些位.
*/
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;
}
// 将修正后的 SCR 值存入软件配置缓存中.
chan->chan_reg.dma_scr = dma_scr;

/*
* 2b. *** 关键逻辑: 在硬件上简化状态以便 resume ***
* 如果这是一个循环/双缓冲传输, 我们需要暂时禁用硬件的这些模式.
* 否则, 当 resume 时, 硬件的 NDTR 自动重载值会使用当前剩余的(较小的)值,
* 这将导致下一个周期的长度错误.
* 通过暂时禁用这些模式, 我们将 resume 后的传输变成了一个普通的一次性传输.
*/
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);
}

/*
* 3. 捕获剩余工作量: 读取硬件的 NDTR 寄存器,
* 该值表示在硬件停止时, 还有多少数据项没有传输.
* 将其保存在软件配置缓存中, 这是 resume 操作最关键的输入.
*/
chan->chan_reg.dma_sndtr = stm32_dma_read(dmadev, STM32_DMA_SNDTR(chan->id));

/*
* 4. 更新软件状态机, 将通道正式标记为"已暂停".
*/
chan->status = DMA_PAUSED;

dev_dbg(chan2dev(chan), "vchan %p: paused\n", &chan->vchan);
}

stm32_dma_pausestm32_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;

// 1. 状态检查: 只有处于"正在进行"(IN_PROGRESS)状态的传输才能被暂停.
if (chan->status != DMA_IN_PROGRESS)
return -EPERM; // 返回"操作不允许"错误

// 2. 进入临界区: 获取自旋锁并禁用中断, 防止与中断处理程序发生竞态条件.
spin_lock_irqsave(&chan->vchan.lock, flags);

/*
* 3. 停止硬件: 调用 stm32_dma_disable_chan 来安全地禁用硬件流并等待其停止.
* 这个函数内部会轮询硬件的EN位, 确保传输已完全停止.
* 至关重要的是, STM32 DMA硬件在被禁用时, 会自动保持其内部状态,
* 特别是 NDTR (剩余数据计数器) 会停留在被禁用时的值.
*/
ret = stm32_dma_disable_chan(chan);
if (!ret)
/*
* 4. 更新软件状态: 如果硬件成功停止, 调用 stm32_dma_handle_chan_paused.
* 这个内部函数(未显示)会将通道的软件状态设置为 DMA_PAUSED,
* 并可能会将硬件 NDTR 寄存器的值读出并保存在 chan->chan_reg.dma_sndtr 中,
* 以便 resume 时使用.
*/
stm32_dma_handle_chan_paused(chan);

// 5. 退出临界区
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)
{
// ... (获取各种指针和变量)

// 1. 状态检查: 只有处于"已暂停"(PAUSED)状态的传输才能被恢复.
if (chan->status != DMA_PAUSED)
return -EPERM;

// ... (读取并检查硬件的EN位, 确保它确实是禁用的)

// 2. 进入临界区
spin_lock_irqsave(&chan->vchan.lock, flags);

/*
* 3. *** 状态重构的核心 ***
* 这部分逻辑用于计算出传输中断了多少数据, 并相应地调整地址指针.
*/

// 3a. 找到被暂停的那个 scatter-gather (sg) 块的原始配置.
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];

// 3b. 计算已传输的数据量.
// ndtr: sg块开始时的总数据项.
// chan_reg.dma_sndtr: 暂停时硬件寄存器中剩余的数据项.
// offset: (开始时 - 剩余的) = 已传输的数据项.
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);

// 3c. 获取原始的地址指针.
spar = sg_req->chan_reg.dma_spar;
sm0ar = sg_req->chan_reg.dma_sm0ar;
sm1ar = sg_req->chan_reg.dma_sm1ar;

// 3d. 根据地址是否配置为自增, 更新地址寄存器.
// 如果配置了自增, 新的地址 = 原始地址 + 已传输的字节数.
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; // 如果内存不自增, 偏移量强制为0.

// ... (处理复杂的双缓冲模式(DBM)下的地址更新)

/*
* 4. 恢复数据计数器. 写入的是暂停时硬件中剩余的数据量.
*/
stm32_dma_write(dmadev, STM32_DMA_SNDTR(id), chan_reg.dma_sndtr);

/*
* 5. 临时禁用循环/双缓冲模式.
* 这是一个关键的技巧. 为了只完成当前被中断的这一个sg块, 需要暂时关闭
* 硬件的自动重载功能. 驱动会在这次传输完成后, 在中断处理程序中手动
* 重新配置并启动下一个循环/sg块.
*/
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);

// ... (如果之前是双缓冲, 预配置下一个sg块)

/*
* 6. *** 重启硬件 ***
* 更新软件状态为"正在进行", 然后将包含EN位的新配置写入硬件SCR寄存器.
*/
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);

// 7. 退出临界区
spin_unlock_irqrestore(&chan->vchan.lock, flags);

dev_dbg(chan2dev(chan), "vchan %p: resumed\n", &chan->vchan);

return 0;
}

stm32_dma_terminate_allstm32_dma_synchronize: 传输的终止与同步

这两个函数是DMA引擎框架中负责流程控制的两个关键回调。terminate_all提供了一种强制、立即中止所有传输的机制, 主要用于错误恢复或驱动卸载。而synchronize则提供了一种阻塞式等待机制, 确保在CPU继续执行之前, 所有已启动的DMA传输都已完成。


stm32_dma_terminate_all: 强制中止所有传输

此函数的核心原理是执行一个**”焦土式”的清理操作**。它会立即停止硬件, 并清理掉该通道上所有正在进行和排队等待的传输任务, 包括软件描述符和硬件状态。

这个过程必须是原子的, 以防止在清理过程中有新的中断或任务提交发生, 因此它在自旋锁的保护下执行关键步骤:

  1. 停止当前传输: 如果有一个传输正在硬件上运行(chan->desc有效), 它会首先在软件层面将其标记为完成(即使是被中止的), 并调用vchan_terminate_vdesc来通知使用者驱动该任务已终止。然后, 它调用stm32_dma_stop来强制停止硬件。
  2. 清空队列: 它调用vchan_get_all_descriptorsvirt-dma框架中该通道的所有队列(submittedissued)中的描述符全部移动到一个临时的本地链表中。
  3. 释放资源: 在释放自旋锁之后, 它调用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
/*
* stm32_dma_terminate_all - 终止一个通道上的所有传输
* @c: 指向通用 dma_chan 结构体的指针
* @return: 总是返回 0 (成功).
*/
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);

/*
* 获取自旋锁并禁用本地中断. 这是至关重要的, 以确保在清理所有队列和硬件状态时,
* 不会与中断处理程序(ISR)发生竞态条件.
*/
spin_lock_irqsave(&chan->vchan.lock, flags);

/*
* 检查当前是否有一个正在硬件上运行的描述符.
*/
if (chan->desc) {
/*
* 软件层面: 立即将当前运行的描述符在 cookie 系统中标记为完成.
* 这可以防止任何等待此 cookie 的代码永远阻塞.
*/
dma_cookie_complete(&chan->desc->vdesc.tx);
/*
* 软件层面: 通知 virt-dma 框架此描述符已被终止.
* 这可能会触发回调, 通知使用者驱动传输失败.
*/
vchan_terminate_vdesc(&chan->desc->vdesc);
/*
* 硬件层面: 如果通道正忙, 调用 stm32_dma_stop 来强制停止硬件传输.
*/
if (chan->busy)
stm32_dma_stop(chan);
/*
* 清除指向当前描述符的指针.
*/
chan->desc = NULL;
}

/*
* 软件层面: 调用 virt-dma 的辅助函数, 将此通道的所有队列(已提交, 已发出)中
* 剩余的全部描述符, 都移动到临时的 head 链表中.
*/
vchan_get_all_descriptors(&chan->vchan, &head);
/*
* 释放自旋锁, 恢复中断.
*/
spin_unlock_irqrestore(&chan->vchan.lock, flags);
/*
* 软件层面: 在锁之外, 调用 virt-dma 的辅助函数来遍历 head 链表,
* 并释放其中所有描述符占用的内存.
*/
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
/*
* stm32_dma_synchronize - 同步一个DMA通道
* @c: 指向通用 dma_chan 结构体的指针
*
* 此函数会阻塞, 直到所有已提交到此通道的传输都完成为止.
*/
static void stm32_dma_synchronize(struct dma_chan *c)
{
struct stm32_dma_chan *chan = to_stm32_dma_chan(c);

/*
* 完全委托给 virt-dma 框架的通用同步函数.
* vchan_synchronize 内部会等待, 直到 chan->vchan 的 desc_issued 链表变为空.
* 当DMA中断处理程序完成一个传输并将其从 desc_issued 链表移除时,
* 这个等待条件才有可能被满足.
*/
vchan_synchronize(&chan->vchan);
}

‘stm32_dma_prep_dma_memcpy’: 准备内存到内存的DMA传输

此函数是STM32 DMA驱动程序中负责内存到内存 (’memcpy’) 类型传输的回调函数。 它的核心原理是将一个用户请求的大块内存复制作, 分解(segmentation) 成一个或多个符合STM32 DMA硬件单次传输能力上限的小块传输任务, 并为每一个小块任务精心准备一套完整的硬件寄存器配置, 最终将这些配置打包成一个描述符(descriptor), 提交给’virt-dma’框架排队等待执行。

这个函数是实现高效内存复制的关键, 它通过以下步骤将一个抽象的’memcpy’请求转化为具体的硬件指令集:

  1. 分片计算: 由于STM32 DMA的’NDTR’(数据传输数量)寄存器有其最大值(’STM32_DMA_ALIGNED_MAX_DATA_ITEMS’), 一个大的内存复制请求必须被拆分成多个DMA传输。 函数首先通过’DIV_ROUND_UP’计算出总共需要多少个这样的小块传输, 这决定了需要分配多大的描述符。

  2. 描述符分配: 它动态地分配一个’stm32_dma_desc’结构体, 该结构体尾部包含一个足够容纳所有小块传输配置(’sg_req’)的弹性数组。

  3. 循环准备: 函数进入一个循环, 为每一个小块传输(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’): 使能传输完成和传输错误中断。
  4. 提交给框架: 当所有小块的“蓝图”都绘制完成后, 整个描述符通过’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;

/* 1. 计算需要将传输分数成多少个 sg 块 ( 段) */
num_sgs = DIV_ROUND_UP(len, STM32_DMA_ALIGNED_MAX_DATA_ITEMS);
/*
* 2. 分配描述符内存, struct_size 用于为尾部的弹性数组 sg_req 分配足够空间
*/
desc = kzalloc(struct_size(desc, sg_req, num_sgs), GFP_NOWAIT);
if (!desc)
return NULL;
desc->num_sgs = num_sgs;

threshold = chan->threshold;

/* 3. 循环为每个 sg 块准备硬件配置 */
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 設為1字節, 讓 get_best_burst 根據其他參數進行優化.
*/
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;
}

/*
* 4. *** 繪製藍圖 ***
* 將啟動此 sg 塊所需的所有寄存器值, 預先緩存到描述符中.
*/
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;
/* 配置FIFO控制寄存器, 包括FIFO閾值 */
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);
/* 外設地址寄存器(SPAR)被用作源地址 */
desc->sg_req[i].chan_reg.dma_spar = src + offset;
/* 內存地址寄存器(SM0AR)被用作目標地址 */
desc->sg_req[i].chan_reg.dma_sm0ar = dest + offset;
/* 數據傳輸數量寄存器(SNDTR)設置為當前塊的長度 */
desc->sg_req[i].chan_reg.dma_sndtr = xfer_count;
/* 在描述符的軟件部分也保存長度信息 */
desc->sg_req[i].len = xfer_count;
}
desc->cyclic = false; // 標記為非循環傳輸

/*
* 5. 將準備好的描述符提交給 virt-dma 框架進行最終的封裝和排隊.
*/
return vchan_tx_prep(&chan->vchan, &desc->vdesc, flags);
}

stm32_dma_chan_irq: DMA通道硬件中断的主处理程序 (顶层)

此函数是硬件中断的直接入口点。当一个STM32 DMA通道完成传输、遇到错误或达到半程点时, 硬件会触发一个中断, 内核的中断子系统最终会调用这个函数来响应该事件。它是中断处理的”上半部”(Top Half), 运行在**硬中断上下文(hardirq context)**中。

其核心原理是快速响应、分类处理、最小化延迟。它必须在尽可能短的时间内完成, 以便让CPU可以尽快响应其他可能更重要的中断。

  1. 获取状态快照: 它首先获取保护通道状态的自旋锁, 然后立即读取所有相关的硬件状态寄存器(中断状态、流控制、FIFO控制)。这确保了它处理的是触发中断那一刻的精确硬件状态。
  2. 错误优先处理: 它会优先检查并处理各种错误标志(如FIFO错误FEI、直接模式错误DMEI)。对于每个错误, 它会清除硬件中的中断标志位, 并检查该错误中断是否被使能。这是一种严谨的做法, 确保驱动只对它明确要求关注的事件做出反应, 并将错误信息记录到内核日志中。
  3. 成功路径处理: 接下来, 它处理最重要的成功事件——传输完成(TCI)。如果传输完成中断发生并且被使能, 且通道不处于软件暂停状态, 它不会在此函数中执行所有后续逻辑, 而是将控制权转交给stm32_dma_handle_chan_done函数, 由该函数执行更复杂的”下半部”逻辑。
  4. 其他事件处理: 它会检查并清除其他类型的中断标志(如半传输完成HTI), 即使当前驱动逻辑没有为它们附加特殊操作, 清除标志也是必需的, 以防止中断风暴。
  5. 兜底与清理: 最后, 它会检查是否有任何未识别的中断标志被置位, 如果有, 则报告一个通用错误。完成所有操作后, 它释放自旋锁, 并返回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
/*
* stm32_dma_chan_irq: DMA通道的中断处理函数 (IRQ Handler)
* @irq: 触发的中断号 (未使用)
* @devid: 传递给中断注册的"cookie", 这里是 stm32_dma_chan 结构体指针
* @return: IRQ_HANDLED 表示中断已被成功处理
*/
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);

// 读取硬件状态: 中断状态, 流控制寄存器(SCR), FIFO控制寄存器(SFCR)
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));

// 检查并处理 FIFO 错误 (FEI)
if (status & STM32_DMA_FEI) {
stm32_dma_irq_clear(chan, STM32_DMA_FEI); // 清除硬件中的中断标志
status &= ~STM32_DMA_FEI; // 从我们的软件状态副本中清除该位
if (sfcr & STM32_DMA_SFCR_FEIE) { // 检查FIFO错误中断是否被使能
if (!(scr & STM32_DMA_SCR_EN) && !(status & STM32_DMA_TCI))
// 如果通道已被硬件禁用且不是传输完成导致, 这是一个严重错误
dev_err(chan2dev(chan), "FIFO Error\n");
else
// 否则, 可能只是正常的FIFO上溢/下溢, 作为调试信息打印
dev_dbg(chan2dev(chan), "FIFO over/underrun\n");
}
}
// 检查并处理直接模式错误 (DMEI)
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");
}

// 检查并处理传输完成中断 (TCI) - 这是最常见的成功路径
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;
}

// 检查并处理半传输完成中断 (HTI)
if (status & STM32_DMA_HTI) {
stm32_dma_irq_clear(chan, STM32_DMA_HTI);
status &= ~STM32_DMA_HTI; // 驱动当前没有为半传输实现回调, 但仍需清除标志
}

// 兜底检查: 如果在处理完所有已知中断后, status 仍不为0
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工作流, 并精确地管理描述符和硬件状态:

  1. 循环模式 (cyclic):
    • 通知客户端: 它立即调用vchan_cyclic_callback。这个函数通常会调度一个tasklet, 由该tasklet在软中断上下文中去执行客户端驱动提供的回调函数。这遵循了将耗时工作移出硬中断上下文的最佳实践。
    • 硬件管理: 它接着检查硬件是否处于自动循环模式(CIRCDBM)。
      • 如果不是, 意味着驱动正在”手动”模拟循环传输。在这种情况下, 硬件在完成一轮后已经停止, 必须调用stm32_dma_post_resume_reconfigure完全重新编程DMA寄存器并手动重启下一次传输。
      • 如果, 硬件会自动处理循环。驱动只需为硬件的双缓冲模式准备好下一个数据段(stm32_dma_configure_next_sg)即可。
  2. 单次/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
/*
* stm32_dma_handle_chan_done: 处理通道完成事件
* @chan: 发生事件的DMA通道
* @scr: 从硬件读取的流控制寄存器(SCR)的快照
*/
static void stm32_dma_handle_chan_done(struct stm32_dma_chan *chan, u32 scr)
{
if (!chan->desc)
return; // 如果没有活动的描述符, 直接返回

// 判断当前传输是否是循环模式
if (chan->desc->cyclic) {
// 调用虚拟DMA通道的循环回调函数, 这通常会调度tasklet来通知客户端
vchan_cyclic_callback(&chan->desc->vdesc);
if (chan->trig_mdma) // 如果此DMA用于触发MDMA, 则特殊处理, 直接返回
return;
stm32_dma_sg_inc(chan); // 增加scatter-gather段的索引, 为下一轮做准备

// 核心逻辑: 判断是硬件自动循环还是软件模拟循环
if (!(scr & (STM32_DMA_SCR_CIRC | STM32_DMA_SCR_DBM)))
// 如果硬件的循环(CIRC)和双缓冲(DBM)模式都未开启, 说明是软件模拟,
// 硬件已停止, 需要手动重新配置并启动下一轮.
stm32_dma_post_resume_reconfigure(chan);
else if (scr & STM32_DMA_SCR_DBM && chan->desc->num_sgs > 2)
// 如果是硬件双缓冲模式, 且有超过2个段, 则需要为硬件配置下一个段的信息
stm32_dma_configure_next_sg(chan);
} else { // 单次或Scatter-Gather模式
chan->busy = false; // 标记通道当前段传输完成
chan->status = DMA_COMPLETE;
// 检查当前完成的段是否是整个传输请求的最后一个段
if (chan->next_sg == chan->desc->num_sgs) {
// 如果是, 整个DMA "cookie" 完成, 通知客户端并清空描述符
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通道的所有关键寄存器恢复到一次传输的初始状态, 然后重新使能通道。这模拟了硬件循环模式的行为, 但完全由软件控制。

  1. 清理和定位: 它首先清除任何可能残留的中断状态, 然后根据当前scatter-gather索引, 精确地定位到本轮传输应该使用的那个数据段描述符(sg_req)。
  2. 寄存器恢复: 它从sg_req中读取预先保存好的初始值, 并依次写回到DMA硬件寄存器中, 包括:
    • SNDTR: 传输数据项的数量。
    • SPAR: 外设地址。
    • SM0AR/SM1AR: 内存地址(在双缓冲模式下有两个)。
  3. 模式恢复: 它检查原始配置, 如果需要, 重新在流控制寄存器(SCR)中设置CIRC(循环)或DBM(双缓冲)标志。
  4. 重启: 最后, 它在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
/*
* stm32_dma_post_resume_reconfigure: 在暂停/恢复后重新配置通道
* @chan: 需要被重新配置的DMA通道
*/
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);

// 定位到当前应该使用的scatter-gather段
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)寄存器
stm32_dma_write(dmadev, STM32_DMA_SNDTR(chan->id), sg_req->chan_reg.dma_sndtr);

// 恢复外设地址(SPAR)寄存器
stm32_dma_write(dmadev, STM32_DMA_SPAR(id), sg_req->chan_reg.dma_spar);

// 恢复内存地址(SM0AR/SM1AR)寄存器
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);

// 恢复循环(CIRC)或双缓冲(DBM)模式
if (chan->chan_reg.dma_scr & STM32_DMA_SCR_DBM) {
dma_scr |= STM32_DMA_SCR_DBM;
// 恢复CT位(当前目标内存), DBM模式下CT位会自动翻转, 这里要恢复到初始状态
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);

// 为双缓冲模式配置下一个SG段(如果需要)
stm32_dma_configure_next_sg(chan);

// 打印寄存器值, 用于调试
stm32_dma_dump_reg(chan);

// 最终, 在流控制寄存器(SCR)中置位EN, 重新启动DMA传输
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控制器驱动之间的桥梁, 其核心原理如下:

  1. 解析硬件描述: 函数的输入dma_spec包含了从客户端设备树的dmas属性中解析出的原始数据。例如, SPI驱动的设备树节点中可能会有一行dmas = <&dma1 7 0x420 0x800>;dma_spec->args就包含了{7, 0x420, 0x800, ...}这些原始的整数。
  2. 翻译为有意义的配置: 函数做的第一件事就是将这些匿名的数字翻译成一个有意义的、驱动内部使用的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。
  3. 验证与资源定位: 函数会进行严格的有效性检查, 确保设备树中提供的值在硬件支持的范围内。然后, 它使用channel_id作为索引, 直接定位到DMA控制器驱动内部代表该硬件流的stm32_dma_chan结构。
  4. 通道申请与锁定: 最关键的一步是调用dma_get_slave_channel。这是一个向通用DMA引擎框架发出的请求, 意图**”申请并独占”**这个物理DMA通道。如果该通道已经被其他驱动占用, 此调用将失败, 从而正确地管理了共享硬件资源的访问。
  5. 应用静态配置: 一旦成功获得通道的独占使用权, 它就会调用stm32_dma_set_config, 将从设备树中解析出的静态配置(特别是DMAMUX的请求线)应用到硬件或软件状态中。这完成了通道的预配置, 使其准备好为该特定外设服务。
  6. 返回通用句柄: 最后, 它返回一个标准的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
/*
* stm32_dma_of_xlate: OF(设备树)DMA请求的"翻译"回调函数.
* @dma_spec: 指向 of_phandle_args 结构体的指针, 包含了从客户端设备树的
* "dmas" 属性中解析出的 phandle 参数.
* @ofdma: 指向 of_dma 结构体的指针, of_dma_data 成员包含了DMA控制器设备的数据.
* @return: 成功时返回一个有效的 dma_chan 指针, 失败时返回 NULL.
*/
static struct dma_chan *stm32_dma_of_xlate(struct of_phandle_args *dma_spec,
struct of_dma *ofdma)
{
/*
* 获取DMA控制器设备的主数据结构.
*/
struct stm32_dma_device *dmadev = ofdma->of_dma_data;
struct device *dev = dmadev->ddev.dev;
/*
* 定义一个 stm32_dma_cfg 结构体, 用于存放从 dma_spec 翻译过来的配置信息.
*/
struct stm32_dma_cfg cfg;
/*
* 定义指向驱动内部通道结构 stm32_dma_chan 的指针.
*/
struct stm32_dma_chan *chan;
/*
* 定义指向通用DMA通道结构 dma_chan 的指针, 这是最终要返回的句柄.
*/
struct dma_chan *c;

/*
* STM32的DMA请求需要至少4个参数. 这是一个有效性检查.
*/
if (dma_spec->args_count < 4) {
dev_err(dev, "Bad number of cells\n");
return NULL;
}

/*
* "翻译"过程: 将设备树中的原始整数映射到有意义的配置字段.
*/
cfg.channel_id = dma_spec->args[0]; // 参数0: DMA流/通道号
cfg.request_line = dma_spec->args[1]; // 参数1: DMAMUX请求线
cfg.stream_config = dma_spec->args[2]; // 参数2: 流的静态配置(方向, 优先级等)
cfg.features = dma_spec->args[3]; // 参数3: 特殊功能标志

/*
* 再次进行有效性检查, 确保通道号和请求线ID在硬件支持的范围内.
*/
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;
}

/*
* 使用翻译出的 channel_id, 直接索引到DMA设备内部的通道数组, 找到对应的硬件通道.
*/
chan = &dmadev->chan[cfg.channel_id];

/*
* 调用 dma_get_slave_channel, 这是向DMA引擎框架申请独占使用该通道的标准方法.
* 如果通道已被占用, 此函数将返回NULL.
*/
c = dma_get_slave_channel(&chan->vchan.chan);
if (!c) {
dev_err(dev, "No more channels available\n");
return NULL;
}

/*
* 成功申请到通道后, 将从设备树解析出的静态配置应用到该通道.
* 这通常会配置DMAMUX等硬件.
*/
stm32_dma_set_config(chan, &cfg);

/*
* 返回一个标准的、通用的 dma_chan 句柄给发出请求的客户端驱动.
*/
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通道了。

整个初始化过程可以分为以下几个关键阶段:

  1. 资源获取与硬件复位:

    • 函数首先为驱动的核心数据结构(stm32_dma_device)分配内存。
    • 它从设备树中获取DMA控制器的寄存器基地址, 并通过ioremap将其映射到内核可访问的地址空间。
    • 它获取并使能DMA控制器所需的时钟。
    • 它获取复位控制器句柄, 并对DMA硬件执行一次”断言-延时-解除断言”的复位序列, 确保硬件处于一个已知的初始状态。
  2. 向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的特定实现, 这正是硬件抽象的核心。
    • 它还定义了支持的数据宽度、传输方向、对齐要求等硬件特性。
  3. 初始化DMA通道:

    • DMA控制器有多个通道, 函数会遍历所有通道, 为每一个通道初始化其软件表示(stm32_dma_chan)。
    • 它会初始化vchan(虚拟通道)系统, 这是DMA引擎用于管理多个客户端对物理通道请求的机制。
    • 为了提高效率, 它会预先计算好每个通道的中断清除和状态标志寄存器的地址和位掩码。
  4. 注册与中断设置:

    • 通过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));

/* Check if user is requesting DMA to trigger STM32 MDMA */
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; // DMA通道的软件描述
struct stm32_dma_device *dmadev; // 整个DMA设备的软件描述
struct dma_device *dd; // 指向 dmadev 中内嵌的通用 dma_device 结构
struct resource *res; // 用于接收内存资源信息
struct reset_control *rst; // 复位控制器句柄
int i, ret; // 循环变量和返回值

// 分配并清零 dmadev 结构体的内存, devm_* 版本确保资源自动释放
dmadev = devm_kzalloc(&pdev->dev, sizeof(*dmadev), GFP_KERNEL);
if (!dmadev)
return -ENOMEM;

// 获取内嵌的 dma_device 结构的指针, 方便后续操作
dd = &dmadev->ddev;

// 从设备树获取索引为0的内存资源(寄存器地址), 并进行 ioremap 映射
dmadev->base = devm_platform_get_and_ioremap_resource(pdev, 0, &res);
if (IS_ERR(dmadev->base))
return PTR_ERR(dmadev->base);

// 获取DMA控制器的时钟
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;
}

// 从设备树读取 "st,mem2mem" 属性, 判断此DMA是否支持内存到内存的传输
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传输的最大段大小
dma_set_max_seg_size(&pdev->dev, STM32_DMA_ALIGNED_MAX_DATA_ITEMS);

// --- 填充 dma_device 结构, 向内核描述硬件能力和操作方法 ---
dma_cap_set(DMA_SLAVE, dd->cap_mask); // 支持外设到内存/内存到外设
dma_cap_set(DMA_PRIVATE, dd->cap_mask); // 支持私有通道分配
dma_cap_set(DMA_CYCLIC, dd->cap_mask); // 支持循环模式 (如音频)
/*
* 作用: 当一个客户端驱动请求一个DMA通道时, DMA引擎核心将调用 stm32_dma_alloc_chan_resources.
*/
dd->device_alloc_chan_resources = stm32_dma_alloc_chan_resources;
/*
* 作用: 当客户端驱动释放一个DMA通道时, DMA引擎核心将调用 stm32_dma_free_chan_resources.
*/
dd->device_free_chan_resources = stm32_dma_free_chan_resources;
/*
* 作用: 当客户端驱动查询当前DMA传输的状态(还剩多少数据)时, DMA引擎核心将调用 stm32_dma_tx_status.
*/
dd->device_tx_status = stm32_dma_tx_status;
/*
* 作用: 当客户端驱动准备好传输后, DMA引擎核心调用 stm32_dma_issue_pending 来启动队列中下一个待处理的传输.
*/
dd->device_issue_pending = stm32_dma_issue_pending;
/*
* 作用: 当客户端驱动需要准备一个 scatter-gather (多缓冲区)的"从设备"传输(如SPI->内存)时, DMA引擎核心调用 stm32_dma_prep_slave_sg.
*/
dd->device_prep_slave_sg = stm32_dma_prep_slave_sg;
/*
* 作用: 当客户端驱动需要准备一个循环模式的传输(如I2S音频)时, DMA引擎核心调用 stm32_dma_prep_dma_cyclic.
*/
dd->device_prep_dma_cyclic = stm32_dma_prep_dma_cyclic;
/*
* 作用: 当客户端驱动需要配置DMA通道的特定参数(如外设地址、传输方向)时, DMA引擎核心调用 stm32_dma_slave_config.
*/
dd->device_config = stm32_dma_slave_config;
/*
* 作用: 当需要暂停当前正在进行的DMA传输时, 调用 stm32_dma_pause.
*/
dd->device_pause = stm32_dma_pause;
/*
* 作用: 当需要从暂停状态恢复DMA传输时, 调用 stm32_dma_resume.
*/
dd->device_resume = stm32_dma_resume;
/*
* 作用: 当需要强制中止并清理一个通道上所有传输时, 调用 stm32_dma_terminate_all.
*/
dd->device_terminate_all = stm32_dma_terminate_all;
/*
* 作用: 当需要确保一个通道上所有已提交的操作都已完成时(CPU与DMA同步), 调用 stm32_dma_synchronize.
*/
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);
}

// 循环初始化每个DMA通道的软件结构
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);
}

// 向内核的DMA引擎子系统注册此DMA设备
ret = dma_async_device_register(dd);
if (ret)
goto clk_free;

// 为每个DMA通道获取并注册中断处理函数
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;

// 请求中断, 将 stm32_dma_chan_irq 注册为中断服务程序
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;
}
}

// 将此DMA控制器注册为设备树的DMA提供者
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;
}

// 将dmadev保存到平台设备的私有数据中, 方便以后获取
platform_set_drvdata(pdev, dmadev);

// 初始化并使能运行时电源管理 (Runtime PM)
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设备
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控制器硬件。

其工作原理如下:

  1. 定义电源管理操作: 首先, 它定义了一个dev_pm_ops结构体, stm32_dma_pm_ops。这个结构体包含了一系列函数指针, 用于响应内核的电源管理事件。通过使用SET_SYSTEM_SLEEP_PM_OPSSET_RUNTIME_PM_OPS宏, 它将驱动内部的挂起/恢复函数(如stm32_dma_pm_suspend)与标准的系统睡眠和运行时电源管理(Runtime PM)钩子关联起来。这使得DMA控制器可以在系统进入低功耗状态或自身空闲时被安全地关闭, 并在需要时被唤醒。
  2. 定义平台驱动主体: 接着, 它定义了platform_driver的核心结构体stm32_dma_driver。这个结构体是驱动的”身份证”, 它告诉内核:
    • 名称(name): 驱动的名字是 “stm32-dma”。
    • 匹配方式(of_match_table): 驱动通过一个名为stm32_dma_of_match的表来与设备树中的节点进行匹配。当内核在设备树中找到一个节点的compatible属性与此表中的条目匹配时, 就认为找到了一个该驱动可以管理的设备。
    • 电源管理(pm): 将上一步定义的电源管理操作挂接到驱动上。
    • 探测函数(probe): 当匹配成功时, 内核应该调用的核心初始化函数是stm32_dma_probe。这个probe函数(未在此代码段中显示)才是真正负责初始化DMA硬件、分配通道等工作的函数。
  3. 注册驱动: 最后, stm32_dma_init函数作为驱动的初始化入口, 调用platform_driver_registerstm32_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", },
{ /* sentinel */ },
};
MODULE_DEVICE_TABLE(of, stm32_dma_of_match);

/*
* 定义一个静态的、常量类型的 dev_pm_ops 结构体, 用于处理电源管理事件.
* dev_pm_ops 是 "device power management operations" 的缩写.
*/
static const struct dev_pm_ops stm32_dma_pm_ops = {
/*
* SET_SYSTEM_SLEEP_PM_OPS 是一个宏, 用于快速设置系统级别的睡眠和唤醒回调函数.
* 当整个系统进入睡眠状态 (如 suspend-to-RAM) 时, 内核会调用 stm32_dma_pm_suspend.
* 当系统从睡眠中唤醒时, 内核会调用 stm32_dma_pm_resume.
*/
SET_SYSTEM_SLEEP_PM_OPS(stm32_dma_pm_suspend, stm32_dma_pm_resume)
/*
* SET_RUNTIME_PM_OPS 是一个宏, 用于设置运行时电源管理 (Runtime PM) 的回调函数.
* 当DMA设备在运行时因为空闲而可以被关闭以节省功耗时, 内核会调用 stm32_dma_runtime_suspend.
* 当需要再次使用DMA设备时, 内核会调用 stm32_dma_runtime_resume 将其唤醒.
* 第三个参数是 runtime_idle 回调, 这里为 NULL, 表示没有特殊操作.
*/
SET_RUNTIME_PM_OPS(stm32_dma_runtime_suspend,
stm32_dma_runtime_resume, NULL)
};

/*
* 定义一个静态的 platform_driver 结构体, 这是平台驱动程序的核心.
*/
static struct platform_driver stm32_dma_driver = {
/*
* .driver: 这是一个内嵌的 device_driver 结构体.
*/
.driver = {
/*
* .name: 驱动程序的名称. 这个名称会显示在sysfs中, 并可用于非设备树平台的匹配.
*/
.name = "stm32-dma",
/*
* .of_match_table: 指向一个 "OF match table" (OF=Open Firmware, 即设备树).
* 这个表包含了一系列 compatible 字符串, 内核会用它来匹配设备树中的设备节点.
* 这是现代嵌入式Linux中将驱动与设备关联起来的主要方式.
*/
.of_match_table = stm32_dma_of_match,
/*
* .pm: 将上面定义的电源管理操作结构体 stm32_dma_pm_ops 关联到这个驱动上.
*/
.pm = &stm32_dma_pm_ops,
},
/*
* .probe: 一个函数指针, 指向驱动的探测函数 stm32_dma_probe.
* 当内核发现一个与本驱动匹配的设备时, 就会调用这个函数来初始化设备.
* 这是驱动中最重要的函数之一.
*/
.probe = stm32_dma_probe,
};

/*
* 驱动的初始化函数. __init 宏告诉编译器将此函数放入特殊的 ".init.text" 段.
* 在内核启动完成后, 这段内存可以被释放, 以节省RAM.
*/
static int __init stm32_dma_init(void)
{
/*
* 调用 platform_driver_register 函数, 将 stm32_dma_driver 注册到内核的平台总线核心中.
* 注册成功后, 内核就会开始为这个驱动寻找匹配的设备.
* 函数返回注册操作的结果 (成功为0, 失败为负错误码).
*/
return platform_driver_register(&stm32_dma_driver);
}
/*
* subsys_initcall 是一个宏, 它将 stm32_dma_init 函数的地址放入一个特殊的函数指针列表.
* 内核在启动过程中, 会按照预定的顺序 (core_initcall, subsys_initcall, device_initcall等)
* 依次调用这些列表中的函数.
* subsys_initcall 确保了这个DMA驱动在核心子系统都初始化完毕后, 能够较早地被注册.
*/
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接口将数据传输进来。
  • 流程:
    1. DMA工作: 普通DMA负责处理SPI的外设请求, 将一小块数据(例如1KB)从SPI数据寄存器搬运到内存中的一个Ping-Pong缓冲区A。
    2. 触发MDMA: 当DMA完成对缓冲区A的填充后, 它不产生CPU中断, 而是自动触发MDMA的一个通道
    3. MDMA接力: MDMA被触发后, 开始将缓冲区A中的1KB数据搬运到外部SDRAM的一个巨大缓冲区中进行存储, 或者对这1KB数据进行格式转换后存入另一个处理缓冲区。
    4. 并行处理: 在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的潜力发挥到极致, 设计出真正高性能、低功耗的嵌入式系统。