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观察内核日志。判定通过的标志是日志中依次出现以下关键节点且无错误信息:
Step 1后CPU填充0xAA;Step 2中设备读到0xAA且校验通过(说明dma_sync_single_for_device后CPU写入对设备可见);Step 3设备写入递增数据;Step 4中CPU读到设备写入的数据且首字节为0x00、末字节为0xff(说明dma_sync_single_for_cpu后设备写入对CPU可见);Step 5设备再次校验通过;- 最终输出
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。顺序颠倒会导致缓存脏数据未刷新或无效化。