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 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问题,希望能帮你少走弯路:
- 上拉电阻:I2C总线需要上拉电阻,典型值4.7kΩ。电阻太大,信号上升沿变慢;电阻太小,功耗增加。我一般先焊4.7kΩ,如果总线电容大(长线、多设备),换成2.2kΩ。
- 总线冲突:两个主设备同时驱动总线,会导致数据错误。I2C有仲裁机制,但最好还是避免多主设备同时操作。
- 时钟拉伸:从设备可以拉低SCL让主设备等待。有些主设备不支持时钟拉伸,会超时报错。选型时要注意。
- 地址冲突:两个相同型号的设备挂在同一总线上,如果地址不可配置,就会冲突。我遇到过两次,最后只能加I2C多路复用器解决。
避坑指南:我曾经在一个项目中,I2C总线偶尔死锁。折腾了两天,最后发现是SDA被从设备拉低后,主设备没有发送时钟脉冲来释放总线。解决办法:在初始化时发送9个时钟脉冲,让从设备释放SDA。这个技巧叫“I2C总线恢复”,建议收藏。