Custom SPI peripheral in QEMU

Custom SPI peripheral in QEMU

In previous posts the process for creating a custom memory mapped and I2C peripheral in QEMU was shown.

In this post we will go through the process of creating a new SPI peripheral, and userspace support for it. The following things will be covered

SPI support for Cubieboard in QEMU

QEMU v9.1.0 does not support emulation of SPI controller peripheral for Cubieboard/Allwinner A10.

Therefore, I have submitted the following patches for Allwinner A10 SPI controller, so it will be supported from the next QEMU version.

The peripheral itself is modeled after the Allwinner A10 reference manual, U-Boot and the Linux driver code.

The idea was to implement only the minimal functionality that allows U-Boot and Linux to interact with the devices, which influenced some implementation decisions. For instance, the DMA access is not implemented, nor is support for half-word or word access, since both U-Boot and Linux kernel drivers use only byte-wide access.

Custom SPI QEMU peripheral

The process for implementing support for emulation of a SPI peripheral under QEMU is similar to the process for a memory-mapped or an I2C peripheral.

The peripheral needs to be connected to an existing SPI controller, tied to a correct Chip Select (CS) line and should provide responses when data is written to and read from SPI bus.

Peripheral description

In this example we will implement a temperature sensor which has similar functionality to the one implemented in I2C example. Once enabled, it will return a random value in the range of 15.0 to 25.0 with 0.5 degrees (Celsius) step on every read.


The peripheral has following registers

offset name description
0x0 ID Unique ID register, always returning value 0x5A
0x1 CONFIG Configuration register, used to enable temperature measurement
0x2 TEMPERATURE Data register holding current temperature. The value is coded as Q5.1 to be able to hold 0.5 degree step

Bit values of CONFIG register are shown in the following table

name pos dflt description
Reserved 7:1 0 Reserved
EN 0 0 Enable device

The access to the peripheral should always consist of two bytes. The first byte is the command byte, which specifies the type of operation (read or write) and index of register that is accessed. The value of the second byte depends on whether the operation is read or write.

The command byte fields are described in the following table

name pos description
Write/Read 7 Specify read (0) or write (1)
Index 6:4 Register index
Reserved 3:0 Reserved

The read sequence is shown in the following diagram

SPI custom peripheral read sequence

and write sequence in the following diagram

SPI custom peripheral write sequence

Based on the register description, the supported command bytes are

Byte description
00h Read ID register
10h Read control register
20h Read Temperature register
90h Write to control register

Custom peripheral implementation

The implementation of the peripheral is available in the github repository.

Functions of interest are the ones related to the data handling. The global structure is defined in the following way

typedef struct SPISensor {
    /*< private >*/
    SSIPeripheral parent_obj;
    /*< public >*/
    uint8_t regs[NR_REGS]; // peripheral registers
    uint8_t cycle; // cycle counter
    uint8_t ptr; // current register index
    bool write_nread; // indicator if write or read is in progress
} SPISensor;

The SPI transfer consists of both write and read sequence. As described previously, each SPI access consists of two bytes, and the first (command) byte contains information on the operation, read or write, and the index of register to access.

static uint32_t spi_sens_transfer(SSIPeripheral *dev, uint32_t value)
{
    SPISensor *s = SPI_SENS(dev);
    uint32_t retval = 0;
    spi_sens_write(s, value);
    retval = spi_sens_read(s);
    s->cycle++;
    return retval;
}

Writing to the device

The only register that supports writing is the CTRL register.

/* Interpret a command-byte */
static void spi_sens_write(SPISensor *s, uint32_t value)
{
    /* Parse command byte */
    if (s->cycle == 0) {
        s->write_nread = (value & CB_START) != 0;
        s->ptr = (value & CB_REG_MASK) >> CB_REG_SHIFT;
    } else {
        if (s->write_nread) {
            if (s->ptr == REG_CTRL_OFFSET) {
                s->regs[s->ptr] = value;
            }
        }
    }
}

Reading from the device

All registers support reading, so it is just a matter of returning the current register value.

/* Return zero on first cycle, then return the requested register */
static uint32_t spi_sens_read(SPISensor *s)
{
    if (!s->write_nread) {
        if (s->cycle == 0) {
            /* generate new temperature sample */
            if (s->regs[REG_CTRL_OFFSET] & REG_CTRL_EN_MASK) {
                s->regs[REG_TEMPERATURE_OFFSET] = spi_sens_get_temperature();
            } else {
                s->regs[REG_TEMPERATURE_OFFSET] = 0xff;
            }
        } else {
            return s->regs[s->ptr];
        }
    }
    return 0;
}

We also want each read to trigger loading of random value to the TEMPERATURE register. That is performed on every command byte that is received.

The register is modified only if the device is currently enabled, otherwise a value of 0xff will be stored.

Integrating into QEMU

Similarly to the Custom I2C peripheral, the new SPI peripheral must be attached to a SPI controller and connected to the correct Chip Select line.

