I2C协议基础:两线制通信的精髓

各位同学,今天我们来聊聊I2C协议。说实话,I2C是我在嵌入式开发中用得最多的总线之一。它只有两根线,却能挂载几十个设备,这个设计思路非常巧妙。

I2C全称是Inter-Integrated Circuit,由飞利浦公司在80年代推出。它的核心思想很简单:用两根线完成所有通信。一根时钟线SCL,一根数据线SDA。你想想看,SPI至少需要3根线(MOSI、MISO、SCK),UART需要2根但只能点对点。I2C用2根线实现了多设备总线,这就是它的价值所在。

核心要点:I2C是同步、半双工、多主从的总线协议。所有设备共享SCL和SDA,通过地址区分目标设备。

两线制信号与时序

SCL是时钟线,由主设备驱动。SDA是数据线,双向传输。我刚开始学I2C时有个误区,以为SDA只在SCL高电平时采样。其实恰恰相反——数据在SCL低电平时变化,高电平时采样。这个细节很重要,很多时序问题都出在这里。

来看一个典型的I2C数据传输时序:

SCL:  ████████    ████████    ████████
      ██    ██    ██    ██    ██    ██
SDA:  ██ ████ ██  ██ ████ ██  ██ ████ ██
       D7  D6  D5  D4  D3  D2  D1  D0

每个SCL脉冲传输1位数据。SCL高电平时,SDA必须保持稳定;SCL低电平时,SDA才能变化。这个规则保证了数据采样的可靠性。

我的经验:调试I2C时序时,用示波器同时抓SCL和SDA。先看SCL高电平时SDA是否稳定抖动,这是最常见的信号完整性问题。

起始条件与停止条件

I2C总线的起始和停止条件很特别。它们不是通过电平状态,而是通过信号跳变来定义的。

条件 信号变化 说明
起始条件 (START) SCL高电平时,SDA从高→低 总线从空闲进入忙碌状态
停止条件 (STOP) SCL高电平时,SDA从低→高 总线从忙碌回到空闲状态
重复起始 (RESTART) 不发送STOP,直接再发START 用于改变传输方向或切换从设备

为什么用跳变而不是电平?我个人的理解是:这样可以在不占用额外引脚的情况下,用两根线编码出三种状态(空闲、起始、停止)。设计很巧妙。

避坑指南:我曾经在一个项目中,主设备发送STOP后立即发送新的START。结果从设备没反应过来,导致通信失败。后来加了至少4.7μs的等待时间才解决。记住:STOP和下一个START之间要有足够的空闲时间。

应答机制(ACK/NACK)

I2C的应答机制是它可靠性的关键。每传输完一个字节(8位数据),接收方必须在第9个SCL周期拉低SDA表示应答(ACK),或者保持高电平表示非应答(NACK)。

应答机制的作用:

  • ACK:数据接收成功,可以继续发送下一字节
  • NACK:接收失败、设备忙、或传输结束
  • 从设备NACK:通常表示地址错误或设备不存在
  • 主设备NACK:表示这是最后一个字节,从设备停止发送

我调试I2C时,第一件事就是看应答位。如果地址阶段收到NACK,90%的情况是地址写错了或者设备没上电。如果数据阶段收到NACK,可能是从设备忙或者不支持当前操作。

关键规则:应答位由接收方控制。主设备发送数据时,从设备应答;从设备发送数据时,主设备应答。主设备通过NACK告诉从设备“够了,停止发送”。

I2C协议框架图

下面这张图展示了I2C一次完整的数据传输流程。从START开始,到STOP结束,中间包含地址、读写位、数据和应答。

I2C 一次完整数据传输流程 START 从设备地址 (7位) R/W 位 ACK/NACK 数据 (8位) ACK/NACK 可重复多个数据字节 STOP 说明 START → 地址 → R/W → ACK → 数据 → ACK → ... → STOP 每个字节后都有应答位,确保传输可靠性

I2C vs SPI:核心差异

很多初学者会问:I2C和SPI到底选哪个?我个人的经验是:看场景。下面这个表格列出了它们的核心差异:

特性 I2C SPI
引脚数 2根(SCL + SDA) 3~4根(SCK + MOSI + MISO + CS)
传输模式 半双工 全双工
速度 标准100kHz,快速400kHz,高速3.4MHz 通常10MHz~50MHz,甚至更高
设备数量 理论上127个(7位地址) 受限于CS引脚数量
应答机制 硬件应答,可靠性高 无硬件应答,需软件校验
适用场景 传感器、EEPROM、RTC等低速设备 ADC、DAC、LCD、SD卡等高速设备

我的建议:如果设备数量多、引脚紧张、速度要求不高,选I2C。如果追求速度、需要全双工、设备数量少,选SPI。我做过一个项目,用了8个I2C传感器,只用了2根线,省下的GPIO都用来做按键了。

实际调试中的常见问题

最后分享几个我在项目中遇到的I2C问题,希望能帮你少走弯路:

  1. 上拉电阻:I2C总线需要上拉电阻,典型值4.7kΩ。电阻太大,信号上升沿变慢;电阻太小,功耗增加。我一般先焊4.7kΩ,如果总线电容大(长线、多设备),换成2.2kΩ。
  2. 总线冲突:两个主设备同时驱动总线,会导致数据错误。I2C有仲裁机制,但最好还是避免多主设备同时操作。
  3. 时钟拉伸:从设备可以拉低SCL让主设备等待。有些主设备不支持时钟拉伸,会超时报错。选型时要注意。
  4. 地址冲突:两个相同型号的设备挂在同一总线上,如果地址不可配置,就会冲突。我遇到过两次,最后只能加I2C多路复用器解决。

避坑指南:我曾经在一个项目中,I2C总线偶尔死锁。折腾了两天,最后发现是SDA被从设备拉低后,主设备没有发送时钟脉冲来释放总线。解决办法:在初始化时发送9个时钟脉冲,让从设备释放SDA。这个技巧叫“I2C总线恢复”,建议收藏。