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 配置¶
- I2C1 → Mode:
I2C - Speed Mode:
Fast Mode(400 kHz)通常足够 - 引脚:PB6 (SCL),PB7 (SDA)
- 外部上拉电阻: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, ®, 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, ®, 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Ω)