I²C通信完整流程详解:START、地址、ACK、数据、Repeated START和STOP一次讲透

I²C通信完整流程详解:START、地址、ACK、数据、Repeated START和STOP一次讲透

前言

在 STM32、GD32、ESP32 等单片机开发中,I²C 几乎是最常见的板级通信协议之一。

很多器件都会使用 I²C:

  • 温度传感器
  • EEPROM
  • RTC
  • 电源管理芯片
  • GPIO 扩展芯片
  • ADC/DAC
  • 风扇控制芯片
  • 电流/电压监控芯片
  • OLED
  • 各类传感器

在 STM32 HAL 库中,我们经常直接调用:

c 复制代码
HAL_I2C_Master_Transmit();
HAL_I2C_Master_Receive();

HAL_I2C_Mem_Write();
HAL_I2C_Mem_Read();

于是很容易产生一种错觉:

I²C 通信就是调用一个 HAL 函数。

实际上,一个完整 I²C 事务背后会经历:

text 复制代码
总线空闲
   ↓
START
   ↓
从机地址 + R/W
   ↓
ACK
   ↓
数据 / 寄存器地址
   ↓
ACK
   ↓
数据
   ↓
ACK / NACK
   ↓
STOP

如果是典型的"读取寄存器",中间还会出现:

text 复制代码
Repeated START

理解这些底层过程,对排查下面的问题非常重要:

text 复制代码
I²C设备不响应
HAL_I2C返回HAL_ERROR
地址总是NACK
读出来的数据全是0xFF
切换设备后通信异常
逻辑分析仪有波形但程序报错
HAL_I2C_Mem_Read到底发了什么
7位地址和8位地址到底怎么换算

本文就从最基础的物理层开始,把整个 I²C 通信过程完整讲清楚。


一、I²C是什么

I²C 全称:

text 复制代码
Inter-Integrated Circuit

最初由 Philips 提出。

I²C 是一种:

两线制、同步、串行、支持多设备挂接的通信总线。

最核心的两根线是:

text 复制代码
SCL:Serial Clock
串行时钟线

SDA:Serial Data
串行数据线

基本结构:

text 复制代码
             VCC
              │
          ┌───┴───┐
          R       R
          │       │
SCL ──────┼───────┼──────
          │       │
SDA ──────┼───────┼──────
          │       │

      ┌─────────┐
      │ Master  │
      └─────────┘
          │
   ┌──────┼──────┐
   │      │      │
 Slave1 Slave2 Slave3

一条 I²C 总线上可以连接多个从设备。

主机通过:

text 复制代码
设备地址

选择目标从机。


二、I²C为什么只需要两根线

I²C 使用:

text 复制代码
SCL
SDA

就能够完成通信。

其中:

SCL

主要由主机产生时钟。

每一个时钟周期对应一个数据位的传输节拍。


SDA

负责真正的数据传输。

地址、R/W 位、数据和 ACK/NACK 都通过 SDA 传输。


因此可以简单理解为:

text 复制代码
SCL:告诉大家什么时候看数据
SDA:告诉大家数据是什么

三、I²C为什么必须有上拉电阻

I²C 的一个关键电气特性是:

SDA 和 SCL 通常采用开漏/开集输出结构。

设备通常只能主动:

text 复制代码
拉低总线

而不能主动把总线强推为高电平。

当所有设备释放总线后,由外部上拉电阻将线路拉高。

因此:

text 复制代码
设备想输出0:
主动拉低

设备想输出1:
释放线路
由上拉电阻拉高

可以理解为:

text 复制代码
          VCC
           │
          Rpull-up
           │
SDA ───────┼────────
           │
        开漏输出
           │
          GND

这也是为什么 I²C 原理图中通常都会看到:

text 复制代码
SCL上拉电阻
SDA上拉电阻

四、为什么I²C使用开漏结构

因为一条总线上可能有多个设备。

如果某个设备强制输出高电平,而另一个设备强制输出低电平:

text 复制代码
高电平驱动
    VS
低电平驱动

可能产生很大的短路电流。

开漏结构避免了这个问题。

总线逻辑可以理解为:

text 复制代码
只要有任何设备拉低
总线就是0

只有所有设备都释放
总线才是1

这个特性也是:

text 复制代码
ACK
仲裁
Clock Stretching

等机制能够实现的基础。


五、I²C总线空闲状态是什么

当没有任何设备通信时:

text 复制代码
SCL = 1
SDA = 1

也就是:

text 复制代码
SCL ───────────────── HIGH

SDA ───────────────── HIGH

这是 I²C 总线的空闲状态。

所以遇到 I²C 完全不能通信时,第一件事情就可以测:

text 复制代码
SCL空闲是不是高电平?
SDA空闲是不是高电平?

如果某一根线长期为低:

text 复制代码
SCL = 0
或者
SDA = 0

就说明总线可能已经被某个设备拉死。


六、I²C数据什么时候允许变化

I²C 有一个非常重要的时序规则:

正常数据传输期间,SDA 应该在 SCL 低电平期间变化,在 SCL 高电平期间保持稳定。

也就是:

text 复制代码
SCL低:
允许修改SDA

SCL高:
接收方读取SDA

例如:

text 复制代码
SCL  __/‾‾\__/‾‾\__/‾‾\__

SDA  ___0_____1_____0_____
      ↑       ↑
    低电平   低电平
    期间变化

为什么?

因为当 SCL 高电平时:

text 复制代码
SDA发生特殊跳变

会被解释成:

text 复制代码
START
或者
STOP

所以必须特别区分。


七、START起始条件是什么

I²C 通信正式开始之前,主机必须先产生:

text 复制代码
START

起始条件。

START 的定义是:

当 SCL 保持高电平时,SDA 从高电平变成低电平。

即:

text 复制代码
SCL = HIGH

SDA:
HIGH → LOW

波形:

text 复制代码
SCL  ───────────────
                HIGH

SDA  ────────┐
             └────────
             ↑
           START

所以 START 最核心的记忆方法:

SCL 高,SDA 高→低,就是 START。


八、为什么必须先发送START

START 相当于告诉整个 I²C 总线上的设备:

我要开始一笔新的通信了。

它有几个作用:

  1. 宣布总线事务开始;
  2. 让所有从机开始监听;
  3. 主机开始占用总线;
  4. 为后续设备地址做准备。

完整过程:

text 复制代码
空闲
SCL=1
SDA=1

   ↓

START
SCL=1时
SDA 1→0

   ↓

发送地址

九、START之后第一件事是什么

START 之后,主机并不会立刻发送业务数据。

而是先发送:

text 复制代码
目标从机地址
+
读写方向

典型 7 位 I²C 地址格式:

text 复制代码
A6 A5 A4 A3 A2 A1 A0 R/W

一共:

text 复制代码
8 bit

其中:

text 复制代码
A6~A0:7位从机地址

R/W:读写方向

十、R/W位是什么意思

R/W 位决定:

这一阶段是主机写数据给从机,还是主机从从机读取数据。

通常:

text 复制代码
R/W = 0
表示Write

R/W = 1
表示Read

例如从机 7 位地址:

text 复制代码
0x50

二进制:

text 复制代码
1010000

左移一位:

text 复制代码
10100000

写操作:

text 复制代码
10100000
        ↑
       R/W=0

也就是:

text 复制代码
0xA0

读操作:

text 复制代码
10100001
        ↑
       R/W=1

也就是:

text 复制代码
0xA1

因此:

text 复制代码
7位地址 = 0x50

写地址字节 = 0xA0
读地址字节 = 0xA1

十一、7位地址和8位地址为什么经常搞混

这是 I²C 最常见的坑之一。

假设芯片真实的 7 位地址是:

text 复制代码
0x50

那么:

text 复制代码
Write地址字节 = 0xA0
Read地址字节  = 0xA1

于是不同资料里可能写:

text 复制代码
Slave Address = 0x50

也可能看到:

text 复制代码
Write Address = 0xA0
Read Address  = 0xA1

它们实际上可能是在描述同一个设备。


十二、STM32 HAL里的地址到底传哪个

在很多 STM32 HAL I²C API 中,DevAddress 参数使用的是已经左移后的地址形式。

例如:

c 复制代码
#define DEVICE_ADDR    (0x50U << 1)

调用:

c 复制代码
HAL_I2C_Master_Transmit(
    &hi2c1,
    DEVICE_ADDR,
    data,
    length,
    100
);

即:

text 复制代码
0x50 << 1
=
0xA0

但工程中一定要注意:

不同库、不同API、不同芯片资料对地址表示方式可能不同。

所以遇到地址问题时,要确认三个东西:

text 复制代码
器件手册写的是7位还是8位地址?
你的驱动API需要7位还是左移后的地址?
逻辑分析仪显示的是哪一种?

千万不要只凭数值判断。


十三、地址发送以后谁来判断是否匹配

START 后:

text 复制代码
主机发送
7位地址 + R/W

总线上的所有从机都会监听。

例如:

text 复制代码
Master发送:
0x50

Slave1地址:
0x20

Slave2地址:
0x50

Slave3地址:
0x68

只有:

text 复制代码
Slave2

发现自己的地址匹配。

因此只有它应该响应。

其他设备继续保持释放 SDA。


十四、什么是ACK

ACK 全称:

text 复制代码
Acknowledge

表示:

text 复制代码
应答

I²C 每发送完:

text 复制代码
8 bit

之后,还会有:

text 复制代码
第9个时钟

这个第 9 位就是 ACK/NACK 位。

例如:

text 复制代码
D7 D6 D5 D4 D3 D2 D1 D0 ACK

十五、ACK是怎么产生的

发送方发送完 8 位以后:

text 复制代码
发送方释放 SDA

接收方在第 9 个时钟期间:

text 复制代码
拉低 SDA

表示 ACK。

也就是:

text 复制代码
第9位:

SDA = 0
→ ACK

波形概念:

text 复制代码
SCL   _|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_
       1   2   3   4   5   6   7   8   9

SDA   D7  D6  D5  D4  D3  D2  D1  D0   0
                                      ↑
                                     ACK

记忆:

第9位低电平 = ACK。


十六、ACK到底是谁发的

ACK 不是永远由从机发送。

应该记住:

谁接收前面的8位数据,谁负责产生第9位应答。


场景1:主机写从机

例如:

text 复制代码
Master → Slave

主机发送:

text 复制代码
地址
寄存器地址
数据

那么接收方是从机。

因此:

text 复制代码
Slave产生ACK

流程:

text 复制代码
Master发送8bit
       ↓
Slave第9位拉低SDA
       ↓
ACK

场景2:主机读从机

此时:

text 复制代码
Slave → Master

从机发送数据。

接收方变成:

text 复制代码
Master

所以:

text 复制代码
Master负责发送ACK/NACK

这点非常重要。


十七、什么是NACK

NACK:

text 复制代码
Not Acknowledge

表示:

text 复制代码
没有应答

第 9 个时钟期间:

text 复制代码
SDA保持高电平

就是 NACK。

所以:

text 复制代码
SDA = 0 → ACK

SDA = 1 → NACK

可以记成:

第9位:低是ACK,高是NACK。


十八、为什么会出现NACK

NACK 不一定代表硬件坏了。

常见原因包括:

1. 地址错误

主机发送:

text 复制代码
0x50

但总线上没有这个设备。

于是:

text 复制代码
Address NACK

2. 从机没有上电

从机完全不能响应。


3. 地址模式写错

例如:

text 复制代码
本应传0xA0
却传0x50

或者反过来。


4. 从机正忙

例如 EEPROM 内部正在执行写操作。

短时间内可能不响应。


5. 从机不支持当前操作

例如:

  • 寄存器地址无效;
  • 当前状态不允许访问;
  • 数据长度错误。

6. 主机主动发送NACK

在主机读取从机时:

主机读取最后一个字节以后,通常主动发送 NACK,表示"我不要更多数据了"。

所以:

text 复制代码
读操作最后一个NACK

往往是正常行为,而不是错误。


十九、为什么每8位数据后都有ACK/NACK

因为 I²C 需要让发送方知道:

text 复制代码
这个字节到底有没有被正确接收?

所以通信节奏实际上是:

text 复制代码
8位数据
  ↓
1位应答
  ↓
8位数据
  ↓
1位应答
  ↓
8位数据
  ↓
1位应答

也就是:

text 复制代码
8 + 1
8 + 1
8 + 1
...

如果发送多个字节:

text 复制代码
Byte1 → ACK
Byte2 → ACK
Byte3 → ACK
Byte4 → ACK/NACK

二十、I²C数据按照什么顺序发送

I²C 数据通常:

最高位 MSB 先发送。

例如发送:

text 复制代码
0xA5

二进制:

text 复制代码
1010 0101

发送顺序:

text 复制代码
D7 D6 D5 D4 D3 D2 D1 D0

1  0  1  0  0  1  0  1

然后进入:

text 复制代码
第9位 ACK/NACK

二十一、SDA在什么时候改变

正常数据位期间:

SDA 应该在 SCL 低电平期间改变。

然后在:

text 复制代码
SCL高电平期间

保持稳定。

例如:

text 复制代码
SCL  __/‾‾\__/‾‾\__/‾‾\__

SDA  __0_____1_____0_______

接收方通常在时钟有效阶段读取 SDA。


二十二、一个完整I²C写操作是什么样

假设:

text 复制代码
从机地址:0x50
寄存器地址:0x10
写入数据:0x55

典型写流程:

text 复制代码
START
   ↓
Slave Address + W
   ↓
ACK
   ↓
Register Address
   ↓
ACK
   ↓
Data
   ↓
ACK
   ↓
STOP

具体可以表示:

text 复制代码
START

0x50 + Write

ACK

0x10

ACK

0x55

ACK

STOP

二十三、完整写寄存器时序

逻辑上:

text 复制代码
S
│
├─ Address + W
│
├─ ACK
│
├─ Register Address
│
├─ ACK
│
├─ Data
│
├─ ACK
│
└─ P

其中:

text 复制代码
S = START
P = STOP

二十四、STM32 HAL写寄存器示例

例如设备:

text 复制代码
7位地址:0x50
寄存器:0x10
数据:0x55

可以:

c 复制代码
#define DEVICE_ADDR    (0x50U << 1)

uint8_t value = 0x55;

HAL_StatusTypeDef ret;

ret = HAL_I2C_Mem_Write(
    &hi2c1,
    DEVICE_ADDR,
    0x10,
    I2C_MEMADD_SIZE_8BIT,
    &value,
    1,
    100
);

if (ret != HAL_OK)
{
    /* I2C error */
}

它底层概念上完成:

