How to Write an I2C Driver in Linux: Master Complete Beginner-Friendly Guide

0b63979cd9494aa401d1fce2d73bb002
On: February 22, 2026
How to Write an I2C Driver

Table of Contents

This guide walks you through writing an I2C driver in Linux from scratch. Whether you are a beginner touching Linux kernel driver development for the first time or preparing for an embedded systems interview, you will find practical code, clear explanations, and real-world context here.

What Is an I2C Driver and Why Does It Matter?

If you have been working with microcontrollers or Linux-based embedded systems for a while, you have almost certainly bumped into I2C. It stands for Inter-Integrated Circuit, and it is one of the most popular communication protocols in the embedded world. Sensors, EEPROM chips, real-time clocks, display controllers a huge chunk of the hardware you interact with on a daily basis talks over I2C.

But here is the thing: most tutorials show you how to use I2C. Very few show you how to write an I2C driver from the ground up inside the Linux kernel. That is exactly what we are going to do today.

By the end of this guide, you will understand what an I2C driver is, how the I2C subsystem works in Linux, how to write a basic I2C client driver, how to read and write registers on a real device, and how to test everything using common kernel debugging tools. No hand-waving, no magic just real code and real explanations.

Understanding the I2C Protocol First

Before you write a single line of kernel code, you need to understand what I2C actually is at the hardware level. Trust me — this saves you hours of debugging later.

I2C is a synchronous, multi-master, multi-slave serial communication protocol. It uses just two wires: SDA (Serial Data) and SCL (Serial Clock). One device acts as the master and controls the clock. The other devices are slaves and respond when addressed.

Each slave device has a unique 7-bit address (or sometimes 10-bit). The master kicks off every transaction by sending the slave address along with a read/write bit. If the slave acknowledges, the data transfer begins. This is the core of how I2C works.

Why I2C Is So Common in Embedded Systems

I2C only needs two signal lines, which makes it incredibly hardware-friendly. You can chain multiple devices on the same bus as long as each has a unique address. This matters when board space and pin count are limited which is basically always in embedded design. Common devices you would connect over I2C include temperature sensors like the LM75, accelerometers like the MPU6050, OLED displays driven by the SSD1306, and I2C EEPROMs like the AT24C series. If you are learning how to write an I2C device driver, working with one of these is a great starting point

How the Linux I2C Subsystem Works

Linux has a very clean layered architecture for I2C. Understanding this architecture is crucial before you write your I2C driver. Here is how it breaks down:

The Three Layers You Need to Know

The first layer is the I2C adapter driver. This is the low-level driver that controls the physical I2C bus controller on your SoC or board. On a Raspberry Pi, BeagleBone, or any ARM-based platform, this is already written for you. You do not need to touch it unless you are writing drivers for custom silicon.

The second layer is the I2C core. This lives in drivers/i2c/i2c-core-base.c in the kernel source tree. It is the glue that sits between adapters and client drivers. It provides the API that you, as a driver writer, will call.

The third layer is the I2C client driver. This is what you write. A client driver talks to a specific I2C device — your sensor, your EEPROM, your display. This is the layer we focus on in this entire guide.

Key Data Structures in the I2C Subsystem

There are four structures you will use constantly when writing an I2C driver. Get comfortable with all of them.

struct i2c_adapter

This represents the physical I2C bus controller. Think of it as the hardware that drives the SDA and SCL lines. When your driver calls i2c_transfer(), it is using the adapter underneath. You typically do not define this — the platform or SoC driver does.

struct i2c_client

This represents the slave device sitting on the bus. It holds the device address, the adapter it belongs to, and the device name. When the kernel instantiates your device, it creates an i2c_client for you and passes it to your probe function.

struct i2c_client {
    unsigned short flags;
    unsigned short addr;       /* 7-bit I2C address */    char name[I2C_NAME_SIZE];
    struct i2c_adapter *adapter;
    struct device dev;
    int irq;
    /* ... */};

struct i2c_driver

This is the structure you define in your driver. It ties your probe, remove, and id_table together. When the kernel matches a device to your driver (via the id_table or device tree), it calls your probe function.

struct i2c_driver {
    int (*probe)(struct i2c_client *client,
                 const struct i2c_device_id *id);
    int (*remove)(struct i2c_client *client);
    struct device_driver driver;
    const struct i2c_device_id *id_table;
    /* ... */};

