跳转至

I²C

I²C(Inter-Integrated Circuit,IIC)是 Philips 1982 年提出的两线同步串行总线。只需 2 根线(SCL + SDA)就能挂载多个设备,是连接传感器、OLED 显示屏、EEPROM 的最常用方式。


一、原理

信号线与硬件要求

信号 含义 方向
SCL 串行时钟(Serial Clock) 主机输出(多主时共享)
SDA 串行数据(Serial Data) 双向(开漏)

开漏 + 上拉电阻

I²C 采用开漏(Open-Drain)结构:所有设备只能将总线拉低,不能主动拉高。总线由上拉电阻(通常 4.7 kΩ)连接到 VCC,空闲时保持高电平。

VCC
 ├── 4.7kΩ ── SCL ──┬── MCU.SCL
 │                   ├── 从机1.SCL
 │                   └── 从机2.SCL
 └── 4.7kΩ ── SDA ──┬── MCU.SDA
                     ├── 从机1.SDA
                     └── 从机2.SDA

通信速度

模式 速率
标准模式(Standard Mode) 100 kbps
快速模式(Fast Mode) 400 kbps
快速+ 模式(Fast Mode Plus) 1 Mbps
高速模式(High Speed) 3.4 Mbps(少用)

地址与寻址

每个 I²C 从设备有一个 7-bit 地址(少数设备用 10-bit)。主机通过发送地址来选中目标设备,无需独立 CS 引脚。

地址冲突

同一总线上的两个设备不能有相同地址。多数传感器有 1~3 位硬件地址引脚(接 VCC 或 GND),可以配置。

常见器件地址:

器件 默认地址 地址引脚配置
MPU6050 0x68 / 0x69 AD0 接 GND / VCC
BMP280 0x76 / 0x77 SDO 接 GND / VCC
SSD1306 OLED 0x3C / 0x3D SA0 接 GND / VCC
ADS1115 ADC 0x48 ~ 0x4B ADDR 接 GND/VCC/SDA/SCL
PCF8574 IO扩展 0x20 ~ 0x27 A0~A2

时序(读操作示例)

START  地址(7bit) R/W=1  ACK  数据字节  ACK  STOP
  │                │      │              │
  S ─[0x68][1]─── A ──[data]─────── A/N ─ P
         ↑              ↑              ↑
      主机发送         从机发送       主机发 NAK 表示结束

二、STM32 使用

CubeMX 配置

  1. I2C1 → Mode: I2C
  2. Speed Mode: Fast Mode(400 kHz)通常足够
  3. 引脚:PB6 (SCL),PB7 (SDA)
  4. 外部上拉电阻:4.7 kΩ 接 3.3V(重要!STM32 内部有弱上拉但通常不够)

HAL 基础操作

/* ---- 扫描 I²C 总线上的设备 ---- */
void i2c_scan(void) {
    printf("扫描 I2C 设备...\r\n");
    for (uint8_t addr = 1; addr < 128; addr++) {
        // HAL_I2C_IsDeviceReady: 尝试与地址通信
        if (HAL_I2C_IsDeviceReady(&hi2c1, addr << 1, 3, 10) == HAL_OK) {
            printf("  找到设备: 0x%02X\r\n", addr);
        }
    }
}

/* ---- 写寄存器 ---- */
HAL_StatusTypeDef i2c_write_reg(uint8_t dev_addr, uint8_t reg, uint8_t val) {
    uint8_t data[2] = {reg, val};
    return HAL_I2C_Master_Transmit(&hi2c1, dev_addr << 1, data, 2, 100);
}

/* ---- 读单个寄存器 ---- */
uint8_t i2c_read_reg(uint8_t dev_addr, uint8_t reg) {
    uint8_t val;
    HAL_I2C_Master_Transmit(&hi2c1, dev_addr << 1, &reg, 1, 100);
    HAL_I2C_Master_Receive(&hi2c1, dev_addr << 1, &val, 1, 100);
    return val;
}

/* ---- 读多个连续寄存器 ---- */
void i2c_read_regs(uint8_t dev_addr, uint8_t reg, uint8_t *buf, uint8_t len) {
    HAL_I2C_Master_Transmit(&hi2c1, dev_addr << 1, &reg, 1, 100);
    HAL_I2C_Master_Receive(&hi2c1, dev_addr << 1, buf, len, 100);
}

/* ---- 使用 Mem 系列函数(更简洁)---- */
// 写一个字节到寄存器
HAL_I2C_Mem_Write(&hi2c1, 0x68 << 1, 0x6B, I2C_MEMADD_SIZE_8BIT, &data, 1, 100);

