[toc]

在这里插入图片描述

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

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
static inline int dmaengine_pause(struct dma_chan *chan)
{
if (chan->device->device_pause)
return chan->device->device_pause(chan);

return -ENOSYS;
}

static inline int dmaengine_resume(struct dma_chan *chan)
{
if (chan->device->device_resume)
return chan->device->device_resume(chan);

return -ENOSYS;
}
static inline enum dma_status dmaengine_tx_status(struct dma_chan *chan,
dma_cookie_t cookie, struct dma_tx_state *state)
{
return chan->device->device_tx_status(chan, cookie, state);
}



static inline struct dma_async_tx_descriptor *dmaengine_prep_dma_cyclic(
struct dma_chan *chan, dma_addr_t buf_addr, size_t buf_len,
size_t period_len, enum dma_transfer_direction dir,
unsigned long flags)
{
if (!chan || !chan->device || !chan->device->device_prep_dma_cyclic)
return NULL;

return chan->device->device_prep_dma_cyclic(chan, buf_addr, buf_len,
period_len, dir, flags);
}


/**
* dmaengine_terminate_async() - Terminate all active DMA transfers
* @chan: The channel for which to terminate the transfers
*
* Calling this function will terminate all active and pending descriptors
* that have previously been submitted to the channel. It is not guaranteed
* though that the transfer for the active descriptor has stopped when the
* function returns. Furthermore it is possible the complete callback of a
* submitted transfer is still running when this function returns.
*
* dmaengine_synchronize() needs to be called before it is safe to free
* any memory that is accessed by previously submitted descriptors or before
* freeing any resources accessed from within the completion callback of any
* previously submitted descriptors.
*
* This function can be called from atomic context as well as from within a
* complete callback of a descriptor submitted on the same channel.
*
* If none of the two conditions above apply consider using
* dmaengine_terminate_sync() instead.
*/
static inline int dmaengine_terminate_async(struct dma_chan *chan)
{
if (chan->device->device_terminate_all)
return chan->device->device_terminate_all(chan);

return -EINVAL;
}

/**
* dmaengine_synchronize() - Synchronize DMA channel termination
* @chan: The channel to synchronize
*
* Synchronizes to the DMA channel termination to the current context. When this
* function returns it is guaranteed that all transfers for previously issued
* descriptors have stopped and it is safe to free the memory associated
* with them. Furthermore it is guaranteed that all complete callback functions
* for a previously submitted descriptor have finished running and it is safe to
* free resources accessed from within the complete callbacks.
*
* The behavior of this function is undefined if dma_async_issue_pending() has
* been called between dmaengine_terminate_async() and this function.
*
* This function must only be called from non-atomic context and must not be
* called from within a complete callback of a descriptor submitted on the same
* channel.
*/
static inline void dmaengine_synchronize(struct dma_chan *chan)
{
might_sleep();

if (chan->device->device_synchronize)
chan->device->device_synchronize(chan);
}

/**
* dmaengine_terminate_sync() - Terminate all active DMA transfers
* @chan: The channel for which to terminate the transfers
*
* Calling this function will terminate all active and pending transfers
* that have previously been submitted to the channel. It is similar to
* dmaengine_terminate_async() but guarantees that the DMA transfer has actually
* stopped and that all complete callbacks have finished running when the
* function returns.
*
* This function must only be called from non-atomic context and must not be
* called from within a complete callback of a descriptor submitted on the same
* channel.
*/
static inline int dmaengine_terminate_sync(struct dma_chan *chan)
{
int ret;

ret = dmaengine_terminate_async(chan);
if (ret)
return ret;

dmaengine_synchronize(chan);

return 0;
}
/**
* dma_async_issue_pending - flush pending transactions to HW
* @chan: target DMA channel
*
* This allows drivers to push copies to HW in batches,
* reducing MMIO writes where possible.
*/
static inline void dma_async_issue_pending(struct dma_chan *chan)
{
chan->device->device_issue_pending(chan);
}

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

dma_request_chan: 统一的DMA从通道请求接口

本代码片段展示了Linux内核中一个关键且通用的API——dma_request_chan。这是设备驱动程序(“客户端”)用来向DMA子系统请求一个专用的、用于外设数据传输的DMA通道(“从通道”)的标准入口点。其核心功能是提供一个统一的接口,它能自动通过现代的固件描述(如设备树)或旧的过滤机制来查找并分配一个合适的DMA通道,同时优雅地处理驱动间的加载顺序依赖问题(即“探测延迟”,Probe Deferral)。

实现原理分析

