在 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),动态方式(设备树)是更推荐的做法。