[toc]

在这里插入图片描述

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
218

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;
/* 将每个通道注册搭配DMA设备的链表上 */
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);

好的,我将对您提供的这段关于Linux内核中STM32 DMA控制器驱动底层传输启动代码的进行分析。

stm32_dma_start_transfer: STM32 DMA硬件传输的启动引擎

本代码片段展示了STM32 DMA控制器驱动的核心引擎——stm32_dma_start_transfer函数。这是在DMA传输被提交(submit)触发(issue)后,驱动内部真正与硬件交互、启动一次DMA数据搬运的函数。其核心功能是:从virt_dma_chan的待处理队列中获取一个传输描述符,解析出其中预先计算好的寄存器值,然后将这些值批量写入到STM32 DMA控制器的物理寄存器中,最后通过置位EN(Enable)位来点燃引擎,启动传输。

实现原理分析

此机制是通用dmaengine框架与具体STM32 DMA硬件之间的“最后一公里”。它将dmaengine抽象的、与硬件无关的传输请求(virt_dma_desc),转化为对硬件寄存器的精确、有序的写操作。

  1. 获取传输任务:

    • vchan_next_desc(&chan->vchan): vchan(virtual channel)是dmaengine提供的一个通用队列。此函数从队列中取出下一个待处理的传输描述符(vdesc)。
    • chan->desc = to_stm32_dma_desc(vdesc);: 将通用的vdesc转换为STM32 DMA驱动特定的stm32_dma_desc。这个特定结构中包含了之前在prep阶段(如dmaengine_prep_dma_cyclic)就已经预先计算好的所有寄存器值。
  2. 安全启动序列:

    • stm32_dma_disable_chan(chan): 这是至关重要的第一步。在向DMA寄存器写入新配置之前,必须确保该DMA流(stream/channel)处于禁用状态。此函数会轮询等待SCR寄存器的EN位变为0。
  3. 寄存器批量写入:

    • sg_req = &chan->desc->sg_req[chan->next_sg];: 获取当前散列表(scatter-gather list)段对应的寄存器配置。对于简单的非SG传输,只有一个段。
    • reg = &sg_req->chan_reg;: 获取预计算好的寄存器值结构体。
    • stm32_dma_write(dmadev, STM32_DMA_SCR(chan->id), reg->dma_scr);: 这是核心的硬件操作。函数将预先计算好的值,逐一写入到DMA流的各个物理寄存器中:
      • SCR (Stream Configuration Register): 配置传输方向、数据宽度、优先级、中断使能等。
      • SPAR (Stream Peripheral Address Register): 设置外设端的地址(如USARTx_RDR)。
      • SM0AR (Stream Memory 0 Address Register): 设置内存端的地址(DMA缓冲区的地址)。
      • SFCR (Stream FIFO Control Register): 配置DMA内部的FIFO模式。
      • SM1AR (Stream Memory 1 Address Register): 用于双缓冲模式下的第二个内存地址。
      • SNDTR (Stream Number of Data to Transfer Register): 设置本次传输的数据量。
    • 这种先在内存中准备好所有值,然后一次性批量写入硬件的设计,非常高效且清晰。
  4. 循环与双缓冲 (stm32_dma_configure_next_sg):

    • STM32的DMA控制器支持双缓冲模式(Double Buffer Mode),这是实现无缝循环传输的基础。
    • DMA流有两个内存地址寄存器:SM0ARSM1ARSCR寄存器中有一个CT(Current Target)位,用于指示当前DMA正在使用哪个内存地址。
    • stm32_dma_configure_next_sg的作用是预先配置下一个缓冲区。当DMA正在向SM0AR对应的缓冲区传输数据时(此时CT=0),这个函数会被调用,将下一个SG段的内存地址写入到SM1AR中。当DMA完成当前传输后,硬件会自动将CT位翻转为1,并无缝地开始向SM1AR的地址传输,同时产生一次中断。中断服务程序会再次调用此函数,将再下一个段的地址写入SM0AR,如此循环往复。
    • if (chan->desc->cyclic): 只有在循环模式下,才会调用此函数来预配置下一个缓冲区。
  5. 启动传输:

    • reg->dma_scr |= STM32_DMA_SCR_EN;: 在软件中将使能位置位。
    • stm32_dma_write(dmadev, STM32_DMA_SCR(chan->id), reg->dma_scr);: 最后一步,将包含EN=1SCR值写入硬件,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
/**
* @brief 启动一个STM32 DMA通道的传输。
* @param chan STM32 DMA通道的私有数据。
*/
static void stm32_dma_start_transfer(struct stm32_dma_chan *chan)
{
/* ... 变量声明 ... */

/* 步骤1:确保DMA通道当前是禁用的。这是一个同步点。 */
ret = stm32_dma_disable_chan(chan);
if (ret < 0)
return;

/* 如果当前没有描述符,则从vchan队列中获取下一个。 */
if (!chan->desc) {
vdesc = vchan_next_desc(&chan->vchan);
if (!vdesc)
return; /* 没有待处理的传输。 */

list_del(&vdesc->node);

chan->desc = to_stm32_dma_desc(vdesc);
chan->next_sg = 0; /* 从第一个SG段开始。 */
}

/* ... 处理循环传输中的SG段索引回绕 ... */

/* 获取当前SG段预计算好的寄存器值。 */
sg_req = &chan->desc->sg_req[chan->next_sg];
reg = &sg_req->chan_reg;

/* ... 特殊情况处理:当DMA触发MDMA时,禁用TCIE ... */
if (chan->trig_mdma && chan->dma_sconfig.direction != DMA_MEM_TO_DEV)
reg->dma_scr &= ~STM32_DMA_SCR_TCIE;

/* 步骤2:将预计算好的值批量写入DMA流的硬件寄存器。 */
/* 此时EN位必须为0。 */
reg->dma_scr &= ~STM32_DMA_SCR_EN;
stm32_dma_write(dmadev, STM32_DMA_SCR(chan->id), reg->dma_scr); /* 配置寄存器 */
stm32_dma_write(dmadev, STM32_DMA_SPAR(chan->id), reg->dma_spar); /* 外设地址 */
stm32_dma_write(dmadev, STM32_DMA_SM0AR(chan->id), reg->dma_sm0ar); /* 内存地址0 */
stm32_dma_write(dmadev, STM32_DMA_SFCR(chan->id), reg->dma_sfcr); /* FIFO控制 */
stm32_dma_write(dmadev, STM32_DMA_SM1AR(chan->id), reg->dma_sm1ar); /* 内存地址1 */
stm32_dma_write(dmadev, STM32_DMA_SNDTR(chan->id), reg->dma_sndtr); /* 传输长度 */

/* 将SG索引指向下一个段。 */
stm32_dma_sg_inc(chan);

/* ... 清理可能残留的中断状态标志 ... */
status = stm32_dma_irq_status(chan);
if (status)
stm32_dma_irq_clear(chan, status);

/* 如果是循环传输,则预先配置下一个SG段的地址到备用寄存器。 */
if (chan->desc->cyclic)
stm32_dma_configure_next_sg(chan);

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

/* 步骤3:启动DMA。 */
chan->busy = true;
chan->status = DMA_IN_PROGRESS;
reg->dma_scr |= STM32_DMA_SCR_EN; /* 在软件中将EN位置位。 */
/* 将包含EN=1的SCR值写入硬件,DMA传输立即开始。 */
stm32_dma_write(dmadev, STM32_DMA_SCR(chan->id), reg->dma_scr);

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


/**
* @brief 为双缓冲/循环模式配置下一个SG段的内存地址。
* @param chan STM32 DMA通道。
*/
static void stm32_dma_configure_next_sg(struct stm32_dma_chan *chan)
{
/* ... */
/* 读取SCR寄存器,以检查CT(Current Target)位。 */
dma_scr = stm32_dma_read(dmadev, STM32_DMA_SCR(id));

sg_req = &chan->desc->sg_req[chan->next_sg];

/*
* CT位指示当前硬件正在使用哪个内存地址寄存器。
* 我们需要将下一个段的地址写入到“备用”的那个寄存器中。
*/
if (dma_scr & STM32_DMA_SCR_CT) {
/* 当前目标是M1,所以我们配置M0。 */
dma_sm0ar = sg_req->chan_reg.dma_sm0ar;
stm32_dma_write(dmadev, STM32_DMA_SM0AR(id), dma_sm0ar);
/* ... */
} else {
/* 当前目标是M0,所以我们配置M1。 */
dma_sm1ar = sg_req->chan_reg.dma_sm1ar;
stm32_dma_write(dmadev, STM32_DMA_SM1AR(id), dma_sm1ar);
/* ... */
}
}

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的潜力发挥到极致, 设计出真正高性能、低功耗的嵌入式系统。


stm32_mdma_init: 在子系统初始化阶段注册 STM32 MDMA platform_driver

此函数是一个极其明确的、用于在 Linux 内核启动过程中注册 STM32 MDMA 平台驱动的初始化入口函数。它不涉及任何 DMA 硬件寄存器访问、不创建通道、不配置 DMAengine,而是将 MDMA 驱动挂接到 platform bus,使其具备参与设备匹配与 probe 流程的资格。

其核心原理是:在内核进入 subsys 初始化阶段时,将 stm32_mdma_driver 注册到 platform 总线;当设备树中出现匹配的 MDMA 节点时,由内核驱动模型自动触发 stm32_mdma_probe,完成真正的设备级初始化。

  1. 驱动注册:通过 platform_driver_register,将 stm32_mdma_driver 插入 platform bus 的驱动链表。
  2. 初始化时序控制:通过 subsys_initcall,确保该注册发生在内核子系统初始化阶段,而不是模块加载阶段。
  3. 职责分离:该函数仅负责“驱动级初始化”,所有硬件与 DMAengine 相关逻辑都延迟到 .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
static const struct of_device_id stm32_mdma_of_match[] = {
{ .compatible = "st,stm32h7-mdma", },
{ /* sentinel */ },
};
MODULE_DEVICE_TABLE(of, stm32_mdma_of_match);

static const struct dev_pm_ops stm32_mdma_pm_ops = {
SET_SYSTEM_SLEEP_PM_OPS(stm32_mdma_pm_suspend, stm32_mdma_pm_resume)
SET_RUNTIME_PM_OPS(stm32_mdma_runtime_suspend,
stm32_mdma_runtime_resume, NULL)
};

static struct platform_driver stm32_mdma_driver = {
.probe = stm32_mdma_probe,
.driver = {
.name = "stm32-mdma",
.of_match_table = stm32_mdma_of_match,
.pm = &stm32_mdma_pm_ops,
},
};
/*
* stm32_mdma_init: 注册 STM32 MDMA platform_driver
*
* 该函数在内核 subsys 初始化阶段被调用,
* 用于将 STM32 MDMA 驱动注册到 platform 总线。
*/
static int __init stm32_mdma_init(void)
{
/*
* 将 stm32_mdma_driver 注册到 platform bus。
*
* 注册成功后:
* - 若系统中已存在匹配的 platform_device,
* 内核将立即调用其 .probe 回调;
* - 若设备稍后才出现,则在匹配成功时调用 .probe。
*/
return platform_driver_register(&stm32_mdma_driver);
}

/*
* 指定 stm32_mdma_init 在 subsys 初始化阶段执行。
*
* subsys_initcall 早于 module_init,
* 用于保证驱动在相关子系统可用后尽早完成注册。
*/
subsys_initcall(stm32_mdma_init);

stm32_mdma_probe: 解析设备树资源并完成 STM32 MDMA 控制器的设备级初始化

此函数是STM32 MDMA 驱动真正的设备级初始化入口。它由 platform 总线在设备树匹配成功后调用,用于把一个具体的 MDMA 控制器实例(寄存器、中断、时钟、复位等资源)初始化为 Linux DMAengine 可用的 DMA 控制器。

其核心原理是:把设备树描述的硬件资源转换为内核中可管理的对象,并把该控制器注册到 DMAengine 与 OF-DMA 框架。整个过程包含四类关键动作:

  1. 设备树参数读取与约束建立:读取 dma-channelsdma-requestsst,ahb-addr-masks 等属性,构成后续合法性边界与资源规模。
  2. 资源获取与使能:映射寄存器资源、获取并使能时钟、执行可选复位序列,确保 MDMA 处于可访问且可控状态。
  3. DMAengine 设备建模:设置 dma_device 的能力掩码与回调函数集合,并初始化每个通道的 virt_dma_chan
  4. 注册与对外发布:申请中断、注册到 DMAengine 核心、注册 OF-DMA 翻译器,使设备树客户端能通过 phandle 申请 MDMA 通道。

在 STM32H750(单核、无 MMU)视角下,需要强调的点是:

  • 单核不会改变该函数的语义;该函数本身是典型“启动阶段一次性初始化”。
  • 无 MMU 对此类驱动模型代码没有直接影响(这里的 __iomemdevm_* 等属于内核抽象层)。
  • 真正与“传输正确性/性能”相关的部分主要来自:能力声明、回调绑定、通道初始化方式,以及后续具体 prep/issue 路径。
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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
/*
* stm32_mdma_probe: 初始化一个 STM32 MDMA 控制器实例并注册到 DMAengine
* @pdev: platform 设备实例,承载设备树节点与资源
*
* 该函数完成:
* - 读取设备树属性(通道数、请求线数、地址掩码等)
* - 映射寄存器资源
* - 获取并使能时钟、执行可选复位
* - 初始化 dma_device 能力与回调
* - 初始化每个虚拟通道 vchan
* - 申请 IRQ
* - 注册 DMAengine 与 OF-DMA 控制器
*/
static int stm32_mdma_probe(struct platform_device *pdev)
{
struct stm32_mdma_chan *chan;
struct stm32_mdma_device *dmadev;
struct dma_device *dd;
struct device_node *of_node;
struct reset_control *rst;
u32 nr_channels, nr_requests;
int i, count, ret;

/* 获取设备树节点;若无设备树描述,则不支持该设备实例 */
of_node = pdev->dev.of_node;
if (!of_node)
return -ENODEV;

/*
* 读取 MDMA 通道数量。
* 若未提供该属性,则退回到驱动支持的最大通道数,并给出告警。
* 该值用于确定后续通道初始化循环次数与资源规模。
*/
ret = device_property_read_u32(&pdev->dev, "dma-channels", &nr_channels);
if (ret) {
nr_channels = STM32_MDMA_MAX_CHANNELS;
dev_warn(&pdev->dev, "MDMA defaulting on %i channels\n", nr_channels);
}

/*
* 读取 MDMA 请求线数量。
* 请求线用于配置触发源选择与合法性校验;缺省时退回到最大值。
*/
ret = device_property_read_u32(&pdev->dev, "dma-requests", &nr_requests);
if (ret) {
nr_requests = STM32_MDMA_MAX_REQUESTS;
dev_warn(&pdev->dev, "MDMA defaulting on %i request lines\n", nr_requests);
}

/*
* 读取 st,ahb-addr-masks 的元素个数。
* 该属性用于对某些地址域施加掩码约束(与具体 SoC AHB 地址映射相关)。
* 若不存在该属性,count 视为 0。
*/
count = device_property_count_u32(&pdev->dev, "st,ahb-addr-masks");
if (count < 0)
count = 0;

/*
* 分配 dmadev,并同时为可变长度数组 ahb_addr_masks[] 预留空间。
* 采用 devm_ 管理,确保探测失败或卸载时自动释放。
*/
dmadev = devm_kzalloc(&pdev->dev,
struct_size(dmadev, ahb_addr_masks, count),
GFP_KERNEL);
if (!dmadev)
return -ENOMEM;

/* 保存可变数组长度与设备能力参数 */
dmadev->nr_ahb_addr_masks = count;
dmadev->nr_channels = nr_channels;
dmadev->nr_requests = nr_requests;

/* 读取地址掩码数组本体(若 count 为 0,该调用等价于无操作) */
device_property_read_u32_array(&pdev->dev, "st,ahb-addr-masks",
dmadev->ahb_addr_masks, count);

/*
* 映射寄存器资源(resource index 0)。
* 返回值为 __iomem 指针,用于后续寄存器读写封装函数访问。
*/
dmadev->base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(dmadev->base))
return PTR_ERR(dmadev->base);

/*
* 获取时钟并使能。
* MDMA 寄存器访问与内部状态机运行依赖时钟;未使能时钟会导致访问异常或硬件不响应。
*/
dmadev->clk = devm_clk_get(&pdev->dev, NULL);
if (IS_ERR(dmadev->clk))
return dev_err_probe(&pdev->dev, PTR_ERR(dmadev->clk),
"Missing clock controller\n");

ret = clk_prepare_enable(dmadev->clk);
if (ret < 0) {
dev_err(&pdev->dev, "clk_prep_enable error: %d\n", ret);
return ret;
}

/*
* 可选复位:若存在 reset controller,则执行一次 assert->短延时->deassert。
* 目的:把 MDMA 控制器恢复到确定的硬件初始状态,避免早期阶段残留配置影响驱动假设。
*/
rst = devm_reset_control_get(&pdev->dev, NULL);
if (IS_ERR(rst)) {
ret = PTR_ERR(rst);
if (ret == -EPROBE_DEFER)
goto err_clk;
} else {
reset_control_assert(rst);
udelay(2);
reset_control_deassert(rst);
}

/*
* 初始化 DMAengine 设备对象 dd(dmadev->ddev)。
* 该对象向 DMAengine core 描述“这个控制器支持什么、如何操作它”。
*/
dd = &dmadev->ddev;

/* 声明能力:支持 SLAVE / PRIVATE / CYCLIC / MEMCPY */
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_cap_set(DMA_MEMCPY, dd->cap_mask);

/*
* 绑定 DMAengine 设备级回调。
* 这些回调定义资源管理、描述符准备、状态查询、启动/暂停/恢复/终止等行为。
*/
dd->device_alloc_chan_resources = stm32_mdma_alloc_chan_resources;
dd->device_free_chan_resources = stm32_mdma_free_chan_resources;
dd->device_tx_status = stm32_mdma_tx_status;
dd->device_issue_pending = stm32_mdma_issue_pending;
dd->device_prep_slave_sg = stm32_mdma_prep_slave_sg;
dd->device_prep_dma_cyclic = stm32_mdma_prep_dma_cyclic;
dd->device_prep_dma_memcpy = stm32_mdma_prep_dma_memcpy;
dd->device_config = stm32_mdma_slave_config;
dd->device_pause = stm32_mdma_pause;
dd->device_resume = stm32_mdma_resume;
dd->device_terminate_all = stm32_mdma_terminate_all;
dd->device_synchronize = stm32_mdma_synchronize;

/*
* descriptor_reuse 为 true 表示描述符可复用,这影响 DMAengine 对描述符生命周期的管理策略。
* 其正确性依赖驱动对描述符释放/重用路径的严格实现。
*/
dd->descriptor_reuse = true;

/*
* 声明支持的总线宽度(源/目的)以及方向集合。
* 这些字段用于在 DMAengine 层面进行能力匹配与参数合法性约束。
*/
dd->src_addr_widths = BIT(DMA_SLAVE_BUSWIDTH_1_BYTE) |
BIT(DMA_SLAVE_BUSWIDTH_2_BYTES) |
BIT(DMA_SLAVE_BUSWIDTH_4_BYTES) |
BIT(DMA_SLAVE_BUSWIDTH_8_BYTES);

dd->dst_addr_widths = BIT(DMA_SLAVE_BUSWIDTH_1_BYTE) |
BIT(DMA_SLAVE_BUSWIDTH_2_BYTES) |
BIT(DMA_SLAVE_BUSWIDTH_4_BYTES) |
BIT(DMA_SLAVE_BUSWIDTH_8_BYTES);

dd->directions = BIT(DMA_DEV_TO_MEM) | BIT(DMA_MEM_TO_DEV) | BIT(DMA_MEM_TO_MEM);

/* residue 粒度与最大 burst 约束,影响剩余量统计语义与上层期望 */
dd->residue_granularity = DMA_RESIDUE_GRANULARITY_BURST;
dd->max_burst = STM32_MDMA_MAX_BURST;

/* 建立 dma_device 与 struct device 的绑定,并初始化通道链表头 */
dd->dev = &pdev->dev;
INIT_LIST_HEAD(&dd->channels);

/*
* 初始化每个通道对象:
* - 设置通道 id
* - 读取 CCR 的 Secure 标记并记录为 reserved(用于后续过滤与分配约束)
* - 初始化 virt-dma 通道,并设置描述符释放回调
*/
for (i = 0; i < dmadev->nr_channels; i++) {
chan = &dmadev->chan[i];
chan->id = i;

/* 若硬件将该通道标记为 Secure,则记录为 reserved,避免非安全域使用 */
if (stm32_mdma_read(dmadev, STM32_MDMA_CCR(i)) & STM32_MDMA_CCR_SM)
dmadev->chan_reserved |= BIT(i);

chan->vchan.desc_free = stm32_mdma_desc_free;
vchan_init(&chan->vchan, dd);
}

/*
* 获取 IRQ 并申请中断处理函数。
* 中断用于处理传输完成、块完成、错误等事件,是驱动正确性的核心路径之一。
*/
dmadev->irq = platform_get_irq(pdev, 0);
if (dmadev->irq < 0) {
ret = dmadev->irq;
goto err_clk;
}

ret = devm_request_irq(&pdev->dev, dmadev->irq, stm32_mdma_irq_handler,
0, dev_name(&pdev->dev), dmadev);
if (ret) {
dev_err(&pdev->dev, "failed to request IRQ\n");
goto err_clk;
}

/*
* 注册 DMAengine 设备。
* 注册成功后,DMAengine core 才会将该控制器对外可见,并允许客户端申请通道。
*/
ret = dmaenginem_async_device_register(dd);
if (ret)
goto err_clk;

/*
* 注册 OF-DMA 控制器,使设备树中的 DMA phandle 能通过 xlate 函数映射为 dma_chan。
* 这是设备树客户端获取 MDMA 通道的关键路径。
*/
ret = of_dma_controller_register(of_node, stm32_mdma_of_xlate, dmadev);
if (ret < 0) {
dev_err(&pdev->dev, "STM32 MDMA DMA OF registration failed %d\n", ret);
goto err_clk;
}

/*
* 保存驱动私有数据,并启用 runtime PM。
* runtime PM 是否实际生效由系统策略决定,但驱动需提供可用的 enable/disable 入口。
*/
platform_set_drvdata(pdev, dmadev);
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 MDMA driver registered\n");
return 0;

err_clk:
/* 失败路径:撤销时钟使能,保证资源状态可预测 */
clk_disable_unprepare(dmadev->clk);
return ret;
}

