3. I2C通信协议详解

各位同学好,今天我们来聊聊I2C。说实话,I2C是我在嵌入式开发中用得最多的总线之一。它简单、灵活,两根线就能搞定一堆传感器。但越是简单的东西,坑往往越多。我刚开始做驱动时,就在I2C上栽过跟头——传感器死活不响应,查了两天才发现是上拉电阻选错了。

好,咱们从头捋一遍I2C的核心知识点。

3.1 I2C总线物理层

I2C总线就两根线:SDA(数据线)和SCL(时钟线)。所有设备都挂在这两根线上,主设备发起通信,从设备响应。

这里有个关键点:SDA和SCL都是开漏输出。什么意思?说白了,设备只能把线拉低,不能主动拉高。拉高靠的是外部的上拉电阻。

为什么用开漏?因为多个设备可以共用一根线。任何一个设备拉低,总线就是低电平。这样就避免了多个设备同时输出高低电平导致的短路问题。

每个I2C设备都有一个7位或10位的地址。7位地址最常见,范围是0x00到0x7F。我习惯把地址表打印出来贴在调试台上,省得每次都要翻手册。

3.2 时序详解

I2C的时序其实不复杂,但几个关键动作必须记牢。

3.2.1 起始条件

SCL为高电平时,SDA从高电平切换到低电平。这就是起始信号。

// 起始条件伪代码
void i2c_start(void) {
    SDA_HIGH();
    SCL_HIGH();
    delay();  // 保持一段时间
    SDA_LOW();   // SDA先拉低
    delay();
    SCL_LOW();   // 然后SCL拉低
}

嗯,这里要注意:起始信号只能由主设备发出。从设备不能主动发起通信。

3.2.2 停止条件

SCL为高电平时,SDA从低电平切换到高电平。这就是停止信号。

void i2c_stop(void) {
    SDA_LOW();
    SCL_HIGH();
    delay();
    SDA_HIGH();  // SDA拉高,产生停止条件
    delay();
}

我曾经遇到过一个奇怪的问题:停止信号发完后,总线一直忙。后来发现是停止条件没保持足够时间,从设备没识别到。加了个微秒级的延时就解决了。

3.2.3 应答机制

每传输完一个字节(8位),接收方必须发送一个应答位(ACK)。

  • ACK(应答):接收方在第9个时钟周期拉低SDA,表示「收到,继续」
  • NACK(非应答):接收方在第9个时钟周期保持SDA高电平,表示「别发了,我收够了」或「出错了」

个人经验:读数据时,主设备在最后一个字节要发送NACK,然后跟一个停止信号。这是告诉从设备:「别再发了,我要结束了。」很多新手在这里忘记发NACK,导致从设备一直发数据,总线卡死。

3.3 读写流程

I2C的读写流程其实就两个套路:写和读。咱们一个一个看。

3.3.1 写操作

写操作的流程是这样的:

  1. 主设备发送起始条件
  2. 主设备发送从设备地址 + 写位(0)
  3. 从设备返回ACK
  4. 主设备发送寄存器地址
  5. 从设备返回ACK
  6. 主设备发送数据字节
  7. 从设备返回ACK
  8. 重复步骤6-7,直到数据发完
  9. 主设备发送停止条件
// 写一个字节到指定寄存器
bool i2c_write_byte(uint8_t dev_addr, uint8_t reg_addr, uint8_t data) {
    i2c_start();
    if (!i2c_send_byte(dev_addr << 1 | 0)) {  // 地址 + 写位
        i2c_stop();
        return false;
    }
    if (!i2c_send_byte(reg_addr)) {  // 寄存器地址
        i2c_stop();
        return false;
    }
    if (!i2c_send_byte(data)) {  // 数据
        i2c_stop();
        return false;
    }
    i2c_stop();
    return true;
}

3.3.2 读操作

读操作稍微复杂一点,需要先写寄存器地址,再重新发起起始条件来读数据。

  1. 主设备发送起始条件
  2. 主设备发送从设备地址 + 写位(0)
  3. 从设备返回ACK
  4. 主设备发送寄存器地址
  5. 从设备返回ACK
  6. 主设备发送重复起始条件
  7. 主设备发送从设备地址 + 读位(1)
  8. 从设备返回ACK
  9. 主设备读取数据字节,发送ACK
  10. 重复步骤9,直到读完
  11. 主设备发送NACK + 停止条件

关键点:重复起始条件(Repeated Start)不是停止+起始,而是在不释放总线的情况下直接发新的起始信号。这样做的好处是:不会让其他设备抢占总线。

3.4 速率与上拉电阻选择

这部分是实战中容易出问题的地方。我来说说我的经验。

3.4.1 I2C速率

I2C有几种标准速率:

模式 最高速率 典型应用
标准模式 100 kbit/s 大多数传感器
快速模式 400 kbit/s EEPROM、ADC
快速模式+ 1 Mbit/s 高速传感器
高速模式 3.4 Mbit/s 特殊应用

我个人习惯:能用100k就别用400k。速率越高,对布线和上拉电阻的要求越苛刻。压传感器这种低速设备,100k完全够用。

3.4.2 上拉电阻选择

上拉电阻的取值直接影响信号质量。选小了,功耗大;选大了,上升沿变缓,高速通信会出问题。

计算公式:

R_min = Vcc / I_ol_max
R_max = t_r / (0.8473 × C_bus)

其中:

  • Vcc:总线电压(通常是3.3V或5V)
  • I_ol_max:设备最大灌电流(查手册,一般3-5mA)
  • t_r:允许的上升时间(100k模式:1μs;400k模式:0.3μs)
  • C_bus:总线电容(包括引脚电容和走线电容,一般10-50pF)

实际工程中,我一般这样选:

总线电压 100k模式 400k模式
3.3V 4.7kΩ 2.2kΩ
5V 4.7kΩ 1.5kΩ

避坑指南:我曾经在一个项目里用了10kΩ的上拉电阻,100k速率下没问题。后来换了个传感器,总线电容大了,波形上升沿变得很缓,通信时好时坏。示波器一看,上升沿都快1.5μs了。换成4.7kΩ后问题解决。

所以我的建议是:上拉电阻宁小勿大。只要不超过灌电流限制,小一点更可靠。

3.5 知识体系总览

下面这张图是我自己整理的I2C知识体系,方便大家对照学习:

I2C通信协议 物理层 SDA / SCL 开漏输出 上拉电阻 时序 起始条件 停止条件 应答机制 读写流程 写操作 读操作 重复起始 速率与上拉电阻 标准/快速/高速 电阻计算与选择

这张图把I2C的核心知识点串起来了。物理层是基础,时序是规矩,读写流程是操作步骤,速率和上拉电阻是工程实践。四者缺一不可。

我的建议:学I2C最好的方法就是拿示波器看波形。把起始条件、停止条件、ACK、NACK一个个抓出来看。看懂了波形,I2C就没什么秘密了。


专注资料整理