专题导航
嵌入式系统I2CFreeRTOS 任务状态与调度机制I2C 总线原理与工程实践FreeRTOS 任务基础
文章目录
嵌入式系统

I2C

I2C的学习笔记。

1. 摘要与目标

I2C 是嵌入式系统里非常常见的片间通信总线,典型用途是让 MCU 连接 EEPROM、OLED、RTC、IMU、温湿度传感器、IO 扩展芯片等低速外设。很多初学者能调用 HAL_I2C_Mem_Read(),但遇到无 ACK、地址左移、上拉电阻、Repeated START 或总线卡死时,就很难判断问题到底在协议、接线还是代码。本篇从工程排查角度整理 I2C 的核心机制:先建立 SDA/SCL 与开漏上拉的总线模型,再解释 START/STOP、ACK/NACK、7 位地址、寄存器读写流程,最后给出 STM32 常见实现方式和故障定位顺序。

本文适用于学习常见 MCU 与 I2C 外设之间的单主多从通信,示例以 STM32 HAL 为主,但协议结论不依赖具体开发板。读者只需了解基本 C 语言、GPIO 电平、MCU 外设初始化和寄存器概念。本文重点讨论 Standard-mode、Fast-mode 和 Fast-mode Plus,不展开 SMBus/PMBus 等衍生协议的完整差异。

学习时可以按三层模型建立理解:硬件层关注 SDA/SCL、开漏输出和上拉电阻;协议层关注 START/STOP、ACK/NACK、地址与读写方向;工程层再将这些流程映射到 STM32 HAL 调用、错误码和总线恢复策略。

I2C 总线结构示意图

图 1:I2C 总线结构示意图

2. 原理与关键机制

2.1 核心概念:SDA、SCL 与开漏上拉

I2C 使用两根信号线:SCL 是时钟线,SDA 是数据线。总线空闲时,两根线都应为高电平。I2C 设备的输出级通常不是推挽输出,而是开漏或开集输出:设备可以主动把线拉低,但不能主动把线推高;高电平由上拉电阻把总线拉回 VCC。

这种设计有三个好处:

  1. 多个设备可以共享同一根 SDA/SCL,不会出现一个设备推高、另一个设备拉低的硬冲突。
  2. ACK/NACK 可以由接收方在第 9 个时钟周期拉低 SDA 来表达。
  3. 多主仲裁和时钟拉伸都可以利用“低电平优先”的总线特性实现。

可以把 I2C 的电平关系记成一句话:

总线电平规则

  • 输出 0:设备主动拉低总线
  • 输出 1:设备释放总线,由上拉电阻拉高
  • 只要有一个设备拉低,总线就是低电平
  • 所有设备都释放,总线才是高电平

2.2 数据有效规则、START 和 STOP

普通数据位传输时,I2C 有一条非常重要的规则:SCL 为高电平期间,SDA 必须保持稳定;SDA 只允许在 SCL 为低电平时变化。接收方通常在 SCL 高电平期间采样 SDA,因此如果 SDA 在高电平期间变化,就可能被解释成控制条件。

从主机视角看,发送一个数据位的基本节奏是:先在 SCL 为低时设置 SDA,再释放 SCL 让其变高,接收方在高电平窗口内读取 SDA,最后主机再将 SCL 拉低准备下一位。因此,逻辑分析仪上看到的 SDA 跳变通常应位于 SCL 低电平区间。

但 START 和 STOP 是两个例外:

START / STOP 控制条件

  • START:SCL 为高时,SDA 从高变低。
  • STOP:SCL 为高时,SDA 从低变高。

总线空闲时 SDA 和 SCL 都是高电平。主机发出 START 后,总线进入忙状态;直到 STOP 出现,其他主机才能把这次传输视为已结束。如果主机在不发 STOP 的情况下再次产生 START,就是 Repeated START(重复起始)。它常用于在一次连续交易中更换读写方向,同时不释放总线。

I2C 时序中 SCL 高电平期间 SDA 保持稳定,START 是 SDA 高到低,STOP 是 SDA 低到高,数据字节后第 9 个时钟为 ACK 或 NACK

