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 #include
s at the top of your xlnx-xor-test.c file:
#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
#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
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
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
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
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
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
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
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
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
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
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
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
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:
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:
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:
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