该函数是连接设备驱动与具体DMA控制器驱动的核心枢纽。它将复杂的DMA通道匹配和分配过程抽象化,其实现策略是“固件优先,过滤为辅”。

  1. 统一的固件优先方法:

    • 函数首先获取设备的固件节点(fwnode),这是一个通用的句柄,可以是设备树(Device Tree)节点或ACPI节点。
    • 设备树路径: 如果是设备树系统(is_of_node),它会调用of_dma_request_slave_channel。这个函数会解析设备驱动对应的设备树节点中的dmasdma-names属性。例如,一个UART的设备树节点可能会有dmas = <&mdma_channel_x ...>dma-names = "rx"dma_request_chan(dev, "rx")就会精确地请求到mdma_channel_x这个物理通道。这是最现代、最精确的匹配方式。
    • ACPI路径: 如果是ACPI系统,则调用acpi_dma_request_slave_chan_by_name,执行类似的功能。
  2. 优雅的依赖处理 (EPROBE_DEFER):

    • if (PTR_ERR(chan) == -EPROBE_DEFER): 这是一个至关重要的机制。当of_dma_request_slave_channel返回-EPROBE_DEFER时,意味着设备树中虽然描述了DMA通道,但对应的DMA控制器驱动尚未初始化完成dma_request_chan会立即将这个错误码返回给调用者(如stm32_usart_serial_probe)。调用者看到这个错误后,会中止自己的初始化并同样返回-EPROBE_DEFER。这会通知内核驱动核心:“我的依赖(DMA控制器)还没准备好,请稍后**重新探测(re-probe)**我”。这完美地解决了驱动加载顺序的问题。
  3. 旧式过滤回退机制:

    • 如果基于固件的查找失败(但不是因为-EPROBE_DEFER),代码会进入一个回退路径。
    • mutex_lock(&dma_list_mutex): 锁定一个全局列表。
    • list_for_each_entry_safe(d, _d, &dma_device_list, global_node): 它会遍历系统中所有已注册的DMA控制器设备。
    • dma_filter_match: 对于每一个DMA控制器,它会使用一个filter函数和name来判断这个控制器是否能为当前设备dev服务。这是一种比较旧的、基于平台数据或硬件特定逻辑的匹配方法。
    • 如果回退机制也找不到任何匹配的通道,它会返回-EPROBE_DEFER,寄希望于一个合适的DMA控制器可能在未来才被注册。
  4. 通道分配后的处理:

    • 一旦成功获取到一个dma_chan,函数会执行一系列“收尾”工作:
    • chan->slave = dev;: 建立从DMA通道到客户端设备的反向链接,正式确立主从关系。
    • sysfs_create_link: 在sysfs中创建符号链接。这极大地增强了系统的可观测性。例如,它会在DMA通道的sysfs目录下创建一个名为slave的符号链接,指向使用它的设备(如ttySTM0);同时,在设备的目录下也会创建一个指向DMA通道的链接。这使得系统管理员可以轻松地从命令行查看到设备与DMA通道的绑定关系。

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

硬件交互

  1. 设备树是关键: 在STM32H750平台上,dma_request_chan的执行完全依赖于设备树(DTS)的正确配置。当stm32_usart_serial_probe调用dma_request_chan(&pdev->dev, "rx")时:
    • dev就是usart1platform_device
    • of_dma_request_slave_channel函数会被调用。
    • 它会查找usart1节点下的dmas = <&dma1_stream0 ...>dma-names = "rx", ...属性。
    • 它发现”rx”对应的是dma1_stream0。于是,它会向dma1(STM32的DMA控制器之一)的驱动请求分配第0个流(stream)。
    • dma1的驱动会返回一个代表DMA1_Stream0这个物理硬件的struct dma_chan实例。
  2. 硬件连接的软件体现: 这个返回的dma_chan不仅仅是一个数据结构,它内部包含了操作DMA1_Stream0所有相关寄存器(如配置寄存器CR、源地址PAR、目标地址M0AR等)所需的信息和函数指针。dma_request_chan的成功调用,标志着STM32 USART的硬件DMA请求信号(USART_DR寄存器有数据)与STM32 DMA控制器的物理通道之间的软件链路已经建立。

单核环境影响

  • DMA的主要目的是将数据搬运工作从CPU卸载,这对于只有一个核心的STM32H750来说,可以极大地提升性能,使其在DMA传输数据的同时能够执行其他计算任务。
  • 函数中的mutex_lock(&dma_list_mutex)在单核系统上用于防止在遍历全局DMA设备列表时被其他任务抢占,从而保证了列表的完整性。

无MMU影响

  • dma_request_chan函数本身不处理内存地址,所以与MMU无关。
  • 然而,它所属的DMA引擎框架是MMU/IOMMU感知的。在无MMU的系统上,后续的DMA操作(如dma_map_single或使用dma_alloc_coherent返回的地址)会直接使用物理地址。DMA引擎框架的抽象使得设备驱动代码(如STM32 USART驱动)几乎无需关心底层是物理地址还是DMA虚拟地址,从而具有很好的可移植性。

