Skip to content

How to Implement an I2C Device Driver

Time estimate: ~90+ minutes Prerequisites: Enable I2C, Device Tree and Drivers

Learning Objectives

By the end of this tutorial you will be able to:

  • Explain the Linux kernel module lifecycle (init, probe, read, exit)
  • Build and load a kernel module (.ko) for an I2C sensor
  • Write a character device driver step by step — from skeleton to working /dev/ node
  • Create a Device Tree overlay to bind a driver to hardware
  • Read sensor data through a /dev/ device node
  • Set up udev rules for device permissions
Device Trees and the Kernel/User-Space Boundary

On a microcontroller you hard-code pin numbers and bus addresses into your firmware. Linux separates that hardware description into a Device Tree -- a data structure passed to the kernel at boot that tells it which buses exist, what devices are connected, and at which addresses. Each Device Tree node has a compatible string (e.g., "mcp9808") that the kernel matches against registered drivers. Overlays extend the base tree at runtime, so you can add a sensor without recompiling the kernel.

Once the kernel matches a DT node to a driver, the driver's probe() function runs: it initialises the hardware and registers a user-space interface -- typically a character device in /dev/ or attributes in /sys/. Applications in user space never touch hardware registers directly; they open the device file and call read() or write(), and the kernel's driver performs the actual I2C transaction safely. This boundary protects the system: a bug in your application cannot corrupt the bus or crash the kernel.

In this tutorial you will create both sides of the binding: a Device Tree overlay that describes where the MCP9808 sensor is, and a kernel module that provides the how -- the code that reads the temperature register and exposes it through /dev/mcp9808.

For deeper reading see the Device Tree and Drivers reference page.


Why Write a Kernel Driver?

In the Enable I2C tutorial you read the MCP9808 temperature sensor from Python using smbus. That works — so why go through the effort of writing a kernel driver?

Aspect User-space I2C (smbus / i2c-tools) Kernel driver
Setup effort Minimal — pip install smbus Must compile a .ko, write a DT overlay
Concurrent access No inter-process locking — sequences can interleave (see below) Driver provides mutex protection for multi-step sequences
Standard interface Custom script, custom API Standard /dev/ node — any language can open() + read()
udev integration None — you must know the bus number and address Device appears automatically in /dev/ with configurable permissions
Power management Manual — your script runs 24/7 Kernel can suspend/resume the device
Other kernel consumers Not possible — only your user-space script sees the data hwmon, IIO, thermal subsystems can use the driver

Rule of thumb: for quick prototyping → user-space I2C. For production embedded systems or when multiple applications need the sensor → kernel driver.

The I2C Concurrency Problem

