1. I2C中断机制深度解析:从轮询到事件驱动的效率跃迁
在嵌入式开发中,尤其是面对传感器阵列、EEPROM、实时时钟这类需要频繁进行小数据量交互的外设时,通信效率直接决定了系统的响应速度和整体性能。I2C总线以其简洁的两线制(SDA数据线、SCL时钟线)和多主多从的架构,成为了嵌入式领域的常客。然而,很多开发者,尤其是初学者,往往停留在最基本的轮询式读写操作上,让宝贵的CPU周期浪费在等待ACK或检查总线状态上。今天,我们就来深入聊聊如何利用I2C模块内置的中断机制,将你的系统从“忙等待”的泥潭中解放出来,实现真正高效的事件驱动通信。以经典的Freescale(现NXP)56F80x系列微控制器的I2C模块为例,其寄存器设计清晰地展现了中断驱动的精髓。
轮询的困境:想象一下,你的主控MCU需要从10个温度传感器读取数据。如果采用轮询,程序流程大致是:启动传输->等待传输完成标志->读取数据->处理->下一个传感器。这个“等待”过程,CPU其实是在空转,不断地读取某个状态寄存器位,什么实质工作也做不了。当传感器增多或通信速率受限时,这种浪费尤为明显,系统实时性大打折扣。
中断的优势:中断机制则完全不同。你只需在初始化时配置好“哪些事件发生时需要通知CPU”,例如“接收FIFO数据达到阈值”、“一次传输完成”或“总线出现错误”。配置完成后,CPU就可以转头去执行其他更重要的任务,比如运行控制算法、刷新显示或处理网络数据。当预设的I2C事件发生时,硬件会自动拉起中断信号,CPU暂停当前工作,跳转到对应的中断服务程序(ISR)进行快速处理(如搬移数据、清除标志),处理完毕立即返回。整个过程,CPU只在必要时介入,实现了并发处理能力。
56F80x的I2C模块将中断源分门别类,组织得非常清晰。其核心是两组状态寄存器:RISTAT(原始中断状态寄存器)和ISTAT(中断状态寄存器)。RISTAT是“实况转播”,无论你是否关心,任何事件发生都会立刻置位对应的位。而ISTAT则是“订阅频道”,只有你在IENBL(中断使能寄存器)中使能了的事件,其状态才会反映到ISTAT中,并最终可能产生CPU中断。这种设计给了开发者极大的灵活性:你可以通过查询RISTAT进行调试,了解总线上发生的一切;而在产品代码中,则通过配置IENBL来精准控制触发中断的事件,避免不必要的打断。
1.1 核心中断源分类与功能解读
56F80x的I2C中断可以归纳为五大类,每一类都对应着通信流程中的关键节点或异常情况。理解它们是正确配置的前提。
1. 错误中断(Error Interrupt):这是系统的“安全气囊”。当通信出现异常时,这类中断能确保系统不被挂死,而是进入错误处理流程。主要包括:
- TXABRT(传输中止):这是最复杂的错误源。当I2C模块无法完成CPU请求的操作时触发。原因可能多达十余种,例如:主设备在仲裁中丢失总线、从设备模式下收到了读请求却试图执行写操作、尝试在不允许重复起始条件的情况下发起特定操作、发送的地址或数据未收到从设备的应答(NACK)等。一旦发生,TXABRT位会置位,并且发送和接收FIFO会被清空,以防止错误数据被误用。
- TXOVR(发送溢出):当发送FIFO已满,CPU却试图继续写入数据时触发。这通常意味着软件发送数据的速度快于硬件串行化的速度,或者中断处理不及时。
- RXOVR(接收溢出):当接收FIFO已满,总线上却还有数据传来时触发。从设备会正常应答(ACK),但溢出的数据字节会丢失。这是严重的通信错误,意味着数据接收跟不上。
- RXUND(接收欠载):当接收FIFO为空,CPU却试图从中读取数据时触发。这通常是由于软件读取逻辑有误,或者与从设备的通信协议不同步。
2. 传输中断(Transmit Interrupt):管理数据发送流程,实现“按需供给”。
- TXDONE(发送完成):在从发送器模式下,当主设备没有应答(NACK)最后一个发送的字节时,此位置位。这实际上是主设备发送的一个“停止读取”信号,告知从设备:“数据够了,本次传输结束”。对于从设备来说,这是一个明确的完成事件。
- RDREQ(读请求):这是从设备发送模式下的核心中断。当I2C模块作为从发送器,且主设备发起读请求时,此位置位。此时,I2C模块会主动将SCL时钟线拉低(总线等待),直到CPU向发送FIFO写入数据。这给了CPU准备数据的时间,是实现从设备动态响应的关键。
- TXEMPTY(发送空):当发送FIFO中的数据量达到或低于TXFT(发送FIFO阈值)寄存器设定的阈值时置位。你可以将其理解为“发送缓冲区快空了,需要补充弹药”。通过合理设置阈值,可以提前触发中断,让CPU有时间准备下一批数据,从而避免发送过程中出现断流。
3. 接收中断(Receive Interrupt):管理数据接收流程,实现“批量处理”。
- RXFULL(接收满):当接收FIFO中的数据量达到或超过RXFT(接收FIFO阈值)寄存器设定的阈值时置位。这是最常用的接收中断。与其在每收到一个字节就中断一次,不如设置一个阈值(例如,FIFO深度为4时,设置阈值为2),当收到2个或更多字节时再一次性中断处理,大大降低了中断频率,提升了系统效率。
4. 状态中断(Status Interrupt):监控总线物理层状态。
- STDET(起始条件检测):总线上出现起始(START)或重复起始(Repeated START)条件时置位。可用于监听总线活动,或在多主系统中判断通信的开始。
- STPDET(停止条件检测):总线上出现停止(STOP)条件时置位。标志着一帧通信的结束。
- ACT(活动状态):当总线从空闲状态转为忙碌状态时置位。它是主设备活动(MSTACT)和从设备活动(SLVACT)状态的逻辑或(OR)。一个重要的细节是:如果I2C总线仍处于忙碌状态,即使CPU读取CLRACT寄存器,此位也不会被清除。这确保了状态的真实性。
5. 通用呼叫中断(General Call Interrupt):
- GC:当总线上出现通用呼叫地址(0x00)时置位。通用呼叫是一种广播地址,所有从设备都能识别,用于同时向总线上的所有设备发送命令或数据。
1.2 中断使能与处理流程精要
理解了中断源,配置就变得有章可循。中断处理的典型流程如下:
初始化配置:
- 配置I2C时钟速率、自身地址等基本参数。
- 根据应用需求,设置TXFT和RXFT阈值。例如,如果每次通信都是读取4字节的传感器数据,可以将RXFT设置为3(当FIFO中有3或4个数据时触发中断),这样一次中断就能取走全部数据。
- 向IENBL寄存器写入值,使能所需的中断源。例如,如果设备主要作为从接收器,可以开启RXFULL和错误中断;如果作为从发送器,则需要开启RDREQ、TXEMPTY和TXDONE。
中断服务程序(ISR)设计:
- 进入ISR后,首先读取ISTAT寄存器,判断具体是哪个(或哪些)中断事件触发。
- 根据ISTAT的值,跳转到对应的处理分支。
- 在处理分支中,执行必要操作(如从RXFIFO读取数据、向TXFIFO写入数据),然后必须读取对应的清除寄存器来清除中断标志位。这是关键!例如,处理完接收数据后,需要读取CLRRXOVR(如果发生了溢出)或通过读取数据降低FIFO水平来自动清除RXFULL;处理发送后,可能需要读取CLRTXDONE。
- 对于TXABRT这种复杂错误,除了读取CLRTXABRT清除中断标志,强烈建议同时读取TXABRTSRC(传输中止源寄存器)。这个14位的寄存器会精确告诉你中止的原因(如地址无应答、仲裁丢失等),这对于调试和实现健壮的错误恢复机制至关重要。
关键经验:中断标志清除的“读操作”56F80x I2C模块的中断清除机制非常典型:通过读取(Read)特定的清除寄存器来完成,而不是写入。例如,清除ACT中断是读取CLRACT寄存器的地址,清除TXABRT是读取CLRTXABRT寄存器的地址。在C语言编程中,这常常表现为对一个volatile指针的解引用操作,例如:
dummy = *((volatile uint16_t*)CLRACT_ADDR);。这个“dummy”读取操作本身,就是向硬件发出的清除指令。
2. 关键寄存器配置详解与实战策略
寄存器是开发者与I2C硬件模块对话的窗口。56F80x的I2C寄存器虽然看起来繁多,但逻辑清晰。我们将其分为几类,并结合代码片段讲解如何配置。
2.1 核心控制与状态寄存器组
ENBL(使能寄存器) - 地址基址 + $36这是I2C模块的总开关。Bit 0 (EN) 置1使能整个I2C模块,清0则禁用。
- 禁用时的行为:当EN=0时,接收FIFO (RXFIFO) 会被清空并保持复位状态,TXFLR和RXFLR寄存器被清零。但需要注意的是,RISTAT和ISTAT寄存器中的状态位会保持活动,直到I2C模块真正进入空闲状态。这为你安全地关闭模块提供了判断依据。
- 重要警告:数据手册中特别强调,应避免在ENBL寄存器为0时向DATA寄存器写入数据,否则可能导致不可预测的操作。因此,正确的操作顺序是:先配置其他参数,最后置位EN;关闭时,先确保通信完成,再清除EN。
STAT(状态寄存器) - 地址基址 + $38这是一个只读寄存器,提供实时的、非中断相关的FIFO和活动状态,非常适合在轮询或中断ISR中快速判断硬件状态。
- Bit 6 (SLVACT) / Bit 5 (MSTACT):分别指示从机或主机的有限状态机是否处于非空闲状态。你可以通过它们精确知道当前模块是作为主设备在发起通信,还是作为从设备在响应请求。
- Bit 4 (RFF) / Bit 3 (RFNE):指示接收FIFO是否完全满(RFF=1)以及是否非空(RFNE=1)。RFNE位可以被软件用来安全地、彻底地清空接收FIFO(while(RFNE) { data = read_DATA(); })。
- Bit 2 (TFE) / Bit 1 (TFNF):指示发送FIFO是否完全空(TFE=1)以及是否非满(TFNF=1)。在发送数据前,检查TFNF可以确保不会发生溢出(TXOVR)。
- Bit 0 (ACT):总线活动状态,是SLVACT和MSTACT的逻辑或。一个快速判断总线是否繁忙的方法。
TXFLR / RXFLR(FIFO级别寄存器) - 地址基址 + $3A / + $3C这两个只读寄存器分别指示发送和接收FIFO中当前有效数据的条目数。它们对于实现高级数据流管理非常有用。例如,在DMA传输中,你可以根据TXFLR的值来判断何时需要触发DMA请求以填充更多数据,或者根据RXFLR的值来判断何时可以启动DMA读取。当I2C模块被禁用或发生传输中止(TXABRT)时,这两个寄存器会被清零。
2.2 中断配置寄存器组实战
IENBL(中断使能寄存器)这是中断系统的“订阅管理器”。它的每一位与RISTAT/ISTAT寄存器中的中断状态位一一对应。向某一位写1,就表示“当这个事件发生时,请将其反映到ISTAT中,并可能产生中断信号”。你的配置直接决定了系统的中断行为模式。
一个典型从设备发送/接收配置示例: 假设我们设计一个智能传感器从设备,它需要:
- 快速响应主设备的读请求(发送数据)。
- 高效接收主设备的配置命令(接收数据)。
- 及时知晓传输完成。
- 能处理任何通信错误。
对应的IENBL配置可能如下(假设寄存器位定义与手册一致):
// 假设寄存器地址定义 #define I2C_IENBL (*(volatile uint16_t*)(I2C_BASE + 0x0C)) // 假设IENBL偏移量为0x0C void I2C_Slave_Interrupt_Init(void) { uint16_t temp = 0; // 使能错误中断:传输中止、溢出、欠载 temp |= (1 << 6); // 使能 TXABRT 中断 temp |= (1 << 3); // 使能 TXOVR 中断 temp |= (1 << 1); // 使能 RXOVR 中断 temp |= (1 << 0); // 使能 RXUND 中断 // 使能传输中断:读请求、发送完成、发送空(用于提前填充) temp |= (1 << 5); // 使能 RDREQ 中断 temp |= (1 << 7); // 使能 TXDONE 中断 temp |= (1 << 4); // 使能 TXEMPTY 中断,阈值需另设 // 使能接收中断:接收满 temp |= (1 << 2); // 使能 RXFULL 中断,阈值需另设 // 写入IENBL寄存器 I2C_IENBL = temp; }TXFT / RXFT(FIFO阈值寄存器) - 地址基址 + $1E / + $1C这两个寄存器是平衡中断频率与响应延迟的关键。它们都是8位寄存器,但只有低2位(Bit 1-0)有效,因为FIFO深度为4(00, 01, 10, 11对应0-3个条目)。
- TXFT(发送FIFO阈值):设置一个值N(0-3)。当TXFIFO中的数据条目数小于或等于N时,TXEMPTY状态位(及其中断)被触发。例如,设置TXFT=1,意味着当FIFO中数据少于等于1个时,就请求CPU来补充数据。这可以避免发送过程中FIFO完全清空导致的通信暂停。
- RXFT(接收FIFO阈值):设置一个值M(0-3)。当RXFIFO中的数据条目数大于或等于M时,RXFULL状态位(及其中断)被触发。例如,设置RXFT=3,意味着当FIFO收到3个或4个数据时,才产生中断让CPU一次性读取。这显著减少了中断次数。
配置示例:
// 设置发送阈值:当FIFO中数据<=1个时触发TXEMPTY I2C_TXFT = 1; // 写入阈值1 // 设置接收阈值:当FIFO中数据>=3个时触发RXFULL I2C_RXFT = 3; // 写入阈值32.3 中断清除寄存器组与ISR编写范式
所有以“CLR”开头的寄存器(如CLRACT, CLRTXDONE, CLRTXABRT等)都是只读寄存器。读取它们的操作,就是清除对应中断标志位的信号。在ISR中,必须根据检测到的中断源,读取相应的清除寄存器。
一个综合性的I2C中断服务程序框架:
__interrupt void I2C_IRQ_Handler(void) { uint16_t istat_value = I2C_ISTAT; // 读取中断状态寄存器 // 1. 处理错误中断(优先级通常最高) if (istat_value & (1<<6)) { // TXABRT uint16_t abort_src = I2C_TXABRTSRC; // 读取中止源,用于诊断 // ... 错误处理逻辑,如重试、记录日志、恢复状态 ... uint16_t clear_dummy = I2C_CLRTXABRT; // 读取清除寄存器,清除TXABRT标志 } if (istat_value & (1<<3)) { // TXOVR // ... 处理发送溢出,检查发送逻辑 ... uint16_t clear_dummy = I2C_CLRTXOVR; } if (istat_value & (1<<1)) { // RXOVR // ... 处理接收溢出,数据已丢失,需重置通信 ... uint16_t clear_dummy = I2C_CLRRXOVR; } if (istat_value & (1<<0)) { // RXUND // ... 处理接收欠载,检查读取逻辑 ... uint16_t clear_dummy = I2C_CLRRXUND; } // 2. 处理接收中断 if (istat_value & (1<<2)) { // RXFULL // 循环读取,直到FIFO为空或低于阈值 while (I2C_STAT & (1<<3)) { // 检查STAT.RFNE (接收FIFO非空) g_rx_buffer[rx_index++] = I2C_DATA; // 从DATA寄存器读取数据 if (rx_index >= BUFFER_SIZE) { /* 处理缓冲区满 */ } } // RXFULL标志会在FIFO数据被读走后由硬件自动清除 } // 3. 处理传输中断 if (istat_value & (1<<5)) { // RDREQ (从发送模式,主设备要读数据) // 主设备正在等待数据,SCL被拉低 // 准备数据并写入TX FIFO if (tx_data_available) { I2C_DATA = g_tx_buffer[tx_index++]; // 写入数据,这会释放SCL,通信继续 } else { // 无数据可发,可以写入一个默认值或触发错误 } uint16_t clear_dummy = I2C_CLRRDREQ; // 清除RDREQ标志 } if (istat_value & (1<<7)) { // TXDONE // 作为从设备,发送被主设备NACK,本次发送序列结束 tx_in_progress = false; // ... 后续处理 ... uint16_t clear_dummy = I2C_CLRTXDONE; } if (istat_value & (1<<4)) { // TXEMPTY // 发送FIFO快空了,提前准备更多数据 if (tx_data_available && (I2C_STAT & (1<<1))) { // 检查STAT.TFNF (发送FIFO非满) I2C_DATA = g_tx_buffer[tx_index++]; } // TXEMPTY标志会在写入数据使FIFO水平超过阈值后由硬件自动清除 } // 4. 处理状态中断(如需要) if (istat_value & (1<<9)) { // ACT // 总线活动检测,可用于功耗管理或调试 uint16_t clear_dummy = I2C_CLRACT; // 注意:仅当总线空闲时才能清除 } // ... 处理其他状态中断 ... // 最后,可能需要清除汇总的中断标志(取决于MCU的中断控制器) }3. 高级应用场景与故障排查实战
掌握了基础配置和中断处理,我们来看看如何将这些机制应用到复杂场景中,并解决那些让人头疼的典型问题。
3.1 多字节传输与流控制策略
在实际应用中,单字节读写很少见,更多的是多字节的块传输。中断结合FIFO阈值是处理多字节传输的利器。
场景一:主设备读取从设备的多字节数据(如传感器读数)
- 从设备侧(发送方)配置:
- 使能RDREQ和TXEMPTY中断。
- 设置一个合理的TXFT阈值,例如1。
- 当RDREQ中断到来(主设备发起读请求),在ISR中立即向TX FIFO写入第一个数据字节,并清除RDREQ标志。这会释放SCL线,主设备开始读取第一个字节。
- 由于TX FIFO被消耗,当其中数据量低于TXFT阈值时,会触发TXEMPTY中断。在TXEMPTY的ISR中,继续填充后续数据到TX FIFO。这样形成了一个“流水线”:主设备在前面读,从设备的ISR在后面填,只要填充速度不低于读取速度,通信就能流畅进行。
- 当主设备发送NACK(最后一个字节),从设备会触发TXDONE中断,标志本次读取会话结束。
场景二:主设备向从设备写入多字节数据(如配置参数)
- 从设备侧(接收方)配置:
- 使能RXFULL中断。
- 设置RXFT阈值等于你期望一次性处理的字节数。例如,如果配置命令总是4字节,则设置RXFT=3。这样,当4字节数据全部到达FIFO后,才产生一次中断,ISR一次性读取4字节进行处理,效率远高于每字节中断一次。
- 在RXFULL的ISR中,使用while循环结合检查STAT.RFNE位,确保将FIFO中的所有数据读空。
3.2 传输中止(TXABRT)深度诊断与恢复
TXABRT是最常见的错误之一,原因复杂。盲目重试往往无效,必须根据TXABRTSRC寄存器进行诊断。
| TXABRTSRC位 | 名称 | 触发条件 | 典型原因与处理策略 |
|---|---|---|---|
| Bit 0 | AD7NACK | 7位地址无应答 | 从设备地址错误、从设备未上电、总线连接问题。检查地址、电源和线路。 |
| Bit 1 | AD1NACK | 10位地址第一字节无应答 | 同上,针对10位地址模式。 |
| Bit 2 | AD2NACK | 10位地址第二字节无应答 | 从设备不支持10位地址,或第二字节传输出错。 |
| Bit 3 | TDNACK | 数据字节无应答 | 从设备在接收数据过程中(如写入寄存器)因内部忙、写保护或数据错误而回NACK。检查从设备状态和发送的数据。 |
| Bit 4 | GCNACK | 通用呼叫无应答 | 总线上没有设备响应广播地址,正常。 |
| Bit 5 | GCREAD | 通用呼叫后发读命令 | 软件协议错误。通用呼叫后通常只跟写操作。 |
| Bit 7 | SACKDET | START字节被应答 | 在启用START字节模式(一种扩展寻址方式)时,START字节不应被应答。可能总线有设备行为异常。 |
| Bit 9 | SNORST | 禁止重复START时尝试发送START字节 | 控制器配置矛盾。需要使能重复START(CTRL[5]=1),或禁用特殊模式(TAR[11]=0, TAR[10]=0)。注意:此位清除逻辑特殊,需先修复配置源。 |
| Bit 10 | RNORST | 禁止重复START时尝试10位地址读 | 10位地址读操作需要重复START条件。需使能CTRL[5](RSTEN)。 |
| Bit 11 | MSTDIS | 尝试主模式命令但主模式被禁用 | 检查I2C控制寄存器,确保主模式已使能。 |
| Bit 12 | AL | 仲裁丢失 | 多主竞争总线失败。通常是正常现象,应重新发起传输。 |
| Bit 13 | SLVFLSH | 从设备收到读请求但TX FIFO有旧数据 | 前一次发送的数据未消耗完。应在每次TXDONE或传输结束后清空/重置TX FIFO状态。 |
| Bit 14 | SLVAL | 从设备发送时仲裁丢失 | 从设备在响应读请求发送数据时,与其他主设备发送冲突。较少见,检查多主总线管理。 |
| Bit 15 | SLVRD | 从设备期待写命令却收到读命令 | 软件协议顺序错误。从设备在作为发送器时,CPU错误地发出了读命令(向DATA寄存器写入了读控制位)。 |
恢复流程建议:
- 发生TXABRT中断后,在ISR中立即读取TXABRTSRC值并保存到日志或变量中。
- 读取CLRTXABRT寄存器清除中断标志。
- 根据TXABRTSRC的值执行不同的恢复策略:
- 对于地址/数据无应答(ADxNACK, TDNACK):延时后重试,重试次数超过阈值则上报错误。
- 对于仲裁丢失(AL):立即或短延时后重新发起传输。
- 对于配置错误(SNORST, RNORST, MSTDIS, GCREAD):检查并修正软件配置,可能需重新初始化I2C模块。
- 对于SLVFLSH:清空TX FIFO,并重新评估数据准备逻辑。
- 在恢复操作前,确保通过查询STAT.ACT位或等待足够时间,确保总线已恢复空闲状态。
3.3 常见问题排查速查表
| 现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 无法进入中断 | 1. IENBL寄存器未正确使能。 2. 处理器全局中断未开启。 3. I2C模块的EN位未使能。 4. 中断向量表配置错误。 | 1. 检查IENBL写入值。 2. 检查CPU的全局中断使能位。 3. 确认ENBL寄存器的EN=1。 4. 核对启动文件和中断服务函数名。 |
| 中断频繁触发,系统卡死 | 1. 中断标志未清除,导致连续中断。 2. FIFO阈值设置不合理(如RXFT=0)。 3. ISR执行时间过长。 | 1. 确保ISR中读取了对应的CLR寄存器。 2. 调整TXFT/RXFT至合理值。 3. 优化ISR,只做最必要的操作(如搬移数据),将复杂处理放到主循环。 |
| 数据丢失(接收方) | 1. RXOVR中断发生(FIFO溢出)。 2. RXFULL中断处理太慢,未及时读取。 | 1. 检查ISR中是否处理了RXOVR。 2. 提高RXFULL中断优先级,或增大RXFT值以提前预警。 |
| 发送停滞(从设备) | 1. RDREQ中断未及时响应,SCL被无限拉低。 2. TXEMPTY中断未及时填充数据,FIFO耗尽。 3. 发生了TXABRT。 | 1. 确保RDREQ中断使能且ISR响应快。 2. 检查TXEMPTY中断和填充逻辑。 3. 检查TXABRT状态和TXABRTSRC。 |
| 只能通信一次,后续失败 | 1. 传输完成后状态未正确复位。 2. TXABRT发生后未正确恢复。 3. FIFO中残留数据导致后续协议错乱。 | 1. 在每次传输序列结束后,检查并清除所有相关状态位。 2. 实现完整的TXABRT诊断与恢复流程。 3. 在通信开始前,确认STAT寄存器显示FIFO为空、总线空闲。 |
4. 从理论到实践:一个完整的从设备中断驱动示例
让我们整合所有知识,为一个虚拟的“环境传感器从设备”编写一个简化的、中断驱动的I2C从机驱动程序框架。该设备地址为0x48,支持主设备读取4字节的传感器数据(温度、湿度),并接收2字节的配置命令。
// 假设的寄存器地址映射(需根据具体芯片手册修改) #define I2C_BASE 0x4000 #define I2C_ENBL (*(volatile uint16_t*)(I2C_BASE + 0x36)) #define I2C_STAT (*(volatile uint16_t*)(I2C_BASE + 0x38)) #define I2C_DATA (*(volatile uint16_t*)(I2C_BASE + 0x00)) // DATA寄存器地址假设 #define I2C_IENBL (*(volatile uint16_t*)(I2C_BASE + 0x0C)) #define I2C_ISTAT (*(volatile uint16_t*)(I2C_BASE + 0x08)) #define I2C_TXFT (*(volatile uint16_t*)(I2C_BASE + 0x1E)) #define I2C_RXFT (*(volatile uint16_t*)(I2C_BASE + 0x1C)) #define I2C_CLRRDREQ (*(volatile uint16_t*)(I2C_BASE + 0x28)) #define I2C_CLRTXDONE (*(volatile uint16_t*)(I2C_BASE + 0x2C)) #define I2C_CLRTXABRT (*(volatile uint16_t*)(I2C_BASE + 0x2A)) // ... 其他CLR寄存器 // 全局变量 volatile uint8_t g_sensor_data[4] = {0x11, 0x22, 0x33, 0x44}; // 模拟传感器数据 volatile uint8_t g_config_cmd[2]; // 存储接收到的配置命令 volatile uint8_t g_tx_index = 0; // 发送数据索引 volatile uint8_t g_rx_index = 0; // 接收数据索引 volatile bool g_tx_pending = false; // 是否有数据待发送 void I2C_Slave_Init(void) { // 1. 禁用I2C模块(EN=0),进行安全配置 I2C_ENBL = 0x0000; // 2. 配置I2C自身地址、时钟等(此处省略具体寄存器配置) // ... (配置SAR寄存器为0x48,配置时钟分频等) ... // 3. 配置FIFO阈值 I2C_TXFT = 1; // 发送FIFO<=1时请求数据 I2C_RXFT = 1; // 接收FIFO>=1时触发中断(对于2字节命令,也可以设为1,来一个字节处理一个) // 4. 配置中断使能 uint16_t ienbl_val = 0; ienbl_val |= (1 << 6); // 使能 TXABRT ienbl_val |= (1 << 5); // 使能 RDREQ ienbl_val |= (1 << 7); // 使能 TXDONE ienbl_val |= (1 << 4); // 使能 TXEMPTY ienbl_val |= (1 << 2); // 使能 RXFULL ienbl_val |= (1 << 1); // 使能 RXOVR ienbl_val |= (1 << 0); // 使能 RXUND I2C_IENBL = ienbl_val; // 5. 清除所有可能挂起的中断标志(通过读取CLRINT或各个CLR寄存器) volatile uint16_t dummy; dummy = *((volatile uint16_t*)(I2C_BASE + 0x20)); // 假设CLRINT地址 // 6. 使能I2C模块 I2C_ENBL |= 0x0001; // 设置EN=1 // 7. 使能处理器级别的I2C中断(此处依赖具体MCU的中断控制器) // enable_irq(I2C_IRQn); } __interrupt void I2C_IRQ_Handler(void) { uint16_t istat = I2C_ISTAT; // --- 错误处理 --- if (istat & (1 << 6)) { // TXABRT uint16_t abort_src = I2C_TXABRTSRC; // 读取原因 // 可在此记录错误码 abort_src volatile uint16_t clr = I2C_CLRTXABRT; // 清除标志 g_tx_index = 0; // 重置发送状态 g_tx_pending = false; // 根据abort_src进行特定恢复(此处简化) } if (istat & ((1 << 3) | (1 << 1) | (1 << 0))) { // TXOVR, RXOVR, RXUND // 简单清除标志,实际应用需更复杂处理 if (istat & (1 << 3)) { volatile uint16_t clr = I2C_CLRTXOVR; } if (istat & (1 << 1)) { volatile uint16_t clr = I2C_CLRRXOVR; } if (istat & (1 << 0)) { volatile uint16_t clr = I2C_CLRRXUND; } // 可能需要进行FIFO重置或通信重启 } // --- 接收处理 --- if (istat & (1 << 2)) { // RXFULL // 读取所有可用的数据 while (I2C_STAT & (1 << 3)) { // 检查 RFNE if (g_rx_index < 2) { // 我们只期望2字节配置命令 g_config_cmd[g_rx_index++] = (uint8_t)(I2C_DATA & 0xFF); } else { // 数据超出预期,读取并丢弃或视为错误 volatile uint8_t dummy = (uint8_t)(I2C_DATA & 0xFF); } } // RXFULL标志由硬件在FIFO水平下降后自动清除 if (g_rx_index >= 2) { // 收到完整命令,可以在此处或主循环中处理 // process_config_command(g_config_cmd); g_rx_index = 0; // 为下一次接收重置索引 } } // --- 发送处理 --- if (istat & (1 << 5)) { // RDREQ // 主设备要读数据,准备第一个字节 g_tx_index = 0; g_tx_pending = true; if (g_tx_index < 4) { I2C_DATA = g_sensor_data[g_tx_index++]; } volatile uint16_t clr = I2C_CLRRDREQ; // 必须清除RDREQ } if (g_tx_pending && (istat & (1 << 4))) { // TXEMPTY 且正在发送中 // 继续填充后续数据 while ((I2C_STAT & (1 << 1)) && (g_tx_index < 4)) { // TFNF非满 且 有数据 I2C_DATA = g_sensor_data[g_tx_index++]; } // TXEMPTY标志在写入数据后由硬件自动清除 } if (istat & (1 << 7)) { // TXDONE // 主设备发送了NACK,发送完成 g_tx_pending = false; g_tx_index = 0; volatile uint16_t clr = I2C_CLRTXDONE; // 可以在这里触发一个信号,通知主循环发送完成 } }这个示例展示了如何将不同的中断源组织在一个ISR里,协同工作。关键点在于状态管理:g_tx_pending标志确保了只有在RDREQ启动的发送过程中,才响应TXEMPTY中断去填充数据。同时,错误处理优先,并且所有通过读取清除的标志都得到了妥善处理。
最后,我想分享一个在调试复杂I2C中断问题时非常有效的方法:状态机可视化。不要仅仅依赖调试器看寄存器值。可以在ISR入口处,将ISTAT、RISTAT、STAT甚至TXABRTSRC的值通过一个简单的串口打印出来(注意不要影响ISR时序),或者记录到内存数组中。通过观察这些状态位在通信过程中的变化序列,你就能像看故事书一样,清晰地看到“主设备发起起始条件->从设备收到地址匹配->RDREQ触发->TXEMPTY触发->数据被逐个读出->TXDONE触发”的完整流程。任何偏离这个剧本的行为,都会在日志中暴露无遗,这比盲目猜测要高效得多。I2C中断机制虽然细节繁多,但一旦掌握,它将成为你构建高效、可靠嵌入式系统的得力工具。