【airbus-seclab/qemu_blog加工翻译 04】深入了解 QEMU:添加设备

在这篇文章中,我们将了解如何创建一个简单的新设备。其他帖子将专门讨论更复杂的设备,例如 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

对于机器,我们需要创建正确的 TypeInfoDeviceClass 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_varargsaddr 参数是 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_irqINT_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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值