Linux device driver for custom I2C peripheral in QEMU

Linux device driver for custom I2C peripheral in QEMU

In the I2C custom component post a new QEMU I2C peripheral was shown. Additionally, a C++ application (also Rust application) that can be used to interact with the peripheral from userspace was shown.

The C++ and Rust applications were both examples of the “I2C userspace driver”, i.e. a way to interact with the I2C device without a dedicated kernel driver.

Besides the userspace driver, a proper Linux device driver can be created for this I2C peripheral and that will be the topic of this post. The following items will be covered

I2C driver organization

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

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

Through the next subsections a parallel between the platform driver created in the Linux driver for memory-mapped peripheral post (will be referenced as “platform device driver” in the rest of the text) and the I2C 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 I2C sens driver repository.

Module creation

In the platform device driver example, the kernel module was initialized and deinitializated using the module_init and module_exit functions.

Since the module initialization and deinitialization will only register/unregister the driver, in this example the module_i2c_driver helper function will be used, which will create corresponding init and exit functions.

static struct i2c_driver i2csens_driver = {
  .driver = {
    .name = DRIVER_NAME,
    .of_match_table = i2csens_of_match,
  },
  .probe = i2csens_probe,
  .id_table = i2csens_id,
};
module_i2c_driver(i2csens_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 by adding the following node under the i2c0 node

  i2csens: sens@36 {
    compatible = "mistra,i2csens";
    reg = <0x36>;
  };

The compatible string matches the defined string in the i2csens_of_match table

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

I2C client structure

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

The i2c_client structure is passed to the _probe function and the driver data structure will be attached to it using i2c_set_clientdata (similar to platform_set_drvdata from the platform device driver).

static int i2csens_probe(struct i2c_client *client)
{
  struct i2csens *data;

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

  i2c_set_clientdata(client, data);

  /* proceed with the initialization */
}

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

Note

The I2C 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 I2C address 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 I2C peripheral has the ID register that can be used for that purpose.

Register map

For data exchange on the I2C bus the universal I2C transfer functions, like i2c_transfer, or i2c_master_send and i2c_master_recv can be used.

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

The I2C regmap is initialized using the devm_regmap_init_i2c function, where corresponding regmap configuration is passed

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

static const struct regmap_config i2csens_regmap_config = {
  .reg_bits = 8,
  .val_bits = 8,
  .cache_type = REGCACHE_NONE,
  .max_register = I2CSENS_REG_DATA,
  .writeable_reg = i2csens_regmap_is_writeable,
};

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, I2CSENS_REG_ID, &regval);

Sysfs attributes

The custom I2C peripheral is simple and has two main functions 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 i2csens *data = dev_get_drvdata(dev);
  unsigned int ctrl;
  int enable;
  int err;

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

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

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

  err = regmap_write(data->regmap, I2CSENS_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 i2csens *data = dev_get_drvdata(dev);
  unsigned int temperature;
  int err;

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

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

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

static const struct attribute_group i2csens_group = {
  .attrs = i2csens_attrs,
};

The attributes are created in the same way as in the platform device driver example, the only difference is that they will not be tied in with a new device class, but instead they will be added to the kobject file using the sysfs_create_group function called within the _probe callback

sysfs_create_group(&client->dev.kobj, &i2csens_group);

Building and testing

In the I2C 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 i2csensdrv.ko that should be copied to the rootfs, and under the running system the module can be loaded using

insmod i2csensdrv.ko

Yocto integration

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

SUMMARY = "I2C 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/i2csens-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-i2csens-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 feature/i2c-driver branch of the meta-mistra repository.

Testing

Once the module is loaded the sysfs directory corresponding to the I2C device can be checked. In this case, the path is /sys/bus/i2c/devices/1-0036/, 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/i2c/devices/1-0036/
data       driver     enable     modalias   name       of_node    power      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 I2C device.

The I2C sensor measurements can be enabled using

echo 1 > /sys/bus/i2c/devices/1-0036/enable

and then the value can be read using

cat /sys/bus/i2c/devices/1-0036/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 I2C device driver was created to interact with the custom QEMU I2C 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".