// 读多个字节
uint8_t buf[14];
HAL_I2C_Mem_Read(&hi2c1, 0x68 << 1, 0x3B, I2C_MEMADD_SIZE_8BIT, buf, 14, 100);

实战:MPU6050 读取加速度和陀螺仪

#define MPU6050_ADDR  0x68
#define MPU6050_WHO_AM_I  0x75
#define MPU6050_PWR_MGMT1 0x6B
#define MPU6050_ACCEL_XOUT 0x3B

typedef struct {
    float ax, ay, az;   // 加速度 (g)
    float gx, gy, gz;   // 角速度 (°/s)
    float temp;
} MPU6050_Data;

void mpu6050_init(void) {
    // 唤醒:清除 SLEEP 位
    uint8_t val = 0x00;
    HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDR << 1, MPU6050_PWR_MGMT1,
                      I2C_MEMADD_SIZE_8BIT, &val, 1, 100);

    // 验证:读 WHO_AM_I(应为 0x68)
    uint8_t id;
    HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDR << 1, MPU6050_WHO_AM_I,
                     I2C_MEMADD_SIZE_8BIT, &id, 1, 100);
    printf("MPU6050 ID: 0x%02X (期望 0x68)\r\n", id);
}

void mpu6050_read(MPU6050_Data *data) {
    uint8_t raw[14];
    HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDR << 1, MPU6050_ACCEL_XOUT,
                     I2C_MEMADD_SIZE_8BIT, raw, 14, 100);

    int16_t ax_raw = (int16_t)(raw[0] << 8 | raw[1]);
    int16_t ay_raw = (int16_t)(raw[2] << 8 | raw[3]);
    int16_t az_raw = (int16_t)(raw[4] << 8 | raw[5]);
    int16_t temp_raw = (int16_t)(raw[6] << 8 | raw[7]);
    int16_t gx_raw = (int16_t)(raw[8] << 8 | raw[9]);
    int16_t gy_raw = (int16_t)(raw[10] << 8 | raw[11]);
    int16_t gz_raw = (int16_t)(raw[12] << 8 | raw[13]);

    // 量程:±2g → LSB/g = 16384
    data->ax = ax_raw / 16384.0f;
    data->ay = ay_raw / 16384.0f;
    data->az = az_raw / 16384.0f;

    // 量程:±250°/s → LSB/(°/s) = 131
    data->gx = gx_raw / 131.0f;
    data->gy = gy_raw / 131.0f;
    data->gz = gz_raw / 131.0f;

    data->temp = (float)temp_raw / 340.0f + 36.53f;
}

实战:SSD1306 OLED 显示

#define OLED_ADDR  0x3C

void oled_send_cmd(uint8_t cmd) {
    uint8_t buf[2] = {0x00, cmd};  // 0x00 = Control byte (Co=0, DC=0)
    HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDR << 1, buf, 2, 100);
}

void oled_init(void) {
    oled_send_cmd(0xAE); // Display OFF
    oled_send_cmd(0xD5); oled_send_cmd(0x80); // Clock divide
    oled_send_cmd(0xA8); oled_send_cmd(0x3F); // Multiplex 64
    oled_send_cmd(0x20); oled_send_cmd(0x00); // Horizontal addressing
    oled_send_cmd(0x8D); oled_send_cmd(0x14); // Charge pump ON
    oled_send_cmd(0xAF); // Display ON
}

三、Linux 使用

使能 I²C 设备

# 树莓派:/boot/config.txt
dtparam=i2c_arm=on

# 查看 I²C 总线
ls /dev/i2c-*

# 安装工具
sudo apt install i2c-tools

# 扫描总线上的设备(非常有用!)
i2cdetect -y 1        # -y 跳过确认,1 = /dev/i2c-1

# 读单个字节:i2cget <bus> <addr> <reg>
i2cget -y 1 0x68 0x75  # 读 MPU6050 WHO_AM_I

# 写单个字节:i2cset <bus> <addr> <reg> <val>
i2cset -y 1 0x68 0x6B 0x00  # 唤醒 MPU6050

Python smbus2

from smbus2 import SMBus
import struct

bus = SMBus(1)   # /dev/i2c-1

# 写寄存器
bus.write_byte_data(0x68, 0x6B, 0x00)   # 设备地址, 寄存器, 值

# 读单字节
val = bus.read_byte_data(0x68, 0x75)     # MPU6050 WHO_AM_I

