Jun 25, 2022

Developing custom memory-mapped peripheral for QEMU

This is part 1 of the Linux device driver development post series.

In the previous post I presented some of the goals for doing this work with QEMU.

In this post I will cover the following things

Designing custom memory-mapped device in QEMU

QEMU can emulate many different peripherals. More importantly, it is possible to create new peripherals that are emulated.

That is particularly useful when someone wants to learn device driver development, since that peripheral will be unique, and it would be impossible to find already existing driver.


In this post we will develop a new, custom peripheral for QEMU.

The peripheral will have several 32-bit registers and ability to generate interrupts. It will function as an 4-digit BCD counter. It can be enabled or disabled, generate interrupts on each count, and counting frequency can be selected.

In the next post we will develop Linux device driver that will enable access to this peripheral.

Register map

Register map for the custom peripheral is shown in the following table

offset name description
0x0 CONFIG Configuration register, used to enable component, interrupt generation and select frequency
0x4 STATUS Status register, used to read/clear interrupt flag
0x8 DATA Data register holding current counter value

Bit values of CONFIG register are shown in the following table

name pos dflt description
Reserved 31:3 0 Reserved
FREQ 2 0 Frequency setting:
  • 0 - normal frequency (1 Hz)
  • 1 - fast frequency (2 Hz)
IEN 1 0 Interrupt enable
EN 0 0 Enable device

Bit values of STATUS register are shown in the following table

name pos dflt description
Reserved 31:2 0 Reserved
IFG 1 0 Interrupt flag
Reserved 0 0 Reserved

Bit values of DATA register are shown in the following table

name pos dflt description
Reserved 31:16 0 Reserved
SAMPLE 15:0 0 Current counter value

Fitting into Vexpress-A9 memory map

In order to instantiate and use developed memory-mapped component, we need to integrate it into the Vexpress-A9 memory map and connect it to interrupt controller.

Looking at the memory map for Vexpress-A9, there are several regions that are unused, where we can place our custom component. In this example, I chose offset 0x00018000 in the motherboard peripheral memory map, region CS7. This means that the absolute address is 10018000.

Also, we need an interrupt line for the custom component. Again, looking at the documentation, there are several "reserved" interrupt lines which we can use. In this example, I chose interrupt line 29.

QEMU implementation

Details of implementation of a new memory-mapped device in QEMU will be shown in this section. Main points will be displayed, so someone can use it as instructions for creating a new device.

The device will be described by it's registers and added to the Versatile Express memory map.

Register mapping

QEMU has a very simple and verbose way of describing registers of a component.

For each register, it's offset from the base address is specified, as well as bit-fields that the register consists of. Additionally, for each bitfiled access permissions can be specified and callback functions that are executed before and/or after accessing the register.


Register description for our custom component can be described as

REG32(CTRL, 0x00)
    FIELD(CTRL,     EN,     0,  1)      /* component enable */
    FIELD(CTRL,     IEN,    1,  1)      /* interrupt enable */
    FIELD(CTRL,     FREQ,   2,  1)      /* sampling frequency setting */

REG32(STATUS, 0x04)
    FIELD(STATUS,   IFG,    1,  1)      /* interrupt flag */

REG32(DATA, 0x08)
    FIELD(DATA,     SAMPLE, 0,  16)     /* current value */

The previous code excerpt defines that we have three 32-bit registers called CTRL (offset 0x0 from base address), STATUS (offset 0x4) and DATA (offset 0x8). If we look at the the register CTRL, it has three bit-fields that are used: EN (bit position 0, size 1 bit), IEN (bit position 1, size 1 bit), FREQ (bit position 2, size 1 bit). Similar goes for other two registers.

NOTE: The details of the REG32 and FIELD macros can be found in hw/registerfields.h include file. Thing to keep in mind is that these macros create additional macros (mask, shift, etc.) which we can use later for accessing individual registers and bit-fields.


In order to specify actions that are performed when someone tries to access these registers (read or write), following data must be defined

static const RegisterAccessInfo mm_sens_regs_info[] = {
    {   .name = "CTRL",           .addr = A_CTRL,
        .reset = 0,
        .rsvd = ~(R_CTRL_EN_MASK | R_CTRL_IEN_MASK | R_CTRL_FREQ_MASK),
        .post_write = r_ctrl_post_write,
    },
    {   .name = "STATUS",           .addr = A_STATUS,
        .reset = 0,
        .rsvd = ~R_STATUS_IFG_MASK,
        .post_write = r_status_post_write,
    },
    {   .name = "DATA",         .addr = A_DATA,
        .reset = 0,
        .rsvd = ~R_DATA_SAMPLE_MASK,
        .ro = R_DATA_SAMPLE_MASK,
    },
};