Opening /dev/i2c-1 does not lock the bus or the device. Multiple processes can open the same device file simultaneously. The kernel serialises individual I2C transfers at the bus level (bytes don't collide on the wire), but it does not prevent interleaving of higher-level sequences across multiple system calls.

Most sensors require multi-step operations — for example, "write register address" then "read data". If two scripts run concurrently, their steps can interleave:

Script A: set register pointer → 0x10
Script B: set register pointer → 0x20   ← sneaks in between A's calls
Script A: read 2 bytes          → reads from 0x20, not 0x10!

This doesn't physically damage the sensor, but it causes wrong reads, inconsistent data, or unexpected NACKs.

User-space workarounds:

  • Combined transaction — put write+read into a single I2C_RDWR ioctl so the bus adapter executes them atomically:
    from smbus2 import SMBus, i2c_msg
    write = i2c_msg.write(0x18, [0x05])   # register address
    read  = i2c_msg.read(0x18, 2)         # read 2 bytes
    bus.i2c_rdwr(write, read)             # atomic on the bus
    
  • Advisory file lock — use flock() so all cooperating scripts serialise access:
    import fcntl
    with open("/var/lock/i2c-bus1-addr0x18.lock", "w") as lockf:
        fcntl.flock(lockf, fcntl.LOCK_EX)
        # ... all I2C operations for this device ...
        fcntl.flock(lockf, fcntl.LOCK_UN)
    
    This is advisory — it only works if every script uses the same locking scheme.
  • Single daemon — centralise all sensor access in one process, expose data over a socket or D-Bus.

Kernel driver advantage: a driver can hold a struct mutex across the entire read sequence (set register, read data, decode). Every user-space caller goes through file_operations.read(), so the driver's mutex guarantees atomicity without requiring cooperation between applications. Note: the kernel doesn't add this automatically — you implement it in the driver. In this tutorial's simple MCP9808 driver, i2c_smbus_read_word_swapped() performs the write+read as a single bus transaction, which is sufficient. For sensors requiring multi-step configuration sequences, you would add an explicit mutex_lock()/mutex_unlock() around the critical section.


Theory: Key Kernel Structures

Before writing code, understand the four structures every I2C character device driver uses.

Driver Lifecycle

Stage Function Purpose
Load module module_init() Register the I2C driver with the kernel
Find device probe() Device Tree matched — initialise hardware, create /dev/ node
Device in operation file_operations User-space interaction (open, read, release)
Unload module module_exit() Unregister I2C driver, free resources

struct file_operations

Defines how user-space programs interact with your device:

static const struct file_operations mcp9808_fops = {
    .owner   = THIS_MODULE,
    .open    = mcp9808_open,
    .read    = mcp9808_read,
    .release = mcp9808_release,
};

When a program calls read() on /dev/mcp9808, the kernel invokes your mcp9808_read() function.

struct cdev and Major/Minor Numbers

A character device (struct cdev) connects your file_operations to a device number. The kernel identifies devices by a major number (which driver) and minor number (which instance). alloc_chrdev_region() asks the kernel to assign a free major number automatically.

struct i2c_driver

Tells the I2C subsystem about your driver:

static struct i2c_driver mcp9808_i2c_driver = {
    .driver = { .name = "mcp9808", .of_match_table = mcp9808_of_match },
    .probe  = mcp9808_probe,
    .remove = mcp9808_remove,
    .id_table = mcp9808_id,
};

The compatible string in of_match_table must match the Device Tree node — this is how the kernel knows to call your probe() when the hardware is present.


Hands-on: Build the Driver Step by Step

You will build the MCP9808 driver incrementally. Each step adds one concept, and you can compile and test after every step.

Course Source Repository

This tutorial references source files from the course repository. If you haven't cloned it yet on your Pi:

cd ~
git clone https://github.com/OE-KVK-H2IoT/embedded-linux.git

The complete driver source is at ~/embedded-linux/drivers/mcp9808.c. The steps below walk through building it from scratch — the repository copy is there as a reference if you get stuck.

Prepare the Build Environment

# Create a working directory
mkdir -p ~/mcp9808-driver && cd ~/mcp9808-driver

# Make sure kernel headers are installed
sudo apt install -y linux-headers-$(uname -r)

Create the Makefile:

obj-m += mcp9808.o

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

all:
    make -C $(KDIR) M=$(PWD) modules

clean:
    make -C $(KDIR) M=$(PWD) clean

Step 1: Skeleton Module

Start with the absolute minimum — a module that loads and unloads.

Create mcp9808.c:

#include <linux/module.h>
#include <linux/init.h>

static int __init mcp9808_init(void)
{
    pr_info("mcp9808: module loaded\n");
    return 0;
}

static void __exit mcp9808_exit(void)
{
    pr_info("mcp9808: module unloaded\n");
}

module_init(mcp9808_init);
module_exit(mcp9808_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("MCP9808 Temperature Sensor Driver");

Build and test:

make
sudo insmod mcp9808.ko
dmesg | tail -1
sudo rmmod mcp9808
dmesg | tail -1
Checkpoint — Skeleton Works

You should see mcp9808: module loaded and mcp9808: module unloaded in dmesg.


Step 2: I2C Driver Registration

Replace the manual init/exit with proper I2C driver registration. The kernel will call probe() when it finds a matching Device Tree node, and remove() when the driver is unloaded.

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

static int mcp9808_probe(struct i2c_client *client)
{
    dev_info(&client->dev, "mcp9808: probe called (addr=0x%02x)\n",
         client->addr);
    return 0;
}

static void mcp9808_remove(struct i2c_client *client)
{
    dev_info(&client->dev, "mcp9808: remove called\n");
}

static const struct i2c_device_id mcp9808_id[] = {
    { "mcp9808", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, mcp9808_id);

static const struct of_device_id mcp9808_of_match[] = {
    { .compatible = "mcp9808" },
    { }
};
MODULE_DEVICE_TABLE(of, mcp9808_of_match);

static struct i2c_driver mcp9808_i2c_driver = {
    .driver = {
        .name = "mcp9808",
        .of_match_table = mcp9808_of_match,
    },
    .probe    = mcp9808_probe,
    .remove   = mcp9808_remove,
    .id_table = mcp9808_id,
};

module_i2c_driver(mcp9808_i2c_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("MCP9808 Temperature Sensor Driver");

Key changes: - module_i2c_driver() replaces manual module_init/module_exit — it generates both automatically - i2c_device_id[] matches by name (for old-style binding) - of_device_id[] matches by compatible string (for Device Tree binding) - probe() receives the struct i2c_client — your handle to the I2C device

Build it with make. Before we can test probe(), the kernel needs to know the MCP9808 exists on the I2C bus — that requires a Device Tree overlay.

Checkpoint — I2C Driver Compiles

make should produce mcp9808.ko without errors. Don't load it yet — probe() won't fire until we create the Device Tree overlay.


Device Tree Overlay

The driver code is ready, but the kernel still doesn't know where the MCP9808 is connected. A Device Tree overlay tells it: "there is a device with compatible = "mcp9808" on I2C bus 1 at address 0x18."

First, confirm the sensor's I2C address using i2c-tools:

i2cdetect -y 1

You should see the MCP9808 at address 0x18:

     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:                         -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- 18 -- -- -- -- -- -- --
...
Tip

If the address is different (e.g., 0x19, 0x1A), the MCP9808 address pins (A0–A2) are pulled high. Use the address you see here in the DT overlay below.

Create mcp9808-overlay.dts:

vim mcp9808-overlay.dts

Paste this:

/dts-v1/;                      // Device Tree Source format version 1
/plugin/;                     // Marks this file as a DT overlay plugin

/ {
    compatible = "brcm,bcm2835"; // Targets all Raspberry Pi models (Broadcom BCM2835/6/7/1)

    fragment@0 {
        target = <&i2c1>;       // Apply this overlay to the i2c1 controller (usually on GPIO 2/3)

        __overlay__ {
            #address-cells = <1>; // I2C addresses are 1 byte
            #size-cells = <0>;    // No memory-mapped size, so size-cells is 0

            mcp9808@19 {           // Create a device node at I2C address 0x18
                compatible = "mcp9808"; // Must match the driver's i2c_device_id[]
                reg = <0x19>;           // I2C address of the device
            };
        };
    };
};
Set I2C device address

Set the proper I2C device address in the Device Tree overlay config. Modify from 19 to 18 in the node and the reg also

Tips for Customizing

If you're using multiple sensors or want to rename the node (e.g., temp1@18), that's totally doable.

|Field|Meaning|
|---|---|
|`compatible`|Must match the driver's name (`i2c_device_id[]`) in your kernel module|
|`i2c1`|Refers to the second I2C bus (first is `i2c0`, usually for firmware use)|
|`reg = <0x18>`|Set to your device's I2C address (`i2cdetect -y 1` helps verify it)|

Compile and Install the Overlay

dtc -@ -I dts -O dtb -o mcp9808.dtbo mcp9808-overlay.dts
sudo cp mcp9808.dtbo /boot/firmware/overlays/

Add to /boot/firmware/config.txt:

dtoverlay=mcp9808

Reboot:

sudo reboot
Checkpoint — Overlay Compiled and Installed

After reboot, dmesg | grep mcp9808 may not show anything yet (we haven't loaded the driver). But i2cdetect -y 1 should show 0x18 (not UU yet, since no driver has claimed it).

Test the Driver with the Overlay

Now load your Step 2 driver:

cd ~/mcp9808-driver
sudo insmod mcp9808.ko
dmesg | grep mcp9808

You should see:

mcp9808 1-0018: mcp9808: probe called (addr=0x18)

The kernel matched the DT node's compatible = "mcp9808" to your driver's of_match_table and called probe(). Verify with:

i2cdetect -y 1

Address 0x18 now shows UU — the kernel driver owns this device.

Unload for now:

sudo rmmod mcp9808
Checkpoint — Probe Fires

dmesg shows probe called (addr=0x18). From this point on, every time you modify the driver, the cycle is: makesudo insmod mcp9808.ko → test → sudo rmmod mcp9808.


Step 3: Read Temperature in Probe

Let's verify we can talk to the sensor. Read the temperature register in probe() and print it to the kernel log:

static int mcp9808_probe(struct i2c_client *client)
{
    s32 raw;
    int temp;

    dev_info(&client->dev, "mcp9808: probe called (addr=0x%02x)\n",
         client->addr);

    /* Read 2-byte temperature register (register 0x05) */
    raw = i2c_smbus_read_word_swapped(client, 0x05);
    if (raw < 0) {
        dev_err(&client->dev, "Failed to read temperature: %d\n", raw);
        return raw;
    }

    /* Mask out flag bits (upper 3 bits are alert flags) */
    raw &= 0x1FFF;

    /* Convert to millidegrees Celsius (0.0625°C per LSB) */
    if (raw & 0x1000)
        temp = (raw - 0x2000) * 625 / 10;  /* Negative temperature */
    else
        temp = raw * 625 / 10;

    dev_info(&client->dev, "Temperature: %d.%04d °C\n",
         temp / 10000, abs(temp % 10000));

    return 0;
}
Info

i2c_smbus_read_word_swapped() reads a 16-bit value and byte-swaps it — the MCP9808 sends MSB first (big-endian) but SMBus returns LSB first. This is the kernel equivalent of the byte-swap you did in the Python script.

Checkpoint — Temperature in Kernel Log

Rebuild and load: make && sudo insmod mcp9808.ko. Then dmesg | grep mcp9808 should show the temperature (e.g., Temperature: 26.7500 °C). Unload with sudo rmmod mcp9808 before the next step.


Step 4: Character Device Registration

Now add a /dev/mcp9808 device node so user-space programs can read the temperature. Add the character device setup in probe():

#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/of.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/uaccess.h>

struct mcp9808_data {
    struct i2c_client *client;
    struct cdev cdev;
    dev_t devnum;
    struct class *class;
    struct device *device;
};

static struct mcp9808_data *mcp9808_dev;

/* file_operations will be added in Step 5 */
static const struct file_operations mcp9808_fops = {
    .owner = THIS_MODULE,
};

static int mcp9808_probe(struct i2c_client *client)
{
    int ret;

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

    mcp9808_dev->client = client;

    /* 1. Allocate a device number */
    ret = alloc_chrdev_region(&mcp9808_dev->devnum, 0, 1, "mcp9808");
    if (ret < 0) {
        dev_err(&client->dev, "Failed to allocate chrdev region\n");
        return ret;
    }

    /* 2. Initialise and add the character device */
    cdev_init(&mcp9808_dev->cdev, &mcp9808_fops);
    mcp9808_dev->cdev.owner = THIS_MODULE;

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

    /* 3. Create device class and device — this creates /dev/mcp9808 */
    mcp9808_dev->class = class_create("mcp9808");
    if (IS_ERR(mcp9808_dev->class)) {
        ret = PTR_ERR(mcp9808_dev->class);
        goto err_class;
    }

    mcp9808_dev->device = device_create(mcp9808_dev->class, NULL,
                        mcp9808_dev->devnum, NULL,
                        "mcp9808");
    if (IS_ERR(mcp9808_dev->device)) {
        ret = PTR_ERR(mcp9808_dev->device);
        goto err_device;
    }

    dev_info(&client->dev, "mcp9808 registered as /dev/mcp9808\n");
    return 0;

err_device:
    class_destroy(mcp9808_dev->class);
err_class:
    cdev_del(&mcp9808_dev->cdev);
err_cdev:
    unregister_chrdev_region(mcp9808_dev->devnum, 1);
    return ret;
}

The error-handling goto chain is a standard Linux kernel pattern — if step 3 fails, it undoes steps 2 and 1.

Checkpoint — /dev/mcp9808 Exists

Rebuild and load: make && sudo insmod mcp9808.ko. Then ls -la /dev/mcp9808 should show a character device. Unload with sudo rmmod mcp9808 before the next step.


Step 5: Implement read()

This is the core — when a user runs cat /dev/mcp9808, the kernel calls your read() function:

static ssize_t mcp9808_read(struct file *file, char __user *buf,
                size_t count, loff_t *offset)
{
    s32 raw;
    int temp;
    char temp_str[32];
    int len;

    if (*offset > 0)
        return 0;  /* Already read — return EOF */

    /* Read temperature register */
    raw = i2c_smbus_read_word_swapped(mcp9808_dev->client, 0x05);
    if (raw < 0)
        return raw;

    raw &= 0x1FFF;

    /* Convert to millidegrees */
    if (raw & 0x1000)
        temp = (raw - 0x2000) * 625 / 10;
    else
        temp = raw * 625 / 10;

    /* Format as "XX.XXXX\n" */
    len = snprintf(temp_str, sizeof(temp_str), "%d.%04d\n",
               temp / 10000, abs(temp % 10000));

    if (count < len)
        len = count;

    if (copy_to_user(buf, temp_str, len))
        return -EFAULT;

    *offset += len;
    return len;
}

static const struct file_operations mcp9808_fops = {
    .owner = THIS_MODULE,
    .read  = mcp9808_read,
};

Key points: - copy_to_user() safely copies data from kernel space to user space — you cannot use memcpy() because the user buffer may be paged out - The *offset check prevents re-reading: after one read(), subsequent calls return 0 (EOF) until the file is closed and reopened - snprintf() formats the temperature as a human-readable string

Checkpoint — cat /dev/mcp9808 Works

Rebuild and load: make && sudo insmod mcp9808.ko. Then sudo cat /dev/mcp9808 should print the temperature (e.g., 26.7500). We'll fix the permissions with a udev rule later.


Step 6: Cleanup in remove()

When the module is unloaded, free everything in reverse order of allocation:

static void mcp9808_remove(struct i2c_client *client)
{
    device_destroy(mcp9808_dev->class, mcp9808_dev->devnum);
    class_destroy(mcp9808_dev->class);
    cdev_del(&mcp9808_dev->cdev);
    unregister_chrdev_region(mcp9808_dev->devnum, 1);
    dev_info(&client->dev, "mcp9808: driver removed\n");
}

The order matters: destroy the device node first (so user-space cannot access it), then the class, then the cdev, then the number.

Checkpoint — Clean Unload

Rebuild with the full code, then: make && sudo insmod mcp9808.ko → verify /dev/mcp9808 exists and sudo cat /dev/mcp9808 prints temperature → sudo rmmod mcp9808 → verify /dev/mcp9808 is gone and dmesg | tail shows the removal message.


Complete Driver

Here is the final mcp9808.c with all steps combined:

#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/of.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/uaccess.h>

struct mcp9808_data {
    struct i2c_client *client;
    struct cdev cdev;
    dev_t devnum;
    struct class *class;
    struct device *device;
};

static struct mcp9808_data *mcp9808_dev;

static ssize_t mcp9808_read(struct file *file, char __user *buf,
                size_t count, loff_t *offset)
{
    s32 raw;
    int temp;
    char temp_str[32];
    int len;

    if (*offset > 0)
        return 0;

    raw = i2c_smbus_read_word_swapped(mcp9808_dev->client, 0x05);
    if (raw < 0)
        return raw;

    raw &= 0x1FFF;

    if (raw & 0x1000)
        temp = (raw - 0x2000) * 625 / 10;
    else
        temp = raw * 625 / 10;

    len = snprintf(temp_str, sizeof(temp_str), "%d.%04d\n",
               temp / 10000, abs(temp % 10000));

    if (count < len)
        len = count;

    if (copy_to_user(buf, temp_str, len))
        return -EFAULT;

    *offset += len;
    return len;
}

static const struct file_operations mcp9808_fops = {
    .owner = THIS_MODULE,
    .read  = mcp9808_read,
};

static int mcp9808_probe(struct i2c_client *client)
{
    int ret;

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

    mcp9808_dev->client = client;

    ret = alloc_chrdev_region(&mcp9808_dev->devnum, 0, 1, "mcp9808");
    if (ret < 0)
        return ret;

    cdev_init(&mcp9808_dev->cdev, &mcp9808_fops);
    mcp9808_dev->cdev.owner = THIS_MODULE;

    ret = cdev_add(&mcp9808_dev->cdev, mcp9808_dev->devnum, 1);
    if (ret < 0)
        goto err_cdev;

    mcp9808_dev->class = class_create("mcp9808");
    if (IS_ERR(mcp9808_dev->class)) {
        ret = PTR_ERR(mcp9808_dev->class);
        goto err_class;
    }

    mcp9808_dev->device = device_create(mcp9808_dev->class, NULL,
                        mcp9808_dev->devnum, NULL,
                        "mcp9808");
    if (IS_ERR(mcp9808_dev->device)) {
        ret = PTR_ERR(mcp9808_dev->device);
        goto err_device;
    }

    dev_info(&client->dev, "mcp9808 registered as /dev/mcp9808\n");
    return 0;

err_device:
    class_destroy(mcp9808_dev->class);
err_class:
    cdev_del(&mcp9808_dev->cdev);
err_cdev:
    unregister_chrdev_region(mcp9808_dev->devnum, 1);
    return ret;
}

static void mcp9808_remove(struct i2c_client *client)
{
    device_destroy(mcp9808_dev->class, mcp9808_dev->devnum);
    class_destroy(mcp9808_dev->class);
    cdev_del(&mcp9808_dev->cdev);
    unregister_chrdev_region(mcp9808_dev->devnum, 1);
    dev_info(&client->dev, "mcp9808: driver removed\n");
}

static const struct i2c_device_id mcp9808_id[] = {
    { "mcp9808", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, mcp9808_id);

static const struct of_device_id mcp9808_of_match[] = {
    { .compatible = "mcp9808" },
    { }
};
MODULE_DEVICE_TABLE(of, mcp9808_of_match);

static struct i2c_driver mcp9808_i2c_driver = {
    .driver = {
        .name = "mcp9808",
        .of_match_table = mcp9808_of_match,
    },
    .probe    = mcp9808_probe,
    .remove   = mcp9808_remove,
    .id_table = mcp9808_id,
};

module_i2c_driver(mcp9808_i2c_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("MCP9808 Temperature Sensor Driver");

The sensor of the MCP9808 documentation.


Install the Driver Permanently

Once you have verified the complete driver works (insmodcat /dev/mcp9808rmmod), install it so it loads automatically at boot.

Check Module Info

modinfo mcp9808.ko
linux@eslinux:~/mcp9808-driver $ modinfo mcp9808.ko
filename:       /home/linux/mcp9808-driver/mcp9808.ko
license:        GPL
description:    MCP9808 Temperature Sensor Driver
author:         Your Name
srcversion:     7E4548216D745A7EDB4F521
alias:          i2c:mcp9808
depends:
name:           mcp9808
vermagic:       6.6.51+rpt-rpi-v8 SMP preempt mod_unload modversions aarch64

Install it properly

Copy it into the system modules directory:

sudo cp mcp9808.ko /lib/modules/$(uname -r)/kernel/drivers/misc/

Update module dependencies:

sudo depmod -a


Read temperature

Read the temperature with:

cat /dev/mcp9808

The permission denied:

linux@eslinux:~ $ cat /dev/mcp9808
cat: /dev/mcp9808: Permission denied

The user has not permission to read the file:

linux@eslinux:~ $ ls -la /dev/mcp9808
crw------- 1 root root 237, 0 Apr 25 23:43 /dev/mcp9808

Make Device Accessible to Users (udev rule)

Create a udev rule so any user can read the sensor. The file must end in .rules — udev ignores files without this extension:

# Note: the filename must end in .rules
RULE="/etc/udev/rules.d/99-mcp9808.rules"
echo 'KERNEL=="mcp9808", MODE="0666"' | sudo tee "$RULE"
sudo udevadm control --reload
sudo udevadm trigger

Verify the permissions changed:

ls -la /dev/mcp9808
# Expected: crw-rw-rw- 1 root root 237, 0 ... /dev/mcp9808
Warning

If permissions are still crw-------, check the filename: ls /etc/udev/rules.d/99-mcp* — it must be 99-mcp9808.rules, not 99-mcp9808 (without extension).

Now try again read the temperature with:

cat /dev/mcp9808

linux@eslinux:~ $ cat /dev/mcp9808
26.7500

Now the driver is working properly.


What Just Happened?

You implemented the complete driver development workflow for an I2C sensor, step by step:

  1. Skeleton modulemodule_init/module_exit to verify the build system works
  2. I2C driver registrationstruct i2c_driver with probe()/remove(), matched by compatible string
  3. Temperature readingi2c_smbus_read_word_swapped() to talk to the sensor, same decoding logic as the Python script in Enable I2C
  4. Character devicealloc_chrdev_region, cdev_init, class_create, device_create/dev/mcp9808 appears
  5. User-space interfacefile_operations.read with copy_to_user() lets any program read the temperature
  6. Device Tree overlay — tells the kernel where the hardware is
  7. udev rule — automatic device permissions

This is the same workflow used for any Linux I2C driver, whether it's a temperature sensor, accelerometer, or display controller. The Device Tree describes the hardware, and the driver provides the software interface.


Course Overview | Next: OLED Display →