从零写OS内核|348:I2C 总线——嵌入式系统的血管

摘要:I2C 是嵌入式系统中最常见的低速总线,只要两根线就能挂大量设备——传感器、EEPROM、RTC、触摸屏控制器……本篇文章从 I2C 协议原理、主从架构、到 Linux i2c-tools 实战,带你掌握这条”系统血管”的用法。


1. I2C 协议:为什么两根线能挂那么多设备

1.1 物理层:开漏 + 上拉

I2C 只有两根信号线:

  • SCL(Serial Clock):时钟线
  • SDA(Serial Data):数据线

这两根线都是开漏输出 + 上拉电阻,意味着:

Bash
       VCC ──────[上拉 4.7KΩ] ──── SDA/SCL
                            │
         ┌──────────────────┘
         ↓
    ┌────┴────┐
    │  Driver │ (开漏:只能拉低,或Hi-Z高阻态)
    └─────────┘

开漏的好处是:任何一个设备都可以把线拉低,但没有人能强制拉高。这样避免了多主设备竞争时的高电平冲突。即使主设备数量不确定,也能安全共享总线。

1.2 地址:7 位还是 10 位?

I2C 从设备地址是 7 位(或者 10 位扩展模式)。7 位地址意味着理论上最多挂 128 个设备,但地址 0000 000 保留,所以实际最多 112 个。

Bash
   7位地址格式:
   [A6][A5][A4][A3][A2][A1][A0][R/W]

   示例:MPU6050 地址 = 0x68 (110 1000)
   写操作:0xD0 (0x68 << 1 | 0)
   读操作:0xD1 (0x68 << 1 | 1)

注意地址是左移 1 位后加 R/W 位,所以很多芯片手册会写”0x68″但实际读写地址是 0xD0/0xD1。

1.3 开始/停止条件

Bash
SCL=高 + SDA=下降沿 → START
SCL=高 + SDA=上升沿 → STOP

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

1.4 传输格式:每个字节后跟 ACK

Bash
    Master → Slave          Master ← Slave
    [START][ADDR+W][ACK][DATA][ACK][DATA][ACK]...[STOP]
                        ↑
                  Slave 可以拉低 SDA (ACK)
                  如果 NACK,Master 停止发送

每个字节(8 bits)后,接收方必须 pull SDA low 表示 ACK(除非是最后一个字节或 NACK 情况)。

1.5 I2C 读写时序图

写一个字节:

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

读一个字节:

Bash
Master        SCL   SDA
              ───   ───
START:        ──┐  ──┐
ADDR+R:        │  D7 D6 D5 D4 D3 D2 D1 D0
               │ ────────────────────────────
ACK:          ─┘  (master pull Sda low during ACK)
DATA:          │  D7 D6 D5 D4 D3 D2 D1 D0
               │◄───────────────────────────
ACK:          ─┘  (master pull Sda low)
STOP:          │  (SDA rise while SCL high)
               └─────────────────────────────

2. Linux i2c-tools:快速调试神器

2.1 安装与查看总线

Bash
$ sudo apt install i2c-tools -y

$ ls /dev/i2c-*
/dev/i2c-0  /dev/i2c-1  /dev/i2c-2

$ i2cdetect -l
i2c-1   i2c        808622C1:00                    I2C adapter
i2c-2   i2c        808622C1:01                    I2C adapter
i2c-0   i2c        SMBus PIIX4 adapter at 9080   SMBus adapter

2.2 扫描总线上的设备

Bash
$ sudo i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:                         -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 4b --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- 68 -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --

地址 0x68(MPU6050)和 0x4b(光照传感器)在总线上。

2.3 读写寄存器

MPU6050 是 6 轴加速度计 + 陀螺仪,最常用的 I2C 传感器之一:

Bash
# 读取 MPU6050 WHO_AM_I 寄存器 (0x75)
$ sudo i2cget -y 1 0x68 0x75
0x68
# 返回 0x68,说明芯片正常(这就是它的默认 WHO_AM_I 值)

# 读取加速度原始值(ACCEL_XOUT_H = 0x3B, ACCEL_XOUT_L = 0x3C)
$ sudo i2cget -y 1 0x68 0x3B w
0x1e00  # 大端序,返回 0x001e(X轴加速度高字节)

# 写入:配置采样率分频器 (SMPLRT_DIV = 0x19)
$ sudo i2cset -y 1 0x68 0x19 0x07

# 读取多个寄存器(burst read)
$ sudo i2cdump -y 1 0x68 0x3B i
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00: 1e 00 3d 00 00 00 80 3f 00 00 00 00 00 00 00 00
...

2.4 用 python py-smbus 读写

Python
import smbus2

bus = smbus2.SMBus(1)
addr = 0x68

# 读取 WHO_AM_I
who = bus.read_byte_data(addr, 0x75)
print(f"MPU6050 WHO_AM_I = 0x{who:02x}")

