2. I2C通信时序:起始条件、停止条件、数据有效性、字节传输与ACK/NACK机制

I2C的时序,说白了就是一根时钟线(SCL)和一根数据线(SDA)之间跳的「双人舞」。很多初学者觉得I2C协议简单,不就是两根线嘛。但我在项目中踩过的坑告诉我——时序不对,一切白费。

咱们先把这个舞蹈的基本舞步拆开来看。

2.1 起始条件(START Condition)与停止条件(STOP Condition)

I2C总线上,SCL和SDA在空闲时都是高电平(由上拉电阻拉高)。什么时候算一次传输开始?什么时候算结束?协议里定义得很清楚。

条件 SCL状态 SDA变化 说明
起始条件(S) 高电平 从高→低跳变 SCL为高时,SDA由高变低
停止条件(P) 高电平 从低→高跳变 SCL为高时,SDA由低变高
重复起始(Sr) 高电平 从高→低跳变 不发送停止,直接再发一次起始

关键记忆点:起始和停止的唯一区别,就是SDA跳变的方向。起始是「高→低」,停止是「低→高」。而且这两个跳变都必须在SCL为高电平时发生。

我曾经在一个项目里,用GPIO模拟I2C时,起始条件写成了SCL低电平时拉低SDA。结果总线上的从设备根本不响应。查了两天才发现——起始条件必须在SCL高电平时采样SDA的下降沿。这个细节,示波器一看就明白,但代码里很容易忽略。

2.2 数据有效性:SCL低电平时准备,高电平时采样

I2C的数据传输有一个铁律:

  • SCL为低电平时:SDA允许变化(数据准备阶段)
  • SCL为高电平时:SDA必须稳定(数据采样阶段)

为什么这样设计?你想想看,如果SCL高电平时SDA还在跳变,那接收方到底采到的是0还是1?没法确定。所以协议强制要求:SCL高电平期间,SDA不能动。

⚠️ 避坑提醒:我曾经在驱动里为了「提速」,在SCL上升沿之后立刻改变SDA。结果从设备偶尔能读到正确数据,偶尔读错。后来用逻辑分析仪一看,SDA在SCL高电平期间有毛刺。记住:SCL高电平期间,SDA必须纹丝不动。

2.3 字节传输与ACK/NACK机制

I2C每次传输一个字节(8位),数据从最高位(MSB)开始发送。发完8位之后,第9个时钟周期用来做应答。

应答机制是这样的:

  • ACK(应答):接收方在第9个时钟周期将SDA拉低,表示「收到,请继续」
  • NACK(非应答):接收方在第9个时钟周期保持SDA高电平,表示「收完了,别发了」或「我没准备好」
场景 发送方 接收方 第9个SCL的SDA状态
主机发送地址 主机 从机 从机拉低 = ACK,从机不拉 = NACK
主机发送数据 主机 从机 从机拉低 = ACK,从机不拉 = NACK
主机接收数据 从机 主机 主机拉低 = ACK(还要继续读),主机不拉 = NACK(读够了)

💡 个人经验:我习惯在驱动里把NACK当作一种「通信状态码」来处理。比如读从机寄存器时,如果从机返回NACK,大概率是地址不对或者从机没准备好。这时候不要盲目重试,先检查硬件连接和地址配置。

2.4 完整字节传输时序

一个完整的字节传输,包含9个SCL时钟周期:

  1. 第1-8个SCL周期:传输数据位(D7→D0),SCL低电平时SDA变化,SCL高电平时采样
  2. 第9个SCL周期:应答位,发送方释放SDA(拉高),接收方决定拉低(ACK)还是保持高(NACK)

这里有个容易搞混的点:谁释放SDA?

  • 如果是主机发送数据给从机:主机在8位数据发完后释放SDA,从机拉低应答
  • 如果是主机从从机读取数据:从机在8位数据发完后释放SDA,主机拉低应答(表示还要继续读)

核心原则:谁发数据,谁控制SDA;发完8位后,谁发数据谁释放SDA,让对方控制应答。

2.5 时序知识体系总览

下面这张图把I2C时序的核心要素串起来了。我建议你把它存下来,写驱动时对照着看。

I2C通信时序知识体系 起始条件 (S) 停止条件 (P) 数据有效性 字节传输 (8位) ACK / NACK SCL高 → SDA下降沿 SCL高 → SDA上升沿 SCL低:SDA可变化 MSB优先,8位数据 + 1位应答 第9个SCL:SDA低=ACK,高=NACK 核心:SCL高电平采样SDA,SCL低电平准备数据,第9周期做应答 重复起始(Sr):不发送停止,直接再发一次起始条件,用于多字节读写或寄存器寻址 所有时序跳变必须满足建立时间(tSU)和保持时间(tHD),具体值查芯片数据手册

2.6 常见时序问题与调试方法

我在调试I2C驱动时,遇到过几个典型问题,分享给你:

  • 从机无应答:最常见。先查地址对不对(7位还是10位?左对齐还是右对齐?),再查SCL频率是否在从机支持范围内
  • 数据错位:大概率是SCL高电平时SDA发生了跳变。用示波器看SDA在SCL高电平期间有没有毛刺
  • 总线死锁:从机把SDA拉低后不释放。这时候可以发9个时钟脉冲让从机复位,或者直接硬件复位从机

💡 调试小技巧:我习惯在驱动里加一个「时序检查模式」——每次发送起始条件后,读一下SDA和SCL的电平状态,确认总线状态符合预期。虽然会慢一点,但调试阶段能省大量时间。