在这篇文章中,我们将了解如何创建一个简单的新设备。其他帖子将专门讨论更复杂的设备,例如 PCI 和中断控制器。
QEMU 设备树(缩写)
QEMU 监视器为您提供不同的命令来检查正在运行的实例的设备:
这里有很多东西。从机器本身,到CPU对象、DMA引擎、PCI控制器、PCI总线、系统总线、网络(pcnet)、定时器(pit)和中断控制器(pic)。
它们都是 QEMU 对象。您还可以使用 info qtree 命令获得更详细的视图。
QEMU 监控命令
请注意,监视器命令是通过 QMP API 实现的,并在 QEMU 源代码中被引用为 hmp command
。命令子集特定于目标。构建 PowerPC QEMU 时,请查看 ppc-softmmu/hmp-commands-info.h
。所有可用的命令都位于 hmp-commands-info.hx
它们看起来像下面这样:
{
.name = "mtree",
.args_type = "flatview:-f,dispatch_tree:-d,owner:-o",
.params = "[-f][-d][-o]",
.help = "show memory tree (-f: dump flat view for address spaces;"
"-d: dump dispatch tree, valid with -f only);"
"-o: dump region owners/parents",
.cmd = hmp_info_mtree,
},
其中 hmp_info_mtree()
是处理程序。
设备是一个 QObject
对于机器,我们需要创建正确的 TypeInfo
、DeviceClass
和 DeviceState
初始化函数。
让我们实现 CPIOM EDC 设备的最小代码。我们不关心它的内部结构,我们只需要知道:
- 它有几个 IO 内存映射寄存器
- 它可以引发中断
- 它连接到系统总线
static void cpiom_edc_init(Object *obj)
{
cpiom_edc_state_t *s = CPIOM_EDC(obj);
SysBusDevice *d = SYS_BUS_DEVICE(obj);
memory_region_init_io(&s->reg1, obj, &edc_reg1_ops, s,
CPIOM_EDC_NAME"-reg1", CPIOM_MMAP_EDC_REG_SIZE);
memory_region_init_io(&s->reg2, obj, &edc_reg2_ops, s,
CPIOM_EDC_NAME"-reg2", 6*4);
memory_region_init_io(&s->err, obj, &edc_err_ops, s,
CPIOM_EDC_NAME"-err", CPIOM_MMAP_PPCERR_SIZE);
sysbus_init_mmio(d, &s->reg1);
memory_region_add_subregion(get_system_memory(), CPIOM_MMAP_PPCERR, &s->err);
sysbus_init_irq(d, &s->irq);
}
static void cpiom_edc_class_init(ObjectClass *klass, void *data)
{
DeviceClass *dc = DEVICE_CLASS(klass);
dc->desc = CPIOM_EDC_NAME;
}
static const TypeInfo cpiom_edc_info = {
.name = CPIOM_EDC_NAME,
.parent = TYPE_SYS_BUS_DEVICE,
.instance_size = sizeof(cpiom_edc_state_t),
.instance_init = cpiom_edc_init,
.class_init = cpiom_edc_class_init,
};
static void cpiom_edc_register_types(void)
{
type_register_static(&cpiom_edc_info);
}
type_init(cpiom_edc_register_types)
现在看起来很熟悉。为了符合 QOM 标准,我们的设备需要一个头文件,其中可能包含以下内容:
#define CPIOM_MMAP_EDC_REG 0x20001000
#define CPIOM_MMAP_EDC_REG_SIZE 0x1000
#define CPIOM_MMAP_PPCERR 0x20002000
#define CPIOM_MMAP_PPCERR_SIZE 0x2000
#define CPIOM_EDC_NAME "cpiom-edc"
#define CPIOM_EDC(obj) OBJECT_CHECK(cpiom_edc_state_t,(obj),CPIOM_EDC_NAME)
typedef struct cpiom_edc_state
{
/*< private >*/
SysBusDevice parent_obj;
/*< public >*/
MemoryRegion reg1, reg2, err;
qemu_irq irq;
} cpiom_edc_state_t;
这里的一切本质上都是设备初始化样板。给出名称、具体类型、内存地址范围…
无需再次解释,您应该为每个 IO 内存区域实现关联的 MemoryRegionOps
。
一件重要的事情是,我们的 EDC 设备是 SysBusDevice,这要归功于它的 TypeInfo
(.parent = TYPE_SYS_BUS_DEVICE
)。我们将能够利用 SysBus API。
实例化我们的设备
回到机器初始化代码:
cpiom_t* cpiom_init_mcs(MachineState *mcs)
{
...
cpiom_init_dev(cpiom);
}
static void cpiom_init_dev(cpiom_t *cpiom)
{
...
create_edc(cpiom);
....
}
static void create_edc(cpiom_t *cpiom)
{
cpiom->edc = sysbus_create_varargs(
"cpiom-edc", CPIOM_MMAP_EDC_REG,
qdev_get_gpio_in_named(cpiom->intc, "ITN", INT_N_IT_EDC_ERR),
NULL);
cpiom_edc_state_t *s = CPIOM_EDC(cpiom->edc);
memory_region_add_subregion(cpiom->config, 0xc, &s->reg2);
}
我们有一个特定的 EDC 设备初始化函数来完成复杂的事情。简而言之,它将:
- 通过系统总线通用服务实例化我们的设备
- 将其 IRQ 线连接到我们的 CPIOM 中断控制器(
cpiom->intc
,稍后会详细介绍)
最后一个内存区域 reg2
作为子区域附加到 CPIOM 板 cpiom->config
内存区域。这是 CPIOM 特定区域,其中有多个器件配置寄存器。因此,我们的一些 EDC 设备寄存器在offset 0xc
处映射到该区域。conifg
区域本身是系统内存
的 IO 内存子区域。您可以对现有内存区域进行分段并与新的子区域重叠。
创建系统总线设备
从非常低的级别开始,设备创建是通过qdev_create()
和qdev_init_nofail()
完成的。请参阅 qdev-core.h
中的文档。它们本质上找到正确的设备 TypeInfo
并相应地实例化 DeviceClass
,从而导致我们的 cpiom_edc_class_init/cpiom_edc_init
函数的执行。
sysbus_create_varargs
函数是 qdev_xxx()
函数以及自动 IO 映射和 IRQ 注册的包装:
DeviceState *sysbus_create_varargs(const char *name, hwaddr addr, ...)
{
DeviceState *dev;
SysBusDevice *s;
va_list va;
qemu_irq irq;
int n;
dev = qdev_create(NULL, name);
s = SYS_BUS_DEVICE(dev);
qdev_init_nofail(dev);
if (addr != (hwaddr)-1) {
sysbus_mmio_map(s, 0, addr);
}
va_start(va, addr);
n = 0;
while (1) {
irq = va_arg(va, qemu_irq);
if (!irq) {
break;
}
sysbus_connect_irq(s, n, irq);
n++;
}
va_end(va);
return dev;
}
映射设备IO内存区域
sysbus_create_varargs
的 addr
参数是 cpiom_edc_init
中分配的 IO 内存区域地址 CPIOM_MMAP_EDC_REG 。请记住,我们并没有直接将其作为子区域附加到系统内存区域,而是这样做了:
static void cpiom_edc_init(Object *obj)
{
...
memory_region_init_io(&s->reg1, obj, &edc_reg1_ops, ...);
sysbus_init_mmio(d, &s->reg1);
...
}
每个 SysBusDevice
对象都有一个 QDEV_MAX_MMIO
条目的内部 mmio
数组。 sysbus_init_mmio()
函数将安装这样的条目。然后sysbus_create_varargs
函数将调用sysbus_mmio_map()
它将在内部将给定的内存区域注册为系统内存的子区域。
连接 IRQ 线
该函数的其余参数是可变长度、以 NULL 结尾的 qemu_irq
。我们将在中断控制器的帖子中了解有关 IRQ 的更多信息。现在假设 qemu_irq
是一个 GPIO,其输出
部分连接到另一个 GPIO 的输入
部分。
我们的 EDC 设备正是如此。首先在 EDC 设备初始化期间:
static void cpiom_edc_init(Object *obj)
{
...
sysbus_init_irq(d, &s->irq);
...
}
sysbus_init_irq()
函数将注册 EDC irq 对象的 输出
部分。接下来,在 EDC 设备创建 (create_edc()
) 期间,提供给 sysbus_create_varargs
的参数是:
qdev_get_gpio_in_named(cpiom->intc, "ITN", INT_N_IT_EDC_ERR)
简而言之,它是属于cpiom->intc
设备的 qemu_irq
号 INT_N_IT_EDC_ERR
的一部分,该设备恰好是一个中断控制器(您可能会猜到,一个具有大量 irq 线的特殊设备)。
sysbus_create_varargs
函数将 sysbus_connect_irq()
这个 irq 与我们的 EDC 设备 qemu_irq
输出部分。 GPIO 连接代码如下所示:
qdev_connect_gpio_out_named(DEVICE(dev),
SYSBUS_DEVICE_GPIO_IRQ,
0,
qdev_get_gpio_in_named(cpiom->intc,
"ITN", INT_N_IT_EDC_ERR));
从逻辑上讲,当我们的设备想要引发 IRQ 时,它会 qemu_set_irq(irq,1)
自己的 qemu_irq
对象,以便连接的 qemu_irq
接收信号。
构建设备
不要忘记修复 /hw/cpiom/Makefile.objs
以添加要构建的设备对象文件 edc.o