2. 流式DMA映射与同步

任务目标

本案例实现一个完整的字符设备驱动,演示流式DMA映射的正确用法:使用dma_map_single建立映射、通过dma_sync_single_for_cpu和dma_sync_single_for_device维护缓存一致性,最后用dma_unmap_single解除映射。驱动维护一个DMA缓冲区,用户态通过ioctl触发一次完整的“设备写→CPU读→设备读”同步流程,以此验证各API的调用时机与效果。

核心机制说明

流式DMA映射适用于单次或少量数据传输,映射建立后,CPU与设备对同一缓冲区的访问必须通过同步函数显式交接所有权:CPU写入数据后、设备读取前调用dma_sync_single_for_device;设备写入数据后、CPU读取前调用dma_sync_single_for_cpu。本案例在内存中模拟设备行为(直接修改缓冲区内容),重点展示同步API的调用序列,而非真实硬件交互。

完整驱动代码

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <linux/dma-mapping.h>
#include <linux/uaccess.h>
#include <linux/delay.h>

#define DEV_NAME "dma_sync_demo"
#define BUF_SIZE 4096
#define IOCTL_MAGIC 'D'
#define IOCTL_START _IO(IOCTL_MAGIC, 1)   /* 触发完整同步流程 */

static int major;
static struct cdev dma_cdev;
static struct class *dma_class;
static struct device *dma_device;

static char *kbuf;
static dma_addr_t dma_handle;
static struct device *dma_dev_ptr;

/* 模拟设备写操作:设备向DMA缓冲区写入数据 */
static void device_write_to_buf(void)
{
    int i;
    for (i = 0; i < BUF_SIZE; i++)
        kbuf[i] = (char)(i & 0xff);
    pr_info("device: wrote %d bytes to DMA buffer\n", BUF_SIZE);
}

/* 模拟设备读操作:设备读取并校验DMA缓冲区 */
static int device_read_from_buf(void)
{
    int i;
    for (i = 0; i < BUF_SIZE; i++) {
        if (kbuf[i] != (char)(i & 0xff)) {
            pr_err("device: data mismatch at offset %d\n", i);
            return -EIO;
        }
    }
    pr_info("device: read %d bytes, all data valid\n", BUF_SIZE);
    return 0;
}

static long dma_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    int ret = 0;

    switch (cmd) {
    case IOCTL_START:
        pr_info("=== Step 1: CPU writes pattern to buffer ===\n");
        memset(kbuf, 0xAA, BUF_SIZE);

        pr_info("=== Step 2: sync for device, device reads ===\n");
        dma_sync_single_for_device(dma_dev_ptr, dma_handle, BUF_SIZE,
                                   DMA_BIDIRECTIONAL);
        ret = device_read_from_buf();  /* 应读到0xAA,校验失败则报错 */
        if (ret)
            break;

        pr_info("=== Step 3: device writes new data ===\n");
        device_write_to_buf();

        pr_info("=== Step 4: sync for cpu, CPU reads ===\n");
        dma_sync_single_for_cpu(dma_dev_ptr, dma_handle, BUF_SIZE,
                                DMA_BIDIRECTIONAL);
        if (kbuf[0] != 0x00 || kbuf[255] != 0xff) {
            pr_err("cpu: data mismatch after device write\n");
            ret = -EIO;
            break;
        }
        pr_info("cpu: verified device-written data (first=0x%02x, last=0x%02x)\n",
                kbuf[0], kbuf[255]);

        pr_info("=== Step 5: sync for device again, device verifies ===\n");
        dma_sync_single_for_device(dma_dev_ptr, dma_handle, BUF_SIZE,
                                   DMA_BIDIRECTIONAL);
        ret = device_read_from_buf();  /* 应读到设备自己写入的递增数据 */
        if (ret)
            break;

        pr_info("DMA sync flow completed successfully\n");
        break;

    default:
        return -ENOTTY;
    }
    return ret;
}

static int dma_open(struct inode *inode, struct file *filp)
{
    return 0;
}

static int dma_release(struct inode *inode, struct file *filp)
{
    return 0;
}

static struct file_operations dma_fops = {
    .owner = THIS_MODULE,
    .open = dma_open,
    .release = dma_release,
    .unlocked_ioctl = dma_ioctl,
};

