DEV Community

Cover image for Developing I2C Drivers on Embedded Linux: A Hands-On Guide
Tony He
Tony He

Posted on • Edited on

Developing I2C Drivers on Embedded Linux: A Hands-On Guide

👉 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>;
    };
};
Enter fullscreen mode Exit fullscreen mode

📌 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");
Enter fullscreen mode Exit fullscreen mode

🧪 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
Enter fullscreen mode Exit fullscreen mode

You should see the probe log in dmesg:

Probing temp sensor at 0x48
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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)