struct i2c_msg

This represents a single I2C message — a read or a write transaction on the bus. When you call i2c_transfer(), you pass an array of these messages. Each message has a slave address, flags, a byte count, and a buffer pointer.

struct i2c_msg {
    __u16 addr;    /* Slave address */    __u16 flags;   /* 0 = write, I2C_M_RD = read */    __u16 len;     /* Data length */    __u8 *buf;     /* Data buffer */};

Setting Up Your Development Environment

Before you write your I2C kernel module, you need a working kernel build environment. Here is what you need:

  • A Linux machine (native or VM — Ubuntu 22.04 or later works great)
  • Kernel headers or the full kernel source tree
  • A Makefile for building out-of-tree modules
  • Cross-compilation tools if you are targeting ARM (arm-linux-gnueabihf-gcc)
  • An actual I2C device to test with — an MPU6050 or AT24C02 EEPROM works perfectly

Install Required Packages

  • sudo apt update
  • sudo apt install build-essential linux-headers-$(uname -r)
  • sudo apt install i2c-tools # Very useful for testing

Verify Your I2C Bus

Before writing any driver code, check that your I2C bus is working and your device is detected. On a Raspberry Pi or BeagleBone:

i2cdetect -y 1

This command scans I2C bus 1 and prints a grid of detected device addresses. If your sensor shows up (say at address 0x68 for an MPU6050), you are good to go.

Writing Your First I2C Driver: Step by Step

Now we get to the real work. We are going to write a complete, working I2C client driver for a simple device. We will use the AT24C02 I2C EEPROM as our example because it is cheap, easy to get, and the register access pattern is straightforward. The full driver will have: module init/exit, an i2c_driver struct, a probe function, a remove function, basic read/write functions using i2c_transfer, and a sysfs interface so you can read/write from userspace.

Step 1: Include the Right Headers

#include 

#include 

#include 

#include 

#include 

#include 

#include 

Step 2: Define the Device ID Table

The id_table tells the kernel which devices your driver supports. The kernel uses this to match your driver to a device — either from a platform file or from the device tree.

static const struct i2c_device_id myeeprom_id[] = {
    { "at24c02", 0 },
    { }   /* Sentinel */};
MODULE_DEVICE_TABLE(i2c, myeeprom_id);

Step 3: Write the Probe Function

The probe function is called by the kernel when it finds a matching device. This is where you initialize your device, allocate memory, and set up any interfaces (like sysfs or character device files). Think of probe as your driver’s constructor.

static int myeeprom_probe(struct i2c_client *client,

                          const struct i2c_device_id *id)

{

    dev_info(&client->dev, "myeeprom: probing device at 0x%02x\n",

             client->addr);

    /* Check if adapter supports everything we need */
    if (!i2c_check_functionality(client->adapter,

                                  I2C_FUNC_SMBUS_BYTE_DATA)) {

        dev_err(&client->dev, "Adapter does not support SMBus\n");

        return -ENODEV;

    }

    return 0;  /* Probe success */
}

Step 4: Write the Remove Function

The remove function is called when the device is removed or the driver is unloaded. Clean up whatever you allocated in probe. Release memory, unregister interfaces, and free IRQs if you grabbed any.

static int myeeprom_remove(struct i2c_client *client)

{

    dev_info(&client->dev, "myeeprom: removing device\n");

    return 0;

}

Step 5: Define the i2c_driver Struct

static struct i2c_driver myeeprom_driver = {

    .driver = {

        .name  = "myeeprom",

        .owner = THIS_MODULE,

    },

    .probe    = myeeprom_probe,

    .remove   = myeeprom_remove,

    .id_table = myeeprom_id,

};

Step 6: Module Init and Exit

These two functions register and unregister your driver with the I2C core. The module_i2c_driver macro is a shorthand that does exactly this — it expands into the init and exit boilerplate for you.

module_i2c_driver(myeeprom_driver);

MODULE_LICENSE("GPL");

MODULE_AUTHOR("Raj Kumar ");

MODULE_DESCRIPTION("Simple AT24C02 I2C EEPROM Driver");

Reading and Writing Registers Over I2C

Having a probe function is great, but your I2C driver needs to actually communicate with the device. Let us look at the two main ways to do that in the Linux kernel.

Method 1: Using SMBus Functions