代码分析

1
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
static const struct dma_slave_map *dma_filter_match(struct dma_device *device,
const char *name,
struct device *dev)
{
int i;

if (!device->filter.mapcnt)
return NULL;

for (i = 0; i < device->filter.mapcnt; i++) {
const struct dma_slave_map *map = &device->filter.map[i];

if (!strcmp(map->devname, dev_name(dev)) &&
!strcmp(map->slave, name))
return map;
}

return NULL;
}

/**
* @brief 尝试分配一个独占的DMA从通道。
* @param dev 指向客户端设备的指针。
* @param name 从通道的名称(如 "rx" 或 "tx")。
* @return struct dma_chan* 成功则返回指向DMA通道的指针,失败则返回错误指针。
*/
struct dma_chan *dma_request_chan(struct device *dev, const char *name)
{
struct fwnode_handle *fwnode = dev_fwnode(dev);
struct dma_device *d, *_d;
struct dma_chan *chan = NULL;

/* 首先尝试通过固件节点(Device Tree或ACPI)来获取通道。 */
if (is_of_node(fwnode))
chan = of_dma_request_slave_channel(to_of_node(fwnode), name);
else if (is_acpi_device_node(fwnode))
chan = acpi_dma_request_slave_chan_by_name(dev, name);

/* 如果返回EPROBE_DEFER,表示依赖的DMA控制器未就绪,直接返回此错误。 */
if (PTR_ERR(chan) == -EPROBE_DEFER)
return chan;

/* 如果成功找到通道,则直接跳转到found标签。 */
if (!IS_ERR_OR_NULL(chan))
goto found;

/* 回退路径:通过DMA过滤映射来查找通道。 */
mutex_lock(&dma_list_mutex);
/* 遍历全局的DMA设备列表。 */
list_for_each_entry_safe(d, _d, &dma_device_list, global_node) {
dma_cap_mask_t mask;
const struct dma_slave_map *map = dma_filter_match(d, name, dev);

if (!map)
continue;

dma_cap_zero(mask);
dma_cap_set(DMA_SLAVE, mask);

/* 在此DMA控制器上查找一个候选通道。 */
chan = find_candidate(d, &mask, d->filter.fn, map->param);
if (!IS_ERR(chan))
break; /* 找到即跳出循环。 */
}
mutex_unlock(&dma_list_mutex);

if (IS_ERR(chan))
return chan;
/* 如果回退路径也找不到,则返回EPROBE_DEFER,寄希望于DMA控制器稍后注册。 */
if (!chan)
return ERR_PTR(-EPROBE_DEFER);

found:
#ifdef CONFIG_DEBUG_FS
/* 为调试目的,分配一个包含客户端设备名的字符串。 */
chan->dbg_client_name = kasprintf(GFP_KERNEL, "%s:%s", dev_name(dev), name);
#endif

/* 分配一个包含DMA和通道名的字符串。 */
chan->name = kasprintf(GFP_KERNEL, "dma:%s", name);
if (!chan->name)
return chan;
/* 建立从DMA通道到客户端设备的反向链接。 */
chan->slave = dev;

/* 在sysfs中创建从通道到客户端的符号链接。 */
if (sysfs_create_link(&chan->dev->device.kobj, &dev->kobj,
DMA_SLAVE_NAME))
dev_warn(dev, "无法创建DMA %s符号链接\n", DMA_SLAVE_NAME);
/* 在sysfs中创建从客户端到通道的符号链接。 */
if (sysfs_create_link(&dev->kobj, &chan->dev->device.kobj, chan->name))
dev_warn(dev, "无法创建DMA %s符号链接\n", chan->name);

return chan;
}
EXPORT_SYMBOL_GPL(dma_request_chan);

find_candidate: DMA私有通道的搜索与获取

本代码片段展示了dma_request_chan内部使用的一个核心辅助函数——find_candidate。其核心功能是根据指定的匹配条件(能力掩码和过滤函数),在一个DMA控制器设备上查找一个合适的、可用的私有通道,并立即尝试获取(acquire)它。这是一个“查找并锁定”的原子性操作,它通过管理私有通道计数和状态,确保了找到的通道被独占式地分配给请求者,并且它在找不到可用通道时,能够正确地返回-EPROBE_DEFER以支持驱动探测延迟机制。

实现原理分析

