一、背景
qemu是一个很好的调试工具,能够模拟各种硬件设备,有时我们也想尝试自己添加一个设备;本文将简单介绍如何添加一个设备的例子,帮助大家理解qemu的架构。
二、qemu源码及编译
2.1 qemu编译方法
下载qemu源码,本文源码例子基于QEMU v8.2.0-rc4 release :
cd build
../configure --enable-virtfs --enable-slirp --target-list=aarch64-softmmu
make
--enable-virtfs : 开启virtfs 选项,
--enable-slirp :使能网卡user选项
--target-list=aarch64-softmmu : 这里指定只编译 qemu-system-aarch64,否则编译各个平台,耗时较长
2.2 设备定义
挂载sysbus上,同时该设备支持读写及复位操作;默认设备buffer 存储0x5a数据;
模拟一个ROM memory设备,对应的芯片手册如下:
寄存器地址 | 操作 | 描述 |
---|---|---|
0x0~0x3 | 只读,设备的chip_id(4bytes) | chip id |
0x8 | 写入1后会复位,所有ROM寄存器值变成0x5A | 复位寄存器 |
0x10~0xFF | ROM可用的读写区域,初始值为0x5A; | ROM存储空间 |
2.3 QOM(QEMU Object Model)
这部分架构说明目前网络上也有相关资料,可以参考下:The QEMU Object Model (QOM); 后续有机会再单独整理;
2.4 添加设备的代码
From 3e40d1f1be3e60dab7e7824b0c6bb563c2e0c5ec Mon Sep 17 00:00:00 2001
From: geek <XXXXX>
Date: Sat, 21 Dec 2024 18:00:55 +0800
Subject: [PATCH] add rom test device and with device tree node for virt
machine
---
hw/arm/Kconfig | 1 +
hw/arm/virt.c | 34 +++++++++++
hw/misc/Kconfig | 3 +
hw/misc/meson.build | 3 +
hw/misc/test_rom_device.c | 117 ++++++++++++++++++++++++++++++++++++++
hw/misc/test_rom_device.h | 8 +++
include/hw/arm/virt.h | 1 +
7 files changed, 167 insertions(+)
create mode 100644 hw/misc/test_rom_device.c
create mode 100644 hw/misc/test_rom_device.h
diff --git a/hw/arm/Kconfig b/hw/arm/Kconfig
index bed6ff8c5f..89b745bce6 100644
--- a/hw/arm/Kconfig
+++ b/hw/arm/Kconfig
@@ -37,6 +37,7 @@ config ARM_VIRT
select SDHCI
select PL310 # cache controller
select PL080 # DMA controller
+ select TEST_ROM_DEVICE # add the test rom device
config CHEETAH
bool
diff --git a/hw/arm/virt.c b/hw/arm/virt.c
index be2856c018..02443ecc6d 100644
--- a/hw/arm/virt.c
+++ b/hw/arm/virt.c
@@ -82,6 +82,8 @@
#include "hw/char/pl011.h"
#include "qemu/guest-random.h"
+#include "hw/misc/test_rom_device.h"
+
#define DEFINE_VIRT_MACHINE_LATEST(major, minor, latest) \
static void virt_##major##_##minor##_class_init(ObjectClass *oc, \
void *data) \
@@ -156,6 +158,8 @@ static const MemMapEntry base_memmap[] = {
[VIRT_NVDIMM_ACPI] = { 0x09090000, NVDIMM_ACPI_IO_LEN},
[VIRT_PVTIME] = { 0x090a0000, 0x00010000 },
[VIRT_SECURE_GPIO] = { 0x090b0000, 0x00001000 },
+ [VIRT_TEST_ROM] = { 0x090c0000, 0x00000100 }, /* add the test rom device */
+
[VIRT_MMIO] = { 0x0a000000, 0x00000200 },
/* ...repeating for a total of NUM_VIRTIO_TRANSPORTS, each of that size */
[VIRT_PLATFORM_BUS] = { 0x0c000000, 0x02000000 },
@@ -962,6 +966,32 @@ static void create_gpio_keys(char *fdt, DeviceState *pl061_dev,
"gpios", phandle, 3, 0);
}
+static void create_test_rom(const VirtMachineState *vms, int rom_index)
+{
+ MachineState *ms = MACHINE(vms);
+
+ hwaddr base = vms->memmap[rom_index].base;
+ hwaddr size = vms->memmap[rom_index].size;
+
+ /* pass the platform can access address for test rom device */
+ test_rom_create(base);
+
+ /* create the device node
+ *
+ * test_rom {
+ * compatible = "test_rom";
+ * reg = <0x090c0000 0x100>; // 示例:物理地址0x090c0000,长度0x100
+ * status = "okay";
+ * };
+ */
+ qemu_fdt_add_subnode(ms->fdt, "/test_rom");
+ qemu_fdt_setprop_string(ms->fdt, "/test_rom", "compatible", "test_rom");
+
+ qemu_fdt_setprop_sized_cells(ms->fdt, "/test_rom", "reg",
+ 2, base, 2, size);
+
+}
+
#define SECURE_GPIO_POWEROFF 0
#define SECURE_GPIO_RESET 1
@@ -2314,6 +2344,10 @@ static void machvirt_init(MachineState *machine)
create_platform_bus(vms);
+ /* create test rom device and related device tree node */
+ create_test_rom(vms, VIRT_TEST_ROM);
+
+
if (machine->nvdimms_state->is_enabled) {
const struct AcpiGenericAddress arm_virt_nvdimm_acpi_dsmio = {
.space_id = AML_AS_SYSTEM_MEMORY,
diff --git a/hw/misc/Kconfig b/hw/misc/Kconfig
index cc8a8c1418..cbecd107a3 100644
--- a/hw/misc/Kconfig
+++ b/hw/misc/Kconfig
@@ -200,4 +200,7 @@ config IOSB
config XLNX_VERSAL_TRNG
bool
+config TEST_ROM_DEVICE
+ bool
+
source macio/Kconfig
diff --git a/hw/misc/meson.build b/hw/misc/meson.build
index 36c20d5637..0b8bc354a4 100644
--- a/hw/misc/meson.build
+++ b/hw/misc/meson.build
@@ -18,6 +18,9 @@ system_ss.add(when: 'CONFIG_ARM11SCU', if_true: files('arm11scu.c'))
system_ss.add(when: 'CONFIG_ARM_V7M', if_true: files('armv7m_ras.c'))
+# add test rom device
+system_ss.add(when: 'CONFIG_TEST_ROM_DEVICE', if_true: files('test_rom_device.c'))
+
# Mac devices
system_ss.add(when: 'CONFIG_MOS6522', if_true: files('mos6522.c'))
system_ss.add(when: 'CONFIG_DJMEMC', if_true: files('djmemc.c'))
diff --git a/hw/misc/test_rom_device.c b/hw/misc/test_rom_device.c
new file mode 100644
index 0000000000..1c6dc01733
--- /dev/null
+++ b/hw/misc/test_rom_device.c
@@ -0,0 +1,117 @@
+#include "qemu/osdep.h"
+#include "qapi/error.h"
+#include "hw/sysbus.h"
+#include "hw/misc/test_rom_device.h"
+
+#define TYPE_TEST_ROM "test_rom"
+typedef struct TestRomState TestRomState_t;
+
+DECLARE_INSTANCE_CHECKER(TestRomState_t, TEST_ROM,
+ TYPE_TEST_ROM)
+
+#define REG_CHIP_ID 0x0
+#define REG_RESET 0x8 //restore all the rom buffer value to default
+#define REG_BUFF_START 0x10
+#define REG_BUFF_END 0x100
+
+#define CHIP_ID 0x9960000A
+
+struct TestRomState {
+ SysBusDevice parent_obj;
+ MemoryRegion iomem;
+ uint64_t chip_id;
+ unsigned char rom_buff[REG_BUFF_END];
+};
+
+static void reset_rom_buff(TestRomState_t *pState)
+{
+ for(int i = REG_BUFF_START-1; i < REG_BUFF_END; i++) {
+ pState->rom_buff[i] = 0x5a;
+ }
+}
+
+static uint64_t test_rom_read(void *opaque, hwaddr addr, unsigned int size)
+{
+ TestRomState_t *pState = opaque;
+
+ if (addr < REG_CHIP_ID + 4)
+ {
+ return (pState->chip_id >> (addr*8)) & 0xFF;
+
+ } else {
+
+ if(addr >= REG_BUFF_START) {
+ return pState->rom_buff[addr-1];
+ }
+ else {
+ return 0;
+ }
+ }
+
+ return 0;
+}
+
+static void test_rom_write(void *opaque, hwaddr addr, uint64_t val,
+ unsigned width)
+{
+ TestRomState_t *pState= opaque;
+
+ switch (addr) {
+ case REG_CHIP_ID:
+ break;
+ case REG_RESET: //when write reset register, will restore the rom buff value
+ reset_rom_buff(pState);
+ break;
+ default:
+ if(addr >= REG_BUFF_START) {
+ //only support wirte by byte
+ pState->rom_buff[addr-1] = (unsigned char)val;
+ }
+ break;
+ }
+}
+
+static const MemoryRegionOps test_rom_ops = {
+ .read = test_rom_read,
+ .write = test_rom_write,
+ .valid.min_access_size = 1,
+ .valid.max_access_size = 1,
+ .endianness = DEVICE_LITTLE_ENDIAN,
+};
+
+static void test_rom_instance_init(Object *obj)
+{
+ TestRomState_t *pState = TEST_ROM(obj);
+
+ //alloc memory map region
+ memory_region_init_io(&pState->iomem, obj, &test_rom_ops, pState, TYPE_TEST_ROM, REG_BUFF_END);
+ sysbus_init_mmio(SYS_BUS_DEVICE(obj), &pState->iomem);
+ reset_rom_buff(pState);
+ pState->chip_id = CHIP_ID;
+}
+
+static const TypeInfo test_rom_info = {
+ .name = TYPE_TEST_ROM,
+ .parent = TYPE_SYS_BUS_DEVICE,
+ .instance_size = sizeof(TestRomState_t),
+ .instance_init = test_rom_instance_init,
+
+};
+
+static void test_rom_register_types(void)
+{
+ type_register_static(&test_rom_info);
+}
+type_init(test_rom_register_types)
+
+DeviceState *test_rom_create(hwaddr addr)
+{
+ SysBusDevice *busdev;
+ DeviceState *dev = qdev_new(TYPE_TEST_ROM);
+ busdev = SYS_BUS_DEVICE(dev);
+ sysbus_realize_and_unref(busdev, &error_fatal);
+ sysbus_mmio_map(busdev, 0, addr);
+
+ return dev;
+}
diff --git a/hw/misc/test_rom_device.h b/hw/misc/test_rom_device.h
new file mode 100644
index 0000000000..8861a70b20
--- /dev/null
+++ b/hw/misc/test_rom_device.h
@@ -0,0 +1,8 @@
+#ifndef HW_TEST_ROM_DEVICE_H
+#define HW_TEST_ROM_DEVICE_H
+
+#include "qom/object.h"
+
+DeviceState *test_rom_create(hwaddr);
+
+#endif
diff --git a/include/hw/arm/virt.h b/include/hw/arm/virt.h
index f69239850e..89aa3d2da6 100644
--- a/include/hw/arm/virt.h
+++ b/include/hw/arm/virt.h
@@ -60,6 +60,7 @@ enum {
VIRT_GIC_REDIST,
VIRT_SMMU,
VIRT_UART,
+ VIRT_TEST_ROM,
VIRT_MMIO,
VIRT_RTC,
VIRT_FW_CFG,
--
2.34.1
代码 | 说明 |
---|---|
hw/arm/virt.c include/hw/arm/virt.h hw/arm/Kconfig | 这部分修改对应qemu 启动参数中设置的-machine virt;因为virt machine没有独立的设备树,这里需要做两件事:给virt machine添加一个VIRT_TEST_ROM设备;添加对应的test_rom设备树信息(为后续驱动加载使用) |
hw/misc/Kconfig hw/misc/meson.build hw/misc/test_rom_device.c hw/misc/test_rom_device.h | 描述test_rom设备信息,调用QOM相关API注册一个sys_bus下的设备(有兴趣的同学可以修改尝试下I2C_BUS); |
注:由于本人qemu使用过的都是 -machine virt; 上面的前半部分修改只适用于virt machine, 如果使用其他machine(如:vexpress-a15,xilinx-zynq-a9), 请自行将第一部分修改迁移到对应machine的文件;另外很多machine是带dts的,可以将第一部分中设备树的修改添加到linux kernel对应board的dts中,类似:
test_rom@0x090c0000 {
compatible = "test_rom";
reg = <0x090c0000 0x100>; // 示例地址和长度
};
如果你不知道上面一段话的意思,那请qemu测试时使用和本文一致的 -machine virt参数;具体参数可以参考:
无人知晓:qemu单步调试arm64 linux kernel
三、设备测试验证
3.1 用IO读写工具测试设备
工具:
如果搭建的rootfs是busybox,
使用busybox devmem <address>
如果是ubuntu,可以直接 使用devmem2工具 (apt install devmem2)
读取内存地址: 要读取特定的物理内存地址,可以使用如下命令:
sudo ./devmem2 <address>
其中 <address> 是你要读取的十六进制物理地址。
写入内存地址: 写入特定的物理内存地址时,可以指定数据宽度(8位、16位、32位或64位,默认是32位),然后跟上要写入的数据:
sudo ./devmem2 <address> <width> <value>
<width> 可以是 b (8-bit), w (16-bit), l (32-bit) 或者 q (64-bit)。
<value> 是要写入的值,通常为十六进制格式。
测试:
geek@localhost:~$ sudo devmem2 0x090c0000
/dev/mem opened.
Memory mapped at address 0xffffa5916000.
Value at address 0x90C0000 (0xffffa5916000): 0x9960000A
3.2 驱动程序
用deepseek完成驱动程序:
需求描述如下(deepseek输入):
写一个linux平台设备驱动
1、设备树compitable name为test_rom
2、设备树reg 有两个参数,其中第一个参数是test_rom的寄存器物理地址;第二个参数是寄存器地址空间的长度
3、提供misc字符节点,有读写寄存器的能力,启动读接口的返回值是从test_rom的寄存器起始地址到地址空间长度存储的寄存器值,
寄存器按byte读取;写接口有两个参数,第一个参数是寄存器地址,第二个参数是往寄存器地址写入的值,寄存器按byte写入
deepseek输出(做了点简单修改,PS:deepseek生成代码居然会编译报错, 使用未定义的结构体字段):
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/platform_device.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/slab.h>
#include <linux/of_address.h>
#include <linux/of.h>
#define DEVICE_NAME "test_rom"
#define MAX_REG_SIZE 256
struct test_rom_dev {
void __iomem *regs;
resource_size_t regs_len;
struct mutex lock;
struct miscdevice miscdev;
};
static ssize_t test_rom_read(struct file *file, char __user *buf,
size_t count, loff_t *ppos)
{
struct test_rom_dev *dev = container_of(file->private_data,
struct test_rom_dev,
miscdev);
u8 *reg_values;
int ret, i;
loff_t pos = *ppos;
int num_regs = dev->regs_len;
if (pos >= num_regs)
return 0;
reg_values = kmalloc(num_regs, GFP_KERNEL);
mutex_lock(&dev->lock);
for (i = 0; i < dev->regs_len; i++) {
reg_values[i] = ioread8(dev->regs + i);
}
mutex_unlock(&dev->lock);
ret = copy_to_user(buf, reg_values, num_regs);
kfree(reg_values);
if (ret)
return -EFAULT;
*ppos = num_regs;
return num_regs;
}
static ssize_t test_rom_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
struct test_rom_dev *dev = container_of(file->private_data,
struct test_rom_dev,
miscdev);
u32 reg_addr, reg_val;
char input[MAX_REG_SIZE];
int ret;
if (copy_from_user(input, buf, count))
return -EFAULT;
input[count] = '\0';
ret = sscanf(input, "%d %d", ®_addr, ®_val);
if (reg_addr >= dev->regs_len)
return -EINVAL;
pr_info("write %x val %x\n", reg_addr, reg_val);
mutex_lock(&dev->lock);
iowrite8((unsigned char)reg_val, dev->regs + reg_addr);
mutex_unlock(&dev->lock);
return count;
}
static const struct file_operations test_rom_fops = {
.owner = THIS_MODULE,
.read = test_rom_read,
.write = test_rom_write,
};
static int test_rom_probe(struct platform_device *pdev)
{
struct resource *res;
struct test_rom_dev *dev;
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
dev->regs = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(dev->regs))
return PTR_ERR(dev->regs);
dev->regs_len = resource_size(res);
mutex_init(&dev->lock);
platform_set_drvdata(pdev, dev);
dev->miscdev.minor = MISC_DYNAMIC_MINOR;
dev->miscdev.name = DEVICE_NAME;
dev->miscdev.fops = &test_rom_fops;
return misc_register(&dev->miscdev);
}
static int test_rom_remove(struct platform_device *pdev)
{
struct test_rom_dev *dev = platform_get_drvdata(pdev);
misc_deregister(&dev->miscdev);
return 0;
}
static const struct of_device_id test_rom_of_match[] = {
{ .compatible = "test_rom" },
{},
};
MODULE_DEVICE_TABLE(of, test_rom_of_match);
static struct platform_driver test_rom_driver = {
.probe = test_rom_probe,
.remove = test_rom_remove,
.driver = {
.name = DEVICE_NAME,
.of_match_table = test_rom_of_match,
},
};
module_platform_driver(test_rom_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("DeepSeek-R1");
MODULE_DESCRIPTION("Test ROM device driver");
上述代码生成一个测试节点,能够读写test_rom寄存器:
- 读会获取test_rom整个地址空间对应寄存器值
- 写入方式只支持10进制输入,第一个参数是要写入的寄存器地址,第二个参数是希望修改的寄存器值
3.3 驱动验证
设备树检查:
/proc/device-tree/test_rom$ ls
compatible name reg
/proc/device-tree/test_rom$ cat compatible
test_rom
/proc/device-tree/test_rom$ cat name
test_rom
/proc/device-tree/test_rom$ hexdump reg
0000000 0000 0000 0c09 0000 0000 0000 0000 0001
0000010
读写寄存器
1、读取所有寄存器
/test # hexdump -C /dev/test_rom
00000000 0a 00 60 99 00 00 00 00 00 00 00 00 00 00 00 00 |..`.............|
00000010 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a |ZZZZZZZZZZZZZZZZ|
*
00000100
2、写入寄存器
/test # echo 17 1 > /dev/test_rom
[ 64.189871] write 11 val 1
/test # hexdump -C /dev/test_rom
00000000 0a 00 60 99 00 00 00 00 00 00 00 00 00 00 00 00 |..`.............|
00000010 5a 01 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a |Z.ZZZZZZZZZZZZZZ|
00000020 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a |ZZZZZZZZZZZZZZZZ|
*
00000100
/test # echo 16 1 > /dev/test_rom
[ 104.156481] write 10 val 1
/test # hexdump -C /dev/test_rom
00000000 0a 00 60 99 00 00 00 00 00 00 00 00 00 00 00 00 |..`.............|
00000010 01 01 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a |..ZZZZZZZZZZZZZZ|
00000020 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a |ZZZZZZZZZZZZZZZZ|
*
00000100
3、复位所有buf
/test # echo 8 1 > /dev/test_rom
[ 136.402657] write 8 val 1
/test # hexdump -C /dev/test_rom
00000000 0a 00 60 99 00 00 00 00 00 00 00 00 00 00 00 00 |..`.............|
00000010 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a |ZZZZZZZZZZZZZZZZ|
*
00000100
五、总结
使用好qemu能够有效的提升我们学习OS/驱动的效率,理解和掌握添加虚拟设备在实际工作和驱动自动化测试中也有一定作用;本文的例子比较简单,抛转引玉,简单介绍了一个虚拟设备添加的方法。另外用deepseek编写驱动效率也比较高,如果描述好需求,代码生成质量还可以,做一点简单修改就可以满足要求了。
参考:
qemu 源码编译 qemu-system-aarch64 的方法
https://blogs.oracle.com/linux/post/how-to-emulate-block-devices-with-qemu