在 QEMU 中,定义外设的方式可以分为两种主要模式:
- 硬编码方式:在 Machine 代码中直接实例化和连接外设。
- 动态方式:通过设备树(Device Tree)描述外设,操作系统在启动时动态发现和初始化外设。
以下是对这两种方式的详细说明,以及如何在 QEMU 中定义外设的具体方法。
1. 硬编码方式
在硬编码方式中,外设的实例化和连接直接在 Machine 代码中完成。这种方式通常用于简单的虚拟平台或需要精确控制外设行为的场景。
(1)外设的定义
- 外设通常是一个 QEMU 设备(
DeviceState
),其实现位于hw/
目录下的某个文件中。 - 例如,UART 的实现可能位于
hw/char/serial.c
或hw/char/cadence_uart.c
。
(2)外设的实例化
- 在 Machine 代码中,使用
qdev_create()
或object_new()
创建外设实例。 - 例如:
DeviceState *uart = qdev_create(NULL, "cadence_uart");
(3)外设的连接
- 将外设连接到系统的总线(如系统总线
sysbus
)上。 - 例如:
sysbus_mmio_map(SYS_BUS_DEVICE(uart), 0, UART_BASE_ADDRESS); sysbus_connect_irq(SYS_BUS_DEVICE(uart), 0, irq);
(4)示例代码
以下是一个简单的示例,展示了如何在 Machine 代码中实例化和连接一个 UART 外设:
// hw/arm/my_machine.c
static void my_machine_init(MachineState *machine) {
// 创建 CPU
Object *cpu = object_new(TYPE_MY_CPU);
// 初始化内存
MemoryRegion *ram = g_new(MemoryRegion, 1);
memory_region_init_ram(ram, NULL, "my_machine.ram", 128 * MiB, &error_fatal);
memory_region_add_subregion(get_system_memory(), 0, ram);
// 实例化 UART 外设
DeviceState *uart = qdev_create(NULL, "cadence_uart");
qdev_prop_set_chr(uart, "chardev", serial_hd(0)); // 连接到串口
qdev_init_nofail(uart);
// 将 UART 连接到系统总线
sysbus_mmio_map(SYS_BUS_DEVICE(uart), 0, UART_BASE_ADDRESS);
sysbus_connect_irq(SYS_BUS_DEVICE(uart), 0, qdev_get_gpio_in(DEVICE(cpu), UART_IRQ));
}
2. 动态方式(设备树)
在动态方式中,外设的描述通过设备树(Device Tree)传递给操作系统,操作系统在启动时动态发现和初始化外设。这种方式通常用于复杂的虚拟平台或需要支持多种硬件配置的场景。
(1)设备树的生成
- 在 Machine 代码中,动态生成设备树。
- 设备树描述了外设的地址、中断号、寄存器布局等信息。
- 例如:
void create_device_tree(MachineState *machine) { void *fdt = create_empty_fdt(); qemu_fdt_add_subnode(fdt, "/uart"); qemu_fdt_setprop_string(fdt, "/uart", "compatible", "xlnx,versal-uart"); qemu_fdt_setprop_cells(fdt, "/uart", "reg", UART_BASE_ADDRESS, UART_SIZE); qemu_fdt_setprop_cell(fdt, "/uart", "interrupts", UART_IRQ); save_device_tree(fdt, machine->fdt); }
(2)外设的实现
- 外设的实现仍然位于
hw/
目录下的某个文件中。(本例子:/uart/versal-uart
) - 外设的实现需要支持设备树绑定(即实现
DeviceClass
的realize
方法)。
(3)操作系统的支持
- 操作系统(如 Linux 内核)通过解析设备树来发现和初始化外设。
- 设备树中的
compatible
属性用于匹配操作系统的驱动程序。
注意: 在此说的操作系统指的是QEMU 内部模拟的虚拟操作系统 就例如: -kernel
启动的便是Linux内核映像
(4)示例代码
以下是一个简单的示例,展示了如何通过设备树描述外设:
// hw/arm/my_machine.c
static void create_device_tree(MachineState *machine) {
void *fdt = create_empty_fdt();
if (!fdt) {
error_report("Failed to create empty FDT");
exit(1);
}
// 添加内存节点
qemu_fdt_add_subnode(fdt, "/memory");
qemu_fdt_setprop_string(fdt, "/memory", "device_type", "memory");
qemu_fdt_setprop_cells(fdt, "/memory", "reg", 0, 0, ram_size);
// 添加 UART 节点
qemu_fdt_add_subnode(fdt, "/uart");
qemu_fdt_setprop_string(fdt, "/uart", "compatible", "xlnx,versal-uart");
qemu_fdt_setprop_cells(fdt, "/uart", "reg", UART_BASE_ADDRESS, UART_SIZE);
qemu_fdt_setprop_cell(fdt, "/uart", "interrupts", UART_IRQ);
// 保存设备树
save_device_tree(fdt, machine->fdt);
}
static void my_machine_init(MachineState *machine) {
// 创建 CPU
Object *cpu = object_new(TYPE_MY_CPU);
// 初始化内存
MemoryRegion *ram = g_new(MemoryRegion, 1);
memory_region_init_ram(ram, NULL, "my_machine.ram", 128 * MiB, &error_fatal);
memory_region_add_subregion(get_system_memory(), 0, ram);
// 生成设备树
create_device_tree(machine);
}
或者手动导入DTB
步骤1:定义设备树
/ {
chosen {
bootargs = "console=ttyS0";
};
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
serial@101f0000 {
compatible = "arm,pl011";
reg = <0x101f0000 0x1000>;
interrupts = <0 29 4>;
clock-frequency = <24000000>;
};
};
};
在这个设备树片段中,我们定义了一个 serial@101f0000 节点,表示一个 UART 外设。它的属性包括:
- compatible: 外设的类型(arm,pl011 表示一个 PL011 UART)。
- reg: 外设的基地址和大小。
- interrupts: 外设使用的中断号。
- clock-frequency: 时钟频率。
步骤2:QEMU 中的外设实现
在 QEMU 中,设备的实现通常位于 hw/
目录下。对于串口外设,QEMU 提供了一个对应的设备模型(比如 pl011
)。具体实现位于 hw/serial/pl011.c
文件中。
在 hw/serial/pl011.c
中,你会找到 PL011
串口控制器的实现。QEMU 会根据设备树中的 compatible 字段来匹配外设类型,在本例中是 arm,pl011
。
示例代码: hw/serial/pl011.c(简化)
#include "qemu/osdep.h"
#include "hw/char/pl011.h"
#include "hw/serial.h"
#include "qemu/module.h"
static const char *const pl011_compat[] = {
"arm,pl011",
NULL
};
static void pl011_init(DeviceState *dev)
{
// 这里是外设初始化代码
SerialPL011State *s = (SerialPL011State *)dev;
s->base_address = 0x101f0000;
s->irq = 29;
s->clock = 24000000;
// 更多初始化
}
static DeviceInfo pl011_info = {
.name = "PL011 UART",
.init = pl011_init,
.exit = NULL,
};
static void pl011_register_types(void)
{
register_device_type(&pl011_info);
}
static void pl011_device_init(PCIBus *bus, DeviceState *dev, uint64_t addr)
{
// 将设备挂载到 bus,设置设备地址等
pl011_init(dev);
}
static const TypeInfo pl011_type_info = {
.name = "pl011",
.parent = TYPE_SERIAL,
.instance_size = sizeof(SerialPL011State),
.class_size = sizeof(SerialClass),
.class_init = NULL,
};
static void pl011_class_init(void)
{
type_register_static(&pl011_type_info);
}
type_init(pl011_class_init);
在这段代码中:
- pl011_init() 是初始化函数,它设置了 UART 的基地址、时钟频率和中断号等。
- pl011_device_init()将这个设备注册到 QEMU 的设备总线上,并将其映射到设备树中的位置。
步骤 3:在 QEMU 中加载设备树
你在启动 QEMU 时使用了-machine virt
参数来指定使用 virt
机器类型,这样 QEMU 会加载与 virt
配套的设备树和硬件模型。
qemu-system-aarch64 -machine virt -m 1G -kernel my_kernel.img -append "console=ttyS0" -dtb my_device_tree.dtb
在这个命令中:
- -machine virt 表示使用 virt 机器类型,它会为你加载一个与之匹配的设备树模板。
- -dtb my_device_tree.dtb 是你传递给 QEMU 的设备树文件。QEMU 会解析该文件,找到 /soc/serial@101f0000 路径,并根据路径信息加载相应的外设(在本例中是 UART 外设)。
步骤 4:设备树和 QEMU 代码的动态绑定
当 QEMU 启动时,它会解析设备树中的节点并实例化相应的硬件设备模型:
- 根据设备树中的路径
/soc/serial@101f0000
,QEMU 会找到与之对应的设备类型pl011
,这是由设备树中的compatible = "arm,pl011"
来指示的。 - QEMU 会在内部的设备模型中找到
pl011
的实现,并根据设备树中提供的配置信息(基地址、时钟、中断等)来初始化这个 UART 设备。
在此过程中,QEMU 内部的pl011
设备模型会获取设备树中定义的reg
(基地址)、interrupts
(中断号)、clock-frequency
(时钟频率)等信息,并据此完成设备的初始化
3. 两种方式的对比
特性 | 硬编码方式 | 动态方式(设备树) |
---|---|---|
灵活性 | 较低,硬件配置固定 | 较高,支持多种硬件配置 |
可维护性 | 较差,硬件描述分散在代码中 | 较好,硬件描述集中在设备树中 |
适用场景 | 简单的虚拟平台 | 复杂的虚拟平台 |
操作系统支持 | 需要为每种硬件平台编写特定的内核代码 | 同一份内核代码支持多种硬件平台 |
开发难度 | 较低,适合初学者 | 较高,需要熟悉设备树语法和绑定 |
4. 总结
- 硬编码方式:直接在 Machine 代码中实例化和连接外设,适合简单的虚拟平台。
- 动态方式:通过设备树描述外设,操作系统动态发现和初始化外设,适合复杂的虚拟平台。
- 选择哪种方式取决于你的需求和平台的复杂度。对于现代虚拟平台(如
xlnx-versal-virt
),动态方式(设备树)是更推荐的做法。