1. I2C总线协议深度解析:从两根线到嵌入式通信基石
搞嵌入式开发,尤其是和传感器、EEPROM、RTC这些外设打交道,I2C总线绝对是绕不开的一道坎。它不像SPI那样需要四根线,也不像UART那样需要事先约定好波特率,两根线(SDA数据线、SCL时钟线)就能搞定多设备通信,硬件成本低,布线简单,听起来很美好。但真正用起来,特别是当你需要在一个复杂的系统中稳定、高效地使用它时,你会发现手册里那些时序图、寄存器描述背后,藏着不少“坑”。今天,我就结合自己多年在TI C2000系列MCU,特别是TMS320F28003x上的实战经验,把I2C从最底层的电气特性到上层的软件驱动,掰开揉碎了讲清楚。无论你是刚接触I2C的新手,还是想深入理解F28003x I2C模块特性的老手,这篇文章都能给你带来实实在在的干货。
I2C的核心思想是“共享总线,主从协作”。想象一下一个会议室(总线),里面有多个人(从设备),但只有一个主持人(主设备)有话筒(SCL时钟线)。主持人说“现在请7号位(从设备地址)发言”,并把话筒递过去,7号位的人才能通过公共的发言通道(SDA数据线)说话。其他人虽然都听着,但只有被点到名的才能回应。这就是I2C最基本的“主-从”通信模型。TMS320F28003x的I2C模块,既可以当这个“主持人”(主模式),也可以当“参会者”(从模式),甚至能支持多个“主持人”轮流主持(多主模式),功能相当全面。接下来,我们就从最基础的信号讲起,一步步深入到F28003x的具体应用。
1.1 I2C的物理层:开漏输出与线与逻辑
为什么I2C只需要两根线就能实现双向通信?秘诀就在于它的物理层采用了“开漏输出”结构。无论是主设备的SCL、SDA引脚,还是从设备的,其内部驱动电路可以等效为一个连接到地的开关(MOS管)。当这个开关闭合时,总线被拉低到低电平(逻辑0);当开关断开时,总线并不被驱动到高电平,而是依靠外接的上拉电阻拉到电源电压(逻辑1)。
注意:这个外接的上拉电阻是必须的!没有它,总线就无法回到高电平。电阻值的选择是个学问,太小则电流大、功耗高,在低电平期间可能超出驱动器的灌电流能力;太大则上升沿太慢,可能无法满足高速模式下的时序要求。TI的应用报告《I2C Bus Pullup Resistor Calculation》是必读材料。对于常见的3.3V系统,在标准模式(100kbps)下,4.7kΩ是个不错的起点;快速模式(400kbps)下,可能需要减小到2.2kΩ甚至更低,具体需根据总线电容计算。
这种“开漏+上拉”的结构带来了一个关键特性:“线与”(Wired-AND)。任何连接到总线上的设备,只要它输出低电平,整条线就是低电平;只有当所有设备都输出高阻态(开关断开)时,总线才是高电平。这为实现“时钟同步”和“仲裁”机制奠定了硬件基础。如果两个主设备同时开始传输,它们会在产生SCL时钟的同时监听SDA线上的实际电平。如果某个主设备输出高电平(试图释放总线),但检测到SDA线是低电平(被另一个主设备拉低),它就意识到发生了冲突,并立即退出竞争,这就是仲裁过程。
1.2 协议层:起始、停止、应答与数据有效性
理解了物理层,再看协议层的几个关键信号就清晰了。
起始(S)和停止(P)条件:这是主设备控制总线所有权的标志。起始条件定义为:在SCL为高电平期间,SDA线发生一个从高到低的跳变。停止条件则相反:在SCL为高电平期间,SDA线发生从低到高的跳变。在F28003x中,通过配置I2CMDR寄存器的STT和STP位来产生这些条件。这里有个极易出错的地方:模块在复位状态(IRS=0)时是无法检测总线上的起始/停止条件的。因此,在初始化模块(将IRS置1)后,必须等待一段时间(确保总线上至少发生了一次起始或停止条件),再去读取I2CSTR寄存器中的BB(Bus Busy)位来获取正确的总线状态,然后才能发起自己的传输。否则,你可能在总线忙的时候误发起传输,导致仲裁失败或通信混乱。
数据有效性:I2C协议规定,SDA线上的数据必须在SCL的高电平期间保持稳定。数据的变化只能发生在SCL为低电平期间。这意味着时钟线掌控着数据采样的节奏。在F28003x中,作为主设备时,模块内部的可编程时钟分频器(由I2CPSC、I2CCLKL、I2CCLKH寄存器控制)负责产生这个精准的SCL时钟;作为从设备时,则严格遵循主设备提供的时钟。
应答(ACK)机制:这是I2C可靠性的重要保障。每个字节(8位数据)传输后,都会跟一个额外的时钟脉冲(第9个脉冲)。在这个脉冲期间,发送方(无论是主还是从)会释放SDA线(输出高阻),而接收方则需要将SDA线拉低,以此表示“字节已收到”(ACK)。如果接收方没有拉低(SDA保持高),则表示“非应答”(NACK),通常意味着传输结束或出错。在F28003x中,作为接收方时,模块硬件会自动在收到每个字节后发出ACK(除非软件配置为NACK);作为发送方时,则需要通过检查I2CSTR寄存器中的NACK位来判断对方是否应答。
2. TMS320F28003x I2C模块架构与核心配置
F28003x的I2C模块是一个高度集成化的数字外设,它不仅仅实现了I2C协议的物理层和链路层,还提供了许多便利的功能来减轻CPU负担。理解其内部架构,是进行正确配置和高效编程的前提。
2.1 模块内部数据流与FIFO
模块的核心数据通路围绕着几个关键寄存器展开,其非FIFO模式下的数据流如图26-2所示(参考技术手册):
- 发送路径:CPU将待发送数据写入I2CDXR(数据发送寄存器)。当发送移位寄存器I2CXSR为空时,数据从I2CDXR自动加载到I2CXSR,然后由硬件控制,在SCL时钟的同步下,一位一位地通过SDA引脚发送出去。
- 接收路径:从SDA引脚接收到的串行数据,首先被移入I2CRSR(接收移位寄存器)。当一个完整的数据字节接收完毕后,数据会自动从I2CRSR拷贝到I2CDRR(数据接收寄存器),等待CPU读取。
为了提高数据传输效率,F28003x的I2C模块内置了16级深度的发送FIFO(TX FIFO)和接收FIFO(RX FIFO)。这是一个巨大的性能提升点。在非FIFO模式下,每发送或接收一个字节都可能产生中断,CPU频繁被中断打扰。启用FIFO后,你可以一次性写入最多16个字节到TX FIFO,模块会按顺序自动发送;同样,可以连续接收最多16个字节到RX FIFO,再一次性读取。你可以通过设置SCIFFTX和SCIFFRX寄存器中的中断级别(如设置TXFFIL=8),让FIFO半满或达到特定深度时才产生中断,从而大幅降低中断频率,提升系统效率。
2.2 时钟系统:精度与速度的源头
I2C通信的时序精度完全依赖于时钟。F28003x的I2C模块时钟生成路径如图26-3所示,分为两级分频:
- 模块时钟分频:系统时钟SYSCLK首先经过一个可编程预分频器(I2CPSC寄存器)。分频后的频率即为I2C模块的工作时钟
Fmod。手册中明确强调,为了满足所有I2C协议时序规范,Fmod必须在7-12 MHz之间。这是一个硬性约束,计算分频值时必须保证。Fmod = SYSCLK / (I2CPSC.IPSC + 1) - 主时钟(SCL)分频:当模块作为主设备时,需要产生SCL时钟。SCL的高低电平时间由I2CCLKL(低电平时间)和I2CCLKH(高电平时间)两个寄存器分别控制。SCL的周期
Tmst计算公式为:Tmst = Tmod * [(ICCH + d) + (ICCL + d)]其中,Tmod是模块时钟周期,ICCH和ICCL分别是高、低电平分频系数,d是一个与IPSC值相关的固定延迟(见手册表26-1)。这里的d是TI硬件设计引入的固有延迟,用于满足建立/保持时间,计算波特率时必须考虑进去,否则实际通信速率会与预期有偏差。
实操心得:配置波特率(如400kbps)时,建议使用TI提供的DriverLib库函数SCI_setBaud(),它会帮你完成这些繁琐的计算。如果必须手动计算,步骤是:先根据SYSCLK和7-12MHz约束确定IPSC,得到Fmod;然后根据目标SCL频率,结合公式和d值,反算出合适的ICCH和ICCL值。通常设置ICCH = ICCL可以得到占空比50%的时钟。
2.3 关键寄存器精讲与DriverLib映射
手动操作寄存器是理解底层原理的好方法,但在实际项目开发中,使用TI提供的DriverLib库函数能极大提高开发效率和代码可读性、可维护性。手册中的表25-21清晰地列出了寄存器与库函数的映射关系。
以SCICTL1寄存器为例,它控制着模块的基本使能、软件复位、休眠模式等。对应的DriverLib函数有:
SCI_enableModule()/SCI_disableModule(): 使能/禁用整个I2C模块。注意:在修改大部分配置(如波特率、地址)前,应先禁用模块(IRS=0),配置完成后再使能。SCI_performSoftwareReset(): 执行软件复位。这是一个快速将模块恢复到已知状态的方法,但如前所述,复位后会丢失总线状态(BB位),需要重新同步。SCI_enableTxModule()/SCI_enableRxModule(): 分别使能发送器和接收器。在从模式下,通常需要根据主设备的命令来动态切换。
再比如SCIPRI寄存器,它只有一个关键的FREESOFT字段(位4-3)。这个寄存器决定了当仿真器挂起事件(例如你在CCS中命中一个断点)时,I2C模块的行为。这在调试实时通信系统时至关重要。
00(立即停止):一旦调试器暂停CPU,I2C模块也立即停止。这可能导致一个不完整的字节停留在总线上,造成从设备困惑。01(完成当前收/发序列后停止):这是最安全、最推荐的调试设置。模块会完成当前正在传输或接收的整个字节序列(由I2CCNT或重复模式决定)后再停止,避免破坏总线上的数据包。10/11(自由运行):模块完全不受调试器影响,继续工作。这在观察实时数据流时有用,但如果代码停在修改I2C配置的地方,可能会引发问题。
强烈建议:在初始化阶段,通过SCI_setConfig()函数进行整体配置,它封装了波特率、数据位、校验位、地址模式等多个参数的设置。对于FREESOFT这类特殊寄存器,DriverLib可能没有直接函数,需要直接操作寄存器:HWREGH(I2CA_BASE + I2C_O_PRI) = 0x0010;// 设置FREESOFT=01b。
3. F28003x I2C实战:主从模式配置与数据收发
理论说得再多,不如一行代码。我们直接进入实战环节,分别看看如何将F28003x配置为主设备和从设备,并进行数据通信。
3.1 主设备模式配置与传输流程
将F28003x配置为I2C主设备,通常是为了主动读取传感器数据或向存储器写入配置。流程可以参照手册中的图26-7。
步骤1:引脚与模块初始化
#include "driverlib.h” void I2CA_InitMaster(void) { // 1. 配置GPIO引脚为I2C功能 (例如GPIO18-SCIA, GPIO19-SDAA) // 先配置MUX,再配置Qualification为异步,避免毛刺 GPIO_setPinConfig(GPIO_18_I2CA_SCL); GPIO_setPinConfig(GPIO_19_I2CA_SDA); GPIO_setQualificationMode(18, GPIO_QUAL_ASYNC); // SCL GPIO_setQualificationMode(19, GPIO_QUAL_ASYNC); // SDA // 2. 初始化I2C模块,配置为主模式,7位地址,100kbps波特率 I2C_initMaster(I2CA_BASE, DEVICE_SYSCLK_FREQ, 100000, I2C_DUTYCYCLE_50); // 3. 使能I2C模块 I2C_enableModule(I2CA_BASE); // 4. 等待总线空闲(重要!) while(I2C_isBusBusy(I2CA_BASE)); }步骤2:主设备发送数据(写操作)向一个从设备(假设地址0x50)写入两个字节数据{0x00, 0xAA}。
void I2CA_WriteData(uint16_t slaveAddr, uint8_t *data, uint16_t dataSize) { // 1. 设置从设备地址和传输字节数(非重复模式) I2C_setSlaveAddress(I2CA_BASE, slaveAddr); // 0x50 << 1 (7位地址左移1位) I2C_setDataCount(I2CA_BASE, dataSize); // 设置I2CCNT // 2. 配置为主发送器,产生起始条件,并在传输结束后产生停止条件 I2C_setConfig(I2CA_BASE, I2C_MASTER_SEND_MODE); // 隐含了MST=1, TRX=1, STT=1, STP=1 // 3. 将数据逐个放入数据寄存器(或FIFO) for(uint16_t i = 0; i < dataSize; i++) { // 等待发送缓冲器就绪(非FIFO模式看TXRDY位,FIFO模式看状态) while(!I2C_getStatus(I2CA_BASE) & I2C_STAT_TXRDY); I2C_putData(I2CA_BASE, data[i]); } // 4. 等待传输完成(ARDY位置位)和停止条件产生 while(!I2C_getStatus(I2CA_BASE) & I2C_STAT_ARDY); // 可选:检查NACK错误 if(I2C_getStatus(I2CA_BASE) & I2C_STAT_NACK) { // 处理无应答错误 } }步骤3:主设备接收数据(读操作)从从设备(地址0x50)的某个寄存器(假设先写入寄存器地址0x01,再读取数据)读取3个字节。
void I2CA_ReadData(uint16_t slaveAddr, uint8_t regAddr, uint8_t *rxBuf, uint16_t rxSize) { // 1. 发送从设备地址和要读取的寄存器地址(写操作) I2C_setSlaveAddress(I2CA_BASE, slaveAddr); I2C_setDataCount(I2CA_BASE, 1); // 先发送1个字节(寄存器地址) I2C_setConfig(I2CA_BASE, I2C_MASTER_SEND_MODE); // MST=1, TRX=1, STT=1, STP=0 (不发停止位) I2C_putData(I2CA_BASE, regAddr); while(!I2C_getStatus(I2CA_BASE) & I2C_STAT_ARDY); // 等待地址发送完成 // 2. 发送重复起始条件,切换为接收模式 I2C_setSlaveAddress(I2CA_BASE, slaveAddr); I2C_setDataCount(I2CA_BASE, rxSize); // 准备接收rxSize个字节 I2C_setConfig(I2CA_BASE, I2C_MASTER_RECEIVE_MODE); // MST=1, TRX=0, STT=1, STP=1 // 3. 读取数据 for(uint16_t i = 0; i < rxSize; i++) { while(!I2C_getStatus(I2CA_BASE) & I2C_STAT_RXRDY); // 等待接收数据就绪 rxBuf[i] = I2C_getData(I2CA_BASE); } // 4. 等待整个传输完成 while(!I2C_getStatus(I2CA_BASE) & I2C_STAT_ARDY); }关键点解析:读操作通常分为“写寄存器地址”和“读数据”两个阶段,中间用“重复起始条件”(Repeated START)连接,而不是停止条件。这保证了主设备在切换方向时不释放总线控制权,防止其他主设备趁虚而入。在代码中,第一次发送设置
STP=0,第二次设置STP=1,模块会自动在中间产生重复起始条件。
3.2 从设备模式配置与响应机制
将F28003x配置为I2C从设备,可以让其他主设备(如另一个MCU或主机处理器)来读取它的数据或向其发送命令。
步骤1:从设备初始化
void I2CA_InitSlave(void) { // 1. 配置GPIO引脚(同主设备) GPIO_setPinConfig(GPIO_18_I2CA_SCL); GPIO_setPinConfig(GPIO_19_I2CA_SDA); GPIO_setQualificationMode(18, GPIO_QUAL_ASYNC); GPIO_setQualificationMode(19, GPIO_QUAL_ASYNC); // 2. 设置自身的7位从设备地址(例如0x68) I2C_setSlaveAddress(I2CA_BASE, 0x68); // 3. 不设置波特率(从设备时钟由主设备提供) // 4. 使能模块,并启用接收中断(准备接收地址) I2C_enableModule(I2CA_BASE); I2C_enableInterrupt(I2CA_BASE, I2C_INT_RX_IS_READY); // 使能接收数据就绪中断 I2C_clearInterruptStatus(I2CA_BASE, I2C_INT_RX_IS_READY); }步骤2:从设备中断服务程序(ISR)处理从设备的工作是事件驱动的,最适合用中断来处理。
__interrupt void I2CA_ISR(void) { uint16_t status = I2C_getInterruptSource(I2CA_BASE); // 1. 判断是否被寻址(作为从设备) if(status & I2C_INT_ARB_IS_LOST) { // 仲裁丢失,通常发生在多主系统,从设备无需处理,清中断即可 I2C_clearInterruptStatus(I2CA_BASE, I2C_INT_ARB_IS_LOST); } // 2. 接收到自身地址,并判断主设备是读还是写 if(status & I2C_INT_ADDR_IS_READY) { // 读取状态寄存器,判断数据方向 if(I2C_isStopDetected(I2CA_BASE)) { // 检测到停止条件,本次传输结束 } else { if(I2C_getStatus(I2CA_BASE) & I2C_STAT_TRX) { // TRX=1,主设备要写数据给本从设备(本设备为接收器) // 准备接收数据,使能接收中断 I2C_enableInterrupt(I2CA_BASE, I2C_INT_RX_IS_READY); } else { // TRX=0,主设备要从本从设备读数据(本设备为发送器) // 准备发送数据,将第一个数据写入I2CDXR,使能发送中断 I2C_putData(I2CA_BASE, myTxDataBuffer[txIndex++]); I2C_enableInterrupt(I2CA_BASE, I2C_INT_TX_IS_READY); } } I2C_clearInterruptStatus(I2CA_BASE, I2C_INT_ADDR_IS_READY); } // 3. 接收数据就绪中断 if(status & I2C_INT_RX_IS_READY) { uint8_t receivedData = I2C_getData(I2CA_BASE); // 处理接收到的数据... // 如果接收缓冲区满(RSFULL=1),需要及时读取数据,否则SCL会被拉低 I2C_clearInterruptStatus(I2CA_BASE, I2C_INT_RX_IS_READY); } // 4. 发送数据就绪中断(发送缓冲区空) if(status & I2C_INT_TX_IS_READY) { if(txIndex < txDataSize) { I2C_putData(I2CA_BASE, myTxDataBuffer[txIndex++]); } else { // 数据发送完毕,禁用发送中断,或发送最后一个字节后等待停止条件 I2C_disableInterrupt(I2CA_BASE, I2C_INT_TX_IS_READY); } I2C_clearInterruptStatus(I2CA_BASE, I2C_INT_TX_IS_READY); } // 5. 其他中断处理(NACK, STOP等) if(status & I2C_INT_NACK) { // 收到非应答,处理错误 I2C_clearInterruptStatus(I2CA_BASE, I2C_INT_NACK); } // ... 清除PIE中断标志 }从设备模式下的核心难点:状态机的管理。从设备需要根据主设备的命令(地址字节中的R/W位)在“接收器”和“发送器”角色间无缝切换。中断服务程序中的逻辑必须清晰,确保在正确的时间点(如地址匹配后)判断方向,并正确配置模块(TRX位)和准备数据。手册中的图26-6(从设备TX/RX流程图)是编写从设备代码的最佳参考。
4. 高级功能、调试技巧与避坑指南
掌握了基本的主从通信后,我们来看看F28003x I2C模块的一些高级特性,以及在实际项目中必然会遇到的“坑”和解决方法。
4.1 重复模式(Repeat Mode)与自由数据格式
重复模式(I2CMDR.RM = 1):在标准模式(非重复模式)下,传输的字节数由I2CCNT寄存器严格限定。而在重复模式下,I2CCNT被忽略,数据传输会一直持续,直到软件主动设置STP位产生停止条件,或设置新的STT位产生重复起始条件。这在传输数据流长度不确定或需要持续传输(如音频数据流)时非常有用。在重复模式下,每完成一个字节的传输或接收,ARDY位都会置位,可以用于触发中断或DMA请求。
自由数据格式(I2CMDR.FDF = 1):这是一种简化的格式,它去掉了地址帧。通信直接从数据字节开始,且整个传输过程中数据方向不变。这要求通信双方事先约定好角色(谁发谁收),并且都支持此格式。它适用于点对点、角色固定的简单通信场景。特别注意:自由数据格式不支持数字回环模式(DLB=1)。
4.2 FIFO与DMA的协同使用
对于大数据量传输,频繁的CPU中断仍然是瓶颈。F28003x的I2C模块支持与DMA控制器联动,实现数据搬运的完全硬件自动化。
配置流程:
- 配置I2C FIFO:通过
SCI_enableFIFO()使能TX/RX FIFO,并设置合适的中断水位(SCI_setFIFOInterruptLevel)。例如,设置TX FIFO中断在空时触发,RX FIFO中断在半满时触发。 - 配置DMA:将DMA的触发源(TRIG)设置为对应的I2C事件,如
DMA_TRIGGER_I2CAINTX(对应TX FIFO空)和DMA_TRIGGER_I2CAINTRX(对应RX FIFO有数据)。 - 建立传输:对于发送,CPU只需将数据块首地址和长度配置给DMA,DMA会在TX FIFO空时自动将数据从内存搬移到I2C的发送FIFO。对于接收,DMA会在RX FIFO达到设定水位时,自动将数据从FIFO搬移到指定的内存区域。
优势:CPU被彻底解放,仅在DMA传输完成时产生一次中断进行处理。这对于需要高速、连续读写外部存储器(如EEPROM)或ADC数据流的应用至关重要。
4.3 常见问题排查与调试心得
在实际项目中,I2C通信失败是常态。下面是我总结的一个快速排查清单:
| 现象 | 可能原因 | 排查步骤与解决方法 |
|---|---|---|
| 通信完全无响应 | 1. 物理连接问题(线断、虚焊) 2. 上拉电阻缺失或阻值过大 3. 主/从设备地址错误 4. I2C模块未使能(IRS=0) 5. 引脚功能未正确映射 | 1. 用万用表测量SDA/SCL对地电压,正常应为电源电压(因上拉)。用示波器看是否有波形。 2. 确认上拉电阻已焊接,值在1kΩ-10kΩ之间(高速取小值)。 3. 确认7位地址左移1位后写入寄存器。用逻辑分析仪抓取起始条件后的第一个字节。 4. 检查I2CMDR寄存器的IRS位是否为1。 5. 检查GPIO的MUX配置寄存器是否设置为I2C功能。 |
| 能发送地址但无应答(NACK) | 1. 从设备地址错误或不存在 2. 从设备电源或复位异常 3. 从设备忙或处于不可访问状态 4. 时序不满足从设备要求(SCL频率太快) | 1. 核对从设备数据手册的地址(注意地址位的最低有效位是R/W位)。 2. 检查从设备的电源、复位引脚。 3. 有些设备(如EEPROM)写操作后需要几毫秒的写入周期(Write Cycle),期间会NACK。 4. 降低SCL时钟频率(如从400k降到100k)试试。检查I2C模块时钟 Fmod是否在7-12MHz范围内。 |
| 通信随机出错,数据错误 | 1. 总线干扰(长线、靠近噪声源) 2. 电源噪声 3. 从设备驱动能力不足 4. 软件未及时响应(FIFO溢出/下溢) | 1. 缩短总线长度,使用双绞线,远离电机、开关电源等噪声源。可在SDA/SCL上串联小电阻(如22Ω)并加对地小电容(如10pF)滤波。 2. 检查电源纹波,为MCU和从设备增加去耦电容。 3. 总线上挂载设备过多,总线电容过大,导致上升沿过慢。减小上拉电阻或使用I2C缓冲器。 4. 检查中断服务程序是否过长,是否及时读取RX FIFO数据或写入TX FIFO数据。启用FIFO并合理设置中断水位。检查RSFULL(接收满)和XSMT(发送空)状态位。 |
| 调试时命中断点导致通信失败 | 仿真挂起时I2C模块行为不当 | 将SCIPRI寄存器的FREESOFT位设置为01b(完成当前序列后停止)。这样在CCS中暂停时,I2C模块会完成当前字节的传输再停止,避免在比特中间停止导致总线挂死。 |
| 多主系统中仲裁丢失 | 多个主设备同时发起传输 | 检查代码逻辑,确保每个主设备在发起传输前都检测总线是否空闲(BB位)。仲裁丢失后,模块会自动切换到从接收模式,需要软件检测ARBL位并重新尝试发送。 |
一个真实的坑:我曾经遇到一个诡异的问题,主设备向某个从设备写数据总是失败,但读操作正常。用逻辑分析仪抓波形,发现地址和ACK都正常,但第一个数据字节后的ACK位,从设备没有拉低。排查良久,最终发现是从设备的寄存器地址字节发送后,主设备没有产生重复起始条件,而是产生了停止条件,导致从设备认为单次写操作已经结束。随后主设备再发读命令时,从设备已经不在预期的状态了。根本原因是代码中在写寄存器地址后,错误地将STP位设为了1。修正为在写地址后设置STP=0并紧跟重复起始条件后,问题解决。教训:仔细分析时序图,理解“写地址-读数据”这种复合操作必须使用重复起始条件,而不是“停止-起始”组合。
最后,关于调试工具,一个逻辑分析仪(如Saleae)或带I2C解码功能的示波器是必不可少的。它能直观地展示起始、停止、地址、数据、ACK/NACK每一个比特,是定位协议层问题最快的方法。而万用表则用于快速检查电源、上拉电压等基础电气条件。