static int __init dma_init(void)
{
    dev_t devno;

    /* 分配DMA缓冲区 */
    kbuf = kzalloc(BUF_SIZE, GFP_KERNEL);
    if (!kbuf)
        return -ENOMEM;

    /* 注册字符设备 */
    alloc_chrdev_region(&devno, 0, 1, DEV_NAME);
    major = MAJOR(devno);
    cdev_init(&dma_cdev, &dma_fops);
    cdev_add(&dma_cdev, devno, 1);

    dma_class = class_create(THIS_MODULE, DEV_NAME);
    dma_device = device_create(dma_class, NULL, devno, NULL, DEV_NAME);
    dma_dev_ptr = dma_device;

    /* 建立流式DMA映射 */
    dma_handle = dma_map_single(dma_dev_ptr, kbuf, BUF_SIZE,
                                DMA_BIDIRECTIONAL);
    if (dma_mapping_error(dma_dev_ptr, dma_handle)) {
        pr_err("dma_map_single failed\n");
        device_destroy(dma_class, devno);
        class_destroy(dma_class);
        cdev_del(&dma_cdev);
        unregister_chrdev_region(devno, 1);
        kfree(kbuf);
        return -ENOMEM;
    }
    pr_info("dma_sync_demo: mapped %d bytes, dma_addr=0x%llx\n",
            BUF_SIZE, (unsigned long long)dma_handle);
    return 0;
}

static void __exit dma_exit(void)
{
    dev_t devno = MKDEV(major, 0);

    /* 解除流式DMA映射 */
    dma_unmap_single(dma_dev_ptr, dma_handle, BUF_SIZE, DMA_BIDIRECTIONAL);

    device_destroy(dma_class, devno);
    class_destroy(dma_class);
    cdev_del(&dma_cdev);
    unregister_chrdev_region(devno, 1);
    kfree(kbuf);
    pr_info("dma_sync_demo: unmapped and exited\n");
}

module_init(dma_init);
module_exit(dma_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Streaming DMA mapping and sync demo");

编译与加载

# Makefile
obj-m := dma_sync_demo.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean
make
sudo insmod dma_sync_demo.ko
sudo chmod 666 /dev/dma_sync_demo

用户态测试程序

#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>

#define IOCTL_MAGIC 'D'
#define IOCTL_START _IO(IOCTL_MAGIC, 1)

int main(void)
{
    int fd = open("/dev/dma_sync_demo", O_RDWR);
    if (fd < 0) {
        perror("open");
        return 1;
    }
    if (ioctl(fd, IOCTL_START, 0) < 0) {
        perror("ioctl");
        close(fd);
        return 1;
    }
    printf("ioctl completed, check dmesg for details\n");
    close(fd);
    return 0;
}
gcc -o test_dma test_dma.c
sudo ./test_dma
dmesg | tail -20

验证方法与判定标准

执行测试程序后,通过dmesg观察内核日志。判定通过的标志是日志中依次出现以下关键节点且无错误信息:

  1. Step 1后CPU填充0xAA;
  2. Step 2中设备读到0xAA且校验通过(说明dma_sync_single_for_device后CPU写入对设备可见);
  3. Step 3设备写入递增数据;
  4. Step 4中CPU读到设备写入的数据且首字节为0x00、末字节为0xff(说明dma_sync_single_for_cpu后设备写入对CPU可见);
  5. Step 5设备再次校验通过;
  6. 最终输出DMA sync flow completed successfully。

若在Step 2或Step 5出现data mismatch,说明同步调用缺失或方向错误;若Step 4校验失败,说明CPU侧未正确获取设备写入的数据。卸载驱动:sudo rmmod dma_sync_demo,日志应显示unmapped and exited。

常见问题

  • dma_map_single返回错误:确认设备指针有效且内存分配使用GFP_KERNEL;部分平台要求缓冲区地址对齐到缓存行大小。
  • 数据不一致:检查是否在CPU访问前遗漏dma_sync_single_for_cpu,或在设备访问前遗漏dma_sync_single_for_device。方向参数需与映射时一致(本案例统一使用DMA_BIDIRECTIONAL)。
  • 同步方向错误:CPU写→设备读必须调用for_device;设备写→CPU读必须调用for_cpu。顺序颠倒会导致缓存脏数据未刷新或无效化。