Introduction: Unlocking AOSP QEMU’s Potential
The Android Open Source Project (AOSP) emulator, powered by QEMU, is an indispensable tool for Android developers and system engineers. It provides a robust virtual environment for testing applications, framework changes, and even low-level system components. However, its default set of emulated peripherals, while comprehensive for typical use cases, can be limiting when experimenting with novel hardware designs, custom silicon, or specialized I/O needs. This guide delves into the advanced topic of developing custom peripherals for AOSP QEMU, specifically leveraging the virtio-mmio interface to extend the emulator’s functionality.
By understanding how to integrate custom devices, you can simulate new sensor types, create bespoke communication interfaces, or test kernel drivers for yet-to-be-produced hardware – all within the familiar, powerful QEMU environment. We’ll explore the underlying architecture, walk through the creation of a simple virtio-mmio device, and demonstrate how to write a corresponding Linux kernel driver for the guest Android system.
Understanding AOSP QEMU Architecture and virtio-mmio
QEMU operates primarily as a system emulator, virtualizing an entire machine, including a CPU, memory, and various peripherals. For AOSP, QEMU typically emulates an ARM64 (AArch64) or x86-64 architecture, presenting a virtualized hardware platform that the Android kernel and user-space applications interact with. Key to this interaction are Memory-Mapped I/O (MMIO) and device trees.
- Memory-Mapped I/O (MMIO): Peripherals communicate with the CPU by exposing their registers as memory locations. The CPU writes to or reads from these specific memory addresses to control the device or retrieve data.
- Device Trees (DTS/DTB): In modern Linux kernels (including Android’s), device trees describe the hardware components of a system. They inform the kernel about available devices, their MMIO addresses, interrupt lines, and other configuration parameters, allowing drivers to find and initialize them dynamically.
virtio is a paravirtualization standard that defines a common set of interfaces for hypervisors and guests to interact with virtualized devices. It aims to provide efficient, high-performance I/O for virtual machines. While traditional virtio devices often rely on PCI, virtio-mmio provides a simpler memory-mapped interface, making it ideal for embedded systems and direct integration into QEMU without a full PCI subsystem overhead. It uses a set of standard registers at a known MMIO base address to communicate device status, features, and capabilities.
The virtio-mmio Device Registers
A typical virtio-mmio device exposes a set of registers accessible via MMIO. These include:
VIRTIO_MMIO_MAGIC_VALUE: A fixed value (0x74726976 or ‘virt’) to identify a virtio-mmio device.VIRTIO_MMIO_VERSION: The virtio version (e.g., 0x2 for virtio 1.0).VIRTIO_MMIO_DEVICE_ID: Identifier for the specific device type (e.g., 1 for network, 2 for block).VIRTIO_MMIO_VENDOR_ID: An optional vendor identifier.VIRTIO_MMIO_DEVICE_FEATURES/VIRTIO_MMIO_DRIVER_FEATURES: For feature negotiation.VIRTIO_MMIO_QUEUE_SEL,VIRTIO_MMIO_QUEUE_NUM,VIRTIO_MMIO_QUEUE_PFN, etc.: For managing virtqueues.VIRTIO_MMIO_STATUS: Device/driver status bits.
For our custom device, we might add additional, custom registers beyond these standard ones to implement specific functionality.
Developing a Simple Custom virtio-mmio Device in QEMU
Let’s create a hypothetical custom ‘calculator’ device that performs a simple addition operation. We’ll add two custom MMIO registers: one for operand A, one for operand B, and one for the result.
Step 1: Define the Custom Device Structure and Registers
First, we need to define our device in QEMU’s C code. We’ll extend a basic virtio-mmio device. For simplicity, we’ll imagine our custom registers are located after the standard virtio-mmio registers.
#include "hw/virtio/virtio-mmio.h"typedef struct CustomCalculatorDevice { VirtIOMMIO virtio_mmio; uint32_t operand_a; uint32_t operand_b; uint32_t result;} CustomCalculatorDevice;enum { CALC_REG_OPERAND_A = 0x100, // Offset from virtio_mmio base CALC_REG_OPERAND_B = 0x104, CALC_REG_RESULT = 0x108, CALC_REG_DO_ADD = 0x10C, // Trigger for addition};
Step 2: Implement QEMU Device Operations (Read/Write)
We’ll need to define how QEMU handles reads and writes to our custom registers. This involves creating a MemoryRegionOps structure.
static uint64_t calculator_mmio_read(void *opaque, hwaddr addr, unsigned size){ CustomCalculatorDevice *s = opaque; uint64_t val = 0; if (addr >= CALC_REG_OPERAND_A && addr <= CALC_REG_DO_ADD) { // Handle custom registers switch (addr) { case CALC_REG_OPERAND_A: val = s->operand_a; break; case CALC_REG_OPERAND_B: val = s->operand_b; break; case CALC_REG_RESULT: val = s->result; break; default: // Read from standard virtio-mmio registers val = virtio_mmio_read(&s->virtio_mmio, addr, size); break; } } else { // Read from standard virtio-mmio registers val = virtio_mmio_read(&s->virtio_mmio, addr, size); } return val;}static void calculator_mmio_write(void *opaque, hwaddr addr, uint64_t val, unsigned size){ CustomCalculatorDevice *s = opaque; if (addr >= CALC_REG_OPERAND_A && addr <= CALC_REG_DO_ADD) { switch (addr) { case CALC_REG_OPERAND_A: s->operand_a = val; break; case CALC_REG_OPERAND_B: s->operand_b = val; break; case CALC_REG_DO_ADD: if (val == 1) { // Trigger addition s->result = s->operand_a + s->operand_b; qemu_log_mask(LOG_GUEST_ERROR, "Calculator: %u + %u = %u
", s->operand_a, s->operand_b, s->result); } break; default: // Write to standard virtio-mmio registers virtio_mmio_write(&s->virtio_mmio, addr, val, size); break; } } else { // Write to standard virtio-mmio registers virtio_mmio_write(&s->virtio_mmio, addr, val, size); }}static const MemoryRegionOps calculator_mmio_ops = { .read = calculator_mmio_read, .write = calculator_mmio_write, .endianness = DEVICE_LITTLE_ENDIAN, .impl.min_access_size = 4, .impl.max_access_size = 4,};
This is a simplified example. A real virtio device would implement virtqueues for asynchronous I/O. For our calculator, direct MMIO access is sufficient.
Step 3: Integrate and Register the Device
You would define a `TYPE_CUSTOM_CALCULATOR_DEVICE` and associated `qemu_class_init` and `instance_init` functions, then instantiate it from `hw/virtio/virtio-mmio.c` or a custom QEMU board model. This usually involves:
static void custom_calculator_device_realize(DeviceState *dev, Error **errp){ CustomCalculatorDevice *s = CUSTOM_CALCULATOR_DEVICE(dev); DeviceState *qdev = dev; MemoryRegion *parent_mem = get_system_memory(); // Initialize virtio-mmio base virtio_mmio_init(&s->virtio_mmio, qdev, 0x1000 /* size of MMIO region */, 0, 0, 0, 0, TYPE_VIRTIO_MMIO_BASE); // Map our custom MMIO ops memory_region_init_io(&s->virtio_mmio.mmio, qdev, &calculator_mmio_ops, s, TYPE_CUSTOM_CALCULATOR_DEVICE, 0x1000); memory_region_add_subregion(parent_mem, s->virtio_mmio.addr, &s->virtio_mmio.mmio);}static void custom_calculator_device_class_init(ObjectClass *oc, void *data){ DeviceClass *dc = DEVICE_CLASS(oc); dc->realize = custom_calculator_device_realize; dc->fw_name = "custom_calculator"; // ... other standard virtio device class initializations ...}static const TypeInfo custom_calculator_device_info = { .name = TYPE_CUSTOM_CALCULATOR_DEVICE, .parent = TYPE_DEVICE, // Or TYPE_VIRTIO_DEVICE if truly complex virtio .instance_size = sizeof(CustomCalculatorDevice), .class_init = custom_calculator_device_class_init, .abstract = false,};DEFINE_TYPE_WITH_CLASS(custom_calculator_device_info);
Finally, to add it to a QEMU instance, you would modify the board setup code (e.g., `hw/arm/virt.c` for `virt` board) to instantiate your device and assign it an MMIO base address and size. For example:
// In your board setup function (e.g., virt_board_init)DeviceState *calc_dev = qdev_new(TYPE_CUSTOM_CALCULATOR_DEVICE);object_property_add_child(OBJECT(qdev_get_machine()), "custom-calc", OBJECT(calc_dev), NULL);qdev_prop_set_uint64(calc_dev, "reg_addr", 0x10000000); // Base MMIO addressqdev_prop_set_uint64(calc_dev, "reg_size", 0x1000); // Size of MMIO regionqdev_realize_and_unref(calc_dev, virtio_mmio_bus_get_parent_bus(vdev->virtio_mmio_bus), &error_fatal);
This would also require an update to the device tree generation to expose this device to the guest kernel.
Developing a Linux Driver for the Custom Device (Guest Side)
Once the QEMU device is ready, the Android guest needs a kernel driver to interact with it. We’ll create a simple kernel module.
Step 1: Kernel Module Structure
#include <linux/module.h>#include <linux/kernel.h>#include <linux/platform_device.h>#include <linux/io.h>#include <linux/of.h> // For device tree matching#define CALC_REG_OPERAND_A 0x100#define CALC_REG_OPERAND_B 0x104#define CALC_REG_RESULT 0x108#define CALC_REG_DO_ADD 0x10Cstatic void __iomem *calculator_base;static int custom_calculator_probe(struct platform_device *pdev){ struct resource *res; int ret = 0; pr_info("Custom Calculator: Probing device...
"); res = platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) { pr_err("Custom Calculator: No MMIO resource found!
"); return -ENODEV; } calculator_base = devm_ioremap_resource(&pdev->dev, res); if (IS_ERR(calculator_base)) { pr_err("Custom Calculator: Failed to ioremap resource!
"); return PTR_ERR(calculator_base); } pr_info("Custom Calculator: Device found and mapped at %p
", calculator_base); // Test interaction writel(10, calculator_base + CALC_REG_OPERAND_A); writel(20, calculator_base + CALC_REG_OPERAND_B); writel(1, calculator_base + CALC_REG_DO_ADD); // Trigger calculation mdelay(10); // Give QEMU time to process (not strictly necessary for simple calc) pr_info("Custom Calculator: Result of 10 + 20 = %u
", readl(calculator_base + CALC_REG_RESULT)); return ret;}static int custom_calculator_remove(struct platform_device *pdev){ pr_info("Custom Calculator: Removing device.
"); return 0;}static const struct of_device_id custom_calculator_of_match[] = { { .compatible = "qemu,custom-calculator", }, // Must match device tree compatible string { },};MODULE_DEVICE_TABLE(of, custom_calculator_of_match);static struct platform_driver custom_calculator_driver = { .probe = custom_calculator_probe, .remove = custom_calculator_remove, .driver = { .name = "custom-calculator", .of_match_table = of_match_ptr(custom_calculator_of_match), },};module_platform_driver(custom_calculator_driver);MODULE_LICENSE("GPL");MODULE_AUTHOR("Your Name");MODULE_DESCRIPTION("Custom Calculator virtio-mmio driver");
Step 2: Building and Deploying the Driver
To compile this as a kernel module for AOSP, you’ll need the Android kernel source tree. Typically, you’d create a `Makefile` like this:
KDIR := $(ANDROID_BUILD_TOP)/common/kernel-$(TARGET_KERNEL_ARCH)PWD := $(shell pwd)obj-m := custom_calculator.oall:$(MAKE) -C $(KDIR) M=$(PWD) modulesclean:$(MAKE) -C $(KDIR) M=$(PWD) clean
Replace `$(ANDROID_BUILD_TOP)` and `$(TARGET_KERNEL_ARCH)` with your actual AOSP build environment variables. After building your kernel module (`custom_calculator.ko`), push it to the running emulator:
adb push custom_calculator.ko /data/local/tmp/adb shellinsmod /data/local/tmp/custom_calculator.koadb logcat | grep "Custom Calculator"
You should see the output from your kernel module showing the calculated result, confirming successful communication between your custom QEMU device and its guest driver.
Advanced Considerations and Conclusion
This tutorial demonstrated a very basic MMIO device. Real-world virtio devices utilize virtqueues for asynchronous, scatter-gather I/O, which significantly improves performance for bulk data transfers (e.g., network packets, disk blocks). Implementing virtqueues involves more complex buffer management and notification mechanisms, but the foundational virtio-mmio register access remains similar.
Further considerations include:
- Interrupt Handling: For devices that need to notify the CPU of events, QEMU and the guest driver must be configured for interrupt lines.
- DMA: Direct Memory Access (DMA) allows devices to read from or write to guest memory directly without CPU intervention, crucial for high-throughput peripherals.
- Security: Carefully consider the implications of new devices having direct access to guest memory or privileged operations.
By mastering custom peripheral development for AOSP QEMU, you gain unparalleled flexibility in testing and prototyping. Whether it’s emulating a novel hardware accelerator, a custom sensor, or a specialized network interface, virtio-mmio provides a robust and relatively straightforward path to extend the Android emulator’s capabilities, enabling deeper innovation and more comprehensive system validation.
Android Mobile Specs & Compare Directory
Are you researching mobile hardware properties, processor SoCs, GPU chipsets, or RAM configurations? Access our complete specs catalog to compare up to 5 devices side-by-side!
Compare Devices Specs →