QEMU Device Model Development



Writing your own device model

Specification:

In this example, we will be creating a new basic QEMU device model. This device will be attached to the memory-mapped bus (AXI bus) in QEMU. This example device is going to have two 32-bit registers that are both read and write. The device is also going to have one interrupt.
In QEMU, every register is mapped as per real hardware. Which means it will usually have a reset value, readable/writable bits. The two registers are:

Xdata Register (Offset 0x0)

Reset value: 0x0

Each time the Xdata register is written, the current value is bitwise XORed with the previously written value. Example:

If the Xdata registers hold the current value of 0xFFFF0105 and software writes 0xFF00030A, the new value will be

      0xFFFF0105
(+) 0xFF00030A
=     0x00FF020F

The Xdata register value can be read back by software as normal (without side effects).

Match Register (Offset 0x4)

Reset value: 0xFFFFFFFF

The match register is a 32bit register that can be read and written by software. If the Xdata register value exactly matches the match register at any time, the interrupt pin is asserted. The interrupt can be cleared by writing any value to the match register.

Creating the Device Model

Go to your QEMU source tree. 

cd /path_to_QEMU/qemu

Create a file and add necessary #includes

Create a file xlnx-xor-test.c in hw/misc subdirectory. Open this File for editing. Paste the below #includes at the top of your xlnx-xor-test.c file:

hw/misc/xlnx-xor-test.c
#include "qemu/osdep.h"
#include "hw/sysbus.h"
#include "hw/register.h"
#include "qemu/bitops.h"
#include "qemu/log.h"
#include "qapi/error.h"
#include "hw/irq.h"

These above includes will give access to the various APIs we will interact with to construct our device model.

Define the model name and Err flags

hw/misc/xlnx-xor-test.c
#ifndef XOR_TEST_ERR_DEBUG
#define XOR_TEST_ERR_DEBUG 1
#endif

#define TYPE_XOR_TEST "xlnx.xor-test"
#define XOR_TEST(obj) \
    OBJECT_CHECK(XorTestState, (obj), TYPE_XOR_TEST)

In the above section, the ERR_DEBUG logic defines a symbol for debugging but defines it to 0 to disable it by default. This is useful for adding debug-only code that should be conditionally compiled in only by developers. In this example, we will keep the debug on for checking what is going on when a user reads/writes to these registers.

TYPE_XOR_TEST is the string name of our device. Note the value of this string, it will be used by FDT generic to match the device model to a device tree node (via the DTS compatible property) - more on this later.

XOR_TEST is what’s called a QOM cast macro. It allows object casts to our new device model type.

Define registers

hw/misc/xlnx-xor-test.c
REG32(XDATA, 0x0)
REG32(MATCHER, 0x4)

#define R_MAX (R_MATCHER + 1)

This defines some constant symbols for our two registers. Note the offsets match our spec. Check include/hw/register.h for the definition of REG32 macro to see exactly what it defines, but it will define both register index offset as well as bus address offset for each. The R_MAX definition is used to define the MATCHER register as the last register.

Define the device state struct

hw/misc/xlnx-xor-test.c
typedef struct XorTestState {
    SysBusDevice parent_obj;

    MemoryRegion iomem;
    qemu_irq irq;

    uint32_t regs[R_MAX];
    RegisterInfo regs_info[R_MAX];
} XorTestState;

This XorTestState is the device state. The physical state of the device at any given time is captured in this struct. The parent_obj field is used by QOM to implement the object-oriented inheritance. We won’t use this at all - only core code uses this feature.
iomem and irq are our two external interfaces, for the register interface and interrupt pin respectively. regs is the raw state of our two registers. We can index into this array directly to get either the xdata or the matcher register.

Define irq function

hw/misc/xlnx-xor-test.c
static void xor_test_update_irq(XorTestState *s)
{
    if (s->regs[R_XDATA] == s->regs[R_MATCHER]) {
        qemu_irq_raise(s->irq);
    }
}

This is a private function that our code logic can call to update the IRQ. Remember from the spec that if XDATA matches MATCHER, the interrupt will assert. This inspects the device state (s->regs) and causes this interrupt raise should they match.

Define the post write function for Matcher register

hw/misc/xlnx-xor-test.c
static void xor_test_matcher_post_write(RegisterInfo *reg, uint64_t val64)
{
    XorTestState *s = XOR_TEST(reg->opaque);

    qemu_irq_lower(s->irq);
    xor_test_update_irq(s);
}

This function is going to be called after software writes to the MATCHER register. It implements the needed side effects. That is, as per the spec the interrupt is lowered for any write to the MATCHER register. We also call xor_test_update_irq, as a change in the matcher value could now cause the matcher and xdata to match. So we need to check for this condition. Note the update of s->regs[R_MATCHER] is not done here. This will be done by the core register code for us.

Define the pre-write function for Xdata register

