This is a quick write-up on qemu riscv emulation. Qemu provides several APIs to create user defined boards(in qemu lingo, it’s called machine) and devices in this board.

risc virt Board Link to heading

virt machine is a basic machine to run riscv code with VIRTIO MMIO

RISC-V machine with 16550a UART and VirtIO MMIO

In hw/riscv/virt.c, The initialization code for the virt machine. virt_machine_typeinfo registers the type with qemu QOM using function virt_machine_class_init.

static const TypeInfo virt_machine_typeinfo = {
    .name       = MACHINE_TYPE_NAME("virt"),
    .parent     = TYPE_MACHINE,
    .class_init = virt_machine_class_init,
    .instance_init = virt_machine_instance_init,
    .instance_size = sizeof(RISCVVirtState),
    .interfaces = (InterfaceInfo[]) {
         { TYPE_HOTPLUG_HANDLER },
         { }
    },
};

static void virt_machine_init_register_types(void)
{
    type_register_static(&virt_machine_typeinfo);
}

type_init(virt_machine_init_register_types)

Let’s get virt_machine_instance_init out of the way.

static void virt_machine_instance_init(Object *obj)
{
    RISCVVirtState *s = RISCV_VIRT_MACHINE(obj);

    s->oem_id = g_strndup(ACPI_BUILD_APPNAME6, 6);
    s->oem_table_id = g_strndup(ACPI_BUILD_APPNAME8, 8);
    s->acpi = ON_OFF_AUTO_AUTO;
}

virt_machine_class_init is called on class initialization as part of QOM. And registers virt_machine_init for machine initialization and it’s does most of the heavy lifting.

static void virt_machine_class_init(ObjectClass *oc, void *data)
{
    char str[128];
    MachineClass *mc = MACHINE_CLASS(oc);
    HotplugHandlerClass *hc = HOTPLUG_HANDLER_CLASS(oc);

    mc->desc = "RISC-V VirtIO board";
    mc->init = virt_machine_init;

It also registers some class properties but defining their setters/getters. Anyway moving on.

    object_class_property_add_bool(oc, "aclint", virt_get_aclint,
                                   virt_set_aclint);
    object_class_property_set_description(oc, "aclint",
                                          "Set on/off to enable/disable "
                                          "emulating ACLINT devices");

    object_class_property_add_str(oc, "aia", virt_get_aia,
                                  virt_set_aia);
    object_class_property_set_description(oc, "aia",
                                          "Set type of AIA interrupt "
                                          "conttoller. Valid values are "
                                          "none, aplic, and aplic-imsic.");

    object_class_property_add_str(oc, "aia-guests",
                                  virt_get_aia_guests,
                                  virt_set_aia_guests);
    sprintf(str, "Set number of guest MMIO pages for AIA IMSIC. Valid value "
                 "should be between 0 and %d.", VIRT_IRQCHIP_MAX_GUESTS);
    object_class_property_set_description(oc, "aia-guests", str);
    object_class_property_add(oc, "acpi", "OnOffAuto",
                              virt_get_acpi, virt_set_acpi,
                              NULL, NULL);
    object_class_property_set_description(oc, "acpi",
                                          "Enable ACPI");

virt_machine_init Link to heading

virt_machine_init does a lot of things, but at the end it registers virt_machine_done to be called by qemu core to finish the initialization.

TODO

static void virt_machine_init(MachineState *machine)
{
    ...
    ...
    s->machine_done.notify = virt_machine_done;
    qemu_add_machine_init_done_notifier(&s->machine_done);
}

virt_machine_done Link to heading

In virt_machine_done, The actual firmware is loaded and control is passed back to qemu to start execution loop.

    const char *firmware_name = riscv_default_firmware_name(&s->soc[0]);
    ...

    firmware_end_addr = riscv_find_and_load_firmware(machine, firmware_name,
                                                     start_addr, NULL);

Note, firmware is opensbi defined hw/riscv/boot.c,

#define RISCV32_BIOS_BIN    "opensbi-riscv32-generic-fw_dynamic.bin"
#define RISCV64_BIOS_BIN    "opensbi-riscv64-generic-fw_dynamic.bin"

and riscv_load_firmware is eventually called.

target_ulong riscv_load_firmware(const char *firmware_filename,

                                 hwaddr firmware_load_addr,
                                 symbol_fn_t sym_cb)
{
    uint64_t firmware_entry, firmware_end;
    ssize_t firmware_size;

    g_assert(firmware_filename != NULL);

    if (load_elf_ram_sym(firmware_filename, NULL, NULL, NULL,
                         &firmware_entry, NULL, &firmware_end, NULL,
                         0, EM_RISCV, 1, 0, NULL, true, sym_cb) > 0) {
        return firmware_end;
    }

    firmware_size = load_image_targphys_as(firmware_filename,
                                           firmware_load_addr,
                                           current_machine->ram_size, NULL);

Back to virt_machine_done where device tree is loaded

    fdt_load_addr = riscv_compute_fdt_addr(memmap[VIRT_DRAM].base,
                                           memmap[VIRT_DRAM].size,
                                           machine);
    riscv_load_fdt(fdt_load_addr, machine->fdt);

    /* load the reset vector */
    riscv_setup_rom_reset_vec(machine, &s->soc[0], start_addr,
                              virt_memmap[VIRT_MROM].base,
                              virt_memmap[VIRT_MROM].size, kernel_entry,
                              fdt_load_addr);