text 复制代码
START
↓
Address + W
↓
ACK
↓
0x10
↓
ACK
↓
0x55
↓
ACK
↓
STOP

二十五、为什么HAL_I2C_Master_Transmit和Mem_Write不一样

例如:

c 复制代码
HAL_I2C_Master_Transmit();

更像是:

向某个 I²C 从机发送一串普通字节。

而:

c 复制代码
HAL_I2C_Mem_Write();

更加适合:

带内部寄存器地址的 I²C 器件。

例如:

text 复制代码
温度传感器
风扇控制芯片
GPIO扩展芯片
EEPROM
电源管理芯片

很多设备内部都有:

text 复制代码
Register Address

因此 HAL 帮你把:

text 复制代码
设备地址
+
寄存器地址
+
数据

组合起来。


二十六、I²C读数据为什么比写更复杂

写操作很直观:

text 复制代码
Master
   ↓
Slave

主机一路发送。

但是读取寄存器时存在一个问题:

从机怎么知道主机到底想读哪个寄存器?

例如从机内部:

text 复制代码
0x00:状态寄存器
0x01:温度高字节
0x02:温度低字节
0x03:配置寄存器

主机想读:

text 复制代码
0x02

必须先告诉从机:

text 复制代码
我要访问0x02

所以常见寄存器读操作不是简单:

text 复制代码
START
Address + R

而是先"写寄存器地址",再切换为读。


二十七、典型寄存器读流程

假设要读:

text 复制代码
Slave = 0x50
Register = 0x10

完整流程:

text 复制代码
START
   ↓
Address + W
   ↓
ACK
   ↓
Register Address
   ↓
ACK
   ↓
Repeated START
   ↓
Address + R
   ↓
ACK
   ↓
Slave发送Data
   ↓
Master发送NACK
   ↓
STOP

注意:

中间不是一定先 STOP 再 START。

很多器件推荐使用:

text 复制代码
Repeated START

二十八、什么是Repeated START

Repeated START:

text 复制代码
重复起始条件

本质上与 START 的时序规则相同:

SCL 高电平期间,SDA 从高变低。

区别是:

text 复制代码
普通START:
总线原本处于空闲状态

Repeated START:
前一个事务还没完全释放总线
主机直接重新发起START

所以:

text 复制代码
START
Address + W
ACK
Register
ACK
Repeated START
Address + R
ACK
Data
NACK
STOP

非常常见。


二十九、为什么使用Repeated START

因为主机需要:

  1. 先写寄存器地址;
  2. 保持总线控制权;
  3. 立即切换到读模式;
  4. 防止中间被其他主机抢占。

例如:

text 复制代码
先告诉设备我要读0x10
        ↓
不释放总线
        ↓
重新发送设备地址+Read
        ↓
读取0x10内容

这就是典型的:

text 复制代码
Register Read

三十、主机读一个字节时谁发ACK

例如:

text 复制代码
Slave → Master

从机发送:

text 复制代码
8bit Data

因为主机是接收方,所以:

text 复制代码
第9位由Master决定

如果主机还想继续读取:

text 复制代码
Master发送ACK

如果这个字节是最后一个:

text 复制代码
Master发送NACK

表示:

数据够了,不要继续发了。


三十一、主机读取多个字节

例如读取 4 字节:

text 复制代码
Slave → Byte1
Master → ACK

Slave → Byte2
Master → ACK

Slave → Byte3
Master → ACK

Slave → Byte4
Master → NACK

Master → STOP

所以:

text 复制代码
前几个字节:
ACK

最后一个字节:
NACK

这是非常重要的读操作规则。


三十二、为什么最后一个字节要NACK

因为 ACK 的含义可以理解为:

我收到这个字节了,请继续发送。

那么最后一个字节之后,如果主机仍然 ACK:

text 复制代码
Slave可能认为:
主机还要下一个字节

因此主机需要通过:

text 复制代码
NACK

明确告诉从机:

这是最后一个字节,停止继续发送。

随后主机产生:

text 复制代码
STOP

结束事务。


三十三、STM32 HAL读取寄存器示例

例如读取:

text 复制代码
Slave 7位地址:0x50
Register:0x10

代码:

c 复制代码
#define DEVICE_ADDR    (0x50U << 1)

uint8_t value = 0;

HAL_StatusTypeDef ret;

ret = HAL_I2C_Mem_Read(
    &hi2c1,
    DEVICE_ADDR,
    0x10,
    I2C_MEMADD_SIZE_8BIT,
    &value,
    1,
    100
);

if (ret == HAL_OK)
{
    printf("value = 0x%02X\r\n", value);
}
else
{
    printf("I2C read failed\r\n");
}

底层概念上类似:

text 复制代码
START
↓
Address + W
↓
ACK
↓
Register 0x10
↓
ACK
↓
Repeated START
↓
Address + R
↓
ACK
↓
Read Data
↓
NACK
↓
STOP

三十四、16位寄存器地址怎么办

有些器件内部寄存器地址不是8位,而是16位。

