I2C Client Driver Checklist- Free Linux Device Drivers Course

I2C Client Driver Checklist
Free Linux Kernel Development Course — I2C Client Drivers, Part 6
Kernel 6.x Ready
Device Tree Only
Free Linux Device Drivers Course

Welcome back to our free Linux kernel development course. In the last two lectures of this free Linux device drivers course series, you saw how an I2C client driver can bind purely through a device tree node, without a hand-written board file. In this lecture we tie every piece together into a single i2c client driver checklist that you can reuse for any real sensor, EEPROM, or expander chip you work with. We will also clear up a very old point of confusion — whether an i2c_device_id table is still mandatory — and finish with one complete, original driver you can build and load on kernel 6.x.

free embedded systems course
free linux development course
free linux device drivers course
free linux kernel development course
i2c client driver checklist

What You Will Learn

  • Why i2c_device_id used to be mandatory, and why modern kernels no longer require it for device-tree-only drivers
  • The full five-step checklist for writing any I2C client driver from scratch
  • How to declare an I2C child node under an I2C bus node without any board file
  • A complete, original I2C client driver you can compile against a 6.x kernel
  • How to build, load, and verify the driver using i2c-tools and dmesg
  • Common mistakes beginners make when registering I2C drivers, and how to avoid them

Prerequisites

Before this lecture, you should be comfortable with the earlier parts of this I2C chapter: I2C bus fundamentals (SDA/SCL, addressing), the i2c_driver/i2c_client structures, i2c_transfer() and SMBus calls, and declaring an I2C device node in the device tree. You will also need a Linux kernel source tree (6.x recommended), a cross-compiler or a Raspberry Pi / BeagleBone style board with an accessible I2C bus, and basic familiarity with make and insmod.

Why id_table Was Once Mandatory in I2C Drivers

On very old kernels, the I2C core rejected a driver’s probe call unless it supplied an i2c_device_id table — even if the device was already matched through the device tree’s of_match_table. This forced every driver author to keep a legacy ID table around purely to satisfy a core-level check, regardless of whether device tree matching was actually being used.

That restriction was lifted a long time ago. On every kernel you will realistically work with today (5.x and 6.x), the I2C core happily probes a driver that supplies either an i2c_device_id table or an of_device_id table — you no longer need both. If your hardware is described entirely through device tree, you can drop the legacy ID table completely and rely on of_match_table alone.

You will still see the legacy table in production drivers because many I2C drivers also support non-DT boards (x86 platforms using ACPI or static board files), so keeping both tables around remains good practice for a driver meant to be portable. But for a device-tree-only embedded target, one table is enough.

Matching Path Decision
Your Target Table(s) Needed
Device tree only (typical ARM/RISC-V SoC board) of_device_id alone is sufficient
Legacy board file / ACPI x86 target i2c_device_id alone is sufficient
Driver shipped for multiple platform types Keep both tables for portability

Declaring the I2C Device Without a Board File

The device tree remains the recommended way to describe an I2C peripheral on modern embedded Linux boards. A child node is placed directly under the I2C bus controller’s node, and the kernel builds the matching struct i2c_client automatically at boot — you never construct it by hand. All the node needs is a compatible string that matches an entry in your driver’s of_device_id table, and a reg property holding the 7-bit slave address.

Because we already covered node syntax in detail earlier in this chapter, here we focus on the part beginners skip: verifying the node actually reached the kernel before you even load your driver.

# Confirm the overlay or built-in node is visible to the kernel
ls /proc/device-tree/i2c@*/ep-therm@48/

# Read back the compatible string the kernel parsed
cat /proc/device-tree/i2c@*/ep-therm@48/compatible

If that path does not exist, your driver’s probe function will never run — no amount of debugging the C code will help until the node itself is present in the running device tree.

The I2C Client Driver Checklist

Every I2C client driver you write, regardless of what the chip actually does, follows the same five-step shape. Keep this checklist next to you the first few times you write one:

Five Steps To Any I2C Client Driver
1 Declare an of_device_id table (and an i2c_device_id table if legacy support is needed)
2 Register each table with MODULE_DEVICE_TABLE(of, ...) / MODULE_DEVICE_TABLE(i2c, ...)
3 Write probe() to identify the chip, configure it, and store private data with i2c_set_clientdata()
4 Fill a struct i2c_driver with your tables, probe, remove, and driver name
5 Register with module_i2c_driver()

Complete Worked Example: ep_ds_therm

Here is a full, original I2C client driver for a small SMBus-style temperature sensor. It matches purely through device tree, keeps a mutex-protected private context per client, and reads the temperature register on open. This is written for kernel 6.x and uses the modern single-argument probe() signature.

#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/of.h>
#include <linux/mutex.h>

#define EP_THERM_TEMP_REG   0x00

struct ep_therm_data {
    struct i2c_client *client;
    struct mutex lock;
    s16 last_temp_raw;
};

static int ep_therm_read_temp(struct ep_therm_data *data)
{
    int ret;

    mutex_lock(&data->lock);
    ret = i2c_smbus_read_word_swapped(data->client, EP_THERM_TEMP_REG);
    if (ret >= 0)
        data->last_temp_raw = (s16)ret;
    mutex_unlock(&data->lock);

    return ret;
}