SMBus is a subset of I2C, and Linux has a very clean API for it. If your device is SMBus-compatible (most simple sensors and EEPROMs are), use these. They are simpler and handle a lot of the transaction overhead for you.

/* Read a single byte from a register */
s32 i2c_smbus_read_byte_data(struct i2c_client *client, u8 command);

/* Write a single byte to a register */
s32 i2c_smbus_write_byte_data(struct i2c_client *client,

                               u8 command, u8 value);

Here is a real example. Say you want to read the temperature from an LM75 sensor (register 0x00):

s32 temp_raw = i2c_smbus_read_byte_data(client, 0x00);

if (temp_raw < 0) {

    dev_err(&client->dev, "Failed to read temperature\n");

    return temp_raw;

}

dev_info(&client->dev, "Raw temp: %d\n", temp_raw);

Method 2: Using i2c_transfer for Raw I2C Transactions

When SMBus functions are not enough — for example, when you need combined write-then-read transactions or non-standard protocols — you drop down to i2c_transfer. This gives you full control over every byte on the bus.

Here is how to write a register address and then read back two bytes from it:

int mydevice_read_register(struct i2c_client *client,

                            u8 reg, u8 *val, int len)

{

    struct i2c_msg msgs[2];

    int ret;

    /* First message: write register address */
    msgs[0].addr  = client->addr;

    msgs[0].flags = 0;          /* Write */
    msgs[0].len   = 1;

    msgs[0].buf   = ®

    /* Second message: read the data back */
    msgs[1].addr  = client->addr;

    msgs[1].flags = I2C_M_RD;  /* Read */
    msgs[1].len   = len;

    msgs[1].buf   = val;

    ret = i2c_transfer(client->adapter, msgs, 2);

    if (ret != 2) {

        dev_err(&client->dev, "i2c_transfer failed: %d\n", ret);

        return ret < 0 ? ret : -EIO;

    }

    return 0;

}

The key insight here: i2c_transfer returns the number of messages successfully transferred, not the number of bytes. So for 2 messages, you check ret == 2.

Adding Device Tree Support to Your I2C Driver

Modern Linux systems use the device tree to describe hardware. If you want your I2C driver to work on ARM platforms (Raspberry Pi, BeagleBone, i.MX boards, etc.), you need to add device tree support. This is not optional — it is the standard approach for any driver you want to upstream or deploy on real hardware.

Add an of_match_table

static const struct of_device_id myeeprom_of_match[] = {

    { .compatible = "atmel,at24c02" },

    { }

};

MODULE_DEVICE_TABLE(of, myeeprom_of_match);

Then add it to your i2c_driver struct:

.driver = {

    .name           = "myeeprom",

    .of_match_table = myeeprom_of_match,

    .owner          = THIS_MODULE,

},

Device Tree Node Example

In your board’s .dts or .dtsi file, add a node like this to describe your I2C device:

&i2c1 {

    status = "okay";

    eeprom@50 {

        compatible = "atmel,at24c02";

        reg = <0x50>;

    };

};

When the kernel boots and parses this device tree, it will instantiate an i2c_client for your device at address 0x50 and call your probe function automatically.

Writing the Complete Makefile

You need a Makefile to build your kernel module. Here is a clean, minimal one that works for any out-of-tree I2C driver:

obj-m += myeeprom.o

KDIR := /lib/modules/$(shell uname -r)/build

PWD  := $(shell pwd)

all:

   $(MAKE) -C $(KDIR) M=$(PWD) modules

clean:

   $(MAKE) -C $(KDIR) M=$(PWD) clean

Build it with:

make

Load it with:

sudo insmod myeeprom.ko

Check kernel logs:

dmesg | tail -20

Testing and Debugging Your I2C Driver

Writing the code is half the battle. Testing and debugging is where you really learn what is happening. Here are the tools and techniques that actually work.

i2c-tools for Quick Hardware Verification

Before you even load your driver, use i2c-tools to verify communication with the device:

# Scan the bus for devices

i2cdetect -y 1

# Read a register directly

i2cget -y 1 0x50 0x00

# Write a value to a register

i2cset -y 1 0x50 0x00 0xAB

These tools bypass your driver entirely and talk to the hardware directly through the i2c-dev interface. If these work but your driver does not, the problem is in your driver code, not the hardware.

Using dmesg for Kernel Log Messages

Always use the dev_info, dev_warn, and dev_err macros in your driver rather than printk. They prefix your messages with the device name automatically, which makes log filtering much easier:

dev_info(&client->dev, “Driver probed at address 0x%02x\n”, client->addr);

dev_err(&client->dev, “Read failed with error %d\n”, ret);

Then check your messages:

dmesg | grep myeeprom

Using /sys/bus/i2c/devices

After your driver loads and probe succeeds, check the sysfs tree:

ls /sys/bus/i2c/devices/

cat /sys/bus/i2c/devices/1-0050/name

This tells you that the kernel has matched your driver to the device and probe was called successfully.

Common Errors and What They Mean

If probe returns -ENODEV, it means either the device is not on the bus or i2c_check_functionality failed. Run i2cdetect to verify the device is present.

If i2c_transfer returns -ETIMEDOUT, the device is not acknowledging. Check your wiring, pull-up resistors, and bus speed.

If i2c_transfer returns -EIO, there was a bus error. This often points to missing or wrong pull-up resistors on SDA and SCL lines.

If you see -EBUSY, something else already has the device — either another driver is bound to it or i2c-dev is holding it open.

Adding a Sysfs Interface to Your I2C Driver

Sysfs is the standard way to expose driver attributes to userspace in Linux. Adding a sysfs attribute to your I2C driver lets you read sensor data or write configuration values directly from the command line or a Python script — without writing a separate userspace application.

Define a Sysfs Attribute

static ssize_t temperature_show(struct device *dev,

                                  struct device_attribute *attr,

                                  char *buf)

{

    struct i2c_client *client = to_i2c_client(dev);

    s32 val = i2c_smbus_read_byte_data(client, 0x00);

    if (val < 0)

        return val;

    return sprintf(buf, "%d\n", val);

}

static DEVICE_ATTR_RO(temperature);

Register the Attribute in Probe

ret = device_create_file(&client->dev, &dev_attr_temperature);

if (ret) {

    dev_err(&client->dev, "Failed to create sysfs file\n");

    return ret;

}

After loading, read the value from userspace:

cat /sys/bus/i2c/devices/1-0048/temperature

Full Working I2C Driver Example (Complete Code)

Here is the complete, minimal I2C driver putting everything together. You can use this as a template for any new I2C device driver you write:

#include 

#include 

#include 

#include 

#include 

struct mydevice_data {

    struct i2c_client *client;

};

static ssize_t value_show(struct device *dev,

                           struct device_attribute *attr, char *buf)

{

    struct i2c_client *client = to_i2c_client(dev);

    s32 val = i2c_smbus_read_byte_data(client, 0x00);

    if (val < 0) return val;

    return sprintf(buf, "%d\n", val);

}

static DEVICE_ATTR_RO(value);

static int mydevice_probe(struct i2c_client *client,

                           const struct i2c_device_id *id)

{

    struct mydevice_data *data;

    int ret;

    if (!i2c_check_functionality(client->adapter,

                                  I2C_FUNC_SMBUS_BYTE_DATA))

        return -ENODEV;

    data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);

    if (!data) return -ENOMEM;

    data->client = client;

    i2c_set_clientdata(client, data);

    ret = device_create_file(&client->dev, &dev_attr_value);

    if (ret) return ret;

    dev_info(&client->dev, "mydevice probed at 0x%02x\n", client->addr);

    return 0;

}

static int mydevice_remove(struct i2c_client *client)

{

    device_remove_file(&client->dev, &dev_attr_value);

    return 0;

}

static const struct i2c_device_id mydevice_id[] = {

    { "mydevice", 0 }, { }

};

MODULE_DEVICE_TABLE(i2c, mydevice_id);

static const struct of_device_id mydevice_of_match[] = {

    { .compatible = "myvendor,mydevice" }, { }

};

MODULE_DEVICE_TABLE(of, mydevice_of_match);

static struct i2c_driver mydevice_driver = {

    .driver = {

        .name           = "mydevice",

        .of_match_table = mydevice_of_match,

        .owner          = THIS_MODULE,

    },

    .probe    = mydevice_probe,

    .remove   = mydevice_remove,

    .id_table = mydevice_id,

};

module_i2c_driver(mydevice_driver);

MODULE_LICENSE("GPL");

MODULE_AUTHOR("Raj Kumar");

MODULE_DESCRIPTION("Generic I2C Device Driver Template");

I2C Driver vs SPI Driver: Key Differences

