嵌入式 MCU

I2C 面试手册

复习 I2C 总线模型、时序规则、寄存器读写、STM32 HAL 使用和常见故障排查。

难度:进阶

核心速记卡

概念一句话定义关键限制
开漏上拉设备只能主动拉低 SDA/SCL,高电平由上拉电阻恢复上拉太弱会导致上升沿慢,上拉太强会增加拉低电流
数据有效规则普通数据位在 SCL 高电平期间必须保持稳定SCL 高时 SDA 跳变会被解释为 START 或 STOP
STARTSCL 高电平时 SDA 从高变低表示一次传输开始,总线进入 busy
STOPSCL 高电平时 SDA 从低变高表示一次传输结束,总线释放
ACK每 8 位数据后的第 9 个 SCL 周期,接收方拉低 SDA读最后一个字节后主机发送 NACK 是正常结束
7 位地址常见 I2C 地址不含 R/W 位STM32 HAL 常传入 7 位地址 << 1
Repeated START不发送 STOP 的情况下再次产生 START常用于先写寄存器地址,再切换为读方向
Clock Stretching从设备拉低 SCL 让主机等待主机控制器和超时配置必须支持
多主仲裁主机发送 1 但读到 0 时失去仲裁基于开漏线与特性,不破坏获胜报文

基础问答

Q: I2C 为什么只需要两根线就能挂多个设备?

答: I2C 使用 SCL 时钟线和 SDA 数据线,多个主从设备共享这两根线。设备通过地址区分目标,通过开漏输出避免硬冲突:任何设备都只能拉低总线,释放后由上拉电阻恢复高电平。

Q: 为什么 SDA 和 SCL 要使用开漏输出和上拉电阻?

答: 开漏结构让多个设备可以安全共享同一根线。如果一个设备要输出 0,就主动拉低;如果要输出 1,就释放总线。只要有设备拉低,总线就是低电平;所有设备都释放,总线才是高电平。

Q: I2C 的数据有效规则是什么?

答: 普通数据传输时,SCL 为高电平期间 SDA 必须保持稳定,SDA 只应在 SCL 为低电平时变化。接收方通常在 SCL 高电平窗口采样 SDA。

Q: START 和 STOP 条件分别是什么?

答: START 是 SCL 高电平时 SDA 从高变低;STOP 是 SCL 高电平时 SDA 从低变高。它们是数据有效规则的两个特殊例外,用来标记传输开始和结束。

Q: ACK 和 NACK 怎么判断?NACK 一定是错误吗?

答: 每传完 8 位后,第 9 个 SCL 周期用于应答。接收方拉低 SDA 是 ACK,释放 SDA 让其保持高电平是 NACK。NACK 不一定是错误,例如主机读取最后一个字节后主动 NACK,是告诉从设备“我不继续读了”。

Q: I2C 的 7 位地址和 8 位地址有什么区别?

答: 7 位地址是设备地址本身;总线上实际发送的地址字节由 7 位地址加 1 位 R/W 位组成。比如设备 7 位地址是 0x68,写地址字节是 0xD0,读地址字节是 0xD1。在 STM32 HAL 中通常传 0x68 << 1,不要把 0xD0 再左移一次。

Q: 写寄存器的一般流程是什么?

答: 典型流程是 START -> DevAddr(W) -> ACK -> RegAddr -> ACK -> Data -> ACK -> STOP。其中 RegAddr 是外设内部寄存器地址,不是 I2C 设备地址。

Q: 读寄存器为什么通常要先写后读?

答: 因为主机要先告诉从设备“接下来从哪个寄存器开始读”。典型流程是 START -> DevAddr(W) -> RegAddr -> Repeated START -> DevAddr(R) -> Data -> NACK -> STOP。前半段写的是寄存器地址,不是写寄存器内容。

深入追问

Q: Repeated START 和 STOP 后再 START 有什么区别?

答: Repeated START 不释放总线,可以在同一次事务里切换读写方向,并保持从设备内部寄存器指针的上下文。STOP 后再 START 会结束前一次事务,在多主场景下还可能让其他主机插入。

追问: 哪些场景必须优先考虑 Repeated START? 答: 读传感器寄存器、EEPROM 随机读、需要先发送命令再读结果的外设,都要检查数据手册是否要求 Repeated START。

Q: 多字节读取时,为什么最后一个字节后主机要发送 NACK?

答: 读阶段的数据发送方是从设备,接收方是主机。主机对中间字节 ACK,表示继续读;对最后一个字节 NACK,表示不再需要更多数据,然后产生 STOP 结束事务。

追问: 如果最后一个字节后还 ACK 会怎样? 答: 从设备可能继续准备下一个字节,而主机又要结束传输,容易造成状态不一致;具体后果取决于控制器和从设备实现。

