I2C 时序基础:从空闲到字节传输
各位同学,今天我们来聊聊 I2C 的时序基础。说实话,这部分内容看起来简单,但很多新手甚至老手都在这里栽过跟头。我刚开始做驱动时,就因为起始条件没搞对,折腾了整整一个下午。嗯,咱们一步步来。
1. 空闲状态:总线在“休息”时什么样?
I2C 总线空闲时,SCL(时钟线)和 SDA(数据线)都处于高电平。为什么?因为这两根线都是开漏输出,外部接了上拉电阻。没有设备拉低它们,自然就是高电平。
你想想看,如果总线空闲时不是高电平,那起始条件就没法判断了。我见过有人把上拉电阻焊错了,结果总线一直低电平,主设备死活发不出起始信号。
关键点:空闲状态 = SCL 高 + SDA 高。这是所有 I2C 通信的起点和终点。
2. 起始条件:通信的“发令枪”
起始条件(Start Condition)的定义很简单:SCL 为高电平时,SDA 从高电平切换到低电平。
说白了,就是主设备告诉所有从设备:“注意了,我要开始发数据了!”
我个人习惯在代码里这样实现起始条件:
void i2c_start(void) {
SDA_HIGH(); // 确保 SDA 高
SCL_HIGH(); // SCL 高
delay_us(5); // 保持一段时间
SDA_LOW(); // SDA 拉低,产生起始条件
delay_us(5); // 保持
SCL_LOW(); // 准备开始传输数据
}
这里有个坑:起始条件必须在 SCL 高电平时产生。我曾经在调试一个温湿度传感器时,发现设备一直没响应。用逻辑分析仪一看,原来是代码里先拉低了 SCL,再拉低 SDA——这根本不是起始条件,从设备根本认不出来。
注意:起始条件后,总线被认为处于“忙”状态。其他主设备看到这个信号,就知道总线被占用了,不会尝试发起通信。
3. 停止条件:通信的“句号”
停止条件(Stop Condition)正好相反:SCL 为高电平时,SDA 从低电平切换到高电平。
实现代码也很直观:
void i2c_stop(void) {
SDA_LOW(); // 确保 SDA 低
SCL_HIGH(); // SCL 高
delay_us(5); // 保持
SDA_HIGH(); // SDA 拉高,产生停止条件
delay_us(5); // 保持
}
停止条件之后,总线恢复空闲状态。从设备收到停止条件,就知道这次传输结束了。
我记得有一次,一个同事写的驱动在读取数据后忘了发停止条件。结果总线一直处于忙状态,后续所有通信都卡死了。嗯,这种问题用逻辑分析仪一看就明白,但排查起来确实让人头疼。
小技巧:有些从设备支持“重复起始条件”(Repeated Start),即在一次传输中不发送停止条件,直接发新的起始条件。这在读取寄存器数据时非常有用,可以避免总线被其他设备抢占。
4. 数据有效性:什么时候采样数据?
I2C 的数据有效性规则很简单:数据在 SCL 高电平时被采样,在 SCL 低电平时可以变化。
换句话说:
- SCL 为高时,SDA 必须保持稳定
- SCL 为低时,SDA 可以变化
为什么这样设计?因为从设备只在 SCL 的上升沿或高电平期间采样 SDA。如果 SDA 在 SCL 高电平时变化,从设备可能采到错误的数据。
我刚开始做 I2C 驱动时,总想着在 SCL 高电平时改变 SDA 来“加快速度”。结果数据全是乱的。后来才明白,时序不是你想快就能快的,必须遵守协议规范。
核心原则:SCL 高电平期间,SDA 不能变。这是 I2C 通信的“铁律”。
5. 字节格式:8 位数据 + 1 位应答
I2C 的字节格式非常固定:每个字节 8 位,高位在前(MSB first),后面跟一个应答位(ACK/NACK)。
传输一个字节的流程如下:
- 主设备在 SCL 的每个脉冲中发送 1 位数据(从高位到低位)
- 第 8 位发送完后,主设备释放 SDA(拉高)
- 主设备产生第 9 个 SCL 脉冲
- 从设备在第 9 个脉冲期间拉低 SDA(表示 ACK)或保持高(表示 NACK)
用表格总结一下:
| SCL 脉冲序号 | 作用 | SDA 状态 |
|---|---|---|
| 1 | 发送第 7 位(MSB) | 由主设备控制 |
| 2-7 | 发送第 6-1 位 | 由主设备控制 |
| 8 | 发送第 0 位(LSB) | 由主设备控制 |
| 9 | 应答位 | 由从设备控制 |
发送一个字节的代码示例:
uint8_t i2c_send_byte(uint8_t data) {
uint8_t i;
for (i = 0; i < 8; i++) {
// 从高位到低位逐位发送
if (data & 0x80) {
SDA_HIGH();
} else {
SDA_LOW();
}
data <<= 1; // 左移,准备下一位
SCL_HIGH(); // 产生时钟脉冲
delay_us(5);
SCL_LOW();
delay_us(5);
}
// 释放 SDA,等待应答
SDA_HIGH();
SCL_HIGH();
delay_us(5);
uint8_t ack = SDA_READ(); // 读取应答位
SCL_LOW();
return ack; // 0 表示 ACK,1 表示 NACK
}
注意:应答位是必须的。如果从设备没有应答(NACK),主设备应该发送停止条件终止传输。我见过有人忽略 NACK,继续发数据,结果从设备根本不理会,白白浪费带宽。
6. 时序图:一图胜千言
下面这张 SVG 图把 I2C 的完整时序画出来了。从空闲状态开始,到起始条件,再到数据传输,最后停止条件。你仔细看看,每个细节都在里面。
7. 避坑指南:我踩过的那些坑
最后,分享几个我实际项目中遇到的坑,希望对你有帮助:
- 起始条件重复触发:有一次我在循环里连续调用起始条件,没检查总线状态。结果从设备被搞晕了,数据全乱。记住:起始条件只能在空闲状态或停止条件后发送。
- 应答位超时:有些从设备处理速度慢,需要等待一段时间才会拉低 SDA 应答。我习惯在应答位加一个超时机制,比如等待 1ms,如果还没收到 ACK 就报错。
- 上拉电阻值不对:I2C 总线的上拉电阻一般在 4.7kΩ 左右。电阻太大,信号上升沿变慢;电阻太小,功耗增加。我见过有人用 10kΩ 电阻,结果 400kHz 模式下波形都变形了。
调试建议:如果你刚开始学 I2C,强烈建议买个逻辑分析仪。几十块钱的东西,能让你看到 SCL 和 SDA 的实时波形。我每次调试 I2C 设备,第一件事就是挂上逻辑分析仪,看看起始条件、数据、应答、停止条件是不是对的。