stm32_mdma_of_xlate: 将设备树 DMA phandle 参数翻译为可用的 MDMA 通道并固化通道配置

stm32_mdma_of_xlate 是 STM32 MDMA 控制器驱动注册到 OF-DMA 框架的 xlate 回调:当设备树里的某个 DMA 客户端通过 phandle 引用该 MDMA 控制器时,OF-DMA 会把 phandle 参数打包成 struct of_phandle_args,并调用本函数完成两件事:

  1. 做参数合法性约束(参数个数、request line 范围、优先级范围)。
  2. 向 DMAengine 申请一个符合条件的通道,并把设备树参数固化到 chan->chan_config,作为后续“准备描述符/配置寄存器”的静态约束输入。 ([codebrowser.dev][1])

在 STM32H750(单核、无 MMU)场景下,本函数的关键意义不在于硬件差异,而在于:它把“设备树静态约束”转换为“DMAengine 通道对象 + 驱动私有配置”,从而让上层驱动后续调用 DMAengine API 时不需要重复携带设备树参数;并且通道分配仍必须通过 DMAengine 的全局机制完成(即使单核也可能在不同上下文交错访问全局通道表,因此不能省略锁语义)。

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
static bool stm32_mdma_filter_fn(struct dma_chan *c, void *fn_param)
{
struct stm32_mdma_chan *chan = to_stm32_mdma_chan(c);
struct stm32_mdma_device *dmadev = stm32_mdma_get_dev(chan);

/* Check if chan is marked Secure */
if (dmadev->chan_reserved & BIT(chan->id))
return false;

return true;
}

/**
* @brief 将设备树中的 MDMA phandle 参数翻译为一个可用的 DMAengine 通道,并固化通道配置
*
* 本函数是 OF-DMA 的 xlate 回调入口:负责解析 dma_spec 参数,申请通道,并把解析结果保存到
* 驱动私有通道结构体中,供后续 prepare/issue 等路径使用。
*
* @param dma_spec 设备树 phandle 参数集合,args[0..4] 的语义由本驱动定义
* @param ofdma OF-DMA 框架对象,携带控制器实例指针与设备树节点等上下文
* @return 成功时返回可用的 struct dma_chan*;失败时返回 NULL
*/
static struct dma_chan *stm32_mdma_of_xlate(struct of_phandle_args *dma_spec,
struct of_dma *ofdma)
{
/* 通过 OF-DMA 上下文取回本控制器实例(probe 注册 of_dma_controller_register 时写入) */
struct stm32_mdma_device *dmadev = ofdma->of_dma_data;

/* 复制控制器对外声明的能力掩码,用于通道申请时的 capability 过滤 */
dma_cap_mask_t mask = dmadev->ddev.cap_mask;

/* 驱动私有通道指针:用于把解析出的配置固化到 chan->chan_config */
struct stm32_mdma_chan *chan;

/* DMAengine 通道抽象:作为返回值与后续 DMAengine API 的输入 */
struct dma_chan *c;

/* 通道配置快照:由设备树参数构造,既用于过滤器选择通道,也用于最终固化 */
struct stm32_mdma_chan_config config;

/* 设备树参数个数检查:本驱动要求至少 5 个参数(request/priority/transfer/mask_addr/mask_data) */
if (dma_spec->args_count < 5) {
/* 记录错误:这里使用控制器设备对象打印,便于定位是哪一个 MDMA 控制器实例 */
dev_err(mdma2dev(dmadev), "Bad number of args\n");
return NULL; /* 以 NULL 表示 xlate 失败,OF-DMA 将认为无法得到通道 */
}

/* 将 config 清零,确保未显式设置的字段保持确定性(避免栈垃圾影响过滤或后续路径) */
memset(&config, 0, sizeof(config));

/* 解析设备树参数:request line(触发源/请求线编号) */
config.request = dma_spec->args[0];

/* 解析设备树参数:优先级(驱动用该值约束 CCR/优先级相关寄存器的编程) */
config.priority_level = dma_spec->args[1];

/* 解析设备树参数:传输配置(用于后续 CTCR 等寄存器组合,xlate 阶段只固化不解码) */
config.transfer_config = dma_spec->args[2];

/* 解析设备树参数:地址掩码(与硬件 mask address 寄存器语义对应,作为后续编程输入) */
config.mask_addr = dma_spec->args[3];

/* 解析设备树参数:数据掩码(与硬件 mask data 寄存器语义对应,作为后续编程输入) */
config.mask_data = dma_spec->args[4];

/* request line 合法性检查:避免客户端引用不存在的请求线 */
if (config.request >= dmadev->nr_requests) {
dev_err(mdma2dev(dmadev), "Bad request line\n");
return NULL;
}

/* 优先级合法性检查:驱动只支持到 VERY_HIGH_PRIORITY(通常对应硬件允许的最大编码) */
if (config.priority_level > STM32_MDMA_VERY_HIGH_PRIORITY) {
dev_err(mdma2dev(dmadev), "Priority level not supported\n");
return NULL;
}

/*
* 申请一个 DMAengine 通道:
* - mask:能力筛选条件
* - stm32_mdma_filter_fn:驱动过滤器(会基于 config 与通道保留位等约束筛选通道)
* - &config:过滤器参数(把设备树约束参与到通道选择中)
* - ofdma->of_node:限制匹配到当前控制器实例的通道集合
*/
c = __dma_request_channel(&mask, stm32_mdma_filter_fn, &config, ofdma->of_node);
if (!c) {
/* 没有可用通道:可能是通道耗尽,也可能是过滤条件导致无匹配 */
dev_err(mdma2dev(dmadev), "No more channels available\n");
return NULL;
}

/* 将通用 dma_chan 转换为驱动私有 stm32_mdma_chan,以便写入私有配置字段 */
chan = to_stm32_mdma_chan(c);

/* 固化通道配置:后续数据路径无需再从设备树解析,直接使用 chan->chan_config */
chan->chan_config = config;

/* 返回通道给 OF-DMA/DMAengine 客户端使用 */
return c;
}

m2m_hw 是什么

m2m_hw 在这份驱动里的含义是:“内存到内存(M2M)的硬件触发模式”,更具体一点是你代码里那句注释所说的场景:
由 STM32 的另一个 DMA(常见是 DMA/BDMA)产生硬件触发节拍,来驱动 MDMA 按 SG 段逐段执行

它不是“普通的 memcpy”(纯软件发起、MDMA 自己跑完),而是强调:

  • 触发源不是 CPU,而是“另一个 DMA/外设事件”
  • MDMA 的每个 SG 节点可能需要配合“上游 DMA 的重装填/再使能”
  • 所以驱动在 prep_slave_sg() 里对 DMA_MEM_TO_DEV 的 m2m_hw 情况,把每个硬件描述符的 cmar/cmdr 清零,目的是改变完成标志/节拍相关的行为,让 CPU 能在合适时机“重装填上游 STM32 DMA,并更新 dmaengine 框架状态”(你贴的英文注释就是这个意思)

你可以把它理解为:MDMA 在这里扮演“被节拍驱动的搬运器”,节拍来自另一个 DMA。


“外设从模式 SG 传输” 是什么

“外设从模式(slave)”在 DMAengine 里指:DMA 的一端是外设寄存器地址(固定或按规则变化),另一端是内存,常见方向:

  • DMA_DEV_TO_MEM:外设 -> 内存(接收)
  • DMA_MEM_TO_DEV:内存 -> 外设(发送)

“SG(scatter-gather)传输”指:一次传输不是一段连续缓冲区,而是一组分散的内存片段struct scatterlist 数组)。驱动需要把这些片段转换成硬件能执行的“多段/链式”传输。

所以“外设从模式 SG 传输”合起来就是:

外设 <-> 内存 的 DMA 传输,内存侧由多个 scatterlist 段组成;驱动负责把这些段编成硬件描述符链,并交给 DMA 引擎执行。

你贴的 stm32_mdma_prep_slave_sg() 就是做这件事:分配 desc(含多个 node),再 setup_xfer() 把 SG 段转成硬件描述符,最后 vchan_tx_prep() 把它挂入 virt-dma 队列。

stm32_mdma_slave_config / stm32_mdma_prep_slave_sg: 固化外设侧配置并将 SG 请求转换为可提交的 MDMA 描述符

