How to Write an I2C Linux Device Driver Using Raspberry Pi

On: March 9, 2026
How to Write an I2C Linux Device Driver Using Raspberry Pi

So you want to write an I2C Linux device driver on a Raspberry Pi. Good choice. This is one of those topics that sounds intimidating at first but once you see the big picture, everything clicks into place fast.

By the time you finish reading this, you will know how I2C works at the protocol level, how Linux organizes the I2C subsystem internally, how to set up your Raspberry Pi for driver development, and most importantly how to write a real working I2C Linux device driver from scratch. Every line of code here is explained in plain English, not textbook Latin.

Write an I2C Linux Device Driver

1. What is I2C and Why Does It Matter for Embedded Linux?

I2C stands for Inter-Integrated Circuit. It was invented by Philips Semiconductor back in 1982, and somehow it is still everywhere. You will find it on temperature sensors, pressure sensors, gyroscopes, OLED displays, EEPROMs, real-time clocks, and basically any cheap embedded peripheral you can think of.

The reason I2C stuck around is simple: it only needs two wires to connect multiple devices, and it requires almost no extra logic on the microcontroller side. Compared to SPI which needs a separate chip select line per device, I2C is cleaner to wire up when you have five or six sensors on the same board.

Now here is why the I2C Linux device driver topic specifically matters. On a Raspberry Pi, you are running a full Linux kernel. When your application code tries to read from a temperature sensor, it does not talk directly to the hardware pins. There is a whole stack between your userspace code and the actual I2C bus on the silicon. That stack is the Linux I2C subsystem, and the I2C device driver is your entry point into it.

If you want to use an existing sensor with a mainline kernel driver, you might never need to write one yourself. But if you are working with a custom sensor, a proprietary chip, or any device that does not have an upstream driver yet, you will need to write an I2C client driver. That is exactly what this guide covers.

Understanding how to write an I2C Linux kernel module also teaches you something that goes beyond just I2C. It teaches you how Linux handles hardware abstraction in general. The same concepts apply when you move on to SPI drivers, USB drivers, and beyond.

2. Understanding the I2C Protocol: SDA, SCL, Addresses and Transfers

Before writing a single line of driver code, you need to understand what is actually happening on the wire. Skipping this part is the biggest mistake beginners make. They copy-paste driver code without knowing what i2c_master_send is actually doing, and then they spend hours debugging something they could have figured out in ten minutes with protocol knowledge.

The Two Wires: SDA and SCL

I2C uses exactly two signal lines:

SDA (Serial Data Line): This is where the actual bits travel. Both master and slave devices share this single line for data.

SCL (Serial Clock Line): This is the clock. The master device drives it, and all slaves listen to it. Every bit of data on SDA is sampled on a specific edge of SCL.

Both lines are open-drain, meaning any device on the bus can pull them low, but pulling them high is done by external pull-up resistors. This is what allows multiple devices to share the same wire without shorting each other out. Typical pull-up resistor values are 4.7k ohms for standard mode and 1k ohm for fast mode.

Addressing: How the Master Talks to the Right Slave

Every I2C device has a 7-bit address. Some newer devices use 10-bit addressing but 7-bit is what you will deal with 95% of the time. That means up to 128 devices can theoretically share one I2C bus, though a few addresses are reserved so the practical limit is around 112.

When your Raspberry Pi wants to talk to, say, a BMP280 pressure sensor at address 0x76, it starts a transaction by asserting the START condition, then sending the 7-bit address followed by a read/write bit. Every slave on the bus listens. Only the one whose address matches responds with an ACK (acknowledge) bit.

A Complete I2C Transaction

Here is what a typical I2C write transaction looks like step by step:

  1. Master asserts START condition: SDA goes low while SCL is high
  2. Master sends the 7-bit slave address + write bit (0)
  3. Slave pulls SDA low to send ACK
  4. Master sends register address (which register inside the slave to write to)
  5. Slave ACKs again
  6. Master sends data byte(s)
  7. Slave ACKs each byte
  8. Master asserts STOP condition: SDA goes high while SCL is high

A read transaction is similar but after addressing the device and sending the register, the master sends a repeated START, then the address again with the read bit set. After that the slave drives the data bytes and the master sends ACK until it is done, then sends NACK on the last byte followed by STOP.

This matters for your driver because when you call kernel functions like i2c_master_send and i2c_master_recv, or the SMBus helpers, this is exactly what they are orchestrating under the hood.

SMBus vs Raw I2C

You will hear the term SMBus a lot in Linux I2C driver code. SMBus (System Management Bus) is a subset of I2C with stricter timing and a defined set of transaction types: byte read/write, word read/write, block read/write, and a few others. Most simple sensor drivers use SMBus helper functions because they are simpler to use and work reliably with both SMBus-only and full-I2C adapters.

The main SMBus helpers you will use are:

  • i2c_smbus_read_byte_data(client, reg) – Read a single byte from a register
  • i2c_smbus_write_byte_data(client, reg, value) – Write a single byte to a register
  • i2c_smbus_read_word_data(client, reg) – Read a 16-bit word from a register
  • i2c_smbus_read_i2c_block_data(client, reg, len, buf) – Read a block of bytes

For more complex operations, you use raw I2C transfers with i2c_transfer(), which takes an array of struct i2c_msg structures.

3. The Linux I2C Subsystem Architecture Explained Simply

The Linux I2C subsystem has three layers. Understanding these three layers is what makes everything else make sense.

Layer 1: The I2C Bus Driver (Adapter Driver)

