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 调用、错误码和总线恢复策略。
图 1:I2C 总线结构示意图
2. 原理与关键机制
2.1 核心概念:SDA、SCL 与开漏上拉
I2C 使用两根信号线:SCL 是时钟线,SDA 是数据线。总线空闲时,两根线都应为高电平。I2C 设备的输出级通常不是推挽输出,而是开漏或开集输出:设备可以主动把线拉低,但不能主动把线推高;高电平由上拉电阻把总线拉回 VCC。
这种设计有三个好处:
- 多个设备可以共享同一根 SDA/SCL,不会出现一个设备推高、另一个设备拉低的硬冲突。
- ACK/NACK 可以由接收方在第 9 个时钟周期拉低 SDA 来表达。
- 多主仲裁和时钟拉伸都可以利用“低电平优先”的总线特性实现。
可以把 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(重复起始)。它常用于在一次连续交易中更换读写方向,同时不释放总线。

图 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,因此应以数据手册给出的时序图为准。

图 3:寄存器读操作不是一开始就读,而是先用写方向发送寄存器地址,再用 Repeated START 切换到读方向。
2.7 速率模式与上拉电阻
常见 I2C 速率可以先记四类:
| 模式 | 典型最大速率 | 工程使用建议 |
|---|---|---|
| Standard-mode | 100 kbit/s | 初次调试、长线、普通传感器优先使用 |
| Fast-mode | 400 kbit/s | OLED、IMU、数据量稍大的外设常用 |
| Fast-mode Plus | 1 Mbit/s | 需要目标设备和主机都支持,且上拉和布线满足要求 |
| High-speed mode | 3.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 使用前准备
- 从外设数据手册确认 7 位设备地址、寄存器地址宽度、最高速率和上电等待时间。
- 在 CubeMX 中配置 SCL/SDA 复用开漏引脚和 I2C 时序,初次调试建议使用 100 kHz。
- 确认供电、共地和外部上拉电阻正常,总线空闲时 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 函数中容易混淆的参数主要有:
| 参数 | 含义 | 注意事项 |
|---|---|---|
hi2c | I2C 外设句柄 | 例如 &hi2c1 |
DevAddress | HAL 使用的设备地址 | 通常传入 7 位地址 << 1 |
MemAddress | 目标外设内部的寄存器地址 | 不是 I2C 设备地址 |
MemAddSize | 寄存器地址宽度 | 根据手册选择 I2C_MEMADD_SIZE_8BIT 或 I2C_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_OK、HAL_ERROR、HAL_BUSY 或 HAL_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 故障不要一上来改代码。推荐顺序是:先看电,再看线,再看地址,再看时序,最后看外设内部状态。每次只改变一个变量,并保留错误码、波形或日志作为证据。

图 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 问题四:读出的数据一直是 0xFF 或 0x00
- 发生条件:通信函数返回成功或偶尔成功,但数据不符合预期。
- 证据:日志显示固定值,业务逻辑异常。
- 原因假设:寄存器地址错误、读写方向错误、未等待传感器转换完成、读长度错误、字节序错误。
- 定位过程:先读设备 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 问题拆成可验证的硬件、协议或驱动问题。实际项目中,最终结论必须由真实波形、日志和测试结果支撑,生成插图只能帮助解释,不能替代证据。