Linux device driver for the custom SPI peripheral in QEMU

Linux device driver for the custom SPI peripheral in QEMU

In the post on Linux driver for custom I2C device the overall structure of a Linux I2C device driver was presented. The character device and sysfs attributes creation were also covered.

Similar can be done for the SPI custom component. In the original post SPI userspace driver was presented as C++ application (also as a Rust application).

In this post creation of a SPI Linux device driver will be presented. The following items will be covered

SPI driver organization

The SPI driver will have similar functionalities and organization as the Linux driver for custom I2C device.

The goal for the SPI driver is be to provide the interface towards the main functionalities of the SPI sens device: enabling/disabling and reading the current data value via the sysfs interface.

Through the next subsections the difference between the I2C driver created in the I2C Linux device driver post and the SPI driver presented in this post will be made.

Tip

All the source code examples presented in this post as well as all of the supporting files are present in the SPI sens driver repository.

Module creation

The module initialization and deinitialization for the SPI driver, like in the I2C driver example, will only register/unregister the driver. Therefore, the module_spi_driver helper function can be used, which creates corresponding init and exit functions.

static struct spi_driver spisens_driver = {
 .driver = {
  .name = DRIVER_NAME,
  .of_match_table = spisens_of_match,
 },
 .probe = spisens_probe,
 .id_table = spisens_id,
};
module_spi_driver(spisens_driver);

Device tree

The matching of the Linux driver and the device is done using the Device Tree description. Therefore, the Device Tree description for the CubieboardNG should be updated to enable the SPI sensor driver.

Unlike the I2C post, where patch to the existing Device Tree description was made, in this post the Device Tree Overlay will be used. The overlay will add the SPI sensor device under the spi0 peripheral.

// SPDX-License-Identifier: GPL-2.0-or-later OR MIT
/*
 * Copyright 2025 Strahinja Jankovic <strahinja@mistrasolutions.com>
 */

// CubieboardNG Custom QEMU SPI device

/dts-v1/;
/plugin/;

/ {
  compatible = "cubietech,a10-cubieboard", "allwinner,sun4i-a10";
};

&spi0 {
  #address-cells = <1>;
  #size-cells = <0>;
  status = "okay";

  spisens: sens@0 {
    compatible = "mistra,spisens";
    reg = <0>;
  };
};

The compatible string matches the defined string in the spisens_of_match table

static const struct of_device_id spisens_of_match[] = {
  { .compatible = "mistra,spisens", },
  { /* end of table */ }
};
MODULE_DEVICE_TABLE(of, spisens_of_match);

SPI device structure

The same way that the i2c_client structure was the base structure used to tie the driver and the resources together in the I2C device driver example, in this post the spi_device structure will be used.

The spi_device structure is passed to the _probe function and the driver data structure will be attached to it using spi_set_drvdata (similar to i2c_set_drvdata from the I2C device driver).

static int spisens_probe(struct spi_device *spi)
{
  struct spisens *data;

  data = devm_kzalloc(&spi->dev, sizeof(*data), GFP_KERNEL);
  if (!data)
    return -ENOMEM;

  spi_set_drvdata(spi, data);

  /* proceed with the initialization */
}

One more (optional) thing that can be done when initializing the SPI device is to check if it really is the correct device.

Note

The SPI device “discovery” and matching with the corresponding driver is done based on the device tree description, using the compatible string. However, the matching will only connect the driver to the SPI CS signal provided in the device tree description, but there are no guarantees that the correct device is found at that address.

The checking can be performed by reading a register with a known value, and the Custom SPI peripheral has the ID register that can be used for that purpose.

Register map

For data exchange on the SPI bus the universal SPI transfer functions, like spi_write, spi_read or spi_sync can be used.

However, since the peripheral has a clear set of registers, the regmap API will be used.

The SPI regmap is initialized using the devm_regmap_init_spi function, where corresponding regmap configuration is passed. Compared to the I2C example, the SPI regmap configuration is a bit more complex since the first byte, holding the address has a bit more complex structure.

Unlike I2C, where register index was provided with the offset 0, for SPI sens the register index is held in the bits 6:4 of the command byte. Therefore, the .reg_stride has to be set to indicate that register indexing is from bit 4.

Also, the information about type of operation (read/write) is also encoded in the command byte using bit 7, so that information has to be passed to the regmap configuration using .write_flag_mask.

#define SPISENS_REG_SHIFT  4
#define SPISENS_REG_CTRL   (1 << SPISENS_REG_SHIFT)

static bool spisens_regmap_is_writeable(struct device *dev, unsigned int reg)
{
  return reg == SPISENS_REG_CTRL;
}

static const struct regmap_config spisens_regmap_config = {
  .reg_bits = 8,
  .reg_stride = SPISENS_REG_SHIFT,
  .val_bits = 8,
  .cache_type = REGCACHE_NONE,
  .max_register = SPISENS_REG_DATA,
  .writeable_reg = spisens_regmap_is_writeable,
  .write_flag_mask = BIT(7),
};

