1. 概述
本文讲述创建一个简单的QEMU模拟设备,并为其开发内核态驱动流程。文档整体分为设备模拟和驱动开发两部分。
2. QEMU设备模拟实现
2.1 编码思路
下文将模拟一个PCI设备,提供内存映射IO(MMIO)功能,允许用户空间通过驱动对设备bar寄存器进行数据dma读写。
第一步:PCI 设备类型注册
#define TYPE_PCI_PRACTICE_DEVICE "pratice"
static void pci_pratice_register_types(void)
{
static InterfaceInfo interfaces[] = {
{ INTERFACE_CONVENTIONAL_PCI_DEVICE },
{ },
};
static const TypeInfo pratice_info = {
.name = TYPE_PCI_PRATICE_DEVICE,
.parent = TYPE_PCI_DEVICE,
.instance_size = sizeof(PraticeState),
.class_init = pratice_class_init, //设备类的初始化函数
.interfaces = interfaces,
};
type_register_static(&pratice_info);
}
type_init(pci_pratice_register_types)
第二步:实现设备类的初始化函数,这里需要注意的是在 Class 的初始化函数中我们应当设置父类 PCIDeviceClass
的一系列基本属性(也就是 PCI 设备的基本属性)
static void pratice_class_init(ObjectClass *class, void *data)
{
DeviceClass *dc = DEVICE_CLASS(class);
PCIDeviceClass *k = PCI_DEVICE_CLASS(class);
// 设置PCI设备属性
k->realize = pci_pratice_realize;
k->vendor_id = PCI_VENDOR_ID_PRATICE;
k->device_id = PCI_DEVICE_ID_PRATICE;
k->revision = PRATICE_PCI_REVID;
k->class_id = PCI_CLASS_OTHERS;
set_bit(DEVICE_CATEGORY_MISC, dc->categories);
}
第三步:设备实例初始化函数实现
// 设备实例结构体定义
struct PraticeState {
PCIDevice parent_obj; // 继承PCI设备
MemoryRegion mmio; // MMIO内存区域
char buf[DMA_SIZE]; // 设备BAR空间,设备存储空间
};
// 设备实例初始化
static void pci_pratice_realize(PCIDevice *pdev, Error **errp)
{
PraticeState *pratice = PRATICE(pdev);
// 初始化MMIO区域
memory_region_init_io(&pratice->mmio, OBJECT(pratice), &pratice_mmio_ops, pratice, "pratice-mmio", 4 * KiB);
// 注册PCI BAR
pci_register_bar(pdev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &pratice->mmio);
}
第四步:设备MMIO内存区域读写回调函数定义,需要注意的是这里传入的 hwaddr
类型参数其实为相对addr而非绝对addr
// MMIO区域操作集合
static const MemoryRegionOps pratice_mmio_ops = {
.read = pratice_mmio_read,
.write = pratice_mmio_write
};
// 实现MMIO操作回调
static uint64_t pratice_mmio_read(void *opaque, hwaddr addr, unsigned size)
{
// 从设备缓冲区读取数据
memcpy(&val, &pratice->buf[addr], size);
return val;
}
static void pratice_mmio_write(void *opaque, hwaddr addr, uint64_t val,
unsigned size)
{
memcpy(&pratice->buf[addr], &val, size);
}
2.2 编译步骤和运行Qemu
第一步:在 构建系统中加入设备
在 hw/misc/meson.build
中加入如下语句:
softmmu_ss.add(when: 'CONFIG_PCI_PRATICE', if_true: files('pratice.c'))
并在 hw/misc/Kconfig
中添加如下内容,这表示我们的设备会在 CONFIG_PCI_DEVICES=y
时编译:
config PRATICE
bool
default y if PCI_DEVICES
depends on PCI
第二步:在Qemu运行脚本附加上 -device pratice
,之后随便起一个 Linux 系统,此时使用 lspci
指令我们便能看到我们新添加的 pci 设备。红框部分为设备pci属性 vender_id:device_id (revision)
3. Linux内核驱动开发
3.1 驱动代码实现
第一步:定义设备信息
#define DRIVER_NAME "pratice"
#define PCI_VENDOR_ID_PRATICE 0x2025
#define PCI_DEVICE_ID_PRATICE 0x0605
#define BAR 0
#define DMA_SIZE 4096
第二步定义设备结构体
struct pratice_device {
struct pci_dev *pdev;
struct cdev cdev;
dev_t devno;
void __iomem *mmio;
struct mutex lock;
};
第三步:内核模块install/remove函数实现
static struct pci_device_id ids[] = {
{ PCI_DEVICE(PCI_VENDOR_ID_PRATICE, PCI_DEVICE_ID_PRATICE) },
{ 0 }
};
MODULE_DEVICE_TABLE(pci, ids);
static struct pci_driver pratice_driver = {
.name = DRIVER_NAME,
.id_table = ids,
.probe = pratice_probe,
.remove = pratice_remove,
};
static int __init pratice_init(void) {
int ret;
if ((ret = pci_register_driver(&pratice_driver)) < 0) {
printk(KERN_ERR "[%s] Init failed. \\n", DRIVER_NAME);
return ret;
}
printk(KERN_INFO "[%s] Init sucessfully. \\n", DRIVER_NAME);
return ret;
}
static void __exit pratice_exit(void) {
pci_unregister_driver(&pratice_driver);
printk(KERN_INFO "[%s] exited. \\n", DRIVER_NAME);
}
module_init(pratice_init);
module_exit(pratice_exit);
第四步:探测和移除函数实现 (pratice_probe、pratice_remove
)
static int pratice_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
// 启用PCI设备
pci_enable_device(pdev);
// 映射MMIO区域
dev->mmio = pci_iomap(pdev, BAR, pci_resource_len(pdev, BAR));
// 注册字符设备
alloc_chrdev_region(&dev->devno, 0, 1, DRIVER_NAME);
cdev_init(&dev->cdev, &fops);
cdev_add(&dev->cdev, dev->devno, 1);
// 创建设备节点
device_create(pratice_class, NULL, dev->devno, NULL, "pratice");
// 初始化互斥锁
mutex_init(&dev->lock);
}
static void pratice_remove(struct pci_dev *pdev)
{
struct pratice_device *dev = pci_get_drvdata(pdev);
device_destroy(pratice_class, dev->devno);
class_destroy(pratice_class);
cdev_del(&dev->cdev);
unregister_chrdev_region(dev->devno, 1);
pci_iounmap(pdev, dev->mmio);
mutex_destroy(&dev->lock);
kfree(dev);
pci_disable_device(pdev);
}
第五步:文件操作实现
static const struct file_operations fops = {
.owner = THIS_MODULE,
.read = pratice_read,
.write = pratice_write,
.open = pratice_open
};
static ssize_t pratice_read(struct file *filp, char __user *buf, ...)
{
// 1. 检查访问边界
// 2. 加锁保护
// 3. memcpy_fromio读取硬件数据
// 4. copy_to_user传至用户空间
}
static ssize_t pratice_read(struct file *filp, char __user *buf, ...)
{
// 1. 检查访问边界
// 2. 加锁保护
// 3. memcpy_fromio读取硬件数据
// 4. copy_to_user传至用户空间
}
static int edu_wxd_open(struct inode *inode, struct file *filp)
{
// 1.通过字符设备结构找到对应的驱动设备实例
// 2. 将设备实例保存到文件对象的私有数据区
}
3.2 编译和install驱动
第一步:编译
make -C $(KERNELDIR) M=$(PWD) modules
第二步:install驱动
insmod pratice_driver.ko
3.3 用户空间测试
在用户空间打开设备文件,进行相应的读写测试。
4. 总结
本文档包括:
- 如何在QEMU中创建自定义PCI设备
- 如何实现模拟设备驱动
此学习case可作为开发真实硬件驱动的基础框架,后续可扩展支持中断等高级功能。