图 2:判断 I2C 波形时,先看 SCL 高电平期间 SDA 是否稳定,再识别 START、STOP 和第 9 个时钟的 ACK/NACK。

2.3 字节格式与 ACK/NACK

I2C 每次传输的基本单位是 8 位字节,通常最高位 MSB 先传。每传完 8 位后,第 9 个 SCL 时钟用于应答:

应答规则

  • ACK:接收方把 SDA 拉低,表示收到这个字节。
  • NACK:接收方释放 SDA,SDA 保持高电平,表示不应答。

应答位开始前,发送方必须释放 SDA,把第 9 个时钟的控制权交给接收方。如果接收方在此时拉低 SDA,主机读到的就是 ACK;如果没有设备拉低,上拉电阻会使 SDA 保持高电平,表现为 NACK。

“谁是接收方”取决于当前传输方向:

阶段发送方第 9 个时钟的应答方
主机发送地址或写数据主机被寻址的从设备
从设备返回读数据从设备主机

在多字节读取中,主机通常对前面的字节返回 ACK,表示“请继续发送”;对最后一个字节返回 NACK,通知从设备停止驱动 SDA,然后主机产生 STOP。所以读操作末尾的 NACK 是正常的结束信号,不是通信失败。

NACK 不一定表示“错误”。常见含义包括:

  • 地址阶段 NACK:总线上没有设备响应这个地址,或设备未上电、未准备好。
  • 写数据阶段 NACK:目标设备不能接收该字节,例如寄存器地址非法或内部忙。
  • 读数据最后一个字节后 NACK:主机主动告诉从设备“我不继续读了”。

2.4 7 位地址、8 位地址与 R/W 位

I2C 常见设备地址是 7 位地址,但实际传输的地址字节包含 7 位地址和 1 位读写方向:地址字节 = [7 位设备地址][R/W]。其中,R/W = 0 表示写,R/W = 1 表示读。

以常见的 0x68 为例:

  • 7 位地址:0x68
  • 8 位写地址:0x68 << 1 | 0 = 0xD0
  • 8 位读地址:0x68 << 1 | 1 = 0xD1

所以在 STM32 HAL 里经常看到:

#define DEV_ADDR_7BIT 0x68
#define DEV_ADDR_HAL (DEV_ADDR_7BIT << 1)

容易出错的地方在于:有些数据手册写 7 位地址,有些示例代码或逻辑分析仪显示 8 位读写地址。如果把 0xD0 当成 7 位地址再左移一次,最终地址就会错误,通常表现为无 ACK。

2.5 写寄存器流程

很多 I2C 外设内部都有寄存器。写寄存器时,主机通常先发送设备地址和写方向,再发送目标寄存器地址,最后发送要写入的数据:

S → DevAddr(W) → ACK → RegAddr → ACK → Data → ACK → P

其中:

  • S 是 START。
  • DevAddr(W) 是设备地址加写方向。
  • RegAddr 是目标设备内部寄存器地址,不是 I2C 设备地址。
  • Data 是写入寄存器的数据。
  • P 是 STOP。

这个流程中,主机负责发送地址字节、寄存器地址和数据;从设备则在每个字节后决定是否应答。不同阶段的 NACK 指向不同问题:

  • 设备地址后 NACK:通常是地址错误、设备未上电或未就绪。
  • 寄存器地址后 NACK:可能是命令格式不支持,或设备当前无法接收。
  • 数据后 NACK:可能是设备忙、写保护或超出可写范围。

连续写多个字节时,很多外设会在每次接收数据后自动增加内部寄存器指针,但是否支持连续写、页边界如何处理,必须以外设数据手册为准。

2.6 读寄存器流程与 Repeated START

读寄存器通常比写寄存器多一步,因为主机要先告诉目标设备“我要读哪个寄存器”,再切换为读方向获取数据。典型流程是:

S → DevAddr(W) → ACK → RegAddr → ACK → Sr → DevAddr(R) → ACK → Data → NACK → P

这里的 Sr 是 Repeated START,也就是重复起始条件。它的作用是在不释放总线的情况下切换传输方向,并保持目标设备内部寄存器指针的上下文。

前半段使用写方向,不是要改变寄存器内容,而是向从设备送入“接下来从哪个寄存器开始读”的地址。随后的 Repeated START 让主机在保持这个寄存器指针的同时,重新发送设备地址并将 R/W 位改为读。

