Custom SPI peripheral in QEMU
- October 20, 2024
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
- Custom SPI QEMU peripheral
- SPI Userspace handling
- Testing in QEMU
- Summary
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
and write sequence in the following diagram
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
- Device should be initialized
- 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
-
Opening the SPI controller device file
m_fd = open("/dev/spidev0.0", O_RDWR);
-
Enabling device by writing
1
toCTRL
register. The write is performed by creating aspi_ioc_transfer
instance, filling the transmit and receive portions of it and performing an ioctl callSPI_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.