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

你有没有想过:一块陀螺仪芯片或触摸屏,是怎么和主控芯片通信的? 它们不像 USB 那样复杂,也没有 PCIe 那么庞大的带宽——它们用的是 I2C。

I2C(Inter-Integrated Circuit)是嵌入式系统中最常见的低速总线,只需要两根线(SCL + SDA)就能挂多个设备。本文从协议原理讲到 Linux 实战,帮你建立完整的 I2C 调试能力。


一、I2C 协议:两根线怎么传数据

I2C 是同步串行协议,只有两根线:

  • SCL(Serial Clock):时钟线,主设备提供
  • SDA(Serial Data):数据线,双向半双工

Bash
         Master                           Slave 1          Slave 2
           │                                │                 │
  ─────────┼────────────────────────────────┼─────────────────┼───── SCL
           │                                │                 │
  ─────────┼────────────────────────────────┼─────────────────┼───── SDA
           │                                │                 │
      开始条件                              │                 │
      写地址 0x68 (7位) + W(0)              │                 │
      ←─── ACK ────                         │                 │
      写寄存器地址 0x43                      │                 │
      ←─── ACK ────                         │                 │
      写数据 0x00                            │                 │
      ←─── ACK ────                         │                 │
      停止条件                              │                 │

时序细节:

  1. Start(S):SCL 高时 SDA 拉低
  2. Address + R/W:7 位地址 + 1 位读写位(共 8 位)
  3. ACK:从设备在第 9 个时钟拉低 SDA 表示应答
  4. Data:每字节后跟 ACK
  5. Stop(P):SCL 高时 SDA 拉高

I2C 地址是 7 位的,但传输时左移 1 位 + R/W 位,所以实际传输 8 位:

Bash
# MPU6050 陀螺仪 I2C 地址 0x68
# 写操作:地址左移 + 0 = 0xD0
# 读操作:地址左移 + 1 = 0xD1

二、主从架构和地址冲突

I2C 是单主多从总线,一个主设备可以驱动多个从设备,每个从设备有唯一的 7 位地址。

常见地址规则:

  • 从设备地址由厂商固定,或通过引脚接地/接高电平配置
  • 有些芯片有两组地址(AD0 引脚不同)
  • I2C 地址不允许重复(冲突)

Bash
# Linux 上用 i2cdetect 扫描总线上的所有设备
$ i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:                         -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- 50 -- -- -- -- -- -- -- -- --  ← EEPROM
60: -- -- -- -- -- -- -- -- -- 68 -- -- -- -- -- -- --  ← MPU6050
70: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 77  ← BMP280

三、SMBus vs I2C:它们的区别

SMBus(System Management Bus)是 I2C 的一个子集,广泛用于笔记本电脑的硬件监控:

特性 I2C SMBus
地址格式 7 位 + 10 位扩展 7 位
最大频率 100kHz/400kHz/3.4MHz 100kHz
传输超时 无定义 有(35ms)
典型应用 传感器、EEPROM 电池、EC、温感

Linux 的 i2c-dev 同时支持 I2C 和 SMBus 协议。


四、Linux i2c-dev 驱动架构

Linux 为用户空间提供了 /dev/i2c-N 设备文件,通过 ioctl 可以直接操作 I2C 总线:

Bash
  用户程序(i2c-tools / 自定义程序)
        │
        ▼ ioctl(I2C_SLAVE/I2C_SMBUS)
  /dev/i2c-1
        │
        ▼
  ┌─────────────────────┐
  │   i2c-dev.ko        │  ← 字符设备层
  └──────────┬──────────┘
             │ i2c_client / i2c_adapter
  ┌─────────┴─────────────┐
  │  i2c-core.ko          │  ← 总线抽象层
  └──────────┬───────────┘
             │ i2c_algorithm
  ┌──────────┴───────────────┐
  │  i2c-pxa / i2c-i801      │  ← 控制器驱动
  └───────────────────────────┘

实战命令:

Bash
# 1. 查看系统有几条 I2C 总线
$ ls /sys/bus/i2c/devices/
i2c-0  i2c-1  i2c-2

# 2. 扫描总线上的设备(I2C-1)
$ i2cdetect -y 1

# 3. 读取设备寄存器(MPU6050 示例,读取 WHO_AM_I = 0x75)
$ i2cget -y 1 0x68 0x75
0x68    # 返回 0x68 = MPU6050 ✓

# 4. 写设备寄存器
$ i2cset -y 1 0x68 0x6B 0x00  # PWR_MGMT_1 = 0(唤醒 MPU6050)

# 5. 读取多字节(读取 MPU6050 加速度计数据)
$ i2cdump -y 1 0x68
# 会 dump 出所有寄存器的值

# 6. 查看总线上已加载的驱动
$ ls /sys/bus/i2c/devices/i2c-1/
device  driver  name  new_device  uevent
$ cat /sys/bus/i2c/devices/i2c-1/name
AST2600 i2c

五、wandos 的 I2C 实现

wandos 目前在 arch/x86/core/i2c.cpp 实现了模拟 I2C bitbang(GPIO 模拟 I2C 时序):

Cpp
// wandos i2c bitbang(简化)
void i2c_start() {
    SDA_HIGH; SCL_HIGH;
    udelay(4);
    SDA_LOW;  // SCL 高时 SDA 拉低 = START
    SCL_LOW;
}

void i2c_stop() {
    SCL_LOW; SDA_LOW;
    SCL_HIGH;
    udelay(4);
    SDA_HIGH; // SCL 高时 SDA 拉高 = STOP
}

uint8_t i2c_write_byte(uint8_t data) {
    for (int i = 7; i >= 0; i--) {
        SCL_LOW;
        if (data & (1 << i)) SDA_HIGH; else SDA_LOW;
        SCL_HIGH;  // SCL 上升沿数据采样
    }
    SCL_LOW;
    SDA_HIGH; // 释放 SDA 准备收 ACK
    SCL_HIGH;
    uint8_t ack = SDA_READ; // 读 ACK
    SCL_LOW;
    return ack;
}

wandos I2C 的局限:

  • 纯软件模拟,性能差
  • 只支持主设备,不支持多主
  • 尚未对接真实的 I2C 控制器硬件

公众号 footer

相关阅读:

  • 《从零写OS内核 | SPI 总线——高速串行通信》(349)
  • 《从零写OS内核 | 总线与地址空间——CPU 是怎么找到每个设备的》(343)

动手环节:
在你的嵌入式设备上运行 i2cdetect -y N(N=总线编号),看看能扫到哪些 I2C 设备。如果有树莓派,可以接一个 MPU6050 陀螺仪模块验证。

仓库地址:
github.com/golang12306/os-kernel-from-scratch


关注公众号「AI不着急」,回复”资料”获取内核学习路线图。

最后修改: 2024年8月29日

作者

评论

发表评论

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