进入读阶段后,SDA 的数据驱动权转交给从设备,而 SCL 仍通常由主机产生。主机在每个字节后返回 ACK 或 NACK:

读取多字节:Data1 → ACK → Data2 → ACK → ... → LastData → NACK → P

中间字节后的 ACK 表示继续读取,最后一个字节后的 NACK 则用来结束读操作。部分设备允许先 STOP 再重新 START 读取,但也有设备要求必须使用 Repeated START,因此应以数据手册给出的时序图为准。

I2C 写寄存器流程从 START 到 DevAddr(W)、RegAddr、Data、STOP;读寄存器流程先写 RegAddr,再通过 Repeated START 切换为 DevAddr(R) 读取 Data,最后 NACK 和 STOP

图 3:寄存器读操作不是一开始就读,而是先用写方向发送寄存器地址,再用 Repeated START 切换到读方向。

2.7 速率模式与上拉电阻

常见 I2C 速率可以先记四类:

模式典型最大速率工程使用建议
Standard-mode100 kbit/s初次调试、长线、普通传感器优先使用
Fast-mode400 kbit/sOLED、IMU、数据量稍大的外设常用
Fast-mode Plus1 Mbit/s需要目标设备和主机都支持,且上拉和布线满足要求
High-speed mode3.4 Mbit/s普通 MCU 外设项目较少直接使用

上拉电阻的选择不是固定值,而是一个折中:

  • 电阻太小:上拉太强,设备拉低总线时电流过大,可能达不到有效低电平。
  • 电阻太大:上拉太弱,上升沿太慢,高速通信时来不及恢复到有效高电平。
  • 总线电容越大、线越长、设备越多,上升沿越慢。

工程经验上,短线 3.3 V 系统可从下面范围起步,再用示波器或逻辑分析仪确认波形:

  • 100 kHz:4.7 kΩ ~ 10 kΩ 常见。
  • 400 kHz:2.2 kΩ ~ 4.7 kΩ 常见。
  • 更高速或更大总线电容:需要按上升时间和驱动能力重新计算。

这些数值不能替代计算和实测。真正的判断依据是:目标设备能否把线可靠拉低,以及 SDA/SCL 上升沿能否在当前速率下满足时序要求。

2.8 Clock Stretching

Clock Stretching 是从设备通过拉低 SCL 让主机等待的流控机制。当主机完成一个低电平阶段并尝试释放 SCL 时,从设备可以继续保持 SCL 为低,直到内部处理完成。主机不能只假设自己已经释放 SCL 就代表总线上已经是高电平,而应确认实际 SCL 电平。

常见场景包括从设备正在准备下一个数据字节,或者内部处理速度暂时跟不上总线时钟。但并非所有主机控制器、软件模拟 I2C 或驱动超时配置都能正确容忍长时间拉伸。遇到 SCL 长时间为低时,需要区分三种情况:正常 Clock Stretching、从设备状态机卡死,以及物理短路或引脚配置错误。

2.9 多主仲裁

多主场景下,两个主机可能几乎同时检测到总线空闲并发出 START。由于 SDA 和 SCL 都是开漏线,多个设备可以在不发生推挽短路的情况下同时驱动总线,并在发送过程中监测实际 SDA 电平。

仲裁的核心规则是:主机想发送 1 时会释放 SDA,但如果此时读到总线是 0,说明另一个主机正在拉低 SDA,它就失去了仲裁。发送 0 的主机不会察觉异常,可以继续传输。

仲裁结果

  • 本机发送:1
  • 实际读取:0
  • 结果:本机失去仲裁,停止驱动总线。

仲裁是逐位进行的,不会破坏获胜主机正在发送的字节。普通单主 MCU 项目很少需要实际处理多主仲裁,但这个机制能帮助理解为什么 I2C 必须使用“拉低或释放”的开漏方式,以及为什么低电平在总线上具有优先级。

3. STM32 HAL 使用方法

STM32 HAL 将 I2C 外设的状态检查、时序控制和数据收发封装成了一组统一 API。学习时不必先记函数原型,更重要的是理解每类 API 对应哪种 I2C 传输流程。