static int ep_therm_probe(struct i2c_client *client)
{
    struct ep_therm_data *data;
    int ret;

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

    data->client = client;
    mutex_init(&data->lock);
    i2c_set_clientdata(client, data);

    ret = ep_therm_read_temp(data);
    if (ret < 0) {
        dev_err(&client->dev, "initial temperature read failed: %d\n", ret);
        return ret;
    }

    dev_info(&client->dev, "ep_ds_therm ready, raw=0x%04x\n", data->last_temp_raw);
    return 0;
}

static void ep_therm_remove(struct i2c_client *client)
{
    struct ep_therm_data *data = i2c_get_clientdata(client);

    mutex_destroy(&data->lock);
    dev_info(&client->dev, "ep_ds_therm removed\n");
}

static const struct of_device_id ep_therm_of_match[] = {
    { .compatible = "packtclass,ep-ds-therm" },
    { }
};
MODULE_DEVICE_TABLE(of, ep_therm_of_match);

static struct i2c_driver ep_therm_driver = {
    .driver = {
        .name = "ep_ds_therm",
        .of_match_table = ep_therm_of_match,
    },
    .probe = ep_therm_probe,
    .remove = ep_therm_remove,
};
module_i2c_driver(ep_therm_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Original demo I2C client driver for the free Linux kernel development course");

Matching device tree node for this driver:

&i2c1 {
    status = "okay";

    ep-therm@48 {
        compatible = "packtclass,ep-ds-therm";
        reg = <0x48>;
    };
};

Building and Testing on Target

# Build the module against your kernel source tree
make -C /lib/modules/$(uname -r)/build M=$PWD modules

# Load it
sudo insmod ep_ds_therm.ko

# Confirm the probe ran
dmesg | tail -5

# Confirm the device is visible on the bus before/after loading
sudo i2cdetect -y 1

# Unload when done
sudo rmmod ep_ds_therm

If i2cdetect shows the address as UU, that is expected — it means a kernel driver already claimed the device, which is exactly what you want to see once ep_ds_therm is loaded.

Common Mistakes and Troubleshooting

Symptom Likely Cause
probe() never runs Device tree node missing, disabled (status = "disabled"), or compatible string mismatch
Driver loads but no device found Wrong reg address, or bus number in the overlay does not match the physical bus
“scheduling while atomic” on read SMBus/I2C calls made from atomic or interrupt context — always call them from process context
Compile error on remove() Using the old int-returning remove() signature on kernel 6.1+, which now expects void

Best Practices

  • Prefer of_match_table alone for device-tree-only targets; only add i2c_device_id if you need legacy or ACPI compatibility
  • Use devm_kzalloc() for private data so it is freed automatically on driver removal
  • Protect any shared client state with a mutex, since I2C transfers can sleep
  • Return meaningful negative errno values from probe() so failures are easy to diagnose from dmesg
  • Keep MODULE_DEVICE_TABLE() calls in place even for DT-only drivers — this is what allows module auto-loading via udev

Performance and Security Considerations

I2C is a slow bus by design (typically 100 kHz to 1 MHz), so avoid polling it from a tight loop — use interrupts or reasonable sleep intervals instead. On the security side, never trust register values read back from an external chip without range-checking them before exposing them to userspace through sysfs or a character device, since a malfunctioning or spoofed device could otherwise feed unexpected data into your driver’s logic.

Chapter Summary: I2C Client Drivers

Across this chapter of the free Linux kernel development course you have covered the full path from bus fundamentals to a working, device-tree-driven I2C client driver: I2C bus basics and the driver/client structures, i2c_transfer() and the SMBus function family, i2c-tools debugging, device tree node declaration and matching, and now the complete registration checklist with a working example. You are now equipped to write an I2C client driver for real hardware from a blank file.

Key Takeaways

  • Modern kernels only require one matching table — of_device_id or i2c_device_id — not both
  • Every I2C client driver follows the same five-step structure regardless of the chip
  • Always verify the device tree node reached the kernel before debugging your C code
  • Use devm_ allocation and mutex protection as your default starting point

Up next in this free embedded Linux course, we move from I2C to the SPI subsystem and write our first SPI client driver.

FAQ

Do I still need an i2c_device_id table on kernel 6.x?

No, not if your device is matched purely through device tree. An of_device_id table alone is enough for probe() to run.

What happens if I forget MODULE_DEVICE_TABLE?

The driver can still load manually with insmod, but udev-based auto-loading and modalias matching will not work, so the module will not load automatically when the device appears.

Can one driver support both device tree and legacy board files?

Yes. Provide both an of_device_id table and an i2c_device_id table in the same i2c_driver structure; the kernel matches whichever mechanism applies to the running platform.

Why does my probe function never get called?

The most common cause is that the device tree node is missing, disabled, or has a compatible string that does not exactly match an entry in your driver’s table.

Is the remove() function still required in every I2C driver?

It is good practice to define one so any manually acquired resources are released, even though devm-managed resources are freed automatically. On kernel 6.1 and later it must return void, not int.

What is the difference between i2c_smbus calls and i2c_transfer()?

SMBus calls are a simpler, more restrictive subset of I2C operations with fixed message shapes, while i2c_transfer() gives you full control over raw I2C messages, including custom repeated-start sequences.

Where should I put shared client state in a real driver?

In a private structure allocated in probe() with devm_kzalloc(), stored via i2c_set_clientdata(), and protected with a mutex since I2C access can sleep.

 

Continue the Free Linux Kernel Development Course

Next up: writing your first SPI client driver, the other major serial bus in embedded Linux.

Leave a Reply

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