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协议知识体系 物理层 SCL(时钟线) SDA(数据线) 开漏输出 + 上拉电阻 多主多从架构 协议层 起始条件(START) 停止条件(STOP) 数据有效性 应答机制(ACK/NACK) 时序分析 SCL频率 建立/保持时间 上升/下降时间 总线电容影响 应用场景 传感器读取 | EEPROM读写 | ADC/DAC配置 | 显示屏控制 图1: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);
}

嗯,这个技巧帮我省了不少排查时间。你也试试看。


可可精品资料库