3.1 使用前准备

  1. 从外设数据手册确认 7 位设备地址、寄存器地址宽度、最高速率和上电等待时间。
  2. 在 CubeMX 中配置 SCL/SDA 复用开漏引脚和 I2C 时序,初次调试建议使用 100 kHz。
  3. 确认供电、共地和外部上拉电阻正常,总线空闲时 SDA/SCL 应均为高电平。

3.2 常用 HAL API

API用途对应的协议行为
HAL_I2C_IsDeviceReady()检查某个地址是否有设备应答发送 START 和地址,检查是否收到 ACK
HAL_I2C_Master_Transmit()主机向从设备发送原始字节流地址写阶段后按顺序发送缓冲区数据
HAL_I2C_Master_Receive()主机从从设备接收原始字节流地址读阶段后连续接收数据
HAL_I2C_Mem_Write()向外设内部寄存器写入数据先发送寄存器地址,再发送数据
HAL_I2C_Mem_Read()读取外设内部寄存器先写寄存器地址,再用 Repeated START 切换为读方向

Master_Transmit/Receive 适合自定义命令帧或没有寄存器模型的设备;Mem_Write/Read 则适合传感器、EEPROM 等常见寄存器型外设。

3.3 关键参数

HAL 函数中容易混淆的参数主要有:

参数含义注意事项
hi2cI2C 外设句柄例如 &hi2c1
DevAddressHAL 使用的设备地址通常传入 7 位地址 << 1
MemAddress目标外设内部的寄存器地址不是 I2C 设备地址
MemAddSize寄存器地址宽度根据手册选择 I2C_MEMADD_SIZE_8BITI2C_MEMADD_SIZE_16BIT
pData / Size数据缓冲区及字节数多字节数据还要确认字节序
Timeout阻塞调用的最长等待时间单位和行为以对应 STM32 HAL 文档为准

例如,读取 7 位地址为 0x68 的设备中 0x75 寄存器:

uint8_t value;
HAL_StatusTypeDef status = HAL_I2C_Mem_Read(
&hi2c1,
0x68 << 1,
0x75,
I2C_MEMADD_SIZE_8BIT,
&value,
1,
100
);

这段示例的重点是参数对应关系,不是封装一套通用驱动。

3.4 阻塞、中断与 DMA 模式

HAL 为常用传输函数提供了阻塞、中断和 DMA 版本:

模式常见 API 后缀特点适用场景
阻塞无后缀函数等待传输完成或超时初次调试、低频小数据量通信
中断_IT函数快速返回,完成后进入回调不希望 CPU 阻塞等待的异步任务
DMA_DMA由 DMA 搬运数据,完成后进入回调较大数据块或高频传输

学习和排查阶段可以先使用阻塞 API,因为调用顺序和错误位置更直观。切换到中断或 DMA 后,还需要处理完成回调、错误回调、缓冲区生命周期和并发访问。

3.5 返回状态与错误定位

阻塞 API 通常返回 HAL_OKHAL_ERRORHAL_BUSYHAL_TIMEOUT。返回值不是 HAL_OK 时,可以继续读取 HAL_I2C_GetError(),再结合芯片系列的 HAL 文档判断是 NACK、总线错误、仲裁丢失还是超时。

if (status != HAL_OK) {
uint32_t error = HAL_I2C_GetError(&hi2c1);
printf("I2C status=%d, error=0x%08lX\r\n", status, error);
}

错误处理时不要只重试函数,还要检查地址格式、总线空闲电平、ACK 波形和外设当前状态。

4. 故障排查

4.1 排查原则

I2C 故障不要一上来改代码。推荐顺序是:先看电,再看线,再看地址,再看时序,最后看外设内部状态。每次只改变一个变量,并保留错误码、波形或日志作为证据。

I2C 故障排查从空闲电平开始,依次检查供电共地、SDA/SCL 接线、上拉电阻、设备地址、ACK 波形、寄存器地址、Repeated START 和从设备状态

图 4:I2C 排查要先排除硬件和总线电平问题,再进入 HAL 参数、寄存器流程和外设状态判断。

