I2C总线协议详解:硬件原理与通信时序

四层架构

  • 应用程序:决定要显示什么,比如Hello
  • 库函数:决定字符怎么转, ‘H’ -> 点阵字符
  • 设备驱动:决定芯片怎么写,如操作SSD1306寄存器
  • HAL库:决定信号怎么发,用 HAL_I2C_Master_Transmit

硬件连接

I2C有两条数据线:

  • SCL(Serial Clock):时钟线,进行时钟同步
  • SDA(Serial Data):数据线,发送数据

所以I2C是半双工的,因为只有一条数据线。

I2C是一拖多的,可以一个主设备带多个从设备,比如:

1
2
3
4
5
6
7
8
STM32(主设备)
     ┌─────┴─────┐
    SCL         SDA
     │           │
   ┌─┼───┬───┬───┼───┬───┐
   │ │   │   │   │   │   │
  OLED 陀螺仪 EEPROM  其他...

为什么需要上拉电阻?

SCL、SDA都是开漏输出,只能拉低,不能主动拉高。 默认状态下就是高电平,由上拉电阻拉高。任何设备拉低,整条线就变低,这样就实现了线与逻辑,是实现一拖多的基础。

为什么要用开漏输出?

不用会发生什么

如果不用开漏输出,假设: 主设备想输出高电平,接VCC; 从设备想输出低电平,接GND 就会短路导致烧坏电路。

1
2
3
设备A输出高电平 ────────┐
                        ├──→ 短路!烧电路
设备B输出低电平 ────────┘

普通推挽输出有两个MOS管:

1
2
3
4
5
6
VCC ── [P-MOS] ── 输出 ── [N-MOS] ── GND
         ↑ 高电平导通      ↑ 低电平导通

A输出高 → P-MOS导通 → 输出接VCC
B输出低 → N-MOS导通 → 输出接GND
结果:VCC直接连GND,短路

开漏输出如何解决

去掉上面的P-MOS,只保留下面的N-MOS:

1
2
3
VCC ── [上拉电阻] ── 输出 ── [N-MOS] ── GND
                      只有一个管子

想输出高电平,不导通就行,上拉电阻会拉高。想输出低电平,导通N-MOS,电平会被拉低。 输出高电平,代表设备不参与。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
设备A输出"高"(不驱动)──→ 悬空,不影响
                          ├──→ SDA = 低(B拉低)
设备B输出"低"(拉低)────→ 接地


设备A输出"高"(不驱动)──→ 悬空
                          ├──→ SDA = 高(上拉)
设备B输出"高"(不驱动)──→ 悬空


设备A输出"低"(拉低)────→ 接地
                          ├──→ SDA = 低
设备B输出"低"(拉低)────→ 接地

任何组合都不会短路,因为没设备真的能输出高电平。 I2C总线上有多个设备,开漏输出让多个设备同时控制一个线这件事变得安全。

信号定义

Start

1
2
3
4
SCL: ‾‾‾‾‾‾‾‾‾‾‾‾(保持高)
SDA: ‾‾‾‾\_________(高→低,在SCL高时发生)
       这就是Start

含义: 设备说"我要开始说话了"

因为默认就是高电平,Start是高拉低更合理。

Start是一个瞬间的跳变信号,不是持续状态。

Start之后立即开始发时钟:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
完整时序:
Start信号          第1时钟周期(发地址B7)    第2时钟周期(发地址B6)
         ↓                   ↓                        ↓
SCL: ‾‾‾‾‾‾‾‾‾‾‾‾\__/‾‾\__/__/‾‾\__/__/‾‾\__/...
               ↑     ↑    ↑   ↑    ↑    ↑   ↑
            Start后  低   高   低   高    低   高
            SCL变低  ↑         ↑
                   设置B7    从设备读B7

SDA: ‾‾‾‾‾\_________________/‾‾‾\___/___/‾‾‾\___/...
         ↑   ↑                 ↑       ↑
      Start  Start后           B7=1    B6=0
            保持低             ↑       ↑
                            SCL高时   SCL高时
                            从设备读  从设备读

在SCL变低后,第一次变高就开始发数据,也就是第一个时钟的时候。

Stop

1
2
3
4
SCL: ‾‾‾‾‾‾‾‾‾‾‾‾(保持高)
SDA: _____/‾‾‾‾‾‾(低→高,在SCL高时发生)
       这就是Stop

含义: 设备说"我说完了"

时钟线只有主设备能控制,数据线则是任何设备都能控制。

数据位

SCL低时,SDA可以变化(准备数据) SCL高时,SDA必须稳定(接收方读取)

那怎么表示01呢?SCL高电平的时候,SDA是什么就是什么。