例如:

text 复制代码
Register Address = 0x1234

那么可以使用:

c 复制代码
HAL_I2C_Mem_Read(
    &hi2c1,
    DEVICE_ADDR,
    0x1234,
    I2C_MEMADD_SIZE_16BIT,
    &value,
    1,
    100
);

此时寄存器地址阶段可能发送:

text 复制代码
0x12
0x34

具体字节顺序和设备协议要看器件手册。


三十五、STOP停止条件是什么

通信完成后,主机通常发送:

text 复制代码
STOP

STOP 的定义是:

当 SCL 保持高电平时,SDA 从低电平变化为高电平。

即:

text 复制代码
SCL = HIGH

SDA:
LOW → HIGH

波形:

text 复制代码
SCL  ───────────────

SDA  ________┐
             └──────
          STOP方向相反示意

更准确记忆:

text 复制代码
START:
SCL高时
SDA 高 → 低

STOP:
SCL高时
SDA 低 → 高

所以:

起始看高到低,停止看低到高。


三十六、STOP的作用是什么

STOP 表示:

text 复制代码
本次I²C事务结束

并让总线重新回到:

text 复制代码
SCL = 1
SDA = 1

的空闲状态。

它的作用包括:

  1. 结束当前事务;
  2. 通知从机通信完成;
  3. 释放总线;
  4. 允许下一次通信开始。

三十七、完整I²C通信流程总结

最基础的通信流程:

text 复制代码
Idle
 ↓
START
 ↓
Address + R/W
 ↓
ACK
 ↓
Data
 ↓
ACK/NACK
 ↓
STOP

如果多个数据字节:

text 复制代码
START
↓
Address + R/W
↓
ACK
↓
Byte1
↓
ACK
↓
Byte2
↓
ACK
↓
Byte3
↓
ACK/NACK
↓
STOP

三十八、典型写寄存器完整流程

text 复制代码
START
↓
Slave Address + W
↓
ACK
↓
Register Address
↓
ACK
↓
Data
↓
ACK
↓
STOP

记成:

先找设备,再找寄存器,再写数据。


三十九、典型读寄存器完整流程

text 复制代码
START
↓
Slave Address + W
↓
ACK
↓
Register Address
↓
ACK
↓
Repeated START
↓
Slave Address + R
↓
ACK
↓
Data
↓
NACK
↓
STOP

记成:

先写寄存器地址,再重新起始切换到读模式。


四十、逻辑分析仪应该怎么看I²C

工程中排查 I²C 最有效的工具之一就是:

text 复制代码
逻辑分析仪

例如抓到:

text 复制代码
START
0x50 Write
ACK
0x10
ACK
0x55
ACK
STOP

就可以还原为:

text 复制代码
向地址0x50设备
寄存器0x10
写入0x55

四十一、逻辑分析仪排查的正确顺序

建议按照:

第一步:找START

检查:

text 复制代码
SCL高时
SDA是否高→低

第二步:看设备地址

确认:

text 复制代码
是不是预期地址

第三步:看R/W

确认:

text 复制代码
Write还是Read

第四步:看第9位

这是最关键的位置:

text 复制代码
ACK还是NACK

如果地址后立即 NACK:

重点检查:

text 复制代码
设备地址
器件供电
SDA/SCL
地址脚
芯片是否存在

第五步:看数据

检查:

text 复制代码
寄存器地址是否正确
数据顺序是否正确
多字节字节序是否正确

第六步:看STOP

确认:

text 复制代码
事务是否正确结束

四十二、如果地址直接NACK应该检查什么

这是最常见问题。

例如:

text 复制代码
START
Address 0x50
NACK
STOP

说明:

主机已经发起通信,但没有设备对这个地址应答。

重点检查:

  1. 从机是否上电;
  2. GND是否共地;
  3. SCL/SDA是否接反;
  4. 是否有上拉电阻;
  5. 地址是否正确;
  6. 7位/8位地址是否弄错;
  7. 地址选择脚是否正确;
  8. 器件是否处于复位;
  9. 器件是否正在忙;
  10. 时钟速度是否超过设备能力。

四十三、为什么读出来全是0xFF

如果数据:

text 复制代码
0xFF
0xFF
0xFF

不一定说明从机真的返回了 0xFF

需要看逻辑分析仪。

可能是:

text 复制代码
SDA一直处于高电平

原因包括:

  • 从机没有真正驱动 SDA;
  • 地址错误;
  • 读阶段没有成功;
  • 从机没响应;
  • 软件读取失败后使用了未更新缓冲区。

因此不能只看:

c 复制代码
printf("%02X", data);

还要看真实总线波形。


四十四、为什么读出来全是0x00

类似地:

text 复制代码
0x00

也不一定是正确数据。

可能是:

  • SDA被某个器件拉低;
  • 总线卡死;
  • 缓冲区提前清零但读取失败;
  • 地址或寄存器错误;
  • 器件真的返回0。

所以要同时检查:

text 复制代码
HAL返回值
+
逻辑分析仪

四十五、I²C总线为什么会卡死

典型现象:

text 复制代码
SDA长期为0

主机无法正常产生 START。

可能原因:

  • 从机在发送数据中途主机复位;
  • 从机认为事务还没结束;
  • 软件没有正确发送STOP;
  • 时钟中途停止;
  • 上电时序异常;
  • 外设状态机异常。

例如:

text 复制代码
Slave已经拉低SDA准备发送下一bit
        ↓
Master突然Reset
        ↓
Slave不知道主机已经重启
        ↓
SDA一直保持低

四十六、I²C总线恢复怎么做

常见工程办法是:

如果 SDA 被从机拉低,可以临时把 SCL 配成 GPIO,手动产生若干个时钟脉冲。

通常可以尝试:

text 复制代码
9个SCL脉冲

让从机内部状态机把剩余bit走完。

然后重新产生:

text 复制代码
STOP

最后重新初始化 I²C 外设。

概念流程:

text 复制代码
检测SDA=0
   ↓
关闭I²C外设
   ↓
SCL改GPIO
   ↓
产生9个时钟
   ↓
尝试生成STOP
   ↓
恢复I²C复用
   ↓
重新初始化

这是很多工业嵌入式设备中非常实用的恢复策略。


四十七、为什么是9个时钟

因为:

text 复制代码
一个I²C字节
=
8bit数据
+
1bit ACK/NACK

如果从机卡在某个字节中间,最多补齐若干位后让其完成当前字节和应答周期。

9个脉冲是一种非常常见的总线恢复做法。

实际是否有效还取决于从机芯片实现。


四十八、什么是Clock Stretching

虽然 SCL 通常由主机控制,但部分从机如果暂时处理不过来,可以:

text 复制代码
把SCL拉低

暂缓通信。

这叫:

text 复制代码
Clock Stretching

也就是时钟拉伸。

流程:

text 复制代码
Master尝试释放SCL
      ↓
Slave继续拉低SCL
      ↓
Master看到SCL还没有真正变高
      ↓
等待
      ↓
Slave处理完成
      ↓
释放SCL
      ↓
SCL被上拉到高

所以:

主机"释放SCL"不一定等于线路马上变高。


四十九、I²C速度有哪些

常见模式包括:

text 复制代码
Standard-mode
100 kbit/s

Fast-mode
400 kbit/s

Fast-mode Plus
1 Mbit/s

还有更高速模式,具体设备支持能力要看标准和器件规格。

工程中最常见:

text 复制代码
100kHz
400kHz

五十、为什么400kHz通信失败,100kHz却正常

如果降低 I²C 频率以后通信恢复,常见原因包括:

  • 上拉电阻过大;
  • 总线电容过大;
  • 走线过长;
  • 挂载设备太多;
  • 从机不支持更高频率;
  • 时序参数错误;
  • 电平转换器速度不足。

因为 I²C 高电平主要依赖:

text 复制代码
上拉电阻
+
总线电容

形成 RC 上升。

近似:

text 复制代码
上升速度与 Rpull-up × Cbus 有关

当:

text 复制代码
R太大
C太大

SDA/SCL 上升沿会变慢。

100kHz 还能工作,400kHz 就可能不满足时序。


五十一、上拉电阻应该选多大

实际取值需要根据:

  • 总线电压;
  • 总线电容;
  • 通信速度;
  • 器件最大灌电流;
  • 上升时间要求;

计算。

工程中经常见到:

text 复制代码
1kΩ
2.2kΩ
4.7kΩ
10kΩ

但不要认为:

text 复制代码
I²C上拉永远就是4.7kΩ

4.7kΩ 只是非常常见的经验值之一。


五十二、STM32如何判断设备是否在线

HAL 可以使用:

c 复制代码
HAL_I2C_IsDeviceReady();

例如:

c 复制代码
#define DEVICE_ADDR    (0x50U << 1)

if (HAL_I2C_IsDeviceReady(
        &hi2c1,
        DEVICE_ADDR,
        3,
        100) == HAL_OK)
{
    printf("I2C device online\r\n");
}
else
{
    printf("I2C device offline\r\n");
}

它很适合:

  • 开机检测设备;
  • I²C扫描;
  • 判断从机是否在线;
  • 故障恢复。

五十三、实现一个简单I²C扫描器

可以扫描常见7位地址:

c 复制代码
void I2C_Scan(void)
{
    for (uint8_t addr = 1; addr < 0x7F; addr++)
    {
        if (HAL_I2C_IsDeviceReady(
                &hi2c1,
                (uint16_t)(addr << 1),
                1,
                10) == HAL_OK)
        {
            printf(
                "Found I2C device: 0x%02X\r\n",
                addr
            );
        }
    }
}

如果输出:

text 复制代码
Found I2C device: 0x20
Found I2C device: 0x4E
Found I2C device: 0x68

说明总线上检测到对应7位地址设备。


五十四、I²C扫描器也不是万能的

