Cover image for QEMU Hardware Modeling

QEMU Hardware Modeling

Timeline

Timeline

2025-11-23

  1. init
This article introduces the core mechanisms and implementation flow of QEMU hardware modeling, detailing the six stages of virtual machine creation, the peripheral modeling process using PL011 UART and edu device as examples (covering the QOM type system, MMIO callbacks, DMA transfers, and asynchronous computation), and provides an in-depth analysis of the complete interrupt routing and handling flow from device triggering to Guest OS response.

Environment

1
2
3
4
5
6
wget https://download.qemu.org/qemu-10.1.2.tar.xz
tar xvJf qemu-10.1.2.tar.xz
cd qemu-10.1.2
mkdir -p output
./configure --prefix=$PWD/output --target-list=aarch64-softmmu,riscv64-softmmu --enable-debug
bear -- make -j$(nproc)

Create .clangd

1
2
3
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include "qemu/osdep.h"
#include "qemu-main.h"
#include "qemu/main-loop.h"
#include "system/replay.h"
#include "system/system.h"


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);

#ifdef CONFIG_DARWIN
static int os_darwin_cfrunloop_main(void)
{
CFRunLoopRun();
g_assert_not_reached();
}
int (*qemu_main)(void) = os_darwin_cfrunloop_main;
#endif


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 machine instance
}

Machine type selection and registration

Core component: MachineClass (machine class description)

Key operations:

  • Match user-specified machine type via select_machine()
  • Look up the corresponding TypeImpl from the global type table type_table
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// system/vl.c
static 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.c
GSList *object_class_get_list(const char *implements_type) {
g_hash_table_foreach(type_table, object_class_foreach_tramp, &data);
// Iterate through all registered machine types
}

Machine object instantiation

Core component: MachineState (machine runtime state)

Key operations:

  • Via QOM’s object_new_with_class() to instantiate the machine
  • Initialize basic properties such as memory and CPU topology
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
// system/vl.c
static 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.c

static 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
// system/vl.c
void 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 -accel parameter
  • Create vCPU threads and bind them to physical CPUs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// accel/accel-system.c
int 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.c
void 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 stage: qemu_main_loop()

Load firmware in qemu_init
Load firmware in qemu_init

Enter the final stage qemu_main_loop
Enter the final stage qemu_main_loop

system/runstate.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
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: Implement device class inheritance hierarchy via QOM
  • Precise state modeling: Full-cycle management of registers, FIFO, and interrupt states
  • Hardware interface implementation: MMIO callbacks handle CPU access, character backend interfaces with host I/O
  • Interrupt system integration: Works in coordination with the platform interrupt controller (PLIC)
  • Device lifecycle: implements complete lifecycle including reset, migration, and destruction

Serial port
Serial port

Device type definition

Core mechanism: QEMU Object Model (QOM)

Key operations:

  • Define TypeInfo structure to describe device type
  • Implement class initialization (class_init) and instance initialization (instance_init)
  • Register into the global type system
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 // hw/char/pl011.c
static 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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// hw/char/pl011.c
struct PL011State {
SysBusDevice parent_obj;

MemoryRegion iomem;
uint32_t flags;
uint32_t lcr;// Line control register
uint32_t rsr;// Interrupt mask register
uint32_t cr;// Status flag bit
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 to host terminal or file
qemu_irq irq[6];// Interrupt signal line
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// hw/char/pl011.c
static 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
  • Register MMIO Callback Functions
  • Connect Interrupt Signal
  • Bind Character Device Backend
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// hw/char/pl011.c

static 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 Interrupt */
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.c
static 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 Function Logic

Core Components:

  • MMIO Read/Write Callbacks
  • Interrupt Generation Logic
  • FIFO Management
  • Character Device Communication
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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

1
2
3
4
5
6
7
8
9
10
11
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 code example for simple PL011 device modeling

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
/* hw/char/pl011.c - Complete implementation of PL011 UART */

#define TYPE_PL011 "pl011"
#define PL011(obj) OBJECT_CHECK(PL011State, (obj), TYPE_PL011)

/* 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 callback */
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 status
}
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 status 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)

Instantiate PL011 in RISC-V virt machine:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* 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); // Mapped to 0x10000000
sysbus_connect_irq(SYS_BUS_DEVICE(dev), 0, plic_irq[UART0_IRQ]); // Connection interrupted

// Add node to device tree
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
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:

1
2
3
4
5
6
7
8
9
10
11
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

This modeling approach allows 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 the CPU and peripherals essentially involves address access and interrupt notification:

  • Address Access: The CPU reads and writes device registers via MMIO (Memory-Mapped I/O) or PIO (Port I/O)
  • Interrupt Notification: The peripheral sends event signals to the CPU via the interrupt controller
