Timeline
Timeline
2025-11-23
- init
This article introduces the core process and key technical points of QEMU hardware modeling. It first outlines the six stages of QEMU virtual machine creation: command-line parsing, Machine type selection and registration, object instantiation, hardware device initialization, accelerator and CPU initialization, and starting the virtual machine, covering key components such as the main() function, MachineClass, MachineState, and QOM. Then, using the PL011 serial port and the edu teaching device as examples, it analyzes in detail the implementation methods of peripheral device modeling, including device type definition, state structure design, class initialization, MMIO mapping, FIFO management, interrupt generation, DMA transfer, and background thread asynchronous processing. The article also deeply explores the complete interrupt modeling flow, from device-level triggering and PLIC arbitration to CPU interrupt injection and Guest OS handling, and mentions debugging and tracing methods. Overall, this article systematically demonstrates how QEMU accurately simulates hardware behavior through the QOM object model, providing support for full-stack development from bare-metal programs to the Linux kernel.
Environment
123456 | wget https://download.qemu.org/qemu-10.1.2.tar.xztar xvJf qemu-10.1.2.tar.xzcd qemu-10.1.2mkdir -p output./configure --prefix=$PWD/output --target-list=aarch64-softmmu,riscv64-softmmu --enable-debugbear -- make -j$(nproc) |
Create .clangd
123 | CompileFlags: Add: -Wno-unknown-warning-option Remove: [-m*, -f*] |
gdb
1 | $ gdb -args ./build/qemu-system-riscv64 -M virt -device edu,id=edu1 -nographic |
Machine Modeling
The core process of QEMU creating a virtual machine can be divided into 6 stages, each involving specific components and operations.
Command-line parsing
Entry function: main() Key operations:
- Parse the -M parameter to specify the machine type (e.g., virt)
- Initialize the QOM (QEMU Object Model) type system
- Create the global state machine MachineState
system/main.c
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051 | static void *qemu_default_main(void *opaque){ int status; replay_mutex_lock(); bql_lock(); status = qemu_main_loop(); qemu_cleanup(status); bql_unlock(); replay_mutex_unlock(); exit(status);}int (*qemu_main)(void);static int os_darwin_cfrunloop_main(void){ CFRunLoopRun(); g_assert_not_reached();}int (*qemu_main)(void) = os_darwin_cfrunloop_main;int main(int argc, char **argv){ qemu_init(argc, argv); bql_unlock(); replay_mutex_unlock(); if (qemu_main) { QemuThread main_loop_thread; qemu_thread_create(&main_loop_thread, "qemu_main", qemu_default_main, NULL, QEMU_THREAD_DETACHED); return qemu_main(); } else { qemu_default_main(NULL); g_assert_not_reached(); }} |
system/vl.c
12345678910111213141516171819 | void qemu_init(int argc, char **argv) { ... machine_opts_dict = qdict_new(); // Create the machine parameter dictionary ... case QEMU_OPTION_M: // Parse the -M parameter case QEMU_OPTION_machine: { bool help; keyval_parse_into(machine_opts_dict, optarg, "type", &help, &error_fatal); if (help) { machine_help_func(machine_opts_dict); exit(EXIT_SUCCESS); } break; } .. qemu_create_machine(machine_opts_dict); // Create the machine instance} |
Machine type selection and registration
Core component: MachineClass (machine class description)
Key operations:
- Match the user-specified machine type via select_machine()
- Look up the corresponding TypeImpl from the global type table type_table
12345678910111213141516171819202122232425262728293031323334 | // system/vl.cstatic MachineClass *select_machine(QDict *qdict, Error **errp){ ERRP_GUARD(); const char *machine_type = qdict_get_try_str(qdict, "type"); g_autoptr(GSList) machines = object_class_get_list(TYPE_MACHINE, false); MachineClass *machine_class = NULL; if (machine_type) { machine_class = find_machine(machine_type, machines); if (!machine_class) { error_setg(errp, "unsupported machine type: \"%s\"", machine_type); } qdict_del(qdict, "type"); } else { machine_class = find_default_machine(machines); if (!machine_class) { error_setg(errp, "No machine specified, and there is no default"); } } if (!machine_class) { error_append_hint(errp, "Use -machine help to list supported machines\n"); } return machine_class;}// qom/object.cGSList *object_class_get_list(const char *implements_type) { g_hash_table_foreach(type_table, object_class_foreach_tramp, &data); // Iterate over all registered machine types} |
Machine object instantiation
Core component: MachineState (machine runtime state)
Key operations:
- Through QOM’s object_new_with_class() instantiates the machine
- Initialize basic properties such as memory and CPU topology
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111 | // system/vl.cstatic void qemu_create_machine(QDict *qdict){ MachineClass *machine_class = select_machine(qdict, &error_fatal); object_set_machine_compat_props(machine_class->compat_props); current_machine = MACHINE(object_new_with_class(OBJECT_CLASS(machine_class))); object_property_add_child(object_get_root(), "machine", OBJECT(current_machine)); qemu_create_machine_containers(OBJECT(current_machine)); object_property_add_child(machine_get_container("unattached"), "sysbus", OBJECT(sysbus_get_default())); if (machine_class->minimum_page_bits) { if (!set_preferred_target_page_bits(machine_class->minimum_page_bits)) { /* This would be a board error: specifying a minimum smaller than * a target's compile-time fixed setting. */ g_assert_not_reached(); } } cpu_exec_init_all(); if (machine_class->hw_version) { qemu_set_hw_version(machine_class->hw_version); } /* * Get the default machine options from the machine if it is not already * specified either by the configuration file or by the command line. */ if (machine_class->default_machine_opts) { QDict *default_opts = keyval_parse(machine_class->default_machine_opts, NULL, NULL, &error_abort); qemu_apply_legacy_machine_options(default_opts); object_set_properties_from_keyval(OBJECT(current_machine), default_opts, false, &error_abort); qobject_unref(default_opts); }}// hw/core/machine.cstatic void machine_initfn(Object *obj){ MachineState *ms = MACHINE(obj); MachineClass *mc = MACHINE_GET_CLASS(obj); ms->dump_guest_core = true; ms->mem_merge = (QEMU_MADV_MERGEABLE != QEMU_MADV_INVALID); ms->enable_graphics = true; ms->kernel_cmdline = g_strdup(""); ms->ram_size = mc->default_ram_size; // Default memory size ms->maxram_size = mc->default_ram_size; if (mc->nvdimm_supported) { ms->nvdimms_state = g_new0(NVDIMMState, 1); object_property_add_bool(obj, "nvdimm", machine_get_nvdimm, machine_set_nvdimm); object_property_set_description(obj, "nvdimm", "Set on/off to enable/disable " "NVDIMM instantiation"); object_property_add_str(obj, "nvdimm-persistence", machine_get_nvdimm_persistence, machine_set_nvdimm_persistence); object_property_set_description(obj, "nvdimm-persistence", "Set NVDIMM persistence" "Valid values are cpu, mem-ctrl"); } if (mc->cpu_index_to_instance_props && mc->get_default_cpu_node_id) { ms->numa_state = g_new0(NumaState, 1); object_property_add_bool(obj, "hmat", machine_get_hmat, machine_set_hmat); object_property_set_description(obj, "hmat", "Set on/off to enable/disable " "ACPI Heterogeneous Memory Attribute " "Table (HMAT)"); } /* SPCR */ ms->acpi_spcr_enabled = true; object_property_add_bool(obj, "spcr", machine_get_spcr, machine_set_spcr); object_property_set_description(obj, "spcr", "Set on/off to enable/disable " "ACPI Serial Port Console Redirection " "Table (spcr)"); /* default to mc->default_cpus */ ms->smp.cpus = mc->default_cpus; // Default CPU count ms->smp.max_cpus = mc->default_cpus; ms->smp.drawers = 1; ms->smp.books = 1; ms->smp.sockets = 1; ms->smp.dies = 1; ms->smp.clusters = 1; ms->smp.modules = 1; ms->smp.cores = 1; ms->smp.threads = 1; for (int i = 0; i < CACHE_LEVEL_AND_TYPE__MAX; i++) { ms->smp_cache.props[i].cache = (CacheLevelAndType)i; ms->smp_cache.props[i].topology = CPU_TOPOLOGY_LEVEL_DEFAULT; } machine_copy_boot_config(ms, &(BootConfiguration){ 0 });} |
Hardware device initialization
Core phase: machine_run_board_init()
Key operations:
- Call the machine-specific init() method (e.g., RISC-V virt’s virt_machine_init())
- Initialize core components such as CPU, memory controller, and bus
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111 | // system/vl.cvoid machine_run_board_init(MachineState *machine, const char *mem_path, Error **errp){ ERRP_GUARD(); MachineClass *machine_class = MACHINE_GET_CLASS(machine); /* This checkpoint is required by replay to separate prior clock reading from the other reads, because timer polling functions query clock values from the log. */ replay_checkpoint(CHECKPOINT_INIT); if (!xen_enabled()) { /* On 32-bit hosts, QEMU is limited by virtual address space */ if (machine->ram_size > (2047 << 20) && HOST_LONG_BITS == 32) { error_setg(errp, "at most 2047 MB RAM can be simulated"); return; } } if (machine->memdev) { ram_addr_t backend_size = object_property_get_uint(OBJECT(machine->memdev), "size", &error_abort); if (backend_size != machine->ram_size) { error_setg(errp, "Machine memory size does not match the size of the memory backend"); return; } } else if (machine_class->default_ram_id && machine->ram_size && numa_uses_legacy_mem()) { if (object_property_find(object_get_objects_root(), machine_class->default_ram_id)) { error_setg(errp, "object's id '%s' is reserved for the default" " RAM backend, it can't be used for any other purposes", machine_class->default_ram_id); error_append_hint(errp, "Change the object's 'id' to something else or disable" " automatic creation of the default RAM backend by setting" " 'memory-backend=%s' with '-machine'.\n", machine_class->default_ram_id); return; } if (!machine_class->create_default_memdev(current_machine, mem_path, errp)) { return; } } if (machine->numa_state) { numa_complete_configuration(machine); if (machine->numa_state->num_nodes) { machine_numa_finish_cpu_init(machine); if (machine_class->cpu_cluster_has_numa_boundary) { validate_cpu_cluster_to_numa_boundary(machine); } } } if (!machine->ram && machine->memdev) { machine->ram = machine_consume_memdev(machine, machine->memdev); } /* Check if the CPU type is supported */ if (machine->cpu_type && !is_cpu_type_supported(machine, errp)) { return; } if (machine->cgs) { /* * With confidential guests, the host can't see the real * contents of RAM, so there's no point in it trying to merge * areas. */ machine_set_mem_merge(OBJECT(machine), false, &error_abort); /* * Virtio devices can't count on directly accessing guest * memory, so they need iommu_platform=on to use normal DMA * mechanisms. That requires also disabling legacy virtio * support for those virtio pci devices which allow it. */ object_register_sugar_prop(TYPE_VIRTIO_PCI, "disable-legacy", "on", true); object_register_sugar_prop(TYPE_VIRTIO_DEVICE, "iommu_platform", "on", false); } accel_init_interfaces(ACCEL_GET_CLASS(machine->accelerator)); machine_class->init(machine); // Call the machine-specific initialization function phase_advance(PHASE_MACHINE_INITIALIZED);}// hw/riscv/virt.c (RISC-V example)static void virt_machine_init(MachineState *machine) { // ... // 1. Initialize CPU topology for (int i = 0; i < smp_cpus; i++) { object_initialize_child(OBJECT(machine), "cpu", &s->soc[i], CPU_TYPE); } //... // 2. Initialize memory system memory_region_init_ram(&s->ram, "riscv_virt_board.ram", ram_size); //... // 3. Initialize peripheral bus sysbus_init_mmio(SYS_BUS_DEVICE(&s->plic), &s->plic_mmio); //... // 4. Create device tree (FDT) create_fdt(s); //...} |
Accelerator and CPU initialization
Core components: AccelState and CPUState
Key operations:
- Select KVM/TCG accelerator based on the -accel parameter
- Create vCPU threads and bind them to physical CPUs
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051 | // accel/accel-system.cint accel_init_machine(AccelState *accel, MachineState *ms){ AccelClass *acc = ACCEL_GET_CLASS(accel); int ret; ms->accelerator = accel; *(acc->allowed) = true; ret = acc->init_machine(accel, ms);// Initialize accelerator if (ret < 0) { ms->accelerator = NULL; *(acc->allowed) = false; object_unref(OBJECT(accel)); } else { object_set_accelerator_compat_props(acc->compat_props); } return ret;}// accel/kvm/kvm-all.c (KVM example)static void kvm_init(MachineState *ms) { kvm_state->fd = open("/dev/kvm", O_RDWR); // Open KVM device kvm_state->vmfd = ioctl(kvm_state->fd, KVM_CREATE_VM); // Create virtual machine}// system/cpus.cvoid qemu_init_vcpu(CPUState *cpu){ MachineState *ms = MACHINE(qdev_get_machine()); cpu->nr_threads = ms->smp.threads; cpu->stopped = true; cpu->random_seed = qemu_guest_random_seed_thread_part1(); if (!cpu->as) { /* If the target cpu hasn't set up any address spaces itself, * give it the default one. */ cpu->num_ases = 1; cpu_address_space_init(cpu, 0, "cpu-memory", cpu->memory); } /* accelerators all implement the AccelOpsClass */ g_assert(cpus_accel != NULL && cpus_accel->create_vcpu_thread != NULL); cpus_accel->create_vcpu_thread(cpu); while (!cpu->created) { qemu_cond_wait(&qemu_cpu_cond, &bql); }} |
Start virtual machine
Final phase: qemu_main_loop()


system/runstate.c
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970 | int qemu_main_loop(void){ int status = EXIT_SUCCESS; while (!main_loop_should_exit(&status)) { main_loop_wait(false); } return status;}static bool main_loop_should_exit(int *status){ RunState r; ShutdownCause request; if (qemu_debug_requested()) { vm_stop(RUN_STATE_DEBUG); } if (qemu_suspend_requested()) { qemu_system_suspend(); } request = qemu_shutdown_requested(); if (request) { qemu_kill_report(); qemu_system_shutdown(request); if (shutdown_action == SHUTDOWN_ACTION_PAUSE) { vm_stop(RUN_STATE_SHUTDOWN); } else { if (shutdown_exit_code != EXIT_SUCCESS) { *status = shutdown_exit_code; } else if (request == SHUTDOWN_CAUSE_GUEST_PANIC && panic_action == PANIC_ACTION_EXIT_FAILURE) { *status = EXIT_FAILURE; } return true; } } request = qemu_reset_requested(); if (request) { pause_all_vcpus(); qemu_system_reset(request); resume_all_vcpus(); /* * runstate can change in pause_all_vcpus() * as iothread mutex is unlocked */ if (!runstate_check(RUN_STATE_RUNNING) && !runstate_check(RUN_STATE_INMIGRATE) && !runstate_check(RUN_STATE_FINISH_MIGRATE)) { runstate_set(RUN_STATE_PRELAUNCH); } } if (qemu_wakeup_requested()) { pause_all_vcpus(); qemu_system_wakeup(); notifier_list_notify(&wakeup_notifiers, &wakeup_reason); wakeup_reason = QEMU_WAKEUP_REASON_NONE; resume_all_vcpus(); qapi_event_send_wakeup(); } if (qemu_powerdown_requested()) { qemu_system_powerdown(); } if (qemu_vmstop_requested(&r)) { vm_stop(r); } return false;} |
Peripheral Modeling (PL011 UART)
The modeling of the PL011 UART in QEMU fully demonstrates the core process of hardware peripheral emulation:
- Type system integration: implements the device class inheritance hierarchy through QOM
- Accurate state modeling: full-cycle management of registers, FIFO, and interrupt state
- Hardware interface implementation: MMIO callbacks handle CPU access, character backend connects to host I/O
- Interrupt system integration: works with the platform interrupt controller (PLIC)
- Device lifecycle: implements the complete lifecycle such as reset, migration, and destruction

Device type definition
Core mechanism: QEMU Object Model (QOM)
Key operations:
- Define the TypeInfo structure to describe the device type
- Implement class initialization (class_init) and instance initialization (instance_init)
- Register into the global type system
12345678910111213141516 | // hw/char/pl011.cstatic const TypeInfo pl011_arm_info = { .name = TYPE_PL011, .parent = TYPE_SYS_BUS_DEVICE, .instance_size = sizeof(PL011State), .instance_init = pl011_init, .class_init = pl011_class_init,};static void pl011_register_types(void){ type_register_static(&pl011_arm_info); type_register_static(&pl011_luminary_info); // pl011_luminary_info (Luminary Micro special version, Luminary was acquired by TI, an early ARM SoC)}type_init(pl011_register_types) |
Device state structure design
Core component: PL011State
Key fields:
- Register state (lcr, imsc, etc.)
- FIFO buffer
- Interrupt signal line
- Character backend (CharBackend)
1234567891011121314151617181920212223242526272829303132 | // hw/char/pl011.cstruct PL011State { SysBusDevice parent_obj; MemoryRegion iomem; uint32_t flags; uint32_t lcr; // Line control register uint32_t rsr; // Receive status/error clear register uint32_t cr; // Control register uint32_t dmacr; uint32_t int_enabled; uint32_t int_level; uint32_t read_fifo[PL011_FIFO_DEPTH]; /* FIFO buffer */ uint32_t ilpr; uint32_t ibrd; uint32_t fbrd; uint32_t ifl; int read_pos; int read_count; int read_trigger; CharBackend chr; // Connect host terminal or file qemu_irq irq[6]; // Interrupt signal lines (PL011 supports 6 interrupt sources) Clock *clk; bool migrate_clk; const unsigned char *id; /* * Since some users embed this struct directly, we must * ensure that the C struct is at least as big as the Rust one. */ uint8_t padding_for_rust[16];}; |
Device class initialization
Core tasks:
Set device properties
Bind realize and reset methods
Define device migration support
12345678910111213141516 | // hw/char/pl011.cstatic void pl011_class_init(ObjectClass *oc, const void *data){ DeviceClass *dc = DEVICE_CLASS(oc); dc->realize = pl011_realize; device_class_set_legacy_reset(dc, pl011_reset); dc->vmsd = &vmstate_pl011; device_class_set_props(dc, pl011_properties);}static const Property pl011_properties[] = { DEFINE_PROP_CHR("chardev", PL011State, chr), DEFINE_PROP_BOOL("migrate-clk", PL011State, migrate_clk, true),}; |
Device instantiation and hardware connection
Core operations:
- Initialize memory region (MemoryRegion)
- Register MMIO callback functions
- Connect interrupt signals
- Bind character device backend
123456789101112131415161718192021222324252627282930 | // hw/char/pl011.cstatic void pl011_init(Object *obj){ SysBusDevice *sbd = SYS_BUS_DEVICE(obj); PL011State *s = PL011(obj); int i; /* Initialize memory region */ memory_region_init_io(&s->iomem, OBJECT(s), &pl011_ops, s, "pl011", 0x1000); sysbus_init_mmio(sbd, &s->iomem); for (i = 0; i < ARRAY_SIZE(s->irq); i++) { /* Initialize interrupts */ sysbus_init_irq(sbd, &s->irq[i]); } s->clk = qdev_init_clock_in(DEVICE(obj), "clk", pl011_clock_update, s, ClockUpdate); s->id = pl011_id_arm;}// hw/char/pl011.cstatic void pl011_realize(DeviceState *dev, Error **errp){ PL011State *s = PL011(dev); /* Connect character device */ qemu_chr_fe_set_handlers(&s->chr, pl011_can_receive, pl011_receive, pl011_event, NULL, s, NULL, true);} |
Implement device functional logic
Core components:
- MMIO read/write callbacks
- Interrupt generation logic
- FIFO management
- Character device communication
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091 | static void pl011_write(void *opaque, hwaddr offset, uint64_t value, unsigned size){ PL011State *s = (PL011State *)opaque; unsigned char ch; trace_pl011_write(offset, value, pl011_regname(offset)); switch (offset >> 2) { case 0: /* UARTDR */ // Data register ch = value; pl011_write_txdata(s, ch); break; case 1: /* UARTRSR/UARTECR */ s->rsr = 0; break; case 6: /* UARTFR */ /* Writes to Flag register are ignored. */ break; case 8: /* UARTILPR */ s->ilpr = value; break; case 9: /* UARTIBRD */ s->ibrd = value & IBRD_MASK; pl011_trace_baudrate_change(s); break; case 10: /* UARTFBRD */ s->fbrd = value & FBRD_MASK; pl011_trace_baudrate_change(s); break; case 11: /* UARTLCR_H */ /* Reset the FIFO state on FIFO enable or disable */ if ((s->lcr ^ value) & LCR_FEN) { pl011_reset_rx_fifo(s); pl011_reset_tx_fifo(s); } if ((s->lcr ^ value) & LCR_BRK) { int break_enable = value & LCR_BRK; qemu_chr_fe_ioctl(&s->chr, CHR_IOCTL_SERIAL_SET_BREAK, &break_enable); pl011_loopback_break(s, break_enable); } s->lcr = value; pl011_set_read_trigger(s); break; case 12: /* UARTCR */ /* ??? Need to implement the enable bit. */ s->cr = value; pl011_loopback_mdmctrl(s); break; case 13: /* UARTIFS */ s->ifl = value; pl011_set_read_trigger(s); break; case 14: /* UARTIMSC */ s->int_enabled = value; pl011_update(s); break; case 17: /* UARTICR */ s->int_level &= ~value; pl011_update(s); break; case 18: /* UARTDMACR */ s->dmacr = value; if (value & 3) { qemu_log_mask(LOG_UNIMP, "pl011: DMA not implemented\n"); } break; default: qemu_log_mask(LOG_GUEST_ERROR, "pl011_write: Bad offset 0x%x\n", (int)offset); }}static void pl011_receive(void *opaque, const uint8_t *buf, int size){ trace_pl011_receive(size); /* * In loopback mode, the RX input signal is internally disconnected * from the entire receiving logics; thus, all inputs are ignored, * and BREAK detection on RX input signal is also not performed. */ if (pl011_loopback_enabled(opaque)) { return; } for (int i = 0; i < size; i++) { pl011_fifo_rx_put(opaque, buf[i]); }} |
Interrupt handling and status update
1234567891011 | static void pl011_update(PL011State *s){ uint32_t flags; int i; flags = s->int_level & s->int_enabled; trace_pl011_irq_state(flags != 0); for (i = 0; i < ARRAY_SIZE(s->irq); i++) { qemu_set_irq(s->irq[i], (flags & irqmask[i]) != 0); /* Update interrupt line */ }} |
A simple code example for PL011 device modeling
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161 | /* hw/char/pl011.c - Full implementation of PL011 UART *//* Register offset definitions */enum { PL011_DR = 0x00, // Data register PL011_FR = 0x18, // Flag register PL011_ILPR = 0x20, // Low-power register PL011_IBRD = 0x24, // Integer baud rate PL011_FBRD = 0x28, // Fractional baud rate PL011_LCRH = 0x2C, // Line control PL011_CR = 0x30, // Control register PL011_IMSC = 0x38, // Interrupt mask};/* Device state structure */typedef struct PL011State { SysBusDevice parent_obj; MemoryRegion iomem; CharBackend chr; qemu_irq irq; /* Register state */ uint32_t readbuff; uint32_t flags; uint32_t lcr; uint32_t cr; uint32_t imsc; uint32_t int_level; /* FIFO state */ uint8_t read_fifo[PL011_FIFO_DEPTH]; uint32_t read_pos; uint32_t read_count; uint32_t read_trigger;} PL011State;/* MMIO operation callbacks */static const MemoryRegionOps pl011_ops = { .read = pl011_read, .write = pl011_write, .endianness = DEVICE_NATIVE_ENDIAN, .impl.min_access_size = 4, .impl.max_access_size = 4,};/* Read operation implementation */static uint64_t pl011_read(void *opaque, hwaddr offset, unsigned size) { PL011State *s = opaque; uint32_t ret = 0; switch (offset) { case PL011_DR: if (s->read_count > 0) { ret = s->read_fifo[s->read_pos]; s->read_pos = (s->read_pos + 1) % PL011_FIFO_DEPTH; s->read_count--; pl011_update(s); // Update interrupt state } break; case PL011_FR: ret = s->flags; break; case PL011_IMSC: ret = s->imsc; break; } return ret;}/* Write operation implementation */static void pl011_write(void *opaque, hwaddr offset, uint64_t value, unsigned size) { PL011State *s = opaque; switch (offset) { case PL011_DR: qemu_chr_fe_write(&s->chr, (uint8_t*)&value, 1); break; case PL011_LCRH: s->lcr = value; break; case PL011_IMSC: s->imsc = value; pl011_update(s); break; }}/* Character device receive callback */static void pl011_receive(void *opaque, const uint8_t *buf, int size) { PL011State *s = opaque; if (s->read_count < PL011_FIFO_DEPTH) { int slot = (s->read_pos + s->read_count) % PL011_FIFO_DEPTH; s->read_fifo[slot] = *buf; s->read_count++; if (s->read_count >= s->read_trigger) { s->int_level |= INT_RX; qemu_set_irq(s->irq, 1); } }}/* Interrupt state update */static void pl011_update(PL011State *s) { uint32_t int_level = 0; // Receive interrupt (RX) if ((s->imsc & INT_RX) && (s->read_count > 0)) { int_level |= INT_RX; } // Transmit interrupt (TX) if ((s->imsc & INT_TX) && (s->xmit_count < PL011_FIFO_DEPTH)) { int_level |= INT_TX; } qemu_set_irq(s->irq, int_level ? 1 : 0);}/* Device reset */static void pl011_reset(DeviceState *dev) { PL011State *s = PL011(dev); s->lcr = 0; s->cr = 0x300; s->imsc = 0; s->read_count = 0; s->read_pos = 0; s->int_level = 0; qemu_set_irq(s->irq, 0);}/* Device initialization */static void pl011_realize(DeviceState *dev, Error **errp) { PL011State *s = PL011(dev); SysBusDevice *sbd = SYS_BUS_DEVICE(dev); memory_region_init_io(&s->iomem, OBJECT(s), &pl011_ops, s, "pl011", 0x1000); sysbus_init_mmio(sbd, &s->iomem); sysbus_init_irq(sbd, &s->irq); qemu_chr_fe_set_handlers(&s->chr, pl011_can_receive, pl011_receive, NULL, NULL, s, NULL, true);}/* Type registration */static const TypeInfo pl011_info = { .name = TYPE_PL011, .parent = TYPE_SYS_BUS_DEVICE, .instance_size = sizeof(PL011State), .class_init = pl011_class_init,};static void pl011_register_types(void) { type_register_static(&pl011_info);}type_init(pl011_register_types) |
Instantiating PL011 in the RISC-V virt machine:
12345678910111213141516171819 | /* hw/riscv/virt.c - Machine initialization */static void virt_machine_init(MachineState *machine) { // ... /* Create PL011 serial port */ DeviceState *dev = qdev_new(TYPE_PL011); qdev_prop_set_chr(dev, "chardev", serial_hd(0)); sysbus_realize_and_unref(SYS_BUS_DEVICE(dev), &error_fatal); sysbus_mmio_map(SYS_BUS_DEVICE(dev), 0, 0x10000000); // Map to 0x10000000 sysbus_connect_irq(SYS_BUS_DEVICE(dev), 0, plic_irq[UART0_IRQ]); // Connect interrupt // Add device tree node qemu_fdt_add_subnode(fdt, "/soc/serial"); qemu_fdt_setprop_string(fdt, "/soc/serial", "compatible", "arm,pl011"); qemu_fdt_setprop_cells(fdt, "/soc/serial", "reg", 0x10000000, 0x1000); qemu_fdt_setprop_cells(fdt, "/soc/serial", "interrupts", UART0_IRQ);} |
Key implementation points
Precise register modeling:
- Implement precise behavior of all 32 registers
- Handle special bits (e.g., FIFO enable bit, interrupt mask bit)
- Support baud rate calculation (IBRD/FBRD)
FIFO state machine:
12345678910111213 | static void fifo_push(PL011State *s, uint32_t value) { if (s->read_count < PL011_FIFO_DEPTH) { uint32_t slot = (s->read_pos + s->read_count) % PL011_FIFO_DEPTH; s->read_fifo[slot] = value; s->read_count++; // Interrupt trigger conditions if (s->read_count >= s->read_trigger) { s->int_level |= INT_RX; qemu_set_irq(s->irq, 1); } }} |
Clock domain handling:
- Implement pl011_clock_update callback
- Handle baud rate change events
- Support dynamic clock adjustment
Migration support:
1234567891011 | cstatic const VMStateDescription vmstate_pl011 = { .name = "pl011", .version_id = 2, .fields = (VMStateField[]) { VMSTATE_UINT32(lcr, PL011State), VMSTATE_UINT32_ARRAY(read_fifo, PL011State, PL011_FIFO_DEPTH), VMSTATE_UINT32(read_pos, PL011State), VMSTATE_UINT32(read_count, PL011State), VMSTATE_END_OF_LIST() }}; |
Performance optimization:
- Use bit operations instead of division to calculate baud rate
- Use masks instead of modulo operations for FIFO operations
- Cache interrupt status to reduce computation count
This modeling approach enables PL011 to accurately simulate real hardware behavior in QEMU, supporting full-stack development from bare-metal programs to the Linux kernel.
Peripheral modeling (edu device)
In a computer system, the interaction between CPU and peripherals is essentially address access and interrupt notification:
- Address access: CPU reads and writes device registers through MMIO (Memory-Mapped I/O) or PIO (Port I/O)
- Interrupt notification: Peripherals send event signals to the CPU via the interrupt controller
12345678 | // Pseudocode: CPU and device interaction flowwhile (1) { if (中断触发) { 读取设备状态寄存器; 处理数据; } 写入控制寄存器启动操作;} |
Introduction to the edu device
edu is an official QEMU teaching virtual device, whose functional design is streamlined yet complete:
- Basic functionality: implements factorial calculation (fact register)
- Advanced functionality: DMA transfer, interrupt triggering
- Typical application: demonstrates full lifecycle management of PCI devices
Define device properties
Key point: By inheriting PCIDevice, it automatically gains PCI configuration space management capability.
1234567891011121314151617181920212223242526272829303132 | struct EduState { PCIDevice pdev; // Inherit the PCI device base class MemoryRegion mmio;// MMIO memory region QemuThread thread; // Background computation thread QemuMutex thr_mutex;// Thread lock QemuCond thr_cond; bool stopping; // Thread stop flag uint32_t addr4; uint32_t fact; uint32_t status; // Status register (bit0=computing, bit7=interrupt enable) uint32_t irq_status; struct dma_state { dma_addr_t src; dma_addr_t dst; dma_addr_t cnt; dma_addr_t cmd; } dma; QEMUTimer dma_timer; char dma_buf[DMA_SIZE]; uint64_t dma_mask;}; |
Initialize PCI configuration space
1234567891011121314 | static void edu_class_init(ObjectClass *class, const void *data){ DeviceClass *dc = DEVICE_CLASS(class); PCIDeviceClass *k = PCI_DEVICE_CLASS(class); k->realize = pci_edu_realize; k->exit = pci_edu_uninit; k->vendor_id = PCI_VENDOR_ID_QEMU;// Vendor ID k->device_id = 0x11e8;// Device ID k->revision = 0x10; k->class_id = PCI_CLASS_OTHERS; // Device class set_bit(DEVICE_CATEGORY_MISC, dc->categories);} |
This step allows the operating system to identify the device as “Edu Device” via PCI.
Map MMIO region
12345678910111213141516171819202122 | static void pci_edu_realize(PCIDevice *pdev, Error **errp){ EduState *edu = EDU(pdev); uint8_t *pci_conf = pdev->config; pci_config_set_interrupt_pin(pci_conf, 1); if (msi_init(pdev, 0, 1, true, false, errp)) { return; } timer_init_ms(&edu->dma_timer, QEMU_CLOCK_VIRTUAL, edu_dma_timer, edu); qemu_mutex_init(&edu->thr_mutex); qemu_cond_init(&edu->thr_cond); qemu_thread_create(&edu->thread, "edu", edu_fact_thread, edu, QEMU_THREAD_JOINABLE); memory_region_init_io(&edu->mmio, OBJECT(edu), &edu_mmio_ops, edu, "edu-mmio", 1 * MiB);// 1MB address space pci_register_bar(pdev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &edu->mmio);} |
Address layout:
1234 | 0x00 : 阶乘输入寄存器 (可写)0x04 : 阶乘结果寄存器 (只读)0x80 : DMA 源地址寄存器0x88 : DMA 目标地址寄存器 |
Implement MMIO callback functions
Taking factorial calculation as an example:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111 | static uint64_t edu_mmio_read(void *opaque, hwaddr addr, unsigned size){ EduState *edu = opaque; uint64_t val = ~0ULL; if (addr < 0x80 && size != 4) { return val; } if (addr >= 0x80 && size != 4 && size != 8) { return val; } switch (addr) { case 0x00: val = 0x010000edu; break; case 0x04:// Read result register val = edu->addr4; break; case 0x08: qemu_mutex_lock(&edu->thr_mutex); val = edu->fact; qemu_mutex_unlock(&edu->thr_mutex); break; case 0x20: val = qatomic_read(&edu->status); break; case 0x24: val = edu->irq_status; break; case 0x80: dma_rw(edu, false, &val, &edu->dma.src, false); break; case 0x88: dma_rw(edu, false, &val, &edu->dma.dst, false); break; case 0x90: dma_rw(edu, false, &val, &edu->dma.cnt, false); break; case 0x98: dma_rw(edu, false, &val, &edu->dma.cmd, false); break; } return val;}static void edu_mmio_write(void *opaque, hwaddr addr, uint64_t val, unsigned size){ EduState *edu = opaque; if (addr < 0x80 && size != 4) { return; } if (addr >= 0x80 && size != 4 && size != 8) { return; } switch (addr) { case 0x04: edu->addr4 = ~val; break; case 0x08: if (qatomic_read(&edu->status) & EDU_STATUS_COMPUTING) { break; } /* EDU_STATUS_COMPUTING cannot go 0->1 concurrently, because it is only * set in this function and it is under the iothread mutex. */ qemu_mutex_lock(&edu->thr_mutex); edu->fact = val; qatomic_or(&edu->status, EDU_STATUS_COMPUTING); qemu_cond_signal(&edu->thr_cond); qemu_mutex_unlock(&edu->thr_mutex); break; case 0x20: if (val & EDU_STATUS_IRQFACT) { qatomic_or(&edu->status, EDU_STATUS_IRQFACT); /* Order check of the COMPUTING flag after setting IRQFACT. */ smp_mb__after_rmw(); } else { qatomic_and(&edu->status, ~EDU_STATUS_IRQFACT); } break; case 0x60: edu_raise_irq(edu, val); break; case 0x64: edu_lower_irq(edu, val); break; case 0x80: dma_rw(edu, true, &val, &edu->dma.src, false); break; case 0x88: dma_rw(edu, true, &val, &edu->dma.dst, false); break; case 0x90: dma_rw(edu, true, &val, &edu->dma.cnt, false); break; case 0x98: if (!(val & EDU_DMA_RUN)) { break; } dma_rw(edu, true, &val, &edu->dma.cmd, true); break; }} |
Implement background computation thread
Key design: avoid blocking the main thread, use condition variables for asynchronous computation.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051 | /* * We purposely use a thread, so that users are forced to wait for the status * register. */static void *edu_fact_thread(void *opaque){ EduState *edu = opaque; while (1) { uint32_t val, ret = 1; qemu_mutex_lock(&edu->thr_mutex); while ((qatomic_read(&edu->status) & EDU_STATUS_COMPUTING) == 0 && !edu->stopping) { qemu_cond_wait(&edu->thr_cond, &edu->thr_mutex); } if (edu->stopping) { qemu_mutex_unlock(&edu->thr_mutex); break; } val = edu->fact; qemu_mutex_unlock(&edu->thr_mutex); while (val > 0) { ret *= val--; } /* * We should sleep for a random period here, so that students are * forced to check the status properly. */ qemu_mutex_lock(&edu->thr_mutex); edu->fact = ret; qemu_mutex_unlock(&edu->thr_mutex); qatomic_and(&edu->status, ~EDU_STATUS_COMPUTING); /* Clear COMPUTING flag before checking IRQFACT. */ smp_mb__after_rmw(); if (qatomic_read(&edu->status) & EDU_STATUS_IRQFACT) { bql_lock(); edu_raise_irq(edu, FACT_IRQ);//Trigger interrupt bql_unlock(); } } return NULL;} |
Interrupt trigger mechanism
123456789101112 | static void edu_raise_irq(EduState *edu, uint32_t val){ edu->irq_status |= val; if (edu->irq_status) { if (edu_msi_enabled(edu)) {// Use MSI interrupts msi_notify(&edu->pdev, 0); } else {// Use traditional INTx interrupts pci_set_irq(&edu->pdev, 1); } }} |
DMA transfer implementation
12345678910111213141516171819202122232425262728293031323334 | static void edu_dma_timer(void *opaque){ EduState *edu = opaque; bool raise_irq = false; if (!(edu->dma.cmd & EDU_DMA_RUN)) { return; } // Perform DMA copy if (EDU_DMA_DIR(edu->dma.cmd) == EDU_DMA_FROM_PCI) { uint64_t dst = edu->dma.dst; edu_check_range(dst, edu->dma.cnt, DMA_START, DMA_SIZE); dst -= DMA_START; pci_dma_read(&edu->pdev, edu_clamp_addr(edu, edu->dma.src), edu->dma_buf + dst, edu->dma.cnt); } else { uint64_t src = edu->dma.src; edu_check_range(src, edu->dma.cnt, DMA_START, DMA_SIZE); src -= DMA_START; pci_dma_write(&edu->pdev, edu_clamp_addr(edu, edu->dma.dst), edu->dma_buf + src, edu->dma.cnt); } edu->dma.cmd &= ~EDU_DMA_RUN; if (edu->dma.cmd & EDU_DMA_IRQ) { raise_irq = true; } if (raise_irq) { // Trigger interrupt edu_raise_irq(edu, DMA_IRQ); }} |
Device interaction from the guest’s perspective
When the guest program’s driver accesses the edu device:
- Probe device: identify by PCI ID 0x1234:11e8
- Map MMIO: ioremap() gets the virtual address of registers
- Calculate factorial
- Handle interrupt: clear status flags in the interrupt service routine
Key points summary
edu encapsulates the 5 core elements of peripheral modeling:
1 | 寄存器操作 → 中断通知 → DMA 传输 → 多线程协同 → PCI 规范 |
Mainly involves:
- State machine model: represent device state through register bits (e.g., computing/completed)
- Asynchronous processing: hand off time-consuming operations to a background thread to avoid blocking the vCPU
- Address isolation: each device has its own independent MMIO space
- Interrupt abstraction: supports both traditional INTx and modern MSI modes
- DMA security: address validation prevents virtual machine escape
Interrupt modeling

- Device layer: PL011 triggers an interrupt signal based on hardware state
- Interrupt controller layer: PLIC implements priority arbitration and interrupt distribution
- CPU layer: RISC-V architecture responds to external interrupts and switches context
- Guest layer: The guest program or operating system’s interrupt handler completes device service
Below, we analyze the process step by step:
PL011 triggers an interrupt
12345678910111213 | // hw/char/pl011.cstatic void pl011_update(PL011State *s){ uint32_t flags; int i; flags = s->int_level & s->int_enabled; trace_pl011_irq_state(flags != 0); for (i = 0; i < ARRAY_SIZE(s->irq); i++) { qemu_set_irq(s->irq[i], (flags & irqmask[i]) != 0);// Trigger interrupt }} |
s->irq is connected to a specific interrupt line of the PLIC (e.g., UART0_IRQ=10), and the PLIC then routes the interrupt to the CPU.
PLIC receives the interrupt
PLIC’s GPIO input handler updates the interrupt status:
12345678910111213141516 | // hw/intc/sifive_plic.cstatic void sifive_plic_irq_request(void *opaque, int irq, int level){ SiFivePLICState *s = opaque; if (level > 0) { sifive_plic_set_pending(s, irq, true);// Set the pending bit sifive_plic_update(s); // Trigger update }}static void sifive_plic_set_pending(SiFivePLICState *plic, int irq, bool level){ atomic_set_masked(&plic->pending[irq >> 5], 1 << (irq & 31), -!!level);} |
PLIC arbitration logic
In sifive_plic_In update(), priority arbitration is implemented:
1234567891011121314151617181920212223 | static void sifive_plic_update(SiFivePLICState *plic){ int addrid; /* raise irq on harts where this irq is enabled */ for (addrid = 0; addrid < plic->num_addrs; addrid++) { uint32_t hartid = plic->addr_config[addrid].hartid; PLICMode mode = plic->addr_config[addrid].mode; bool level = !!sifive_plic_claimed(plic, addrid); switch (mode) { case PLICMode_M: qemu_set_irq(plic->m_external_irqs[hartid - plic->hartid_base], level); break; case PLICMode_S: qemu_set_irq(plic->s_external_irqs[hartid - plic->hartid_base], level); break; default: break; } }} |
CPU interrupt injection
Set the CPU interrupt request flag via cpu_interrupt:
1234567891011121314151617181920212223 | // system/cpus.cvoid cpu_interrupt(CPUState *cpu, int mask){ g_assert(bql_locked()); cpus_accel->handle_interrupt(cpu, mask);}// accel/tcg/tcg-accel-ops.cvoid tcg_handle_interrupt(CPUState *cpu, int mask){ cpu->interrupt_request |= mask; /* * If called from iothread context, wake the target cpu in * case its halted. */ if (!qemu_cpu_is_self(cpu)) { qemu_cpu_kick(cpu); } else { qatomic_set(&cpu->neg.icount_decr.u16.high, -1); }} |
- Set CPU_INTERRUPT_HARD flag
- Modify icount to force exit the current TB
CPU interrupt check
Detect interrupt requests in the vCPU main loop:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106 | static inline bool cpu_handle_interrupt(CPUState *cpu, TranslationBlock **last_tb){ /* * If we have requested custom cflags with CF_NOIRQ we should * skip checking here. Any pending interrupts will get picked up * by the next TB we execute under normal cflags. */ if (cpu->cflags_next_tb != -1 && cpu->cflags_next_tb & CF_NOIRQ) { return false; } /* Clear the interrupt flag now since we're processing * cpu->interrupt_request and cpu->exit_request. * Ensure zeroing happens before reading cpu->exit_request or * cpu->interrupt_request (see also smp_wmb in cpu_exit()) */ qatomic_set_mb(&cpu->neg.icount_decr.u16.high, 0); if (unlikely(qatomic_read(&cpu->interrupt_request))) { int interrupt_request; bql_lock(); interrupt_request = cpu->interrupt_request; if (unlikely(cpu->singlestep_enabled & SSTEP_NOIRQ)) { /* Mask out external interrupts for this step. */ interrupt_request &= ~CPU_INTERRUPT_SSTEP_MASK; } if (interrupt_request & CPU_INTERRUPT_DEBUG) { cpu->interrupt_request &= ~CPU_INTERRUPT_DEBUG; cpu->exception_index = EXCP_DEBUG; bql_unlock(); return true; } if (replay_mode == REPLAY_MODE_PLAY && !replay_has_interrupt()) { /* Do nothing */ } else if (interrupt_request & CPU_INTERRUPT_HALT) { replay_interrupt(); cpu->interrupt_request &= ~CPU_INTERRUPT_HALT; cpu->halted = 1; cpu->exception_index = EXCP_HLT; bql_unlock(); return true; } else { const TCGCPUOps *tcg_ops = cpu->cc->tcg_ops; if (interrupt_request & CPU_INTERRUPT_RESET) { replay_interrupt(); tcg_ops->cpu_exec_reset(cpu); bql_unlock(); return true; } /* * The target hook has 3 exit conditions: * False when the interrupt isn't processed, * True when it is, and we should restart on a new TB, * and via longjmp via cpu_loop_exit. */ if (tcg_ops->cpu_exec_interrupt(cpu, interrupt_request)) {// Call architecture-specific interrupt handling if (!tcg_ops->need_replay_interrupt || tcg_ops->need_replay_interrupt(interrupt_request)) { replay_interrupt(); } /* * After processing the interrupt, ensure an EXCP_DEBUG is * raised when single-stepping so that GDB doesn't miss the * next instruction. */ if (unlikely(cpu->singlestep_enabled)) { cpu->exception_index = EXCP_DEBUG; bql_unlock(); return true; } cpu->exception_index = -1; *last_tb = NULL; } /* The target hook may have updated the 'cpu->interrupt_request'; * reload the 'interrupt_request' value */ interrupt_request = cpu->interrupt_request; } if (interrupt_request & CPU_INTERRUPT_EXITTB) { cpu->interrupt_request &= ~CPU_INTERRUPT_EXITTB; /* ensure that no TB jump will be modified as the program flow was changed */ *last_tb = NULL; } /* If we exit via cpu_loop_exit/longjmp it is reset in cpu_exec */ bql_unlock(); } /* Finally, check if we need to exit to the main loop. */ if (unlikely(qatomic_read(&cpu->exit_request)) || icount_exit_request(cpu)) { qatomic_set(&cpu->exit_request, 0); if (cpu->exception_index == -1) { cpu->exception_index = EXCP_INTERRUPT; } return true; } return false;} |
RISC-V interrupt handling
riscv_cpu_do_interrupt implements interrupt context switching:
123456789101112131415161718192021222324 | // target/riscv/cpu_helper.cvoid riscv_cpu_do_interrupt(CPUState *cs){ RISCVCPU *cpu = RISCV_CPU(cs); CPURISCVState *env = &cpu->env; // 1. Save PC to mepc env->mepc = env->pc; // 2. Set interrupt cause env->mcause = MCAUSE_INTR | (IRQ_M_EXT << MCAUSE_CODE_SHIFT); // 3. Jump to interrupt vector if (env->mtvec & MTVEC_MODE_MASK) { // Vectored mode env->pc = (env->mtvec & ~MTVEC_MODE_MASK) + 4 * cause; } else { // Direct mode env->pc = env->mtvec & ~MTVEC_MODE_MASK; } // 4. Update privilege level env->priv = PRV_M;} |
Guest OS interrupt handling
Take the Linux interrupt handling flow as an example:
1234567891011121314151617181920212223 | // arch/riscv/kernel/entry.SENTRY(handle_arch_irq) SAVE_ALL csrr a0, mcause csrr a1, mepc mv a2, sp call riscv_intc_irq // Jump to the C handler RESTORE_ALLEND(handle_arch_irq)// drivers/irqchip/irq-sifive-plic.cstatic void plic_irq_handler(struct irq_desc *desc){ u32 hwirq = readl(plic->regs + PRIORITY_THRESHOLD); struct irq_chip *chip = irq_desc_get_chip(desc); chained_irq_enter(chip, desc); generic_handle_irq(irq_find_mapping(domain, hwirq)); chained_irq_exit(chip, desc); // Complete interrupt handling writel(hwirq, plic->regs + INTERRUPT_COMPLETE);} |
Debugging and tracing
Enable PLIC debugging:
1 | qemu-system-riscv64 -d int,guest_errors -D plic.log |
Key trace points (trace-event):
1 | qemu-system-riscv64 --trace "trace_sifive_plic_*" |
Related functions
123 | // hw/intc/sifive_plic.ctrace_sifive_plic_set_irq(irq, level);trace_sifive_plic_update(cpu_index, max_irq, max_prio); |
Reference:
