👉 Developing I2C Drivers on Embedded Linux: A Hands-On Guide
The I²C (Inter-Integrated Circuit) protocol is a cornerstone of embedded development, enabling simple communication between microcontrollers and peripherals like sensors, EEPROMs, touch controllers, and displays. In this post, we’ll walk through how to write a basic I2C Linux kernel driver from scratch, discuss real-world challenges, and provide tips for integrating it with your custom SBC hardware.
Whether you're working with ARM-based SBCs, custom HMI boards, or industrial modules, mastering I2C driver development can significantly enhance your ability to extend your hardware platform.
🧠 Why Write a Custom I2C Driver?
While the Linux kernel supports many I2C devices out of the box, there are common cases when writing your own driver is necessary:
- You have a proprietary or undocumented I2C peripheral
- You need to support custom communication sequences
- You want to optimize or simplify the driver
- You’re integrating a device not yet mainlined
If you’re building your own custom embedded SBC, having full control over the I2C stack is crucial. Learn more about embedded SBC customization here.
🧱 Step 1: Hardware Setup
Let's assume we are connecting a simple I2C temperature sensor (hypothetical address 0x48
) to an ARM-based SBC via I2C1.
Device Tree Snippet
If your SoC is based on Rockchip, NXP, or Allwinner, your I2C node in the device tree might look like this:
&i2c1 {
status = "okay";
temp_sensor@48 {
compatible = "myvendor,temp-sensor";
reg = <0x48>;
};
};
📌 Note:
compatible
is critical—it tells the kernel which driver to match this node with.
🧠 Step 2: Basic Driver Skeleton
Create a new kernel module: temp_sensor.c
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/kernel.h>
#define DRIVER_NAME "temp_sensor"
static int temp_sensor_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
dev_info(&client->dev, "Probing temp sensor at 0x%02x\n", client->addr);
return 0;
}
static int temp_sensor_remove(struct i2c_client *client)
{
dev_info(&client->dev, "Removing temp sensor driver\n");
return 0;
}
static const struct i2c_device_id temp_sensor_id[] = {
{ "temp-sensor", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, temp_sensor_id);
static const struct of_device_id temp_sensor_of_match[] = {
{ .compatible = "myvendor,temp-sensor" },
{ }
};
MODULE_DEVICE_TABLE(of, temp_sensor_of_match);
static struct i2c_driver temp_sensor_driver = {
.driver = {
.name = DRIVER_NAME,
.of_match_table = temp_sensor_of_match,
},
.probe = temp_sensor_probe,
.remove = temp_sensor_remove,
.id_table = temp_sensor_id,
};
module_i2c_driver(temp_sensor_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kevin Zhang");
MODULE_DESCRIPTION("A simple I2C driver for a temp sensor");
🧪 Step 3: Building the Driver
Assuming you have your kernel headers ready:
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
insmod temp_sensor.ko
You should see the probe log in dmesg
:
Probing temp sensor at 0x48
This confirms the kernel matched your driver to the device tree node and invoked your probe()
.
🧬 Step 4: Reading from the I2C Device
To read data (e.g., temperature):
int ret;
u8 reg = 0x01; // register to read
u8 val;
ret = i2c_smbus_read_byte_data(client, reg);
if (ret < 0)
dev_err(&client->dev, "Read failed\n");
else
val = ret;
You can also implement sysfs
hooks to expose the temperature to user space, or even register with the Linux hwmon subsystem if you're aiming for integration.
🧹 Debug Tips
- Use
i2cdetect -y 1
to scan for connected devices. - Check
dmesg
output for I2C bus errors. - Make sure
CONFIG_I2C_CHARDEV
is enabled in your kernel config.
🏗️ Production Tips
- Always validate electrical pull-ups on I2C lines (1.8V vs 3.3V issues are common).
- Be mindful of I2C bus arbitration if you have multiple masters.
- Consider I2C bit-banging for extremely low-speed devices without kernel support.
📚 Learn More
If you're interested in deeper SBC development topics — from device tree tuning to bootloader configuration and touchscreen integration — check out more content at embedded-sbc.com. You'll find tutorials, sample code, and hardware integration guides.
🛆 Wrap-up
Writing a Linux I2C driver may seem daunting, but with a solid understanding of the kernel interfaces and the device tree system, you can develop robust drivers tailored to your hardware needs.
Key takeaways:
🗰️ Use probe()
and remove()
for basic lifecycle
🔐 Match the device tree with compatible
strings
🔬 Use i2c_smbus_*()
helpers for communication
🛎️ Test thoroughly under your expected voltage and timing conditions
✍️ Want help with Rockchip or NXP-based boards?
Drop your questions in the comments or fork this SBC guide on GitHub.
Top comments (0)