此函数是DMA引擎为“私有”或“专用”通道(即不通过通用池分配,而是由特定驱动直接请求的通道)设计的分配逻辑。它将“寻找”和“获取”两个步骤紧密结合,以处理并发和资源可用性问题。

  1. 第一步:寻找候选者 (private_candidate):

    • 函数的第一步是调用private_candidate(代码未显示)。这个内部函数负责实际的搜索工作。
    • 它会遍历device(一个DMA控制器)上的所有物理通道。
    • 对于每个通道,它会使用mask(例如,必须支持DMA_SLAVE能力)和fn(一个驱动提供的、硬件相关的过滤函数)来进行匹配。fn是关键,它允许进行精确的匹配,例如,fn可能会检查一个DMA通道是否连接到了指定的UART TX请求线上。
    • 如果private_candidate找到一个满足所有条件的空闲通道,它会返回该通道的指针;否则返回NULL
  2. 第二步:尝试获取与状态管理:

    • 如果private_candidate成功找到了一个候选通道(if (chan)),函数会立即尝试正式地获取它。
    • dma_cap_set(DMA_PRIVATE, device->cap_mask): 在DMA控制器设备上设置DMA_PRIVATE标志。这是一个状态标记,告知DMA引擎,该控制器至少有一个通道正在被私有(独占)使用。如注释所言,这会改变某些内部管理逻辑,如引用计数的平衡。
    • device->privatecnt++: 递增该控制器的私有通道使用计数器。
    • err = dma_chan_get(chan): 这是关键的获取操作。它会去真正地“锁定”这个通道,通常是增加通道自身的引用计数,使其不能再被分配给其他请求者。
    • 获取失败处理:
      • 如果dma_chan_get失败,说明在找到候选者和尝试获取它的极短时间窗口内,通道的状态发生了变化(例如,被另一个任务或中断获取)。
      • 代码会执行对称的清理操作:递减privatecnt计数器,并在计数器归零时清除DMA_PRIVATE标志。
      • 一个特殊情况是err == -ENODEV,这表示底层硬件设备或其模块已经被移除。这是一个罕见的竞态条件,函数的响应是果断地将整个DMA控制器设备从全局列表中移除。
  3. 返回值的意义:

    • 函数的返回值设计得非常精妙,它能向调用者传达三种不同的状态:
      1. 成功: private_candidate找到通道,且dma_chan_get成功。函数返回一个有效的struct dma_chan *指针。
      2. 需要延迟探测: private_candidate没有找到任何满足条件的空闲通道。函数返回ERR_PTR(-EPROBE_DEFER)。这告诉调用者(如dma_request_chan),现在没有可用的资源,但未来可能会有(例如,当另一个驱动释放了它占用的通道时),所以应该推迟当前驱动的初始化,稍后再试。
      3. 其他错误: private_candidate找到了通道,但dma_chan_get失败。函数返回具体的错误码(如-EBUSY)。

代码分析

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
/**
* @brief 在一个DMA控制器上查找并尝试获取一个满足条件的候选通道。
* @param device 要搜索的DMA控制器设备。
* @param mask 描述所需能力的掩码(如DMA_SLAVE)。
* @param fn 用于精确匹配的过滤函数。
* @param fn_param 传递给过滤函数的参数。
* @return struct dma_chan* 成功则返回获取的通道,否则返回错误指针。
*/
static struct dma_chan *find_candidate(struct dma_device *device,
const dma_cap_mask_t *mask,
dma_filter_fn fn, void *fn_param)
{
/* 调用内部函数,根据掩码和过滤函数搜索一个空闲的候选通道。 */
struct dma_chan *chan = private_candidate(mask, device, fn, fn_param);
int err;

if (chan) {
/*
* 找到了一个合适的通道,尝试获取、准备并返回它。
* 首先,在控制器设备上设置DMA_PRIVATE标志,
* 以表明该通道不会被发布到通用分配器中。
*/
dma_cap_set(DMA_PRIVATE, device->cap_mask);
device->privatecnt++; /* 增加私有通道计数。 */
err = dma_chan_get(chan); /* 尝试正式获取该通道的所有权。 */

if (err) {
/* 获取失败的处理逻辑。 */
if (err == -ENODEV) {
/* 如果设备在此期间被移除,则从全局列表中删除该DMA控制器。 */
dev_dbg(device->dev, "%s: %s 模块已被移除\n",
__func__, dma_chan_name(chan));
list_del_rcu(&device->global_node);
} else
dev_dbg(device->dev,
"%s: 获取 %s 失败: (%d)\n",
__func__, dma_chan_name(chan), err);

/* 对之前的操作进行回滚:递减私有计数,并在需要时清除标志。 */
if (--device->privatecnt == 0)
dma_cap_clear(DMA_PRIVATE, device->cap_mask);

/* 将chan转换为错误指针以返回。 */
chan = ERR_PTR(err);
}
}

/*
* 如果chan有效(表示查找和获取都成功),则返回通道指针。
* 否则(如果最初就没找到候选通道),返回-EPROBE_DEFER错误指针。
*/
return chan ? chan : ERR_PTR(-EPROBE_DEFER);
}