[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

static int stm32_dma_slave_config(struct dma_chan *c,
struct dma_slave_config *config)
{
struct stm32_dma_chan *chan = to_stm32_dma_chan(c);

memcpy(&chan->dma_sconfig, config, sizeof(*config));

/* Check if user is requesting DMA to trigger STM32 MDMA */
if (config->peripheral_size) {
config->peripheral_config = &chan->mdma_config;
config->peripheral_size = sizeof(chan->mdma_config);
chan->trig_mdma = true;
}

chan->config_init = true;

return 0;
}

static int stm32_dma_probe(struct platform_device *pdev)
{
struct stm32_dma_chan *chan; // DMA通道的软件描述
struct stm32_dma_device *dmadev; // 整个DMA设备的软件描述
struct dma_device *dd; // 指向 dmadev 中内嵌的通用 dma_device 结构
struct resource *res; // 用于接收内存资源信息
struct reset_control *rst; // 复位控制器句柄
int i, ret; // 循环变量和返回值

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

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

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

// 获取DMA控制器的时钟
dmadev->clk = devm_clk_get(&pdev->dev, NULL);
if (IS_ERR(dmadev->clk))
return dev_err_probe(&pdev->dev, PTR_ERR(dmadev->clk), "Can't get clock\n");

// 准备并使能时钟
ret = clk_prepare_enable(dmadev->clk);
if (ret < 0) {
dev_err(&pdev->dev, "clk_prep_enable error: %d\n", ret);
return ret;
}

// 从设备树读取 "st,mem2mem" 属性, 判断此DMA是否支持内存到内存的传输
dmadev->mem2mem = of_property_read_bool(pdev->dev.of_node,
"st,mem2mem");

// 获取复位控制器句柄
rst = devm_reset_control_get(&pdev->dev, NULL);
if (IS_ERR(rst)) {
ret = PTR_ERR(rst);
// 如果是因为依赖的复位控制器驱动未就绪, 则推迟探测
if (ret == -EPROBE_DEFER)
goto clk_free;
} else {
// 执行硬件复位: 拉低复位线, 延时, 释放复位线
reset_control_assert(rst);
udelay(2);
reset_control_deassert(rst);
}

// 设置DMA传输的最大段大小
dma_set_max_seg_size(&pdev->dev, STM32_DMA_ALIGNED_MAX_DATA_ITEMS);

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

dd->src_addr_widths = BIT(DMA_SLAVE_BUSWIDTH_1_BYTE) | // 支持的源数据宽度
BIT(DMA_SLAVE_BUSWIDTH_2_BYTES) |
BIT(DMA_SLAVE_BUSWIDTH_4_BYTES);
dd->dst_addr_widths = BIT(DMA_SLAVE_BUSWIDTH_1_BYTE) | // 支持的目标数据宽度
BIT(DMA_SLAVE_BUSWIDTH_2_BYTES) |
BIT(DMA_SLAVE_BUSWIDTH_4_BYTES);
dd->directions = BIT(DMA_DEV_TO_MEM) | BIT(DMA_MEM_TO_DEV); // 支持的传输方向
dd->residue_granularity = DMA_RESIDUE_GRANULARITY_BURST;
dd->copy_align = DMAENGINE_ALIGN_32_BYTES;
dd->max_burst = STM32_DMA_MAX_BURST;
dd->max_sg_burst = STM32_DMA_ALIGNED_MAX_DATA_ITEMS;
dd->descriptor_reuse = true; // 支持描述符重用
dd->dev = &pdev->dev;
INIT_LIST_HEAD(&dd->channels); // 初始化通道链表

// 如果硬件支持内存到内存, 则添加相应能力和操作函数
if (dmadev->mem2mem) {
dma_cap_set(DMA_MEMCPY, dd->cap_mask);
dd->device_prep_dma_memcpy = stm32_dma_prep_dma_memcpy;
dd->directions |= BIT(DMA_MEM_TO_MEM);
}

// 循环初始化每个DMA通道的软件结构
for (i = 0; i < STM32_DMA_MAX_CHANNELS; i++) {
chan = &dmadev->chan[i];
chan->id = i;
chan->vchan.desc_free = stm32_dma_desc_free;
vchan_init(&chan->vchan, dd); // 初始化虚拟通道

// 预计算该通道的中断清除寄存器地址和标志位, 提高运行时效率
chan->mdma_config.ifcr = res->start;
chan->mdma_config.ifcr += STM32_DMA_IFCR(chan->id);

chan->mdma_config.tcf = STM32_DMA_TCI;
chan->mdma_config.tcf <<= STM32_DMA_FLAGS_SHIFT(chan->id);
}

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

// 为每个DMA通道获取并注册中断处理函数
for (i = 0; i < STM32_DMA_MAX_CHANNELS; i++) {
chan = &dmadev->chan[i];
ret = platform_get_irq(pdev, i); // 从设备树获取中断号
if (ret < 0)
goto err_unregister;
chan->irq = ret;

// 请求中断, 将 stm32_dma_chan_irq 注册为中断服务程序
ret = devm_request_irq(&pdev->dev, chan->irq,
stm32_dma_chan_irq, 0,
dev_name(chan2dev(chan)), chan);
if (ret) {
dev_err(&pdev->dev,
"request_irq failed with err %d channel %d\n",
ret, i);
goto err_unregister;
}
}

// 将此DMA控制器注册为设备树的DMA提供者
ret = of_dma_controller_register(pdev->dev.of_node,
stm32_dma_of_xlate, dmadev);
if (ret < 0) {
dev_err(&pdev->dev,
"STM32 DMA DMA OF registration failed %d\n", ret);
goto err_unregister;
}

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

// 初始化并使能运行时电源管理 (Runtime PM)
pm_runtime_set_active(&pdev->dev);
pm_runtime_enable(&pdev->dev);
pm_runtime_get_noresume(&pdev->dev);
pm_runtime_put(&pdev->dev);

dev_info(&pdev->dev, "STM32 DMA driver registered\n");

return 0;

err_unregister:
// 错误处理: 如果后续步骤失败, 注销已注册的DMA设备
dma_async_device_unregister(dd);
clk_free:
// 错误处理: 关闭并释放时钟
clk_disable_unprepare(dmadev->clk);

return ret;
}

STM32 DMA 驱动的注册与初始化

此代码片段是STM32 DMA驱动程序的入口点。它的核心作用不是执行DMA传输, 而是将整个DMA驱动程序作为一个platform_driver注册到Linux内核的设备模型中。这使得内核能够识别并管理STM32的DMA控制器硬件。

其工作原理如下:

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

在STM32H750系统上, __init宏会将stm32_dma_init函数放入一个特殊的内存段, 这段内存在内核启动完成后可以被释放, 从而为内存资源有限的MCU节省宝贵的RAM。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
static const struct of_device_id stm32_dma_of_match[] = {
{ .compatible = "st,stm32-dma", },
{ /* sentinel */ },
};
MODULE_DEVICE_TABLE(of, stm32_dma_of_match);

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

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

/*
* 驱动的初始化函数. __init 宏告诉编译器将此函数放入特殊的 ".init.text" 段.
* 在内核启动完成后, 这段内存可以被释放, 以节省RAM.
*/
static int __init stm32_dma_init(void)
{
/*
* 调用 platform_driver_register 函数, 将 stm32_dma_driver 注册到内核的平台总线核心中.
* 注册成功后, 内核就会开始为这个驱动寻找匹配的设备.
* 函数返回注册操作的结果 (成功为0, 失败为负错误码).
*/
return platform_driver_register(&stm32_dma_driver);
}
/*
* subsys_initcall 是一个宏, 它将 stm32_dma_init 函数的地址放入一个特殊的函数指针列表.
* 内核在启动过程中, 会按照预定的顺序 (core_initcall, subsys_initcall, device_initcall等)
* 依次调用这些列表中的函数.
* subsys_initcall 确保了这个DMA驱动在核心子系统都初始化完毕后, 能够较早地被注册.
*/
subsys_initcall(stm32_dma_init);

好的,我将对您提供的这段关于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的潜力发挥到极致, 设计出真正高性能、低功耗的嵌入式系统。