I2C 从设备地址与读写操作:7 位地址与 10 位地址、读写位、从设备识别流程

地址格式:7 位 vs 10 位

I2C 总线上每个从设备都有一个唯一的地址。最开始的标准是 7 位地址,后来觉得不够用,又扩展了 10 位地址。说白了,就是给设备发个“门牌号”,让主机知道跟谁说话。

注意:7 位地址空间最多挂 112 个设备(0x00 保留给广播,0x01~0x7F 可用)。10 位地址扩展到 1024 个,但实际用到的很少。

地址字节的发送顺序是这样的:主机先发 7 位地址左移 1 位,最低位放读写位。10 位地址则需要两个字节:第一个字节的高 5 位固定为 11110,接地址的高 2 位,再加读写位;第二个字节是地址的低 8 位。

类型 地址空间 第一个字节格式 第二个字节
7 位地址 0x00 – 0x7F [A6..A0] + R/W 无
10 位地址 0x00 – 0x3FF 11110 + A9..A8 + R/W A7..A0
我的习惯:10 位地址很少用到。我一般在项目中只用 7 位,如果遇到 10 位地址的器件(比如某些音频编码器),我会先把地址映射成两个字节,调试时用逻辑分析仪抓波形确认第一个字节的 11110 标志位。

读写位:R/W

读写位就是地址字节的最低位。0 表示主机要写(发送数据),1 表示主机要读(接收数据)。这个位直接影响后续数据方向。

  • R/W = 0(写):主机发送地址后,再发送数据到从设备。
  • R/W = 1(读):主机发送地址后,从设备开始发送数据,主机接收。
我曾经踩过的坑:有一次我写驱动,把读写位搞反了。地址发 0x38 但忘了加上读位(应该发 0x71),结果从设备一直不应答。排查了半天才发现是地址最低位写错了。后来我习惯在宏定义里直接写完整的 8 位地址(含 R/W),比如 #define ADDR_WRITE 0x38 和 #define ADDR_READ 0x39。

从设备识别流程

主机是怎么知道某个地址的设备是否存在?流程其实很简单:

  1. 主机产生起始条件(SDA 在 SCL 高时由高变低)。
  2. 主机发送 7 位地址(或 10 位地址的第一个字节)+ 读写位。
  3. 主机释放 SDA,并在第 9 个 SCL 时钟周期检测从设备应答信号(ACK:SDA 被从设备拉低;NACK:SDA 保持高)。
  4. 如果收到 ACK,表示设备存在且准备好;如果收到 NACK,表示设备不存在或忙碌。

这是我手画的一个流程图,你看一眼就能记住:

从设备识别流程(7位地址为例) SDA下降沿 → 起始条件 主机发送(地址 << 1)| R/W 第9个SCL:检测SDA ACK? 设备存在,继续传输 设备不存在 / 忙碌 若为10位地址,则发 两字节+两轮应答 从设备必须在该SCL周期 内拉低SDA才能应答。

代码示例:发送地址并检测应答

在实际嵌入式代码中,通常用一个函数完成地址发送和应答检测。我这里用伪代码示意核心逻辑:

bool I2C_SendAddress(uint8_t addr7bit, bool read)
{
    uint8_t byte = (addr7bit << 1) | (read ? 1 : 0);
    I2C_Start();
    I2C_SendByte(byte);
    bool ack = !I2C_ReadSDA();  // 第9个时钟读SDA,低电平表示ACK
    if (!ack) {
        I2C_Stop();
        return false;
    }
    return true;
}
我建议:写底层驱动时,把地址发送和应答检测拆成两个步骤,便于调试。我有时候会加个超时机制,比如循环等待 ACK 超过 10ms 就报错。

避坑指南

  • 地址冲突:两个设备用了同一个地址怎么办?只能改硬件管脚上的地址配置(如果器件支持)。项目选型时要先看地址范围是否有重叠。
  • 10 位地址应答时序:第一个字节的应答表示地址高两位是否被识别,第二个字节的应答表示整个地址是否匹配。理解这一点,读 datasheet 才不会晕。
  • 广播地址(0x00):所有设备都会应答。我一般不用,除非要实现全局复位。
我曾经因为地址问题造成的板级故障:一块板子上有两个温度传感器,地址配置脚都悬空了,结果默认地址都是 0x48。读温度时一塌糊涂。后来我改硬件飞线,一个拉高一个拉低才解决。所以硬件设计时一定要检查地址管脚的上拉下拉电阻。