这两个函数是外设流主线的前两步,关系非常紧密:

  • stm32_mdma_slave_config:把上层(外设驱动)传入的 dma_slave_config 固化到通道私有状态chan->dma_config),并在“STM32 DMA 触发 MDMA(m2m_hw)”这种特殊模式下,从 peripheral_config 里提取 MDMA 需要的 request/cmar/cmdr,写入 chan->chan_config,形成后续构建硬件描述符的静态输入。
  • stm32_mdma_prep_slave_sg:基于 chan->chan_config + chan->dma_config 以及本次 sgl/sg_len/direction,分配 desc,并调用 stm32_mdma_setup_xfer() 把 SG 转换成一组可执行的硬件描述符节点;在 m2m_hw && DMA_MEM_TO_DEV 特例下,按驱动策略把每个节点的 cmar/cmdr 清零,以改变“硬件完成标志的清理方式/节拍”,满足“CPU 重新装填 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
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
/**
* @brief 配置 MDMA 从模式通道:固化 dma_slave_config,并处理“STM32 DMA 触发 MDMA”的扩展参数
* @param[in] c DMAengine 抽象通道指针
* @param[in] config 上层外设驱动提供的从模式配置(方向、地址、宽度、burst 等)
* @return 成功返回 0;失败返回负错误码(本实现始终返回 0)
*/
static int stm32_mdma_slave_config(struct dma_chan *c,
struct dma_slave_config *config)
{
/** 将通用 dma_chan 转为驱动私有 stm32_mdma_chan */
struct stm32_mdma_chan *chan = to_stm32_mdma_chan(c);

/**
* 固化外设侧配置:
* 这里直接按结构体大小拷贝,意味着后续数据路径不再依赖调用者保留 config 生命周期。
*/
memcpy(&chan->dma_config, config, sizeof(*config));

/**
* 判断是否启用“STM32 DMA 触发 MDMA”的扩展模式:
* 该驱动以 peripheral_size 作为是否存在扩展配置的判据。
*
* 注意:这属于驱动约定语义,而非 dma_slave_config 字段在通用框架中的唯一用法。
*/
if (config->peripheral_size) {
/** 扩展配置的解释类型:由调用者在 peripheral_config 中传入并由本驱动按约定解析 */
struct stm32_mdma_dma_config *mdma_config;

/**
* 将通用的 peripheral_config 指针解释为 stm32_mdma_dma_config:
* 该指针的类型与内容完全由“调用者与本驱动的约定”决定,
* 框架不做类型检查,因此这里的解释必须依赖调用者正确传入。
*/
mdma_config = (struct stm32_mdma_dma_config *)chan->dma_config.peripheral_config;

/** 固化 request line:用于后续选择/配置 MDMA 的触发请求线 */
chan->chan_config.request = mdma_config->request;

/** 固化地址掩码输入:后续会写入硬件描述符的 CMAR 字段或相关寄存器语义 */
chan->chan_config.mask_addr = mdma_config->cmar;

/** 固化数据掩码输入:后续会写入硬件描述符的 CMDR 字段或相关寄存器语义 */
chan->chan_config.mask_data = mdma_config->cmdr;

/**
* 标记该通道进入 m2m_hw 模式:
* 用于在 prep/irq 等路径中启用特殊处理(例如对完成标志清理/节拍的策略差异)。
*/
chan->chan_config.m2m_hw = true;
}

/** 本实现不做参数合法性拒绝,直接返回成功 */
return 0;
}

/**
* @brief 准备外设从模式 SG 传输:构建 MDMA 描述符并返回可提交的异步事务描述符
* @param[in] c DMAengine 抽象通道指针
* @param[in] sgl scatterlist 首元素指针
* @param[in] sg_len scatterlist 元素数量
* @param[in] direction 传输方向(DEV_TO_MEM / MEM_TO_DEV 等)
* @param[in] flags dmaengine flags(如 DMA_PREP_INTERRUPT 等)
* @param[in] context 上层透传上下文指针(此处未使用)
* @return 成功返回 dma_async_tx_descriptor 指针;失败返回 NULL
*/
static struct dma_async_tx_descriptor *
stm32_mdma_prep_slave_sg(struct dma_chan *c, struct scatterlist *sgl,
u32 sg_len, enum dma_transfer_direction direction,
unsigned long flags, void *context)
{
/** 将通用 dma_chan 转为驱动私有通道 */
struct stm32_mdma_chan *chan = to_stm32_mdma_chan(c);

/** 取出通道级静态配置引用(由 of_xlate 或 slave_config 等路径写入) */
struct stm32_mdma_chan_config *chan_config = &chan->chan_config;

/** desc:本次要构建的 MDMA 描述符(包含多个 node,每个 node 对应一个 SG 元素或分段) */
struct stm32_mdma_desc *desc;

/** i:循环索引;ret:错误码 */
int i, ret;

/**
* 循环模式互斥约束:
* 一旦该通道已进入 cyclic 模式并持有 cyclic 描述符,则不允许再提交新的非 cyclic 请求。
*
* 该限制的实质是:cyclic 运行时通道的状态机与队列语义不同,
* 若允许混入普通 SG,会破坏“循环推进/回调节拍”的不变式。
*/
if (chan->desc && chan->desc->cyclic) {
/** 报错并拒绝请求 */
dev_err(chan2dev(chan),
"Request not allowed when dma in cyclic mode\n");
return NULL;
}

/**
* 分配描述符对象:
* - sg_len 决定 node 数量上界(典型实现为每个 SG 元素一个 node)
* - desc 内部通常还会关联硬件描述符(从 dma_pool 分配)与物理地址
*/
desc = stm32_mdma_alloc_desc(chan, sg_len);
if (!desc)
return NULL;

/**
* 建立本次传输的硬件执行形态:
* 将 (sgl, sg_len, direction) 转换为一组硬件描述符节点的配置,
* 并把必要的寄存器字段/LLI 链关系写入 desc->node[i].hwdesc 等结构。
*/
ret = stm32_mdma_setup_xfer(chan, desc, sgl, sg_len, direction);
if (ret < 0)
goto xfer_setup_err;

/**
* m2m_hw 特例处理(由 stm32_mdma_slave_config 写入 chan_config->m2m_hw):
* 当 MDMA 是由 STM32 DMA 触发、且方向为 MEM_TO_DEV 时,驱动选择把每个节点的 cmar/cmdr 清零。
*
* 该处理的核心目的是改变“传输完成标志相关的清理/再装填节拍”,
* 使 CPU 能够在合适的时机重新装填 STM32 DMA 的下一段 SG,并同步 dmaengine 框架的部分状态。
*
* 注意:这是驱动对特定硬件协同模式的策略实现,不是通用 SG 传输的必需步骤。
*/
if (chan_config->m2m_hw && direction == DMA_MEM_TO_DEV) {
/** hwdesc:硬件描述符指针;每个 node 一份 */
struct stm32_mdma_hwdesc *hwdesc;

/** 遍历所有 SG 节点并清零对应硬件描述符的 cmar/cmdr 字段 */
for (i = 0; i < sg_len; i++) {
hwdesc = desc->node[i].hwdesc;
hwdesc->cmar = 0;
hwdesc->cmdr = 0;
}
}

/** 明确标记该描述符不是 cyclic 描述符 */
desc->cyclic = false;

/**
* 交给 virt-dma 框架生成通用的 tx_descriptor:
* - vchan_tx_prep 会把 vdesc 挂入 vchan 队列管理体系
* - flags 决定是否需要中断回调、是否允许重复提交等语义
*/
return vchan_tx_prep(&chan->vchan, &desc->vdesc, flags);

xfer_setup_err:
/**
* 失败回收路径:
* - 回收每个 node 分配的硬件描述符(dma_pool_free)
* - 释放 desc 本体(kfree)
*
* 关键点:这里依赖 desc->count 来确定已成功分配/初始化的节点数量,
* 避免对未初始化指针做 free。
*/
for (i = 0; i < desc->count; i++)
dma_pool_free(chan->desc_pool, desc->node[i].hwdesc,
desc->node[i].hwdesc_phys);
kfree(desc);
return NULL;
}

stm32_mdma_alloc_chan_resources / stm32_mdma_free_chan_resources / stm32_mdma_alloc_desc / stm32_mdma_desc_free: 建立通道资源与描述符生命周期闭环

这四段代码构成了 STM32 MDMA 驱动里“资源与对象生命周期”的最小闭环:

  • stm32_mdma_alloc_chan_resources:为一个通道建立硬件描述符内存池desc_pool),并通过 runtime PM 确保控制器处于可访问状态,然后把硬件通道拉回“禁用/干净”的基线状态(stm32_mdma_disable_chan)。
  • stm32_mdma_free_chan_resources:在释放通道资源前,如果通道仍处于 busy,先在 vchan 锁保护下停止硬件并清理当前描述符指针;随后 pm_runtime_put 释放 PM 引用,释放 vchan 资源,销毁描述符池。
  • stm32_mdma_alloc_desc:按 count 分配一个可变长 desc,并为每个 node 从 desc_pool 分配一块 stm32_mdma_hwdesc,形成“软件描述符 + 硬件描述符数组”的组合对象。
  • stm32_mdma_desc_free:virt-dma 统一的释放钩子,只做两件事:逐 node 归还 hwdesc 到 pool,再 kfree(desc) 释放软件描述符本体。

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
/**
* @brief 为一个 MDMA 通道分配运行态资源:创建硬件描述符 pool,并把硬件通道置于可用基线状态
* @param[in] c DMAengine 抽象通道指针
* @return 成功返回 0;失败返回负错误码
*/
static int stm32_mdma_alloc_chan_resources(struct dma_chan *c)
{
/** 将通用 dma_chan 转换为驱动私有通道对象 */
struct stm32_mdma_chan *chan = to_stm32_mdma_chan(c);

/** 获取该通道所属的控制器对象,用于 runtime PM 与硬件控制 */
struct stm32_mdma_device *dmadev = stm32_mdma_get_dev(chan);

/** 通用错误码返回值 */
int ret;

/**
* 创建通道级硬件描述符内存池:
* - 每个元素大小为 struct stm32_mdma_hwdesc
* - 对齐要求使用 __alignof__(struct stm32_mdma_hwdesc)
* - dmam_ 前缀表示该 pool 的生命周期被 device-managed 机制跟踪
*
* 第一个参数使用通道的 device 名称作为 pool 名字,主要用于调试可见性。
*/
chan->desc_pool = dmam_pool_create(dev_name(&c->dev->device),
c->device->dev,
sizeof(struct stm32_mdma_hwdesc),
__alignof__(struct stm32_mdma_hwdesc),
0);
/**
* pool 创建失败:属于硬失败,后续无法构建任何 hwdesc 节点
*/
if (!chan->desc_pool) {
dev_err(chan2dev(chan), "failed to allocate descriptor pool\n");
return -ENOMEM;
}

/**
* 获取 runtime PM 引用并确保设备处于 active:
* - 成功后,寄存器访问与后续 disable/stop 等硬件操作才具有确定语义
* - ret < 0 表示设备未就绪或依赖未满足
*/
ret = pm_runtime_resume_and_get(dmadev->ddev.dev);
if (ret < 0)
return ret;

/**
* 将硬件通道置为禁用/干净状态:
* 该步骤的工程意义是消除“上一次使用残留状态”对新提交事务的影响。
*/
ret = stm32_mdma_disable_chan(chan);
if (ret < 0)
/** 失败则释放 runtime PM 引用,避免引用泄漏导致设备无法进入低功耗 */
pm_runtime_put(dmadev->ddev.dev);

/** 返回 disable 结果(0 表示成功,负值表示失败) */
return ret;
}

/**
* @brief 释放一个 MDMA 通道的运行态资源:停止通道、释放 vchan 资源并销毁描述符 pool
* @param[in] c DMAengine 抽象通道指针
*/
static void stm32_mdma_free_chan_resources(struct dma_chan *c)
{
/** 通用 dma_chan -> 私有通道 */
struct stm32_mdma_chan *chan = to_stm32_mdma_chan(c);

/** 私有通道 -> 控制器,用于 runtime PM put */
struct stm32_mdma_device *dmadev = stm32_mdma_get_dev(chan);

/** 保存并恢复 IRQ 状态的自旋锁标志 */
unsigned long flags;

/** 调试输出:释放哪个通道 */
dev_dbg(chan2dev(chan), "Freeing channel %d\n", chan->id);

/**
* 若通道仍在运行(busy),必须先停止硬件:
* 使用 vchan.lock + irqsave 的原因是避免与中断处理/回调推进路径并发交错,
* 保证 stop 与 chan->desc 置空的原子性。
*/
if (chan->busy) {
spin_lock_irqsave(&chan->vchan.lock, flags);
stm32_mdma_stop(chan);
chan->desc = NULL;
spin_unlock_irqrestore(&chan->vchan.lock, flags);
}

/**
* 释放 runtime PM 引用:
* 与 alloc_chan_resources 中的 pm_runtime_resume_and_get 成对出现。
*/
pm_runtime_put(dmadev->ddev.dev);

/**
* 释放 virt-dma 通道资源:
* 该调用会清理 vchan 队列、cookie 状态等框架对象。
*/
vchan_free_chan_resources(to_virt_chan(c));

/**
* 销毁通道级描述符 pool,并把指针清空:
* 这里使用 dmam_pool_destroy 显式销毁,避免后续错误路径误用该 pool。
*/
dmam_pool_destroy(chan->desc_pool);
chan->desc_pool = NULL;
}

/**
* @brief 分配一个软件描述符对象,并为每个 node 分配一块硬件描述符(来自通道 pool)
* @param[in] chan 私有通道对象
* @param[in] count node 数量(通常等于 sg_len 或其分解后的段数)
* @return 成功返回 desc;失败返回 NULL
*/
static struct stm32_mdma_desc *stm32_mdma_alloc_desc(
struct stm32_mdma_chan *chan, u32 count)
{
/** 可变长描述符对象指针 */
struct stm32_mdma_desc *desc;

/** 循环索引 */
int i;

/**
* 分配可变长 desc:
* struct_size(desc, node, count) 计算包含 count 个 node 的总字节数。
* GFP_NOWAIT 表示不允许睡眠等待;在内存紧张时更容易失败,因此必须严格回滚。
*/
desc = kzalloc(struct_size(desc, node, count), GFP_NOWAIT);
if (!desc)
return NULL;

/** 固化 node 数量,用于后续释放与错误回滚的边界 */
desc->count = count;

/**
* 为每个 node 分配硬件描述符:
* - 从 chan->desc_pool 取出一块 struct stm32_mdma_hwdesc
* - 同时得到该块的物理地址 hwdesc_phys(供硬件链表/寄存器指向使用)
*/
for (i = 0; i < count; i++) {
desc->node[i].hwdesc =
dma_pool_alloc(chan->desc_pool, GFP_NOWAIT,
&desc->node[i].hwdesc_phys);
if (!desc->node[i].hwdesc)
goto err;
}

/** 所有 node 分配成功:返回 desc */
return desc;

err:
/**
* 错误回滚:
* - i 目前指向失败的索引位置
* - 需要释放之前已成功分配的 [0, i) 区间
*/
dev_err(chan2dev(chan), "Failed to allocate descriptor\n");
while (--i >= 0)
dma_pool_free(chan->desc_pool, desc->node[i].hwdesc,
desc->node[i].hwdesc_phys);

/** 释放 desc 本体 */
kfree(desc);
return NULL;
}

/**
* @brief virt-dma 描述符释放回调:归还硬件描述符并释放软件描述符本体
* @param[in] vdesc virt-dma 的通用描述符基类指针
*/
static void stm32_mdma_desc_free(struct virt_dma_desc *vdesc)
{
/** vdesc -> 私有 desc */
struct stm32_mdma_desc *desc = to_stm32_mdma_desc(vdesc);

/** 通过 vdesc->tx.chan 找到所属通道,从而拿到 desc_pool */
struct stm32_mdma_chan *chan = to_stm32_mdma_chan(vdesc->tx.chan);

/** 循环索引 */
int i;

/** 逐 node 归还硬件描述符到 pool */
for (i = 0; i < desc->count; i++)
dma_pool_free(chan->desc_pool, desc->node[i].hwdesc,
desc->node[i].hwdesc_phys);

/** 释放软件描述符对象 */
kfree(desc);
}

stm32_mdma_disable_chan / stm32_mdma_stop: 关闭 MDMA 通道并以轮询确认传输已完成,随后清中断并复位软件忙状态

