
1. I2C总线协议深度解析从两根线到复杂通信搞嵌入式开发尤其是和传感器、存储芯片打交道I2C总线绝对是你绕不开的一道坎。它不像SPI那样需要四根线也不像UART那样需要事先约定好波特率两根线SDA数据线和SCL时钟线就能搞定主从设备之间的通信听起来很美好但实际用起来里面的门道可不少。今天我就结合自己这些年调试TMS320F2837xS这类C2000系列DSP的经验把I2C从最基础的原理到实际寄存器配置的坑给你一次性捋清楚。无论你是刚接触I2C的新手还是想深入理解其内部机制的老鸟这篇文章都能让你有所收获。I2C的核心价值在于它的极简主义和灵活性。在PCB空间寸土寸金、芯片引脚资源紧张的嵌入式设备里用最少的连线实现多设备通信这就是它的立身之本。它采用主从架构一个主设备Master可以控制时钟线SCL并与总线上多个具有唯一地址的从设备Slave通信。协议本身定义了数据的有效性、起始/停止条件、应答机制等确保了通信的可靠性。而像TMS320F2837xS这类现代微控制器其内置的I2C模块更是将许多复杂的状态机逻辑硬件化并通过丰富的寄存器如控制数据传输模式的I2CMDR管理仿真行为的SCIPRI等提供精细的控制能力让我们在软件层面可以更专注于应用逻辑。接下来我们就从协议基础开始一步步拆解。1.1 协议基础两线制下的秩序I2C总线只有两根线串行数据线SDA和串行时钟线SCL。这两根线都是开漏输出结构这意味着它们本身只能输出低电平或者高阻态。要实现高电平输出必须在总线上外接上拉电阻。这种设计天然支持“线与”功能只要任何一个设备将线拉低整条线就是低电平只有当所有设备都释放总线输出高阻态时上拉电阻才能把总线拉到高电平。这是实现多主设备仲裁和时钟同步的物理基础。数据通信以字节为单位进行但每个字节的长度可以是1到8位通过寄存器配置。数据传输时最高位MSB先行。每一个被传输的字节后面都必须跟随一个应答位ACK或非应答位NACK这个位由接收方产生。时钟信号SCL永远由主设备产生但在某些情况下比如从设备需要时间处理数据从设备可以通过将SCL拉低来“时钟延展”迫使主设备进入等待状态直到从设备释放SCL。这是理解I2C实时性的关键。通信的起始和结束由特定的信号序列定义。起始条件S是当SCL为高电平时SDA线上一个从高到低的跳变。停止条件P则是当SCL为高电平时SDA线上一个从低到高的跳变。在起始条件之后、停止条件之前总线被视为“忙”。这些条件都由主设备产生。1.2 主从模式与四种状态机I2C设备在通信中扮演的角色是动态的具体分为四种操作模式这完全取决于当前时刻谁是时钟的控制方以及数据的流向。主发送模式Master-Transmitter这是主设备发起通信时最常见的初始模式。主设备控制SCL并主动向总线上发送从设备地址和后续的数据。当你需要向一个EEPROM写入数据时你的MCU就处于这种模式。主接收模式Master-Receiver主设备在发送了从设备地址并指定了读操作后会切换到此模式。此时主设备仍然控制SCL但数据线SDA的控制权移交给了从设备主设备变为接收方负责读取从设备发出的数据。读取传感器数值就是典型场景。从接收模式Slave-Receiver从设备在上电后默认监听总线。当它检测到起始条件并发现主设备发送的地址与自己的地址匹配且读/写位指示为“写”时它就进入从接收模式准备接收主设备发来的数据。从发送模式Slave-Transmitter当从设备地址匹配且读/写位指示为“读”时从设备进入从发送模式。此时从设备需要按照主设备提供的时钟节拍将数据放到SDA线上。注意模式切换特别是主设备在发送地址后从发送模式切换到接收模式或者从设备根据命令切换是I2C编程中容易出错的地方。务必在状态寄存器如I2CSTR中确认当前状态后再进行下一步操作。1.3 寻址格式7位、10位与自由数据格式如何告诉总线你要和谁说话这就靠寻址格式。TMS320F2837xS的I2C模块支持三种格式通过I2CMDR寄存器的XA和FDF位来选择。7位寻址格式这是最常用、最经典的格式。起始条件后的第一个字节高7位是从设备地址最低位是读/写方向位R/W。0表示主设备写从设备读1表示主设备读从设备写。由于地址是7位理论上有128个地址但一些地址如0000XXX被保留用于特殊用途实际可用地址约112个。对于大多数传感器如BMP280、MPU6050和EEPROM如AT24C02都采用这种格式。10位寻址格式当总线上需要挂载更多设备时7位地址可能不够用。10位寻址将地址分成两字节发送。第一个字节的高5位是固定的“11110”接着是10位地址的最高两位最后是R/W位。第二个字节则是地址的低8位。从设备需要在收到这两个字节后都回复ACK。这种格式的兼容性稍差使用时需确认从设备支持。自由数据格式这是一种简化的格式不发送地址字节。起始条件后直接就是数据字节。这要求通信双方事先约定好身份和方向且在整个传输过程中方向不变。它适用于点对点、固定角色的简单通信场景不适用于多设备总线。2. TMS320F2837xS I2C模块架构与核心寄存器理解了协议我们再把目光聚焦到TMS320F2837xS这颗芯片的I2C模块实现上。TI的C2000系列DSP的I2C外设功能相当完整它不仅仅是一个简单的串行接口更是一个集成了FIFO、时钟同步、噪声滤波和仲裁逻辑的复杂状态机。我们编程的本质就是通过配置一系列寄存器来驱动这个状态机按照我们的意愿工作。2.1 模块整体框图与数据流我们可以把I2C模块想象成一个有严格分工的邮局系统。CPU是写信/读信的人而I2C模块则是负责打包、发送、接收、解包邮件的邮局。核心数据路径涉及四个关键寄存器在非FIFO模式下I2CDXR数据发送寄存器CPU把要发送的数据“信”写到这里。I2CXSR发送移位寄存器邮局的“发件窗口”。数据从I2CDXR自动拷贝到这里然后由硬件控制一位一位地通过SDA线发送出去。I2CRSR接收移位寄存器邮局的“收件窗口”。从SDA线上一位一位接收到的数据先暂存于此。I2CDRR数据接收寄存器接收到的完整数据“信”会从I2CRSR拷贝到这里供CPU读取。当使能了16x8位的**发送FIFOTX FIFO和接收FIFORX FIFO**后I2CDXR和I2CDRR就变成了FIFO的入口和出口。CPU可以连续写入多字节到TX FIFO模块会在总线空闲时自动依次发送同样RX FIFO可以缓存连续接收到的多个字节减少CPU被中断的频率。这对于高速或大数据量传输至关重要。时钟系统是另一个核心。模块时钟由系统时钟SYSCLK经过一个可编程预分频器I2CPSC寄存器产生。这个模块时钟再经过一个由I2CCLKL和I2CCLKH寄存器控制的时钟分频器最终产生出现SCL引脚上的主时钟。计算公式是SCL频率 模块时钟频率 / [(ICCH d) (ICCL d)]。其中ICCH和ICCL分别控制SCL高电平和低电平的持续时间d是一个与预分频值IPSC相关的微小延迟通常为5-7个模块时钟周期用于满足I2C协议严格的时序要求。2.2 核心控制寄存器I2CMDR详解I2CMDRI2C模式寄存器是I2C模块的“大脑”它决定了模块的基本工作模式和当前行为。我们逐位分析其关键字段IRSI2C复位这是模块的总开关。0模块复位关闭1模块运行。任何对I2CPSC预分频寄存器的修改必须在IRS0时进行修改完成后再置1启动模块。RM重复模式决定传输计数模式。0非重复模式标准模式传输字节数由I2CCNT寄存器决定1重复模式传输字节数由软件控制直到发出停止条件或新的起始条件。STT起始条件当模块为主设备MST1时软件置位此位模块会在总线上产生一个起始条件S。硬件会在起始条件发出后自动清除此位。STP停止条件当模块为主设备时软件置位此位模块会在当前传输序列完成后在总线上产生一个停止条件P。硬件在停止条件发出后自动清除此位。MST主设备模式1模块作为主设备控制SCL0模块作为从设备。TRX发送/接收在主模式或自由数据格式的从模式下此位明确指定方向1发送器0接收器。在标准从模式下此位无效方向由主设备发送的地址字节中的R/W位决定。XA扩展地址0使用7位寻址1使用10位寻址。FDF自由数据格式1启用自由数据格式无地址传输。BC位计数指定每个数据字节包含多少位1-8。通常设置为8。RM、STT、STP三位的组合直接决定了总线上的活动序列如下表所示RMSTTSTP总线活动描述000无空闲状态001P产生停止条件010S-A-D..(n)..D.产生起始条件发送地址传输n字节数据nI2CCNT不产生停止条件011S-A-D..(n)..D-P产生起始条件发送地址传输n字节数据产生停止条件100无空闲状态101P产生停止条件110S-A-D-D-D...重复模式起始条件后持续传输数据直到软件产生停止或新的起始条件111无保留不应使用此组合实操心得最常用的组合是RM0, STT1, STP1即启动一次标准的主设备读写事务并在完成后自动释放总线。RM0, STT1, STP0则用于需要产生重复起始条件Repeated START的场景例如先写设备寄存器地址再启动读操作读取数据。2.3 仿真控制寄存器SCIPRI的作用SCIPRI寄存器虽然篇幅不大但在调试阶段至关重要。它决定了当发生仿真挂起事件例如你在CCS调试器中点击了“暂停”或命中了一个断点时I2C模块的行为。其核心字段是FREESOFT位位4-300b立即停止。一旦调试器暂停CPUI2C模块会立刻停止所有活动SCL和SDA线会保持在当时的状态。这可能会中断正在进行的传输导致从设备处于不期望的状态。01b完成当前接收/发送序列后停止。这是更安全的选择。模块会完成当前正在传输的这个字节包括可能的应答位然后再挂起。这可以避免在字节中间被切断造成总线混乱。10b/11b自由运行。即使CPU被调试器暂停I2C模块也继续正常运行不受影响。这在调试与外部设备的实时通信时非常有用因为你暂停程序查看变量时不会破坏I2C总线上的通信。避坑指南在开发初期建议将FREESOFT设置为01b完成当前字节。这能保证你安全地单步调试I2C初始化代码。当调试涉及连续数据传输的应用程序时可以尝试设置为10b/11b自由运行但要注意如果程序停在修改I2C寄存器的代码行自由运行模式下的模块行为可能不可预测。绝对不要在产品代码中依赖自由运行模式来处理关键事务因为仿真挂起不是正常操作。3. 从零开始TMS320F2837xS I2C驱动配置实践理论说再多不如动手调一遍。下面我们以一个具体的场景为例配置TMS320F2837xS的I2C模块作为主设备以100kHz的标准模式向一个7位地址为0x50的EEPROM例如AT24C02写入一个字节数据0xAA到地址0x00然后再读回。3.1 硬件连接与引脚配置首先确保硬件连接正确。将DSP的I2CA_SDA和I2CA_SCL引脚具体引脚号查数据手册通过2.2kΩ - 4.7kΩ的上拉电阻连接到3.3V电源。上拉电阻的值需要根据总线电容和所需速度计算2.2kΩ是400kHz快速模式下的常用值对于100kHz标准模式4.7kΩ也可以能降低功耗。接着在代码中配置GPIO复用功能。以I2C-A为例// 假设使用 GPIO18 作为 SDA, GPIO19 作为 SCL // 1. 禁用引脚上拉外部已上拉 GPIO_setPullUp(myGpioHandle, GPIO_NUMBER_18, GPIO_PULL_DISABLE); GPIO_setPullUp(myGpioHandle, GPIO_NUMBER_19, GPIO_PULL_DISABLE); // 2. 将引脚设置为异步输入无输入限定以匹配I2C协议要求 GPIO_setQualificationMode(myGpioHandle, GPIO_NUMBER_18, GPIO_QUAL_ASYNC); GPIO_setQualificationMode(myGpioHandle, GPIO_NUMBER_19, GPIO_QUAL_ASYNC); // 3. 配置GPIO Mux将引脚功能设置为 I2C-A GPIO_setPinConfig(myGpioHandle, GPIO_18_I2CA_SDA); GPIO_setPinConfig(myGpioHandle, GPIO_19_I2CA_SCL);3.2 模块初始化与时钟配置现在开始配置I2C模块本身。我们目标是100kHz的SCL频率。假设系统时钟SYSCLK为150MHz。计算预分频和时钟分频值I2C模块要求模块时钟在7-12MHz之间。我们选择10MHz。预分频值IPSC SYSCLK / 10MHz - 1 150/10 -1 14。写入I2CPSC寄存器。模块时钟周期Tmod 1/10MHz 100ns。目标SCL周期Tmst 1/100kHz 10us。根据公式Tmst Tmod * [(ICCH d) (ICCL d)]且d在IPSC1时为5。设ICCH ICCL则(ICCH 5) (ICCL 5) 10us / 100ns 100。解得ICCH ICCL (100 - 10)/2 45(0x2D)。软件初始化流程#include driverlib.h void I2CA_Init(void) { // 1. 使能I2C-A模块时钟 SysCtl_enablePeripheral(SYSCTL_PERIPH_CLK_I2CA); // 2. 复位并初始化I2C模块关键步骤先复位才能配置预分频 I2C_disableModule(I2CA_BASE); // 相当于 IRS0 I2C_initModule(I2CA_BASE, 150000000, 100000, I2C_DUTYCYCLE_50); // 使用DriverLib简化计算 // 实际上I2C_initModule内部已经帮我们计算并设置了I2CPSC, I2CCLKL, I2CCLKH。 // 如果想手动配置应在disable后操作 // I2C_setPrescaler(I2CA_BASE, 14); // IPSC 14 // I2C_setClockDivider(I2CA_BASE, 45, 45); // ICCH ICCL 45 // 3. 配置I2C为发送器初始状态后续会根据操作改变、7位地址模式 I2C_setSlaveAddress(I2CA_BASE, 0x50); // 设置目标从设备地址虽主模式下不一定立即用但先配置好 I2C_setDataCount(I2CA_BASE, 2); // 准备发送2个字节EEPROM内存地址高8位0x00和待写数据0xAA // 注意I2C_setDataCount 设置的是I2CCNT寄存器在非重复模式下它决定了传输的字节数。 // 4. 使能I2C模块拉高IRS位 I2C_enableModule(I2CA_BASE); // 5. 可选配置中断。这里我们先用轮询方式。 // I2C_clearInterruptStatus(I2CA_BASE, I2C_INT_ARB | I2C_INT_NACK | I2C_INT_ARDY); // I2C_enableInterrupt(I2CA_BASE, I2C_INT_ARDY); // 使能传输就绪中断 // Interrupt_enable(INT_I2CA); // 使能CPU级中断 }3.3 主设备发送写EEPROM流程我们要执行一个典型的“主发送”序列起始条件 从设备地址写 EEPROM内存地址 数据。uint16_t eepromWrite(uint16_t slaveAddr, uint16_t memAddr, uint8_t data) { uint16_t status 0; // 1. 确保总线空闲 while(I2C_isBusBusy(I2CA_BASE) true) { // 等待总线空闲超时处理应在此添加 } // 2. 配置为主发送模式非重复模式7位地址 I2C_setMode(I2CA_BASE, I2C_MODE_MASTER_SEND); // 这会设置 MST1, TRX1 I2C_setDataCount(I2CA_BASE, 2); // 发送两个字节memAddr 和 data // 3. 将要发送的数据放入数据寄存器或FIFO // 先发送内存地址高字节假设16位地址先发高8位具体看EEPROM规格 // 对于AT24C02这类小容量EEPROM内存地址是8位的直接发送即可。 I2C_putData(I2CA_BASE, memAddr 0xFF); // 发送内存地址低8位0x00 // 注意I2C_putData 在非FIFO模式下是写入I2CDXR在FIFO模式下是写入TX FIFO。 // 4. 发送从设备地址写并启动传输。 // 这里需要组合地址和R/W位。0x50 1左移一位后最低位是R/W位。 // 写操作R/W 0所以地址字节就是 (0x50 1) | 0 0xA0。 I2C_setSlaveAddress(I2CA_BASE, slaveAddr); // 设置地址 I2C_sendStartCondition(I2CA_BASE); // 设置STT1产生起始条件 // 5. 等待地址发送完成并收到应答ARDY中断标志或轮询状态 // 这里使用轮询方式等待ARDY寄存器访问就绪标志 while(I2C_getInterruptStatus(I2CA_BASE) I2C_INT_ARDY 0) { // 等待可加入超时处理 // 超时后应检查 NACK无应答和 ARBLOST仲裁丢失标志 status I2C_getStatus(I2CA_BASE); if(status I2C_STAT_NACK) { // 从设备无应答处理错误 I2C_sendStopCondition(I2CA_BASE); // 发送停止条件 return -1; // 错误码无应答 } } I2C_clearInterruptStatus(I2CA_BASE, I2C_INT_ARDY); // 清除ARDY标志 // 6. 发送第一个数据字节内存地址 I2C_putData(I2CA_BASE, memAddr 0xFF); while(I2C_getInterruptStatus(I2CA_BASE) I2C_INT_ARDY 0) { // 等待数据发送完成 // 同样需要检查错误 } I2C_clearInterruptStatus(I2CA_BASE, I2C_INT_ARDY); // 7. 发送第二个数据字节要写入的数据 I2C_putData(I2CA_BASE, data); while(I2C_getInterruptStatus(I2CA_BASE) I2C_INT_ARDY 0) { // 等待 } I2C_clearInterruptStatus(I2CA_BASE, I2C_INT_ARDY); // 8. 发送停止条件结束本次传输。 // 因为我们初始化时STP0所以需要软件触发停止。 I2C_sendStopCondition(I2CA_BASE); // 设置STP1 // 9. 等待停止条件完成BB位变0 while(I2C_isBusBusy(I2CA_BASE) true) { // 等待总线释放 } // 10. EEPROM写入需要页写时间典型5ms此处应延时 DELAY_US(5000); return 0; // 成功 }3.4 主设备接收读EEPROM流程与重复起始条件读操作稍微复杂需要用到“重复起始条件”。流程是先发送一个“写”序列告诉EEPROM要读的内存地址然后不发送停止条件而是再次发送起始条件即重复起始条件接着发送从设备地址读最后读取数据。uint16_t eepromRead(uint16_t slaveAddr, uint16_t memAddr, uint8_t *rxData) { uint16_t status 0; // 第一部分发送内存地址伪写操作 // 1. 等待总线空闲 while(I2C_isBusBusy(I2CA_BASE) true) {} // 2. 配置为主发送模式发送从设备地址写和内存地址 I2C_setMode(I2CA_BASE, I2C_MODE_MASTER_SEND); I2C_setDataCount(I2CA_BASE, 1); // 本次只发送1个字节内存地址 I2C_setSlaveAddress(I2CA_BASE, slaveAddr); // 地址已包含写标志 // 3. 启动传输发送起始条件 I2C_sendStartCondition(I2CA_BASE); // 4. 等待并发送内存地址字节 while((I2C_getInterruptStatus(I2CA_BASE) I2C_INT_ARDY) 0) {} I2C_clearInterruptStatus(I2CA_BASE, I2C_INT_ARDY); I2C_putData(I2CA_BASE, memAddr 0xFF); while((I2C_getInterruptStatus(I2CA_BASE) I2C_INT_ARDY) 0) {} I2C_clearInterruptStatus(I2CA_BASE, I2C_INT_ARDY); // **关键此处不发送停止条件** // 第二部分重复起始切换为读操作 // 5. 再次发送起始条件重复起始条件 // 在非重复模式下当总线忙BB1时设置STT1产生的是重复起始条件。 I2C_sendStartCondition(I2CA_BASE); // 6. 切换为主接收模式 I2C_setMode(I2CA_BASE, I2C_MODE_MASTER_RECEIVE); // 设置 MST1, TRX0 I2C_setDataCount(I2CA_BASE, 1); // 准备接收1个数据字节 // 7. 发送从设备地址读。读操作R/W 1所以地址字节是 (0x50 1) | 1 0xA1。 // 注意对于读操作I2C_setSlaveAddress 配合 I2C_sendStartCondition 会自动处理地址发送。 // 我们需要重新设置地址虽然从设备地址没变但R/W位变了。 // 实际上在发送重复起始条件后模块会等待我们放入新的地址数据。 // 更常见的做法是在发送重复起始条件后通过I2C_putData发送带读标志的地址。 // 但TI的DriverLib封装了这部分。我们假设I2C_setSlaveAddress和setMode已配置好。 // 等待地址发送完成。 while((I2C_getInterruptStatus(I2CA_BASE) I2C_INT_ARDY) 0) {} I2C_clearInterruptStatus(I2CA_BASE, I2C_INT_ARDY); // 8. 接收数据。在非FIFO模式下接收一个字节后数据会出现在I2CDRR。 // 等待接收数据就绪RRX标志或ARDY标志。 while((I2C_getInterruptStatus(I2CA_BASE) I2C_INT_RRDY) 0) { // RRDY: Receive Ready // 轮询等待 } *rxData I2C_getData(I2CA_BASE); // 从I2CDRR读取数据 I2C_clearInterruptStatus(I2CA_BASE, I2C_INT_RRDY); // 9. 发送停止条件。作为主接收方在读取最后一个字节后应在发送ACK/NACK前配置STP。 // 对于单字节读取我们希望在接收完成后发送NACK和停止条件。 // 设置STP位模块会在接收到最后一个字节后发送NACK并跟随停止条件。 I2C_sendStopCondition(I2CA_BASE); // 10. 等待停止条件完成 while(I2C_isBusBusy(I2CA_BASE) true) {} return 0; }深度解析为什么读操作需要重复起始条件因为I2C总线上的每一次传输方向都是由主设备在发送地址字节时决定的。如果我们先发地址写告诉从设备“我要写数据到你的某个寄存器”然后又想立刻从它那里读数据就必须在不释放总线不发送停止条件的情况下重新宣告一次传输开始并发送地址读来改变数据方向。重复起始条件Sr就是用来实现这个“重启”而不释放总线的操作。4. 高级话题FIFO使用、中断驱动与故障排查掌握了基本的轮询式读写后为了提高效率和实现非阻塞通信我们必须用好FIFO和中断。4.1 FIFO配置与使用TMS320F2837xS的I2C模块包含16级深度的发送和接收FIFO。启用FIFO可以大幅减少CPU中断频率实现数据流缓冲。配置FIFO的步骤在模块初始化并使能后IRS1配置FIFO控制寄存器SCIFFTX和SCIFFRX注意虽然寄存器名带SCI但它们是I2C和SCI共用的FIFO控制寄存器在I2C模块中映射为I2CFFTX和I2CFFRX。设置发送和接收FIFO的中断触发级别。例如设置为8表示当TX FIFO中剩余空间大于等于8个空位时触发TX中断当RX FIFO中数据量大于等于8个时触发RX中断。使能FIFO模式。void I2CA_EnableFIFO(void) { // 禁用模块以安全配置FIFO可选但建议 I2C_disableModule(I2CA_BASE); // 配置发送FIFO使能FIFO设置中断触发级别为8复位TX FIFO I2C_enableFIFO(I2CA_BASE); // 使能FIFO功能 I2C_setFIFOInterruptLevel(I2CA_BASE, I2C_FIFO_TX8, I2C_FIFO_RX8); // TX/RX触发级别均为8 I2C_resetTxFIFO(I2CA_BASE); // 复位TX FIFO指针 I2C_resetRxFIFO(I2CA_BASE); // 复位RX FIFO指针 // 重新使能模块 I2C_enableModule(I2CA_BASE); // 使能FIFO相关中断 I2C_enableInterrupt(I2CA_BASE, I2C_INT_TXFF); // 发送FIFO中断 I2C_enableInterrupt(I2CA_BASE, I2C_INT_RXFF); // 接收FIFO中断 }使用FIFO进行块传输 发送时可以连续调用I2C_putData向TX FIFO填入多个字节硬件会自动依次发送。接收时数据会缓存在RX FIFO中CPU可以一次读取多个。需要密切关注FIFO状态位避免溢出或下溢。4.2 中断服务程序ISR设计中断是处理I2C异步事件的有效方式。需要处理的中断源包括ARDY寄存器访问就绪一个字节发送或接收完成可以访问数据寄存器了。RRDY接收就绪非FIFO模式下接收数据寄存器有数据。XRDY发送就绪非FIFO模式下发送数据寄存器为空可以写入新数据。TXFF发送FIFO中断TX FIFO达到预设的触发条件如空位多于设定值。RXFF接收FIFO中断RX FIFO达到预设的触发条件如数据量多于设定值。NACK无应答发送的地址或数据未收到从设备的应答。ARBLOST仲裁丢失在多主系统中失去总线仲裁。一个健壮的I2C中断服务程序框架如下__interrupt void i2cAISR(void) { uint32_t intSource I2C_getInterruptSource(I2CA_BASE); uint16_t status I2C_getStatus(I2CA_BASE); // 处理错误中断 if(intSource I2C_INT_NACK) { // 处理无应答错误记录日志发送停止条件重置状态机 I2C_sendStopCondition(I2CA_BASE); i2cErrorFlags | NACK_ERROR; I2C_clearInterruptStatus(I2CA_BASE, I2C_INT_NACK); } if(intSource I2C_INT_ARB) { // 处理仲裁丢失通常只需重新尝试发送 i2cErrorFlags | ARB_LOST_ERROR; I2C_clearInterruptStatus(I2CA_BASE, I2C_INT_ARB); // 可能需要重新初始化总线状态 } // 处理数据中断 if(intSource I2C_INT_TXFF) { // TX FIFO有空位可以继续填充数据 while((I2C_getFIFOStatus(I2CA_BASE) I2C_FIFO_TXSTAT_MASK) I2C_FIFO_TX_LEVEL) { if(txBufferIndex txBufferSize) { I2C_putData(I2CA_BASE, txBuffer[txBufferIndex]); } else { // 数据发送完毕可能禁用TXFF中断或设置标志 I2C_disableInterrupt(I2CA_BASE, I2C_INT_TXFF); transferComplete true; break; } } I2C_clearInterruptStatus(I2CA_BASE, I2C_INT_TXFF); } if(intSource I2C_INT_RXFF) { // RX FIFO有数据读取出来 while((I2C_getFIFOStatus(I2CA_BASE) I2C_FIFO_RXSTAT_MASK) I2C_FIFO_RX_LEVEL) { rxBuffer[rxBufferIndex] I2C_getData(I2CA_BASE); if(rxBufferIndex expectedRxSize) { // 收到足够数据 I2C_disableInterrupt(I2CA_BASE, I2C_INT_RXFF); dataReceived true; break; } } I2C_clearInterruptStatus(I2CA_BASE, I2C_INT_RXFF); } // 处理ARDY中断在非FIFO或小数据量时有用 if(intSource I2C_INT_ARDY) { // 一个字节传输完成进行相应处理如切换发送/接收模式 I2C_clearInterruptStatus(I2CA_BASE, I2C_INT_ARDY); } // 必须清除PIE组中断标志 Interrupt_clearACKGroup(INTERRUPT_ACK_GROUP8); // I2C-A通常属于PIE组8 }4.3 常见问题与调试技巧实录在实际项目中I2C通信失败是家常便饭。下面是我踩过的一些坑和总结的排查思路问题1通信完全无响应SCL/SDA线一直为高。排查硬件检查万用表测量SCL/SDA电压应为3.3V上拉后。检查上拉电阻是否焊接阻值是否合适。用示波器观察总线看是否有任何波形。引脚配置确认GPIO已正确复用为I2C功能并且输入限定设置为异步GPIO_QUAL_ASYNC。这是最容易被忽略的一点同步采样会过滤掉I2C的高速信号。模块使能确认I2C模块的时钟已使能SysCtl_enablePeripheral并且IRS位已置1I2C_enableModule。从设备地址确认代码中设置的从设备地址左移一位后与设备手册一致。许多设备有多个地址选择引脚需要核对。问题2能发送起始条件和地址但收不到应答NACK。排查地址错误再次检查从设备地址和R/W位。用逻辑分析仪抓取波形直接看发出的地址字节是多少。从设备电源/复位确保从设备已正确上电并已完成上电复位。有些传感器需要特定的初始化序列。时序问题检查I2C时钟频率是否在从设备支持的范围内例如某些老设备只支持100kHz标准模式。通过调整I2CCLKH和I2CCLKL寄存器来降低速度试试。总线冲突确认总线上没有其他设备包括你的MCU在异常地拉低总线。可以尝试断开其他设备单独测试。问题3通信时好时坏偶尔出现数据错误。排查信号完整性用示波器观察SCL和SDA波形看上升沿/下降沿是否陡峭是否有过冲、振铃或毛刺。长距离或高容性负载会导致边沿变缓可能违反I2C时序规范。考虑减小上拉电阻值如从4.7kΩ换为2.2kΩ以增强驱动能力。中断冲突如果使用了中断确保ISR执行时间足够短没有错过重要的状态处理或标志清除。特别是I2C_clearInterruptStatus一定要及时。FIFO溢出/下溢在高速传输时检查TXFF和RXFF中断的触发级别设置是否合理。如果CPU填充数据太慢TX FIFO会下溢如果CPU读取数据太慢RX FIFO会溢出。可以增加FIFO触发中断的级别或者使用DMA来搬运数据。软件时序在发送停止条件后是否等待了足够的时间I2C_isBusBusy再开始下一次传输在写操作后是否给从设备如EEPROM留足了内部写入时间问题4在调试器中单步执行时I2C通信失败但全速运行正常。原因与解决这几乎肯定是SCIPRI寄存器配置问题。当你在调试器中断点暂停时CPU挂起如果SCIPRI.FREESOFT设置为00b立即停止I2C模块会戛然而止可能停在时钟低电平导致总线锁死。解决在初始化代码中将SCIPRI.FREESOFT设置为01b完成当前字节后停止。如果你使用的是DriverLib可能没有直接函数需要直接操作寄存器// 设置SCIPRI寄存器的FREESOFT字段为01b (完成当前序列后停止) HWREG(I2CA_BASE I2C_O_PRI) (HWREG(I2CA_BASE I2C_O_PRI) ~(0x3 3)) | (0x1 3);这样设置后即使遇到断点I2C模块也会完成当前正在传输的这一个字节将总线置于一个干净的状态后再暂停大大提高了调试的友好度。调试利器逻辑分析仪投资一个哪怕是最基础的逻辑分析仪如Saleae Logic系列对调试I2C、SPI等串行协议有质的提升。它能直观地显示起始、停止、地址、数据、ACK/NACK位一眼就能看出通信序列是否符合预期是定位问题最快的手段。结合示波器观察信号质量软硬件问题基本无处遁形。最后关于TMS320F2837xS的DriverLib库它极大简化了寄存器操作但封装有时会隐藏细节。当你遇到奇怪的问题时不妨直接查看对应寄存器如I2CSTR状态寄存器的值结合技术参考手册TRM中的状态机流程图如图19-6图19-7进行分析往往能发现库函数未能妥善处理的边界条件。理解状态机是彻底驾驭I2C模块的不二法门。