1. 项目概述:从协议到芯片的I2C实战指南
在嵌入式开发的世界里,如何让微控制器(MCU)与周边的传感器、存储器、显示屏等“小零件”高效对话,是每个工程师都要面对的基础课题。I2C总线协议,凭借其简洁的两线制设计和灵活的主从架构,成为了解决这一问题的经典方案。它就像设备间的一种“暗语”,用两根线(SDA数据线和SCL时钟线)就能建立起一套有序的通信规则,极大地节省了宝贵的MCU引脚资源。然而,理解协议规范只是第一步,真正的挑战在于如何将纸面上的时序图和数据帧,转化为芯片寄存器里一个个具体的比特位配置。
本文将以德州仪器(TI)的CC27xx系列无线MCU为例,进行一次从I2C协议原理到寄存器级配置的深度实践。CC27xx作为一款面向低功耗无线应用的MCU,其内置的I2C模块功能完备,是学习寄存器直接操作(即“裸机”开发)的绝佳样板。我们将不仅停留在“如何配置”,更会深入探讨“为何这样配置”,并结合我多年调试嵌入式总线的经验,分享那些数据手册里不会写的避坑技巧和实战心得。无论你是刚接触I2C的新手,还是希望深化对总线底层机制理解的老手,这篇内容都将为你提供一份可直接参考、复现的详细指南。
2. I2C协议核心原理深度拆解
在动手配置寄存器之前,我们必须吃透I2C协议的精髓。它不仅仅是一个电气连接标准,更是一套严谨的通信“礼仪”。理解这套礼仪,是后续一切调试工作的基础。
2.1 总线的基本“礼仪”:起始、停止与数据有效性
I2C总线只有两根线:串行数据线(SDA)和串行时钟线(SCL)。这两根线都需要通过上拉电阻连接到正电源,形成一个“线与”逻辑。这意味着任何设备都可以将线拉低(输出0),但只有当所有设备都释放时,线才会被上拉电阻拉高(为1)。这种设计是实现多主设备仲裁的基础。
通信的开始和结束由特定的信号序列定义,称为起始(S)条件和停止(P)条件。
- 起始条件:在SCL线为高电平期间,SDA线发生一个从高到低的跳变。这个独特的信号告诉总线上所有设备:“注意,我要开始讲话了”。
- 停止条件:在SCL线为高电平期间,SDA线发生一个从低到高的跳变。这表示:“我的话讲完了”。
所有数据传输都发生在这两个条件之间。这里有一个至关重要的规则:数据有效性。SDA线上的数据必须在SCL为高电平期间保持稳定,只有当SCL为低电平时,SDA线上的数据才允许变化。你可以把SCL想象成裁判的哨声,数据(SDA)必须在哨声响起的整个期间(高电平)保持一个固定的姿势,只有在哨声间隙(低电平)才能准备下一个动作。违反这个规则会导致数据错乱。
2.2 通信的“对话”单元:数据帧与地址帧
一次完整的数据传输以起始条件开始,紧接着是一个7位从机地址加1位读写方向位(R/W#)组成的地址帧。这个8位的帧决定了接下来要和哪个设备对话,以及是读数据还是写数据。
- 地址位(Bit 7-1):7位地址,理论上允许总线上挂载128个设备(其中一些地址保留,实际可用约112个)。
- 方向位(Bit 0):0表示主设备要向从设备写入数据(写操作);1表示主设备要向从设备读取数据(读操作)。
地址帧之后,主设备会释放SDA线,并产生一个时钟脉冲(第9个时钟),等待从设备的应答(ACK)。从设备如果识别出自己的地址,必须在这个时钟周期内将SDA线拉低,作为应答信号。如果地址不匹配或从设备忙,从设备则保持SDA为高,这被称为非应答(NACK)。主设备检测到NACK后,通常会发出停止条件来终止本次传输。
地址被应答后,便开始逐个字节的数据传输。每个字节(8位)传输完毕后,接收方都必须发送一个ACK位。数据也是高位(MSB)先传。传输的字节数没有限制,由主设备控制。当主设备作为接收方,且不希望再接收更多数据时,它可以在最后一个字节后发送一个NACK,然后发出停止条件。
2.3 高级“社交”规则:时钟拉伸与仲裁
I2C协议有两个非常巧妙的设计来应对现实世界的复杂性:时钟拉伸和总线仲裁。
时钟拉伸是从设备控制通信节奏的一种方式。如果从设备处理数据的速度跟不上主设备时钟(SCL)的速度,它可以在应答位或数据位之后,主动将SCL线拉低并保持。只要SCL被拉低,总线就进入等待状态,主设备必须等待从设备释放SCL后才能继续发送后续时钟。这相当于从设备对主设备说:“等等,我还没准备好”。CC27xx的I2C模块在目标(从机)模式下,正是通过内部状态机自动处理时钟拉伸,我们通过查询TSTAT.RREQ或TSTAT.TREQ位就能知道从机是否在等待数据。
总线仲裁解决了多个主设备同时发起通信的冲突问题。仲裁发生在SDA线上。当两个主设备同时开始传输时,它们会一边发送自己的数据,一边监测SDA线的实际电平。如果某个主设备输出高电平(释放总线),但检测到SDA线实际是低电平(被另一个主设备拉低),那么它就意识到自己“输”了,会立即停止发送,转为监听模式,等待总线空闲后再重试。仲裁可以持续多位,直到地址或数据出现差异。CC27xx的控制器模块通过CSTA.ARBLST位来报告是否在仲裁中失败,这对于多主系统至关重要。
3. CC27xx I2C模块架构与寄存器全景图
理解了协议,我们再把目光聚焦到CC27xx MCU内部的I2C模块。TI的文档将其清晰地划分为控制器(Controller)和目标(Target)两套逻辑,对应我们常说的主(Master)和从(Slave)。这两套逻辑有各自独立的一套寄存器组,但共享物理的SDA和SCL引脚。
3.1 模块功能框图解读
从提供的框图可以看出,I2C模块的核心是控制器核心(I2C Controller Core)和目标核心(I2C Target Core)。它们像两个独立的“引擎”,一个负责发起和控制通信(控制器),另一个负责响应请求(目标)。通过I2C I/O选择逻辑,这两个核心共享连接到外部引脚I2CSDA和I2CSCL。
控制器侧的关键寄存器包括:
I2C_CSA(Controller Slave Address): 写入你要访问的从机地址和读写方向。I2C_CCTL(Controller Control): 核心控制寄存器,用于触发START、STOP、RUN等操作。I2C_CDR(Controller Data): 数据寄存器,发送时写入数据,接收时读取数据。I2C_CTPR(Controller Timer Period): 设置SCL时钟频率的关键寄存器。I2C_CSTA(Controller Status): 状态寄存器,查询BUSY、ERROR、仲裁丢失等状态。
目标侧的关键寄存器包括:
I2C_TOAR(Target Own Address): 设置本设备作为从机时的地址。I2C_TCTL(Target Control): 目标使能控制。I2C_TDR(Target Data): 目标数据寄存器。I2C_TSTAT(Target Status): 目标状态寄存器,指示接收请求(RREQ)或发送请求(TREQ)。
中断系统两者都有对应的Raw Interrupt Status (RIS)、Interrupt Mask (IMR)、Masked Interrupt Status (MIS)和Interrupt Clear (ICR)寄存器,用于处理传输完成、错误、起始/停止条件检测等事件。
3.2 关键寄存器功能详解与配置逻辑
仅仅知道寄存器名字是不够的,我们必须理解每个关键位在通信流程中扮演的角色。这里我结合流程图和实战经验,重点剖析几个最核心的寄存器。
1. 控制器目标地址寄存器 (I2C_CSA): 这个寄存器决定了“和谁说话”以及“怎么说话”。其低8位有效,[7:1]位存放7位从机地址,[0]位是RS位(Receive/Send)。这里有一个极易混淆的点:RS=0表示控制器要**发送(写)数据给目标;RS=1表示控制器要接收(读)**数据从目标。这和我们从目标角度看的方向是相反的。配置时务必清醒。
2. 控制器控制寄存器 (I2C_CCTL): 这是控制器的“指挥棒”。只有4个有效控制位,但组合起来定义了完整的传输类型:
RUN(Bit 0): 置1启动控制器操作。一次传输中,通常只在开始时置一次。START(Bit 1): 置1在总线上产生一个起始条件或重复起始条件。STOP(Bit 2): 置1在本次数据传输结束后产生一个停止条件。ACK(Bit 3): 在控制器接收模式下,控制是否自动发送ACK。关键技巧:当控制器接收最后一个字节时,必须将此位清零,以发送NACK信号告知从机停止发送。
文档中的流程图(如Controller Single Transmit)给出了具体的数值范例。例如,发起一次单字节发送并停止,需要向CCTL写入0x07(二进制0111),即同时设置STOP=1,START=1,RUN=1。
3. 控制器状态寄存器 (I2C_CSTA): 这是诊断通信状态的“仪表盘”。我们需要频繁查询它:
BUSY(Bit 0): 指示控制器是否正在处理一次操作。重要提示:文档特别指出,在写入CCTL启动操作后,需要等待至少4个系统时钟周期再读取此位,否则可能读到错误状态。在48MHz系统时钟下,这大约83ns,但在代码中最好插入一个短暂延时或直接循环等待。ERR(Bit 1): 任何错误(如地址或数据未被应答)都会置位此位。每次操作后必须检查。ARBLST(Bit 4): 在多主系统中,若仲裁失败,此位置1。BUSBSY(Bit 6): 反映整个I2C总线(而非只是本控制器)是否繁忙。在发起通信前查询此位可以避免干扰正在进行的其他通信。
4. 控制器定时周期寄存器 (I2C_CTPR): 这是配置通信速率的核心。SCL时钟频率由公式SCL_PERIOD = 2 × (1 + TPR) × (SCL_LP + SCL_HP) × CLK_PRD决定。其中SCL_LP和SCL_HP是硬件固定值(通常为6和4),CLK_PRD是系统时钟周期,TPR就是我们写入CTPR[6:0]的值。 例如,系统时钟CLKSVT = 48MHz,目标SCL频率为100kHz(标准模式):TPR = [48,000,000 / (2 * (6+4) * 100,000)] - 1 = 23因此,向CTPR写入0x17(十进制23)即可。CTPR[7]位必须保持为0。
5. 目标状态寄存器 (I2C_TSTAT): 当CC27xx作为从机时,此寄存器告诉我们主设备想干什么:
RREQ(Bit 0): 置1表示主设备已向本目标写入数据,数据在TDR中待读取。TREQ(Bit 1): 置1表示主设备正请求从本目标读取数据,需要向TDR写入待发送数据。FBR(Bit 2): 与RREQ同时置位,表示刚刚收到的是地址帧后的第一个数据字节。这在处理多字节传输协议时很有用。
实操心得:寄存器配置中最常见的错误是位域理解偏差和时序疏忽。务必使用位操作(如
|=、&= ~)来清晰设置或清除特定位,避免直接赋值覆盖其他配置。对于CSTA.BUSY的查询,一定要在写入控制命令后加入足够的延迟,我通常用一个简单的for循环空转几次,这比依赖不精确的延时函数更可靠。
4. CC27xx I2C控制器模式配置与实践
现在,我们进入实战环节,以最常见的控制器(主机)模式为例,一步步完成从初始化到数据收发的完整流程。我将基于文档中的示例,并补充大量实际编程中需要的细节。
4.1 初始化与单字节发送实战
假设我们要向一个地址为0x3B的从设备发送一个字节数据0xA5,系统时钟48MHz,采用标准模式(100kbps)。以下是详细的C语言伪代码步骤和解析:
// 1. 使能I2C模块时钟与电源域 // 假设相关宏已定义,例如 CLKCTL0、IOCFG等寄存器地址 HWREG(CLKCTL0 + OFFSET_CLKENSET0) |= (1 << 6); // 使能I2C0时钟 // 2. 配置SDA和SCL引脚功能为I2C,并设置为开漏模式 // CC27xx的引脚复用通过IOC模块配置。这是关键且易错的一步! // 假设SDA使用GPIO pin 10, SCL使用GPIO pin 11 HWREG(IOCFG_BASE + PIN_10_CFG) = IOCFG_PORTID_I2C0_SDA | IOCFG_OPEN_DRAIN_EN; HWREG(IOCFG_BASE + PIN_11_CFG) = IOCFG_PORTID_I2C0_SCL | IOCFG_OPEN_DRAIN_EN; // 注意:必须使能内部上拉或连接外部上拉电阻,通常配置IOC寄存器中的PULL_CTL为上拉。 // 3. 初始化I2C控制器配置寄存器 (CCR) HWREG(I2C0_BASE + OFFSET_CCR) = 0x0010; // 使能控制器功能 (CFE=1) // 4. 配置SCL时钟频率 (CTPR) // 计算TPR = 23 (0x17), 用于100kHz HWREG(I2C0_BASE + OFFSET_CTPR) = 0x00000017; // 5. 指定目标地址和操作方向 (CSA) // 地址0x3B左移1位后为0x76, RS=0表示发送(写) HWREG(I2C0_BASE + OFFSET_CSA) = 0x00000076; // 0x3B << 1 // 6. 准备要发送的数据 (CDR) HWREG(I2C0_BASE + OFFSET_CDR) = 0xA5; // 7. 启动单字节传输 (CCTL) // 设置 STOP=1, START=1, RUN=1, ACK位在发送模式下无关 HWREG(I2C0_BASE + OFFSET_CCTL) = 0x00000007; // 8. 等待传输完成 (轮询BUSY位) // 重要:先等待至少4个CLKSVT周期 __asm__ volatile ("nop"); __asm__ volatile ("nop"); __asm__ volatile ("nop"); __asm__ volatile ("nop"); while (HWREG(I2C0_BASE + OFFSET_CSTA) & 0x01) { // 等待BUSY位清零 // 在实际项目中,这里应加入超时机制,防止死循环 } // 9. 检查传输是否成功 (检查ERR位) if (HWREG(I2C0_BASE + OFFSET_CSTA) & 0x02) { // ERR位被置1,传输失败(可能地址无应答) // 此处应进行错误处理,例如重试或记录日志 handle_i2c_error(); } else { // 传输成功 }关键点解析与避坑指南:
- 引脚配置是第一步,也是失败的重灾区。务必确认:
- 引脚功能复用到了I2C模块,而不是普通的GPIO。
- 模式设置为开漏输出(Open-Drain)。这是I2C总线“线与”特性的硬件基础,推挽输出会导致总线冲突甚至损坏设备。
- 上拉电阻必须存在。可以在芯片内部上拉使能,或者外部连接(通常4.7kΩ到10kΩ)。没有上拉,总线永远为低,通信无法进行。
CCR寄存器配置:CFE位使能控制器功能。如果也要用到目标功能,需要同时设置TFE位。LPBK位用于内部环回测试,正常通信时保持为0。- 时序等待:写入
CCTL后等待BUSY位清零前,必须先等待至少4个系统时钟周期。这是硬件要求,忽略它会导致状态误判。简单的nop指令或短暂延时循环即可。 - 错误处理:每次操作后检查
ERR位是良好习惯。如果出错,常见的做法是:发送停止条件(如果需要),重置I2C控制器(通过CCR寄存器),重新初始化,然后重试(通常有次数限制)。
4.2 多字节传输与重复起始条件
单字节传输是基础,但实际应用中更常见的是多字节读写,以及组合操作(如先写寄存器地址,再读数据)。这就需要用到重复起始条件(Repeated Start)。
重复起始条件是指在一次通信中,主设备在不释放总线(不发送停止条件)的情况下,再次发送一个起始条件,并可以切换读写方向或寻址另一个从机。这保证了复合操作的原子性,避免了被其他主设备打断。
场景:读取一个I2C温度传感器(假设地址0x48)的2字节温度值。该传感器需要先写入要读取的寄存器指针(例如0x00),然后重新起始并读取数据。
// 第一部分:发送寄存器指针(写操作) // 1. 设置目标地址和写方向 HWREG(I2C0_BASE + OFFSET_CSA) = (0x48 << 1) | 0x0; // RS=0, 写 // 2. 写入寄存器指针数据 HWREG(I2C0_BASE + OFFSET_CDR) = 0x00; // 要读取的寄存器地址 // 3. 启动传输,但不发送停止条件!(STOP=0, START=1, RUN=1) // 这会在发送完数据后,总线保持忙状态,等待后续操作。 HWREG(I2C0_BASE + OFFSET_CCTL) = 0x00000003; // STOP=0, START=1, RUN=1 // 4. 等待本次字节发送完成 delay_cycles(4); // 等待4周期 while (HWREG(I2C0_BASE + OFFSET_CSTA) & 0x01) {} // 等待BUSY if (HWREG(I2C0_BASE + OFFSET_CSTA) & 0x02) { handle_error(); } // 第二部分:重新起始并读取数据(读操作) // 5. 不改变CSA地址,但将方向改为读 (RS=1) // 注意:此时CSA寄存器中的RS位需要更新,但地址部分不变。 // 一种方法是重新写入整个CSA值,或者通过位操作只修改RS位。 HWREG(I2C0_BASE + OFFSET_CSA) = (0x48 << 1) | 0x1; // RS=1, 读 // 6. 对于多字节读取,需要仔细控制ACK。 // 假设读取2个字节。读取第一个字节时,控制器应发送ACK。 // 读取最后一个字节时,控制器应发送NACK,然后发送停止条件。 // 启动接收第一个字节,带ACK,无STOP,有重复START // 根据文档流程图,此时应写入 CCTL = 0x0B (二进制 1011)?这里需要对照文档图25-10。 // 实际上,对于“接收带重复起始”的流程,文档流程图给出了更复杂的多步骤控制。 // 简化流程(轮询方式): // a. 启动接收周期(带重复起始) HWREG(I2C0_BASE + OFFSET_CCTL) = 0x0000000B; // ACK=1, STOP=0, START=1, RUN=1 delay_cycles(4); while (HWREG(I2C0_BASE + OFFSET_CSTA) & 0x01) {} if (HWREG(I2C0_BASE + OFFSET_CSTA) & 0x02) { handle_error(); } uint8_t data_byte1 = HWREG(I2C0_BASE + OFFSET_CDR) & 0xFF; // 读取第一个字节 // b. 接收第二个字节(最后一个),发送NACK,然后发送STOP // 先修改ACK控制位为0(发送NACK)。注意:CCTL的ACK位只在写入时生效。 // 我们需要重新组合控制命令:ACK=0, STOP=1, START=0, RUN=1 -> 0x00000005? (0101) // 但根据流程图,最后一个字节接收后,需要发送STOP。流程是:等待BUSY结束,检查错误,然后读取数据。 // 更安全的做法是遵循文档中的状态机流程图(图25-10),它清晰地展示了多字节接收的轮询步骤。 // 由于篇幅,这里展示一个简化的、但可能不适用于所有情况的代码逻辑: // 在实际开发中,强烈建议实现一个状态机或使用中断来严格遵循官方流程图。 // 7. 读取第二个字节(带NACK和STOP) // 重新配置为接收最后一个字节的模式 HWREG(I2C0_BASE + OFFSET_CCTL) = 0x00000005; // ACK=0, STOP=1, START=0, RUN=1? 这里需要精确对照寄存器位。 // 注意:上述值仅为示例,必须根据CCTL寄存器定义(ACK[3], STOP[2], START[1], RUN[0])精确计算。 // 对于最后一个字节,通常的操作是:START=0(已是重复起始后的周期),RUN=1启动,ACK=0(NACK),STOP=1(结束后停止)。 // 假设 CCTL.ACK=bit3, STOP=bit2, START=bit1, RUN=bit0 // 则值应为: (0<<3) | (1<<2) | (0<<1) | (1<<0) = 0x00000005 delay_cycles(4); while (HWREG(I2C0_BASE + OFFSET_CSTA) & 0x01) {} if (HWREG(I2C0_BASE + OFFSET_CSTA) & 0x02) { handle_error(); } uint8_t data_byte2 = HWREG(I2C0_BASE + OFFSET_CDR) & 0xFF; // 读取第二个字节 // 传输结束,总线进入空闲状态。核心技巧:多字节和重复起始操作是I2C编程的难点。强烈建议不要使用上面简化的轮询代码直接用于生产环境。TI文档中的流程图(图25-9至25-12)是绝对权威的参考。最佳实践是:
- 封装底层函数:针对“单字节发送”、“单字节接收”、“启动传输(带/无STOP)”、“读取数据(带/无ACK)”等基本操作,编写健壮的、带错误处理和超时的函数。
- 实现状态机或使用中断:对于复杂的多步骤传输,使用状态机来跟踪当前步骤(如“等待BUSY清零”、“检查ERR”、“写入CDR”、“设置CCTL进行下一步”),或者使能控制器完成中断,在中断服务程序中进行下一步操作。这比死循环轮询更高效、更可靠。
- 仔细计算CCTL值:根据流程图每一步的要求,精确计算
CCTL寄存器的值。ACK、STOP、START、RUN位的组合决定了硬件的下一步行为。
5. CC27xx I2C目标模式配置与实践
让CC27xx作为从设备(目标)响应主设备的请求,是构建多设备系统的另一面。目标模式的配置相对控制器模式更简单,因为它主要是被动响应。
5.1 目标模式初始化与数据响应
假设我们希望CC27xx作为从机,地址设为0x50,响应主机的读写请求。
// 1. 使能I2C模块时钟与电源域 (同控制器模式) HWREG(CLKCTL0 + OFFSET_CLKENSET0) |= (1 << 6); // 2. 配置SDA和SCL引脚为I2C开漏模式 (同控制器模式,引脚需正确复用) HWREG(IOCFG_BASE + PIN_xx_CFG) = IOCFG_PORTID_I2C0_SDA | IOCFG_OPEN_DRAIN_EN; HWREG(IOCFG_BASE + PIN_yy_CFG) = IOCFG_PORTID_I2C0_SCL | IOCFG_OPEN_DRAIN_EN; // 3. 初始化I2C配置寄存器 (CCR), 使能目标功能 HWREG(I2C0_BASE + OFFSET_CCR) = 0x0020; // 使能目标功能 (TFE=1) // 如果同时作为主从,可以设置为 0x0030 (CFE=1 & TFE=1) // 4. 设置目标自身地址 (TOAR) HWREG(I2C0_BASE + OFFSET_TOAR) = 0x50; // 写入7位地址 0x50 // 5. 激活目标设备 (TCTL) HWREG(I2C0_BASE + OFFSET_TCTL) = 0x00000001; // 设置DA位为1,使能目标 // 6. 主循环中,轮询目标状态寄存器 (TSTAT), 响应请求 void i2c_target_polling_loop(void) { uint32_t tstat; while(1) { tstat = HWREG(I2C0_BASE + OFFSET_TSTAT); // 检查是否有接收数据请求 (主机写数据到本目标) if (tstat & 0x01) { // RREQ位为1 // 读取收到的数据 uint8_t received_data = HWREG(I2C0_BASE + OFFSET_TDR) & 0xFF; // 处理数据... // 读取TDR会自动清除RREQ状态(根据文档,读TDR后FBR位被清除,但RREQ/TREQ的清除机制需确认) // 通常,处理完数据后,硬件状态会更新。 } // 检查是否有发送数据请求 (主机从本目标读数据) if (tstat & 0x02) { // TREQ位为1 // 准备要发送的数据 uint8_t data_to_send = get_data_to_send(); // 写入目标数据寄存器 HWREG(I2C0_BASE + OFFSET_TDR) = data_to_send; // 写入TDR后,硬件会自动将数据发出,并释放时钟拉伸。 // TREQ位会在数据被取走后自动清除。 } // 可选:检查起始/停止条件中断(如果使能了中断) // ... } }目标模式工作逻辑:
- 主机发送起始条件+地址帧(
0xA0, 即0x50<<1 | 0,写方向)。地址匹配后,CC27xx的I2C目标模块会拉低SDA应答。 - 主机开始发送数据字节。每收到一个字节,目标模块会置位
TSTAT.RREQ,并将数据存入TDR。同时,目标模块会执行时钟拉伸,直到软件读取TDR寄存器,RREQ位才会清除,时钟拉伸释放,通信继续。 - 如果主机发送的是读请求(地址帧为
0xA1,即0x50<<1 | 1),则目标模块置位TSTAT.TREQ,并拉伸时钟。软件必须尽快将待发送数据写入TDR,写入后硬件自动发送数据并释放时钟。 TSTAT.FBR位在与RREQ同时置位时,表示当前收到的是地址帧后的第一个数据字节。这对于解析命令字非常有用。
5.2 中断驱动 vs. 轮询模式
上面的示例使用了轮询(Polling)TSTAT寄存器的方式。这对于简单应用或低数据速率场景可行,但会占用CPU资源。
更高效的方式是使用中断。目标模式可以产生三种中断:
- 数据中断(
DATAIM): 当收到数据(RREQ)或收到发送请求(TREQ)时触发。 - 起始条件中断(
STARTIM): 检测到总线起始条件时触发。 - 停止条件中断(
STOPIM): 检测到总线停止条件时触发。
中断配置示例:
// 使能目标数据中断 HWREG(I2C0_BASE + OFFSET_TIMR) = 0x01; // 设置DATAIM位为1 // 在系统中断控制器中使能I2C目标中断线 // ... (取决于具体MCU的中断控制器配置) // 中断服务程序 (ISR) void I2C0_Target_IRQHandler(void) { uint32_t tmis = HWREG(I2C0_BASE + OFFSET_TMIS); // 读取屏蔽中断状态 if (tmis & 0x01) { // DATAMIS中断 uint32_t tstat = HWREG(I2C0_BASE + OFFSET_TSTAT); if (tstat & 0x01) { // RREQ uint8_t data = HWREG(I2C0_BASE + OFFSET_TDR); process_received_data(data); } if (tstat & 0x02) { // TREQ uint8_t data = prepare_data(); HWREG(I2C0_BASE + OFFSET_TDR) = data; } // 清除数据中断 HWREG(I2C0_BASE + OFFSET_TICR) = 0x01; // 写1清除DATAIC } // 检查并清除其他中断 (STARTIC, STOPIC) ... }使用中断可以极大解放CPU,让MCU在等待I2C事件时处理其他任务,是实际项目中的推荐方式。
6. 常见问题排查与调试技巧实录
即使按照手册一步步配置,I2C通信依然可能出问题。以下是我在多年调试中总结的常见故障现象、排查思路和实用技巧。
6.1 典型故障现象与排查步骤
| 故障现象 | 可能原因 | 排查步骤与工具 |
|---|---|---|
| 总线死锁,SCL或SDA线被持续拉低 | 1. 从设备故障或未正确初始化。 2. 通信序列意外中断,从设备处于时钟拉伸状态。 3. 硬件短路或上拉电阻损坏。 | 1.用逻辑分析仪或示波器观察波形,这是最直接的手段。看是哪一方在拉低线路。 2. 逐个断开从设备,定位故障设备。 3. 检查MCU和从设备的电源、复位信号是否正常。 4. 尝试发送多个时钟脉冲。有些MCU提供I2C总线恢复功能,或在GPIO模拟I2C时,可以手动产生SCL时钟(9个以上)直到SDA被释放,然后发送一个停止条件。 |
| 地址无应答(NACK) | 1. 从设备地址错误。 2. 从设备未上电或复位。 3. 总线电平问题(上拉电阻过大导致上升沿太慢)。 4. 从设备忙或处于不可访问状态(如正在写EEPROM)。 | 1. 确认从设备7位地址,并左移1位加上R/W位。 2. 用示波器测量从设备VCC和GND。 3.测量总线波形,看SCL/SDA的上升时间。标准模式要求上升时间<1us,快速模式<300ns。上升太慢可能导致识别错误。尝试减小上拉电阻值(如从10kΩ换为4.7kΩ)。 4. 查阅从设备数据手册,看是否有内部写周期需要等待。 |
| 数据错误或校验失败 | 1. 时钟速度过快,从设备跟不上。 2. 电源噪声或地线干扰。 3. 软件读取/写入数据寄存器时序不对。 4. 中断与主程序竞争资源。 | 1.降低SCL频率(增大CTPR值)测试。2. 检查PCB布局,I2C走线是否远离噪声源(电源、电机驱动等),是否包地。确保共地良好。 3.严格检查代码:在读取 CDR前是否确认BUSY已清零?在写入CDR后是否等待足够时间再启动传输?多字节传输的ACK/NACK控制序列是否正确?4. 在访问I2C数据/状态寄存器的关键段,考虑禁用全局中断。 |
| 只能读写第一个字节,后续失败 | 1. 多字节传输的ACK/NACK控制错误。 2. 从设备对于多字节操作有特殊协议(如需要发送内部寄存器地址自增命令)。 3. 中断处理不当,未及时服务导致超时。 | 1.仔细对照数据手册的流程图,特别是CCTL.ACK位的控制。接收倒数第二个字节应发ACK,最后一个字节发NACK。2. 确认从设备的数据手册,多字节读写是否需要发送特定命令字。 3. 如果使用中断,确保ISR执行时间足够短,没有错过下一个字节的请求。 |
6.2 高级调试技巧与最佳实践
充分利用环回模式(Loopback):CC27xx的I2C模块支持内部环回(设置
CCR.LPBK=1)。在此模式下,控制器的输出直接连接到目标的输入,无需外部设备。这是测试驱动代码是否正确的黄金手段。先让控制器给自己发数据,如果能正常收发,证明寄存器配置、时序控制、中断处理等底层代码是正确的,排除了软件问题。示波器/逻辑分析仪是“终极法官”:当通信异常时,不要盲目猜测。用示波器观察SCL和SDA的实际波形。检查:
- 起始/停止条件:波形是否符合规范?
- 地址和数据帧:发出的地址和数据是否正确?ACK位是否有下拉?
- 时钟频率:实测频率是否与配置值相符?
- 信号质量:是否有过冲、振铃、毛刺?上升/下降时间是否满足要求?
编写可重入的、带超时的I2C驱动函数:你的
I2C_ReadByte、I2C_WriteByte等函数内部必须有超时机制。例如,等待BUSY清零的循环,超过一定次数(如10000次)后自动跳出并返回超时错误。这能防止程序因硬件故障而完全死锁。注意电源与电平兼容:如果MCU和I2C从设备采用不同电压(如3.3V vs 5V),需要使用电平转换器。直接连接可能导致通信不稳定或损坏器件。
上拉电阻的选择与布局:上拉电阻的值(Rp)需要权衡。电阻小,总线电容充电快,上升时间短,适合高速或长距离,但功耗大。电阻大,功耗小,但上升时间慢。通常3.3V系统下,4.7kΩ是一个安全的起点。上拉电阻应靠近主设备放置。如果总线负载重(设备多、走线长),可能需要减小电阻值。
通过将协议原理、寄存器手册、流程图和实战调试经验相结合,我们才能建立起对I2C总线扎实而深刻的理解。CC27xx的I2C模块是一个很好的学习平台,掌握了它的寄存器级操作,再去看其他MCU的I2C外设或者使用更高层次的库函数(如TI的DriverLib),都会有一种豁然开朗的感觉。嵌入式开发就是这样,底层寄存器是基石,理解它,就能构建出稳定可靠的通信大厦。