1
2
3
4
5
6
7
发送数据位 1:
SCL: ___/‾‾‾\___
SDA: ___/‾‾‾\___    ← SCL高时,SDA也是高 = 数据1

发送数据位 0:
SCL: ___/‾‾‾\___
SDA: ‾‾‾\___/‾‾‾    ← SCL高时,SDA是低 = 数据0

空闲

空闲的时候,两根线都是高电平,因为是开漏输出,没设备驱动,默认就是高电平。

情况SCL状态说明
空闲高电平上拉电阻拉高,没人驱动
通信中交替高低主设备驱动,产生时钟
时钟拉伸低电平从设备故意拉低,让主设备等待

主设备是有绝对控制权的,Start和Stop只能主设备发起。

传输格式

主设备写

1
2
3
主设备:Start → [设备地址+Write] → [数据1] → [数据2] → ... → Stop
从设备:           ↓              ↓         ↓
                 ACK            ACK       ACK

注:设备地址+W = 7位地址 + 0(写位)

如果从设备不想接收了,比如缓冲区满了、设备忙碌或者发现数据错误,可以发送NACK。

在发送完8位数据后,紧接着的第九时钟就是ACK/NACK的发送时机。第九时钟的高电平期间,接收方拉低SDA=ACK。

那问题来了,接收方不拉低SDA是不是就是NACK?那怎么知道从设备是不处理ACK呢?还是发生了NACK呢? 答案是在硬件上,发送方是无法区分的,但我们可以在逻辑上区分,通过上下文判断:

情况1:设备不存在(地址阶段NACK)

1
2
3
4
Start → 地址0x50+W → NACK → Stop
                    地址阶段就NACK
                    → 没找到设备

主设备判断:

  • 地址发出去,第一个ACK就是NACK
  • 这个地址上没有设备(或设备坏了)
  • 应该报错、重试、或换地址

情况2:设备拒绝继续(数据阶段NACK)

1
2
3
Start → 地址0x50+W → ACK → 数据1 → ACK → 数据2 → NACK → Stop
        ↑             ↑              ↑
      设备存在      正常接收        设备说"够了"

主设备判断:

  • 地址阶段有ACK → 设备存在
  • 数据阶段才出现NACK → 设备故意拒绝
  • 正常结束,不算错误

主设备读

1
2
3
主设备:Start → [设备地址+Read] → 等待 → [读取数据] → NACK → Stop
从设备:           ↓            ↓         ↓
                 ACK         发数据     (收到NACK停止)

如果主设备发ACK,代表还要继续读,如果主设备发NACK,说明可以停止了。 NACK后从设备释放SDA,主设备可以发Stop。

主设备读后,也要发ACK,如果要终止,需要发NACK,再发Stop,不应该在ACK后接Stop。

如果主设备发送完地址后,从设备没准备好,可以把SCL拉低,这时候主设备就会等待,这就叫时钟拉伸。 这是I2C协议允许的,SCL也是开漏设计就是为了这个。

ACK机制

传输8位数据后:

1
2
3
时钟周期:  1  2  3  4  5  6  7  8  9
            ↑                    ↑
          数据位              ACK位
  • 如果是写操作:从设备发ACK,表示收到了
  • 如果是读操作:主设备发ACK说明还要继续发,发NACK说明够了

ACK怎么发

  • 接收方在第9时钟拉低SDA = ACK
  • 接收方不碰SDA(保持高) = NACK

ACK只能在SDA发,因为SCL是时钟线,不应该进行数据发送。

协议和数据含义

协议只规定了:

  1. Start/Stop信号格式
  2. 7bit地址+1bit数据方向
  3. 每字节8bit+1bit ACK

芯片可以自己规定地址后面的Byte有什么含义!

例子:EEPROM(存储芯片)

1
2
3
Start → [设备地址+W] → [存储地址] → [数据] → Stop
                        ↑            ↑
                    要写到哪里    写什么数据

例子:MPU6050(陀螺仪)

1
2
3
Start → [设备地址+W] → [寄存器地址] → [数据] → Stop
                        ↑              ↑
                    配置哪个寄存器   配置值

结论: I2C协议只规定"怎么传",不规定"传什么"。数据含义要看芯片手册。

总结表格

概念要点
开漏输出只能拉低,不能主动拉高,上拉电阻提供默认高
线与逻辑任何设备拉低,整条线就低
StartSCL高时SDA高→低
StopSCL高时SDA低→高
数据位SCL低时变化,SCL高时稳定
ACK第9时钟,接收方拉低表示确认
读NACK主设备发NACK表示"读完了"
协议边界只规定传输格式,数据含义芯片自定义