I2C Device Driver Basics- Free Linux Device Drivers Course

I2C Device Driver Basics

A free Linux device drivers course lecture on writing an I2C device driver in the Linux kernel, updated for kernel 6.x

15+ min read
Kernel 6.x ready
1 hands-on driver
Free embedded systems course

Topics covered in this free Linux kernel development course lecture

i2c device driver linux kernel
i2c_driver structure
i2c_client structure
probe function
free linux device drivers course
free embedded linux course

If you are looking to build an i2c device driver linux kernel module from scratch, this lecture is your starting point. I2C (Inter-Integrated Circuit) is the two-wire bus you will meet on almost every embedded board — connecting EEPROMs, RTC chips, temperature sensors, PMICs, and GPIO expanders to your SoC. In this lecture of our free Linux device drivers course, we break down exactly how an i2c device driver linux kernel module is structured, how the kernel matches your driver to a physical chip, and how to write a minimal, working client driver on a modern kernel.

What You Will Learn

  • How the I2C bus works at the hardware level (SDA, SCL, open-drain signalling)
  • The relationship between the I2C bus controller driver and an i2c device driver linux kernel module
  • The two core data structures: i2c_driver and i2c_client
  • How the modern, single-argument probe() callback works on kernel 6.x
  • How to store and retrieve per-device private data safely
  • How to write, build, and load your own original I2C client driver

Prerequisites

  • Basic C programming knowledge
  • A working kernel module build environment (kernel headers installed, make toolchain)
  • Familiarity with character device drivers is helpful but not mandatory
  • A Linux board or virtual machine with an accessible I2C bus (a Raspberry Pi or QEMU with an I2C bus emulated is enough for testing)

Understanding the I2C Bus Before Writing an I2C Device Driver

Before you touch a single line of an i2c device driver linux kernel module, you need to understand the wire it talks over. I2C is a two-wire, multi-master, synchronous serial bus originally developed by Philips (now NXP). The two lines are:

  • SDA (Serial Data) — carries the actual data, bidirectional
  • SCL (Serial Clock) — generated by the bus master to synchronize every bit transferred on SDA

Both lines are open-drain. A device can only pull a line low; it can never actively drive it high. External pull-up resistors bring the lines back to +Vdd whenever every device on the bus releases them. This single detail explains why a missing pull-up resistor is one of the most common “device not detected” bugs you will debug as a driver author.

I2C Bus Topology
CPU / SoC
I2C Bus Master
SDA / SCL
RTC Chip (0x51)
EEPROM (0x50)
Temp Sensor (0x48)

The SoC’s CPU is almost always the sole bus master in embedded designs, even though I2C technically supports multi-master operation. Each client device answers to a unique 7-bit (occasionally 10-bit) address, and this address is exactly what your device tree reg property and your driver’s i2c_client structure will describe.

I2C Bus Speed Modes

Mode Clock Speed Typical Use
Standard Mode Up to 100 KHz Simple sensors, EEPROMs
Fast Mode Up to 400 KHz General embedded peripherals
Fast Mode Plus Up to 1 MHz High-throughput sensors, PMICs
High-Speed Mode Up to 3.4 MHz Specialized industrial use
Ultra-Fast Mode Up to 5 MHz Unidirectional, rarely used

Note: the bus speed is configured and enforced by the I2C bus controller driver (for example i2c-imx.c or i2c-bcm2835.c), not by your client driver. As a client driver author, you never manage clock timing yourself.

I2C Device Driver Architecture in the Linux Kernel

The Linux kernel splits I2C support into two clean layers, and understanding this split is the single most important mental model for writing any i2c device driver linux kernel module:

Two-Layer I2C Driver Model
Bus Controller Driver
Owns the SoC’s I2C hardware block, drives SDA/SCL timing, exposes a struct i2c_adapter
I2C Core Framework
Matches devices to drivers, calls your probe()/remove()
Your I2C Client (Device) Driver
Talks to one specific chip using i2c_smbus_* / i2c_transfer() calls

The bus controller driver is board- and SoC-specific — you rarely write one yourself in an embedded application project. Your job as an i2c device driver linux kernel author is almost always at the top layer: the client driver, which handles a single physical chip such as an EEPROM or a sensor and never worries about how SDA/SCL toggling actually happens.

Two Structures That Define Every I2C Device Driver

Every i2c device driver linux kernel module is built around exactly two data structures, both declared in linux/i2c.h:

Structure Represents Who Fills It
struct i2c_driver Your driver itself — its callbacks and matching rules You, at compile time
struct i2c_client One physical device instance on the bus The I2C core, at runtime, from device tree or board data

Rather than reproducing the raw kernel struct definitions here, the table below summarizes the fields you will actually touch while writing your own i2c device driver linux kernel module:

i2c_driver Field Purpose
probe Called when the core finds a matching device; your setup code lives here
remove Called on driver unbind; clean-up counterpart of probe
shutdown Called on system shutdown/reboot, before power is cut
driver.name Name shown in sysfs and used for legacy name matching
driver.of_match_table Device tree compatible-string matching table
id_table Legacy/non-DT matching table, also required for module auto-loading
i2c_client Field Purpose
addr 7-bit slave address of this device on the bus
name Device name string
adapter Pointer back to the bus (i2c_adapter) this device sits on
dev Embedded struct device, used for devm_* allocations and dev_info/dev_err logging
irq Interrupt line assigned to this device, if any

The probe() Function in a Modern I2C Device Driver

probe() is where your i2c device driver linux kernel module actually comes alive. The I2C core calls it exactly once, right after it decides your driver matches a device that has appeared on the bus (usually from a device tree node).

Kernel modernization note: older references show probe() taking two arguments — the i2c_client pointer and an i2c_device_id pointer. Since kernel 6.3, that second parameter is gone. probe() now takes a single struct i2c_client * argument, matching the pattern already used across most other bus types in the kernel. If you look up older sample code, add this modernization mentally before typing anything.

Inside probe(), a well-written i2c device driver linux kernel module typically performs these steps, in order:

  1. Confirm the underlying adapter supports the transfer functionality you need, using i2c_check_functionality()
  2. Allocate a private per-device data structure with devm_kzalloc()
  3. Read any device tree properties specific to this chip
  4. Talk to the hardware once (read a chip ID register) to confirm the correct chip is present
  5. Register with the appropriate kernel subsystem (hwmon, RTC class, input, misc device, and so on)
  6. Store your private data pointer against the client for later retrieval

Per-Device Private Data in an I2C Device Driver

Because the same i2c device driver linux kernel module can bind to several physical chips at once (imagine three identical temperature sensors on one board), you cannot use global variables to store per-chip state. The I2C core gives you a clientdata pointer for exactly this purpose:

  • i2c_set_clientdata(client, priv) — attach your private structure to this client
  • i2c_get_clientdata(client) — retrieve it back inside any other callback (remove, read, write, IRQ handler)

Internally these simply wrap dev_set_drvdata()/dev_get_drvdata() on the embedded struct device, so the same private-data pattern you already know from platform drivers applies directly here.

Hands-On: Writing Your First I2C Device Driver

Let’s put the theory to use. Below is an original, minimal i2c device driver linux kernel module named ep_i2c_intro. It does not copy any book or vendor sample — it is written fresh for this lecture, and it only prints identifying information about the device it binds to, which is exactly what you want in a first driver.

// ep_i2c_intro.c - EmbeddedPathashala original I2C client driver example
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/of.h>

struct ep_i2c_priv {
    struct i2c_client *client;
};

static int ep_i2c_intro_probe(struct i2c_client *client)
{
    struct ep_i2c_priv *priv;

    if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C))
        return -ENODEV;

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

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

    dev_info(&client->dev,
             "ep_i2c_intro bound: name=%s addr=0x%02x irq=%d\n",
             client->name, client->addr, client->irq);

    return 0;
}

static void ep_i2c_intro_remove(struct i2c_client *client)
{
    struct ep_i2c_priv *priv = i2c_get_clientdata(client);

    dev_info(&client->dev, "ep_i2c_intro removed: addr=0x%02x\n",
             priv->client->addr);
}

static const struct of_device_id ep_i2c_intro_of_match[] = {
    { .compatible = "ep,i2c-intro" },
    { }
};
MODULE_DEVICE_TABLE(of, ep_i2c_intro_of_match);