一些器件可能:

  • 某些状态下不响应扫描;
  • 有特殊通信要求;
  • 存在保留地址;
  • 需要先解除复位;
  • 需要等待上电初始化。

所以扫描不到设备时,仍然应该:

text 复制代码
看原理图
看数据手册
看逻辑分析仪

五十五、软件I²C如何产生START

如果使用 GPIO 模拟 I²C,START 可以写成:

c 复制代码
void I2C_Start(void)
{
    SDA_HIGH();
    SCL_HIGH();

    I2C_Delay();

    SDA_LOW();

    I2C_Delay();

    SCL_LOW();
}

关键顺序:

text 复制代码
SDA = 1
SCL = 1

SDA → 0

SCL → 0

真正产生 START 的关键瞬间是:

text 复制代码
SCL为高时
SDA 1→0

五十六、软件I²C如何产生STOP

c 复制代码
void I2C_Stop(void)
{
    SDA_LOW();

    SCL_HIGH();

    I2C_Delay();

    SDA_HIGH();

    I2C_Delay();
}

关键:

text 复制代码
SCL高时
SDA 0→1

即 STOP。


五十七、软件I²C发送一个字节

示例:

c 复制代码
void I2C_SendByte(uint8_t data)
{
    for (uint8_t i = 0; i < 8; i++)
    {
        SCL_LOW();

        if ((data & 0x80U) != 0U)
        {
            SDA_HIGH();
        }
        else
        {
            SDA_LOW();
        }

        I2C_Delay();

        SCL_HIGH();

        I2C_Delay();

        data <<= 1;
    }

    SCL_LOW();
}

可以看到:

text 复制代码
每次先准备SDA
再拉高SCL

非常符合:

SDA 在 SCL 低时变化,高时保持稳定。


五十八、软件I²C读取ACK

示例:

c 复制代码
uint8_t I2C_WaitAck(void)
{
    uint8_t ack;

    SDA_RELEASE();

    I2C_Delay();

    SCL_HIGH();

    I2C_Delay();

    ack = SDA_READ();

    SCL_LOW();

    return ack;
}

如果:

text 复制代码
SDA = 0

说明:

text 复制代码
ACK

如果:

text 复制代码
SDA = 1

说明:

text 复制代码
NACK

需要注意 GPIO 模拟 I²C 时,SDA 必须能正确:

text 复制代码
输出低
+
释放线路
+
读取输入

五十九、I²C和UART最大的区别之一

UART:

text 复制代码
TX
RX

通常是:

text 复制代码
点对点
异步通信

而 I²C:

text 复制代码
SCL
SDA

属于:

text 复制代码
同步通信
共享总线
地址寻址

I²C 不需要每个设备单独占两根通信线。

多个从机可以共用:

text 复制代码
SCL
SDA

所以特别适合板内多个低速芯片通信。


六十、I²C和SPI有什么区别

简单比较:

对比 I²C SPI
典型信号线 2根 4根左右
时钟
地址机制 通常靠CS
多从机 地址区分 CS区分
全双工 通常否 可以
速度 中低速 通常更高
引脚占用 较多
线路结构 开漏+上拉 推挽较常见

所以:

text 复制代码
设备多、速度一般、引脚少
→ I²C非常合适

高速数据传输
→ SPI通常更有优势

六十一、I²C故障完整排查流程

如果 I²C 通信失败,可以按照下面顺序检查。

第一步:检查供电

确认从机:

text 复制代码
VCC正常
GND正常

第二步:检查SCL和SDA空闲状态

正常应为:

text 复制代码
SCL = High
SDA = High

如果不是:

重点排查:

text 复制代码
上拉电阻
短路
从机拉死
GPIO配置

第三步:检查START

逻辑分析仪看:

text 复制代码
SCL高时
SDA是否高→低

第四步:检查设备地址

确认:

text 复制代码
7位地址
8位地址
R/W
HAL传参格式

第五步:检查地址后的ACK

如果:

text 复制代码
Address NACK

先不要继续分析数据。

因为设备压根还没真正响应。


第六步:检查寄存器地址

确认:

text 复制代码
8位还是16位
高字节还是低字节先发
寄存器地址是否存在

第七步:检查数据ACK

如果地址有 ACK,但数据阶段出现 NACK:

重点检查:

text 复制代码
寄存器是否合法
从机是否忙
当前操作是否允许

第八步:检查Repeated START

读寄存器时确认:

text 复制代码
写寄存器地址
↓
Repeated START
↓
读数据

是否符合器件手册。


第九步:检查STOP

事务结束是否正常释放总线。


第十步:检查频率和波形质量

示波器检查:

text 复制代码
上升沿
下降沿
SCL频率
噪声
串扰

六十二、常见现象与原因对照