这两段代码对应你前面看到的资源释放路径里的关键动作:在释放 vchan/desc/pool 之前,驱动必须把硬件通道拉回“确定停止”的状态,并清理残留中断状态,避免后续复用同一通道时出现错误触发或状态机错乱。

  • stm32_mdma_disable_chan:做“硬件层面的停机”,并用 原子轮询确认硬件侧认为“当前传输已完成(CTCIF 置位)”。
  • stm32_mdma_stop:在成功 disable 后,清掉通道的中断状态寄存器(若有残留),最后把 chan->busy 置为 false,完成“软件状态机复位”。
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
/**
* @brief 关闭一个 MDMA 通道:先屏蔽中断,再清 EN,并以原子轮询确认硬件传输完成
* @param[in] chan 目标 MDMA 通道
* @return 成功返回 0;超时返回 -EBUSY
*/
static int stm32_mdma_disable_chan(struct stm32_mdma_chan *chan)
{
/** 控制器对象,用于寄存器访问 */
struct stm32_mdma_device *dmadev = stm32_mdma_get_dev(chan);

/** ccr:通道控制寄存器当前值;cisr:通道中断状态寄存器读取值;id:通道号;reg:CCR 寄存器地址偏移 */
u32 ccr, cisr, id, reg;

/** ret:轮询超时返回码 */
int ret;

/** 取通道号并计算该通道 CCR 寄存器偏移 */
id = chan->id;
reg = STM32_MDMA_CCR(id);

/**
* 先屏蔽该通道所有中断:
* 目的:停机过程中避免新的 IRQ 进入,从而扰乱“停机-确认-清状态”的序列。
*/
stm32_mdma_clr_bits(dmadev, reg, STM32_MDMA_CCR_IRQ_MASK);

/** 读取当前 CCR,判断通道是否处于使能状态 */
ccr = stm32_mdma_read(dmadev, reg);
if (ccr & STM32_MDMA_CCR_EN) {
/**
* 清 EN 关闭通道:
* 这一步是“请求停止”,但硬件可能仍在完成最后一次总线事务。
*/
stm32_mdma_clr_bits(dmadev, reg, STM32_MDMA_CCR_EN);

/**
* 原子轮询等待传输完成标志 CTCIF:
* - 使用 *_atomic 版本,保证在不可睡眠上下文也能调用
* - 条件为 (cisr & CTCIF) 为真
* - 轮询间隔 10us,总超时 1000us
*
* 该同步点的驱动假设是:只有当硬件报告“传输完成”后,
* 才能安全进入后续的资源释放/状态复位,避免硬件仍访问描述符或数据缓冲。
*/
ret = readl_relaxed_poll_timeout_atomic(
dmadev->base + STM32_MDMA_CISR(id), cisr,
(cisr & STM32_MDMA_CISR_CTCIF), 10, 1000);
if (ret) {
/** 超时:说明硬件未在期望窗口内完成,返回忙错误 */
dev_err(chan2dev(chan), "%s: timeout!\n", __func__);
return -EBUSY;
}
}

/** 通道本就未使能或已确认完成:返回成功 */
return 0;
}

/**
* @brief 停止 MDMA 通道:关闭通道、清残留中断状态,并复位软件 busy 状态
* @param[in] chan 目标 MDMA 通道
*/
static void stm32_mdma_stop(struct stm32_mdma_chan *chan)
{
/** 控制器对象 */
struct stm32_mdma_device *dmadev = stm32_mdma_get_dev(chan);

/** status:通道中断状态寄存器快照 */
u32 status;

/** ret:disable 返回码 */
int ret;

/** 关闭 DMA 通道(含轮询确认完成) */
ret = stm32_mdma_disable_chan(chan);
if (ret < 0)
return;

/**
* 若存在残留中断状态,则清除:
* - 先读 CISR 得到当前挂起状态
* - 再向 CIFCR 写入同样的位值进行清除(写 1 清除的典型风格)
*/
status = stm32_mdma_read(dmadev, STM32_MDMA_CISR(chan->id));
if (status) {
dev_dbg(chan2dev(chan), "%s(): clearing interrupt: 0x%08x\n",
__func__, status);
stm32_mdma_set_bits(dmadev, STM32_MDMA_CIFCR(chan->id), status);
}

/** 复位软件忙状态:表示该通道已不再执行任何传输 */
chan->busy = false;
}

stm32_mdma_prep_dma_cyclic: 构建循环(cyclic)传输的硬件描述符链并返回可提交事务

该函数把一段连续环形缓冲区(buf_addr/buf_len)按 period_len 切分为 count 个周期段,并为每个周期段生成一个硬件描述符节点(hwdesc)。最后一个节点会被配置为“循环回到起点”的语义(通过传给 stm32_mdma_setup_hwdesc() 的标志位组合实现),从而形成硬件级循环传输。vchan_tx_prep() 负责把私有 desc 挂入 virt-dma 队列体系,真正启动要等 device_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
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
/**
* @brief 准备 cyclic 传输:把环形缓冲区按 period 切段并生成循环硬件描述符链
* @param[in] c DMAengine 抽象通道
* @param[in] buf_addr 环形缓冲区 DMA 地址(连续区域)
* @param[in] buf_len 缓冲区总长度(必须是 period_len 的整数倍)
* @param[in] period_len 单个周期段长度(必须 <= 硬件最大块长)
* @param[in] direction 传输方向(DMA_MEM_TO_DEV / DMA_DEV_TO_MEM)
* @param[in] flags DMAengine prep flags
* @return 成功返回可提交的 dma_async_tx_descriptor;失败返回 NULL
*/
static struct dma_async_tx_descriptor *
stm32_mdma_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)
{
struct stm32_mdma_chan *chan = to_stm32_mdma_chan(c); /**< 通用通道转私有通道 */
struct stm32_mdma_device *dmadev = stm32_mdma_get_dev(chan); /**< 反查控制器对象,用于寄存器/总线选择等 */
struct dma_slave_config *dma_config = &chan->dma_config; /**< 外设侧配置(src_addr/dst_addr 等) */
struct stm32_mdma_chan_config *chan_config = &chan->chan_config; /**< 通道级静态配置(含 m2m_hw 等) */
struct stm32_mdma_desc *desc; /**< 本次 cyclic 描述符(含 count 个 node) */
dma_addr_t src_addr, dst_addr; /**< 每段的源/目的 DMA 地址 */
u32 ccr, ctcr, ctbr, count; /**< CCR/CTCR/CTBR 组合字段与周期段计数 */
int i, ret; /**< i:段索引;ret:错误码 */

/**
* 若该通道已处于 cyclic 状态,则拒绝再次建立 cyclic:
* 该约束避免“一个通道同时存在多个循环语义的描述符链”,否则状态机无法定义。
*/
if (chan->desc && chan->desc->cyclic) {
dev_err(chan2dev(chan),
"Request not allowed when dma in cyclic mode\n");
return NULL;
}

/**
* 参数合法性:
* - buf_len/period_len 必须非 0
* - 单段长度不能超过硬件最大块长
*/
if (!buf_len || !period_len || period_len > STM32_MDMA_MAX_BLOCK_LEN) {
dev_err(chan2dev(chan), "Invalid buffer/period len\n");
return NULL;
}

/** 必须整除:保证能形成整数个周期段,否则循环边界无法对齐 */
if (buf_len % period_len) {
dev_err(chan2dev(chan), "buf_len not multiple of period_len\n");
return NULL;
}

/** 周期段数量:每个周期段对应一个硬件描述符节点 */
count = buf_len / period_len;

/** 分配描述符与 count 个硬件描述符节点 */
desc = stm32_mdma_alloc_desc(chan, count);
if (!desc)
return NULL;

/**
* 生成一次循环传输的“共用”寄存器域,并选择总线:
* - 对 MEM_TO_DEV:源来自 buf_addr,目的通常来自 dma_config->dst_addr(外设侧)
* - 对 DEV_TO_MEM:源通常来自 dma_config->src_addr(外设侧),目的为 buf_addr
*
* 此处把地址传给 set_xfer_param 的目的,是让其依据地址属性决定宽度/对齐/总线等策略。
*/
if (direction == DMA_MEM_TO_DEV) {
src_addr = buf_addr; /* 源为环形缓冲区起始地址 */
ret = stm32_mdma_set_xfer_param(chan, direction, &ccr, &ctcr,
&ctbr, src_addr, period_len);
stm32_mdma_set_bus(dmadev, &ctbr, STM32_MDMA_CTBR_SBUS,
src_addr);
} else {
dst_addr = buf_addr; /* 目的为环形缓冲区起始地址 */
ret = stm32_mdma_set_xfer_param(chan, direction, &ccr, &ctcr,
&ctbr, dst_addr, period_len);
stm32_mdma_set_bus(dmadev, &ctbr, STM32_MDMA_CTBR_DBUS,
dst_addr);
}

/** 参数推导失败则回收 desc/hwdesc */
if (ret < 0)
goto xfer_setup_err;

/**
* 配置中断使能(写入 desc->ccr,供后续启动时下发到硬件):
* - 先清 IRQ mask(允许中断)
* - 再显式打开:传输错误(TE)、传输完成(CTC)、块完成(BT)中断
*
* cyclic 需要“周期节拍”,块完成中断通常用于 period 回调节奏。
*/
ccr &= ~STM32_MDMA_CCR_IRQ_MASK;
ccr |= STM32_MDMA_CCR_TEIE | STM32_MDMA_CCR_CTCIE | STM32_MDMA_CCR_BTIE;
desc->ccr = ccr;

/**
* 逐周期段构建硬件描述符链:
* 每一段的 buf_addr 都按 i * period_len 递增。
*
* m2m_hw 特例:
* - 使用 (i & 1) 在奇偶段之间对“外设侧地址”做 period_len 偏移
* - 其效果等价于在两个地址之间做 ping-pong(双缓冲/双地址节拍)
* - 该偏移的意义取决于“上游触发源(STM32 DMA)如何装填/切换地址”的协同约定
*/
for (i = 0; i < count; i++) {
if (direction == DMA_MEM_TO_DEV) {
src_addr = buf_addr + i * period_len; /* 源:环形缓冲区第 i 段 */
dst_addr = dma_config->dst_addr; /* 目的:外设侧地址(默认固定) */
if (chan_config->m2m_hw && (i & 1))
dst_addr += period_len; /* m2m_hw:奇数段偏移,形成双地址节拍 */
} else {
src_addr = dma_config->src_addr; /* 源:外设侧地址(默认固定) */
if (chan_config->m2m_hw && (i & 1))
src_addr += period_len; /* m2m_hw:奇数段偏移,形成双地址节拍 */
dst_addr = buf_addr + i * period_len; /* 目的:环形缓冲区第 i 段 */
}

/**
* 写入第 i 个硬件描述符:
* - src/dst/len:该段的搬运参数
* - ctcr/ctbr:共用配置域
* - i == count - 1:标记最后一段(用于建立“回环到首段”或设置末段标志)
* - i == 0:标记首段(用于首段初始化差异)
* - true:表示这是 cyclic 链(由下层据此写入循环相关控制位/next 指针策略)
*/
stm32_mdma_setup_hwdesc(chan, desc, direction, i, src_addr,
dst_addr, period_len, ctcr, ctbr,
i == count - 1, i == 0, true);
}

/** 标记该描述符为 cyclic 描述符,后续提交/终止路径将按 cyclic 语义处理 */
desc->cyclic = true;

/** 交给 virt-dma:生成通用 tx 描述符并入队,等待 issue_pending 启动 */
return vchan_tx_prep(&chan->vchan, &desc->vdesc, flags);

xfer_setup_err:
/**
* 失败回收:
* - 逐段归还 hwdesc 到 pool
* - 释放 desc 本体
*/
for (i = 0; i < desc->count; i++)
dma_pool_free(chan->desc_pool, desc->node[i].hwdesc,
desc->node[i].hwdesc_phys);
kfree(desc);
return NULL;
}

1) 为什么 if (chan_config->m2m_hw && (i & 1)) src_addr += period_len; 要这么做?

在 m2m_hw 模式下,这里的 (i & 1)(奇偶段)+ += period_len 本质是在做乒乓(ping-pong)双地址:

偶数段 i=0,2,4…:用基地址 src_addr = base

奇数段 i=1,3,5…:用第二个地址 src_addr = base + period_len

目的不是“让地址更连续”这么简单,而是让循环的每个 period 在两个地址之间交替,从而匹配一种常见协同模式:
上游触发源(例如另一个 DMA/硬件节拍)往往以“半周期/周期”为节奏在两块缓冲区之间切换。MDMA 的 cyclic 描述符链也必须跟着交替,否则你会出现:

每个 period 都搬同一块源/目的地址,导致另一块缓冲区永远不被消费/填充;

或者上游以双缓冲产出数据,而下游 MDMA 只读其中一块,形成节拍错位。

所以这行代码表达的是:m2m_hw 时,把“外设侧(在框架里叫 dev 侧)”当成两个交替地址使用,用 period_len 作为两块区域的固定偏移量。

你这个质疑点非常关键:不是所有 DMA 都只会“按连续地址搬运”。STM32H7 的 MDMA 明确支持一种硬件级“linked-list mode(链表/列表模式)”,它让硬件在完成一个块(block)后,自己去内存里读取下一组“通道寄存器配置”,从而继续搬运下一段(下一段可以是完全不同的源/目的地址与长度)。

硬件为什么会“按链表推进”?

要把两类“地址”分清楚:

  1. 数据搬运地址(SAR/DAR)
    这是你说的“DMA 按连续地址搬运”的那部分:在单个块内,MDMA 根据 SAR(源地址)/DAR(目的地址)以及增量模式,把数据搬完这一段。

  2. 描述符/配置地址(CxLAR 指向的内存结构)
    MDMA linked-list mode 下,硬件在块传输结束时会触发一个机制:

    • 读取 MDMA_CxLAR(Channel x Link Address Register)里给出的地址;
    • 把该地址指向的一段内存当作“一个 node(节点)”,从里面重新装载一组 MDMA 通道配置寄存器值(包括 CxTCR, CxBNDTR, CxSAR, CxDAR, ... , CxLAR, ... 等);
    • 其中 CxLAR 自身也会被重载:如果新装载的 CxLAR != 0,硬件就认为“还有下一个节点”,继续在下一个块结束时再装载;如果为 0 就停止;如果两个节点互相指向,就形成硬件环形循环。 ([STMicroelectronics][1])

所以:硬件并不是“看不懂链表”。它是通过专门的寄存器 CxLAR把“下一次要装载的配置结构”挂到一个内存地址上;然后硬件作为总线 master 去读这段内存,更新自身寄存器,再按新的 SAR/DAR/BNDTR/... 继续搬下一段。 ([STMicroelectronics][1])

你说的“DMA 只能连续地址”是最基础 DMA的能力;MDMA 这种更复杂的控制器支持“搬运数据的同时,还能从内存装载下一段配置”,因此可以实现非连续段的自动推进。

stm32_mdma_set_bus: 根据地址高位选择 SBUS/DBUS 所在总线