static const MemoryRegionOps mm_sens_reg_ops = {
    .read = register_read_memory,
    .write = register_write_memory,
    .endianness = DEVICE_LITTLE_ENDIAN,
    .valid = {
        .min_access_size = 4,
        .max_access_size = 4,
    }
};

Looking at the second structure, mm_sens_reg_ops, it defines that we are using register read and write functions when accessing this component.

Register functions use the mm_sens_regs_info array defined above, where RegisterAccessInfo structure has several useful fields:

  • name - register name, used for debugging
  • addr - register offset
  • reset - reset value of register
  • ro - read-only bitmask
  • w1c - write one to clear bitmask
  • cor - clear one read bitmask
  • rsvd - reserved bits bitmask
  • pre_write - callback executed before write command
  • post_write - callback executed after write command
  • post_read - callback executed after read command

The post_write functions in the previous code block need to perform certain actions based on the values that are written to the registers.

For instance, after CTRL register bit EN bit is set to 1, the DATA values should start incrementing. Or, after bit FREQ has changed, the frequency of incrementing DATA register should change.

Before we go into details of post_write functions, it would be useful to first explain how is DATA register periodically incremented, as well as how are interrupts implemented in QEMU code.

QEMU timers

QEMU uses timers to enable periodical execution. Timers have a simple API (described in hw/ptimer.h) which we will go over in this section.

Our plan is to increment value of DATA register at two different frequencies: normal (1 Hz) and fast (2 Hz). The value should be incremented only when bit EN in CTRL register is set to 1. The value should also be incremented as BCD value, so each nibble should have values only in the range of 0-9.


Having analyzed our needs, following ptimer functions are of interest

  • ptimer_init - create timer object and define callback that is executed when timer period expires
  • ptimer_set_freq - set timer reload frequency in Hz
  • ptimer_run - start timer and select whether continuous or oneshot mode is used
  • ptimer_stop - stop timer

Based on this, the post_write function for CTRL has two parts.

In the first part FREQ is handled so after every write a check is made if value of FREQ bit has changed, and if it has, timer frequency is updated.

// first part, FREQ handling
...
    new_sfreq = (s->regs[R_CTRL] & R_CTRL_FREQ_MASK) >> R_CTRL_FREQ_SHIFT;

    if (new_sfreq != s->sampling_frequency) {
        s->sampling_frequency = new_sfreq;
        switch (s->sampling_frequency) {
            case FREQ_NORMAL:
                ptimer_set_freq(s->timer, DATA_UPDATE_NORMAL_FREQ);
                break;
            case FREQ_FAST:
                ptimer_set_freq(s->timer, DATA_UPDATE_FAST_FREQ);
                break;
            default:
                DB_PRINT("Unknown frequency %u\n", s->sampling_frequency);
                break;
        }
    }
...

In the second part EN is handled and timer is started/stopped if EN bit is set/reset.

Additionally, if timer is enabled and IEN bit is also set, then evaluation of interrupt generation condition must be made (more on interrupts in next subsection).

// second part, EN/IEN handling
...
    if (s->regs[R_CTRL] & R_CTRL_EN_MASK) {
        /* start timer if not started*/
        ptimer_run(s->timer, 0);

        if (s->regs[R_CTRL] & R_CTRL_IEN_MASK) {
            /* check if alarm should be triggered */
            mm_sens_update_irq(s);
        }
    } else {
        /* stop timer */
        ptimer_stop(s->timer);
    }
...

Increments of DATA register are implemented in the timer callback in the following manner

// DATA incrementing
static void mm_sens_update_data(void *opaque)
{
    MMSensor *s = MM_SENS(opaque);

    s->regs[R_DATA] = s->regs[R_DATA] + 1;
    if ((s->regs[R_DATA] & 0x000fu) > 0x0009u) {
        s->regs[R_DATA] += 0x0006u;
        if ((s->regs[R_DATA] & 0x00f0u) > 0x0090u) {
            s->regs[R_DATA] += 0x0060u;
            if ((s->regs[R_DATA] & 0x0f00u) > 0x0900u) {
                s->regs[R_DATA] += 0x0600u;
                if ((s->regs[R_DATA] & 0xf000u) > 0x9000u) {
                    s->regs[R_DATA] += 0x6000u;
                }
            }
        }
    }

    s->regs[R_STATUS] |= R_STATUS_IFG_MASK;

    mm_sens_update_irq(s);
}