# 读取加速度 X/Y/Z
accel_x = bus.read_word_data(addr, 0x3B)  # ACCEL_XOUT_H
print(f"Accel X = {accel_x}")

# 配置 MPU6050
bus.write_byte_data(addr, 0x19, 0x07)  # SMPLRT_DIV = 7
bus.write_byte_data(addr, 0x1B, 0x00)  # GYRO_CONFIG = 0 (250 deg/s)

bus.close()

3. Linux 内核 I2C 子系统

3.1 架构分层

Bash
用户空间(i2c-tools, 应用程序)
    ↓ I2C ioctl / SMBUS
内核 I2C Core (i2c-core.c)
    ↓
I2C Adapter Driver (比如 i2c-piix4, i2c-i801, i2c-i2c-imx)
    ↓
硬件(南桥 PIIX4 / I2C 控制器 / SoC I2C)

I2C Core 负责注册 I2CAdapter、I2CDriver,维护 device 列表和 driver 列表,实现 device-driver match。

3.2 注册 I2C 设备驱动

C
// 在内核中注册一个 I2C 驱动
static int mysensor_probe(struct i2c_client *client,
                          const struct i2c_device_id *id) {
    dev_info(&client->dev, "MPU6050 probed at %sn", client->adapter->name);
    return 0;
}

static int mysensor_remove(struct i2c_client *client) {
    return 0;
}

static const struct i2c_device_id mpu6050_id[] = {
    { "mpu6050", 0 },
    { },
};
MODULE_DEVICE_TABLE(i2c, mpu5xxx_id);

static struct i2c_driver mpu6050_driver = {
    .driver = {
        .name = "mpu6050",
    },
    .probe = mysensor_probe,
    .remove = mysensor_remove,
    .id_table = mpu6050_id,
};

module_i2c_driver(mpu6050_driver);

3.3 在 DTS 中声明 I2C 设备

C
// 设备树示例
&i2c1 {
    status = "okay";
    mpu6050@68 {
        compatible = "invensense,mpu6050";
        reg = <0x68>;
    };
};

4. I2C 常见问题与调试

4.1 总线拉死(SCL 或 SDA 一直低)

如果某个从设备发送了数据但没发送 ACK,主设备可能卡在等待 ACK 状态。解决方法:

Bash
# 尝试用 i2cdetect 会看到 "UU"(地址被占用)
$ sudo i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:                         -- -- -- -- -- -- -- -- --
...
60: -- -- -- -- -- -- -- 68 -- -- -- -- -- -- -- -- --
                        ↑
                    68 显示为 UU 表示设备正在被某个驱动使用

强制复位 I2C 总线(手动 clock 9 次让从设备释放 SDA):

Bash
# 使用 i2cdetect 的 -f 强制扫描,跳过 ACK 检测
$ sudo i2cdetect -f -y 1

4.2 速率问题

Bash
# 查看 I2C 总线速率
$ cat /sys/bus/i2c/devices/i2c-1/speed
400000   # 400KHz (Fast Mode)

# 或者查看当前配置
$ sudo cat /sys/kernel/debug/i2c/i2c-1/state
i2c-1: polling 100 kHz

可以在加载驱动时设置速率:i2c_bus=1,400000 或在设备树中配置 clock-frequency = <400000>;


5. wandos 对比:简化的 I2C 模拟

wandos 目前的 I2C 实现非常简化:没有完整的中断驱动 I2C 控制器驱动,也没有设备树支持。wandos 的外设模型中,I2C 总线通过 GPIO bit-banging 实现(软件模拟 I2C 时序),主要用来读取 MPU6050 等传感器数据。wandos 未来的方向是实现标准的 Linux I2C 子系统接口,使传感器驱动可以在 wandos 和 Linux 之间共享代码。


6. I2C vs SPI:选哪个?

特性 I2C SPI
线数 2 (SDA+SCL) 4+ (MOSI/MISO/CLK/CS)
速率 100K/400K/3.4M 几十 MHz
地址 7/10 位(总线共享) 每个设备独立 CS
协议 有 ACK/NACK 无协议层(纯数据流)
多主 支持 不支持
适合 传感器、EEPROM、RTC 大容量 Flash、显示屏、高速率

公众号底部

📢 关注公众号【内核栈】,获取更多 OS/内核干货

💡 本文属于「从零写OS内核」系列,欢迎回顾:

  • 篇 347:《显示器——从 Framebuffer 到 GPU》
  • 篇 346:《键盘——按键是怎么变成字符的》
  • 篇 345:《USB 协议》

🔗 回复「I2C」获取 I2C 协议规范和 MPU6050 数据手册


作者:内核栈 | 原创出品
本文永久链接:https://kernelstack.dev/348-i2c

最后修改: 2024年2月13日

作者

评论

发表评论

您的邮箱地址不会被公开。