这个函数只做一件事:根据“地址的高 4bit(0xF0000000 掩码)”判断该地址属于哪类内存窗口,然后把 CTBR 里由 ctbr_mask 指定的那个位域置位或清零,从而选择 MDMA 的源/目的总线(SBUS/DBUS)是走 AHB 还是 AXI。其判断依据来自 dmadev->ahb_addr_masks[](probe 阶段从设备树属性填充)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @brief 根据地址窗口配置 CTBR 中的 SBUS/DBUS 总线选择位
* @param[in] dmadev MDMA 控制器实例,提供 AHB 地址窗口掩码表
* @param[in,out] ctbr 指向将被修改的 CTBR 值(软件构造的寄存器镜像)
* @param[in] ctbr_mask 需要配置的位域掩码(例如 STM32_MDMA_CTBR_SBUS/DBUS)
* @param[in] src_addr 用于判断地址窗口的 32bit 地址(这里按 0xF0000000 做分类)
*/
static void stm32_mdma_set_bus(struct stm32_mdma_device *dmadev, u32 *ctbr,
u32 ctbr_mask, u32 src_addr)
{
u32 mask; /**< 地址窗口掩码后的高位结果 */
int i; /**< 遍历 ahb_addr_masks[] 的索引 */

*ctbr &= ~ctbr_mask; /* 清掉目标位域:默认选择 AXI 或默认态 */
mask = src_addr & 0xF0000000; /* 取地址高 4bit:用作“窗口分类键” */
for (i = 0; i < dmadev->nr_ahb_addr_masks; i++) { /* 遍历设备树提供的 AHB 窗口集合 */
if (mask == dmadev->ahb_addr_masks[i]) { /* 命中:该窗口属于 AHB */
*ctbr |= ctbr_mask; /* 置位:选择 AHB(由硬件位语义决定) */
break; /* 首次命中即可退出 */
}
}
}

stm32_mdma_set_xfer_param: 计算并回填一次传输所需的 CCR/CTCR/CTBR,并预置 CSAR/CDAR

这个函数的价值在于它做了**“把上层抽象参数(width/burst/request/transfer_config/buf_len)收敛成 MDMA 寄存器字段”**的工作,核心点有三类:

  1. TLEN 的“写入值=字节数-1”:从 CTCR 里取出 LEN2,再写回 TLEN(tlen-1),这是硬件编码规则,不是软件习惯。
  2. burst 与 width 的组合约束:强制 maxburst * buswidth <= STM32_MDMA_MAX_BURST,并且 maxburst 必须是 2 的幂;否则直接拒绝。
  3. 当源/目的数据宽度不一致时启用 PKE(pack):避免宽度不一致导致的硬件对齐/吞吐问题,由硬件 pack 逻辑完成数据重排。

它还会根据方向(DMA_MEM_TO_DEV / DMA_DEV_TO_MEM):

  • 计算 设备侧内存侧各自的 SIZE/INCOS/BURST
  • 配置 CTBR.TSEL(请求线);
  • stm32_mdma_set_bus() 选择 SBUS/DBUS;
  • 直接写入本通道 CSARCDAR,把源/目的地址预置到通道寄存器中。
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
/**
* @brief 依据方向与配置计算 MDMA 的 CCR/CTCR/CTBR,并配置通道地址寄存器
* @param[in] chan MDMA 通道私有对象
* @param[in] direction DMA 传输方向(仅支持 MEM_TO_DEV / DEV_TO_MEM)
* @param[out] mdma_ccr 回填的 CCR 软件镜像
* @param[out] mdma_ctcr 回填的 CTCR 软件镜像
* @param[out] mdma_ctbr 回填的 CTBR 软件镜像
* @param[in] addr “内存侧”起始地址(dma_addr_t)
* @param[in] buf_len 本次传输总长度(字节)
* @return 0 表示成功;负值表示参数不合法或宽度计算失败
*/
static int stm32_mdma_set_xfer_param(struct stm32_mdma_chan *chan,
enum dma_transfer_direction direction,
u32 *mdma_ccr, u32 *mdma_ctcr,
u32 *mdma_ctbr, dma_addr_t addr,
u32 buf_len)
{
struct stm32_mdma_device *dmadev = stm32_mdma_get_dev(chan); /**< 控制器实例:寄存器访问与全局能力 */
struct stm32_mdma_chan_config *chan_config = &chan->chan_config; /**< 通道静态配置:request/priority/transfer_config/m2m_hw */
enum dma_slave_buswidth src_addr_width, dst_addr_width; /**< 源/目的地址对应的总线宽度枚举 */
phys_addr_t src_addr, dst_addr; /**< 设备侧地址(这里用 phys_addr_t 承载) */
int src_bus_width, dst_bus_width; /**< 转成 MDMA 寄存器编码后的 width 值(非字节数) */
u32 src_maxburst, dst_maxburst, src_best_burst, dst_best_burst; /**< burst 上限与“最佳 burst”选择结果 */
u32 ccr, ctcr, ctbr, tlen; /**< 寄存器镜像与 TLEN 中间变量 */

src_addr_width = chan->dma_config.src_addr_width; /* 读取上层配置:源宽度 */
dst_addr_width = chan->dma_config.dst_addr_width; /* 读取上层配置:目的宽度 */
src_maxburst = chan->dma_config.src_maxburst; /* 读取上层配置:源 burst */
dst_maxburst = chan->dma_config.dst_maxburst; /* 读取上层配置:目的 burst */

ccr = stm32_mdma_read(dmadev, STM32_MDMA_CCR(chan->id)) & ~STM32_MDMA_CCR_EN; /* 取当前 CCR,并确保软件镜像里 EN 清零 */
ctcr = stm32_mdma_read(dmadev, STM32_MDMA_CTCR(chan->id)); /* 取当前 CTCR 作为基线 */
ctbr = stm32_mdma_read(dmadev, STM32_MDMA_CTBR(chan->id)); /* 取当前 CTBR 作为基线 */

ctcr &= ~STM32_MDMA_CTCR_SWRM; /* 选择硬件请求模式:清 SWRM */
ctcr &= ~STM32_MDMA_CTCR_CFG_MASK; /* 清掉可配置字段位域 */
ctcr |= chan_config->transfer_config & STM32_MDMA_CTCR_CFG_MASK; /* 应用设备树/通道配置给出的 DINC/SINC/INCOS/TRGM/TLEN 等 */

tlen = STM32_MDMA_CTCR_LEN2_GET(ctcr); /* 取 LEN2(“每个 buffer chunk 的长度”语义) */
ctcr &= ~STM32_MDMA_CTCR_LEN2_MSK; /* 清 LEN2 位域 */
ctcr |= STM32_MDMA_CTCR_TLEN((tlen - 1)); /* 写 TLEN:硬件编码要求写 (字节数 - 1) */

ctcr &= ~STM32_MDMA_CTCR_PKE; /* 默认禁用 pack:只有宽度不一致时才启用 */

if (src_maxburst * src_addr_width > STM32_MDMA_MAX_BURST || /* 约束:burst*width 不得超过硬件上限 */
dst_maxburst * dst_addr_width > STM32_MDMA_MAX_BURST) {
dev_err(chan2dev(chan),
"burst size * bus width higher than %d bytes\n",
STM32_MDMA_MAX_BURST);
return -EINVAL; /* 参数不合法:拒绝配置 */
}

if ((!is_power_of_2(src_maxburst) && src_maxburst > 0) || /* 约束:burst 必须是 2 的幂(或为 0 表示未指定) */
(!is_power_of_2(dst_maxburst) && dst_maxburst > 0)) {
dev_err(chan2dev(chan), "burst size must be a power of 2\n");
return -EINVAL; /* 参数不合法:拒绝配置 */
}

ccr &= ~(STM32_MDMA_CCR_SWRQ | STM32_MDMA_CCR_WEX | STM32_MDMA_CCR_HEX | /* 硬件请求:清 SWRQ;并清异常注入相关位 */
STM32_MDMA_CCR_BEX | STM32_MDMA_CCR_PL_MASK); /* 清优先级位域:准备重写 */
ccr |= STM32_MDMA_CCR_PL(chan_config->priority_level); /* 写入通道优先级 */

ctbr &= ~STM32_MDMA_CTBR_TSEL_MASK; /* 清触发选择 */
ctbr |= STM32_MDMA_CTBR_TSEL(chan_config->request); /* 写入 request line:决定由哪个硬件请求触发 */

switch (direction) {
case DMA_MEM_TO_DEV:
dst_addr = chan->dma_config.dst_addr; /* 设备侧目的地址:来自 slave_config */

if (chan_config->m2m_hw) /* 特殊:STM32 DMA 触发 MDMA 的“硬件 M2M”模式 */
dst_addr_width = stm32_mdma_get_max_width(dst_addr, buf_len,
STM32_MDMA_MAX_BUF_LEN); /* 设备侧宽度按地址/长度推导 */
dst_bus_width = stm32_mdma_get_width(chan, dst_addr_width); /* 把 buswidth 枚举转成寄存器编码 */
if (dst_bus_width < 0)
return dst_bus_width; /* 宽度不支持:直接失败 */
ctcr &= ~STM32_MDMA_CTCR_DSIZE_MASK; /* 清目的数据宽度位域 */
ctcr |= STM32_MDMA_CTCR_DSIZE(dst_bus_width); /* 写目的数据宽度 */
if (chan_config->m2m_hw) { /* 硬件 M2M:目的地址也需要 INCOS */
ctcr &= ~STM32_MDMA_CTCR_DINCOS_MASK; /* 清目的 INCOS */
ctcr |= STM32_MDMA_CTCR_DINCOS(dst_bus_width); /* 目的 INCOS 与数据宽度一致 */
}

if (chan_config->m2m_hw) /* 硬件 M2M:目的侧 burst 取最大可用 */
dst_maxburst = STM32_MDMA_MAX_BUF_LEN / dst_addr_width;

dst_best_burst = stm32_mdma_get_best_burst(buf_len, tlen, /* 根据 buf_len 与 tlen 选最合适的 burst */
dst_maxburst,
dst_addr_width);
chan->mem_burst = dst_best_burst; /* 记录:后续可能用于调试/路径判断 */
ctcr &= ~STM32_MDMA_CTCR_DBURST_MASK; /* 清目的 burst 位域 */
ctcr |= STM32_MDMA_CTCR_DBURST((ilog2(dst_best_burst))); /* 写目的 burst:用 log2 编码 */

src_addr_width = stm32_mdma_get_max_width(addr, buf_len, tlen); /* 内存侧宽度推导:尽量使用更宽以提升吞吐 */
chan->mem_width = src_addr_width; /* 记录:内存侧 width */
src_bus_width = stm32_mdma_get_width(chan, src_addr_width); /* width 枚举 -> 寄存器编码 */
if (src_bus_width < 0)
return src_bus_width;
ctcr &= ~STM32_MDMA_CTCR_SSIZE_MASK |
STM32_MDMA_CTCR_SINCOS_MASK; /* 清源侧数据宽度与 INCOS */
ctcr |= STM32_MDMA_CTCR_SSIZE(src_bus_width) |
STM32_MDMA_CTCR_SINCOS(src_bus_width); /* 源侧:宽度 + 递增步长(INCOS) */

src_maxburst = STM32_MDMA_MAX_BUF_LEN / src_addr_width; /* 内存侧最大 burst:由硬件上限/宽度决定 */
src_best_burst = stm32_mdma_get_best_burst(buf_len, tlen,
src_maxburst,
src_addr_width);
chan->mem_burst = src_best_burst; /* 记录:内存侧 burst */
ctcr &= ~STM32_MDMA_CTCR_SBURST_MASK; /* 清源 burst */
ctcr |= STM32_MDMA_CTCR_SBURST((ilog2(src_best_burst))); /* 写源 burst */

stm32_mdma_set_bus(dmadev, &ctbr, STM32_MDMA_CTBR_DBUS, dst_addr); /* 目的侧(设备侧)挂在 DBUS:按地址窗口选 AHB/AXI */

if (dst_bus_width != src_bus_width) /* 宽度不一致:打开 pack 由硬件完成重排 */
ctcr |= STM32_MDMA_CTCR_PKE;

stm32_mdma_write(dmadev, STM32_MDMA_CDAR(chan->id), dst_addr); /* 预置目的地址寄存器:设备侧地址 */
break;

case DMA_DEV_TO_MEM:
src_addr = chan->dma_config.src_addr; /* 设备侧源地址:来自 slave_config */

if (chan_config->m2m_hw) /* 硬件 M2M:源侧宽度按地址/长度推导 */
src_addr_width = stm32_mdma_get_max_width(src_addr, buf_len,
STM32_MDMA_MAX_BUF_LEN);

src_bus_width = stm32_mdma_get_width(chan, src_addr_width); /* width 枚举 -> 寄存器编码 */
if (src_bus_width < 0)
return src_bus_width;
ctcr &= ~STM32_MDMA_CTCR_SSIZE_MASK; /* 清源数据宽度 */
ctcr |= STM32_MDMA_CTCR_SSIZE(src_bus_width); /* 写源数据宽度 */
if (chan_config->m2m_hw) { /* 硬件 M2M:源侧也需要 INCOS */
ctcr &= ~STM32_MDMA_CTCR_SINCOS_MASK;
ctcr |= STM32_MDMA_CTCR_SINCOS(src_bus_width);
}

if (chan_config->m2m_hw) /* 硬件 M2M:源侧 burst 取最大可用 */
src_maxburst = STM32_MDMA_MAX_BUF_LEN / src_addr_width;

src_best_burst = stm32_mdma_get_best_burst(buf_len, tlen,
src_maxburst,
src_addr_width);
ctcr &= ~STM32_MDMA_CTCR_SBURST_MASK; /* 清源 burst */
ctcr |= STM32_MDMA_CTCR_SBURST((ilog2(src_best_burst))); /* 写源 burst */

dst_addr_width = stm32_mdma_get_max_width(addr, buf_len, tlen); /* 内存侧目的宽度推导 */
chan->mem_width = dst_addr_width; /* 记录:内存侧 width */
dst_bus_width = stm32_mdma_get_width(chan, dst_addr_width); /* width 枚举 -> 寄存器编码 */
if (dst_bus_width < 0)
return dst_bus_width;
ctcr &= ~(STM32_MDMA_CTCR_DSIZE_MASK |
STM32_MDMA_CTCR_DINCOS_MASK); /* 清目的宽度与 INCOS */
ctcr |= STM32_MDMA_CTCR_DSIZE(dst_bus_width) |
STM32_MDMA_CTCR_DINCOS(dst_bus_width); /* 目的侧:宽度 + 递增步长 */

dst_maxburst = STM32_MDMA_MAX_BUF_LEN / dst_addr_width; /* 内存侧最大 burst */
dst_best_burst = stm32_mdma_get_best_burst(buf_len, tlen,
dst_maxburst,
dst_addr_width);
ctcr &= ~STM32_MDMA_CTCR_DBURST_MASK; /* 清目的 burst */
ctcr |= STM32_MDMA_CTCR_DBURST((ilog2(dst_best_burst))); /* 写目的 burst */

stm32_mdma_set_bus(dmadev, &ctbr, STM32_MDMA_CTBR_SBUS, src_addr); /* 源侧(设备侧)挂在 SBUS:按地址窗口选 AHB/AXI */

if (dst_bus_width != src_bus_width) /* 宽度不一致:打开 pack */
ctcr |= STM32_MDMA_CTCR_PKE;

stm32_mdma_write(dmadev, STM32_MDMA_CSAR(chan->id), src_addr); /* 预置源地址寄存器:设备侧地址 */
break;

default:
dev_err(chan2dev(chan), "Dma direction is not supported\n"); /* 该 helper 明确不处理其它方向 */
return -EINVAL;
}

*mdma_ccr = ccr; /* 回填 CCR 镜像:供上层写硬件或写描述符 */
*mdma_ctcr = ctcr; /* 回填 CTCR 镜像 */
*mdma_ctbr = ctbr; /* 回填 CTBR 镜像 */

return 0; /* 成功 */
}

stm32_mdma_setup_hwdesc: 填充一个硬件描述符并串接到下一节点(或回环)

这里的“链表”不是 CPU 的 struct list_head,而是硬件描述符链:每个 struct stm32_mdma_hwdesc 在内存里有一个字段 clar,驱动把“下一个描述符的物理地址”写进 clar

  • 非最后节点:clar = desc->node[next].hwdesc_phys

  • 最后节点:

    • 循环:clar = desc->node[0].hwdesc_phys(回到头结点形成环)
    • 非循环:clar = 0(链结束)