hw/misc/xlnx-xor-test.c
static uint64_t xor_test_xdata_pre_write(RegisterInfo *reg, uint64_t val64)
{
    XorTestState *s = XOR_TEST(reg->opaque);

    s->regs[R_XDATA] = s->regs[R_XDATA] ^ val64;
    xor_test_update_irq(s);

    return s->regs[R_XDATA];
}

This function is going to be called before software or a user writes to the Xdata register. It allows the insertion of logic involving the old value of the register as needed by the spec. The argument val64 is the value as written by software.
In this code, we manually update Xdata as the XOR of the old value and new (as required by the spec). We check xor_test_update_irq as this could cause the interrupt condition to go true. We return the value written to the register as this is needed by the core code.

Define the register block

hw/misc/xlnx-xor-test.c
static RegisterAccessInfo xor_test_regs_info[] = {
    {   .name = "XDATA", .addr = A_XDATA,
        .pre_write = xor_test_xdata_pre_write,
    },{ .name = "MATCHER", .addr = A_MATCHER,
        .reset = 0xffffffff,
        .post_write = xor_test_matcher_post_write,
    },
};

This is the register block definition. It creates the register definitions for our two registers. The two register specific functions we just defined are defined as the pre/post write ops for our two registers as needed. The non-zero reset value (0xFFFFFFFF) of the MATCHER register is defined here.

Define the reset function

hw/misc/xlnx-xor-test.c
static void xor_test_reset(DeviceState *dev)
{
    XorTestState *s = XOR_TEST(dev);
    unsigned int i;

    for (i = 0; i < ARRAY_SIZE(s->regs_info); ++i) {
        register_reset(&s->regs_info[i]);
    }
    qemu_irq_lower(s->irq);
}

This is our reset function, called when the device is reset (and at least once on machine creation). The for loop instructs core code (register_reset) to reset all our register based on their defined reset values. We also lower the interrupt as this makes sense on a reset.

Define read/write handler

hw/misc/xlnx-xor-test.c
static const MemoryRegionOps xor_test_ops = {
    .read = register_read_memory,
    .write = register_write_memory,
    .endianness = DEVICE_LITTLE_ENDIAN,
    .valid = {
        .min_access_size = 4,
        .max_access_size = 4,
    },
};

These are the MMIO (AXI) main read and write handlers. They use the register_read and register_write functions to instruct core code to perform the read and write operations based on xor_test_regs_info. This is standard stuff and can be copy-pasted as-is into most Xilinx device models.

Define the init function

hw/misc/xlnx-xor-test.c
static void xor_test_init(Object *obj)
{
    XorTestState *s = XOR_TEST(obj);
    SysBusDevice *sbd = SYS_BUS_DEVICE(obj);

    RegisterInfoArray *reg_array;

    memory_region_init(&s->iomem, obj, TYPE_XOR_TEST,
                        R_MAX * 4);
    reg_array = register_init_block32(DEVICE(obj), xor_test_regs_info,
                               ARRAY_SIZE(xor_test_regs_info),
                               s->regs_info, s->regs,
                               &xor_test_ops,
                               XOR_TEST_ERR_DEBUG,
                               R_MAX * 4);

    memory_region_add_subregion(&s->iomem, 0x00, ®_array->mem);
    sysbus_init_mmio(sbd, &s->iomem);
    sysbus_init_irq(SYS_BUS_DEVICE(obj), &s->irq);
}

This is the device init function. It initiates the device state when the device is created. It sets up the dynamic device registers with the static config defined in xor_test_regs_info. This is standard initialization and can be copy-pasted to all Xilinx devices with little changes. It defines our registers interface and IRQ for use in the wider system (entity definition if you think in RTL).

Sometimes function arguments or some lines in code are split over two lines, this is to keep each line less than 80 characters long. This is required by the QEMU coding specifications.

Define class_init

hw/misc/xlnx-xor-test.c
static void xor_test_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);

    dc->reset = xor_test_reset;
}

The class init function defines our reset handler.

Define this model in an object form

hw/misc/xlnx-xor-test.c
static const TypeInfo xor_test_info = {
 .name = TYPE_XOR_TEST,
 .parent = TYPE_SYS_BUS_DEVICE,
 .instance_size = sizeof(XorTestState),
 .class_init = xor_test_class_init,
 .instance_init = xor_test_init,
};

This is the type of info defining this object in the inheritance hierarchy. The interesting line is .parent, which defines this device as being a child class of the sysbus device abstraction.

Register the model with QEMU core

hw/misc/xlnx-xor-test.c
static void xor_test_register_types(void)
{
    type_register_static(&xor_test_info);
}

type_init(xor_test_register_types)

This final logic registers the device model with the QEMU core. System-level code can now lookup this device and instantiate it as an object.
And we are done! Save xlnx-xor-test.c.

Add the model for compile.

Edit hw/misc/Makefile.objs and add the below line:

hw/misc/Makefile.objs
obj-$(CONFIG_XLNX_VERSAL)+=xlnx-xor-test.o