A question that comes up a lot for beginners: when should you use I2C versus SPI, and how does the driver structure differ?

I2C uses two wires and supports multiple devices on the same bus. SPI uses four wires (MOSI, MISO, SCLK, CS) per device but is faster. For I2C device driver programming, addressing is built into the protocol. For SPI, you manage chip-select lines manually.

The driver structure in Linux is similar — both use probe/remove, both support device tree. The main difference is the bus-specific APIs: i2c_transfer and i2c_smbus_* for I2C, versus spi_transfer and spi_sync for SPI. If you understand one, picking up the other takes maybe a day.

Common I2C Driver Interview Questions

If you are studying for embedded Linux interviews, these are the questions you should be ready to answer about I2C driver development:

What is the difference between i2c_transfer and i2c_smbus_read_byte_data?

i2c_smbus_read_byte_data is a higher-level function that handles a standard SMBus read of one byte from a register. i2c_transfer is the low-level function that gives you direct control over the I2C messages sent on the bus. Use SMBus functions when they fit your device’s protocol; use i2c_transfer when you need custom multi-message transactions.

What does i2c_check_functionality do and why is it important?

i2c_check_functionality queries the adapter to see if it supports a specific set of features — like SMBus byte data, block reads, or I2C combined transactions. Calling it in probe before you do any transfers prevents cryptic failures later when you try to use a feature the adapter does not support.

What is devm_kzalloc and why should you use it?

devm_kzalloc is a device-managed version of kzalloc. Memory allocated with it is automatically freed when the device is removed or the driver is unbound. This prevents memory leaks in your driver without requiring explicit cleanup code in the remove function. Always prefer devm_ variants in modern kernel driver code.

How does the kernel match an I2C driver to a device?

The kernel uses three matching mechanisms: the i2c_device_id table (for platform-instantiated devices), the of_device_id table (for device tree nodes), and ACPI IDs. When a match is found, the kernel calls the driver’s probe function with the corresponding i2c_client.

Best Practices for Writing I2C Drivers in Linux

After writing several I2C drivers, you start to notice patterns that save you from bugs. Here are the practices worth adopting from day one:

  • Always call i2c_check_functionality in probe — do not assume the adapter supports what you need.
  • Use devm_ prefixed allocation functions (devm_kzalloc, devm_request_irq) to avoid resource leaks.
  • Store per-device state in a private structure and use i2c_set_clientdata / i2c_get_clientdata to attach and retrieve it.
  • Check return values from every i2c_smbus_ and i2c_transfer call — never ignore them.
  • Use dev_err / dev_info with &client->dev rather than plain printk — it gives you device-specific log context.
  • Handle -EPROBE_DEFER in probe if your driver depends on another driver (like a clock or regulator) that might not be ready yet.
  • Add device tree support with an of_match_table even if you are initially using i2c_device_id — it makes your driver portable.

Registering an I2C Device Without Device Tree (Legacy Method)

On older kernels or bare-metal-style BSPs, devices are sometimes registered programmatically using i2c_board_info. You will see this in older platform files:

static struct i2c_board_info my_board_info[] = {

    {

        I2C_BOARD_INFO("at24c02", 0x50),

    },

};

i2c_register_board_info(1, my_board_info, ARRAY_SIZE(my_board_info));

This approach is deprecated for new code. Use device tree on all modern Linux platforms. But you will encounter this in legacy codebases, so it is worth knowing.

Wrapping Up: What You Have Learned

If you followed this guide all the way through, you now understand the full picture of how to write an I2C driver in Linux. You know how the I2C protocol works at the hardware level, how the Linux I2C subsystem is layered, what the key data structures are (i2c_adapter, i2c_client, i2c_driver, i2c_msg), how to write probe and remove functions, how to perform read and write operations using both SMBus and raw i2c_transfer, how to add device tree support, how to test your driver with i2c-tools and dmesg, and how to expose your driver’s data through sysfs.

That is not a small amount of knowledge. Most embedded developers I know learned this over months of trial and error. Now you have it in one place.

The best next step from here: grab an I2C sensor (an MPU6050 is perfect), connect it to a Raspberry Pi or BeagleBone, and write a real driver from scratch using this guide as your reference. Nothing cements kernel driver concepts faster than actually debugging a real driver on real hardware.

Next Steps: Check out our guides on SPI driver development, platform device drivers, and writing device tree overlays on embeddedprep.com.

Leave a Comment