We can choose either to use an already existing SPI controller, or to also add a new SPI controller in the memory map, and then attach this component to the newly added SPI controller.

In this example we will use the existing SPI controller and attach our custom SPI component to that SPI controller. The device will be connected to CS line 0.

The excerpt from initialization of emulated Cubieboard is

{
  Object *spi_dev;

  spi_dev = object_resolve_path_component(OBJECT(a10), "spi0");
  if (spi_dev) {
    SSIBus *spi_bus;

    spi_bus = (SSIBus *)qdev_get_child_bus(DEVICE(spi_dev), "spi");
    if (spi_bus) {
      DeviceState *cust_dev;
      qemu_irq cs_line;

      cust_dev = qdev_new("mistra.spisens");
      qdev_prop_set_uint8(cust_dev, "cs", 0);
      qdev_realize_and_unref(cust_dev, BUS(spi_bus), &error_fatal);

      cs_line = qdev_get_gpio_in_named(cust_dev, SSI_GPIO_CS, 0);
      sysbus_connect_irq(SYS_BUS_DEVICE(&a10->spi0), 1, cs_line);
    }
  }
}

The implemented peripheral uses QEMU tracing functionality for logging accesses to individual registers, which help a lot during development and debugging. The tracing is enabled by passing --trace spisens* when starting QEMU.

SPI Userspace handling

With the device created in QEMU we can turn to making a userspace application to access the SPI device.

The program flow will be similar to the one used for the I2C device

  1. Device should be initialized
  2. After device is initialized, we should periodically read the temperature data

The full userspace application can be found in github.

Unlike the I2C peripheral, the SPI will require changes to the Linux source code and device tree description.

The first step is to add the new compatible string to the spidev driver

diff --git a/drivers/spi/spidev.c b/drivers/spi/spidev.c
index 655f2c959cd4..d1a0b4bcb76a 100644
--- a/drivers/spi/spidev.c
+++ b/drivers/spi/spidev.c
@@ -738,6 +738,7 @@ static const struct of_device_id spidev_dt_ids[] = {
        { .compatible = "semtech,sx1301", .data = &spidev_of_check },
        { .compatible = "silabs,em3581", .data = &spidev_of_check },
        { .compatible = "silabs,si3210", .data = &spidev_of_check },
+       { .compatible = "mistra,spisens", .data = &spidev_of_check },
        {},
 };
 MODULE_DEVICE_TABLE(of, spidev_dt_ids);

The second step is to add a new peripheral under the SPI controller in the device tree description, using the newly added compatible string