硬件在完成当前节点后,会通过总线主接口去读取 clar 指向的下一描述符内容,从而实现“自动推进”。因此:

  • stm32_mdma_setup_hwdesc() 确实能配置多个;上限不在“函数”,而在于:

    • 你一次性为 desc->node[] 分配了多少个 hwdesc(内存资源上限);
    • 每个节点的 BNDT/块长度受硬件字段宽度和驱动约束限制(例如 STM32_MDMA_MAX_BLOCK_LEN 之类的上游检查)。
  • 这些节点保存在 desc->node[i].hwdesc 指向的 DMA coherent 内存块里(由 dma_pool_alloc 分配),并且每个节点都对应一个总线可见的 hwdesc_phys

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
/**
* @brief 构造一个 MDMA 硬件描述符节点,并设置 CLAR 指向下一节点/回环/结束
* @param[in] chan MDMA 通道
* @param[in] desc 软件描述符(包含 node[],每个 node 持有一个 hwdesc 与其物理地址)
* @param[in] dir 方向(此处主要用于调用方语义,当前函数不分支处理)
* @param[in] count 当前节点索引
* @param[in] src_addr 本节点源地址(写入 CSAR)
* @param[in] dst_addr 本节点目的地址(写入 CDAR)
* @param[in] len 本节点传输长度(写入 BNDT)
* @param[in] ctcr 本节点 CTCR 预配置值
* @param[in] ctbr 本节点 CTBR 预配置值
* @param[in] is_last 是否为最后一个节点
* @param[in] is_first 是否为第一个节点(当前实现未使用,但保留参数用于调用方一致性)
* @param[in] is_cyclic 是否构造成循环链表(最后节点回指到 0 号节点)
*/
static void stm32_mdma_setup_hwdesc(struct stm32_mdma_chan *chan,
struct stm32_mdma_desc *desc,
enum dma_transfer_direction dir, u32 count,
dma_addr_t src_addr, dma_addr_t dst_addr,
u32 len, u32 ctcr, u32 ctbr, bool is_last,
bool is_first, bool is_cyclic)
{
struct stm32_mdma_chan_config *config = &chan->chan_config; /**< 通道静态配置:mask_addr/mask_data 等 */
struct stm32_mdma_hwdesc *hwdesc; /**< 当前要填的硬件描述符指针 */
u32 next = count + 1; /**< 计算下一个节点索引 */

hwdesc = desc->node[count].hwdesc; /* 取出当前节点的 hwdesc(coherent 内存) */
hwdesc->ctcr = ctcr; /* 写 CTCR:本节点传输控制参数 */
hwdesc->cbndtr &= ~(STM32_MDMA_CBNDTR_BRC_MK | /* 清除 BNDT/重复/累加相关位域:重新写本节点长度 */
STM32_MDMA_CBNDTR_BRDUM |
STM32_MDMA_CBNDTR_BRSUM |
STM32_MDMA_CBNDTR_BNDT_MASK);
hwdesc->cbndtr |= STM32_MDMA_CBNDTR_BNDT(len); /* 写 BNDT:本节点块长度 */
hwdesc->csar = src_addr; /* 写源地址 */
hwdesc->cdar = dst_addr; /* 写目的地址 */
hwdesc->cbrur = 0; /* 复位重复相关寄存器字段(本驱动该路径固定为 0) */
hwdesc->ctbr = ctbr; /* 写 CTBR:触发选择与总线选择等 */
hwdesc->cmar = config->mask_addr; /* 写地址掩码(用于特定硬件触发/屏蔽语义) */
hwdesc->cmdr = config->mask_data; /* 写数据掩码 */

if (is_last) { /* 最后一个节点:决定“回环/结束” */
if (is_cyclic)
hwdesc->clar = desc->node[0].hwdesc_phys; /* 循环:CLAR 指向首节点物理地址,形成硬件环 */
else
hwdesc->clar = 0; /* 非循环:CLAR=0 表示链结束 */
} else {
hwdesc->clar = desc->node[next].hwdesc_phys; /* 非最后:CLAR 指向下一个节点物理地址 */
}

stm32_mdma_dump_hwdesc(chan, &desc->node[count]); /* 调试输出:打印该节点的关键字段 */
}

stm32_mdma_prep_dma_memcpy: 为 DMAengine 的 memcpy 请求构建 STM32 MDMA 硬件描述符并返回可提交的事务描述符

这个函数实现 dma_device.device_prep_dma_memcpy 回调:把一次“内存到内存”的复制请求(srcdest,长度 len)转换为 STM32 MDMA 可执行的硬件描述符(单段或链表),并封装为 virt-dma 的 dma_async_tx_descriptor 返回给上层,供后续 issue_pending 真正启动。([codebrowser.dev][1])

核心实现原理

  • 两级分解策略(决定触发模式 TRGM)

    • len <= STM32_MDMA_MAX_BLOCK_LEN:使用“单描述符”路径;再根据 len <= STM32_MDMA_MAX_BUF_LEN 选择 BUFFERBLOCK 触发模式。([codebrowser.dev][1])
    • len > STM32_MDMA_MAX_BLOCK_LEN:使用 LINKED_LIST(链表)模式,把传输拆成 count = DIV_ROUND_UP(len, MAX_BLOCK_LEN) 个节点,每节点最多 MAX_BLOCK_LEN。([codebrowser.dev][1])
  • 带宽/对齐驱动的参数选择(宽度与突发)
    通过 stm32_mdma_get_max_width()stm32_mdma_get_best_burst()tlen 约束下计算“可行最大宽度”和“最优 burst”,再用 ilog2(best_burst) 写入 CTCR 的 SBURST/DBURST 字段;同时把 SSIZE/DSIZESINCOS/DINCOS 配套设置。([codebrowser.dev][1])
    当源/目的总线宽度不同,置位 PKE(pack/unpack),由硬件在搬运过程中做字节打包/解包。([codebrowser.dev][1])

  • 寄存器“清理 + 重建”式编程
    读取当前通道寄存器镜像(CCR/CTCR/CTBR/CBNDTR),显式清除与 memcpy 无关或可能污染的位域(例如硬件触发选择、优先级、地址掩码/写回等),再只保留 memcpy 路径必需的配置:软件请求模式、地址自增、异常/完成中断选择、总线选择等。([codebrowser.dev][1])

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
218
219
220
221
/**
* stm32_mdma_prep_dma_memcpy - 为 DMAengine memcpy 请求准备 STM32 MDMA 事务描述符
* @c: DMAengine 抽象通道指针(驱动会转换为 stm32_mdma_chan)
* @dest: 目的 DMA 总线地址(dma_addr_t)
* @src: 源 DMA 总线地址(dma_addr_t)
* @len: 需要复制的字节数
* @flags: DMAengine 事务标志(例如 DMA_PREP_INTERRUPT 等)
*
* 返回值:
* 成功:返回可提交的 dma_async_tx_descriptor 指针(virt-dma 封装)
* 失败:返回 NULL
*/
static struct dma_async_tx_descriptor *
stm32_mdma_prep_dma_memcpy(struct dma_chan *c, dma_addr_t dest, dma_addr_t src,
size_t len, unsigned long flags)
{
/* 将通用 dma_chan 转为驱动私有通道对象,后续需要访问 id/vchan/desc 等字段 */
struct stm32_mdma_chan *chan = to_stm32_mdma_chan(c);

/* 从通道反推出控制器实例(寄存器基址、能力、资源等都归属该实例) */
struct stm32_mdma_device *dmadev = stm32_mdma_get_dev(chan);

/* 记录“当前可用的最大总线宽度”,用于后续选择 SSIZE/DSIZE */
enum dma_slave_buswidth max_width;

/* 本次事务的驱动私有描述符对象(包含 vdesc + node[] 硬件描述符) */
struct stm32_mdma_desc *desc;

/* 直接指向第一个硬件描述符(单段路径会直接写 node[0]) */
struct stm32_mdma_hwdesc *hwdesc;

/* CCR/CTCR/CTBR/CBNDTR 为通道关键寄存器组合;count 为链表节点数 */
u32 ccr, ctcr, ctbr, cbndtr, count, max_burst, mdma_burst;

/* best_burst 为“最优突发长度(以 beat 计)”,tlen 为缓冲传输长度参数 */
u32 best_burst, tlen;

/* xfer_count 为每个节点的实际搬运长度;offset 为节点内偏移 */
size_t xfer_count, offset;

/* 源/目的侧的“硬件编码后的宽度枚举值”(写入 CTCR 的 SSIZE/DSIZE) */
int src_bus_width, dst_bus_width;

/* 通用循环变量 */
int i;

/* 若通道当前被配置为 cyclic,则该驱动禁止再用此通道承载新的 memcpy 请求 */
if (chan->desc && chan->desc->cyclic) {
dev_err(chan2dev(chan),
"Request not allowed when dma in cyclic mode\n");
return NULL;
}

/* 链表节点数:每个节点最大承载 STM32_MDMA_MAX_BLOCK_LEN 字节 */
count = DIV_ROUND_UP(len, STM32_MDMA_MAX_BLOCK_LEN);

/* 分配驱动描述符,并为每个节点从 DMA pool 申请硬件描述符缓冲 */
desc = stm32_mdma_alloc_desc(chan, count);
if (!desc)
return NULL;

/* 读取当前硬件寄存器值并基于其“清理/重建”;CCR 需要确保 EN=0(未使能) */
ccr = stm32_mdma_read(dmadev, STM32_MDMA_CCR(chan->id)) & ~STM32_MDMA_CCR_EN;
ctcr = stm32_mdma_read(dmadev, STM32_MDMA_CTCR(chan->id));
ctbr = stm32_mdma_read(dmadev, STM32_MDMA_CTBR(chan->id));
cbndtr = stm32_mdma_read(dmadev, STM32_MDMA_CBNDTR(chan->id));

/* 使能必要的异常中断并清除与 memcpy 无关的位域(优先级/异常屏蔽/IRQ 选择等) */
ccr &= ~(STM32_MDMA_CCR_WEX | STM32_MDMA_CCR_HEX |
STM32_MDMA_CCR_BEX | STM32_MDMA_CCR_PL_MASK |
STM32_MDMA_CCR_IRQ_MASK);
ccr |= STM32_MDMA_CCR_TEIE;

/* 设置软件请求模式 + 源/目的地址自增;同时清理触发模式、宽度、突发、偏移等位域 */
ctcr &= ~(STM32_MDMA_CTCR_BWM | STM32_MDMA_CTCR_TRGM_MSK |
STM32_MDMA_CTCR_PAM_MASK | STM32_MDMA_CTCR_PKE |
STM32_MDMA_CTCR_TLEN_MSK | STM32_MDMA_CTCR_DBURST_MASK |
STM32_MDMA_CTCR_SBURST_MASK | STM32_MDMA_CTCR_DINCOS_MASK |
STM32_MDMA_CTCR_SINCOS_MASK | STM32_MDMA_CTCR_DSIZE_MASK |
STM32_MDMA_CTCR_SSIZE_MASK | STM32_MDMA_CTCR_DINC_MASK |
STM32_MDMA_CTCR_SINC_MASK);
ctcr |= STM32_MDMA_CTCR_SWRM |
STM32_MDMA_CTCR_SINC(STM32_MDMA_INC) |
STM32_MDMA_CTCR_DINC(STM32_MDMA_INC);

/* 清空硬件触发选择(TSEL),确保走软件请求/链表触发语义 */
ctbr &= ~STM32_MDMA_CTBR_TSEL_MASK;

/* 选择源/目的侧总线类型(依据地址范围:AHB/AXI/TCM 等,由 set_bus 内部判定/编码) */
stm32_mdma_set_bus(dmadev, &ctbr, STM32_MDMA_CTBR_SBUS, src);
stm32_mdma_set_bus(dmadev, &ctbr, STM32_MDMA_CTBR_DBUS, dest);

/* 清理 CBNDTR 中与块/缓冲/重复相关的字段,准备重新写 BNDT/BRC 等 */
cbndtr &= ~(STM32_MDMA_CBNDTR_BRC_MK | STM32_MDMA_CBNDTR_BRDUM |
STM32_MDMA_CBNDTR_BRSUM | STM32_MDMA_CBNDTR_BNDT_MASK);

/* -------- 路径 1:len 不超过单个“块”的最大长度,可用单硬件描述符完成 -------- */
if (len <= STM32_MDMA_MAX_BLOCK_LEN) {

/* BNDT:本次块内需要搬运的字节数 */
cbndtr |= STM32_MDMA_CBNDTR_BNDT(len);

/* 根据 len 是否超过“缓冲最大长度”选择 BUFFER 或 BLOCK 触发模式 */
if (len <= STM32_MDMA_MAX_BUF_LEN) {
/* 缓冲传输:通常以“传输完成”中断作为主要完成通知 */
ccr |= STM32_MDMA_CCR_TCIE | STM32_MDMA_CCR_CTCIE;
ctcr |= STM32_MDMA_CTCR_TRGM(STM32_MDMA_BUFFER);
} else {
/* 块传输:以“块完成”中断推进(配合 CTCIE 用于链表/上下文完成语义) */
ccr |= STM32_MDMA_CCR_BTIE | STM32_MDMA_CCR_CTCIE;
ctcr |= STM32_MDMA_CTCR_TRGM(STM32_MDMA_BLOCK);
}

/* TLEN:配置缓冲长度参数;此处固定取 MAX_BUF_LEN 并按硬件定义写 (tlen-1) */
tlen = STM32_MDMA_MAX_BUF_LEN;
ctcr |= STM32_MDMA_CTCR_TLEN((tlen - 1));

/* -------- 计算并配置源侧最优宽度与突发 -------- */
max_width = stm32_mdma_get_max_width(src, len, tlen); /* 可行最大宽度(考虑对齐与长度) */
src_bus_width = stm32_mdma_get_width(chan, max_width); /* 宽度枚举 -> 硬件编码 */

max_burst = tlen / max_width; /* 在 tlen 约束下的最大 burst beat 数 */
best_burst = stm32_mdma_get_best_burst(len, tlen, max_burst, max_width);
mdma_burst = ilog2(best_burst); /* 硬件 burst 字段用 log2 编码 */

ctcr |= STM32_MDMA_CTCR_SBURST(mdma_burst) |
STM32_MDMA_CTCR_SSIZE(src_bus_width) |
STM32_MDMA_CTCR_SINCOS(src_bus_width);

/* -------- 计算并配置目的侧最优宽度与突发 -------- */
max_width = stm32_mdma_get_max_width(dest, len, tlen);
dst_bus_width = stm32_mdma_get_width(chan, max_width);

max_burst = tlen / max_width;
best_burst = stm32_mdma_get_best_burst(len, tlen, max_burst, max_width);
mdma_burst = ilog2(best_burst);

ctcr |= STM32_MDMA_CTCR_DBURST(mdma_burst) |
STM32_MDMA_CTCR_DSIZE(dst_bus_width) |
STM32_MDMA_CTCR_DINCOS(dst_bus_width);

/* 若源/目的宽度不同,启用打包/解包,使硬件完成宽度转换 */
if (dst_bus_width != src_bus_width)
ctcr |= STM32_MDMA_CTCR_PKE;

/* -------- 填充 node[0] 的硬件描述符(单段直接写字段) -------- */
hwdesc = desc->node[0].hwdesc;
hwdesc->ctcr = ctcr; /* CTCR:触发/宽度/突发/增量/打包等综合配置 */
hwdesc->cbndtr = cbndtr; /* CBNDTR:块长度等 */
hwdesc->csar = src; /* 源地址 */
hwdesc->cdar = dest; /* 目的地址 */
hwdesc->cbrur = 0; /* 块重复相关寄存器:memcpy 路径不使用 */
hwdesc->clar = 0; /* 链表地址寄存器:单段路径不使用 */
hwdesc->ctbr = ctbr; /* 总线/触发选择等 */
hwdesc->cmar = 0; /* 掩码地址:memcpy 路径不使用 */
hwdesc->cmdr = 0; /* 掩码数据:memcpy 路径不使用 */

/* 调试:打印硬件描述符内容(不影响功能) */
stm32_mdma_dump_hwdesc(chan, &desc->node[0]);

} else {
/* -------- 路径 2:len 超过单块上限,改用链表(LINKED_LIST)模式 -------- */

/* 配置链表触发模式,并把 TLEN 固定为 MAX_BUF_LEN(写入 tlen-1) */
ctcr |= STM32_MDMA_CTCR_TRGM(STM32_MDMA_LINKED_LIST) |
STM32_MDMA_CTCR_TLEN((STM32_MDMA_MAX_BUF_LEN - 1));

/* 链表模式一般用“块完成”中断推进节点,同时保留 CTCIE 作为上下文完成通知 */
ccr |= STM32_MDMA_CCR_BTIE | STM32_MDMA_CCR_CTCIE;

tlen = STM32_MDMA_MAX_BUF_LEN;

/* 对每个节点计算 xfer_count(<= MAX_BLOCK_LEN),并调用 setup_hwdesc 生成对应 hwdesc */
for (i = 0, offset = 0; offset < len; i++, offset += xfer_count) {

/* 本节点搬运的字节数:最后一个节点可能不足 MAX_BLOCK_LEN */
xfer_count = min_t(size_t, len - offset, STM32_MDMA_MAX_BLOCK_LEN);

/* 注意:这里复用同一套“宽度/突发”选择逻辑,为链表节点配置 CTCR */
max_width = stm32_mdma_get_max_width(src, len, tlen);
src_bus_width = stm32_mdma_get_width(chan, max_width);

max_burst = tlen / max_width;
best_burst = stm32_mdma_get_best_burst(len, tlen, max_burst, max_width);
mdma_burst = ilog2(best_burst);

ctcr |= STM32_MDMA_CTCR_SBURST(mdma_burst) |
STM32_MDMA_CTCR_SSIZE(src_bus_width) |
STM32_MDMA_CTCR_SINCOS(src_bus_width);

max_width = stm32_mdma_get_max_width(dest, len, tlen);
dst_bus_width = stm32_mdma_get_width(chan, max_width);

max_burst = tlen / max_width;
best_burst = stm32_mdma_get_best_burst(len, tlen, max_burst, max_width);
mdma_burst = ilog2(best_burst);

ctcr |= STM32_MDMA_CTCR_DBURST(mdma_burst) |
STM32_MDMA_CTCR_DSIZE(dst_bus_width) |
STM32_MDMA_CTCR_DINCOS(dst_bus_width);

if (dst_bus_width != src_bus_width)
ctcr |= STM32_MDMA_CTCR_PKE;

/* 生成第 i 个节点硬件描述符,并设置是否为首/末节点(末节点决定链表终止语义) */
stm32_mdma_setup_hwdesc(chan, desc, DMA_MEM_TO_MEM, i,
src + offset, dest + offset,
xfer_count, ctcr, ctbr,
i == count - 1, i == 0, false);
}
}

/* 把计算后的 CCR 写入描述符,后续 start_transfer 会把它写回硬件寄存器 */
desc->ccr = ccr;

/* 明确标记为非 cyclic(memcpy 事务语义是一次性) */
desc->cyclic = false;

/* 交给 virt-dma:建立 cookie、挂入待执行队列,并返回通用 tx 描述符给上层 */
return vchan_tx_prep(&chan->vchan, &desc->vdesc, flags);
}