# 读多字节(连续寄存器)
raw = bus.read_i2c_block_data(0x68, 0x3B, 14)   # 从 0x3B 读 14 字节

# 解析 MPU6050 数据
def parse_mpu6050(raw: list):
    vals = struct.unpack(">7h", bytes(raw))   # 7 × int16 大端
    ax, ay, az = vals[0]/16384, vals[1]/16384, vals[2]/16384
    temp = vals[3]/340 + 36.53
    gx, gy, gz = vals[4]/131, vals[5]/131, vals[6]/131
    return ax, ay, az, gx, gy, gz, temp

ax, ay, az, gx, gy, gz, temp = parse_mpu6050(raw)
print(f"加速度: ({ax:.2f}, {ay:.2f}, {az:.2f}) g")
print(f"陀螺仪: ({gx:.1f}, {gy:.1f}, {gz:.1f}) °/s")

bus.close()

完整 MPU6050 驱动类(Python)

from smbus2 import SMBus
import struct
import time

class MPU6050:
    ADDR = 0x68
    REG_WHO_AM_I  = 0x75
    REG_PWR_MGMT1 = 0x6B
    REG_ACCEL_XOUT = 0x3B
    REG_GYRO_CFG  = 0x1B
    REG_ACCEL_CFG = 0x1C

    ACCEL_SCALE = {0: 16384, 1: 8192, 2: 4096, 3: 2048}  # g
    GYRO_SCALE  = {0: 131, 1: 65.5, 2: 32.8, 3: 16.4}    # °/s

    def __init__(self, bus_num=1, addr=0x68):
        self.bus = SMBus(bus_num)
        self.addr = addr
        self._accel_scale = self.ACCEL_SCALE[0]
        self._gyro_scale  = self.GYRO_SCALE[0]
        self._init()

    def _init(self):
        who = self.bus.read_byte_data(self.addr, self.REG_WHO_AM_I)
        assert who == 0x68, f"WHO_AM_I 错误: {hex(who)}"
        self.bus.write_byte_data(self.addr, self.REG_PWR_MGMT1, 0x00)  # 唤醒
        time.sleep(0.1)

    def set_accel_range(self, range_g: int):
        """range_g: 2, 4, 8, 16"""
        cfg = {2:0, 4:1, 8:2, 16:3}[range_g]
        self.bus.write_byte_data(self.addr, self.REG_ACCEL_CFG, cfg << 3)
        self._accel_scale = self.ACCEL_SCALE[cfg]

    def read(self) -> dict:
        raw = self.bus.read_i2c_block_data(self.addr, self.REG_ACCEL_XOUT, 14)
        ax, ay, az, temp_raw, gx, gy, gz = struct.unpack(">7h", bytes(raw))
        return {
            "ax": ax / self._accel_scale,
            "ay": ay / self._accel_scale,
            "az": az / self._accel_scale,
            "temp": temp_raw / 340.0 + 36.53,
            "gx": gx / self._gyro_scale,
            "gy": gy / self._gyro_scale,
            "gz": gz / self._gyro_scale,
        }

    def close(self):
        self.bus.close()

四、注意事项

硬件问题

  • 必须有上拉电阻:忘加上拉是 I²C 不通的 No.1 原因。3.3V 系统用 4.7 kΩ,1.8V 系统用 2.2 kΩ
  • 总线电容限制:总线等效电容 < 400 pF(走线越长、设备越多,电容越大),超标会影响速率上限
  • 电平匹配:3.3V 和 5V 设备混用时,必须加电平转换(如 PCA9306)
  • 地址冲突:同一总线挂载两个相同型号设备前,确认它们的地址是否可配置

软件问题

  • HAL_I2C_Mem_Write/Read 内部已经处理了 Restart 条件,比手动 Transmit + Receive 更可靠
  • I²C 通信失败后,总线可能处于 锁死状态(SCL 或 SDA 被拉低),需要手动发送 9 个时钟脉冲解锁,或重置从机
  • 避免在 I²C 传输中途开关电源,否则会造成总线锁死

调试技巧

  • 先用 i2cdetect 命令(Linux)或扫描函数(STM32)确认设备地址
  • 先读芯片 ID(如 WHO_AM_I)验证通信成功
  • 用逻辑分析仪抓 SCL/SDA 波形,可以看到 ACK/NAK 细节
  • 如果通信时好时坏,考虑降低速率到 100 kHz,或减小上拉电阻值(2.2 kΩ)