1. I2C总线协议深度解析:从两根线到复杂通信
如果你在嵌入式领域摸爬滚打过一段时间,那么对I2C总线一定不会陌生。它就像电子设备内部的“神经系统”,用最精简的连线(两根线)将大脑(主控MCU)和各个感官、执行器(传感器、存储器、IO扩展芯片等)连接起来。我第一次接触I2C是在一个温湿度监测项目上,当时为了读取一个数字温湿度传感器,对着数据手册和示波器折腾了大半天,才真正搞懂那两根线上跳动的波形到底在“说”什么。这种看似简单,实则内涵丰富的通信协议,是嵌入式工程师必须熟练掌握的基本功。
I2C,全称Inter-Integrated Circuit,由飞利浦半导体(现恩智浦NXP)在1980年代推出。它的核心价值在于极简的物理连接和灵活的软件可配置性。在引脚资源宝贵的微控制器世界里,能用两根线(串行数据线SDA和串行时钟线SCL)搞定多个外设的通信,无疑具有巨大的吸引力。协议本身定义了完整的通信规则,包括起始停止信号、地址寻址、应答机制和数据传输格式,确保了不同厂商设备间的互操作性。本文将以TI的C2000系列明星产品TMS320F28003x微控制器为例,不仅带你重温I2C的核心原理,更会深入其I2C外设模块的寄存器级操作和驱动库使用,分享在实际项目中配置、调试I2C通信的实战经验和避坑指南。无论你是刚入门的新手,还是想深入了解特定MCU实现的老手,都能从中找到有价值的内容。
2. I2C核心工作机制与TMS320F28003x模块概览
2.1 I2C总线的基本“语言规则”
要驾驭I2C,首先要理解它在物理层和协议层定下的“规矩”。所有通信都基于“主-从”架构。主设备(Master)负责发起和终止通信,并产生时钟信号;从设备(Slave)则监听总线,在地址匹配时响应主设备的命令。SDA和SCL线都需要通过上拉电阻连接到正电源,形成一个“线与”逻辑,任何设备都可以将线拉低(输出0),而释放总线(输出1)则靠上拉电阻将电平拉高。
通信总是由主设备发起的起始条件(S)开始:在SCL为高电平时,SDA线上一个从高到低的跳变。整个通信序列由停止条件(P)结束:在SCL为高电平时,SDA线上一个从低到高的跳变。在起始和停止条件之间,总线被视为“忙”。每一个字节(8位)数据传输后,都会跟一个应答位(ACK):发送方释放SDA线,接收方在第9个时钟周期将SDA拉低表示应答(ACK),保持高电平则表示非应答(NACK)。这就是I2C最基本的“单词”和“语法”。
TMS320F28003x的I2C模块完全兼容NXP I2C总线规范v2.1,支持标准模式(最高100kbps)和快速模式(最高400kbps)。其内部结构可以抽象为几个关键部分:负责数据移入移出的移位寄存器(I2CRSR/I2CXSR)、供CPU读写数据的数据缓冲寄存器(I2CDRR/I2CDXR)、决定模块行为的模式控制寄存器(I2CMDR)、以及非常实用的16级深度收发FIFO。模块时钟由系统时钟SYSCLK经过一个可编程预分频器(I2CPSC)产生,而作为主设备时的SCL时钟频率,则由模块时钟进一步通过I2CCLKL和I2CCLKH寄存器分频得到。
2.2 TMS320F28003x I2C模块的特色与局限
在开始具体配置前,了解这个模块能做什么、不能做什么至关重要。F28003x的I2C模块功能相当全面:
- 标准协议支持:7位和10位寻址模式、群呼地址、起始字节模式。
- 多主模式支持:内置仲裁逻辑,允许多个主设备竞争总线。
- 复合传输模式:支持主设备在一次通信中切换收发方向(例如,先写寄存器地址,再读数据)。
- 增强型中断:除了基本的数据收发、仲裁丢失等中断事件,还支持基于FIFO填充水平触发的独立收发FIFO中断,这大大减轻了CPU轮询负担。
- 自由数据格式:一种无地址的纯数据流模式,适用于点对点固定角色的通信。
但也要注意它的不支持项:不支持高速模式(Hs-mode,最高3.4Mbps),也不支持古老的CBUS兼容模式。这意味着如果你的外设必须工作在Hs-mode下,则需要选择其他通信接口(如SPI)或更换MCU。对于绝大多数传感器、EEPROM等中低速外设,标准模式和快速模式已经完全够用。
3. 硬件连接、时钟配置与引脚初始化
3.1 上拉电阻的选择与计算
I2C总线依靠上拉电阻将总线拉至高电平。电阻值的选择是个平衡艺术:阻值太小,电流过大,增加功耗且可能超出GPIO引脚驱动能力;阻值太大,总线上升沿变缓,可能无法满足高速模式下的时序要求,导致通信失败。
一个常用的经验值是2.2kΩ到10kΩ之间,在3.3V系统下,2.2kΩ是兼顾速度和功耗的常见选择。但更严谨的做法是根据总线电容和所需速度来计算。总线电容(Cb)包括所有设备引脚电容、PCB走线寄生电容的总和。上升时间tr必须满足协议要求(标准模式tr<1000ns,快速模式tr<300ns)。最小上拉电阻Rp(min)由总线允许的最大低电平灌电流(通常为3mA)决定:Rp(min) > (Vdd - Vol(max)) / Iol。最大上拉电阻Rp(max)由上升时间要求决定:Rp(max) < tr / (0.8473 * Cb),其中0.8473是RC充电时间常数因子。
实操心得:在项目初期,如果无法精确计算总线电容,可以先用一个10kΩ的可调电阻或电位器代替上拉电阻。用示波器观察SCL和SDA线的上升沿,调整电阻值直到波形边沿陡峭且无过冲。实测稳定后,再换成相近阻值的固定电阻。我曾在一个连接了6个传感器的项目中,因为忽略了走线电容,用了10kΩ电阻导致400kbps通信不稳定,后来换成3.3kΩ问题立刻解决。
3.2 系统时钟与SCL时钟的精确配置
I2C模块的时钟链有两级分频。第一级是模块时钟,由SYSCLK通过I2CPSC寄存器分频得到。手册强调,为了满足所有I2C协议时序,模块时钟频率必须在7-12MHz之间。假设SYSCLK为100MHz,要得到10MHz的模块时钟,则I2CPSC应配置为 (100MHz / 10MHz) - 1 = 9。
第二级是SCL主时钟,由模块时钟通过I2CCLKL和I2CCLKH寄存器分频产生。SCL的低电平时间由ICCL值决定,高电平时间由ICCH值决定。SCL周期 Tmst = Tmod * [(ICCH + d) + (ICCL + d)],其中Tmod是模块时钟周期,d是一个与IPSC值相关的延迟因子(IPSC=0时d=7,IPSC=1时d=6,IPSC>1时d=5)。
例如,目标SCL频率为400kHz(周期2.5μs),模块时钟为10MHz(周期0.1μs)。则 (ICCH+d) + (ICCL+d) = 2.5μs / 0.1μs = 25。通常设置ICCH = ICCL以实现占空比50%,那么 (ICCH+d) = (ICCL+d) = 12.5。取整后设置ICCH = ICCL = 7(因为d=5,则7+5=12)。实际周期为 0.1μs * (12+12) = 2.4μs,频率约416.7kHz,在允许误差范围内。
// 假设SYSCLK=100MHz,目标SCL=400kHz void I2C_ClockInit(void) { // 1. 确保I2C模块处于复位状态(IRS=0),才能配置预分频器 I2caRegs.I2CMDR.bit.IRS = 0; // 2. 配置预分频器,得到约10MHz模块时钟 (100MHz / (9+1)) I2caRegs.I2CPSC.all = 9; // IPSC = 9, d = 5 // 3. 使能I2C模块 I2caRegs.I2CMDR.bit.IRS = 1; // 4. 配置SCL时钟分频器,高电平和低电平时间各12个模块时钟周期 // ICCL = ICCH = 12 - d = 12 - 5 = 7 I2caRegs.I2CCLKL = 7; I2caRegs.I2CCLKH = 7; // 5. 等待至少一个总线空闲周期,确保BB位状态正确 DELAY_US(100); // 延迟远大于一次完整传输的时间 }3.3 GPIO引脚复用与配置
TMS320F28003x的I2C引��需要通过GPIO多路复用器映射到具体引脚。配置顺序有讲究,以避免引脚出现毛刺:
- 先配置GPyGMUX寄存器(同时保持对应的GPyMUX为默认值0)。
- 再配置GPyMUX寄存器,将引脚功能选择为I2C。
- 对于输入信号,需要将GPIO输入限定设置为异步模式(GPxQSELn寄存器设置为11b),因为I2C协议本身有同步机制,额外的数字滤波可能会干扰时序。
- 根据硬件设计,决定是否使能内部上拉电阻(通过GPyPUD寄存器)。
void I2C_GPIOInit(void) { // 假设使用I2C-A,引脚为GPIO18(SCL)和GPIO19(SDA) // 1. 配置GPIO18为I2C-A SCL GpioCtrlRegs.GPAGMUX1.bit.GPIO18 = 0; // 先清GMUX GpioCtrlRegs.GPAMUX1.bit.GPIO18 = 2; // 选择I2C-A SCL功能 (具体值查数据手册) GpioCtrlRegs.GPAQSEL1.bit.GPIO18 = 3; // 输入限定为异步 GpioCtrlRegs.GPAPUD.bit.GPIO18 = 0; // 使能内部上拉(如果外部未接) // 2. 配置GPIO19为I2C-A SDA GpioCtrlRegs.GPAGMUX1.bit.GPIO19 = 0; GpioCtrlRegs.GPAMUX1.bit.GPIO19 = 2; // 选择I2C-A SDA功能 GpioCtrlRegs.GPAQSEL1.bit.GPIO19 = 3; GpioCtrlRegs.GPAPUD.bit.GPIO19 = 0; }4. 操作模式详解与寄存器级编程
4.1 主模式与从模式的切换与流程
I2C模块有四种基本操作模式,由主模式位(MST)和发送/接收位(TRX)共同决定。理解状态切换是编程的关键。
作为主设备(MST = 1):
- 主发送模式(TRX = 1):这是主设备的默认起始模式。主设备发送起始信号(S),接着发送7位或10位从机地址和写方向位(R/W=0),然后发送数据字节。每发送一个字节,都需要等待从机的ACK。
- 主接收模式(TRX = 0):主设备发送起始信号(S)和从机地址,但方向位为读(R/W=1)。随后,主设备释放SDA线(转为输入),并在SCL的驱动下读取从机发送的数据。每读完一个字节,主设备需要发送ACK(除非是最后一个字节,则发送NACK)。
模式切换通常发生在一次通信序列中。例如,读取一个I2C EEPROM的常见操作是:主设备先以主发送模式写入要读取的内存地址,然后发送一个重复起始条件(Repeated Start),再以主接收模式读取数据。F28003x通过设置STT(起始)和STP(停止)位来控制总线状态。
作为从设备(MST = 0):
- 从接收模式:模块上电或复位后的默认模式。它持续监听总线,将自己的地址(配置在I2COAR寄存器中)与主设备发出的地址进行比较。如果匹配且R/W=0,则进入从接收模式,准备接收数据。
- 从发送模式:当地址匹配且R/W=1时,模块进入从发送模式,准备向主设备发送数据。
从设备的角色完全由主设备发起的地址帧中的R/W位决定,自身无法主动发起通信。
4.2 非重复模式与重复模式的选择
I2CMDR寄存器中的RM位决定了数据传输的计数方式,这是F28003x I2C模块一个非常实用的特性。
- 非重复模式(RM = 0):传输的字节数由I2CCNT寄存器预先设定。当内部计数器减到0时,如果STP=1,模块会自动产生停止条件;如果STP=0,则设置ARDY(寄存器访问就绪)中断标志,等待软件处理。这里有一个大坑:如果设置I2CCNT = 0,模块会认为要传输65536个字节,而不是0个字节!在初始化时务必检查该寄存器值。
- 重复模式(RM = 1):I2CCNT寄存器的值被忽略。模块会持续传输数据,直到软件设置STP位产生停止条件,或设置STT位产生一个新的起始条件(重复起始)。每完成一个字节的传输或接收,ARDY位都会被置位,方便软件进行流控制。这种模式非常适合传输长度未知或可变的数据流。
注意事项:一旦通过设置STT位启动了I2C事务,在STP位被置起、事务结束之前,绝对不能修改RM位来切换模式。硬件状态机不支持这种动态切换,会导致不可预知的行为。
4.3 关键寄存器配置详解与Driverlib函数映射
直接操作寄存器虽然直观,但易错。TI提供的Driverlib库函数封装了底层操作,提高了代码可读性和可移植性。以下是核心寄存器及其对应的Driverlib函数解析:
| 寄存器 | 功能描述 | 关键位域 | 对应 Driverlib 函数 (sci.h/c) | 实战说明 |
|---|---|---|---|---|
| I2CMDR (模式寄存器) | 核心控制寄存器,决定模块使能、主从模式、重复模式、自由格式等。 | IRS: 模块复位/使能 MST: 主/从模式 TRX: 发送/接收模式 RM: 重复模式 STT/STP: 起始/停止条件 FDF: 自由数据格式 BC: 数据位计数(1-8) | I2C_initMaster()/I2C_initSlave()I2C_setBitCount()I2C_setDataCount()(影响I2CCNT) | 必须先令IRS=0才能配置其他位。STT和STP通常由软件置1,由硬件在操作完成后清0。 |
| I2CPSC (预分频寄存器) | 对SYSCLK进行分频,产生I2C模块时钟。 | IPSC: 分频值 | I2C_setClockPrescaler() | 仅在IRS=0时可写。分频后频率需在7-12MHz范围内。 |
| I2CCLKL/H (时钟低/高时间寄存器) | 设置SCL时钟低电平和高电平的持续时间。 | ICCL, ICCH: 计数值 | I2C_setDataCount()不直接对应。需手动计算并赋值。 | 决定最终的SCL频率。需根据模块时钟和d值计算。 |
| I2CCNT (数据计数寄存器) | 在非重复模式(RM=0)下,指定要传输的字节数。 | 16位计数值 | I2C_setDataCount() | RM=0时有效。设置为N,则传输N个字节。切记不能设为0。 |
| I2CDXR (数据发送寄存器) | CPU将待发送数据写入此寄存器。 | 8位数据 | I2C_putData() | 写入前需检查XRDY(发送就绪)位或TXFFINT标志(如果使用FIFO)。 |
| I2CDRR (数据接收寄存器) | CPU从此寄存器读取接收到的数据。 | 8位数据 | I2C_getData() | 读取前需检查RRDY(接收就绪)位或RXFFINT标志。 |
| I2CSTR (状态寄存器) | 反映模块当前状态和中断标志。 | BB: 总线忙 ARDY: 寄存器访问就绪 RRDY: 接收就绪 XRDY: 发送就绪 NACK: 收到非应答 AL: 仲裁丢失 | I2C_getStatus()I2C_clearStatus() | 中断服务程序中必须读取该寄存器以清除相应的中断标志位(某些位需写1清0)。 |
| I2CIER (中断使能寄存器) | 使能各类中断事件。 | 对应I2CSTR中的各种状态位 | I2C_enableInterrupt()I2C_disableInterrupt() | 使能中断前,需在PIE模块中配置好对应的中断向量和使能。 |
| I2CFFTX/RX (FIFO控制寄存器) | 控制发送/接收FIFO,包括使能、复位、设置中断触发水平。 | TX/RXFFIL: FIFO中断级别 TX/RXFFIENA: FIFO中断使能 TX/RXFFRST: FIFO复位 | I2C_enableFIFO()I2C_setFIFOInterruptLevel()I2C_resetTxFIFO() | 使用FIFO能极大提升效率,减少中断频率。通常设置触发水平为半满或1/4满。 |
关于SCIPRI寄存器:你提供的资料中提到了SCIPRI寄存器,它属于SCI(串行通信接口)模块,用于配置仿真挂起事件(如调试器遇到断点)时外设的行为。其FREESOFT字段决定模块是立即停止、完成当前收/发序列后停止,还是自由运行。对于I2C模块,在实时性要求高的应用中,若担心调试断点影响总线时序,可参考此思路,但I2C模块本身可能没有完全相同的寄存器,其行为可能��全局仿真控制寄存器或模块自身的配置位决定。在调试I2C通信时,如果遇到断点后总线锁死,可能需要检查相关配置,或避免在关键时序处设置断点。
5. 实战:以主模式读写EEPROM为例
我们以一个具体的例子来串联上述知识��使用TMS320F28003x作为主设备,读写一个常见的I2C EEPROM芯片(如AT24C02,7位地址为0x50)。
5.1 初始化流程
#include "driverlib.h" #include "device.h" #define I2C_SLAVE_ADDR 0x50 // 7位地址,左移一位后为0xA0 (写) / 0xA1 (读) #define I2C_MODULE_CLK_HZ 10000000 // 10MHz #define I2C_BUS_CLK_HZ 400000 // 400kHz void I2CA_Init(void) { // 1. 初始化GPIO引脚为I2C功能 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. 初始化I2C模块为主模式 I2C_initMaster(I2CA_BASE, DEVICE_SYSCLK_FREQ, I2C_BUS_CLK_HZ, I2C_DUTYCYCLE_50); // 3. 配置FIFO:使能,设置触发级别为4字节(16级深度的1/4) I2C_enableFIFO(I2CA_BASE); I2C_setFIFOInterruptLevel(I2CA_BASE, I2C_FIFO_TX4, I2C_FIFO_RX4); // 4. 使能模块 I2C_enableModule(I2CA_BASE); // 5. 配置中断(可选,本例使用轮询) // I2C_clearInterruptStatus(I2CA_BASE, I2C_INT_ARB | I2C_INT_NACK | ...); // I2C_enableInterrupt(I2CA_BASE, I2C_INT_ARB | I2C_INT_NACK | I2C_INT_DATA); // _PIE_register(I2CA_INT_NUM, &i2cAISR); }5.2 写入单个字节到指定地址
这是“主发送”模式的典型应用。流程:起始条件 -> 发送设备地址+写 -> 发送内存地址 -> 发送数据 -> 停止条件。
bool I2CA_WriteByte(uint16_t memAddr, uint8_t data) { uint16_t nackBit; // 1. 等待总线空闲 while(I2C_getStatus(I2CA_BASE) & I2C_STATUS_BUS_BUSY) { // 可加入超时处理 } // 2. 设置数据计数:地址(1字节) + 数据(1字节) = 2字节 I2C_setDataCount(I2CA_BASE, 2); // 3. 配置为主发送模式,非重复模式,准备产生起始条件 I2C_setConfig(I2CA_BASE, I2C_MASTER_SEND_MODE); // 内部会设置MST=1, TRX=1, RM=0, STT=1, STP=0 // 4. 发送内存地址(高8位,对于24C02只有低8位有效,高8位可忽略或发0) I2C_putData(I2CA_BASE, (uint8_t)(memAddr >> 8)); // 假设发送高8位地址(实际24C02可能不需要) while(!(I2C_getStatus(I2CA_BASE) & I2C_STATUS_REG_ACCESS_RDY)); // 等待ARDY I2C_clearStatus(I2CA_BASE, I2C_STATUS_REG_ACCESS_RDY); I2C_putData(I2CA_BASE, (uint8_t)(memAddr & 0xFF)); // 发送低8位地址 while(!(I2C_getStatus(I2CA_BASE) & I2C_STATUS_REG_ACCESS_RDY)); I2C_clearStatus(I2CA_BASE, I2C_STATUS_REG_ACCESS_RDY); // 5. 发送数据 I2C_putData(I2CA_BASE, data); while(!(I2C_getStatus(I2CA_BASE) & I2C_STATUS_REG_ACCESS_RDY)); I2C_clearStatus(I2CA_BASE, I2C_STATUS_REG_ACCESS_RDY); // 6. 数据计数已到,产生停止条件。等待停止条件完成。 I2C_setStop(I2CA_BASE); while(I2C_getStatus(I2CA_BASE) & I2C_STATUS_BUS_BUSY); // 7. 检查是否收到NACK(可选,更严谨) nackBit = I2C_getStatus(I2CA_BASE) & I2C_STATUS_NACK; I2C_clearStatus(I2CA_BASE, I2C_STATUS_NACK); if(nackBit) { return false; // 写入失败,可能地址错误或设备未响应 } // 8. EEPROM写入需要页编程时间,典型5ms,必须延迟 DEVICE_DELAY_US(5000); return true; }5.3 从指定地址读取单个字节
这是“复合格式”传输:先以主发送模式写入内存地址,再以主接收模式读取数据。中间用重复起始条件连接,而非停止条件。
bool I2CA_ReadByte(uint16_t memAddr, uint8_t *data) { uint16_t nackBit; // 第一部分:发送内存地址(主发送模式) // 1. 等待总线空闲 while(I2C_getStatus(I2CA_BASE) & I2C_STATUS_BUS_BUSY); // 2. 设置数据计数:仅发送内存地址(1或2字节,取决于EEPROM) I2C_setDataCount(I2CA_BASE, 2); // 假设发送2字节地址 // 3. 配置为主发送模式,非重复模式,起始条件 I2C_setConfig(I2CA_BASE, I2C_MASTER_SEND_MODE); // 4. 发送内存地址 I2C_putData(I2CA_BASE, (uint8_t)(memAddr >> 8)); while(!(I2C_getStatus(I2CA_BASE) & I2C_STATUS_REG_ACCESS_RDY)); I2C_clearStatus(I2CA_BASE, I2C_STATUS_REG_ACCESS_RDY); I2C_putData(I2CA_BASE, (uint8_t)(memAddr & 0xFF)); while(!(I2C_getStatus(I2CA_BASE) & I2C_STATUS_REG_ACCESS_RDY)); I2C_clearStatus(I2CA_BASE, I2C_STATUS_REG_ACCESS_RDY); // 此时数据计数器归零,但不要产生停止条件! // 第二部分:重新起始并读取数据(主接收模式) // 5. 设置数据计数:读取1字节数据 I2C_setDataCount(I2CA_BASE, 1); // 6. 配置为主接收模式,非重复模式,并产生重复起始条件 // 关键:STT需要再次置1,以产生重复起始条件。STP在最后置1。 I2C_setConfig(I2CA_BASE, I2C_MASTER_RECEIVE_MODE); // MST=1, TRX=0, RM=0, STT=1, STP=0 // 7. 等待数据接收完成(ARDY在最后一个字节接收完成后置位) while(!(I2C_getStatus(I2CA_BASE) & I2C_STATUS_REG_ACCESS_RDY)); I2C_clearStatus(I2CA_BASE, I2C_STATUS_REG_ACCESS_RDY); // 8. 产生停止条件,并读取数据 I2C_setStop(I2CA_BASE); *data = I2C_getData(I2CA_BASE); // 从数据接收寄存器读取 // 9. 等待总线空闲 while(I2C_getStatus(I2CA_BASE) & I2C_STATUS_BUS_BUSY); // 10. 检查NACK(通常在地址阶段检查) nackBit = I2C_getStatus(I2CA_BASE) & I2C_STATUS_NACK; I2C_clearStatus(I2CA_BASE, I2C_STATUS_NACK); return (nackBit == 0); }6. FIFO使用技巧与中断服务程序设计
使用查询(轮询)方式虽然简单,但在高吞吐量或低功耗场景下效率低下。F28003x的16级FIFO和中断机制可以完美解决这个问题。
6.1 FIFO配置策略
- 中断水平设置:不要设置为1字节触发一次中断,那样中断频率太高,开销大。通常设置为FIFO深度的1/4、1/2或3/4。例如,设置接收FIFO中断水平为RXFFIL=4,则当FIFO中数据达到4字节时产生中断,一次读取多个数据。
- 发送FIFO:在发送大量数据前,可以先用
I2C_putData()填充多个字节到FIFO,然后启动传输。硬件会自动从FIFO中取出数据发送,并在FIFO数据量低于触发水平时产生中断,请求CPU继续填充。 - 复位FIFO:在通信开始前或出错恢复时,使用
I2C_resetTxFIFO()和I2C_resetRxFIFO()来清空FIFO,确保从一个干净的状态开始。
6.2 中断服务程序(ISR)编写要点
// 假设已配置PIE,将I2CA的INT(非FIFO INT)连接到该ISR __interrupt void i2cAISR(void) { uint16_t status = I2C_getStatus(I2CA_BASE); uint16_t intSource = I2C_getInterruptSource(I2CA_BASE); // 判断具体中断源 if(intSource & I2C_INT_ARB) { // 仲裁丢失:通常发生在多主竞争时。应重新尝试发送。 I2C_clearInterruptStatus(I2CA_BASE, I2C_INT_ARB); // ... 重试逻辑 ... } if(intSource & I2C_INT_NACK) { // 收到非应答:从设备无响应。检查地址、设备是否上电。 I2C_clearInterruptStatus(I2CA_BASE, I2C_INT_NACK); // ... 错误处理逻辑 ... } if(intSource & I2C_INT_DATA) { // 数据中断:ARDY、RRDY或XRDY。需要结合状态寄存器判断。 if(status & I2C_STATUS_REG_ACCESS_RDY) { // ARDY:寄存器访问就绪。在非重复模式下,表示预设字节数传输完成。 I2C_clearStatus(I2CA_BASE, I2C_STATUS_REG_ACCESS_RDY); // 可以在此处决定是否发送停止条件,或准备下一阶段数据。 g_i2cTransferDone = true; } if(status & I2C_STATUS_RECV_READY) { // RRDY:接收数据就绪。如果未用FIFO,应立刻读取I2CDRR。 // 如果用了FIFO,这个标志可能不常用,主要靠RXFFINT。 uint8_t data = I2C_getData(I2CA_BASE); // ... 处理接收到的数据 ... } // 清除数据中断标志 I2C_clearInterruptStatus(I2CA_BASE, I2C_INT_DATA); } // 必须清除PIE组应答位,否则无法响应后续中断 Interrupt_clearACKGroup(INTERRUPT_ACK_GROUP8); // I2C-A属于PIE组8 } // 独立的FIFO中断服务程序(如果使能了) __interrupt void i2cAFIFOISR(void) { uint16_t fifoStatus = I2C_getRxFIFOStatus(I2CA_BASE); uint16_t level = (fifoStatus & I2C_RXFFST_MASK) >> I2C_RXFFST_SHIFT; if(level >= RX_FIFO_INTERRUPT_LEVEL) { // 例如 level >= 4 // 从FIFO中读取多个字节 for(int i = 0; i < level; i++) { g_rxBuffer[g_rxIndex++] = I2C_getData(I2CA_BASE); } } // 清除FIFO中断标志 I2C_clearInterruptStatus(I2CA_BASE, I2C_INT_RXFF); Interrupt_clearACKGroup(INTERRUPT_ACK_GROUP8); }7. 常见问题排查与调试心得
I2C调试是每个嵌入式工程师的必修课。以下是我踩过的一些坑和总结的排查思路:
问题1:通信完全无响应,SCL或SDA线始终为高或始终为低。
- 检查硬件:首先用万用表测量SCL和SDA线对地电压。正常空闲时应为高电平(VDD)。如果一直为低,可能是某设备故障将总线拉死,或者上拉电阻未接/开路。如果一直为高但无波形,可能是主设备未正确输出时钟。
- 检查初始化:确认I2C模块已使能(IRS=1),GPIO复用配置正确,引脚方向设置无误(I2C模块会自动管理方向)。
- 检查地址:确认发送的从设备地址是否正确(7位地址需要左移一位,并加上R/W位)。用逻辑分析仪或示波器抓取起始信号后的第一个字节,核对地址。
问题2:能收到ACK,但数据错误或丢失。
- 检查时钟速度:SCL频率是否在从设备支持的范围内?过快可能导致建立/保持时间不满足。尝试降低SCL频率(增大ICCL/ICCH值)。
- 检查时序:用示波器测量SCL和SDA的时序。重点看SDA数据在SCL高电平期间是否稳定(建立时间tSU:DAT),以及SCL低电平期间SDA变化后到SCL上升沿是否有足够时间(保持时间tHD:DAT)。不满足时序通常需要降低SCL频率或检查从设备时序要求。
- 检查FIFO/中断:如果使用中断或FIFO,是否及时读取了接收数据?接收FIFO溢出会导致数据丢失。检查RXFFOVF标志。
问题3:多主系统中仲裁丢失。
- 理解仲裁:当两个主设备同时发起传输时,它们会持续发送地址和数据,直到出现分歧。SDA线被设计为“线与”,哪个设备发送‘1’(释放总线)而另一个发送‘0’(拉低总线),则发送‘1’的设备检测到总线实际为‘0’,就知道自己仲裁失败,应立即转为从模式并释放总线。
- 处理仲裁丢失:在中断服务程序中检测到AL(仲裁丢失)标志后,应重新尝试发送。代码需要具备重试机制。同时,确保每个主设备有合理的退避算法,避免持续竞争。
问题4:调试时设置断点导致I2C总线锁死。
- 原因分析:当CPU在I2C通信过程中被调试器暂停(断点)时,I2C模块可能仍在驱动SCL或SDA线。如果停在等待状态(如等待ARDY),从设备可能一直在等待时钟继续,导致总线超时或死锁。
- 应对策略:
- 避免在关键循环内设断点:不要在
while(!(I2C_getStatus(...) & I2C_STATUS_XXX_RDY));这样的紧循环里打断点。可以在循环后或判断语句前打断点。 - 使用仿真控制寄存器:查阅芯片手册,看是否有类似SCI模块中SCIPRI的寄存器,可以配置在仿真挂起时让I2C模块进入“自由运行”或“完成当前操作后停止”模式。这可以防止总线被意外拉低。
- 软件复位:在调试脚本或初始化代码中,加入强制复位I2C模块的步骤(设置IRS=0,再置1),并在复位后等待足够长时间(远大于一次传输时间)让总线状态同步。
- 硬件复位:最粗暴但有效的方法,重启整个系统或手动复位MCU。
- 避免在关键循环内设断点:不要在
问题5:使用Driverlib库函数通信失败,但直接写寄存器可以。
- 检查函数调用顺序:Driverlib函数内部可能有严格的依赖关系。例如,必须在调用
I2C_initMaster()设置波特率之前,确保模块处于复位状态(IRS=0)。查看函数源码或文档,确认前置条件。 - 检查参数计算:
I2C_initMaster()函数内部会根据传入的系统时钟和目标SCL频率计算分频值。确保传入的sysclk参数与实际SYSCLK一致。有时直接写寄存器能成功是因为你手动计算并设置了一个可行的值,而库函数计算出的值可能因为整数舍入导致频率略有偏差,在边界条件下可能出问题。 - 查看示例代码:TI的C2000Ware软件包中提供了丰富的示例项目(例如
i2c_ex1_loopback、i2c_ex2_eeprom)。对照示例检查自己的初始化序列和函数调用顺序,是最快的排查方法。
调试I2C,一把好的逻辑分析仪(如Saleae)至关重要。它能直观地展示起始、停止、地址、数据、ACK/NACK位,并自动解析协议,极大提升效率。当通信异常时,抓取波形并与理想波形对比,问题往往一目了然。