Q: Clock Stretching 是什么?排查时如何区分它和总线卡死?

答: Clock Stretching 是从设备主动拉低 SCL,让主机等待内部处理完成。正常拉伸会在有限时间内释放 SCL;如果 SCL 长时间为低,就要区分正常等待、从设备状态机卡死、物理短路或 GPIO 配置错误。

追问: 为什么有些项目会禁用或避免 Clock Stretching? 答: 因为不是所有主机控制器、软件模拟 I2C 或驱动超时配置都能可靠处理长时间 SCL 低电平。工程上要确认主控支持能力,并设置明确超时。

Q: 多主仲裁是怎么实现的?为什么不会破坏数据?

答: 仲裁基于开漏总线的低电平优先规则。主机发送 1 时会释放 SDA,如果它读到总线实际为 0,说明其他主机正在发送 0,于是本机失去仲裁并停止驱动。失败方退出时,之前发送的位与获胜方完全一致,所以不会破坏获胜报文。

追问: 单主 MCU 项目还需要理解仲裁吗? 答: 需要。仲裁能帮助解释 I2C 为什么必须使用开漏输出,以及为什么“释放高、读到低”是总线竞争或异常的重要信号。

Q: 如何选择 I2C 上拉电阻?

答: 上拉电阻要同时满足低电平电流和上升时间要求。电阻太小,设备拉低时电流过大;电阻太大,总线电容充电太慢,高速时上升沿来不及到达有效高电平。短线 3.3 V 系统中,100 kHz 常见 4.7 kΩ ~ 10 kΩ,400 kHz 常见 2.2 kΩ ~ 4.7 kΩ,最终要用波形确认。

追问: 为什么 MCU 内部上拉通常不够? 答: 内部上拉常在几十 kΩ 量级,电阻偏大,上升沿慢,尤其在 400 kHz、长线或多设备场景下容易失效。

Q: I2C 和 SPI 在面试中怎么比较?

答: I2C 线少,支持多设备共享、地址和 ACK,适合 EEPROM、RTC、传感器、PMIC 等低速板内外设;SPI 线多但速度高、全双工、协议简单,适合 Flash、显示屏、ADC/DAC 等高吞吐场景。选择时看速率、引脚数量、设备数量、距离和可靠性需求。

STM32 HAL 高频点

Q: HAL_I2C_IsDeviceReady() 能证明什么?

答: 它能证明指定地址在地址阶段有设备 ACK,适合做地址探测和上电后可用性检查。但它不能证明寄存器地址正确,也不能证明后续读写流程一定正确。

Q: Master_Transmit/ReceiveMem_Write/Read 怎么选?

答: Master_Transmit/Receive 适合发送或接收原始字节流,比如自定义命令帧。Mem_Write/Read 适合寄存器型外设,它会帮你完成“设备地址 + 寄存器地址 + 数据”的常见流程,其中 Mem_Read 通常会包含先写寄存器地址再 Repeated START 读数据的过程。

Q: HAL 里 DevAddressMemAddressMemAddSize 分别是什么?

答: DevAddress 是 I2C 设备地址,STM32 HAL 常传 7 位地址 << 1MemAddress 是外设内部寄存器地址;MemAddSize 是寄存器地址宽度,常见为 I2C_MEMADD_SIZE_8BITI2C_MEMADD_SIZE_16BIT

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

Q: 阻塞、中断和 DMA 模式怎么取舍?

答: 初次调试优先用阻塞模式,因为调用顺序、错误位置和日志最直观。中断模式适合不想阻塞 CPU 的短事务,DMA 适合较大数据块或高频传输。切到中断或 DMA 后,要额外处理回调、错误回调、缓冲区生命周期和并发访问。

Q: HAL 返回 HAL_ERRORHAL_BUSYHAL_TIMEOUT 后该怎么查?

答: 先用 HAL_I2C_GetError() 读取错误码,再结合波形判断是地址 NACK、总线错误、仲裁丢失、超时还是总线忙。不要只盲目重试;应同时检查地址格式、空闲电平、ACK 波形、寄存器流程和外设状态。

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

工程场景题

场景:设备地址阶段一直 NACK

分析思路: 先确认 VCC、GND 和共地,再测总线空闲时 SDA/SCL 是否均为高电平;接着确认 SDA/SCL 是否接反、上拉电阻是否存在、外设上电等待时间是否满足;然后核对 7 位地址、地址脚配置和 HAL 是否左移一次。逻辑分析仪上如果地址字节后第 9 位 SDA 仍为高,就说明没有设备在应答。