This way the BCD requirement is met and counting will look like in the following diagram

QEMU interrupt handling

Interrupt handling in peripheral in QEMU is performed using the qemu_set_irq function. The function receives an additional parameter which indicates whether interrupt is pending or not. If interrupt is pending (and is not masked in the interrupt controller) it will be raised to the CPU.

In the case of our peripheral, interrupt is pending if both bit IFG in STATUS register and bit IEN in CTRL register are set. This condition has to be checked every time a change happens to any of these two registers, so there is a function that can be reused.

// IRQ handling
static void mm_sens_update_irq(MMSensor *s)
{
    bool pending = s->regs[R_CTRL] & s->regs[R_STATUS] & R_CTRL_IEN_MASK;

    qemu_set_irq(s->irq, pending);
}

Integrating with board file and build system

In order to use the custom memory mapped peripheral, it must be 'placed' in the memory map of the emulated board. Since we are using Versatile Express A9, then it's description must be updated.

Luckily, this is done with one simple command, where we can see chosen base address (0x10018000) and interrupt number (29).

sysbus_create_simple("mistra.mmsens", 0x10018000, pic[29]);

Since build system uses meson and ninja, the new component file is added to the build system in the following manner

softmmu_ss.add(files('mmsens.c'))

The patch file with implementation of the custom component is available in github. The main details of our custom component were explained in previous sections. However, there are standard QEMU structures that also must be used in order to describe VMState, as well as initialization and those can be reused from the patch.

Patch is applied to the QEMU source tree with following command

$ cd $QEMU_SRC
$ patch -p1 < BCD-memory-mapped.patch

After the patch is applied, QEMU must be rebuilt.

Testing developed peripheral

Testing peripheral without appropriate Linux device driver is a bit harder, but not impossible.

We can use the embedded debug prints from the memory-mapped component. Before they can be used, the MM_SENS_ERR_DEBUG definition must be changed from 0 to 1. This way all debug prints from the component will be visible.


U-Boot has integrated commands for reading and writing to memory addresses, so we can use it to try to enable the component, interrupt generation and read current data value.

After QEMU is started with

# Run QEMU with SD card and networking
$ qemu-system-arm -M vexpress-a9 -m 1G -kernel $UBOOT \
                  -drive file=sd.img,format=raw,if=sd \
                  -net nic -net tap,ifname=qemu-tap0,script=no \
                  -serial mon:stdio

U-Boot prompt should be reached by pressing a key.

Following commands are available

  • md <addr> - read data from address addr
  • mw <addr> <val> - write val to address addr

We can first try reading the CTRL and DATA registers

# Run QEMU with SD card and networking
U-Boot> md 0x10018000 1
10018000:mistra.mmsens:CTRL: read of value 0x0
 00000000                               ....
U-Boot> md 0x10018008 1
10018008:mistra.mmsens:DATA: read of value 0x0
 00000000                               ....

The lines starting with 10018000:mistra.mmsens: at debug prints from QEMU, while second lines are written by U-Boot.

In order to enable peripheral, so DATA value starts incrementing, we can write 1 to EN bit in CTRL register. If we read DATA register afterwards, we can see that the values are changing.

# Run QEMU with SD card and networking
U-Boot> mw 0x10018000 1
mistra.mmsens:CTRL: write of value 0x1
r_ctrl_post_write: Wrote 1 to CTRL
U-Boot> md 0x10018008 1
10018008:mistra.mmsens:DATA: read of value 0x1
 00000001                               ....
U-Boot> md 0x10018008 1
10018008:mistra.mmsens:DATA: read of value 0x2
 00000002                               ....

We can also check that the IFG in STATUS register is set. However, interrupt is not triggered since handling of this interrupt is not implemented in U-Boot, which is expected. We will implement interrupt handling in the next blog post, when we develop the Linux device driver for this component.

Summary

In this blog post the process of developing a custom memory-mapped peripheral for QEMU is shown. Main details are described and the complete patch is available with full details of the component.

Using this process many different components can be implemented.


In the next blog post I will show process of development of Linux device driver for this component.