diff --git a/arch/arm/boot/dts/allwinner/sun4i-a10-cubieboard.dts b/arch/arm/boot/dts/allwinner/sun4i-a10-cubieboard.dts
index 0645d6064235..21c82bd76e78 100644
--- a/arch/arm/boot/dts/allwinner/sun4i-a10-cubieboard.dts
+++ b/arch/arm/boot/dts/allwinner/sun4i-a10-cubieboard.dts
@@ -234,6 +234,11 @@ &spi0 {
        pinctrl-0 = <&spi0_pi_pins>,
                    <&spi0_cs0_pi_pin>;
        status = "okay";
+       spidev@0{
+               compatible = "mistra,spisens";
+               reg = <0>;
+               spi-max-frequency = <4000000>;
+       };
 };

 &uart0 {

Initialization

The device initialization now involves setting up the SPI userspace communication. Everything is done using the dev file for the selected SPI controller. In our case it is /dev/spidev0.0.

The process can be summarized as

  1. Opening the SPI controller device file

    m_fd = open("/dev/spidev0.0", O_RDWR);
  2. Enabling device by writing 1 to CTRL register. The write is performed by creating a spi_ioc_transfer instance, filling the transmit and receive portions of it and performing an ioctl call SPI_IOC_MESSAGE(1)

    struct spi_ioc_transfer xfer[1] = {0};
    
    // Command byte
    uint8_t cmd_byte = CMD_OP_WRITE | (CTRL << CMD_REG_SHIFT);
    uint8_t data[2];
    data[0] = cmd_byte;
    data[1] = CTRL_ENABLE;
    xfer[0].tx_buf = (__u64)data;      // output buffer
    xfer[0].rx_buf = (__u64)data;      // input buffer
    xfer[0].len = (__u32)sizeof(data); // length of data to read
    
    int retv = ioctl(m_fd, SPI_IOC_MESSAGE(1), &xfer);

Periodic execution

After the device is initialized, we can use a separate thread to periodically initiate reads from SPI device, like it was done for the I2C device.

We will use std::this_thread::sleep_until to enable periodic readouts from the SPI peripheral.

while (m_running)
{
    // Read current time so we know when to timeout
    current_time = std::chrono::system_clock::now();

    // read SPI device and print
    if (!read_reg(static_cast<uint8_t>(SPISensRegs::TEMPERATURE), value))
    {
        m_running = false;
        break;
    }

    std::cout << "Measured " << std::setprecision(3) << (value / 2.) << std::endl;

    // sleep_until
    std::this_thread::sleep_until(current_time + std::chrono::seconds(1));
}

The read process includes writing TEMPERATURE register offset in the command byte and then reading the TEMPERATURE register value from the second received byte.

struct spi_ioc_transfer xfer[1] = {0};

// Command byte
uint8_t cmd_byte = TEMPERATURE << CMD_REG_SHIFT;
uint8_t data[2];
data[0] = cmd_byte;
data[1] = 0x00;
xfer[0].tx_buf = (__u64)data;      // output buffer
xfer[0].rx_buf = (__u64)data;      // input buffer
xfer[0].len = (__u32)sizeof(data); // length of data to read

int retv = ioctl(m_fd, SPI_IOC_MESSAGE(1), &xfer);
value = data[1];

Testing in QEMU

Before testing the userspace application, we need to test that device is integrated properly inside QEMU. For that we can use both U-Boot and SPI userspace test tools.

The idea with testing would be to enable the SPI sensor and read temperature value, to verify that the values can be obtained.

While running these tests the tracing of spisens is enabled, so the trace outputs will be shown as well.

U-Boot testing

The U-Boot has the sspi command which can be enabled using CONFIG_CMD_SPI=y in the U-Boot config.

The sspi command is called in the following way

=> sspi
sspi - SPI utility command

Usage:
sspi [<bus>:]<cs>[.<mode>][@<freq>] <bit_len> <dout> - Send and receive bits
<bus>     - Identifies the SPI bus
<cs>      - Identifies the chip select
<mode>    - Identifies the SPI mode to use
<freq>    - Identifies the SPI bus frequency in Hz
<bit_len> - Number of bits to send (base 10)
<dout>    - Hexadecimal string that gets sent

Therefore, enabling device is performed with the following command

=> sspi 0 16 9001
spisens_select [0x55aa61f33000] select
spisens_write_command Write at command byte with 1, ptr 1
spisens_write_valid Write success at 1, value 1
spisens_select [0x55aa61f33000] deselect
0000

From the logs above, the write is performed by writing a 16 bit value to the device using CS=0, and the value that is written is 0x9001, which is interpreted as:

  • command byte 0x90: write command (0x80) to register 1 (CTRL)
  • data byte 0x01: write value 1, which corresponds to the enable bit

After the command is issued, the CS is set to active state, and the peripheral registers that the write is performed to register 1 with value 1. After the write is performed, the CS is deactivated.

Now that the device is enabled, the temperature can be read using the following command

=> sspi 0 16 2000
spisens_select [0x55aa61f33000] select
spisens_write_command Write at command byte with 0, ptr 2
spisens_read_command Read at command byte with 0, ptr 2
spisens_read_valid Read success at 2, value 47
002F

The value that is returned is 0x2F, which is interpreted as 23.5 degrees. Subsequent reads will return different values

=> sspi 0 16 2000
0026
=> sspi 0 16 2000
002D

spi-pipe test

In the Ubuntu root filesystem the spi-pipe utility is installed using

sudo apt install spi-tools

Once spi-tools are installed, the ID register of the device can be read using

root@mistra:/home/ubuntu# echo -ne "\x00\x00" | spi-pipe -d /dev/spidev0.0 -b 2 -n 1 | xxd
00000000: 005a                                     .Z

The SPI sensor is enabled using

root@mistra:/home/ubuntu# echo -ne "\x90\x01" | spi-pipe -d /dev/spidev0.0 -b 2 -n 1 | xxd
00000000: 0000                                     ..

and the temperature can be read using

root@mistra:/home/ubuntu# echo -ne "\x20\x00" | spi-pipe -d /dev/spidev0.0 -b 2 -n 1 | xxd
00000000: 0023                                     .#

After all these tests, that confirm that the device is operating as expected, the userspace application can be tested.

Userspace application

Userspace application can be compiled in the same manner as the memory-mapped application, via external toolchain or using a Yocto SDK generated toolchain:

mkdir build && cd build
cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/toolchain.cmake ..
make -j

or

source /opt/mistra-framebuffer/4.0/environment-setup-cortexa8t2hf-neon-mistra-linux-gnueabi
mkdir build && cd build
cmake ..
make -j

After the application has been built and copied to the rootfs, it can be started with

spisens-app /dev/spidev0.0

The application will initialize the SPI peripheral and start printing read temperature value every second.

Summary

In this blog post userspace application development for custom QEMU SPI sensor is presented.

The application encapsulates the SPI userspace access and implements periodic reading of data.

It can be improved with unit tests, as well as more interactive behavior, but it is left for some other time.

Subscribe
If you would like to get information as soon as new content is published, please subscribe to the "MistraSolutions newsletter".