Reconfigure and rebuild QEMU using make -j4.

Adding the device to the Device Tree

Go the device tree source repo. Open the file versal-ps-iou.dtsi. Check the file out. You should see device tree nodes for many of the Versal ACAP peripherals. Find the UART1 controller (any peripheral will do really).
Add below lines after serial@MM_UART1 definitions:

versal-ps-iou.dtsi
xor_test: xor_test@0xA0001000 {
        compatible = "xlnx,xor-test";
		reg = <0x0 0xA0001000 0x0 0x1000 0x0>;
};

Note the compatible string which must exactly match the string defined by TYPE_XOR_TEST in the xlnx-xor-test.c source code. The reg property defines the base address of the peripheral.

Save the file. Rebuild the DTB using make. Your device model should be ready for use.

In the above device tree node, we assigned the module to use the address from 0xA0001000 till 0xA0001000 + 0x1000. You may provide different memory addresses but make sure that the address is not used by any other module. Check this for what may go wrong if you enter any random address: WordsofCaution

 See the Device Trees page for more information on how to use device trees.

Adding the device for Zynq UltraScale+ MPSoC

This device can also be compiled for other Xilinx devices. Like for Zynq UltraScale+ MPSoC, add below line in same Makefile.objs under hw/misc/ directory:

hw/misc/Makefile.objs
obj-$(CONFIG_XLNX_ZYNQMP)+=xlnx-xor-test.o

Also, add the above nodes in the Zynq UltraScale+ MPSoC device tree. For example, add the device tree node in zynqmp-iou.dtsi file.

Testing the device model:

Let's test the device model. We will test this in three ways:

Write a simple Baremetal application

Go to the qemu-user-guide-example repository. Under this repository find BareMetal_examples\baremetal_new_model directory and check the file new_model.c. Compile this using make.  If having difficulties compiling this, please check baremetal compilation steps.
In this user application, we will write to the XDATA and MATCHER registers and read back the value from XDATA register after doing the first XOR. Check for prints in QEMU console.  Launch QEMU using below commands:

/Path_to_your_rebuilt_qemu/qemu-system-aarch64 -nographic -M arm-generic-fdt \
    -hw-dtb Path_to_your_dts/dts/LATEST/SINGLE_ARCH/board-versal-ps-virt.dtb \
    -device loader,file= /Path_to_example_directory/new_model.elf,cpu-num=0 \
    -device loader,addr=0xFD1A0300,data=0x8000000e,data-len=4

It will print out all register read or written by the user application.

R/W register using GDB

Information on how to use GDB with QEMU is available in chapter 3. Launch QEMU using the below commands:

/scratch/devops/qemu_docs/qemu/build/aarch64-softmmu/qemu-system-aarch64 \
-M arm-generic-fdt  -serial null -serial null -serial mon:stdio -display none -s \
-hw-dtb /scratch/devops/qemu_docs/dts/LATEST/SINGLE_ARCH/board-versal-ps-virt.dtb \
-m 4G -device loader,addr=0xFD1A0300,data=0x8000000e,data-len=4

Now, in another terminal type the below command:

gdb-multiarch

Connect it to QEMU running in the previous windows using:

target remote localhost:1234

Once connected use below commands to read and write to register:

# Read command format x /x(Reading format) (0xA0001000)register address
x/x 0xA0001000
# Write command format set *(Write format) Register address =  value;
set *((int *) 0xA0001000) = 0xFFFFFF
# Try writing to MATCHER register by replacing address in above instructions.

Observe the QEMU window and you can see that it prints all read and write operations done to the registers.

R/W register from QEMU monitor 

Launch QEMU with using below commands:

/scratch/devops/qemu_docs/qemu/build/aarch64-softmmu/qemu-system-aarch64 \
-M arm-generic-fdt  -serial null -serial null -serial mon:stdio -display none -S \
-hw-dtb /scratch/devops/qemu_docs/dts/LATEST/SINGLE_ARCH/board-versal-ps-virt.dtb \
-m 4G -device loader,addr=0xFD1A0300,data=0x8000000e,data-len=4

 -S option in the above command will freeze the CPUs at startup. Press Ctrl+a followed by c to go to QEMU monitor. After type the below command:

x /x 0xA0001000

It should print something like this:

(qemu) x /x 0xA0001000
xlnx.xor-test:XDATA: read of value 0
00000000a0001000: 0x00000000

In the above example, we tried to read data from 0xA0001000 address i.e. XDATA register. Given that we enabled DEBUG flag in our model, it printed xlnx-xor-test(model-name): XDATA(register): read(operation) of value 0.
Let us read the MATCHER register. Write "x /x 0xA0001004" to QEMU monitor. This should print that MATCHER register was read with the value of 0xffffffff.

MATCHER register has a value of 0xffffffff even though we didn't write any value to it. Check the section "register block" and you can see we define a reset value of 0xffffffff for this register.


© Copyright 2019 - 2022 Xilinx Inc. Privacy Policy