现象 可能原因
SCL/SDA一直高 主机没有启动I²C
SDA一直低 从机拉死/短路
SCL一直低 GPIO/I²C异常或Clock Stretch
地址后NACK 地址错、设备没上电
地址ACK,寄存器NACK 寄存器/协议不支持
100k正常,400k失败 上拉/电容/时序问题
读数据全FF 没人驱动SDA或读取失败
读数据全00 SDA被拉低或缓冲区问题
第一次正常之后失败 STOP/状态机/总线释放问题
MCU复位后I²C卡死 从机仍停在旧事务状态
HAL_BUSY 总线BUSY或软件状态没恢复
HAL_TIMEOUT SCL/SDA状态不符合预期

六十三、看逻辑分析仪时最重要的口诀

可以记住:

text 复制代码
先看START
再看地址
然后看R/W
第9位看ACK
接着看数据
最后找STOP

也就是:

text 复制代码
S
↓
ADDR + R/W
↓
ACK
↓
DATA
↓
ACK/NACK
↓
P

六十四、I²C整套记忆口诀

可以这样记:

起始看高到低,停止看低到高;
先地址,后方向;
每8位后看第9位;
第9位低是ACK,高是NACK;
写操作从机应答,读操作主机决定继续还是结束。

再压缩成:

text 复制代码
START:
SCL高,SDA高→低

STOP:
SCL高,SDA低→高

ACK:
第9位SDA=0

NACK:
第9位SDA=1

地址:
7bit + R/W

Write:
R/W=0

Read:
R/W=1

六十五、面试中怎么回答I²C完整通信流程

如果面试官问:

I²C完整通信流程是什么?

可以这样回答:

I²C 总线空闲时 SCL 和 SDA 都保持高电平。

主机发起通信时,在 SCL 为高电平期间将 SDA 从高拉低,产生 START 起始条件。

随后主机发送 7 位从机地址和 1 位 R/W 方向位。地址匹配的从机会在第 9 个时钟将 SDA 拉低产生 ACK。

之后开始按照每 8 位数据加 1 位 ACK/NACK 的形式传输数据。

写操作时主要由主机发送数据、从机应答;读操作时由从机发送数据、主机负责发送 ACK 或 NACK。

读取寄存器时通常先以写方向发送寄存器地址,再通过 Repeated START 重新发送从机地址并切换到读方向。

最后主机在 SCL 为高电平期间将 SDA 从低变高,产生 STOP 条件,释放总线。


六十六、最后总结

I²C 看起来只是:

text 复制代码
SCL + SDA

两根线,但协议内部其实有非常清晰的通信规则。

一笔最典型的事务:

text 复制代码
START
   ↓
Address + R/W
   ↓
ACK
   ↓
Data
   ↓
ACK / NACK
   ↓
STOP

如果是寄存器写:

text 复制代码
START
↓
Address + W
↓
ACK
↓
Register
↓
ACK
↓
Data
↓
ACK
↓
STOP

如果是寄存器读:

text 复制代码
START
↓
Address + W
↓
ACK
↓
Register
↓
ACK
↓
Repeated START
↓
Address + R
↓
ACK
↓
Data
↓
NACK
↓
STOP

最需要记住的几个核心规则:

text 复制代码
1. 总线空闲:
   SCL=1,SDA=1

2. START:
   SCL高时,SDA高→低

3. STOP:
   SCL高时,SDA低→高

4. 地址:
   常见为7位地址 + 1位R/W

5. R/W:
   0=写
   1=读

6. 每8位之后:
   第9位是ACK/NACK

7. ACK:
   SDA=0

8. NACK:
   SDA=1

9. 主机读最后一个字节:
   通常发送NACK,再发送STOP

10. 读寄存器:
    经常使用Repeated START

最后用一句话总结:

I²C通信的本质,就是主机通过 START 开启事务,用地址找到目标从机,通过 R/W 确定数据方向,每传输 8 位利用第 9 位 ACK/NACK 确认状态,最终通过 STOP 结束并释放总线。


相关推荐
zhanghaha13142 小时前
Python进阶教程:6_JSON 数据解析 —— 新手完全指南
开发语言·python·json
qq_448011163 小时前
C语言中的动态内存分配
c语言·开发语言·php
动词ing3 小时前
【学习笔记】C语言(数组指针与指针数组+字符数组+函数+参数传递+字符串作为形参+递归函数+指针函数+回调函数+结构体嵌套+内存动态分配函数)
c语言·笔记·学习
汉字萌萌哒3 小时前
2024CSP-J入门级C++真题详解
开发语言·c++
arbboter4 小时前
【网络工具】NetProxy网络代理用户手册
开发语言·网络·c#·网络代理·网络异常·代理工具
汉字萌萌哒5 小时前
2019CCF-CSP入门级C++试题解析
java·开发语言·c++
叠层归一研究院5 小时前
AGI 系统(十一):二阶网络 — 二阶递归 × 多主体 (元符号网络)
开发语言·人工智能·算法·php·agi
小雨笙笙5 小时前
C语言:变量三属性——存储类型
c语言·开发语言
果果燕6 小时前
C++ 学习笔记(二):类与对象—— 构造函数与析构函数
开发语言·c++