2. I2C协议详解:I2C总线物理层、时序、起始/停止条件、数据帧格式
大家好,欢迎来到第二讲。上一章我们聊了EEPROM和Flash的基本概念,这一章咱们得把I2C协议彻底吃透。说实话,I2C协议是嵌入式开发里最常用的串行总线之一,但也是坑最多的地方。我见过不少工程师,代码能跑通,但一换芯片、一换线长就出问题,根本原因就是对物理层和时序理解不够深。
2.1 I2C总线物理层:两根线怎么干活?
I2C总线就两根线:SCL(时钟线)和SDA(数据线)。但你别小看这两根线,它们的设计其实很巧妙。
开漏输出 + 上拉电阻,这是I2C物理层的核心。什么叫开漏?说白了,就是芯片的引脚只能拉低电平,不能主动拉高。要拉高怎么办?靠外部的上拉电阻。所以总线空闲时,SCL和SDA都是高电平。
我在项目中遇到过一个问题:有次用了一个长距离的I2C总线,大概50厘米,结果通信老是出错。查了半天,发现是上拉电阻选得太大了,信号上升沿太慢。嗯,这里要注意,上拉电阻的取值跟总线电容有关。一般4.7kΩ是常用值,但总线长了、设备多了,就得用更小的电阻,比如2.2kΩ甚至1kΩ。
上拉电阻选择经验:
- 标准模式(100kHz):4.7kΩ ~ 10kΩ
- 快速模式(400kHz):2.2kΩ ~ 4.7kΩ
- 高速模式(3.4MHz):1kΩ ~ 2.2kΩ
- 总线电容每增加100pF,电阻建议减小30%
还有一个容易忽略的点:I2C设备地址。每个I2C设备都有一个7位或10位的地址。7位地址最多挂127个设备,但实际中因为地址冲突,我一般建议不超过10个。你想想看,地址线只有两根,设备多了地址冲突的概率就大。
2.2 时序基础:时钟和数据怎么配合?
I2C的时序其实不复杂,但细节决定成败。咱们先看最基本的时序图。
SCL是时钟线,由主设备控制。SDA是数据线,数据在SCL低电平时变化,在SCL高电平时采样。这个规则一定要记住:数据在时钟高电平时必须稳定。
为什么会这样?因为接收方是在SCL的上升沿或高电平期间读取SDA的。如果SDA在此时变化,读到的数据就是错的。我刚开始做驱动时,有一次写了个I2C驱动,数据老是错位,查了两天才发现是SDA在SCL高电平时发生了跳变。后来我加了个延时,确保SDA只在SCL低电平时变化,问题就解决了。
个人习惯:我在写I2C时序代码时,会在SCL拉低后加一个微小的延时(比如1us),确保SDA完全稳定后再改变。这个习惯帮我避免了很多奇怪的问题。
2.3 起始条件和停止条件
I2C通信的开始和结束,靠的是两个特殊的信号组合:
- 起始条件(Start Condition):SCL高电平时,SDA从高电平跳变到低电平。
- 停止条件(Stop Condition):SCL高电平时,SDA从低电平跳变到高电平。
注意,这两个条件都要求SCL在高电平。为什么?因为正常数据传输时,SCL高电平时SDA是不允许变化的。所以起始和停止条件就是利用了这个"不允许"的时刻,制造了一个特殊的信号。
我曾经在一个项目中,用逻辑分析仪抓I2C波形,发现停止条件之后总线没有释放。查了半天,原来是从设备在停止条件后还拉着SDA不放。这种情况通常是从设备有bug,或者主设备没有正确发送停止条件。解决办法是:在发送停止条件后,加一个延时再释放总线,或者用GPIO模拟I2C时强制拉高SDA。
避坑指南:我曾经在STM32上用硬件I2C,发现停止条件有时发不出去。后来查手册才知道,硬件I2C的停止条件需要在特定寄存器位清零后才能发送。如果你用硬件I2C遇到类似问题,建议先检查一下总线是否处于忙状态。
2.4 数据帧格式:一个字节怎么传?
I2C的数据帧格式很规整。每个字节8位,高位在前(MSB first)。传输完8位数据后,接收方要发送一个应答位(ACK)。
具体流程是这样的:
- 主设备发送起始条件
- 主设备发送7位从设备地址 + 1位读写位(0=写,1=读)
- 从设备发送应答位(ACK=0表示应答,NACK=1表示不应答)
- 主设备发送数据字节(或接收数据字节)
- 每发送完一个字节,接收方发送应答位
- 主设备发送停止条件
这里有个细节:应答位是在第9个时钟周期产生的。主设备在第9个时钟周期释放SDA,从设备拉低SDA表示应答。如果从设备不应答(比如地址不对、设备忙),SDA保持高电平,主设备就知道出问题了。
我写个简单的代码示例,用GPIO模拟I2C发送一个字节:
// GPIO模拟I2C发送一个字节
void i2c_send_byte(uint8_t data) {
for (int i = 0; i < 8; i++) {
// SCL拉低,准备改变SDA
SCL_LOW();
delay_us(1);
// 设置SDA,高位在前
if (data & 0x80) {
SDA_HIGH();
} else {
SDA_LOW();
}
data <<= 1; // 左移,准备下一位
// SCL拉高,从设备采样
delay_us(1);
SCL_HIGH();
delay_us(2); // 保持高电平足够时间
}
// 第9个时钟:释放SDA,等待应答
SCL_LOW();
delay_us(1);
SDA_INPUT(); // 释放SDA,改为输入模式
SCL_HIGH();
delay_us(1);
// 读取应答位
if (SDA_READ() == 0) {
// ACK
} else {
// NACK,处理错误
}
SCL_LOW();
delay_us(1);
SDA_OUTPUT(); // 恢复输出模式
}
这段代码我实际用过很多次,在AT24C02、M24C64等EEPROM上都验证过。注意看,我在SCL拉低后都加了延时,这是为了保证时序的稳定性。
2.5 多字节传输和重复起始条件
实际应用中,我们经常要读写多个字节。比如读一个EEPROM,先发送地址,再读取数据。这时候就需要用到重复起始条件(Repeated Start)。
重复起始条件就是在没有停止条件的情况下,再次发送起始条件。这样做的好处是:总线不会被释放,其他主设备不会中途插入。
典型的读操作流程:
- 起始条件
- 发送设备地址 + 写位(0)
- 等待应答
- 发送要读取的内存地址(1字节或2字节)
- 等待应答
- 重复起始条件
- 发送设备地址 + 读位(1)
- 等待应答
- 读取数据字节,每读一个字节发送ACK
- 读取最后一个字节时发送NACK,表示不再读了
- 停止条件
你想想看,如果没有重复起始条件,就得先发停止条件,再发起始条件。这中间总线可能被其他设备抢走,导致数据错乱。所以重复起始条件在多主设备系统中特别重要。
关键点总结:
| 信号 | SCL状态 | SDA变化 | 说明 |
|---|---|---|---|
| 起始条件 | 高电平 | 高→低 | 通信开始 |
| 数据位 | 低电平 | 可变化 | 数据准备 |
| 数据位 | 高电平 | 必须稳定 | 数据采样 |
| 应答位 | 第9个时钟 | 从设备拉低 | ACK=0 |
| 停止条件 | 高电平 | 低→高 | 通信结束 |
2.6 实际调试中的常见问题
最后,我分享几个实际调试中遇到的I2C问题,希望能帮你少走弯路。
问题1:总线卡死,SCL或SDA一直被拉低。
这种情况通常是从设备没有释放总线。解决办法:主设备主动发送9个时钟脉冲,让从设备释放SDA。我写过一个小函数,专门用来恢复I2C总线。
问题2:地址发送后没有应答。
先检查地址对不对。7位地址左移1位,再加上读写位。比如设备地址是0x50,写操作就是0xA0,读操作是0xA1。我见过有人直接发0x50,结果没有应答,因为0x50是7位地址,不是8位地址字节。
问题3:时序太快,从设备跟不上。
EEPROM的写操作通常需要时间(比如5ms),如果连续写太快,从设备会不应答。解决办法:每次写操作后加延时,或者检查应答位,如果NACK就重试。
我的调试工具:逻辑分析仪是调试I2C的利器。我习惯用Saleae Logic,便宜又好用。抓一次波形,所有问题一目了然。如果你没有逻辑分析仪,用示波器也可以,但触发设置要调好。
好了,这一章的内容就到这里。I2C协议看起来简单,但实际用起来细节很多。下一章我们会讲EEPROM的具体驱动实现,到时候会用到今天学的所有知识。记住,理解物理层和时序,是写好驱动的基础。