4.2 问题一:没有 ACK

  • 发生条件:发送设备地址后,第 9 个时钟 SDA 仍为高电平。
  • 证据:逻辑分析仪显示地址阶段 NACK,或 HAL 返回 NACK 类错误。
  • 原因假设:设备地址错误、7 位 / 8 位地址混淆、设备未供电、SDA/SCL 接反、无共地、无上拉或外设未准备好。
  • 定位过程:先测 VCC/GND,再测空闲电平,再确认接线,然后扫描地址,最后对照数据手册确认地址脚配置。
  • 处理方法:统一使用 7 位地址记录,调用 HAL 时左移;修正接线和上拉;确认外设上电延时。

4.3 问题二:SDA 或 SCL 一直为低电平

  • 发生条件:总线空闲时 SDA/SCL 不是高电平,而是长期低电平。
  • 证据:万用表、示波器或逻辑分析仪显示总线被拉低。
  • 原因假设:某个从设备卡住、传输中断导致状态机未释放、短路、焊接问题、上拉异常。
  • 定位过程:断开从设备逐个测试;检查是否某个设备释放后总线恢复;必要时手动产生若干 SCL 脉冲尝试释放 SDA。
  • 处理方法:复位目标设备、重新初始化 I2C 外设、增加总线恢复逻辑。

4.4 问题三:100 kHz 正常,400 kHz 不稳定

  • 发生条件:低速读写正常,高速出现 NACK、数据错乱或超时。
  • 证据:升高 I2C 速率后错误率增加;示波器显示上升沿变慢。
  • 原因假设:上拉电阻过大、线长太长、总线电容过大、设备不支持该速率。
  • 定位过程:恢复 100 kHz 复测;缩短线长;减小上拉电阻;减少挂载设备;查目标设备最高速率。
  • 处理方法:保守使用 100 kHz,或根据总线电容和上升时间重新选择上拉电阻。

4.5 问题四:读出的数据一直是 0xFF0x00

  • 发生条件:通信函数返回成功或偶尔成功,但数据不符合预期。
  • 证据:日志显示固定值,业务逻辑异常。
  • 原因假设:寄存器地址错误、读写方向错误、未等待传感器转换完成、读长度错误、字节序错误。
  • 定位过程:先读设备 ID 寄存器;再读状态寄存器;最后读业务数据寄存器。
  • 处理方法:对照数据手册确认寄存器地址、读写顺序、转换时间和多字节拼接方式。

5. 性能、可靠性与安全性

5.1 性能

I2C 的实际速度不只由配置的 fSCL 决定,还受总线电容、上拉电阻、线长、设备驱动能力和 Clock Stretching 影响。工程中不要只看 CubeMX 或驱动里的 100 kHz / 400 kHz 配置,应该用波形确认上升沿和 ACK 时序。

5.2 可靠性

可靠的 I2C 驱动不应该只关心 HAL_OK。至少要考虑:

  • 每次传输设置合理超时。
  • 错误时记录 HAL 状态和错误码。
  • 总线忙或超时时尝试重新初始化外设。
  • 对可能卡住的从设备提供复位或断电恢复方案。
  • 对传感器类设备遵守转换时间和状态位检查。

5.3 安全性

I2C 多用于板内通信,通常不是直接暴露到网络的安全边界。但如果 I2C 外设参与安全功能,例如 EEPROM 存储配置、加密芯片、身份认证芯片,就需要避免把密钥、认证结果或敏感配置完整打印到日志中。

6. 结论

I2C 的学习重点不是背某一个 HAL 函数,而是理解“开漏上拉的共享总线如何承载地址、数据和应答”。只要掌握 SDA/SCL 的时序规则、START/STOP、ACK/NACK、7 位地址与 R/W 位、寄存器读写流程和上拉电阻的工程约束,就能把大多数 I2C 问题拆成可验证的硬件、协议或驱动问题。实际项目中,最终结论必须由真实波形、日志和测试结果支撑,生成插图只能帮助解释,不能替代证据。

7. 官方参考资料

  1. NXP UM10204:I2C-bus specification and user manual
  2. Texas Instruments SLVA689:I2C Bus Pullup Resistor Calculation
  3. ST AN4899:STM32 microcontroller GPIO hardware settings and low-power consumption