In this example, only the CTRL register can be written to.

Reading a register is performed using the regmap_read and writing to a register using regmap_write.

Going back to the example from the previous section, the ID register can be read in the _probe function using

  regmap_read(data->regmap, SPISENS_REG_ID, &regval);

Sysfs attributes

Same as the custom I2C peripheral, the custom SPI peripheral is simple and has two main functionalities to interact with

  • enabling/disabling of the device
  • reading the sensor data

Therefore, two sysfs attributes are enough: enable and data. The enable attribute should be both readable and writable since it is used to modify the contents of the CTRL register, and the data attribute should be read-only.

/**
 * Enable device setter.
 */
static ssize_t enable_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
  struct spisens *data = dev_get_drvdata(dev);
  unsigned int ctrl;
  int enable;
  int err;

  err = regmap_read(data->regmap, SPISENS_REG_CTRL, &ctrl);

  sscanf(buf, "%d", &enable);

  if (!enable) {
    ctrl &= ~CTRL_EN_MASK;
  } else {
    ctrl |= CTRL_EN_MASK;
  }

  err = regmap_write(data->regmap, SPISENS_REG_CTRL, ctrl);
  if (err < 0)
    return err;

  return count;
}
static DEVICE_ATTR_RW(enable);

/**
 * Return temperature in milicelsius
 */
static ssize_t data_show(struct device *dev, struct device_attribute *attr, char *buf)
{
  struct spisens *data = dev_get_drvdata(dev);
  unsigned int temperature;
  int err;

  err = regmap_read(data->regmap, SPISENS_REG_DATA, &temperature);
  temperature *= 1000;
  temperature >>= 1;

  return sprintf(buf, "%d\n", temperature);
}
static DEVICE_ATTR_RO(data);

static struct attribute *spisens_attrs[] = {
  &dev_attr_enable.attr,
  &dev_attr_data.attr,
  NULL,
};

static const struct attribute_group spisens_group = {
  .attrs = spisens_attrs,
};

The attributes are created in the same way as in the I2C device driver example, and they are added to the kobject file using the sysfs_create_group function called within the _probe callback

sysfs_create_group(&spi->dev.kobj, &spisens_group);

Building and testing

In the SPI device driver repo a Makefile is also added that can be used to build the kernel module out-of-tree.

Note

The procedure for adding the driver inside the kernel tree will not be covered, but the process would comprise of copying the source file into the desired location under drivers, and updating the Makefile at that location to build it. Additionally, the Kconfig file could be updated so a choice can be made if the driver should be included in the kernel, built as a module or excluded from the build.

Out-of-tree build

For the out-of-tree build, the approach from the Platform device driver post can be used.

Tip

Update the Makefile with the kernel source path.

Once toolchain is added to the $PATH, the build is started using

make out-of-tree

This will result in creation of spisensdrv.ko that should be copied to the rootfs, and under the running system the module can be loaded using

insmod spisensdrv.ko

Yocto integration

For Yocto integration a recipe should be created for the kernel module, similar to the one created for the I2C device driver

SUMMARY = "SPI QEMU sensor driver"
DESCRIPTION = "${SUMMARY}"
LICENSE = "GPL-2.0-only"
LIC_FILES_CHKSUM = "file://LICENSE;md5=b234ee4d69f5fce4486a80fdaf4a4263"

inherit module

SRC_URI = "git://github.com/straxy/spisens-drv.git;protocol=https;branch=main"
SRCREV = "${AUTOREV}"

S = "${WORKDIR}/git"

# The inherit of module.bbclass will automatically name module packages with
# "kernel-module-" prefix as required by the oe-core build environment.

RPROVIDES:${PN} += "kernel-module-spisens-drv"

The recipe should be added to the mistra-image and after the image is built the driver will be present and loaded on start of the system.

Tip

The updated layer can be found on the scarthgap branch (tag spi_driver) of the meta-mistra repository.

Testing

Once the module is loaded the sysfs directory corresponding to the SPI device can be checked. In this case, the path is /sys/bus/spi/devices/spi0.0/, and in that directory there will be two additional sysfs files: enable and data, corresponding to the sysfs attributes that were defined.

ls /sys/bus/spi/devices/spi0.0/
data  driver  driver_override  enable  modalias  of_node  power  statistics  subsystem  uevent

Tip

Before interacting with the peripheral the PyQt6 application should be started in order to be able to modify the data value read from the SPI device.

The SPI sensor measurements can be enabled using

echo 1 > /sys/bus/spi/devices/spi0.0/enable

and then the value can be read using

cat /sys/bus/spi/devices/spi0.0/data
0

If the PyQt6 application is used as well and watch is set on the reading of the data file, it would make it easy to monitor how the read values change as the slider is moved.

Summary

In this post an example SPI device driver was created to interact with the custom QEMU SPI peripheral. The driver can be further improved and expanded with the improvement of the custom peripheral and could be integrated into a subsystem (like thermal).

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