4. 流式DMA映射单缓冲区

任务目标

用 dma_map_single() 把一块内核缓冲区映射为流式 DMA 地址,交给 PCIe 设备做一次写(设备→内存)和一次读(内存→设备),完成后用 dma_unmap_single() 解除映射,并检查数据一致性。

关键点

  • 流式映射是"一次性"的:映射→传输→解除,不能长期缓存 DMA 地址。
  • 方向必须正确:设备写内存用 DMA_FROM_DEVICE,设备读内存用 DMA_TO_DEVICE。
  • 解除映射前不要访问缓冲区(除非先 dma_sync_single_for_cpu);解除后 CPU 才能安全读写。
  • 映射失败用 dma_mapping_error() 判断,不能只比较 0。
内核缓冲区 kmalloc DMA 地址 dma_addr_t PCIe 设备 BAR / DMA 引擎 map_single 提交地址 设备 DMA 读写内存 unmap_single

完整代码

#include <linux/module.h>
#include <linux/pci.h>
#include <linux/dma-mapping.h>
#include <linux/slab.h>

#define BUF_SIZE 256

static int __init dma_single_init(void)
{
    struct pci_dev *pdev;
    void *buf;
    dma_addr_t dma_addr;
    int ret = 0;

    /* 1. 找一个真实存在的 PCIe 设备(示例:任意第一个设备) */
    pdev = pci_get_device(PCI_ANY_ID, PCI_ANY_ID, NULL);
    if (!pdev) {
        pr_err("no pci device found\n");
        return -ENODEV;
    }
    pr_info("use pci device %s\n", pci_name(pdev));

    /* 2. 分配内核缓冲区 */
    buf = kmalloc(BUF_SIZE, GFP_KERNEL);
    if (!buf) {
        ret = -ENOMEM;
        goto put_dev;
    }
    memset(buf, 0xAA, BUF_SIZE);   /* 预置已知数据 */

    /* 3. 流式映射:设备将写入内存 -> DMA_FROM_DEVICE */
    dma_addr = dma_map_single(&pdev->dev, buf, BUF_SIZE, DMA_FROM_DEVICE);
    if (dma_mapping_error(&pdev->dev, dma_addr)) {
        pr_err("dma_map_single failed\n");
        ret = -EIO;
        goto free_buf;
    }
    pr_info("mapped buf=%p dma=%pad\n", buf, &dma_addr);

    /* 4. 此处应把 dma_addr 写入设备寄存器并启动 DMA。
     *    本示例无真实设备驱动,仅演示映射/解除流程。
     *    真实场景:writel(dma_addr, bar + DMA_SRC_REG);
     *              writel(BUF_SIZE, bar + DMA_LEN_REG);
     *              writel(START, bar + DMA_CTRL_REG);
     *              wait_for_completion(...);
     */

    /* 5. 解除映射,之后 CPU 才能安全读缓冲区 */
    dma_unmap_single(&pdev->dev, dma_addr, BUF_SIZE, DMA_FROM_DEVICE);

    /* 6. 数据一致性检查(无真实设备时数据应保持 0xAA) */
    {
        int i, ok = 1;
        u8 *p = buf;
        for (i = 0; i < BUF_SIZE; i++) {
            if (p[i] != 0xAA) { ok = 0; break; }
        }
        pr_info("consistency check: %s (first byte=0x%02x)\n",
                ok ? "PASS" : "FAIL", p[0]);
    }

    kfree(buf);
    pci_dev_put(pdev);
    return 0;

free_buf:
    kfree(buf);
put_dev:
    pci_dev_put(pdev);
    return ret;
}

static void __exit dma_single_exit(void)
{
    pr_info("dma_single module unloaded\n");
}

module_init(dma_single_init);
module_exit(dma_single_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("streaming DMA single buffer demo");

编译与运行

# Makefile
obj-m += dma_single.o
KDIR := /lib/modules/$(shell uname -r)/build
all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean
make
sudo insmod dma_single.ko
dmesg | tail -n 20
sudo rmmod dma_single

验证方法与通过条件

  • 加载后 dmesg 应出现 use pci device ...、mapped buf=... dma=...、consistency check: PASS。
  • 若系统无 PCIe 设备,会打印 no pci device found 并返回 -ENODEV,属预期失败路径。
  • 一致性检查 PASS 表示:映射/解除流程未破坏缓冲区,且解除后 CPU 可正常访问。
  • 真实设备接入时,把第 4 步替换为实际寄存器写入与完成等待,再检查设备写入的数据是否符合协议预期。