stm32_mdma_start_transfer / stm32_mdma_issue_pending: 从 virt-dma 队列取出描述符并编程寄存器启动 MDMA 传输

stm32_mdma_issue_pending() 是 DMAengine 的“提交触发点”:它在持有 chan->vchan.lock(并关闭本地中断)的条件下,判断 virt-dma 队列里是否存在可发起的事务;若当前通道既没有正在执行的 chan->descchan->busy == false,则调用 stm32_mdma_start_transfer() 将队列头部描述符取出并启动硬件。

stm32_mdma_start_transfer() 则完成“从软件队列到硬件寄存器”的关键转换:

  • virt_dma_chan 的待执行队列取出下一个 virt_dma_desc
  • 转换为驱动私有的 stm32_mdma_desc,取首个硬件描述符 node[0].hwdesc
  • 将描述符中的寄存器快照写入 MDMA 通道寄存器(CCR/CTCR/CBNDTR/CSAR/CDAR/…)
  • 清理残留中断状态后使能通道(EN),若是 MEM2MEM 软件请求模式则额外置位 SWRQ
  • chan->busy = true,表示硬件侧已开始执行
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
/**
* @brief 从 virt-dma 队列取出下一个描述符,编程 MDMA 通道寄存器并启动传输
* @param[in,out] chan STM32 MDMA 驱动私有通道对象
*
* 设计要点:
* 1) 该函数假设调用方已经在合适的锁保护下(通常是持有 chan->vchan.lock)。
* 2) 只使用 desc->node[0] 的硬件描述符作为“首段寄存器装载模板”。
* 3) 对于 MEM2MEM + SWRM(软件请求模式),在 EN 后额外触发一次 SWRQ。
*/
static void stm32_mdma_start_transfer(struct stm32_mdma_chan *chan)
{
/* 通过通道对象反查控制器实例,用于寄存器读写封装 */
struct stm32_mdma_device *dmadev = stm32_mdma_get_dev(chan);
/* virt-dma 的通用描述符基类指针,用于从 vchan 队列取出事务 */
struct virt_dma_desc *vdesc;
/* 指向首个硬件描述符(node[0]),其字段对应通道寄存器装载值 */
struct stm32_mdma_hwdesc *hwdesc;
/* 通道硬件编号,用于计算各寄存器偏移 */
u32 id = chan->id;
/* status:用于读取/清理通道中断状态;reg:临时寄存器地址 */
u32 status, reg;

/* 获取 vchan 队列中的下一个待执行描述符(不为空才可启动) */
vdesc = vchan_next_desc(&chan->vchan);
if (!vdesc) {
/* 队列为空:明确表示当前无正在执行的描述符 */
chan->desc = NULL;
return;
}

/* 从 vchan 队列摘除该描述符节点,避免被重复启动 */
list_del(&vdesc->node);

/* 将 virt-dma 描述符转换为驱动私有描述符,获得硬件描述符数组 */
chan->desc = to_stm32_mdma_desc(vdesc);
/* 取本次事务的首段硬件描述符,作为寄存器初始化模板 */
hwdesc = chan->desc->node[0].hwdesc;
/* 当前正在执行的硬件描述符索引初始化为 0(首段) */
chan->curr_hwdesc = 0;

/* 依次写入通道控制寄存器快照:这些值由 prep 阶段构造并固化在描述符中 */
stm32_mdma_write(dmadev, STM32_MDMA_CCR(id), chan->desc->ccr); /* CCR:中断/优先级/使能等 */
stm32_mdma_write(dmadev, STM32_MDMA_CTCR(id), hwdesc->ctcr); /* CTCR:触发模式/宽度/burst/增量等 */
stm32_mdma_write(dmadev, STM32_MDMA_CBNDTR(id), hwdesc->cbndtr); /* CBNDTR:块长度/重复等 */
stm32_mdma_write(dmadev, STM32_MDMA_CSAR(id), hwdesc->csar); /* CSAR:源地址 */
stm32_mdma_write(dmadev, STM32_MDMA_CDAR(id), hwdesc->cdar); /* CDAR:目的地址 */
stm32_mdma_write(dmadev, STM32_MDMA_CBRUR(id), hwdesc->cbrur); /* CBRUR:块重复/更新寄存器 */
stm32_mdma_write(dmadev, STM32_MDMA_CLAR(id), hwdesc->clar); /* CLAR:链表地址/链表控制相关 */
stm32_mdma_write(dmadev, STM32_MDMA_CTBR(id), hwdesc->ctbr); /* CTBR:触发选择/总线选择等 */
stm32_mdma_write(dmadev, STM32_MDMA_CMAR(id), hwdesc->cmar); /* CMAR:掩码地址(如启用掩码功能) */
stm32_mdma_write(dmadev, STM32_MDMA_CMDR(id), hwdesc->cmdr); /* CMDR:掩码数据(如启用掩码功能) */

/* 读取通道中断状态寄存器,若存在残留状态则写入 IFCR 清除,避免“上次遗留中断”污染本次传输 */
status = stm32_mdma_read(dmadev, STM32_MDMA_CISR(id));
if (status)
stm32_mdma_set_bits(dmadev, STM32_MDMA_CIFCR(id), status);

/* 调试:打印寄存器快照,辅助定位启动前寄存器装载是否正确 */
stm32_mdma_dump_reg(chan);

/* 置位 EN 启动 DMA 通道:此时硬件开始按已装载寄存器运行 */
stm32_mdma_set_bits(dmadev, STM32_MDMA_CCR(id), STM32_MDMA_CCR_EN);

/* 若配置为软件请求模式(SWRM),则在启动后额外触发一次 SWRQ,典型用于 MEM2MEM 启动触发 */
if (hwdesc->ctcr & STM32_MDMA_CTCR_SWRM) {
reg = STM32_MDMA_CCR(id);
stm32_mdma_set_bits(dmadev, reg, STM32_MDMA_CCR_SWRQ);
}

/* 标记通道忙:用于避免 issue_pending 重入时重复启动 */
chan->busy = true;

/* 调试:记录该虚拟通道已启动 */
dev_dbg(chan2dev(chan), "vchan %p: started\n", &chan->vchan);
}

/**
* @brief DMAengine 回调:提交 pending 描述符,必要时启动硬件
* @param[in] c DMAengine 抽象通道指针
*
* 并发语义:
* - 使用 spin_lock_irqsave 保护 vchan 队列与 chan->desc/busy 状态,避免与同核 IRQ/下半部并发。
* - vchan_issue_pending() 用于推进 virt-dma 的“pending -> issued”内部状态。
*/
static void stm32_mdma_issue_pending(struct dma_chan *c)
{
/* 转换为驱动私有通道对象 */
struct stm32_mdma_chan *chan = to_stm32_mdma_chan(c);
/* 保存并恢复中断标志,用于在锁持有期间屏蔽本地中断 */
unsigned long flags;

/* 进入临界区:保护 vchan 队列与启动状态,同时关本地中断避免与 IRQ 处理路径竞态 */
spin_lock_irqsave(&chan->vchan.lock, flags);

/* 若没有可推进的 pending 事务,则无需进一步动作 */
if (!vchan_issue_pending(&chan->vchan))
goto end;

/* 调试:记录已经“提交/发起” */
dev_dbg(chan2dev(chan), "vchan %p: issued\n", &chan->vchan);

/* 仅当当前没有正在执行的描述符且硬件不忙时,才从队列取出一个描述符并启动 */
if (!chan->desc && !chan->busy)
stm32_mdma_start_transfer(chan);

end:
/* 退出临界区:恢复中断并释放自旋锁 */
spin_unlock_irqrestore(&chan->vchan.lock, flags);
}

stm32_mdma_irq_handler: 处理 STM32 MDMA 通道中断并驱动事务推进与错误清理

这个中断处理函数完成三类工作:

  1. 定位触发中断的通道:读取全局中断状态 GISR0,用 __ffs(status) 找到最低位的置位通道号 id,并据此取得 dmadev->chan[id]。这里隐含一个重要约束:一次只处理一个通道(最低位优先),其余通道若也置位,会留到后续中断再次处理。

  2. 按“实际使能的中断源”过滤伪中断:读取通道中断状态 CISR,并把 CRQA 位屏蔽掉(MEM2MEM 场景可能置位但不代表需要处理);再从 CCR 中提取本通道的“中断使能集合” ien,用 if (!(status & ien)) 判定是否为伪中断并快速退出。这一步避免了因为状态位残留或硬件噪声导致的无意义处理。

  3. 逐位清中断并推进状态机:对 TEIF/CTCIF/BRTIF/BTIF/TCIF 等位分别写 CIFCR 清除;其中 CTCIF 会调用 stm32_mdma_xfer_end(chan) 推进一次传输结束路径,而 BTIF 在 cyclic 场景会推进 curr_hwdesc 并触发 vchan_cyclic_callback()。最后如果仍有未知/未处理状态位残留,则统一清除并打印错误,且在 CCR.EN 已被硬件清掉时额外报告“通道被硬件关闭”。

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
/**
* @brief STM32 MDMA 全局中断入口:识别通道、清除中断源、推进传输完成/循环回调并处理错误
* @param[in] irq 中断号(未直接使用,保留以符合 irq_handler 原型)
* @param[in] devid 注册中断时传入的私有指针,这里约定为 struct stm32_mdma_device *
* @return IRQ_HANDLED 表示已处理;IRQ_NONE 表示伪中断/无状态
*/
static irqreturn_t stm32_mdma_irq_handler(int irq, void *devid)
{
/* 取回控制器实例指针:包含寄存器基址与所有通道对象数组 */
struct stm32_mdma_device *dmadev = devid;
/* 当前触发中断的通道指针 */
struct stm32_mdma_chan *chan;
/* reg:通道中断清除寄存器地址;id:通道号;ccr:通道控制寄存器快照;ien:有效中断使能位集合;status:状态位 */
u32 reg, id, ccr, ien, status;

/* 读取全局中断状态:用于判断哪个通道产生中断(这里使用 GISR0 对应一组通道位图) */
status = readl_relaxed(dmadev->base + STM32_MDMA_GISR0);
if (!status) {
/* 全局状态为 0:说明没有任何通道报告中断,按伪中断处理 */
dev_dbg(mdma2dev(dmadev), "spurious it\n");
return IRQ_NONE;
}

/* 选取最低位的置位 bit 作为通道号:一次只处理一个通道(最低位优先) */
id = __ffs(status);
/* 由通道号定位到驱动私有通道对象 */
chan = &dmadev->chan[id];

/* 进入临界区:保护 vchan 队列与通道状态字段,避免与提交/启动路径并发破坏状态 */
spin_lock(&chan->vchan.lock);

/* 读取该通道的中断状态寄存器 */
status = stm32_mdma_read(dmadev, STM32_MDMA_CISR(id));
/* 屏蔽 CRQA:该位在 MEM2MEM 场景可能置位,但不作为需要处理的“事件源” */
status &= ~STM32_MDMA_CISR_CRQA;

/* 读取通道控制寄存器,用于提取中断使能集合(只处理已使能的事件) */
ccr = stm32_mdma_read(dmadev, STM32_MDMA_CCR(id));
/* 从 CCR 的 IRQ_MASK 字段提取“使能的中断位集合”,并做位对齐(与 CISR 位语义匹配) */
ien = (ccr & STM32_MDMA_CCR_IRQ_MASK) >> 1;

/* 若当前状态位与使能位没有交集:认为是伪中断,退出 */
if (!(status & ien)) {
spin_unlock(&chan->vchan.lock);
/* busy 时仍出现伪中断更值得关注,因此用 warn;否则仅 debug */
if (chan->busy)
dev_warn(chan2dev(chan),
"spurious it (status=0x%04x, ien=0x%04x)\n", status, ien);
else
dev_dbg(chan2dev(chan),
"spurious it (status=0x%04x, ien=0x%04x)\n", status, ien);
return IRQ_NONE;
}

/* 计算该通道的中断清除寄存器地址:后续通过写 CIFCR 清除对应事件位 */
reg = STM32_MDMA_CIFCR(id);

/* 处理传输错误中断:读取错误状态寄存器 CESR,清除 TEIF,并从待处理状态中消去该位 */
if (status & STM32_MDMA_CISR_TEIF) {
dev_err(chan2dev(chan), "Transfer Err: stat=0x%08x\n",
readl_relaxed(dmadev->base + STM32_MDMA_CESR(id)));
stm32_mdma_set_bits(dmadev, reg, STM32_MDMA_CIFCR_CTEIF);
status &= ~STM32_MDMA_CISR_TEIF;
}

/* 处理“上下文传输完成”事件:清除 CTCIF,并进入传输结束路径(更新队列/启动下一次等由 xfer_end 决定) */
if (status & STM32_MDMA_CISR_CTCIF) {
stm32_mdma_set_bits(dmadev, reg, STM32_MDMA_CIFCR_CCTCIF);
status &= ~STM32_MDMA_CISR_CTCIF;
stm32_mdma_xfer_end(chan);
}

/* 处理“块重复传输”事件:清除 BRTIF;该事件不推进 curr_hwdesc,仅清除并消去状态位 */
if (status & STM32_MDMA_CISR_BRTIF) {
stm32_mdma_set_bits(dmadev, reg, STM32_MDMA_CIFCR_CBRTIF);
status &= ~STM32_MDMA_CISR_BRTIF;
}

/* 处理“块传输完成”事件:清除 BTIF,并推进当前硬件描述符索引;cyclic 时触发周期回调 */
if (status & STM32_MDMA_CISR_BTIF) {
stm32_mdma_set_bits(dmadev, reg, STM32_MDMA_CIFCR_CBTIF);
status &= ~STM32_MDMA_CISR_BTIF;

/* 进入下一段硬件描述符:链表/多段传输时用于定位当前执行段 */
chan->curr_hwdesc++;

/* cyclic 语义:达到末尾则回绕,并触发一次 cyclic 回调 */
if (chan->desc && chan->desc->cyclic) {
if (chan->curr_hwdesc == chan->desc->count)
chan->curr_hwdesc = 0;
vchan_cyclic_callback(&chan->desc->vdesc);
}
}

/* 处理“末级传输完成”事件:清除 TCIF,并消去状态位(具体完成语义由驱动组合中断方式决定) */
if (status & STM32_MDMA_CISR_TCIF) {
stm32_mdma_set_bits(dmadev, reg, STM32_MDMA_CIFCR_CLTCIF);
status &= ~STM32_MDMA_CISR_TCIF;
}

/* 若仍存在未覆盖的状态位:统一清除并报告为错误,同时检查是否被硬件关闭 EN */
if (status) {
stm32_mdma_set_bits(dmadev, reg, status);
dev_err(chan2dev(chan), "DMA error: status=0x%08x\n", status);
if (!(ccr & STM32_MDMA_CCR_EN))
dev_err(chan2dev(chan), "chan disabled by HW\n");
}

/* 退出临界区:允许提交/启动路径继续推进 */
spin_unlock(&chan->vchan.lock);

/* 返回已处理:本函数已对通道状态与中断位做了明确处理 */
return IRQ_HANDLED;
}

