1. I2C协议基础:物理层、协议层、起始条件、停止条件、数据有效性、应答机制
各位同学好,我是老周。咱们这门课叫《I2C协议与SPI外设驱动CRC校验调试与故障定位实战》,第一讲,得先把I2C的底子打牢。
I2C协议,说白了就是两根线搞定一堆设备通信。我刚开始做嵌入式那会儿,觉得这东西简单,结果调了三天没调通——后来发现是上拉电阻没焊。嗯,这种坑,咱们今天先讲清楚原理,后面再慢慢聊实战。
1.1 物理层:就两根线,但别小看它
I2C总线只有两条线:
- SCL(串行时钟线)——由主设备驱动,控制通信节奏
- SDA(串行数据线)——双向传输数据
这两根线都是开漏输出结构。什么叫开漏?就是芯片只能把线拉低,不能主动拉高。高电平靠外部上拉电阻实现。
关键参数速查表
| 参数 | 标准模式 | 快速模式 | 高速模式 |
|---|---|---|---|
| 最大时钟频率 | 100 kHz | 400 kHz | 3.4 MHz |
| 上拉电阻典型值 | 4.7 kΩ | 2.2 kΩ | 1.0 kΩ |
| 总线电容上限 | 400 pF | 400 pF | 100 pF |
| 最大从设备数量 | 127(7位地址) | 127 | 127 |
个人经验:我习惯在调试板上预留两个上拉电阻位置,一个4.7kΩ,一个2.2kΩ。总线长了换小的,总线短了换大的。省得改板子。
1.2 协议层:谁说话,谁听话
I2C总线上有两种角色:
- 主设备(Master)——发起通信、产生时钟、决定何时开始何时结束
- 从设备(Slave)——监听总线、响应主设备的请求
每个从设备都有一个唯一的地址。7位地址模式最多挂127个设备,10位地址模式可以更多,但实际项目中7位地址够用了。
我记得有一次,一个同事把两个传感器的地址设成一样的,结果总线上一片混乱。嗯,地址冲突这种低级错误,排查起来真能让人抓狂。
1.3 起始条件与停止条件
这是I2C通信的“开关”。没有起始条件,总线上的设备都在睡觉;没有停止条件,总线不知道这次通信结束了。
起始条件(S):SCL为高电平时,SDA从高电平切换到低电平。
停止条件(P):SCL为高电平时,SDA从低电平切换到高电平。
避坑指南:我曾经遇到过一个问题——MCU复位后,SDA线被拉死了。查了半天,发现是从设备在通信中途掉电,总线停在中间状态,没有收到停止条件。解决办法:主设备在初始化时先发一个伪停止条件,把总线“复位”一下。
// 伪停止条件示例:强制释放总线
void i2c_bus_reset(void) {
// 先让SCL和SDA都变成高
SCL_HIGH();
SDA_HIGH();
delay_us(5);
// 模拟一个停止条件
SDA_LOW();
delay_us(5);
SCL_LOW();
delay_us(5);
SCL_HIGH();
delay_us(5);
SDA_HIGH();
delay_us(5);
}
1.4 数据有效性:时钟低时变,时钟高时读
这个规则很简单,但很多人一开始会搞反:
- SCL为低电平时,SDA上的数据可以变化
- SCL为高电平时,SDA上的数据必须稳定,此时接收方采样数据
你想想看,如果SCL高的时候SDA还在跳变,那接收方采到的数据到底是0还是1?没法确定。所以这个规则是I2C通信的基石。
数据有效性规则图解
1.5 应答机制:每发一个字节,都要听回音
I2C的应答机制是它可靠性的关键。每传输完一个字节(8位),接收方必须回复一个应答位(ACK)或非应答位(NACK)。
应答流程:
- 发送方释放SDA总线(拉高)
- 发送方产生第9个SCL时钟脉冲
- 接收方在SCL高电平期间拉低SDA(ACK)或保持高电平(NACK)
我个人的习惯:写驱动时,每次发送完地址字节后,一定要检查ACK。如果没收到ACK,直接报错返回,不要继续发数据。我曾经见过一个驱动,没检查ACK就往下发,结果从设备根本没在线,总线一直卡死。
ACK vs NACK 的含义:
| 场景 | 应答位 | 含义 |
|---|---|---|
| 主设备发送地址后 | ACK | 从设备存在且准备好通信 |
| 主设备发送地址后 | NACK | 从设备不存在或忙 |
| 主设备发送数据后 | ACK | 从设备已接收,可以继续发 |
| 主设备发送数据后 | NACK | 从设备不想收了(比如缓冲区满) |
| 从设备发送数据后 | ACK | 主设备还想继续读 |
| 从设备发送数据后 | NACK | 主设备说够了,停止读 |
曾经踩过的坑:有一次调试温湿度传感器,读出来的数据全是0xFF。用逻辑分析仪一看,从设备发了NACK,但主设备没处理,继续读下一个字节。结果读到的都是总线上的高电平(0xFF)。从那以后,我写驱动必加ACK超时检测。
1.6 完整通信流程:一个字节的旅程
把上面这些知识点串起来,一次完整的I2C写操作是这样的:
- 主设备发送起始条件
- 主设备发送7位从设备地址 + 写位(0)
- 等待从设备回复ACK
- 主设备发送寄存器地址(1字节)
- 等待从设备回复ACK
- 主设备发送数据字节
- 等待从设备回复ACK
- 主设备发送停止条件
读操作稍微复杂一点,需要先写寄存器地址,再重新发起始条件,然后读数据。这个咱们后面讲实战的时候再细说。
本章知识结构