从波形到中断:一篇看懂 I2C 通信原理、地址、ACK 与调试方法
从波形到中断:一篇看懂I2C通信原理、地址、ACK与调试方法
一、本文适用场景
本文适用于以下学习和调试场景:
- 正在学习STM32、GD32或其他单片机的I2C外设;
- 会调用I2C库函数,但看不懂底层通信过程;
- 不清楚SCL和SDA应该在什么时候变化;
- 不理解START、STOP、ACK和NACK的波形特征;
- 不清楚7位地址、8位地址、读地址和写地址之间的关系;
- 不明白I2C发送一个字节后为什么会产生第9个时钟;
- 不清楚SBSEND、ADDSEND、TBE、RBNE、BTC和AERR等标志的含义;
- 使用逻辑分析仪抓取I2C波形,但不知道应该按照什么顺序分析;
- 遇到地址后NACK、总线BUSY、SDA被拉低、接收中断频繁触发等问题。
本文将从I2C的基础采样规则开始,逐步讲解一帧通信的完整流程,并把代码中的状态标志与逻辑分析仪上的实际波形对应起来。
二、I2C总线基础
I2C是一种同步串行通信协议,常用于单片机与传感器、EEPROM、电源管理芯片、风扇控制器、显示屏等器件之间的通信。
I2C总线主要由两根信号线组成:
- SCL:Serial Clock,串行时钟线;
- SDA:Serial Data,串行数据线。
其中,主机通常负责产生SCL时钟,SDA用于传输地址、读写方向、数据以及ACK/NACK应答信号。
I2C最核心的采样规则是:
SCL低电平期间,SDA可以变化。 SCL高电平期间,SDA应保持稳定。发送方一般会在SCL为低电平时修改SDA,为下一位数据做好准备。
接收方则在SCL上升沿附近,或者SCL高电平的稳定区域内读取SDA。
因此,在分析I2C波形时,应该重点观察每一个SCL上升沿附近的SDA电平:
SDA为高电平:表示逻辑1 SDA为低电平:表示逻辑0图1 I2C基础与采样原则
从图中可以看到,SCL低电平期间,SDA可以发生变化;SCL进入高电平后,SDA需要保持稳定,接收方根据SDA的高低电平还原数据。
需要注意的是,START和STOP属于特殊情况。它们允许SDA在SCL高电平期间发生变化,用来表示一次通信的开始和结束。
三、I2C总线空闲状态
在没有设备进行通信时,I2C总线通常处于空闲状态。
总线空闲时:
SCL = 高电平 SDA = 高电平这是因为I2C的SCL和SDA通常采用开漏输出结构,信号线依靠外部上拉电阻恢复为高电平。
主机在发起一次新的通信之前,通常需要先检查总线是否空闲。
在部分MCU中,可以通过BUSY标志判断I2C总线状态。
示例:
while(i2c_flag_get(I2C0,I2C_FLAG_I2CBSY)){/* 等待I2C总线空闲 */}如果总线一直处于BUSY状态,可能存在以下问题:
- 上一次通信没有正常发送STOP;
- SDA被某个从机持续拉低;
- SCL被某个设备持续拉低;
- 主机在通信过程中异常复位;
- GPIO复用配置错误;
- I2C外设没有正确初始化;
- BUSY状态没有被正确恢复。
四、START起始信号
当主机准备发起一次I2C通信时,需要先产生START起始信号。
START的产生条件是:
SCL保持高电平时,SDA由高电平变为低电平。也就是:
SCL = 1 SDA:1 → 0因为正常数据传输时,SDA不应该在SCL高电平期间变化,所以从机一旦检测到这种特殊变化,就知道一次新的通信开始了。
START产生后,主机通常会继续发送:
7位从机地址 + R/W读写方向位在部分GD32或STM32系列MCU中,程序设置START位后,I2C硬件会自动产生START波形。
示例:
i2c_start_on_bus(I2C0);然后等待START发送完成标志:
while(!i2c_flag_get(I2C0,I2C_FLAG_SBSEND)){}其中,SBSEND通常表示:
START起始条件已经成功发送。五、STOP停止信号
当主机完成地址和数据传输后,通常会产生STOP停止信号,释放I2C总线。
STOP的产生条件是:
SCL保持高电平时,SDA由低电平变为高电平。也就是:
SCL = 1 SDA:0 → 1START和STOP可以通过下面的方法快速记忆:
SCL高电平时,SDA高变低:START SCL高电平时,SDA低变高:STOP在程序中,主机通常通过设置STOP位,让I2C硬件自动产生停止波形。
示例:
i2c_stop_on_bus(I2C0);产生STOP以后,SCL和SDA最终都应该恢复为高电平,总线重新进入空闲状态。
六、ACK和NACK是什么
I2C每发送8位地址或数据后,还会额外产生第9个时钟。
前8个时钟用于传输一个完整字节,第9个时钟用于传输ACK或NACK应答信号。
1. ACK应答
ACK表示接收方已经正确接收到前面的地址或数据。
在第9个时钟期间,接收方主动将SDA拉低:
第9个时钟期间: SDA = 0这就是ACK。
ACK可以理解为接收方告诉发送方:
当前字节已经收到,可以继续通信。2. NACK非应答
NACK表示接收方没有应答,或者接收方不准备继续接收后续数据。
在第9个时钟期间,如果没有设备把SDA拉低,SDA保持高电平:
第9个时钟期间: SDA = 1这就是NACK。
3. ACK和NACK由谁产生
ACK或NACK由当前字节的接收方产生。
主机向从机写数据时:
发送方:主机 接收方:从机 ACK/NACK:由从机产生主机从从机读取数据时:
发送方:从机 接收方:主机 ACK/NACK:由主机产生连续读取多个字节时,主机通常对前面的数据返回ACK,表示还要继续读取。
读取最后一个字节后,主机返回NACK,表示:
当前字节是最后一个字节,不需要继续发送。随后主机产生STOP,结束本次通信。
图2 I2C的START、STOP、ACK与NACK
从图中可以看到,START和STOP都发生在SCL高电平期间。每发送8位地址或数据后,第9个时钟用于传输ACK或NACK。
七、为什么发送8位数据后还有第9个时钟
I2C以字节为基本传输单位。
一个完整字节包含8位数据:
D7 D6 D5 D4 D3 D2 D1 D0发送完D0后,主机还会产生第9个时钟,用于让接收方返回应答。
完整过程如下:
第1个时钟:D7 第2个时钟:D6 第3个时钟:D5 第4个时钟:D4 第5个时钟:D3 第6个时钟:D2 第7个时钟:D1 第8个时钟:D0 第9个时钟:ACK或NACK因此,第9位不是数据位,而是应答位。
在分析逻辑分析仪波形时,不要把第9个时钟误认为下一个字节的数据。
八、7位地址和8位地址的关系
I2C地址是开发过程中最容易混淆的部分之一。
很多芯片手册给出的是7位地址,但驱动代码、寄存器或者逻辑分析仪中显示的可能是8位地址字节。
一个完整的I2C地址字节由以下两部分组成:
A6 A5 A4 A3 A2 A1 A0 R/W其中:
A6~A0:7位从机地址 R/W:读写方向位读写方向位的定义为:
R/W = 0:写操作 R/W = 1:读操作因此,总线上真正发送的地址字节,本质上是:
7位从机地址左移1位,再拼接R/W位。九、地址0x20为什么会变成0x40和0x41
假设某个I2C设备的7位地址为:
0x201. 写地址字节
写操作时,R/W位为0。
计算方式:
0x20<<1计算结果:
0x40所以写地址字节为:
0x402. 读地址字节
读操作时,R/W位为1。
计算方式:
(0x20<<1)|0x01计算结果:
0x41所以读地址字节为:
0x41三者之间的关系如下:
7位设备地址:0x20 写地址字节:0x40 读地址字节:0x41对应的二进制形式为:
0x20 = 010 0000 0x40 = 0100 0000 0x41 = 0100 0001代码示例:
uint8_tslave_addr=0x20;uint8_twrite_addr=slave_addr<<1;uint8_tread_addr=(slave_addr<<1)|0x01;运行结果:
write_addr = 0x40 read_addr = 0x41图3 I2C的7位地址、写地址和读地址
从图中可以看到,0x40和0x41通常不是两个不同的I2C设备地址,而是同一个7位地址0x20在写操作和读操作下的两种总线表示。
十、地址到底要不要左移
地址是否需要左移,取决于具体驱动接口的要求。
有些驱动接口要求调用者传入7位地址。
例如:
i2c_write(0x20,data,length);这种情况下,驱动内部可能会自动完成:
地址左移1位 拼接R/W方向位有些底层接口则要求调用者直接传入完整的地址字节。
例如:
i2c_master_addressing(I2C0,0x40,I2C_TRANSMITTER);如果接口内部已经会自动处理地址,而调用者又提前左移一次,就会导致地址错误。
例如,原始7位地址为:
0x20调用者错误地先转换为:
0x40驱动内部又执行一次左移:
0x40 << 1 = 0x80这样总线上发送的地址就会错误,从机无法返回ACK。
调试地址问题时,需要确认以下内容:
- 芯片手册给出的是7位地址还是8位地址字节;
- 驱动函数要求传入7位地址还是完整地址字节;
- 驱动内部是否会自动左移;
- 驱动内部是否会自动拼接R/W位;
- 逻辑分析仪显示的是7位地址还是原始地址字节。
十一、主机写一帧数据的完整流程
一次基本的主机写操作通常包含以下步骤:
1. 等待总线空闲 2. 产生START 3. 发送从机地址和写方向位 4. 等待从机ACK 5. 发送第1个数据字节 6. 等待从机ACK 7. 继续发送后续数据字节 8. 每个字节后等待ACK 9. 产生STOP假设从机7位地址为0x20,主机准备发送两个数据字节。
通信过程可以表示为:
START 0x40 ACK DATA1 ACK DATA2 ACK STOP其中:
0x40 = 0x20 << 10x40的最低位为0,表示当前为写操作。
每发送一个字节后,从机都会通过ACK告诉主机:
当前字节已经接收完成,可以继续发送。如果地址发送后立即出现NACK,应优先检查:
- 地址是否正确;
- 是否混淆7位地址和8位地址;
- 地址是否被重复左移;
- 读写方向位是否正确;
- 从机是否上电;
- 从机是否完成初始化;
- SDA和SCL是否接反;
- 总线上是否存在上拉电阻;
- 从机是否处于可通信状态。
十二、主机读一帧数据的完整流程
一次基本的主机读操作通常包含以下步骤:
1. 等待总线空闲 2. 产生START 3. 发送从机地址和读方向位 4. 等待从机ACK 5. 从机发送数据 6. 主机返回ACK或NACK 7. 产生STOP从地址为0x20的设备读取一个字节时,可以表示为:
START 0x41 ACK DATA NACK STOP其中:
0x41 = (0x20 << 1) | 0x01主机读取最后一个字节后,需要返回NACK,告诉从机:
读取已经结束,不需要继续发送数据。随后主机产生STOP。
连续读取多个字节
假设主机连续读取3个字节,通信过程通常为:
START 读地址 ACK DATA1 主机ACK DATA2 主机ACK DATA3 主机NACK STOP前两个字节后,主机返回ACK,表示继续读取。
最后一个字节后,主机返回NACK,表示当前读取结束。
十三、为什么读取寄存器时经常要先写后读
很多I2C从机内部包含多个寄存器。
主机在读取某个寄存器之前,必须先告诉从机:
我要读取哪个寄存器。因此,常见的寄存器读取流程分为两个阶段。
1. 阶段A:发送寄存器地址
START 从机写地址 ACK 寄存器地址或命令字 ACK2. 阶段B:读取寄存器数据
Repeated START 从机读地址 ACK 从机返回数据 主机NACK STOP完整流程如下:
START 从机写地址 ACK 寄存器地址 ACK Repeated START 从机读地址 ACK 数据 NACK STOPRepeated START称为重复起始信号。
它可以在不释放总线的情况下,重新发起一次地址传输,并将通信方向从写切换为读。
部分设备也允许下面的通信方式:
START 写地址 寄存器地址 STOP 延时一段时间 START 读地址 读取数据 STOP到底使用STOP还是Repeated START,需要查看具体从机芯片的数据手册和通信协议。
图4 一帧I2C通信的完整流程
从总线空闲开始,主机依次产生START、发送地址、等待ACK、发送或接收数据,最后产生STOP。对于寄存器查询类命令,通常需要先写入寄存器地址,再发起读取。
十四、一帧主机写通信的代码流程
下面以主机发送数据为例,说明代码中的主要步骤。
/* 等待总线空闲 */while(i2c_flag_get(I2C0,I2C_FLAG_I2CBSY)){}/* 产生START */i2c_start_on_bus(I2C0);/* 等待START发送完成 */while(!i2c_flag_get(I2C0,I2C_FLAG_SBSEND)){}/* 发送从机地址,方向为写 */i2c_master_addressing(I2C0,slave_addr,I2C_TRANSMITTER);/* 等待地址发送完成 */while(!i2c_flag_get(I2C0,I2C_FLAG_ADDSEND)){}/* 清除地址发送完成标志 */i2c_flag_clear(I2C0,I2C_FLAG_ADDSEND);/* 发送数据 */for(uint8_ti=0;i<length;i++){while(!i2c_flag_get(I2C0,I2C_FLAG_TBE)){}i2c_data_transmit(I2C0,buffer[i]);}/* 等待最后一个字节传输完成 */while(!i2c_flag_get(I2C0,I2C_FLAG_BTC)){}/* 产生STOP */i2c_stop_on_bus(I2C0);需要注意,不同MCU和不同固件库的函数名、标志名以及清除方式可能不同,应以当前芯片的参考手册和库函数说明为准。
十五、I2C中断是怎么产生的
I2C中断并不是总线上每出现一次电平变化,CPU就进入一次中断。
SCL时钟、地址移位和数据移位通常由I2C硬件外设自动完成。
CPU是否进入中断,一般取决于两个条件:
对应状态标志位置位 并且 对应中断使能位已经打开可以简化理解为:
中断请求 = 状态标志有效 && 对应中断已使能例如,发送缓冲区为空时,TBE标志可能置位。
只有在TBE中断被使能的情况下,才会向CPU请求进入对应的I2C中断服务程序。
因此,不应该简单理解成:
SCL每跳变一次,CPU就进入一次中断。SCL的每一个高低电平变化,一般都是由I2C硬件自动产生的。
十六、常见I2C状态和中断标志
在GD32、STM32等MCU中,经常可以看到以下I2C状态标志:
SBSEND ADDSEND TBE RBNE BTC AERR不同芯片中的名称和具体定义可能略有区别,最终应以当前MCU参考手册为准。
1. SBSEND
SBSEND通常表示:
START起始条件已经发送完成。对应波形为:
SCL高电平时,SDA由高变低。主机检测到SBSEND后,通常可以继续发送从机地址。
代码示例:
if(i2c_flag_get(I2C0,I2C_FLAG_SBSEND)){i2c_master_addressing(I2C0,slave_addr,I2C_TRANSMITTER);}2. ADDSEND
在主机模式下,ADDSEND通常表示:
地址阶段已经完成,并且从机已经返回ACK。在从机模式下,ADDSEND也可能表示:
从机地址已经匹配。主机地址发送后,如果从机没有返回ACK,程序通常不会正常进入ADDSEND流程,而可能产生AERR应答错误。
3. TBE
TBE通常表示:
发送数据寄存器为空,可以写入下一个字节。代码示例:
if(i2c_flag_get(I2C0,I2C_FLAG_TBE)){i2c_data_transmit(I2C0,tx_buffer[index]);index++;}需要注意,TBE主要表示发送数据寄存器可以继续写入。
它不一定表示当前字节连同ACK阶段都已经完全结束。
4. BTC
BTC通常表示:
当前字节以及对应的应答阶段已经完成。发送最后一个字节后,程序经常等待BTC,再产生STOP。
代码示例:
while(!i2c_flag_get(I2C0,I2C_FLAG_BTC)){}i2c_stop_on_bus(I2C0);TBE和BTC的含义不能完全等同:
TBE:发送数据寄存器为空,可以装入下一个字节。 BTC:当前字节以及应答阶段已经完成。5. RBNE
RBNE通常表示:
接收数据寄存器非空,已经收到一个新字节。程序需要及时读取接收数据寄存器。
代码示例:
if(i2c_flag_get(I2C0,I2C_FLAG_RBNE)){rx_buffer[index]=i2c_data_receive(I2C0);index++;}读取数据寄存器后,RBNE通常会被清除。
如果RBNE置位后一直不读取数据,可能导致:
- 中断持续触发;
- 接收缓冲区溢出;
- CPU频繁进入中断;
- 其他任务无法及时运行;
- 系统表现为卡死或响应变慢。
6. AERR
AERR通常表示:
地址或数据发送后,没有收到接收方的ACK。地址阶段出现AERR时,应重点检查:
- 从机地址是否正确;
- 地址是否被重复左移;
- 读写方向位是否正确;
- 从机是否已经上电;
- 从机是否在线;
- SDA和SCL接线是否正确;
- 总线上拉电阻是否存在;
- 从机是否处于可响应状态。
图5 I2C状态标志与实际波形阶段的对应关系
SBSEND对应START发送完成,ADDSEND对应地址阶段完成,TBE表示发送寄存器可以写入新数据,RBNE表示接收到新字节,BTC表示字节传输完成,AERR通常表示没有收到ACK。
十七、发送一个字节是否一定进入一次中断
答案是不一定。
在很多简单的I2C中断发送程序中,可能表现为:
发送一个字节 TBE置位 进入I2C中断 装载下一个字节因此,看起来像是发送一个字节就进入一次中断。
但是实际中断次数还与以下因素有关:
- MCU的I2C外设结构;
- 启用了哪些中断源;
- 是否使用发送FIFO;
- 是否使用接收FIFO;
- 是否使用DMA;
- 多个状态是否共用一个中断入口;
- 中断服务程序一次处理几个状态;
- 中断进入之前是否已经有多个标志同时置位。
CPU进入一次I2C中断后,可能会同时检查多个状态标志。
例如:
voidI2C0_EV_IRQHandler(void){if(i2c_flag_get(I2C0,I2C_FLAG_SBSEND)){/* 发送地址 */}if(i2c_flag_get(I2C0,I2C_FLAG_ADDSEND)){/* 处理地址发送完成 */}if(i2c_flag_get(I2C0,I2C_FLAG_TBE)){/* 发送下一个字节 */}if(i2c_flag_get(I2C0,I2C_FLAG_RBNE)){/* 读取接收数据 */}}因此,更准确的理解是:
I2C硬件在不同通信阶段设置状态标志。 当对应中断被使能时,CPU才可能进入中断服务程序。不能简单认为I2C总线上出现一个动作,就一定对应一次固定中断。
十八、逻辑分析仪抓取I2C波形的正确顺序
拿到一段I2C波形后,不建议一开始就直接分析数据内容。
更合理的分析顺序如下。
1. 检查SCL是否正常
首先观察SCL:
- 时钟是否连续;
- 频率是否符合预期;
- 高低电平是否正常;
- 是否存在异常拉长的低电平;
- 是否存在明显毛刺;
- 上升沿和下降沿是否过慢。
如果SCL本身不正常,后续分析地址和数据通常没有意义。
2. 寻找START和STOP
START的特征为:
SCL高电平时,SDA由高变低。STOP的特征为:
SCL高电平时,SDA由低变高。先确定一帧通信从哪里开始,到哪里结束。
3. 识别地址字节和R/W位
START之后的第一个字节通常为地址字节。
需要确认:
- 高7位是否为目标从机地址;
- 最低位是读还是写;
- 工具显示的是7位地址还是完整地址字节。
4. 检查第9个时钟的ACK或NACK
每发送一个地址或数据字节后,都要检查第9个时钟。
如果地址后立即NACK,应优先检查:
- 地址;
- 电源;
- 接线;
- 上拉电阻;
- 从机状态。
5. 分析数据内容
确认地址和ACK正常后,再分析实际数据:
- 数据顺序是否正确;
- 字节长度是否正确;
- 大小端是否正确;
- 命令字是否正确;
- 返回数据是否符合协议;
- 是否包含CRC或PEC;
- 字节间隔是否满足协议要求。
十九、地址后一直NACK怎么排查
如果逻辑分析仪显示地址后一直NACK,可以按照下面的顺序排查。
1. 检查地址格式
确认当前使用的是:
7位地址 还是 8位地址字节例如设备7位地址为0x20:
7位地址:0x20 写地址:0x40 读地址:0x41不要把0x40再次左移。
2. 检查R/W方向
如果当前准备向从机写入命令,地址最低位应该为0。
如果当前准备从从机读取数据,地址最低位应该为1。
3. 检查从机供电
确认:
- 从机电源电压正常;
- 从机地线与主机共地;
- 从机复位引脚状态正常;
- 从机已经完成上电初始化。
4. 检查SDA和SCL接线
确认:
- SDA连接到SDA;
- SCL连接到SCL;
- 没有接反;
- 引脚复用配置正确;
- GPIO没有被其他模块占用。
5. 检查上拉电阻
确认SDA和SCL是否连接了合适的上拉电阻。
没有上拉电阻时,I2C总线可能无法正常恢复高电平。
6. 检查从机地址配置
部分从机具有地址配置引脚,例如:
A0 A1 A2这些引脚的高低电平会影响最终I2C地址。
需要根据原理图和数据手册确认实际地址。
二十、SDA一直被拉低怎么排查
SDA一直为低电平,通常表示总线没有正常释放。
可能原因包括:
- 从机状态机卡在未完成的通信中;
- 上一次通信没有产生STOP;
- 主机在通信过程中异常复位;
- 从机异常拉住SDA;
- SDA线路短路;
- GPIO模式配置错误;
- 引脚复用配置错误;
- 从机仍在等待后续时钟。
部分情况下,可以通过GPIO手动发送若干个SCL时钟,帮助从机退出未完成的接收状态。
常见恢复思路如下:
1. 临时把SCL配置为GPIO输出。 2. 保持SDA释放。 3. 手动输出9个左右的SCL脉冲。 4. 尝试产生STOP。 5. 重新初始化I2C外设。该方法是否适用,需要结合具体从机协议和硬件情况判断。
二十一、总线一直BUSY怎么排查
总线一直BUSY可能由以下原因引起:
- SDA没有恢复高电平;
- SCL被某个器件拉低;
- 上一次通信异常结束;
- STOP没有正确产生;
- I2C外设状态异常;
- GPIO复用配置错误;
- 主机初始化顺序不正确;
- 从机处于异常状态。
建议按照下面的顺序检查:
1. 用逻辑分析仪观察SDA和SCL实际电平。 2. 检查SDA是否被持续拉低。 3. 检查SCL是否被持续拉低。 4. 检查上一次通信是否存在STOP。 5. 检查GPIO复用和开漏配置。 6. 尝试复位I2C外设。 7. 必要时执行总线恢复操作。二十二、为什么I2C需要上拉电阻
I2C的SDA和SCL通常采用开漏输出结构。
开漏输出的特点是:
设备可以主动输出低电平。 设备通常不能主动输出高电平。信号线恢复为高电平,需要依靠外部上拉电阻。
上拉电阻会影响:
- 信号上升沿速度;
- 总线最高通信频率;
- 电平稳定性;
- 总线功耗;
- 设备拉低信号时的电流;
- 多设备通信可靠性。
常见的I2C上拉电阻可能为:
2.2kΩ 4.7kΩ 10kΩ但实际阻值不能只靠经验固定选择,需要结合以下因素:
- I2C总线电压;
- 总线电容;
- PCB走线长度;
- 从机数量;
- 通信频率;
- 芯片允许的低电平灌电流。
上拉电阻过大时,信号上升沿可能过慢。
上拉电阻过小时,设备把总线拉低时的电流会增大。
逻辑分析仪主要观察数字逻辑状态。
如果怀疑信号存在上升沿过慢、振铃、过冲或者电压幅值异常,建议使用示波器观察真实模拟波形。
二十三、波形正常但数据错误怎么排查
如果START、地址、ACK和STOP看起来都正常,但数据内容不符合预期,可以重点检查以下内容:
- 命令字是否正确;
- 寄存器地址是否正确;
- 数据长度是否一致;
- 字节顺序是否正确;
- 大小端是否正确;
- 是否存在CRC;
- 是否存在PEC;
- 校验和计算是否正确;
- 数据结构是否存在对齐问题;
- 主机和从机使用的协议版本是否一致;
- 连续读取时寄存器是否自动递增;
- 是否需要等待从机处理命令;
- 写命令后是否需要延时再读取结果。
例如,一个16位数据可能按下面两种顺序发送。
大端格式:
高字节 低字节小端格式:
低字节 高字节如果主从双方对大小端理解不一致,就会出现波形正常但数据错误的情况。
二十四、如何把代码、标志位和波形对应起来
调试I2C时,建议同时观察以下三类信息。
1. 软件执行流程
等待BUSY清除 产生START 等待SBSEND 发送地址 等待ADDSEND 发送数据 等待TBE或BTC 产生STOP2. I2C状态标志
SBSEND ADDSEND TBE BTC RBNE AERR3. 逻辑分析仪波形
START 地址 ACK 数据 ACK STOP把三者对应起来后,问题会更容易定位。
例如,程序一直等待ADDSEND,而逻辑分析仪显示地址后为NACK。
这时问题通常不在等待循环本身,而更可能是:
- 从机地址错误;
- 地址格式错误;
- 从机没有上电;
- 从机没有应答;
- SDA或SCL连接异常;
- 上拉电阻异常。
再例如,程序频繁进入RBNE中断,但没有读取接收数据寄存器。
这可能导致:
- RBNE持续保持;
- CPU频繁进入中断;
- 接收缓冲区溢出;
- 其他任务得不到执行;
- 系统表现为卡死。
因此,分析I2C问题时不能只看代码,也不能只看逻辑分析仪。
最有效的方法是:
程序执行流程 + I2C状态标志 + 实际总线波形三者同时进行对照。
二十五、I2C调试的推荐步骤
实际项目中,可以按照以下顺序排查I2C问题。
第1步:确认从机供电和共地正常。 第2步:确认SDA、SCL接线和GPIO复用正确。 第3步:确认总线上存在上拉电阻。 第4步:确认总线空闲时SDA、SCL都为高电平。 第5步:检查SCL频率是否符合从机要求。 第6步:寻找START和STOP。 第7步:确认地址字节和R/W位。 第8步:检查地址后的ACK或NACK。 第9步:检查每个数据字节后的ACK或NACK。 第10步:检查数据顺序、长度和校验。 第11步:把波形与代码中的状态标志对应起来。 第12步:必要时使用示波器检查信号质量。不要一开始就直接分析数据内容。
如果地址阶段已经NACK,后面的数据实际上并没有被从机正常接收。
二十六、核心知识总结
学习I2C时,可以先记住以下结论:
I2C主要由SCL和SDA两根信号线组成。
SCL低电平期间,SDA可以变化。
SCL高电平期间,SDA应保持稳定。
SCL高电平时,SDA由高变低表示START。
SCL高电平时,SDA由低变高表示STOP。
每发送8位地址或数据后,第9个时钟用于ACK或NACK。
ACK表示接收方在第9个时钟期间把SDA拉低。
NACK表示第9个时钟期间SDA保持高电平。
7位地址左移1位后,再拼接R/W位,形成总线地址字节。
7位地址0x20对应写地址字节0x40和读地址字节0x41。
主机读取最后一个字节后,通常返回NACK,再产生STOP。
查询寄存器时,通常需要先写入寄存器地址,再发起读取。
SBSEND通常表示START已经发送完成。
ADDSEND通常表示地址阶段已经完成。
TBE表示发送数据寄存器为空,可以写入下一个字节。
RBNE表示接收数据寄存器非空,需要及时读取数据。
BTC通常表示当前字节及应答阶段已经完成。
AERR通常表示地址或数据发送后没有收到ACK。
I2C中断由状态标志和中断使能共同决定。
发送一个字节可能触发一次中断,但不是所有控制器都固定如此。
调试波形时,应按照SCL、START、地址、ACK、数据、STOP的顺序分析。
地址后立即NACK,应优先检查地址、电源、接线、上拉电阻和从机状态。
二十七、结语
真正掌握I2C,不只是会调用发送和接收函数,而是能够理解:
代码为什么停在当前状态 状态标志为什么会置位 总线上实际发生了什么波形当程序执行流程、I2C状态标志和逻辑分析仪波形能够一一对应时,就可以快速判断问题发生在:
- 总线空闲阶段;
- START阶段;
- 地址阶段;
- ACK阶段;
- 数据阶段;
- STOP阶段;
- 中断处理阶段。
这也是从“会调用I2C接口”走向“能够独立调试I2C驱动”的关键一步。
---