This is the lowest layer. It talks to the actual hardware: the I2C controller registers on the SoC. On a Raspberry Pi, the BCM2835/BCM2711 chip has built-in I2C controllers. The adapter driver for this controller is already in the kernel. You do not need to write it unless you are building a custom SoC or adding I2C support to a new platform.

The kernel represents this layer using struct i2c_adapter. Each physical I2C bus on your board corresponds to one adapter object.

Layer 2: The I2C Core

This is the glue layer in the middle. It sits between the adapter driver and the device drivers. It provides the generic API that all I2C device drivers use. When you call i2c_master_send() in your driver, you are calling into the I2C core, which then figures out which adapter to use and calls the adapter driver’s transfer function.

The I2C core also handles driver-device matching, device registration, and the probe/remove lifecycle. You never modify this layer. You just use its API.

Layer 3: The I2C Device Driver (Client Driver)

This is what you write. It sits at the top. It knows about one specific chip: what registers it has, how to initialize it, how to read sensor data from it, and how to expose that data to userspace (usually through sysfs or a character device or the IIO subsystem).

The kernel represents your device as a struct i2c_client. Your driver has a struct i2c_driver that gets registered with the I2C core, and when the kernel finds a matching device (from Device Tree or explicit board info), it calls your probe() function with a pointer to the i2c_client.

This three-layer model means your driver code is completely portable. You write it once, and it works on any Linux platform that has an I2C adapter driver, whether that is a Raspberry Pi, a BeagleBone, an NVIDIA Jetson, or anything else.

4. Setting Up Your Raspberry Pi for I2C Driver Development

Let us set up the development environment. We will use a Raspberry Pi 4 running Raspberry Pi OS (64-bit), but these steps work on Pi 3 and Pi Zero 2W as well.

Enable I2C on Raspberry Pi

By default, the I2C peripheral is disabled in the Pi firmware. Enable it:

sudo raspi-config

Go to Interface Options > I2C > Enable. Reboot when prompted.

After reboot, verify the I2C devices are visible:

ls /dev/i2c*

You should see /dev/i2c-1 (and possibly /dev/i2c-0 and /dev/i2c-20 on Pi 4).

You can also verify with:

lsmod | grep i2c

You should see i2c_bcm2835 and i2c_dev loaded.

Install the Kernel Headers

To build kernel modules, you need the kernel headers matching your running kernel:

sudo apt update
sudo apt install raspberrypi-kernel-headers build-essential

Verify the headers are installed:

ls /lib/modules/$(uname -r)/build

If this directory exists and contains a Makefile, you are good.

Install i2c-tools

These are userspace tools that let you scan the I2C bus and manually read/write registers. They are invaluable for hardware verification before and after writing your driver:

sudo apt install i2c-tools

Scan I2C bus 1 to see what devices are present:

sudo i2cdetect -y 1

If you have a sensor connected, its address will show up in the grid. This is how you verify your hardware is working before touching driver code.

Install Git and a Decent Text Editor

sudo apt install git vim

You can use whatever editor you prefer. Vim, nano, VS Code with Remote SSH, all work fine.

Directory Structure for Your Driver Project

Create a clean workspace:

mkdir ~/i2c_driver_project
cd ~/i2c_driver_project

Inside this directory you will have:

  • my_i2c_driver.c – The main driver source file
  • Makefile – Build instructions
  • test_overlay.dts – Device Tree source for testing

5. Tools and Packages You Need Before Writing a Single Line

Beyond the setup above, here are the specific tools you will use constantly during Raspberry Pi I2C driver development:

i2cdetect: Scans an I2C bus and shows which addresses respond. Run this first any time you connect new hardware.

i2cdump: Dumps all register values from a specific I2C device. Great for verifying your hardware without kernel code.

i2cget / i2cset: Read or write individual registers from the command line. Use these to understand your device before writing any kernel code.

modprobe / insmod / rmmod: Load and unload kernel modules. You will use these constantly during development.

dmesg: View kernel log messages. Every printk() call from your driver goes here. Run dmesg -w in a separate terminal while testing so you see output in real time.

lsmod: List currently loaded kernel modules.

dtoverlay: Apply Device Tree overlays at runtime on Raspberry Pi. Useful for testing before permanently adding to config.txt.

6. The Anatomy of an I2C Linux Device Driver

Before looking at full driver code, understand the core data structures and functions you will use. This is the vocabulary you need.

struct i2c_client

Every I2C device on the bus is represented by a struct i2c_client. This structure contains:

  • addr: The 7-bit I2C address of the device
  • name: A string name matching a driver
  • adapter: Pointer to the i2c_adapter it sits on
  • dev: An embedded struct device for the device model

You receive a pointer to this in your probe() function. You use it as the first argument to all I2C communication functions like i2c_smbus_read_byte_data().

struct i2c_driver

This is how you tell the kernel about your driver. It contains:

  • driver.name: The driver name string
  • probe: Your probe function, called when a matching device is found
  • remove: Called when the device is removed or the driver is unloaded
  • id_table: A table of device names/IDs your driver can handle
  • of_match_table: For Device Tree matching (what you use on Raspberry Pi)

struct i2c_device_id

A table of device name strings your driver supports. Each entry has a name and optional driver data. The table is terminated with an empty entry.

The Module Init and Exit Functions

Every kernel module needs an init function called when loaded and an exit function called when unloaded. For an I2C driver:

  • module_init() calls i2c_add_driver()
  • module_exit() calls i2c_del_driver()

Or you can use the convenience macro module_i2c_driver() which generates both for you automatically.

7. Writing an I2C Linux Device Driver From Scratch (Full Code Walkthrough)

