1. I2C协议基础:从物理层到时序分析
大家好,我是你们的嵌入式Linux驱动讲师。今天咱们来聊聊I2C协议。说实话,I2C这玩意儿在嵌入式开发里太常见了——传感器、EEPROM、ADC、DAC,十有八九都用它。我最早接触I2C是在做一款智能家居网关的时候,当时被一个上拉电阻的问题折腾了两天。嗯,从那以后我就养成了一个习惯:先搞懂物理层,再碰协议层。
1.1 I2C总线物理层
I2C总线就两根线:SCL(时钟线)和SDA(数据线)。但你别小看这两根线,里面的门道不少。
核心特点:
- 半双工通信:同一时刻只能一个方向传数据
- 多主多从:总线上可以挂多个主设备和多个从设备
- 7位/10位地址:7位地址最多挂127个设备(实际要留一些保留地址)
- 开漏输出:SCL和SDA都是开漏结构,必须外接上拉电阻
说到上拉电阻,我踩过坑。有一次我选了个10kΩ的上拉电阻,总线电容稍微大一点,波形就变得圆滚滚的。后来换成4.7kΩ,问题解决了。你想想看,上拉电阻太小会拉低功耗,太大又影响上升沿。一般经验值是4.7kΩ,但具体要看总线长度和挂载设备数量。
上拉电阻选择经验:
- 标准模式(100kHz):4.7kΩ ~ 10kΩ
- 快速模式(400kHz):2.2kΩ ~ 4.7kΩ
- 高速模式(3.4MHz):1kΩ ~ 2.2kΩ
- 总线电容每增加100pF,电阻建议减小30%
还有个细节——SCL和SDA的电压电平必须匹配。如果主控是3.3V,从设备是5V,那就得加电平转换。我见过有人直接硬接,结果烧了从设备的IO口。嗯,这个坑我不希望你踩。
1.2 I2C协议层
协议层说白了就是一套规矩:什么时候开始、什么时候结束、数据怎么传、谁应答谁。咱们一个一个说。
1.2.1 起始条件(START)
起始条件很简单:SCL为高电平时,SDA从高电平跳变到低电平。就这么一下,告诉总线上所有设备:「我要开始通信了,都注意听!」
// 起始条件的时序描述
SCL: ████████▁▁▁▁▁▁▁▁▁▁▁▁▁▁
SDA: ████████▁▁▁▁▁▁▁▁▁▁▁▁▁▁
↑ 这里SDA先拉低,SCL保持高
我个人习惯在写驱动时,用示波器先抓一下START条件。如果波形不对,后面的数据全是废的。
1.2.2 停止条件(STOP)
停止条件正好反过来:SCL为高电平时,SDA从低电平跳变到高电平。告诉所有设备:「我聊完了,你们该干嘛干嘛。」
注意:START和STOP必须由主设备产生。从设备不能主动发起或终止通信。我曾经在调试一个触摸屏驱动时,发现从设备莫名其妙拉低了SDA,结果总线卡死了。后来查手册才知道,那个从设备有个bug,会在异常时拉低总线。解决办法是加个超时复位机制。
1.2.3 数据有效性
数据怎么才算有效?规矩是:SCL为高电平时,SDA上的数据必须稳定。只有在SCL为低电平时,SDA才能变化。
说白了,就是SCL高电平的时候,你去看SDA的电平,那就是当前bit的值。SCL低电平的时候,SDA随便变,那是为下一个bit做准备。
// 数据有效性示例(发送0x55)
SCL: ▁▁▁▁████▁▁▁▁████▁▁▁▁████...
SDA: ████▁▁▁▁████▁▁▁▁████▁▁...
↑ SCL高时读SDA ↑ SCL高时读SDA
读到1 读到0
这里有个容易犯的错:有些新手在SCL上升沿之后立刻去读SDA,但信号可能有毛刺。我建议在SCL高电平的中间位置采样,这样最稳。
1.2.4 应答机制(ACK/NACK)
每次发送完一个字节(8位数据),紧接着会有一个应答位。这个应答位由接收方产生:
- ACK(应答):接收方在第9个SCL周期拉低SDA,表示「收到了,继续发」
- NACK(非应答):接收方在第9个SCL周期保持SDA高电平,表示「别发了,我不想收」或「出错了」
应答机制的关键点:
- 主设备发送完地址后,从设备必须应答(否则说明设备不存在或忙)
- 主设备发送完数据后,从设备必须应答(否则说明写入失败)
- 主设备读取数据时,可以在最后一个字节发送NACK,告诉从设备「够了,别发了」
- 如果从设备不应答,主设备应该发送STOP条件终止通信
我曾经在一个项目中,从设备偶尔会不响应ACK。排查了半天,发现是电源纹波太大,导致从设备内部逻辑复位了。从那以后,我设计电路时都会在I2C设备的电源脚加个100nF的去耦电容。
1.3 I2C时序分析
时序分析,说白了就是看波形对不对。我一般用逻辑分析仪抓波形,然后对照数据手册检查。
下面是一个完整的I2C写操作时序:
START | 设备地址(7位) | R/W=0 | ACK | 寄存器地址(8位) | ACK | 数据(8位) | ACK | STOP
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
SDA拉低 发送地址 写标志 从设备应答 发送寄存器地址 从设备应答 发送数据 从设备应答 SDA释放
读操作稍微复杂一点,需要先写寄存器地址,再重新发送START条件(这叫重复起始条件),然后读数据:
START | 设备地址 | W=0 | ACK | 寄存器地址 | ACK | 重复START | 设备地址 | R=1 | ACK | 读数据 | NACK | STOP
时序参数速查表(标准模式100kHz):
| 参数 | 符号 | 最小值 | 典型值 |
|---|---|---|---|
| SCL时钟频率 | f_SCL | - | 100kHz |
| 保持时间(START条件) | t_HD;STA | 4.0μs | - |
| 建立时间(STOP条件) | t_SU;STO | 4.0μs | - |
| 数据保持时间 | t_HD;DAT | 0μs | - |
| 数据建立时间 | t_SU;DAT | 250ns | - |
| SCL低电平时间 | t_LOW | 4.7μs | - |
| SCL高电平时间 | t_HIGH | 4.0μs | - |
嗯,这些参数看着多,其实你只要记住一个原则:SCL低电平时间要足够长,让SDA有充分的时间变化。如果总线电容大,SDA的上升沿会变慢,这时候就得把SCL频率降低,或者减小上拉电阻。
下面我用SVG画一张I2C协议的整体框架图,帮你把知识点串起来:
这张图把I2C协议分成了三层:物理层是基础,协议层是规矩,时序分析是验证手段。你写驱动的时候,这三层都得心里有数。
最后说个实战经验。我调试I2C驱动时,习惯先写一个简单的「设备扫描」函数——遍历所有可能的设备地址,看哪个设备会应答。这样能快速确认硬件连接和地址配置是否正确。代码大概长这样:
// 伪代码:I2C设备扫描
for (addr = 0x08; addr <= 0x77; addr++) {
i2c_start();
if (i2c_send_byte(addr << 1 | 0) == ACK) {
printf("发现设备: 0x%02X\n", addr);
}
i2c_stop();
delay_ms(1);
}
嗯,这个技巧帮我省了不少排查时间。你也试试看。
可可精品资料库