Linux device driver for custom I2C peripheral in QEMU
- 06 Sep, 2025

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, ®val);
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
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).