4. DMA驱动开发环境搭建:内核源码树配置、设备树DMA节点编写、编译与加载DMA测试模块
搭建一个可复现、可调试的DMA驱动开发环境,是后续编写缓冲区分配与传输优化代码的基础。本章将引导你完成从内核源码树配置、设备树DMA节点编写,到编译并加载最小DMA测试模块的完整流程。
4.1 内核源码树配置与编译
DMA驱动开发需要内核支持 DMA Engine 框架以及对应的 DMA控制器驱动。以下步骤基于Linux 5.10 LTS内核,适用于ARM64和x86_64平台。
4.1.1 获取并解压内核源码
# 下载主线内核
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.10.210.tar.xz
tar -xf linux-5.10.210.tar.xz
cd linux-5.10.210
4.1.2 配置内核选项(DMA Engine相关)
使用 make menuconfig 或 make defconfig 后,手动确认以下关键选项已开启:
| 配置项 | 路径 | 推荐值 | 说明 |
|---|---|---|---|
CONFIG_DMADEVICES |
Device Drivers → DMA Engine support | Y | 启用DMA Engine核心框架 |
CONFIG_DMA_ENGINE |
同上子项 | Y | 启用DMA Engine API |
CONFIG_DMA_OF |
同上子项 | Y | 支持设备树DMA绑定解析 |
CONFIG_PL330_DMA |
DMA Engine support → ARM PL330 DMA | Y 或 M | ARM平台常用DMA控制器(示例) |
CONFIG_DMA_ENGINE_RAID |
DMA Engine support | N | 非RAID场景建议关闭 |
提示: 若使用QEMU + vexpress-a9模拟环境,务必开启
CONFIG_PL330_DMA 和 CONFIG_DMA_OF。x86平台可开启 CONFIG_INTEL_IDMA64 作为替代。
4.1.3 编译内核与模块
# 设置交叉编译工具链(ARM64示例)
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
# 生成默认配置并手动调整
make defconfig
make menuconfig # 按上表确认DMA选项
# 编译内核与模块
make -j$(nproc) Image dtbs modules
# 安装模块到目标根文件系统
make modules_install INSTALL_MOD_PATH=/path/to/rootfs
4.2 设备树DMA节点编写
设备树(Device Tree)是描述硬件拓扑与资源的关键手段。DMA控制器及其使用者(如SPI、I2C、UART)通过 dmas 和 dma-names 属性建立连接。
4.2.1 DMA控制器节点示例(ARM PL330)
/* arch/arm/boot/dts/vexpress-v2p-ca9.dts 片段 */
amba {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
dmac: dma-controller@1e000000 {
compatible = "arm,pl330", "arm,primecell";
reg = <0x1e000000 0x1000>;
interrupts = <0 92 4>;
#dma-cells = <1>; /* 一个cell表示DMA通道号 */
dma-channels = <8>;
dma-requests = <32>;
clocks = <&oscclk>;
clock-names = "apb_pclk";
};
};
4.2.2 DMA使用者节点示例(SPI从DMA)
&spi0 {
status = "okay";
compatible = "arm,pl022", "arm,primecell";
reg = <0x1c000000 0x1000>;
interrupts = <0 31 4>;
dmas = <&dmac 0>, <&dmac 1>; /* tx通道0, rx通道1 */
dma-names = "tx", "rx";
/* SPI从设备 */
spidev@0 {
compatible = "rohm,dh2228fv";
reg = <0>;
spi-max-frequency = <1000000>;
};
};
| 属性 | 类型 | 说明 |
|---|---|---|
#dma-cells |
u32 | DMA控制器specifier的cell数量,通常为1(通道号)或2(通道+请求线) |
dmas |
phandle + specifier数组 | 按顺序列出每个DMA通道的控制器句柄和参数 |
dma-names |
string数组 | 为每个dmas条目命名,驱动通过名称请求通道 |
dma-channels |
u32 | DMA控制器支持的物理通道数(可选) |
dma-requests |
u32 | DMA控制器支持的请求线数(可选) |
常见错误: dmas 与 dma-names 的条目数量必须严格一致,否则内核解析时会报错
of_dma_request_slave_channel: dma-names条目数不匹配。另外,#dma-cells 的值必须与DMA控制器驱动解析 dma_spec 的预期一致。
4.2.3 编译设备树
# 编译单个dtb
make dtbs
# 或单独编译指定设备树
make arch/arm/boot/dts/vexpress-v2p-ca9.dtb
# 检查反编译结果
dtc -I dtb -O dts -o vexpress-v2p-ca9.dts vexpress-v2p-ca9.dtb
4.3 编写最小DMA测试模块
以下模块演示如何从设备树获取DMA通道、分配DMA缓冲区并启动一次简单的内存到内存传输。
4.3.1 模块代码:dma_test.c
// SPDX-License-Identifier: GPL-2.0
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/dmaengine.h>
#include <linux/dma-mapping.h>
#include <linux/slab.h>
struct dma_test_data {
struct dma_chan *chan;
dma_addr_t dma_src;
dma_addr_t dma_dst;
void *src_buf;
void *dst_buf;
size_t buf_size;
};
static void dma_callback(void *param)
{
struct dma_test_data *priv = param;
pr_info("DMA传输完成!目标缓冲区内容: %s\n", (char *)priv->dst_buf);
}
static int dma_test_probe(struct platform_device *pdev)
{
struct dma_test_data *priv;
struct dma_async_tx_descriptor *tx;
dma_cap_mask_t mask;
int ret;
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->buf_size = 256;
/* 1. 从设备树获取DMA通道 */
dma_cap_zero(mask);
dma_cap_set(DMA_MEMCPY, mask);
priv->chan = dma_request_slave_channel(&pdev->dev, "tx");
if (!priv->chan) {
dev_err(&pdev->dev, "无法获取DMA通道\n");
return -ENODEV;
}
/* 2. 分配DMA缓冲区 */
priv->src_buf = dma_alloc_coherent(priv->chan->device->dev,
priv->buf_size,
&priv->dma_src,
GFP_KERNEL);
priv->dst_buf = dma_alloc_coherent(priv->chan->device->dev,
priv->buf_size,
&priv->dma_dst,
GFP_KERNEL);
if (!priv->src_buf || !priv->dst_buf) {
dev_err(&pdev->dev, "DMA缓冲区分配失败\n");
ret = -ENOMEM;
goto err_free;
}
/* 3. 准备测试数据 */
strcpy((char *)priv->src_buf, "Hello DMA World!");
memset(priv->dst_buf, 0, priv->buf_size);
/* 4. 准备DMA传输描述符 */
tx = dmaengine_prep_dma_memcpy(priv->chan, priv->dma_dst,
priv->dma_src, priv->buf_size,
DMA_CTRL_ACK | DMA_PREP_INTERRUPT);
if (!tx) {
dev_err(&pdev->dev, "DMA描述符准备失败\n");
ret = -EIO;
goto err_free;
}
tx->callback = dma_callback;
tx->callback_param = priv;
/* 5. 提交并启动传输 */
dmaengine_submit(tx);
dma_async_issue_pending(priv->chan);
platform_set_drvdata(pdev, priv);
dev_info(&pdev->dev, "DMA测试模块加载成功,传输已启动\n");
return 0;
err_free:
if (priv->src_buf)
dma_free_coherent(priv->chan->device->dev, priv->buf_size,
priv->src_buf, priv->dma_src);
if (priv->dst_buf)
dma_free_coherent(priv->chan->device->dev, priv->buf_size,
priv->dst_buf, priv->dma_dst);
dma_release_channel(priv->chan);
return ret;
}
static int dma_test_remove(struct platform_device *pdev)
{
struct dma_test_data *priv = platform_get_drvdata(pdev);
dmaengine_terminate_sync(priv->chan);
dma_free_coherent(priv->chan->device->dev, priv->buf_size,
priv->src_buf, priv->dma_src);
dma_free_coherent(priv->chan->device->dev, priv->buf_size,
priv->dst_buf, priv->dma_dst);
dma_release_channel(priv->chan);
dev_info(&pdev->dev, "DMA测试模块卸载\n");
return 0;
}
static const struct of_device_id dma_test_of_match[] = {
{ .compatible = "dma-test-device" },
{ }
};
MODULE_DEVICE_TABLE(of, dma_test_of_match);
static struct platform_driver dma_test_driver = {
.probe = dma_test_probe,
.remove = dma_test_remove,
.driver = {
.name = "dma_test",
.of_match_table = dma_test_of_match,
},
};
module_platform_driver(dma_test_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("DMA Course Author");
MODULE_DESCRIPTION("Minimal DMA test module with device tree binding");
4.3.2 Makefile
obj-m := dma_test.o
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
4.3.3 设备树节点补充(需添加到板级dts)
/* 在根节点或amba总线中添加 */
dma_test {
compatible = "dma-test-device";
dmas = <&dmac 3>; /* 使用DMA控制器通道3 */
dma-names = "tx";
status = "okay";
};
4.4 编译与加载测试模块
4.4.1 编译模块
# 使用目标内核源码树编译
make -C /path/to/linux-5.10.210 M=$(pwd) modules
# 检查编译产物
ls -l dma_test.ko
4.4.2 加载与测试
# 加载模块(需先确保设备树节点已生效)
insmod dma_test.ko
# 查看内核日志
dmesg | tail -10
# 预期输出:
# [ 1234.567890] DMA测试模块加载成功,传输已启动
# [ 1234.568000] DMA传输完成!目标缓冲区内容: Hello DMA World!
# 卸载模块
rmmod dma_test
4.4.3 常见问题排查
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| dma_request_slave_channel 返回 NULL | 设备树节点缺少 dmas 属性,或 dma-names 不匹配 | 检查 dtb 是否更新,反编译确认节点内容 |
| dma_alloc_coherent 返回 NULL | DMA掩码未设置,或内存不足 | 在 probe 中调用 dma_set_mask_and_coherent() |
| dmaengine_prep_dma_memcpy 返回 NULL | DMA通道不支持 MEMCPY 传输类型 | 检查 dma_cap_set 中设置的能力掩码 |
| 回调函数未执行 | DMA传输未完成,或中断未正确连接 | 检查 dmesg 中是否有DMA控制器错误,确认中断号 |
调试技巧: 在
dma_test_probe 中增加 dmaengine_slave_config 调用,设置 src_addr_width 和 dst_addr_width 为 DMA_SLAVE_BUSWIDTH_4_BYTES,可适配更多外设场景。使用 CONFIG_DMA_API_DEBUG 内核选项可检测DMA API使用错误。
4.5 环境验证清单
完成上述步骤后,建议逐项确认以下检查点,确保开发环境就绪:
- 内核配置:
CONFIG_DMADEVICES=y,CONFIG_DMA_OF=y,目标DMA控制器驱动已编译 - 设备树:DMA控制器节点
#dma-cells正确,使用者节点dmas与dma-names匹配 - 模块编译:使用目标内核源码树编译,无未定义符号
- 加载测试:模块加载后 dmesg 显示传输完成,目标缓冲区内容正确
- 卸载清理:rmmod 后无内存泄漏,DMA通道正确释放
推荐工作流: 在QEMU + vexpress-a9环境中先完成上述所有步骤,确认无误后再移植到真实硬件。QEMU环境支持
-M vexpress-a9 并内置PL330 DMA控制器,非常适合初期调试。