第3章 I2C地址与读写操作
搞 I2C 调试,第一件事就是看懂地址。很多人觉得地址简单,不就是发个字节嘛。但你想想看,地址少了一位、多了一位、甚至读写位搞反了,整个总线就乱套了。我刚开始用逻辑分析仪抓 I2C 的时候,就因为在地址阶段没搞清读写位,浪费了整整一个下午。
3.1 7 位地址与读写位
标准 I2C 地址长度是 7 位。你说 7 位怎么放?它藏在地址字节的高 7 位,最低位是读写标志:0 表示主机要写,1 表示要读。地址字节的格式很简单:
| Bit 7-1 | Bit 0 (R/W) |
|---|---|
| 7 位从机地址 | 0 = 写, 1 = 读 |
举个例子,从机地址是 0x50(7 位),那写操作的地址字节就是 0xA0(0x50 << 1 | 0),读操作是 0xA1。我自己习惯在代码里这么写:
// 写操作
i2c_start();
i2c_write( (slave_addr << 1) | I2C_WRITE ); // 0xA0
// 读操作
i2c_start();
i2c_write( (slave_addr << 1) | I2C_READ ); // 0xA1
注意:很多新手把从机地址直接当字节发,丢掉了移位。结果从机没响应,还以为是线没接好。
我曾经帮同事排查一个 EEPROM 读数据无效的 bug,抓一看地址字节发的 0xA0,但前面已经是写状态了,后面又跟一个读命令…… 整个时序完全错位。记住:读/写位是主机决定的,不是从机。
3.2 地址字节的完整性检测
发完地址字节,从机会回一个 ACK 或者 NACK。如果地址没匹配,从机不会拉低 SDA,SCL 的第 9 个时钟里 SDA 保持高电平——这就是 NACK。这时候主机一般会发 Stop 或者重发 Start。
如果这时发生了时钟拉伸呢?(没错,时钟拉伸问题在地址阶段也可能出现)从机可能在 ACK 周期前拉低 SCL 来争取准备时间。我抓过一例:一个温湿度传感器每次读地址都要拉伸 120 μs,普通逻辑分析仪默认采样太粗,把 ACK 判断成了 NACK。所以解码的时候,一定要把 SCL 低电平超过 50 μs 的跨度标出来,才能分辨是真正的 NACK 还是被拉伸的 ACK。
💡 我的小经验: 如果数据线上看到地址字节后 SDA 高电平且 SCL 被持续拉低,别急着报地址错误,先量量低电平持续时间。超过 tLOW 最大值(标准模式 100 kHz 下约 4.7 μs)但小于 1 ms,大概率是时钟拉伸。
3.3 10 位地址与保留地址
7 位地址最多只够 127 个从机,不够用怎么办?I2C 搞了个 10 位地址扩展。用前 5 位 11110 作为标志,后面跟地址高 2 位,再加上读写位构成第一个字节;第二个字节是地址低 8 位。听起来复杂,看表就清楚了:
| 第一字节 | 第二字节 | 说明 |
|---|---|---|
| 11110 AA9 A8 R/W | A7 … A0 | AA9A8 是地址高 2 位 |
| 1111 0xxx | — | 保留地址(通用呼叫、START 字节等) |
保留地址往往被忽略,但调试时遇到 0x00(通用呼叫)或 0x01(START 字节)的数据,你可能以为是乱码,其实主机可能在发特殊协议。我遇到过一款 10 位地址的触摸控制器,它发地址字节时第一个字节是 0xF0,第二个字节 0x3C。如果不看完整两个字节,单抓第一个字节会误判成 7 位地址。
3.4 读写操作流程
下面这张图把地址阶段到数据阶段的完整流程画出来了。建议存下来,调时序时对照着看。
3.5 避坑指南
最后说几句我踩过的坑,都是和地址读写有关的:
- 地址移位: 把从机地址左移1位再拼接读写位是标准做法。我以前见过一个代码直接把地址字节设为
0x50(没移位),然后用I2C_write(addr | read_flag),结果读操作变成了0x51,差了一位,根本不对。 - 10位地址的分离判断: 抓第一个字节为
0xF0就立刻认为是从机 NACK,忽略了第二个字节。后来加了状态机,检测到11110头部后才收第二个字节。 - 时钟拉伸在地址阶段的误判: 我有一块 FPGA 转接板模拟 I2C 从机,每次收到地址都会拉低 SCL 约 80 μs。上位机逻辑分析仪配置的采样率太低,把 ACK 判定成了异常。后来我把采样率提到 4 MHz,才看清 SCL 确实被拉低但 SDA 保持了低,确认是拉伸不是 NACK。
🔑 一句话总结: 解码 I2C 地址时,先移位再看 R/W,然后根据第9个时钟的 SDA 电平区分 ACK/NACK/拉伸 — 拉伸时 SCL 会被从机主动拉长,不要急着报 NACK。
好了,地址和读写操作这部分算讲透了。下次你抓总线看到一串 0xA0 0x01 0x02,心里应该能立马拆解成:地址 0x50 + 写方向,然后从机 ACK,接着发数据寄存器地址 0x01,再发数据 0x02。掌握这个基本功,后面谈时钟拉伸的定位才会顺手。