static const struct i2c_device_id ep_i2c_intro_id[] = {
    { "ep-i2c-intro", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, ep_i2c_intro_id);

static struct i2c_driver ep_i2c_intro_driver = {
    .driver = {
        .name           = "ep_i2c_intro",
        .of_match_table = ep_i2c_intro_of_match,
    },
    .probe    = ep_i2c_intro_probe,
    .remove   = ep_i2c_intro_remove,
    .id_table = ep_i2c_intro_id,
};
module_i2c_driver(ep_i2c_intro_driver);

MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Original minimal I2C client driver example");
MODULE_LICENSE("GPL");

Add a matching device tree node so the I2C core actually instantiates a client for this driver:

&i2c1 {
    ep_sensor: ep-sensor@50 {
        compatible = "ep,i2c-intro";
        reg = <0x50>;
    };
};

Building and Loading the I2C Device Driver

$ make -C /lib/modules/$(uname -r)/build M=$PWD modules
$ sudo insmod ep_i2c_intro.ko
$ dmesg | tail -n 3

Expected output once the overlay/device tree node loads and your module binds:

[  102.441233] ep_i2c_intro 1-0050: ep_i2c_intro bound: name=ep-sensor addr=0x50 irq=-1

To remove the driver cleanly:

$ sudo rmmod ep_i2c_intro
$ dmesg | tail -n 1
[  118.902011] ep_i2c_intro 1-0050: ep_i2c_intro removed: addr=0x50

You can also confirm binding without touching dmesg at all:

$ ls /sys/bus/i2c/drivers/ep_i2c_intro/
1-0050  bind  module  uevent  unbind
$ cat /sys/bus/i2c/devices/1-0050/name
ep-sensor

Common Mistakes When Writing an I2C Device Driver

Mistake Symptom Fix
Missing MODULE_DEVICE_TABLE Module loads only with manual insmod, never auto-loads Always export both the of and i2c id tables
Using the old two-argument probe() signature Build failure on kernel 6.3+ Switch to the single-argument probe(struct i2c_client *)
No pull-up resistors on SDA/SCL Device never detected, bus reads all-1s Verify board pull-ups or enable internal ones if the SoC supports it
Blocking sleep/allocation inside an interrupt-context I2C callback “scheduling while atomic” kernel warning Defer I2C transfers to a workqueue or threaded IRQ handler
Forgetting i2c_check_functionality() Silent failures on adapters lacking SMBus support Always validate functionality before your first transfer

Best Practices for I2C Device Drivers

  • Prefer devm_* managed allocations so cleanup happens automatically on remove()
  • Always match through device tree (of_match_table) on modern boards; keep id_table for module autoloading compatibility
  • Read a known chip-ID register in probe() to fail fast on the wrong hardware
  • Keep bus transactions short — I2C is a shared, relatively slow bus
  • Use dev_err()/dev_info(), not printk(), so messages are tied to the correct device

Performance and Security Considerations

I2C is not a high-throughput bus, so an i2c device driver linux kernel module should avoid polling in a tight loop; use interrupts or threaded IRQ handlers wherever the chip supports them. On the security side, treat any I2C device tied to a physical port (for example, exposed pins on a board) as an untrusted input source — validate register reads before trusting them, and never expose raw I2C transfer capability to userspace without careful access control, since a malicious write to the wrong register can corrupt or damage attached hardware.

Real-World Use Cases for I2C Client Drivers

  • RTC chips — battery-backed real-time clocks (DS1307-family and similar)
  • EEPROMs — configuration and calibration data storage
  • Temperature and humidity sensors — environmental monitoring in industrial and IoT boards
  • PMICs — power management chips exposing voltage rails as I2C registers
  • GPIO expanders — adding extra digital I/O pins over I2C when SoC GPIOs run out

Summary and Key Takeaways

  • I2C is a two-wire, open-drain, master-driven bus; pull-up resistors are mandatory
  • An i2c device driver linux kernel module never manages bus timing — the bus controller driver does that
  • struct i2c_driver describes your driver; struct i2c_client describes one physical chip instance
  • Since kernel 6.3, probe() takes a single struct i2c_client * argument
  • i2c_set_clientdata()/i2c_get_clientdata() is the standard way to carry per-device private state

Conclusion

This lecture gave you the complete mental model needed to start writing any i2c device driver linux kernel module — from the physical two-wire bus, through the controller/client split, to a working, modernized probe() implementation you built and tested end to end. In the next lecture of this free Linux device drivers course, we go deeper into actually reading and writing registers on the bus using i2c_smbus_* and i2c_transfer() calls, building directly on the ep_i2c_intro driver you wrote here.

Frequently Asked Questions About I2C Device Drivers

What is an I2C device driver in the Linux kernel?

An i2c device driver linux kernel module is code that binds to one physical chip on an I2C bus and talks to it through the standard struct i2c_driver/struct i2c_client interfaces, without managing bus timing itself.

Do I need to write a bus controller driver too?

No. The bus controller driver is normally already provided by your SoC vendor or mainline kernel. As an application driver author you only write the client driver layer.

Why did probe() lose its second argument?

The i2c_device_id parameter was rarely used since most drivers match through device tree. Kernel 6.3 finalized the transition to a single-argument probe(struct i2c_client *) across the I2C subsystem.

What happens if I forget the id_table in my i2c_driver?

Your module will still bind manually, but automatic module loading based on the modalias exposed in sysfs will not work, which breaks udev-based auto-loading on real systems.

How do I know which I2C address my device uses?

Check the chip datasheet for its 7-bit address, and confirm it against the reg property in your device tree node, which is what the kernel copies into i2c_client->addr.

Can one I2C device driver handle multiple physical chips?

Yes — this is normal. probe() runs once per matched device, and i2c_set_clientdata() keeps each chip’s state separate.

Why is my I2C device never detected?

The most common causes are missing pull-up resistors, a wrong address in the device tree reg property, or a device tree overlay that never actually loaded — check dmesg and /sys/bus/i2c/devices/ first.

Is this free embedded systems course suitable for beginners?

Yes. This free Linux kernel development course lecture assumes only basic C and a working kernel build environment, and every concept is explained from first principles before the hands-on example.

Continue This Free Linux Device Drivers Course

Keep learning I2C, device tree, and kernel driver development with EmbeddedPathashala’s free embedded Linux course.

 

Leave a Reply

Your email address will not be published. Required fields are marked *