Now we build the actual driver. We will write a driver for a hypothetical sensor called “mysensor” at address 0x48 that has two registers: a configuration register at 0x00 and a data register at 0x01. The data register returns a 16-bit temperature value.

This structure matches dozens of real sensors (TMP102, ADS1115, etc.), so this code is immediately applicable to real hardware.

The Makefile

Create this first:

# Makefile for I2C Linux Device Driver
obj-m += my_i2c_driver.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

The obj-m line tells the kernel build system to build my_i2c_driver.c as a loadable module.

The Full Driver Code

// my_i2c_driver.c
// A beginner-friendly I2C Linux device driver for Raspberry Pi
// Demonstrates I2C client driver structure from scratch

#include <linux/module.h>       // Required for all kernel modules
#include <linux/init.h>         // module_init and module_exit macros
#include <linux/i2c.h>          // i2c_client, i2c_driver, I2C API
#include <linux/kernel.h>       // printk, pr_info macros
#include <linux/slab.h>         // kmalloc, kfree
#include <linux/fs.h>           // file_operations for char device
#include <linux/uaccess.h>      // copy_to_user, copy_from_user
#include <linux/cdev.h>         // Character device support
#include <linux/device.h>       // device_create, class_create
#include <linux/of.h>           // Device Tree support
#include <linux/of_device.h>    // of_device_get_match_data

#define DRIVER_NAME     "my_i2c_sensor"
#define CLASS_NAME      "mysensor"
#define DEVICE_NAME     "mysensor0"

/* Register definitions for our hypothetical sensor */
#define REG_CONFIG      0x00
#define REG_DATA_HIGH   0x01
#define REG_DATA_LOW    0x02
#define REG_DEVICE_ID   0x0F

/* Expected device ID value */
#define DEVICE_ID_VALUE 0xA5

/* Configuration register bits */
#define CONFIG_ENABLE   0x01
#define CONFIG_RATE_1HZ 0x02

/* Per-device private data structure
 * This holds everything specific to one instance of the device.
 * If you had two sensors, you would have two of these. */
struct mysensor_data {
    struct i2c_client *client;  // The I2C device this belongs to
    dev_t dev_num;              // Char device number (major:minor)
    struct cdev cdev;           // Kernel char device structure
    struct class *dev_class;    // Device class for /dev entry
    struct device *device;      // Device node in /dev
    int16_t last_reading;       // Last raw sensor reading
};

/* Forward declarations */
static int mysensor_open(struct inode *inode, struct file *file);
static int mysensor_release(struct inode *inode, struct file *file);
static ssize_t mysensor_read(struct file *file, char __user *buf,
                              size_t count, loff_t *offset);

/* File operations for the character device.
 * This is how userspace programs interact with our driver. */
static const struct file_operations mysensor_fops = {
    .owner   = THIS_MODULE,
    .open    = mysensor_open,
    .release = mysensor_release,
    .read    = mysensor_read,
};

/* ============================================================
 * Helper Functions: I2C Communication
 * ============================================================ */

/**
 * mysensor_read_register - Read a single byte from a sensor register
 * @client: The I2C client device
 * @reg: Register address to read from
 *
 * Returns the register value (0-255) on success, negative errno on failure.
 * We use i2c_smbus_read_byte_data because it handles the write-then-read
 * sequence automatically. This covers: START, ADDR+W, REG, RESTART, ADDR+R, DATA, STOP.
 */
static int mysensor_read_register(struct i2c_client *client, u8 reg)
{
    int ret;
    ret = i2c_smbus_read_byte_data(client, reg);
    if (ret < 0) {
        dev_err(&client->dev, "Failed to read register 0x%02x: %d\n", reg, ret);
    }
    return ret;
}

/**
 * mysensor_write_register - Write a single byte to a sensor register
 * @client: The I2C client device
 * @reg: Register address to write to
 * @value: Byte value to write
 *
 * Returns 0 on success, negative errno on failure.
 */
static int mysensor_write_register(struct i2c_client *client, u8 reg, u8 value)
{
    int ret;
    ret = i2c_smbus_write_byte_data(client, reg, value);
    if (ret < 0) {
        dev_err(&client->dev, "Failed to write 0x%02x to register 0x%02x: %d\n",
                value, reg, ret);
    }
    return ret;
}

/**
 * mysensor_read_raw - Read the 16-bit sensor data value
 * @client: The I2C client device
 * @value: Pointer to store the result
 *
 * Reads two consecutive bytes and assembles them into a 16-bit value.
 * The sensor stores the high byte first (big-endian format).
 */
static int mysensor_read_raw(struct i2c_client *client, int16_t *value)
{
    int high, low;

    high = mysensor_read_register(client, REG_DATA_HIGH);
    if (high < 0)
        return high;

    low = mysensor_read_register(client, REG_DATA_LOW);
    if (low < 0)
        return low;

    /* Combine high and low bytes. Sensor is big-endian. */
    *value = (int16_t)((high << 8) | low);
    return 0;
}

/* ============================================================
 * Char Device File Operations
 * ============================================================ */

static int mysensor_open(struct inode *inode, struct file *file)
{
    /* Get the private data pointer from the cdev structure.
     * container_of is a common kernel pattern for going from a
     * member pointer back to the containing structure. */
    struct mysensor_data *data;
    data = container_of(inode->i_cdev, struct mysensor_data, cdev);
    file->private_data = data;
    pr_info("mysensor: device opened\n");
    return 0;
}

static int mysensor_release(struct inode *inode, struct file *file)
{
    pr_info("mysensor: device closed\n");
    return 0;
}

static ssize_t mysensor_read(struct file *file, char __user *buf,
                              size_t count, loff_t *offset)
{
    struct mysensor_data *data = file->private_data;
    char output[32];
    int len;
    int ret;
    int16_t raw_value;

    if (*offset > 0)
        return 0; /* EOF: already returned data once */

    /* Read fresh data from the sensor */
    ret = mysensor_read_raw(data->client, &raw_value);
    if (ret < 0)
        return ret;

    data->last_reading = raw_value;

    /* Convert raw to something meaningful and format it.
     * For this sensor, value / 10 gives degrees Celsius. */
    len = snprintf(output, sizeof(output), "%d.%d C\n",
                   raw_value / 10, abs(raw_value % 10));

    if (count < len)
        return -EINVAL;

    /* copy_to_user: ALWAYS use this when copying to userspace.
     * Direct pointer dereference in kernel for user pointers is dangerous. */
    if (copy_to_user(buf, output, len))
        return -EFAULT;

    *offset += len;
    return len;
}

/* ============================================================
 * Driver Probe and Remove Functions
 * ============================================================ */

/**
 * mysensor_probe - Called when a matching I2C device is found
 * @client: The I2C device that was matched to our driver
 * @id: The entry in our id_table that matched (legacy method)
 *
 * This is the most important function in your driver. It runs once
 * when the kernel finds a device that matches your driver. Here you:
 * 1. Verify the hardware is what you expect
 * 2. Allocate any resources you need
 * 3. Initialize the hardware
 * 4. Register any kernel interfaces (char dev, sysfs, IIO, etc.)
 *
 * Return 0 on success, negative errno on failure.
 * If probe returns non-zero, the device is considered unbound.
 */
static int mysensor_probe(struct i2c_client *client,
                          const struct i2c_device_id *id)
{
    struct mysensor_data *data;
    int ret;
    int device_id;

    dev_info(&client->dev, "Probing mysensor at address 0x%02x\n",
             client->addr);

    /* Step 1: Check if SMBus byte data operations are supported.
     * Not all I2C adapters support all transaction types. */
    if (!i2c_check_functionality(client->adapter,
                                 I2C_FUNC_SMBUS_BYTE_DATA)) {
        dev_err(&client->dev, "SMBus byte data not supported by adapter\n");
        return -EOPNOTSUPP;
    }

    /* Step 2: Verify device ID register to confirm we have the right chip.
     * This prevents the driver from accidentally binding to a different device
     * that happens to be at the same I2C address. */
    device_id = mysensor_read_register(client, REG_DEVICE_ID);
    if (device_id < 0) {
        dev_err(&client->dev, "Cannot read device ID register\n");
        return device_id;
    }

    if (device_id != DEVICE_ID_VALUE) {
        dev_err(&client->dev, "Wrong device ID: expected 0x%02x, got 0x%02x\n",
                DEVICE_ID_VALUE, device_id);
        return -ENODEV;
    }

    dev_info(&client->dev, "Device ID verified: 0x%02x\n", device_id);

    /* Step 3: Allocate private data structure.
     * devm_ prefixed allocations are automatically freed when the device
     * is removed. This is the modern way to manage driver resources. */
    data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;

    data->client = client;

    /* Step 4: Store private data in the client device.
     * i2c_set_clientdata lets you retrieve this later anywhere you have
     * a pointer to the client. */
    i2c_set_clientdata(client, data);

    /* Step 5: Initialize the hardware.
     * Enable the sensor and set it to 1Hz measurement rate. */
    ret = mysensor_write_register(client, REG_CONFIG,
                                  CONFIG_ENABLE | CONFIG_RATE_1HZ);
    if (ret < 0) {
        dev_err(&client->dev, "Failed to configure sensor\n");
        return ret;
    }

    /* Step 6: Register a character device so userspace can read sensor data.
     * alloc_chrdev_region picks an available major number automatically. */
    ret = alloc_chrdev_region(&data->dev_num, 0, 1, DEVICE_NAME);
    if (ret < 0) {
        dev_err(&client->dev, "Failed to allocate char device region\n");
        return ret;
    }

    /* Initialize the cdev structure and add it to the kernel */
    cdev_init(&data->cdev, &mysensor_fops);
    data->cdev.owner = THIS_MODULE;

    ret = cdev_add(&data->cdev, data->dev_num, 1);
    if (ret < 0) {
        dev_err(&client->dev, "Failed to add cdev\n");
        goto err_unregister_chrdev;
    }

    /* Create a device class visible in /sys/class/ */
    data->dev_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(data->dev_class)) {
        ret = PTR_ERR(data->dev_class);
        dev_err(&client->dev, "Failed to create device class\n");
        goto err_del_cdev;
    }

    /* Create the actual device node at /dev/mysensor0 */
    data->device = device_create(data->dev_class, NULL, data->dev_num,
                                 NULL, DEVICE_NAME);
    if (IS_ERR(data->device)) {
        ret = PTR_ERR(data->device);
        dev_err(&client->dev, "Failed to create device node\n");
        goto err_destroy_class;
    }

    dev_info(&client->dev, "mysensor driver probed successfully. "
             "Device: /dev/%s\n", DEVICE_NAME);
    return 0;

    /* Error handling: clean up in reverse order of allocation */
err_destroy_class:
    class_destroy(data->dev_class);
err_del_cdev:
    cdev_del(&data->cdev);
err_unregister_chrdev:
    unregister_chrdev_region(data->dev_num, 1);
    return ret;
}

/**
 * mysensor_remove - Called when the device is removed or driver unloaded
 * @client: The I2C device being removed
 *
 * Undo everything probe() did, in reverse order.
 * Resources allocated with devm_ are freed automatically, so we only
 * need to manually clean up what we allocated without devm_.
 */
static int mysensor_remove(struct i2c_client *client)
{
    struct mysensor_data *data = i2c_get_clientdata(client);

    /* Power down the sensor */
    mysensor_write_register(client, REG_CONFIG, 0x00);

    /* Remove all the char device infrastructure */
    device_destroy(data->dev_class, data->dev_num);
    class_destroy(data->dev_class);
    cdev_del(&data->cdev);
    unregister_chrdev_region(data->dev_num, 1);

    dev_info(&client->dev, "mysensor driver removed\n");
    return 0;
}

/* ============================================================
 * Driver Registration
 * ============================================================ */

/* Device ID table: list of device names this driver handles.
 * The kernel uses this for module autoloading and non-DT matching. */
static const struct i2c_device_id mysensor_id[] = {
    { "mysensor", 0 },
    { }  /* Terminating entry - required */
};
MODULE_DEVICE_TABLE(i2c, mysensor_id);

/* Device Tree match table: used on platforms like Raspberry Pi
 * that boot with a Device Tree. The compatible string here must
 * match what is in your .dts overlay file. */
static const struct of_device_id mysensor_of_match[] = {
    { .compatible = "mycompany,mysensor" },
    { }  /* Terminating entry - required */
};
MODULE_DEVICE_TABLE(of, mysensor_of_match);

/* The main driver structure */
static struct i2c_driver mysensor_driver = {
    .driver = {
        .name           = DRIVER_NAME,
        .of_match_table = mysensor_of_match,
    },
    .probe    = mysensor_probe,
    .remove   = mysensor_remove,
    .id_table = mysensor_id,
};

/* module_i2c_driver() is a convenience macro that generates the
 * module_init() and module_exit() functions automatically.
 * It calls i2c_add_driver() on init and i2c_del_driver() on exit. */
module_i2c_driver(mysensor_driver);

/* Module metadata - required for any kernel module */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name <email@example.com>");
MODULE_DESCRIPTION("Beginner I2C Linux Device Driver for Raspberry Pi");
MODULE_VERSION("1.0");

This is about 250 lines but every single line has a comment explaining why it exists. Read through it slowly. The structure is the same for virtually every I2C Linux device driver in the kernel tree.

8. Device Tree and I2C Overlay Configuration on Raspberry Pi

On Raspberry Pi, hardware is described to the kernel through a Device Tree. When you want to use an I2C device, you tell the kernel it exists by adding it to the Device Tree, either permanently in config.txt or through a runtime overlay.

What is a Device Tree Overlay?

The main Device Tree blob for your Pi is compiled and baked into the firmware. You cannot easily change it. But you can apply overlays on top of it at boot time. These overlays are small DTS (Device Tree Source) files that add or modify nodes.

Creating an Overlay for Your I2C Device

Create a file called mysensor-overlay.dts:

/dts-v1/;
/plugin/;

/ {
    compatible = "brcm,bcm2835";

    fragment@0 {
        /* target = <&i2c1> means we are adding to the i2c1 bus node.
         * On Raspberry Pi 4, the user-accessible I2C bus is i2c1,
         * which corresponds to /dev/i2c-1 and uses pins GPIO2 (SDA)
         * and GPIO3 (SCL). */
        target = <&i2c1>;
        __overlay__ {
            /* Enable the I2C controller */
            status = "okay";

            /* Define our sensor as a child of the I2C bus.
             * The address here (0x48) must match your hardware. */
            mysensor@48 {
                /* compatible string must match of_match_table in driver */
                compatible = "mycompany,mysensor";

                /* I2C address in hex, without 0x prefix */
                reg = <0x48>;

                /* Optional: human readable label */
                label = "my-temperature-sensor";

                /* status = "okay" enables this node.
                 * "disabled" would make the kernel ignore it. */
                status = "okay";
            };
        };
    };
};

Compile it with the Device Tree compiler:

dtc -@ -I dts -O dtb -o mysensor.dtbo mysensor-overlay.dts

The -@ flag is important: it tells dtc to include fixup information so the overlay can reference symbols in the base tree.

Copy the compiled overlay to the overlays directory:

sudo cp mysensor.dtbo /boot/overlays/

Apply it at runtime for testing:

sudo dtoverlay mysensor

Or add to /boot/config.txt for permanent loading:

dtoverlay=mysensor

After loading the overlay, you can verify the device was registered:

ls /sys/bus/i2c/devices/

You should see a new entry like 1-0048 (bus 1, address 0x48).

9. Registering Your Driver: i2c_add_driver and the Probe Function

When you run insmod my_i2c_driver.ko, the kernel calls your module’s init function, which (via module_i2c_driver) calls i2c_add_driver(). This registers your struct i2c_driver with the I2C core.

The I2C core then scans through all registered I2C devices (from Device Tree, board files, or explicit instantiation) and tries to match them to your driver. Matching happens in two ways:

Device Tree matching: The compatible string in your overlay (“mycompany,mysensor”) is compared against your of_match_table. If they match, probe() is called.

ID table matching: The name field in registered i2c_board_info structures is compared against your id_table. This is the older non-DT method.

When a match is found, the kernel calls your probe() function with the matched i2c_client. If probe returns 0, the driver is considered bound to the device. If it returns non-zero, binding fails and the kernel logs the error.

Loading and Unloading Your Module

Build the driver:

cd ~/i2c_driver_project
make

This produces my_i2c_driver.ko.

Load the driver:

sudo insmod my_i2c_driver.ko

Check kernel log immediately:

dmesg | tail -20

If probe succeeded, you will see your dev_info() messages. Check that /dev/mysensor0 exists:

ls -la /dev/mysensor0

Unload the driver:

sudo rmmod my_i2c_driver

10. Reading and Writing Data Over I2C in Kernel Space

There are two approaches to I2C communication in your driver: SMBus helpers and raw I2C transfers. Here is when to use each.

SMBus Helpers (Use These First)

The SMBus helper functions are implemented in drivers/i2c/i2c-core-smbus.c in the kernel. They are simpler and work on both full I2C and SMBus-only adapters. Use them for straightforward register reads and writes.

/* Read a single byte from register */
s32 val = i2c_smbus_read_byte_data(client, reg_addr);

/* Write a single byte to register */
i2c_smbus_write_byte_data(client, reg_addr, value);

/* Read 16-bit word from register (little-endian by default) */
s32 word = i2c_smbus_read_word_data(client, reg_addr);

/* Read a block of bytes */
u8 buf[16];
i2c_smbus_read_i2c_block_data(client, reg_addr, sizeof(buf), buf);

All of these return negative errno on failure and the data value on success.

Raw I2C Transfers with i2c_transfer

For operations that do not fit the SMBus model, use i2c_transfer(). It takes an array of struct i2c_msg and performs them as a single compound transaction.

/* Example: Write to a register then read back 4 bytes
 * This is a common pattern: write the register address, then
 * read the register contents without releasing the bus in between.
 * The I2C_M_RD flag marks a message as a read operation. */

u8 reg = REG_DATA_HIGH;
u8 rx_buf[4];

struct i2c_msg msgs[2] = {
    {
        /* First message: write the register address */
        .addr  = client->addr,
        .flags = 0,           /* 0 = write */
        .len   = 1,
        .buf   = &reg,
    },
    {
        /* Second message: read the data */
        .addr  = client->addr,
        .flags = I2C_M_RD,    /* read flag */
        .len   = sizeof(rx_buf),
        .buf   = rx_buf,
    }
};

int ret = i2c_transfer(client->adapter, msgs, ARRAY_SIZE(msgs));
if (ret != ARRAY_SIZE(msgs)) {
    dev_err(&client->dev, "I2C transfer failed: %d\n", ret);
    return ret < 0 ? ret : -EIO;
}

i2c_transfer() returns the number of messages successfully transferred on success, or negative errno on failure. Always check against the expected message count, not just for non-zero.

Using devm Wrappers for Managed Resources

In modern kernel drivers, prefer devm_ prefixed allocation functions whenever available. These automatically release resources when the device is removed, which prevents memory leaks and makes probe/remove code cleaner.

/* These are freed automatically when device is unbound */
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
gpio = devm_gpiod_get(&client->dev, "reset", GPIOD_OUT_LOW);
irq = devm_request_irq(&client->dev, irq_num, handler, 0, name, data);

11. Testing Your I2C Driver Without Hardware

Not everyone has every sensor on hand. Here is how to develop and test your I2C Linux device driver logic without physical hardware.

Using i2c-stub

The i2c-stub module creates a fake I2C adapter with a register space you can read and write from userspace. It is perfect for testing driver initialization and register access logic.

Load it with a chip address:

sudo modprobe i2c-stub chip_addr=0x48

Find the bus number it created:

ls /sys/bus/i2c/devices/

You will see something like i2c-5 where 5 is the new stub bus number. Now you can populate fake register values:

sudo i2cset -y 5 0x48 0x0F 0xA5  # Set device ID register to expected value
sudo i2cset -y 5 0x48 0x01 0x00  # Set data high byte
sudo i2cset -y 5 0x48 0x02 0xF0  # Set data low byte

Now instantiate a fake device on the stub bus:

echo mysensor 0x48 | sudo tee /sys/bus/i2c/devices/i2c-5/new_device

Load your driver:

sudo insmod my_i2c_driver.ko

Your probe function will be called. Check dmesg to see if device ID verification passes and the driver binds successfully.

Virtual I2C Device Testing

Another approach for more complex testing is to write a small kernel module that creates an i2c_board_info entry explicitly registering your device on an existing adapter. This avoids needing a Device Tree overlay during development.

12. Real World Example: Writing a Driver for the BMP280 Pressure Sensor

Let us apply everything to a real device. The BMP280 from Bosch is an absolute pressure and temperature sensor used in weather stations, drones, and IoT devices. It communicates via I2C at address 0x76 or 0x77 depending on the state of the SDO pin.

Note: The real BMP280 already has a mainline kernel driver at drivers/iio/pressure/bmp280-i2c.c. We will write a simplified version from scratch to learn from it.

BMP280 Register Map Overview

0xD0: chip_id register - should read 0x60
0xF3: status register
0xF4: ctrl_meas - temperature and pressure oversampling + mode
0xF7: press_msb, press_lsb, press_xlsb (3 bytes)
0xFA: temp_msb, temp_lsb, temp_xlsb (3 bytes)
0x88-0xA1: calibration data (factory trimming values)

The BMP280 stores factory calibration coefficients in its non-volatile memory. To get accurate readings, your driver reads these coefficients at probe time and uses them in a compensation formula for every temperature and pressure reading. This is a common pattern in industrial sensor drivers.

Simplified BMP280 Probe Function

#define BMP280_CHIP_ID          0x60
#define BMP280_REG_ID           0xD0
#define BMP280_REG_CTRL_MEAS    0xF4
#define BMP280_REG_PRESS_MSB    0xF7
#define BMP280_REG_CALIB_START  0x88

#define BMP280_MODE_NORMAL      0x03
#define BMP280_OSRS_T_2X        (2 << 5)
#define BMP280_OSRS_P_16X       (5 << 2)

struct bmp280_calib {
    u16 T1;
    s16 T2, T3;
    u16 P1;
    s16 P2, P3, P4, P5, P6, P7, P8, P9;
};

struct bmp280_data {
    struct i2c_client *client;
    struct bmp280_calib calib;
};

static int bmp280_read_calibration(struct i2c_client *client,
                                   struct bmp280_calib *calib)
{
    u8 buf[24];
    int ret;

    ret = i2c_smbus_read_i2c_block_data(client, BMP280_REG_CALIB_START,
                                        sizeof(buf), buf);
    if (ret < 0)
        return ret;

    /* Calibration coefficients are stored little-endian */
    calib->T1 = (u16)(buf[1] << 8 | buf[0]);
    calib->T2 = (s16)(buf[3] << 8 | buf[2]);
    calib->T3 = (s16)(buf[5] << 8 | buf[4]);
    calib->P1 = (u16)(buf[7] << 8 | buf[6]);
    calib->P2 = (s16)(buf[9] << 8 | buf[8]);
    /* ... remaining coefficients similar */

    return 0;
}

static int bmp280_probe(struct i2c_client *client,
                        const struct i2c_device_id *id)
{
    struct bmp280_data *data;
    int chip_id;
    int ret;

    /* Verify chip identity */
    chip_id = i2c_smbus_read_byte_data(client, BMP280_REG_ID);
    if (chip_id < 0)
        return chip_id;

    if (chip_id != BMP280_CHIP_ID) {
        dev_err(&client->dev, "Not a BMP280 (ID=0x%02x)\n", chip_id);
        return -ENODEV;
    }

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

    data->client = client;
    i2c_set_clientdata(client, data);

    /* Load factory calibration data */
    ret = bmp280_read_calibration(client, &data->calib);
    if (ret < 0) {
        dev_err(&client->dev, "Failed to read calibration data\n");
        return ret;
    }

    /* Configure: 2x temperature oversampling, 16x pressure, normal mode */
    ret = i2c_smbus_write_byte_data(client, BMP280_REG_CTRL_MEAS,
                                    BMP280_OSRS_T_2X | BMP280_OSRS_P_16X |
                                    BMP280_MODE_NORMAL);
    if (ret < 0)
        return ret;

    dev_info(&client->dev, "BMP280 ready (ID=0x%02x)\n", chip_id);
    return 0;
}

This gives you the pattern used by actual production sensor drivers: verify chip ID, read calibration, configure measurement mode, expose data.

13. Debugging I2C Drivers on Raspberry Pi

Debugging kernel drivers is different from debugging userspace code. You cannot attach gdb to a running kernel module the normal way. Here are the techniques that actually work.

pr_info and dev_info: Your Best Friends

Use these liberally:

dev_info(&client->dev, "probe called, addr=0x%02x\n", client->addr);
dev_dbg(&client->dev, "read register 0x%02x: 0x%02x\n", reg, val);
dev_warn(&client->dev, "unexpected value in config register\n");
dev_err(&client->dev, "I2C transfer failed: %d\n", ret);

dev_dbg messages are compiled out in non-debug builds. Enable them with:

echo 8 > /proc/sys/kernel/printk  # Maximum verbosity

Or more precisely, enable dynamic debug for your module:

echo module my_i2c_driver +p > /sys/kernel/debug/dynamic_debug/control

i2c-Tools for Hardware Verification

Before loading your driver, always verify the hardware is working at the protocol level:

# Scan bus 1 for all responding devices
sudo i2cdetect -y 1

# Read all registers of device at address 0x48 on bus 1
sudo i2cdump -y 1 0x48

# Read register 0x0F from device 0x48
sudo i2cget -y 1 0x48 0x0F

# Write 0x03 to register 0xF4
sudo i2cset -y 1 0x48 0xF4 0x03

If i2cdetect does not show your device, the problem is hardware (wiring, pull-ups, address). There is no point debugging kernel code for a hardware problem.

Checking the sysfs Device Tree

After loading your overlay and driver:

# See all bound I2C devices
cat /sys/bus/i2c/devices/1-0048/name

# Check if driver is bound
ls /sys/bus/i2c/devices/1-0048/driver

Reading the Kernel Oops

If your driver causes a kernel panic or oops, Linux prints a register dump and stack trace to dmesg. Learn to read it. The key lines are:

  • BUG: Unable to handle kernel NULL pointer dereference – You dereferenced a NULL pointer
  • Call Trace: – The function call stack where the crash happened
  • The hexadecimal addresses can be resolved to function names with addr2line

Common causes of kernel oops in I2C drivers: forgetting to check return values from i2c_smbus calls, freeing memory that was allocated with devm, and NULL pointer dereferences when private data was not set correctly.

14. Common Mistakes Beginners Make and How to Avoid Them

Here are the mistakes that cost people hours of debugging. Learn them now and save yourself the pain.

Mistake 1: Not Checking Return Values from I2C Functions

Every I2C communication function can fail. If you ignore return values, your driver silently uses garbage data and you have no idea why.

/* Wrong - ignores error */
val = i2c_smbus_read_byte_data(client, REG_CONFIG);
config |= val;

/* Right - checks error */
ret = i2c_smbus_read_byte_data(client, REG_CONFIG);
if (ret < 0) {
    dev_err(&client->dev, "config read failed: %d\n", ret);
    return ret;
}
config |= (u8)ret;

Mistake 2: Using copy_from_user / copy_to_user in the Wrong Direction

copy_to_user(user_ptr, kernel_ptr, size) – copies FROM kernel TO user
copy_from_user(kernel_ptr, user_ptr, size) – copies FROM user TO kernel

Getting these backwards will cause a kernel fault immediately.

Mistake 3: Forgetting MODULE_LICENSE

Every kernel module must have MODULE_LICENSE("GPL"). Without it, the kernel taints itself (you will see “tainted kernel” in dmesg) and some kernel functions will be unavailable to your module.

Mistake 4: Allocating Memory in Interrupt Context

If you ever add interrupt handling to your driver, remember that kmalloc() with GFP_KERNEL can sleep and must not be called from interrupt context. Use GFP_ATOMIC instead.

Mistake 5: Not Handling Probe Failure Cleanup Correctly

If probe fails halfway through, you must clean up everything allocated before the failure point. The goto error handling pattern shown in the code above is the standard way to handle this cleanly.

Mistake 6: Confusing I2C Address Formats

The Raspberry Pi’s /dev/i2c-1 interface uses 7-bit addresses. But some datasheets show 8-bit addresses (with the R/W bit included). If your datasheet says the device address is 0x90, the actual 7-bit address for i2cdetect and your driver is 0x48 (0x90 >> 1). Always use 7-bit addresses in kernel code.

Mistake 7: Blocking Operations in Probe

Probe is called with the I2C bus lock held. Do not sleep for extended periods or wait for long timeouts in probe. If your device needs time to initialize after power-on, check if it is ready with a retry loop and short delays using msleep().

15. Next Steps: Where to Go After Your First I2C Driver

Now that you can write a basic I2C Linux device driver, here is where to go next.

Explore the IIO Subsystem

The Industrial I/O (IIO) subsystem is the kernel framework specifically designed for sensors: accelerometers, gyroscopes, pressure sensors, temperature sensors, ADCs. Instead of writing a raw character device, real sensor drivers expose data through IIO, which gives you sysfs attributes, triggered buffering, and compatibility with userspace tools like iio-sensor-proxy automatically.

Look at drivers/iio/pressure/bmp280-i2c.c in the kernel source for a clean example of a production-quality I2C sensor driver using IIO.

Study Existing Drivers in the Kernel Source

The kernel source has hundreds of I2C drivers under drivers/. Some good ones to study for patterns:

  • drivers/hwmon/lm75.c – Simple temperature sensor, clean code
  • drivers/rtc/rtc-ds1307.c – Real-time clock, multiple device variants
  • drivers/iio/imu/mpu6050/ – IMU sensor with DMA and interrupt support

Learn About sysfs Attributes

Instead of a character device, expose your sensor data through sysfs attributes. This is simpler and more Linux-idiomatic for single-value sensor readings:

static ssize_t temperature_show(struct device *dev,
                                 struct device_attribute *attr, char *buf)
{
    struct mysensor_data *data = dev_get_drvdata(dev);
    int16_t val;
    mysensor_read_raw(data->client, &val);
    return sysfs_emit(buf, "%d\n", val);
}
static DEVICE_ATTR_RO(temperature);

After creating the attribute, your temperature value is readable at /sys/bus/i2c/devices/1-0048/temperature with a simple cat command.

Add Interrupt Support

Many sensors assert an interrupt when new data is ready or when a threshold is crossed. Adding interrupt support to your I2C driver involves devm_request_irq(), interrupt handler functions, and often a work queue or threaded interrupt for the actual data processing.

Submit Your Driver to the Linux Kernel

If you write a driver for a real, commercially available device that is not yet in the mainline kernel, consider submitting it upstream. The Linux kernel contribution process involves posting patches to the relevant mailing list (linux-i2c@vger.kernel.org for I2C drivers), getting code review from maintainers, and going through several revision cycles. The kernel maintainers are thorough but fair, and getting a driver accepted upstream means it is maintained forever by the community.

Start by reading Documentation/process/submitting-patches.rst in the kernel source.

Summary: What You Just Learned

Let us recap what you now know about writing an I2C Linux device driver:

The I2C protocol uses two wires, SDA and SCL, with 7-bit device addresses. Every transfer starts with a START condition, then address plus direction bit, then data bytes, each acknowledged by the receiver, then a STOP condition.

The Linux I2C subsystem has three layers: the adapter driver (talks to hardware), the I2C core (the glue), and the client driver (what you write). Your driver lives at the top and uses the core API.

A minimal I2C client driver needs a struct i2c_driver with a probe function, a remove function, an id_table, and an of_match_table. You register it with i2c_add_driver() (or the module_i2c_driver macro).

In probe, verify your hardware with the device ID register, allocate resources with devm_ functions, initialize the hardware, and register any kernel interfaces you need. In remove, undo everything in reverse order.

For I2C communication, use SMBus helpers for simple register reads and writes and i2c_transfer() with struct i2c_msg arrays for complex multi-message transactions.

On Raspberry Pi, devices are declared in Device Tree overlays, compiled with dtc, and applied with dtoverlay. The compatible string in the overlay must match your driver’s of_match_table.

Debug with dmesg, i2c-tools for hardware verification, and i2c-stub for software testing without real hardware.

Final Thoughts

Writing your first I2C Linux device driver feels like a big deal, and it kind of is. You are writing code that runs in the kernel, directly managing hardware. But as you can see, the structure is well-defined and the kernel gives you good tools to work with.

The learning curve here is not about I2C itself. I2C is simple. The curve is understanding how the Linux device model works, how probe and remove fit into device lifecycle management, and how to write kernel code that is safe and correct. Once you have that mental model, every I2C driver you write after this one will come together much faster.

Start with the example driver here, swap out the register addresses and device ID for your actual hardware, and you will have a working I2C Linux device driver running on your Raspberry Pi today.

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

Related Posts

Leave a Comment