这个函数实现 dma_device.device_tx_status 回调:它先用 DMAengine 通用的 dma_cookie_status() 得到 cookie 对应事务的宏观状态(例如 DMA_COMPLETE/DMA_IN_PROGRESS/DMA_ERROR 等),若事务未完成且调用者提供了 state,则在持有 chan->vchan.lock 的临界区内定位对应描述符,并调用 stm32_mdma_desc_residue() 计算剩余量,再通过 dma_set_residue() 写回到 state

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
/**
* @brief 查询指定 cookie 的 DMA 事务状态,并在需要时计算并返回剩余传输量 residue
* @param[in] c DMAengine 抽象通道指针
* @param[in] cookie 需要查询的事务 cookie
* @param[out] state 可选输出:用于返回 residue 等扩展状态;为 NULL 时仅返回 status
* @return DMAengine 定义的事务状态枚举值
*/
static enum dma_status stm32_mdma_tx_status(struct dma_chan *c,
dma_cookie_t cookie,
struct dma_tx_state *state)
{
/* 转换为驱动私有通道对象,用于访问 vchan/desc/curr_hwdesc 等状态 */
struct stm32_mdma_chan *chan = to_stm32_mdma_chan(c);
/* vdesc:用于在 virt-dma 队列中定位“尚未执行/已排队”的描述符 */
struct virt_dma_desc *vdesc;
/* status:DMAengine 通用 cookie 状态 */
enum dma_status status;
/* flags:保存并恢复中断标志,保证临界区内不被同核中断打断 */
unsigned long flags;
/* residue:剩余未搬运字节数,默认 0 */
u32 residue = 0;

/* 先走 DMAengine 通用状态查询:它会根据 cookie 与通道的 completed cookie 等信息得出宏观状态 */
status = dma_cookie_status(c, cookie, state);

/* 若已完成,或调用者不关心扩展信息(state==NULL),直接返回宏观状态 */
if ((status == DMA_COMPLETE) || (!state))
return status;

/* 进入临界区:防止与 IRQ/提交路径并发修改 chan->desc/curr_hwdesc 或 vchan 队列 */
spin_lock_irqsave(&chan->vchan.lock, flags);

/* 在 virt-dma 维护的队列结构中查找该 cookie 对应的描述符(通常对应“尚未开始或尚未完成”的事务) */
vdesc = vchan_find_desc(&chan->vchan, cookie);

/*
* 情况 1:cookie 正好是当前正在执行的 desc(chan->desc)的 cookie
* 此时 residue 需要结合当前执行到第几个硬件描述符(curr_hwdesc)以及硬件寄存器状态来计算
*/
if (chan->desc && cookie == chan->desc->vdesc.tx.cookie)
residue = stm32_mdma_desc_residue(chan, chan->desc,
chan->curr_hwdesc, state);

/*
* 情况 2:cookie 对应的是队列中的某个 vdesc(可能尚未启动,或处于排队等待)
* 对于未启动的描述符,通常从第 0 个硬件描述符开始计算剩余量
*/
else if (vdesc)
residue = stm32_mdma_desc_residue(chan, to_stm32_mdma_desc(vdesc),
0, state);

/* 将计算得到的剩余量写回到 dma_tx_state,供上层查询进度 */
dma_set_residue(state, residue);

/* 退出临界区:恢复中断并释放锁 */
spin_unlock_irqrestore(&chan->vchan.lock, flags);

/* 返回宏观状态(residue 作为附加信息已写入 state) */
return status;
}

stm32_mdma_desc_residue / dma_set_residue: 基于“当前硬件节点 + 当前块剩余”估算事务剩余字节数,并写回到 tx_state

stm32_mdma_desc_residue() 的目标是给 tx_status() 提供 residue(剩余未搬运字节数)。它的实现思路是“上界估计”:

  1. 用硬件寄存器 CLAR 找到当前硬件正在处理的链表节点(或下一节点指针语义,取决于硬件定义),
  2. 把“当前节点之后尚未处理的所有 hwdesc 的 BNDT”累加起来,
  3. 再加上当前硬件寄存器 CBNDTR.BNDT 表示的“当前块剩余”,
  4. 如果启用了 mem_burst,则把 residue 向上取整到 burst_size 的整数倍,避免上层看到“非突发对齐”的剩余量(该值更符合硬件实际可能的总线提交粒度)。

dma_set_residue() 只是把计算结果写入 state->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
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
/**
* @brief 计算某个 STM32 MDMA 描述符的剩余传输字节数 residue(用于 tx_status)
* @param[in] chan 私有通道对象
* @param[in] desc 私有描述符对象(包含 count 个 hwdesc 节点)
* @param[in] curr_hwdesc 当前硬件描述符索引(本实现未使用该入参,依赖 CLAR 寄存器定位)
* @param[out] state tx_state 输出(本函数会写 in_flight_bytes)
* @return 估算的剩余字节数 residue
*
* 计算方法:
* - 通过读取通道 CLAR,定位“当前硬件正在处理的 hwdesc”
* - 累加尚未处理的各 hwdesc 的 BNDT
* - 加上当前 CBNDTR.BNDT 作为当前块的剩余
* - 如启用 mem_burst,则把 residue 向上取整到 burst_size 的整数倍
*/
static size_t stm32_mdma_desc_residue(struct stm32_mdma_chan *chan,
struct stm32_mdma_desc *desc,
u32 curr_hwdesc,
struct dma_tx_state *state)
{
/* 由通道对象获取控制器实例,用于寄存器访问 */
struct stm32_mdma_device *dmadev = stm32_mdma_get_dev(chan);
/* hwdesc:循环中指向某个节点的硬件描述符 */
struct stm32_mdma_hwdesc *hwdesc;
/* cisr:通道中断/状态寄存器快照;clar:硬件当前链表地址寄存器;cbndtr:当前块剩余寄存器 */
u32 cisr, clar, cbndtr;
/* residue:累计的剩余字节数;modulo:用于突发对齐取模;burst_size:一次突发提交的字节粒度 */
u32 residue, modulo, burst_size;
/* i:用于从后向前扫描描述符节点 */
int i;

/* 读取通道状态寄存器:用于后续判断 CRQA 等状态位 */
cisr = stm32_mdma_read(dmadev, STM32_MDMA_CISR(chan->id));

/* 初始化 residue 为 0,后续逐步累加 */
residue = 0;

/*
* 读取 CLAR:用于识别硬件当前正在处理(或将要处理)的链表节点标识。
* 驱动用 “hwdesc->clar == clar” 来匹配当前节点。
*/
clar = stm32_mdma_read(dmadev, STM32_MDMA_CLAR(chan->id));

/*
* 从最后一个节点向前扫描:
* - 一旦遇到 hwdesc->clar 与硬件 CLAR 相等,认为找到了“当前传输节点”,停止累加
* - 在此之前遇到的节点(位于当前节点之后的未处理节点)其 BNDT 被视为“尚未处理的剩余量”,加入 residue
*
* 这种“从尾到头累加直到匹配”实现的是:把当前节点之后所有未处理节点的长度求和。
*/
for (i = desc->count - 1; i >= 0; i--) {
/* 取得第 i 个节点的硬件描述符 */
hwdesc = desc->node[i].hwdesc;

/* 若该节点的 clar 与硬件寄存器 CLAR 相等,则认为当前节点已找到,停止累加 */
if (hwdesc->clar == clar)
break; /* 找到当前传输节点,停止累计未处理节点 */

/* 累加未处理节点的 BNDT(块内传输长度字段) */
residue += STM32_MDMA_CBNDTR_BNDT(hwdesc->cbndtr);
}

/*
* 读取当前 CBNDTR:其 BNDT 字段表示“当前块内尚未传输完成的字节数”
* 将它加入 residue,得到“当前块剩余 + 后续未处理节点总和”的估计。
*/
cbndtr = stm32_mdma_read(dmadev, STM32_MDMA_CBNDTR(chan->id));
residue += cbndtr & STM32_MDMA_CBNDTR_BNDT_MASK;

/* 默认 in_flight_bytes 为 0;后续在特定 MEM2MEM 硬件请求场景下会更新 */
state->in_flight_bytes = 0;

/*
* 若启用了 m2m_hw(memcpy 使用硬件请求线)且 CRQA 置位:
* 认为当前仍有“请求活跃”的 in-flight 字节,这里用当前块 BNDT 作为 in-flight_bytes 的估计值。
*/
if (chan->chan_config.m2m_hw && (cisr & STM32_MDMA_CISR_CRQA))
state->in_flight_bytes = cbndtr & STM32_MDMA_CBNDTR_BNDT_MASK;

/*
* 若未配置 mem_burst,则不做“突发粒度向上取整”,直接返回按 BNDT 计算的 residue。
* 该返回值更接近“字节级剩余”,但可能不符合硬件 burst 提交的实际粒度。
*/
if (!chan->mem_burst)
return residue;

/* burst_size:一次突发提交的总字节数 = burst beats * 每 beat 字节宽度 */
burst_size = chan->mem_burst * chan->mem_width;

/* 计算 residue 对 burst_size 的取模 */
modulo = residue % burst_size;

/*
* 若 residue 不是 burst_size 的整数倍,则向上取整到下一个 burst_size 边界:
* 这样上层看到的 residue 不会低估“硬件按突发提交导致的最小剩余粒度”。
*/
if (modulo)
residue = residue - modulo + burst_size;

/* 返回按突发粒度对齐后的剩余字节数 */
return residue;
}

/**
* @brief 将计算得到的 residue 写入 dma_tx_state
* @param[out] state tx_state 指针(允许为 NULL)
* @param[in] residue 剩余字节数
*
* 这是一个纯赋值函数:不做锁、不做计算,仅写 state->residue。
*/
static inline void dma_set_residue(struct dma_tx_state *state, u32 residue)
{
/* 若调用者提供了 state,则更新 residue 字段 */
if (state)
state->residue = residue;
}

stm32_mdma_terminate_all: 终止通道上所有事务,停止硬件并回收队列中的全部描述符

这个函数实现 dma_device.device_terminate_all 回调:它的语义是强制清空该 DMA 通道上的事务状态——包括正在执行的事务(chan->desc)以及 virt-dma 队列里尚未执行/已排队的事务。实现上分成两段:

  • 锁内:把“正在执行的 vdesc”标记终止、必要时停硬件、把队列里的描述符全部摘到本地链表 head
  • 锁外:统一释放 head 上的描述符对象,避免在自旋锁/关中断的临界区内做潜在的慢操作或可能睡眠的释放路径
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
/**
* @brief 强制终止该通道上的所有 DMA 事务:停止正在执行的传输并释放队列中全部描述符
* @param[in] c DMAengine 抽象通道指针
* @return 始终返回 0(DMAengine terminate_all 通常约定成功/失败由驱动自行定义,这里固定成功)
*
* 并发语义:
* - 锁内完成“状态切断”:终止当前 vdesc、停止硬件、摘取所有队列描述符到本地链表
* - 锁外完成“资源释放”:避免在自旋锁/关中断临界区内做释放工作
*/
static int stm32_mdma_terminate_all(struct dma_chan *c)
{
/* 转换为驱动私有通道对象 */
struct stm32_mdma_chan *chan = to_stm32_mdma_chan(c);
/* 保存并恢复中断标志,确保临界区内不被同核 IRQ 打断 */
unsigned long flags;
/* 本地链表头:用于暂存从 vchan 队列摘下来的所有描述符,锁外统一释放 */
LIST_HEAD(head);

/* 进入临界区:保护 chan->desc/busy 与 vchan 队列结构 */
spin_lock_irqsave(&chan->vchan.lock, flags);

/* 若存在当前正在执行的描述符,则先终止它并在需要时停止硬件 */
if (chan->desc) {
/* 将当前 vdesc 标记为终止:virt-dma 会更新 cookie/状态并做必要的队列处理 */
vchan_terminate_vdesc(&chan->desc->vdesc);

/* 若硬件正在忙,则主动停止 MDMA 通道,避免硬件继续写内存 */
if (chan->busy)
stm32_mdma_stop(chan);

/* 切断驱动对“当前描述符”的引用,避免后续路径误以为仍在执行 */
chan->desc = NULL;
}

/*
* 将 vchan 队列中所有剩余描述符全部摘取到本地 head 链表:
* - 这些通常是已排队但尚未启动的事务
* - 通过“摘取”把它们从 vchan 内部结构移出,后续可在锁外安全释放
*/
vchan_get_all_descriptors(&chan->vchan, &head);

/* 退出临界区:恢复中断并释放锁 */
spin_unlock_irqrestore(&chan->vchan.lock, flags);

/*
* 锁外释放:逐个释放 head 上的描述符
* 释放过程中会调用你之前关注的 chan->vchan.desc_free(例如 stm32_mdma_desc_free)
* 从而释放硬件描述符池对象/链表节点/子结构等。
*/
vchan_dma_desc_free_list(&chan->vchan, &head);

/* 固定返回 0:表示 terminate_all 执行完成 */
return 0;
}