1
2
3
4
5
6
7
8
// Pseudo-code: CPU and Device Interaction Flow
while (1) {
if (中断触发) {
读取设备状态寄存器;
处理数据;
}
写入控制寄存器启动操作;
}

Introduction to the edu Device

edu is a teaching virtual device provided by QEMU, with a streamlined yet complete functional design:

  • Basic Function: Implements factorial calculation (fact register)
  • Advanced features: DMA transfer, interrupt triggering
  • Typical application: Demonstrating full lifecycle management of PCI devices

Define device properties

Key point: Automatically gain PCI configuration space management capability by inheriting PCIDevice.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
struct EduState {
PCIDevice pdev; // Inherit 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;
#define EDU_STATUS_COMPUTING 0x01
#define EDU_STATUS_IRQFACT 0x80
uint32_t status; // Status register (bit0=computing, bit7=interrupt enable)

uint32_t irq_status;

#define EDU_DMA_RUN 0x1
#define EDU_DMA_DIR(cmd) (((cmd) & 0x2) >> 1)
# define EDU_DMA_FROM_PCI 0
# define EDU_DMA_TO_PCI 1
#define EDU_DMA_IRQ 0x4
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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”

Map MMIO region

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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:

1
2
3
4
0x00 : 阶乘输入寄存器 (可写)
0x04 : 阶乘结果寄存器 (只读)
0x80 : DMA 源地址寄存器
0x88 : DMA 目标地址寄存器

Implement MMIO callback function

Take factorial calculation as an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
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, implement asynchronous computation via condition variables.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/*
* 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

1
2
3
4
5
6
7
8
9
10
11
12
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 interrupt
msi_notify(&edu->pdev, 0);
} else {// Use traditional INTx interrupt
pci_set_irq(&edu->pdev, 1);
}
}
}

DMA transfer implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static void edu_dma_timer(void *opaque)
{
EduState *edu = opaque;
bool raise_irq = false;

if (!(edu->dma.cmd & EDU_DMA_RUN)) {
return;
}
// Execute 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 perspective

When the guest program’s driver accesses the edu device:

  1. Probe device: identified by PCI ID 0x1234:11e8
  2. Map MMIO: ioremap() to obtain register virtual addresses
  3. Calculate factorial
  4. Handle interrupt: clear status flag 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: represents device state via register bits (e.g., computing/completed)
  • Asynchronous processing: offload time-consuming operations to background threads to avoid blocking VCPU
  • Address isolation: each device has its own independent MMIO space
  • Interrupt abstraction: supports both traditional INTx and modern MSI modes
  • DMA security: address verification to prevent virtual machine escape

Interrupt modeling

Full-chain processing flow of PL011 interrupt in QEMU
Full-chain processing flow of PL011 interrupt in QEMU

  • Device layer: PL011 triggers interrupt signal based on hardware status
  • Interrupt controller layer: PLIC implements priority arbitration and interrupt distribution
  • CPU layer: RISC-V architecture responds to external interrupts and switches context
  • Guest layer: Guest program or OS interrupt handler completes device service

Below we analyze according to the flow:

PL011 triggers interrupt

1
2
3
4
5
6
7
8
9
10
11
12
13
// hw/char/pl011.c
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);// Trigger interrupt
}
}

s->irq connects to a specific interrupt line of PLIC (e.g., UART0_IRQ=10), and PLIC routes the interrupt to the CPU.

PLIC receives interrupt

PLIC’s GPIO input handler updates interrupt status:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// hw/intc/sifive_plic.c
static 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 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_Implement priority arbitration in update():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 CPU interrupt request flag via cpu_interrupt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// system/cpus.c
void cpu_interrupt(CPUState *cpu, int mask)
{
g_assert(bql_locked());

cpus_accel->handle_interrupt(cpu, mask);
}
// accel/tcg/tcg-accel-ops.c
void 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 current TB

CPU interrupt check

Detect interrupt request in vCPU main loop:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106

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 !defined(CONFIG_USER_ONLY)
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;
}
#endif /* !CONFIG_USER_ONLY */
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 switch:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// target/riscv/cpu_helper.c
void 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) {
// Vector 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

Taking the Linux interrupt handling flow as an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// arch/riscv/kernel/entry.S
ENTRY(handle_arch_irq)
SAVE_ALL
csrr a0, mcause
csrr a1, mepc
mv a2, sp
call riscv_intc_irq // Jump to C Language Handler
RESTORE_ALL
END(handle_arch_irq)

// drivers/irqchip/irq-sifive-plic.c
static 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

1
2
3
// hw/intc/sifive_plic.c
trace_sifive_plic_set_irq(irq, level);
trace_sifive_plic_update(cpu_index, max_irq, max_prio);

Reference: