3. I2C协议基础:7位地址与10位地址格式、读写操作流程
说实话,I2C协议看起来简单,但坑不少。我最早接触I2C是在一个传感器项目上,当时觉得不就是两根线嘛,结果调试了整整两天——地址没对上,从机死活不应答。从那以后,我每次移植I2C驱动,都会先把地址格式和读写时序在纸上画一遍。
这一节,咱们就把I2C的地址体系和读写流程彻底讲透。你搞懂了这些,后面移植代码就是体力活了。
3.1 7位地址格式
I2C总线上每个设备都有一个唯一地址。最常见的格式是7位地址。注意,这里说的7位,是指地址本身占7个bit,但在传输时,这7位会被左移1位,最低位用来表示读写方向。
关键点:7位地址范围是0x00~0x7F,但0x00是通用呼叫地址,0x7F保留,实际可用地址是0x01~0x7E。
我见过不少新手直接拿器件手册上的地址去用,结果发现不对。为什么?因为手册上给的地址往往是8位的——也就是7位地址左移一位后的值。举个例子:
- 某传感器手册写“设备地址 = 0x68”
- 这其实是7位地址0x34左移一位得到的
- 真正的7位地址是0x34
- 写操作时发送0x68,读操作时发送0x69
你想想看,如果搞混了,从机根本不会理你。我习惯的做法是:在代码里直接用7位地址,然后通过宏定义生成读写地址。
// 7位地址定义
#define I2C_ADDR_7BIT 0x34
// 生成读写地址
#define I2C_ADDR_WRITE (I2C_ADDR_7BIT << 1) // 0x68
#define I2C_ADDR_READ ((I2C_ADDR_7BIT << 1) | 1) // 0x69
3.2 10位地址格式
7位地址最多挂127个设备,实际去掉保留地址,也就100个左右。对于复杂系统来说,不够用。所以I2C规范定义了10位地址格式。
10位地址的传输分两个阶段:
- 第一个字节:高5位固定为11110,接着是10位地址的高2位,最后是R/W位
- 第二个字节:10位地址的低8位
说白了,就是第一个字节告诉从机“我是10位地址”,第二个字节才把完整的地址发过去。
| 字节 | Bit7 | Bit6 | Bit5 | Bit4 | Bit3 | Bit2 | Bit1 | Bit0 |
|---|---|---|---|---|---|---|---|---|
| 第1字节 | 1 | 1 | 1 | 1 | 0 | A9 | A8 | R/W |
| 第2字节 | A7 | A6 | A5 | A4 | A3 | A2 | A1 | A0 |
注意:10位地址模式下,从机应答两次——第一次应答第一个字节,第二次应答第二个字节。我曾经遇到一个从机芯片,它的10位地址应答时序有bug,第二个字节的应答位延迟了半个时钟周期。这种问题示波器都难抓,最后是逻辑分析仪才定位到。
3.3 读写操作流程
I2C的读写流程,核心就是一句话:主机控制时钟,从机应答确认。咱们先看最基本的写操作。
3.3.1 写操作流程
- 起始条件:SCL高电平时,SDA从高变低
- 发送从机地址:7位地址 + 写标志(0)
- 等待应答:从机拉低SDA表示应答
- 发送寄存器地址:要写入的目标寄存器
- 等待应答
- 发送数据:一个字节一个字节发
- 等待应答(每发一个字节都要等)
- 停止条件:SCL高电平时,SDA从低变高
// 写操作伪代码
I2C_Start();
I2C_SendByte(0x68); // 7位地址0x34 + 写位0
I2C_WaitAck();
I2C_SendByte(0x10); // 寄存器地址
I2C_WaitAck();
I2C_SendByte(data); // 要写的数据
I2C_WaitAck();
I2C_Stop();
3.3.2 读操作流程
读操作稍微复杂一点,因为要先告诉从机读哪个寄存器,然后再切换为读模式。这就是所谓的“复合操作”。
- 起始条件
- 发送从机地址 + 写标志
- 等待应答
- 发送寄存器地址
- 等待应答
- 重复起始条件(注意,不是停止再开始)
- 发送从机地址 + 读标志
- 等待应答
- 读取数据:主机要发送非应答来结束读取
- 停止条件
我的习惯:读操作的最后,主机要发送非应答(NACK)。这个细节很多人会忘。如果你发了应答,从机会继续发数据,总线就被锁住了。我早期踩过这个坑,读出来的数据全是乱的。
// 读操作伪代码
I2C_Start();
I2C_SendByte(0x68); // 地址 + 写
I2C_WaitAck();
I2C_SendByte(0x10); // 寄存器地址
I2C_WaitAck();
I2C_Start(); // 重复起始
I2C_SendByte(0x69); // 地址 + 读
I2C_WaitAck();
data = I2C_ReadByte();
I2C_SendNack(); // 主机发送非应答
I2C_Stop();
3.4 地址与读写流程的SVG图解
下面这张图把7位地址和10位地址的读写流程放在一起对比,你一看就明白。
3.5 常见问题与避坑
我曾经遇到过一个坑:某个从机芯片的7位地址是0x38,但手册上写的是0x70(左移后的值)。我直接用0x70去发,从机没应答。查了半天才发现,驱动库内部又做了一次左移,实际发出去的是0xE0。所以,一定要搞清楚你的驱动库是接收7位地址还是8位地址。
另外几个常见问题:
- 应答超时:从机可能因为忙而不应答,建议加超时处理,别死等
- 重复起始:有些MCU的I2C外设不支持重复起始,得用GPIO模拟
- 时钟拉伸:从机可能拉低SCL来延长时钟,主机要支持这个特性
- 总线冲突:多主机环境下,地址冲突会导致数据错乱
我的调试习惯:先用逻辑分析仪抓一次完整的读写时序,确认地址、应答、数据都对得上。别一上来就调代码,先看波形。波形对了,代码基本没问题;波形不对,调代码是浪费时间。