Writing an OS Kernel from Scratch | 348: I2C Bus — The Blood Vessels of Embedded Systems

Abstract: I2C is the most common low-speed bus in embedded systems. With just two wires, it can connect a large number of devices — sensors, EEPROMs, RTCs, touchscreen controllers… This article covers I2C protocol principles, master-slave architecture, and Linux i2c-tools hands-on exercises, giving you mastery over this “system blood vessel.”


1. I2C Protocol: Why Two Wires Can Connect So Many Devices

1.1 Physical Layer: Open-Drain + Pull-Up

I2C has only two signal lines:

  • SCL (Serial Clock): Clock line
  • SDA (Serial Data): Data line

Both lines use open-drain output + pull-up resistors, meaning:

Bash
       VCC ──────[Pull-up 4.7KΩ] ──── SDA/SCL
                            │
         ┌──────────────────┘
         ↓
    ┌────┴────┐
    │  Driver │ (Open-drain: can only pull low, or Hi-Z)
    └─────────┘

The advantage of open-drain is: any device can pull the line low, but no one can force it high. This avoids high-level conflicts during multi-master contention. Even if the number of masters is uncertain, the bus can be shared safely.

1.2 Address: 7-bit or 10-bit?

I2C slave device addresses are 7 bits (or 10-bit extended mode). 7-bit addresses theoretically allow up to 128 devices, but address 0000 000 is reserved, so the practical maximum is 112.

Bash
   7-bit address format:
   [A6][A5][A4][A3][A2][A1][A0][R/W]

   Example: MPU6050 address = 0x68 (110 1000)
   Write: 0xD0 (0x68 << 1 | 0)
   Read:  0xD1 (0x68 << 1 | 1)

Note that the address is shifted left 1 bit then the R/W bit is added, so many chip datasheets say “0x68” but the actual read/write addresses are 0xD0/0xD1.

1.3 Start/Stop Conditions

Bash
SCL=high + SDA=falling edge → START
SCL=high + SDA=rising edge → STOP

     START
      ↓
  ┌──────┐          ┌──────────┐
  │      │          │          │
SDA ──    ───────   ──────────  ←  STOP
        └─────────────────────┘

1.4 Transfer Format: Each Byte Followed by ACK

Bash
    Master → Slave          Master ← Slave
    [START][ADDR+W][ACK][DATA][ACK][DATA][ACK]...[STOP]
                        ↑
                  Slave pulls SDA low (ACK)
                  If NACK, Master stops transmitting

After each byte (8 bits), the receiver must pull SDA low to indicate ACK (unless it’s the last byte or a NACK situation).

1.5 I2C Read/Write Timing Diagram

Writing a byte:

Bash
Master        SCL   SDA
              ───   ───
START:        ──┐  ──┐
               │ └──┘   (SDA fall while SCL high)
ADDR+W:        │  D7 D6 D5 D4 D3 D2 D1 D0
               │ ────────────────────────────
ACK:          ─┘  (slave pulls SDA low)
DATA:          │  D7 D6 D5 D4 D3 D2 D1 D0
               │ ────────────────────────────
ACK:          ─┘  (slave pulls SDA low)
STOP:          │  (SDA rises while SCL high)

2. Linux I2C Driver Writing

C
// Minimal I2C client driver
#include <linux/i2c.h>
#include <linux/module.h>

static int my_probe(struct i2c_client *client) {
    dev_info(&client->dev, "I2C device detected at addr 0x%02xn",
             client->addr);
    return 0;
}

static void my_remove(struct i2c_client *client) {
    dev_info(&client->dev, "I2C device removedn");
}

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

static struct i2c_driver my_driver = {
    .driver = {
        .name = "my_i2c_driver",
        .owner = THIS_MODULE,
    },
    .probe    = my_probe,
    .remove   = my_remove,
    .id_table = my_id_table,
};

module_i2c_driver(my_driver);
MODULE_LICENSE("GPL");
Last modified: 2024年2月13日

Author

Comments

Write a Reply or Comment

Your email address will not be published.