核心结论
I2C 学习可以归纳为三层:
电气层:SCL、SDA、开漏输出、上拉电阻
协议层:START、设备地址、数据、ACK、STOP
器件层:寄存器地址、寄存器数据、芯片手册定义
最重要的一句话:
I2C 只负责传输字节;设备地址决定访问哪个芯片,寄存器地址决定访问芯片内部哪个位置,芯片手册决定读回来的字节代表什么。
一、I2C 的硬件结构
I2C 使用两根线:
| 信号 | 英文全称 | 作用 |
|---|---|---|
| SCL | Serial Clock,串行时钟 | 主机产生时钟 |
| SDA | Serial Data,串行数据 | 传输地址和数据 |
本次实验连接:
| 项目 | 配置 |
|---|---|
| STM32 SCL | PB6 |
| STM32 SDA | PB7 |
| 电平 | 3.3V |
| OLED | SSD1306,7 位地址 0x3C |
| 传感器 | MPU6050,AD0 接地,7 位地址 0x68 |
| 必要连接 | 电源正极、电源地、SCL、SDA |
STM32、OLED 和 MPU6050 必须共地。否则即使模块电源灯亮,也不代表通信线路真正连通。
二、为什么需要开漏输出和上拉电阻
I2C 的 SCL、SDA 都是开漏结构:
- 设备可以主动拉低总线
- 设备不能主动输出高电平
- 设备释放总线后,由上拉电阻把线拉高
结构可以表示为:
3.3V
│
上拉电阻
│
SCL 或 SDA ───── 多个设备的开漏引脚共同连接
这样允许多个设备共享总线。任何一个设备拉低,总线就为低;所有设备都释放,总线才为高。
当前软件 I2C 将 PB6、PB7 配置为开漏输出,通过 GPIO 控制电平。
三、SCL 和 SDA 的时序规则
最核心的规则:
SDA 只能在 SCL 低电平期间改变,在 SCL 高电平期间必须保持稳定。
主机在 SCL 高电平期间读取 SDA:
SCL __/‾‾\__/‾‾\__/‾‾\__
↑ ↑ ↑
采样 读取 读取 读取
SDA ‾‾‾‾‾‾‾‾‾\________
稳定保持 在 SCL 低电平期间变化
每条 I2C 消息都由 START 开始、STOP 结束:
| 条件 | 动作 |
|---|---|
| START | SCL 为高时,SDA 从高变低 |
| STOP | SCL 为高时,SDA 从低变高 |
普通数据位不能在高电平期间随意变化,否则可能被误认为 START 或 STOP。
四、设备地址与读写方向
I2C 芯片通常使用 7 位地址。实际发送 8 位:前 7 位选择设备,最低位表示方向。
bit7~bit1:7 位设备地址
bit0:0 = 写,1 = 读
MPU6050 的 7 位地址是 0x68:
0x68 = 1101000
写地址:1101000 0 = 0xD0
读地址:1101000 1 = 0xD1
这里需要准确理解:
0xD0不是单独的“写命令”0xD0表示“访问 0x68 这个设备,本次方向为写”0xD1表示“访问 0x68 这个设备,本次方向为读”
OLED 的 7 位地址是 0x3C:
0x3C << 1 = 0x78
所以串口打印的 0x78 与扫描得到的 0x3C 是同一个 OLED 地址,只是一个使用 8 位写法,一个使用 7 位写法。
五、I2C 怎样传输一个字节
每个数据字节包含 8 位,采用高位先发:
bit7 → bit6 → bit5 → bit4 → bit3 → bit2 → bit1 → bit0
发送完 8 位后,还有第 9 个时钟,用于 ACK:
| ACK 状态 | SDA 电平 | 含义 |
|---|---|---|
| ACK | 接收方拉低 | 收到,可以继续 |
| NACK | SDA 保持高 | 不再接收,结束读取 |
读取多个字节时:
- 非最后一个数据字节:主机发送 ACK,表示继续读
- 最后一个数据字节:主机发送 NACK,表示不再继续
因此“读取一个字节”实际上也需要 9 个时钟:8 个数据时钟加 1 个 ACK 时钟。
0x6B = 0110 1011 只是教学示例。I2C 协议只规定如何传输字节,不规定固定字节内容。
六、写寄存器的完整流程
以 MPU6050 将 PWR_MGMT_1 写成 0x01 为例:
START
→ 0xD0 + ACK 设备地址,方向为写
→ 0x6B + ACK 选择 PWR_MGMT_1 寄存器
→ 0x01 + ACK 写入新值
STOP
对应关系:
| 总线内容 | 意义 |
|---|---|
0xD0 | 选择 MPU6050,并声明准备写 |
0x6B | 选择芯片内部寄存器 |
0x01 | 写入该寄存器的新值 |
普通配置寄存器写入新值后,会覆盖原来的值:
原值:PWR_MGMT_1 = 0x40
写入:PWR_MGMT_1 ← 0x01
结果:PWR_MGMT_1 = 0x01
芯片随后根据新值改变工作方式。特殊命令寄存器也可能采用“写 1 触发动作”等规则,必须查看具体芯片手册。
七、读寄存器的完整流程
读取 MPU6050 的 WHO_AM_I:
START
→ 0xD0 + ACK 设备地址,方向为写
→ 0x75 + ACK 选择 WHO_AM_I 寄存器
→ 重复 START
→ 0xD1 + ACK 设备地址,方向改为读
→ 芯片返回一个字节
→ 主机 NACK
STOP
这里最容易混淆的是:“读取为什么一开始用了写地址?”
原因是读取分为两个阶段:
第一阶段:先选中芯片,并设置内部寄存器指针
第二阶段:重新以读方向访问同一芯片,读取指针所指寄存器的内容
第一阶段并不写入新的配置值,只发送寄存器地址。但从总线层面看,寄存器地址本身也是一个数据字节,因此此时 SDA 上当然有数据。
可以对比为:
写配置:设备写地址 + 寄存器地址 + 新值
读数据:设备写地址 + 寄存器地址 + 重复START + 设备读地址 + 返回数据
不能把“读操作”理解成只发送 0xD1。必须先告诉芯片从哪个寄存器开始读取。
八、为什么使用重复 START
读取过程中:
D0 → 寄存器地址 → 重复START → D1
重复 START 的作用是:
- 不发送 STOP
- 不释放总线
- 直接改变设备访问方向
- 保持之前设置的寄存器指针
如果中间先发送 STOP,部分器件可能退出当前访问状态,后续读地址就不一定从刚才指定的寄存器开始。
九、寄存器地址、寄存器和数据的关系
可以把芯片内部想成一排小存储盒:
| 概念 | 例子 | 类比 |
|---|---|---|
| 设备地址 | 0x68 | 哪栋楼 |
| 读写方向 | 0xD0/0xD1 | 来写东西还是取东西 |
| 寄存器地址 | 0x75 | 楼里的房间号 |
| 寄存器 | 芯片内部存储单元 | 房间 |
| 寄存器值 | 0x68 | 房间里当前保存的内容 |
重要区别:
- 寄存器地址:选择操作哪个存储位置
- 寄存器值:该位置里面保存的数据
- 写入普通配置寄存器时,新值会覆盖旧值
十、MPU6050 初始化实验
程序先读取 WHO_AM_I:
寄存器地址:0x75
返回内容:0x68
确认芯片身份后,写入五个配置寄存器:
| 寄存器 | 写入值 | 控制效果 |
|---|---|---|
PWR_MGMT_1 | 0x01 | 退出睡眠,选择时钟源 |
CONFIG | 0x03 | 开启约 44Hz 低通滤波 |
SMPLRT_DIV | 0x04 | 内部 1kHz 分频后约 200Hz |
GYRO_CONFIG | 0x00 | 陀螺仪量程 ±250°/s |
ACCEL_CONFIG | 0x00 | 加速度计量程 ±2g |
这说明“写数据控制芯片”的本质:
向控制寄存器写入不同值,芯片会改变采样率、量程、滤波、电源状态等行为。
十一、MPU6050 连续读取 14 字节
程序设置起始寄存器 0x3B,连续读取 14 字节。MPU6050 读取一个寄存器后,内部地址会自动递增。
数据顺序固定为:
| 字节位置 | 寄存器 | 含义 |
|---|---|---|
| 1~2 | 0x3B/0x3C | X 轴加速度高/低字节 |
| 3~4 | 0x3D/0x3E | Y 轴加速度高/低字节 |
| 5~6 | 0x3F/0x40 | Z 轴加速度高/低字节 |
| 7~8 | 0x41/0x42 | 温度高/低字节 |
| 9~10 | 0x43/0x44 | X 轴角速度高/低字节 |
| 11~12 | 0x45/0x46 | Y 轴角速度高/低字节 |
| 13~14 | 0x47/0x48 | Z 轴角速度高/低字节 |
MPU6050 使用大端格式,高字节在前:
高字节:0x45
低字节:0x16
合并:0x45 << 8 | 0x16 = 0x4516
不能把 0x45 和 0x16 直接相加。
传感器数据是有符号 16 位数。例如:
0xF2B0 → -3408
物理量换算公式:
| 数据类型 | 换算公式 |
|---|---|
| 加速度 | 原始值 ÷ 16384,单位 g |
| 陀螺仪 | 原始值 ÷ 131,单位 °/s |
| 温度 | 原始值 ÷ 340 + 36.53,单位 ℃ |
例如:
0x4516 = 17686
17686 ÷ 16384 ≈ 1.079g
当前实际读取帧示例:
[RAW] 45 16 F2 B0 F3 90 F4 10 FF 00 00 5B 00 7C
拆分后:
AX = 45 16
AY = F2 B0
AZ = F3 90
TEMP = F4 10
GX = FF 00
GY = 00 5B
GZ = 00 7C
十二、SSD1306 OLED 的控制方式
OLED 同样使用“设备地址加后续字节”的结构,但 SSD1306 的第二个字节是控制字节,而不是普通寄存器地址。
| 控制字节 | 后续字节含义 |
|---|---|
0x00 | OLED 命令 |
0x40 | OLED 显存数据 |
发送命令:
START + 0x78 + 0x00 + 命令字节 + STOP
发送像素数据:
START + 0x78 + 0x40 + 显存数据 + STOP
因此,不同器件虽然都使用 I2C,设备地址后面的内容需要按各自手册解释。
SSD1306 中:
- 命令控制屏幕参数和显示模式
- 显存数据决定哪些像素点亮
- 程序先修改本地显存缓冲区,再整屏发送给 OLED
十三、本次实验完成的内容
实验工程:i2c_oled
完成内容:
- 使用 GPIO 实现软件 I2C。
- 扫描总线并识别 OLED 和 MPU6050。
- 读取 MPU6050 的
WHO_AM_I。 - 写入配置寄存器并启动 MPU6050。
- 连续读取 14 字节传感器原始数据。
- 将数据换算成加速度、温度和角速度。
- 通过串口输出原始帧和物理量。
- 在 OLED 中显示 MPU6050 温度。
- 温度页固定显示,每秒更新一次。
- 验证 OLED 与 MPU6050 可以同时挂在一条 I2C 总线上。
上电后的关键串口输出:
ACK: 0x3C
ACK: 0x68
WHO_AM_I=0x68
TEMP 27.7 C
[RAW] 45 16 F2 B0 F3 90 F4 10 FF 00 00 5B 00 7C
该结果说明:
- 地址扫描成功
- 两个设备都能回应 ACK
- MPU6050 身份识别正确
- 寄存器读写成功
- 14 字节数据读取成功
- OLED 与传感器并联通信正常
十四、本次实验遇到的问题
并联 OLED 和 MPU6050 后,曾经只扫描到 OLED,MPU6050 没有 ACK。检查软件时序、降低速度和尝试交换逻辑引脚都没有解决,最终重新连接模块线路后恢复。
这个故障提供了几个重要经验:
- 模块电源灯亮不代表 SCL/SDA 已经真正接通。
- 扫描不到地址时,应优先检查接线、共地、供电和上拉。
0x68没有 ACK,不一定是程序错误。- 总线通信失败应先验证最基础的地址 ACK,再分析寄存器内容。
- 两设备并联后,要保证每个模块的 SCL 都接总线 SCL,SDA 都接总线 SDA。
十五、软件 I2C 与硬件 I2C
| 项目 | 软件 I2C | 硬件 I2C |
|---|---|---|
| 时序产生 | GPIO 手动翻转 | STM32 I2C 外设自动产生 |
| CPU 占用 | 较高 | 较低 |
| 引脚灵活度 | 高 | 受外设和复用引脚限制 |
| 时序理解 | 最直观 | 部分过程被硬件封装 |
| 适合场景 | 学习协议、故障诊断、低速设备 | 产品开发、高速或多设备通信 |
当前实验采用软件 I2C,速度设置为保守低速,优先保证时序容易观察和模块兼容。
软件 I2C 的核心实现包括:
- START:SCL 高时拉低 SDA
- 发送字节:每次先准备 SDA,再产生 SCL 高电平
- 第 9 拍释放 SDA,读取 ACK
- 接收字节:主机释放 SDA,从机驱动 8 位数据
- 最后一字节发送 NACK
- 读寄存器使用重复 START
- STOP:SCL 高时释放 SDA
代码中还包含总线恢复逻辑:如果初始化时发现 SDA 被某个从机持续拉低,则最多发送 9 个 SCL 脉冲,再发送 STOP,让设备释放总线。
十六、I2C、SPI 与场景选择
| 总线 | 线数 | 特点 | 常见设备 |
|---|---|---|---|
| I2C | 2 根公共线 | 节省引脚,多设备共享,需要地址和上拉 | OLED、传感器、EEPROM |
| SPI | 通常 4 根 | 速度高,片选线随设备增加 | Flash、显示屏、无线模块 |
简单选择原则:
- 设备多、速度要求一般、希望节省引脚:优先考虑 I2C
- 数据量大、刷新速度快:优先考虑 SPI
- 协议学习和时序诊断:软件 I2C 很直观
- 正式产品中追求稳定和降低 CPU 占用:通常使用硬件 I2C
十七、学习新 I2C 芯片的检查表
拿到新器件后依次确认:
- 7 位设备地址是多少?
- 地址引脚是否会改变设备地址?
- SCL/SDA 应接在哪里,是否需要上拉?
- 控制寄存器有哪些?
- 每个控制值代表什么功能?
- 数据寄存器从哪个地址开始?
- 一次需要读取几个字节?
- 连续读取时地址是否自动递增?
- 数据是高字节在前还是低字节在前?
- 是否是有符号数,需要什么换算公式?
- 写操作后能否回读同一寄存器确认配置?
十八、最终必须记牢的四个模型
1. 一个设备地址
7 位设备地址 + 读写方向位
2. 一次写操作
设备写地址 → 寄存器地址 → 新值
3. 一次读操作
设备写地址 → 寄存器地址 → 重复START → 设备读地址 → 返回数据
读操作前半段不是写入配置值,而是设置内部寄存器指针。
4. 数据含义
寄存器地址 + 芯片手册 + 配置量程 → 字节含义和物理量
复习时只需回答这五个问题
- SDA 为什么只能在 SCL 低电平期间变化?
- 为什么 I2C 总线需要上拉电阻?
0xD0和0xD1分别表示什么?- 读取寄存器时为什么先发写地址,再发读地址?
- 读回一组字节后,怎样判断每个字节代表什么数据?
如果这五个问题都能用自己的话回答,就说明 I2C 的协议、寄存器模型和实验流程已经形成完整理解。