2. I2C通信时序详解:起始条件、停止条件、数据有效性、应答机制、时钟同步与仲裁
I2C总线的时序,说白了就是一套“谁在什么时候干什么”的规矩。我刚开始调I2C的时候,总觉得时序图看着头疼,后来发现只要抓住几个关键点,这东西其实挺直观的。咱们一个一个拆开讲。
2.1 起始条件与停止条件
总线上所有的通信,都从起始条件开始,以停止条件结束。这是I2C最基础的握手动作。
- 起始条件(S):SCL为高电平时,SDA从高电平切换到低电平。
- 停止条件(P):SCL为高电平时,SDA从低电平切换到高电平。
关键点:起始和停止条件都由主机产生。从机只能被动响应,不能主动发起。
我在项目中遇到过一个问题:某个传感器偶尔会“死掉”,怎么发起始条件都没反应。后来用示波器一抓,发现是停止条件没发干净,SDA被拉低了没释放。嗯,这里要注意——停止条件之后,SDA和SCL都必须回到高电平,总线才算空闲。
2.2 数据有效性
I2C的数据传输,有一个硬性规定:SCL高电平时,SDA必须稳定。只有在SCL低电平时,SDA才能变化。
为什么会这样?因为接收方是在SCL的上升沿或高电平期间采样SDA。如果SDA在SCL高电平时乱跳,采样结果就是错的。
| SCL状态 | SDA允许变化? | 说明 |
|---|---|---|
| 高电平 | 不允许 | 数据稳定,接收方采样 |
| 低电平 | 允许 | 数据切换,准备下一位 |
你想想看,如果从机在SCL高电平时发现SDA变了,它会认为这是一个起始或停止条件,而不是数据位。这就是为什么I2C的时序要求这么严格。
2.3 应答机制
I2C的应答机制,是保证通信可靠性的核心。每传输一个字节(8位),接收方必须在第9个SCL周期拉低SDA,表示“我收到了”。
- ACK(应答):第9个SCL周期,SDA为低电平。表示接收成功,可以继续发下一字节。
- NACK(非应答):第9个SCL周期,SDA为高电平。表示接收失败或不想接收,主机应发送停止条件。
个人习惯:我写驱动时,会在每次发送完地址字节后检查ACK。如果没收到ACK,直接报错并重试,而不是继续发数据。这样可以避免很多奇怪的问题。
我曾经踩过一个坑:某个从机在写寄存器时,前两个字节都正常ACK,第三个字节突然NACK了。查了半天,发现是那个寄存器是只读的,我写了个不该写的地址。所以,应答机制不仅是“收到了”,还隐含了“我允许你继续”的意思。
2.4 时钟同步与仲裁
这两个概念,是多主机I2C系统的核心。虽然大部分场景下我们只用单主机,但理解它们能帮你更好地理解I2C的底层逻辑。
2.4.1 时钟同步
时钟同步,说白了就是多个主机同时发SCL时,谁的低电平时间长,谁就说了算。I2C总线是“线与”结构——谁拉低,总线就是低电平。
具体过程是这样的:
- 多个主机同时开始发送SCL时钟。
- 第一个主机拉低SCL,其他主机检测到SCL为低,也把自己的SCL拉低。
- 第一个主机释放SCL(拉高),但其他主机可能还在拉低。
- 总线SCL保持低电平,直到所有主机都释放了SCL。
- 所有主机都释放后,SCL才变高,开始下一个周期。
结果就是:SCL的低电平时间,由最慢的主机决定。这保证了所有主机都能跟上节奏。
2.4.2 仲裁
仲裁解决的是“多个主机同时发数据,谁赢”的问题。I2C的仲裁机制很巧妙——谁先发低电平,谁就赢。
过程是这样的:
- 多个主机同时发送起始条件,开始传输。
- 它们都在SDA上发送自己的数据位(包括地址位)。
- 如果一个主机发送高电平,但检测到SDA是低电平(因为另一个主机拉了低),它就输了。
- 输的主机立即释放SDA和SCL,退出竞争。
- 赢的主机继续传输,不受影响。
注意:仲裁只在SDA上进行,SCL的时钟同步保证了所有主机在同一个节奏下比较数据。仲裁过程中不会丢失数据,也不会破坏总线状态。
我记得有一次调试一个多主机系统,两个MCU同时访问同一个传感器。其中一个MCU总是莫名其妙地挂掉。后来发现是仲裁逻辑没处理好——输掉仲裁的MCU没有及时释放总线,导致总线锁死。解决方案很简单:在仲裁失败的中断服务程序里,把I2C外设复位一下就好了。
2.5 完整时序图
下面这张SVG图,把上面讲的几个关键时序节点串在了一起。你可以把它当作一个“速查表”,调试的时候对照着看。
2.6 时序参数速查
标准模式下,I2C的时序参数有明确要求。下面这张表是我常用的,调试时对照着看很方便。
| 参数 | 符号 | 标准模式 | 快速模式 | 说明 |
|---|---|---|---|---|
| SCL时钟频率 | f_SCL | 100 kHz | 400 kHz | 最高频率 |
| 起始条件保持时间 | t_HD;STA | 4.0 μs | 0.6 μs | SCL低→SDA低 |
| SCL低电平时间 | t_LOW | 4.7 μs | 1.3 μs | SCL低电平持续时间 |
| SCL高电平时间 | t_HIGH | 4.0 μs | 0.6 μs | SCL高电平持续时间 |
| 数据建立时间 | t_SU;DAT | 250 ns | 100 ns | SDA稳定→SCL高 |
| 停止条件建立时间 | t_SU;STO | 4.0 μs | 0.6 μs | SCL高→SDA高 |
个人经验:我写驱动时,一般不会卡着最小时序参数去设计。比如标准模式下,我会把SCL周期设到12μs左右(约83kHz),留出余量。这样即使从机响应慢一点,或者总线电容大一点,通信也不会出问题。