面试表达: 地址阶段 NACK 优先怀疑硬件连接、供电、上拉、地址格式和外设未就绪,不应第一反应就是改业务代码。

场景:SDA 或 SCL 一直低,总线 busy

分析思路: 先区分是 SDA 低还是 SCL 低。SCL 持续低可能是从设备 Clock Stretching、设备卡死、短路或主控 GPIO 配置错误;SDA 低而 SCL 可控时,可以尝试输出最多 9 个 SCL 脉冲,让从设备完成未结束的字节,再发送 STOP。若仍不能恢复,应复位 I2C 外设、复位从设备或断电定位。

面试表达: 总线恢复逻辑只能作为补救,不能替代对短路、上拉、电源和设备复位条件的检查。

场景:100 kHz 正常,400 kHz 不稳定

分析思路: 这是典型的上升沿和时序裕量问题。速率升高后,SDA/SCL 从低恢复到高的时间变短,如果上拉太弱、线太长、设备太多或总线电容太大,就会出现 NACK、误采样或超时。先降回 100 kHz 验证,再缩短连线、减小上拉电阻、减少挂载设备,并确认目标设备支持 400 kHz。

面试表达: I2C 速率不是 CubeMX 配了 400 kHz 就一定可靠,必须由总线电容、上拉电阻和实际波形共同决定。

场景:读出的数据总是 0xFF

分析思路: 0xFF 常意味着 SDA 在读数据阶段一直为高,可能是从设备根本没有驱动数据线、寄存器读流程错误、读错地址或 NACK 后主机仍读取了释放状态。先读固定 ID 寄存器,再检查是否使用了“先写寄存器地址 + Repeated START + 读”的流程。

面试表达: 固定 0xFF 不要直接解释成传感器测量值异常,先确认读阶段是否真的有从设备在驱动 SDA。

场景:读出的数据总是 0x00 或多字节数据不对

分析思路: 0x00 可能是 SDA 被异常拉低,也可能是寄存器未配置、数据未转换完成或读到了状态清零值。多字节错误还要检查字节序、寄存器地址是否自动递增、是否跨页或跨寄存器块。排查时优先读设备 ID、状态寄存器,再读业务数据。

面试表达: 数据异常要把“通信成功”和“业务数据有效”分开判断,HAL_OK 只说明传输层没有报错。

易错点

  • 误区: I2C 地址就是数据手册里看到的那个十六进制值。 正解: 先判断手册给的是 7 位地址还是包含 R/W 位的 8 位地址,STM32 HAL 通常传 7 位地址 << 1

  • 误区: NACK 一定代表通信失败。 正解: 地址或写数据阶段 NACK 常表示异常,但读最后一个字节后的 NACK 是主机主动结束读取。

  • 误区: Repeated START 可以随便换成 STOP + START。 正解: Repeated START 不释放总线,并保持某些从设备的寄存器指针上下文;是否能替换要看数据手册。

  • 误区: I2C 只要接线对,速度就能随便提高。 正解: 速率受上拉电阻、总线电容、线长、设备驱动能力和时序裕量限制。

  • 误区: MCU 内部上拉足够做 I2C 上拉。 正解: 内部上拉通常阻值偏大,初期低速短线可能碰巧可用,但不能作为可靠设计依据。

  • 误区: HAL_I2C_Mem_Read() 失败就说明 HAL 有问题。 正解: 更常见原因是地址左移、寄存器地址宽度、Repeated START 要求、外设未就绪或总线波形问题。

  • 误区: 总线卡死时一直软件重试即可。 正解: 应记录错误码,检查实际电平,必要时执行 SCL 脉冲恢复、重新初始化 I2C、复位从设备或断电恢复。

面试口述模板

如果面试官让你“讲一下 I2C”,可以按这段组织:

I2C 是一种两线式同步串行总线,用 SDA 传数据、SCL 提供时钟。它的关键是开漏输出加上拉电阻,所以多个设备可以共享总线,并通过低电平优先实现 ACK、仲裁和时钟拉伸。协议上,SCL 高电平期间 SDA 应保持稳定,START 是 SCL 高时 SDA 高到低,STOP 是 SCL 高时 SDA 低到高。每个字节 8 位 MSB 先发,第 9 个时钟由接收方 ACK 或 NACK。常见寄存器读不是直接读,而是先写设备地址和寄存器地址,再用 Repeated START 切到读方向。工程排查时我会先看空闲电平和上拉,再看地址 ACK、寄存器流程、速率波形和外设状态。

一句话总结

I2C 面试的核心不是背 HAL 函数,而是能用“开漏上拉的共享总线模型”解释地址、ACK、Repeated START、上拉电阻和故障波形。

官方参考资料