QEMU Custom Device Models
About 1262 wordsAbout 4 min
2026-03-14
This page builds a complete, compilable QEMU device model from scratch using the QEMU Object Model (QOM). It covers the full QOM lifecycle, MemoryRegion registration, interrupt delivery, chardev integration, VMState save/restore, and device properties.
QOM (QEMU Object Model) Foundations
QOM is QEMU's C-based object system, providing single inheritance, interfaces, and typed properties without requiring C++. Every device, bus, machine, and CPU in QEMU is a QOM object.
Core Types
Object ← base of all objects
└── DeviceState ← all hardware devices
└── SysBusDevice ← devices on the system bus (MMIO + IRQs)
└── PL011State ← example: the PL011 UARTTypeInfo Registration
Every QOM type is registered with a TypeInfo struct:
static const TypeInfo my_uart_info = {
.name = TYPE_MY_UART, /* "my-uart" */
.parent = TYPE_SYS_BUS_DEVICE,
.instance_size = sizeof(MyUARTState), /* allocate this much */
.instance_init = my_uart_instance_init, /* constructor */
.class_init = my_uart_class_init, /* class vtable setup */
};
static void my_uart_register_types(void)
{
type_register_static(&my_uart_info);
}
type_init(my_uart_register_types); /* runs at QEMU startup */type_init registers a constructor via a GCC __attribute__((constructor)) section, so it runs before main().
Object Lifecycle
| Phase | Function | Description |
|---|---|---|
| Type registration | class_init | Sets up the class vtable once per type |
| Memory allocation | (qom internal) | g_malloc0(instance_size) |
| Instance init | instance_init | Initialize per-instance fields; no hardware yet |
| Realization | realize / DeviceClass.realize | Connect to buses, map MMIO, connect IRQs |
| Unrealization | unrealize | Unplug hot-removable devices |
| Finalization | instance_finalize | Free resources |
The key distinction: instance_init sets up data structures (it may not access other devices); realize performs board-level wiring and is called after all devices exist.
Complete Device Example: my-uart
A minimal but fully functional UART-like device that:
- Has a 32-byte TX FIFO
- Exposes MMIO registers (DATA, STATUS, CONTROL)
- Fires an IRQ when TX data is written
- Connects to a host chardev for I/O
Header-Style Declarations
Using the OBJECT_DECLARE_SIMPLE_TYPE macro (QEMU >= 6.0):
/* my_uart.h */
#ifndef MY_UART_H
#define MY_UART_H
#include "hw/sysbus.h"
#include "chardev/char-fe.h"
#define TYPE_MY_UART "my-uart"
OBJECT_DECLARE_SIMPLE_TYPE(MyUARTState, MY_UART)
/* Register offsets */
#define MY_UART_REG_DATA 0x00 /* W: transmit byte; R: receive byte */
#define MY_UART_REG_STATUS 0x04 /* R: status flags */
#define MY_UART_REG_CTRL 0x08 /* R/W: control register */
#define MY_UART_REG_SIZE 0x10 /* total MMIO size */
#define MY_UART_STATUS_TXRDY (1u << 0) /* TX ready (FIFO not full) */
#define MY_UART_STATUS_RXRDY (1u << 1) /* RX byte available */
#define MY_UART_CTRL_TXIRQEN (1u << 0) /* enable TX interrupt */
#define MY_UART_CTRL_RXIRQEN (1u << 1) /* enable RX interrupt */
struct MyUARTState {
SysBusDevice parent_obj; /* QOM parent — must be first */
MemoryRegion mmio; /* MMIO region, 0x10 bytes */
CharBackend chr; /* host chardev backend */
qemu_irq irq; /* single interrupt output line */
uint32_t ctrl; /* CTRL register state */
uint8_t rxbuf; /* one-byte RX buffer */
bool rx_pending; /* byte available in rxbuf */
};
#endif /* MY_UART_H */OBJECT_DECLARE_SIMPLE_TYPE(MyUARTState, MY_UART) expands to:
typedef struct MyUARTState MyUARTState;
DECLARE_INSTANCE_CHECKER(MyUARTState, MY_UART, TYPE_MY_UART)which defines MY_UART(obj) as a type-checked cast macro.
Implementation
/* my_uart.c */
#include "qemu/osdep.h"
#include "hw/qdev-properties-system.h"
#include "hw/registerfields.h"
#include "my_uart.h"
#include "qapi/error.h"
/* ─── MemoryRegionOps ─────────────────────────────────────── */
static uint64_t my_uart_read(void *opaque, hwaddr offset, unsigned size)
{
MyUARTState *s = MY_UART(opaque);
switch (offset) {
case MY_UART_REG_DATA:
if (s->rx_pending) {
uint8_t val = s->rxbuf;
s->rx_pending = false;
/* Notify chardev: we can accept another byte */
qemu_chr_fe_accept_input(&s->chr);
return val;
}
return 0xFF; /* no data: return 0xFF (undefined per spec) */
case MY_UART_REG_STATUS: {
uint32_t status = MY_UART_STATUS_TXRDY; /* TX always ready */
if (s->rx_pending)
status |= MY_UART_STATUS_RXRDY;
return status;
}
case MY_UART_REG_CTRL:
return s->ctrl;
default:
qemu_log_mask(LOG_UNIMP,
"my-uart: unimplemented read at offset 0x%" HWADDR_PRIx "\n",
offset);
return 0;
}
}
static void my_uart_write(void *opaque, hwaddr offset,
uint64_t value, unsigned size)
{
MyUARTState *s = MY_UART(opaque);
switch (offset) {
case MY_UART_REG_DATA: {
/* Transmit one byte via chardev */
uint8_t ch = (uint8_t)(value & 0xFF);
qemu_chr_fe_write_all(&s->chr, &ch, 1);
/* If TX interrupts are enabled, assert IRQ then deassert */
if (s->ctrl & MY_UART_CTRL_TXIRQEN) {
qemu_set_irq(s->irq, 1);
qemu_set_irq(s->irq, 0);
}
break;
}
case MY_UART_REG_CTRL:
s->ctrl = (uint32_t)value;
break;
default:
qemu_log_mask(LOG_UNIMP,
"my-uart: unimplemented write at offset 0x%" HWADDR_PRIx "\n",
offset);
break;
}
}
static const MemoryRegionOps my_uart_ops = {
.read = my_uart_read,
.write = my_uart_write,
.endianness = DEVICE_LITTLE_ENDIAN,
.valid = {
.min_access_size = 4,
.max_access_size = 4,
},
};
/* ─── CharDev receive callback ───────────────────────────── */
static int my_uart_chr_can_receive(void *opaque)
{
MyUARTState *s = MY_UART(opaque);
return s->rx_pending ? 0 : 1; /* accept one byte at a time */
}
static void my_uart_chr_receive(void *opaque, const uint8_t *buf, int size)
{
MyUARTState *s = MY_UART(opaque);
s->rxbuf = buf[0];
s->rx_pending = true;
if (s->ctrl & MY_UART_CTRL_RXIRQEN) {
qemu_set_irq(s->irq, 1);
}
}
/* ─── VMState (save/restore) ─────────────────────────────── */
static const VMStateDescription my_uart_vmstate = {
.name = "my-uart",
.version_id = 1,
.minimum_version_id = 1,
.fields = (VMStateField[]) {
VMSTATE_UINT32(ctrl, MyUARTState),
VMSTATE_UINT8(rxbuf, MyUARTState),
VMSTATE_BOOL(rx_pending, MyUARTState),
VMSTATE_END_OF_LIST()
}
};
/* ─── Device lifecycle ───────────────────────────────────── */
static void my_uart_realize(DeviceState *dev, Error **errp)
{
MyUARTState *s = MY_UART(dev);
SysBusDevice *sbd = SYS_BUS_DEVICE(dev);
/* Initialize MMIO region */
memory_region_init_io(&s->mmio, OBJECT(s), &my_uart_ops, s,
TYPE_MY_UART, MY_UART_REG_SIZE);
sysbus_init_mmio(sbd, &s->mmio);
/* Initialize one IRQ output line */
sysbus_init_irq(sbd, &s->irq);
/* Register chardev callbacks */
qemu_chr_fe_set_handlers(&s->chr,
my_uart_chr_can_receive,
my_uart_chr_receive,
NULL, /* event callback */
NULL, /* backend changed */
s, NULL, true);
}
static void my_uart_instance_init(Object *obj)
{
MyUARTState *s = MY_UART(obj);
s->ctrl = 0;
s->rx_pending = false;
}
/* ─── Device properties ──────────────────────────────────── */
static Property my_uart_properties[] = {
DEFINE_PROP_CHR("chardev", MyUARTState, chr),
DEFINE_PROP_END_OF_LIST(),
};
/* ─── Class init (vtable setup) ──────────────────────────── */
static void my_uart_class_init(ObjectClass *oc, void *data)
{
DeviceClass *dc = DEVICE_CLASS(oc);
dc->realize = my_uart_realize;
dc->vmsd = &my_uart_vmstate;
device_class_set_props(dc, my_uart_properties);
set_bit(DEVICE_CATEGORY_MISC, dc->categories);
}
/* ─── Type registration ──────────────────────────────────── */
static const TypeInfo my_uart_info = {
.name = TYPE_MY_UART,
.parent = TYPE_SYS_BUS_DEVICE,
.instance_size = sizeof(MyUARTState),
.instance_init = my_uart_instance_init,
.class_init = my_uart_class_init,
};
static void my_uart_register_types(void)
{
type_register_static(&my_uart_info);
}
type_init(my_uart_register_types)Integrating the Device into a Machine
In a machine's init function, instantiate and wire up the device:
static void my_board_init(MachineState *machine)
{
/* ... create CPU, RAM, etc. ... */
/* Create and realize the UART */
DeviceState *uart = qdev_new(TYPE_MY_UART);
/* Set the chardev property to the first -serial chardev */
qdev_prop_set_chr(uart, "chardev", serial_hd(0));
sysbus_realize_and_unref(SYS_BUS_DEVICE(uart), &error_fatal);
/* Map MMIO at board-specific address */
sysbus_mmio_map(SYS_BUS_DEVICE(uart), 0, 0x40001000);
/* Connect IRQ to NVIC input 3 */
sysbus_connect_irq(SYS_BUS_DEVICE(uart), 0,
qdev_get_gpio_in(nvic, 3));
}VMState: Save and Restore
VMStateDescription describes how to serialize device state for live snapshots and migration. VMSTATE_* macros handle common types:
| Macro | Type |
|---|---|
VMSTATE_UINT8(field, state) | uint8_t |
VMSTATE_UINT32(field, state) | uint32_t |
VMSTATE_UINT32_ARRAY(field, state, n) | uint32_t[n] |
VMSTATE_BOOL(field, state) | bool |
VMSTATE_STRUCT(field, state, ver, vmsd, type) | nested struct |
VMSTATE_FIFO8(field, state) | Fifo8 |
VMSTATE_END_OF_LIST() | terminator |
QEMU automatically serializes these fields on savevm and restores them on loadvm. The version_id and minimum_version_id handle forward/backward compatibility.
Device Properties
Properties allow machine code or command-line options to configure device parameters:
static Property my_uart_properties[] = {
DEFINE_PROP_CHR("chardev", MyUARTState, chr),
DEFINE_PROP_UINT32("baud", MyUARTState, baud_rate, 115200),
DEFINE_PROP_BOOL("fifo", MyUARTState, fifo_enabled, true),
DEFINE_PROP_END_OF_LIST(),
};Set from machine code:
qdev_prop_set_uint32(uart, "baud", 9600);Set from command line (-global option):
-global my-uart.baud=9600Building the Device into QEMU
Add the device to the QEMU meson build:
In hw/char/meson.build:
system_ss.add(when: 'CONFIG_MY_UART', if_true: files('my_uart.c'))In hw/char/Kconfig:
config MY_UART
bool
select SERIALIn the machine's Kconfig:
select MY_UARTThe device is then compiled into qemu-system-arm (or whichever target selects it) and available as TYPE_MY_UART.
Inspecting Devices at Runtime
# List all realized devices (QOM tree)
(qemu) info qtree
# Show memory region tree
(qemu) info mtree
# Show device by type
(qemu) qom-list /machine/peripheral/my-uart[0]
# Read a property via QMP
{"execute": "qom-get",
"arguments": {"path": "/machine/peripheral/my-uart[0]",
"property": "baud"}}