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
内核源码树配置与编译流程 获取内核源码 配置DMA选项 编译内核+dtbs 编译模块 安装模块到rootfs 关键检查点: • 确认 .config 中 CONFIG_DMADEVICES=y 且 CONFIG_DMA_OF=y • 确认目标平台DMA控制器驱动已编译(如 PL330、Intel IDMA64)

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
设备树DMA节点绑定关系 DMA控制器 dmac: dma-controller@1e000000 #dma-cells = <1> SPI控制器 dmas = <&dmac 0>, <&dmac 1> UART控制器 dmas = <&dmac 2> 自定义DMA测试模块 绑定规则 1. dmas 顺序对应 dma-names 名称 2. 每个dmas条目包含 <控制器句柄 通道号> 3. 驱动通过dma-names 调用dma_request_slave_channel() 4. 通道号由#dma-cells决定

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";
};
DMA测试模块传输流程 dma_request_slave_channel dma_alloc_coherent dmaengine_prep_dma_memcpy dmaengine_submit + issue_pending DMA中断 → dma_callback() 数据流: src_buf (CPU可访问) → dma_src (DMA物理地址) → DMA控制器搬运 → dma_dst (DMA物理地址) → dst_buf (CPU可访问)

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 环境验证清单

完成上述步骤后,建议逐项确认以下检查点,确保开发环境就绪:

  1. 内核配置:CONFIG_DMADEVICES=y,CONFIG_DMA_OF=y,目标DMA控制器驱动已编译
  2. 设备树:DMA控制器节点 #dma-cells 正确,使用者节点 dmas 与 dma-names 匹配
  3. 模块编译:使用目标内核源码树编译,无未定义符号
  4. 加载测试:模块加载后 dmesg 显示传输完成,目标缓冲区内容正确
  5. 卸载清理:rmmod 后无内存泄漏,DMA通道正确释放
推荐工作流: 在QEMU + vexpress-a9环境中先完成上述所有步骤,确认无误后再移植到真实硬件。QEMU环境支持 -M vexpress-a9 并内置PL330 DMA控制器,非常适合初期调试。