1. I2C中断与DMA机制的核心价值:从轮询到事件驱动的效率革命
在嵌入式开发领域,尤其是基于TI Sitara或Cortex-A系列处理器的项目中,I2C总线是连接各类传感器、EEPROM、RTC和IO扩展芯片的“血管”。早期接触I2C编程时,很多人都是从简单的轮询(Polling)模式开始的:写一个字节,然后死等状态位;再写下一个,再等。这种方式在小数据量、低频率的场合勉强能用,但一旦涉及连续读取大量传感器数据,或者系统需要同时处理多个任务时,CPU就会被牢牢绑死在I2C总线上,效率低下,功耗飙升。
真正让I2C发挥威力的,是其内置的中断和DMA机制。这不仅仅是“锦上添花”的功能,而是将通信从“CPU全程保姆式陪护”转变为“事件驱动、自动执行”的关键。中断让CPU得以“解放”,只在数据就绪、传输完成或发生错误等关键时刻被唤醒处理,其余时间可以处理其他任务或进入低功耗模式。DMA则更进一步,它允许数据在I2C控制器和系统内存之间直接搬运,完全绕过CPU,实现了零开销的大批量数据传输。
而这一切的“控制中枢”,就是一组特定的控制寄存器。TI的I2C控制器文档里,关于中断和DMA的寄存器描述往往分散且略显晦涩,特别是I2C_IRQSTS、I2C_IRQEN_SET/CLR和I2C_WE这几个寄存器,它们共同构成了中断管理的“三层架构”:状态报告、使能控制和唤醒管理。理解它们,你就能精准地驾驭I2C通信的每一个异步事件,构建出既高效又可靠的嵌入式系统。本文将从实际开发的角度,深入拆解这些寄存器的每一位,并结合DMA配置,分享一套经过实战检验的配置流程和避坑指南。
2. 中断状态寄存器(I2C_IRQSTS):系统通信的“仪表盘”
I2C_IRQSTS寄存器是理解整个中断系统的起点。你可以把它想象成汽车仪表盘上的故障灯和状态指示灯。它实时反映了I2C控制器内部发生的各种事件。但这里有一个至关重要的概念:这个寄存器显示的是“已使能且已发生”的中断状态。也就是说,如果一个中断事件在I2C_IRQEN_SET中没有被使能,那么即使它实际发生了,也不会在IRQSTS中置位,更不会向CPU申请中断。
该寄存器是一个可读可写的寄存器,但其写操作有特殊语义:向某一位写1会清除该位(清零中断标志),写0则无效。这种“写1清零”(Write-1-to-clear)的机制非常普遍,目的是让软件能安全地确认并清除已处理的中断,避免重复进入中断服务程序。
2.1 关键状态位详解与实战场景
我们挑几个最核心、最常打交道的位来深入分析:
AL (Arbitration Lost, 位0) - 仲裁丢失这是多主系统中的典型问题。当两个或更多主设备同时尝试启动传输时,它们会通过监控SDA线电平进行仲裁。如果本机输出高电平(释放总线),但检测到SDA线为低电平(被其他主设备拉低),则说明仲裁失败。此时硬件会自动将AL位置1,并释放总线控制权,切换为从设备模式。
注意:仲裁丢失后,本机的传输被中止。你的中断服务程序必须检测此位,并做出相应处理,通常是记录错误、重置传输状态,并可能等待随机时间后重试。忽略此中断可能导致程序逻辑卡死,误以为传输仍在进行。
NACK (No Acknowledge, 位1) - 无应答当主设备发送完一个字节(地址或数据)后,如果在第9个时钟脉冲期间没有检测到从设备的应答信号(SDA线为高),此位置1。这通常意味着:1) 从设备地址错误或不存在;2) 从设备忙;3) 物理连接故障。
实操心得:在发送模式下,
NACK中断非常有用。你可以在中断里立刻停止后续数据的发送,而不是傻傻地把整个数据包发完。这能节省总线时间,并快速进入错误处理流程。
ARDY (Register Access Ready, 位2) - 寄存器访问就绪这个中断标志容易被人忽略,但极其重要。当I2C_CON寄存器中的I2C_EN位被清零(停止I2C操作)或一次数据传输(非DMA模式)完成后,此位置1。它标志着I2C控制器内部状态机已稳定,可以安全地配置下一个传输参数(如从机地址、数据长度等)。
踩过的坑:在连续发起多次I2C传输时,如果没有等待
ARDY置位就急着重写I2C_CNT(数据计数寄存器)或I2C_SA(从机地址寄存器),可能会导致配置被忽略或传输混乱。稳妥的做法是,在每次传输启动前或传输模式切换后,检查或等待ARDY中断。
RRDY (Receive Data Ready, 位3) 与 XRDY (Transmit Data Ready, 位4) - 数据就绪这是最常用的两个中断。在非FIFO和非DMA模式下:
- XRDY:当数据发送寄存器(
I2C_DATA)为空,可以写入下一个待发送字节时,此位置1。 - RRDY:当数据接收寄存器(
I2C_DATA)已从总线接收到一个完整字节,可以读取时,此位置1。 你的中断服务程序的核心任务就是响应这两个中断:在XRDY中断中写入数据,在RRDY中断中读取数据。
STC (Start Condition, 位6) - 起始条件检测当I2C控制器检测到总线上出现起始条件(SDA在SCL高电平时由高变低)时,此位置1。这个中断在从机模式下尤其关键。作为从机,你需要通过这个中断知道有主设备在呼叫你,然后去读取I2C_STAT寄存器中的AAS(Addressed As Slave)位,确认自己是否被寻址,以及是读操作还是写操作。
AAS (Addressed As Slave, 位9) - 被寻址为从机当总线上广播的从机地址与I2C_OA(自身地址寄存器)中设置的地址匹配时,此位置1。结合STC中断,从机设备可以精确判断通信的开始。
XUDF (Transmit Underflow, 位10) 与 ROVR (Receive Overrun, 位11) - FIFO错误在启用FIFO或DMA时,这两个错误标志位至关重要。
- XUDF (发送下溢):当CPU或DMA来不及向发送FIFO填充数据,而I2C控制器需要数据发送时发生。这会导致发送中断。
- ROVR (接收上溢):当接收FIFO已满,但I2C控制器又收到新数据时发生。旧数据会被新数据覆盖,造成数据丢失。 这两个错误通常意味着你的中断响应或DMA配置速度跟不上总线速度,需要优化。
BB (Bus Busy, 位12) - 总线忙这是一个只读状态位,反映SDA和SCL线的实际状态。当检测到总线被占用(起始条件后,停止条件前)时,此位置1。在发起传输前查询此位,可以避免在总线忙时误操作,这是实现软件多主支持的基础。
2.2 中断状态寄存器的操作范式
理解了各个位的含义,操作流程就清晰了。一个典型的中断服务程序(ISR)开头应该是这样的:
void I2C_ISR(void) { uint32_t irqStatus = HWREG(I2C0_BASE + I2C_IRQSTS); // 读取中断状态 // 1. 处理错误中断(优先级最高) if (irqStatus & I2C_IRQSTS_AL) { // 仲裁丢失处理:记录日志,重置控制器,准备重试 HWREG(I2C0_BASE + I2C_IRQSTS) = I2C_IRQSTS_AL; // 写1清零 handleArbitrationLost(); } if (irqStatus & I2C_IRQSTS_NACK) { // 无应答处理:可能是地址错误,停止当前传输 HWREG(I2C0_BASE + I2C_IRQSTS) = I2C_IRQSTS_NACK; handleNackError(); } if (irqStatus & I2C_IRQSTS_XUDF) { // 发送下溢:检查DMA或CPU填充是否太慢 HWREG(I2C0_BASE + I2C_IRQSTS) = I2C_IRQSTS_XUDF; handleTxUnderflow(); } // 2. 处理正常传输中断 if (irqStatus & I2C_IRQSTS_RRDY) { // 读取数据 rxBuffer[rxIndex++] = HWREG(I2C0_BASE + I2C_DATA); HWREG(I2C0_BASE + I2C_IRQSTS) = I2C_IRQSTS_RRDY; // 清除标志 if (rxIndex >= expectedLength) { // 接收完成,可能置位某个完成标志 transferComplete = true; } } if (irqStatus & I2C_IRQSTS_XRDY) { // 写入下一个数据 HWREG(I2C0_BASE + I2C_DATA) = txBuffer[txIndex++]; HWREG(I2C0_BASE + I2C_IRQSTS) = I2C_IRQSTS_XRDY; if (txIndex >= txLength) { // 发送完成,后续可能等待ARDY } } // 3. 处理传输完成/就绪中断 if (irqStatus & I2C_IRQSTS_ARDY) { HWREG(I2C0_BASE + I2C_IRQSTS) = I2C_IRQSTS_ARDY; // 寄存器可访问,可以安全开始下一次配置 registerAccessReady = true; } }关键技巧:在ISR中,务必先读取
IRQSTS的值保存到局部变量,再用这个变量来判断和清除中断。不要直接多次读取IRQSTS寄存器来判断不同位,因为在读取和判断之间,如果有新的中断发生,状态可能改变,导致判断逻辑出错或丢失中断。
3. 中断使能寄存器(I2C_IRQEN_SET/CLR):精准的事件过滤器
如果说IRQSTS是仪表盘,那么I2C_IRQEN_SET和I2C_IRQEN_CLR就是仪表盘上各个指示灯的开关键。它们是一对“置位-清零”寄存器,用于独立地使能或禁用特定的中断源。这种设计比单一的读写寄存器更安全,可以避免在多任务或中断环境中,修改一位时意外影响其他位。
3.1 寄存器工作机制与位域映射
这两个寄存器的位定义与I2C_IRQSTS寄存器中的中断状态位是一一对应的。例如:
I2C_IRQEN_SET的XRDY_IE(位4) 控制I2C_IRQSTS的XRDY(位4) 中断是否能使能。I2C_IRQEN_CLR的XRDY_IE(位4) 用于清除(禁用)该中断使能。
它们的操作语义也是“写1有效,写0无效”:
- 向
I2C_IRQEN_SET的某位写1:使能对应的中断。 - 向
I2C_IRQEN_CLR的某位写1:禁用对应的中断。 - 向任何一位写0:无任何效果。
3.2 实战配置策略:按需开启,分层管理
在项目初期,一个常见的错误是盲目开启所有中断。这会导致中断过于频繁,ISR执行时间过长,甚至可能因为处理不及时而丢失中断或发生错误。合理的策略是按需、分层配置。
1. 错误中断:通常必须开启像AL(仲裁丢失)、NACK(无应答)、XUDF(发送下溢)、ROVR(接收上溢)这类错误中断,一旦发生通常意味着通信失败或系统异常,必须让CPU知晓并进行处理。因此,在初始化时就应该使能它们。
// 使能关键错误中断 HWREG(I2C0_BASE + I2C_IRQEN_SET) = I2C_IRQEN_SET_AL_IE | I2C_IRQEN_SET_NACK_IE | I2C_IRQEN_SET_XUDF_IE | I2C_IRQEN_SET_ROVR_IE;2. 数据流中断:根据传输模式动态管理对于XRDY和RRDY,是否需要开启取决于你使用的数据传输模式:
- 纯中断模式(无DMA,无FIFO):必须开启。每个字节的发送/接收都依赖它们。
- FIFO中断模式:可以配置FIFO阈值中断。当发送FIFO空间大于某个阈值,或接收FIFO数据量达到某个阈值时,才触发一次中断,然后CPU批量处理多个字节。这时可能不需要
XRDY/RRDY,而是使用I2C_BUF寄存器配置的阈值中断。 - DMA模式:通常要关闭
XRDY/RRDY中断。因为数据搬运由DMA控制器完成,CPU不应被每个字节就绪事件打扰。DMA有自己的完成或错误中断。
3. 控制与状态中断:按场景选择
ARDY:在复杂的、需要连续发起不同配置的传输序列中非常有用。在简单单次传输中可能不需要。STC和AAS:仅在从机模式下需要开启。主机模式不需要关心起始条件或被寻址。BF(总线空闲):在多主系统中,可以用来检测总线何时空闲,以便本机尝试获取总线控制权。
4. 一个完整的初始化与传输流程示例假设我们要配置I2C为主发送模式,使用中断(非DMA)发送一段数据:
void I2C_MasterTx_Init(void) { // 1. 软件复位I2C模块(确保处于已知状态) HWREG(I2C0_BASE + I2C_SYSC) |= I2C_SYSC_SRST; while(!(HWREG(I2C0_BASE + I2C_SYSS) & I2C_SYSS_RDONE)); // 等待复位完成 // 2. 配置时钟、引脚复用等(此处省略) // 3. 清除所有可能挂起的中断标志(写1清零) HWREG(I2C0_BASE + I2C_IRQSTS) = 0xFFFFFFFF; // 4. 先禁用所有中断源(通过CLR寄存器写1) HWREG(I2C0_BASE + I2C_IRQEN_CLR) = 0xFFFFFFFF; // 5. 按需使能中断:错误中断 + 发送就绪中断 + 寄存器就绪中断 HWREG(I2C0_BASE + I2C_IRQEN_SET) = I2C_IRQEN_SET_AL_IE | I2C_IRQEN_SET_NACK_IE | I2C_IRQEN_SET_XRDY_IE | I2C_IRQEN_SET_ARDY_IE; // 6. 配置I2C控制器为主发送模式、设置时钟等 HWREG(I2C0_BASE + I2C_CON) = I2C_CON_I2C_EN | I2C_CON_MST; // 使能I2C,主机模式 // ... 其他配置 // 7. 使能CPU级别的I2C中断(配置中断控制器,如ARM GIC或NVIC) // IntEnable(INT_I2C0); // 示例 } void I2C_StartTransfer(uint8_t slaveAddr, uint8_t *data, uint32_t len) { // 等待总线空闲(可选,多主系统需要) while(HWREG(I2C0_BASE + I2C_IRQSTS) & I2C_IRQSTS_BB); // 配置从机地址和数据长度 HWREG(I2C0_BASE + I2C_SA) = slaveAddr; HWREG(I2C0_BASE + I2C_CNT) = len; // 保存发送数据和长度到全局/静态变量,供ISR使用 txBuffer = data; txLength = len; txIndex = 0; transferComplete = false; // 启动传输(设置START位) HWREG(I2C0_BASE + I2C_CON) |= I2C_CON_STT; // 等待传输完成(通过ARDY中断或轮询标志) while(!transferComplete) { // 可以进入低功耗模式,等待中断唤醒 __asm("WFI"); } }避坑指南:在使能具体中断源(
IRQEN_SET)之前,务必先清除所有可能旧的中断状态(IRQSTS)。否则,一个之前发生但未处理的中断标志,可能会在使能中断的瞬间立即触发中断,导致程序进入不可预知的状态。顺序应该是:清除状态 -> 禁用所有中断 -> 使能所需中断。
4. 唤醒使能寄存器(I2C_WE):低功耗系统的“守夜人”
在电池供电或对功耗敏感的嵌入式设备中,CPU和外围模块大部分时间处于睡眠或低功耗模式。I2C_WE(Wakeup Enable)寄存器就是为这种场景设计的。它允许特定的I2C事件(如接收到数据、总线活动)将系统从低功耗模式中“唤醒”,而无需CPU持续轮询。
4.1 唤醒机制的工作原理
唤醒事件与中断事件共享同一套状态检测逻辑。区别在于:
- 中断:当事件发生且被使能时,触发一个同步的IRQ信号给CPU,CPU在正常运行时处理。
- 唤醒:当事件发生且被
I2C_WE使能时,触发一个异步的唤醒信号(swakeup线),这个信号可以直接连接到电源管理单元(PRCM),将整个芯片或相关模块从低功耗状态中拉出来,然后再产生中断(如果也使能了的话)。
这里有一个关键点:大多数唤醒事件需要功能时钟(Functional Clock)使能才能被正确检测和同步。唯一的例外是STC_WE(起始条件唤醒)。文档明确指出,当功能时钟被关��时,STC唤醒是异步检测的,这意味着它可能受到毛刺干扰。因此,如果功能时钟不能被关闭(I2C_SYSC.ClockActivity配置为10或11),就不应该使能STC_WE。
4.2 关键唤醒源的应用场景
DRDY_WE (Data Ready Wakeup, 位3)这是最常用的唤醒源之一。设想一个场景:一个温度传感器通过I2C连接到MCU,MCU大部分时间深度睡眠。传感器每隔一分钟测量一次,并将数据准备好。MCU可以配置为被DRDY事件唤醒(通常需要结合从机模式下的AAS地址匹配),醒来后读取数据,处理完毕再次睡眠。这实现了极低的平均功耗。
AAS_WE (Addressed As Slave Wakeup, 位9)当I2C控制器作为从机,且处于空闲模式时,如果总线上有主设备发送了与本机地址匹配的地址帧,此唤醒事件可以将系统唤醒。这对于从机设备等待主机命令的场景非常有用。
AL_WE (Arbitration Lost Wakeup, 位0)文档对此有特别强调:AL_WE必须仅在多主通信中启用。如果在一个单主系统(或者本机不作为主设备参与仲裁)中启用了它,并且发生了描述中的场景(主机发送,FIFO满,进入空闲,然后丢失仲裁),模块将无法唤醒CPU,导致系统卡死在一个不确定的状态。
BF_WE (Bus Free Wakeup, 位8)在多主系统中,本机如果想发起传输,需要等待总线空闲。与其让CPU轮询BB位,不如使能BF_WE,让CPU进入睡眠,当总线空闲时自动被唤醒,然后尝试获取总线。
4.3 低功耗I2C从机配置示例
以下是一个配置I2C为低功耗从机,通过地址匹配唤醒的简化流程:
void I2C_Slave_LowPower_Init(void) { // 1. 基本初始化(复位、时钟、引脚) // ... // 2. 配置自身从机地址 HWREG(I2C0_BASE + I2C_OA) = MY_SLAVE_ADDRESS; // 3. 配置为从机接收模式 HWREG(I2C0_BASE + I2C_CON) = I2C_CON_I2C_EN; // 主机模式位MST为0 // 4. 清除中断状态,禁用所有中断(防止意外唤醒后立即进中断) HWREG(I2C0_BASE + I2C_IRQSTS) = 0xFFFFFFFF; HWREG(I2C0_BASE + I2C_IRQEN_CLR) = 0xFFFFFFFF; // 5. 配置唤醒使能:我们只希望被寻址时唤醒 // 注意:也需要使能全局唤醒使能位 I2C_SYSC.ENAWAKEUP HWREG(I2C0_BASE + I2C_SYSC) |= I2C_SYSC_ENAWAKEUP; HWREG(I2C0_BASE + I2C_WE) = I2C_WE_AAS_WE; // 仅使能地址匹配唤醒 // 6. 使能必要的后续中断(唤醒后处理数据用) // 例如,唤醒后我们需要接收数据,所以使能RRDY和ARDY // 但此刻先不使能,等被唤醒进入ISR后再根据情况开启 // HWREG(I2C0_BASE + I2C_IRQEN_SET) = I2C_IRQEN_SET_RRDY_IE | I2C_IRQEN_SET_ARDY_IE; // 7. 将I2C模块置于可唤醒的低功耗模式(具体操作依赖系统电源管理) // 例如,关闭I2C功能时钟,但保持接口时钟供电 // Power_setDependency(PERIPH_I2C0); // 确保电源域开启 // 然后CPU执行WFI进入睡眠 } // 系统被AAS事件唤醒后,会先进入唤醒处理流程,然后可能产生中断 void I2C_Slave_ISR(void) { uint32_t irqStatus = HWREG(I2C0_BASE + I2C_IRQSTS); if (irqStatus & I2C_IRQSTS_AAS) { HWREG(I2C0_BASE + I2C_IRQSTS) = I2C_IRQSTS_AAS; // 被寻址了!判断是读还是写操作? // 需要结合其他状态位,例如通过I2C_CON的TRX位判断 uint16_t stat = HWREG(I2C0_BASE + I2C_STAT); if (stat & I2C_STAT_DIR) { // 假设DIR=1为读(主设备读从机) // 主机要读数据,使能XRDY中断,准备发送数据 HWREG(I2C0_BASE + I2C_IRQEN_SET) = I2C_IRQEN_SET_XRDY_IE; txIndex = 0; } else { // 主机要写数据,使能RRDY中断,准备接收数据 HWREG(I2C0_BASE + I2C_IRQEN_SET) = I2C_IRQEN_SET_RRDY_IE; rxIndex = 0; } } // ... 处理XRDY, RRDY, ARDY等数据中断 }重要提醒:使用唤醒功能时,一定要理清“唤醒事件”和“中断事件”的关系。一个唤醒事件发生后,系统模块被上电,时钟恢复,此时可能会随即产生对应的中断(如果该中断源在
IRQEN中也使能了)。因此,在进入低功耗前,通常建议只使能唤醒寄存器(WE),而禁用中断使能寄存器(IRQEN)。待系统被唤醒后,在初始化代码或第一个ISR中,再根据具体需要使能相应的中断。这样可以避免在电源状态不稳定时处理复杂的中断逻辑。
5. DMA使能与控制寄存器:解放CPU的终极武器
当需要传输的数据量超过几十个字节时,中断模式的每个字节都进一次ISR的开销就变得不可忽视。此时,DMA(直接内存访问)是必然选择。TI的I2C控制器提供了与DMA控制器协同工作的专用寄存器。
5.1 DMA相关寄存器概览
I2C_DMARXEN_SET/I2C_DMARXEN_CLR(偏移 38h / 40h):用于使能或禁用接收方向的DMA请求。只有最低位DMARX_EN_SET/DMARX_EN_CLR有效。I2C_DMATXEN_SET/I2C_DMATXEN_CLR(偏移 3Ch / 44h):用于使能或禁用发送方向的DMA请求。只有最低位DMATX_TRANSMIT_SET/DMATX_EN_CLR有效。I2C_DMARXWAKE_EN/I2C_DMATXWAKE_EN(偏移 48h / 4Ch):这两个寄存器结构与I2C_WE类似,用于使能特定DMA事件(如XRDY,RRDY)的唤醒功能。但请注意,文档中这两个寄存器的位描述似乎存在笔误,多处将“wakeup”写成了“interrupt disabled/enabled”。根据上下文和寄存器名,它们的功能应是使能对应DMA事件的唤醒。
全局使能开关:文档特别强调,I2C_BUF.RDMA_EN和I2C_BUF.XDMA_EN是全局的DMA使能位,默认是禁用的。即使你在DMARXEN_SET中使能了DMA,也必须同时将I2C_BUF.RDMA_EN置1,DMA请求才会真正产生。发送方向同理。
5.2 DMA传输的完整配置流程
配置I2C使用DMA进行数据收发,需要I2C控制器和DMA控制器两边协同工作。以下是一个主发送模式使用DMA的典型步骤:
步骤1:配置I2C控制器基本参数
// 初始化I2C为主机模式,设置时钟等 HWREG(I2C0_BASE + I2C_CON) = I2C_CON_I2C_EN | I2C_CON_MST; // 配置从机地址 HWREG(I2C0_BASE + I2C_SA) = SLAVE_ADDR; // 配置要传输的数据数量(字节数) HWREG(I2C0_BASE + I2C_CNT) = DATA_LENGTH;步骤2:配置DMA控制器这取决于你使用的具体DMA控制器(如EDMA, uDMA等)。通常需要:
- 设置源地址(发送时是内存地址,接收时是
I2C_DATA寄存器地址)。 - 设置目标地址(发送时是
I2C_DATA寄存器地址,接收时是内存地址)。 - 设置传输数据量(与
I2C_CNT一致或更大)。 - 设置地址递增模式。
- 设置触发信号:绑定到I2C的发送或接收DMA请求线。
步骤3:使能I2C的DMA功能
// 1. 全局使能DMA HWREG(I2C0_BASE + I2C_BUF) |= I2C_BUF_XDMA_EN; // 使能发送DMA // 2. 使能具体的DMA请求通道(通过SET寄存器) HWREG(I2C0_BASE + I2C_DMATXEN_SET) = I2C_DMATXEN_SET_DMATX_TRANSMIT_SET; // 3. (可选)如果需要DMA事件唤醒低功耗系统,配置唤醒使能 // HWREG(I2C0_BASE + I2C_DMATXWAKE_EN) = I2C_DMATXWAKE_EN_XRDY; // 同时确保 I2C_SYSC.ENAWAKEUP 已使能 // 4. (重要)禁用对应的字节中断!避免DMA和中断同时响应XRDY/RRDY HWREG(I2C0_BASE + I2C_IRQEN_CLR) = I2C_IRQEN_CLR_XRDY_IE; // 但错误中断(AL, NACK)和完成中断(ARDY)通常仍需开启 HWREG(I2C0_BASE + I2C_IRQEN_SET) = I2C_IRQEN_SET_AL_IE | I2C_IRQEN_SET_NACK_IE | I2C_IRQEN_SET_ARDY_IE;步骤4:启动传输并等待完成
// 启动DMA传输(具体函数取决于DMA控制器) DMAStartChannel(DMA_CHANNEL_I2C0_TX); // 启动I2C传输(产生START条件) HWREG(I2C0_BASE + I2C_CON) |= I2C_CON_STT; // 等待传输完成。可以通过: // 1. 轮询I2C_IRQSTS的ARDY位(DMA完成,I2C控制器就绪) // 2. 等待DMA控制器的传输完成中断 // 3. 等待I2C的ARDY中断(如果使能了) while(!(HWREG(I2C0_BASE + I2C_IRQSTS) & I2C_IRQSTS_ARDY)) { // 也可以在这里检查错误中断,如NACK if (HWREG(I2C0_BASE + I2C_IRQSTS) & I2C_IRQSTS_NACK) { // 处理错误,停止DMA和I2C DMAStopChannel(DMA_CHANNEL_I2C0_TX); HWREG(I2C0_BASE + I2C_CON) |= I2C_CON_STP; // 发送STOP handleError(); break; } } // 传输完成,发送STOP条件 HWREG(I2C0_BASE + I2C_CON) |= I2C_CON_STP; // 清除ARDY标志 HWREG(I2C0_BASE + I2C_IRQSTS) = I2C_IRQSTS_ARDY; // 停止DMA通道 DMAStopChannel(DMA_CHANNEL_I2C0_TX); // 禁用I2C的DMA请求(可选,为下次传输准备) HWREG(I2C0_BASE + I2C_DMATXEN_CLR) = I2C_DMATXEN_CLR_DMATX_EN_CLR; HWREG(I2C0_BASE + I2C_BUF) &= ~I2C_BUF_XDMA_EN;5.3 DMA模式下的中断与唤醒考量
在DMA模式下,中断配置策略需要调整:
XRDY/RRDY中断:必须禁用。数据搬运由DMA负责。ARDY中断:强烈建议使能。它标志着一次DMA传输的结束(所有数据已通过I2C总线送出/接收),是发起STOP条件或进行后续操作的安全时机。- 错误中断(
AL,NACK,XUDF,ROVR):必须使能。DMA只能搬运数据,无法处理总线错误。一旦发生NACK或仲裁丢失,I2C控制器会停止,但DMA可能还在傻傻地请求数据,需要CPU中断来紧急停止DMA并处理错误。 - 唤醒配置:如果系统需要在DMA传输完成或出错时从低功耗模式唤醒,除了配置
I2C_WE,还需要配置I2C_DMATXWAKE_EN或I2C_DMARXWAKE_EN。例如,使能ARDY_WE,可以在DMA传输完成、ARDY事件发生时唤醒系统。
6. 常见问题排查与调试技巧实录
即使理解了所有寄存器,实际调试中依然会遇到各种问题。下面是我在多个项目中总结的常见“坑点”和解决方法。
6.1 中断不触发或触发一次后不再触发
现象:配置好中断后,第一次传输能正常进入ISR,但后续传输中断再无响应。根因与解决:
- 未清除中断标志(最常见):在ISR中,必须对
I2C_IRQSTS中检测到的位写1清零。如果忘了清除,该标志位会一直为1,导致硬件认为中断仍在挂起,不会产生新的中断边沿。确保你的ISR中有类似HWREG(I2C_BASE + I2C_IRQSTS) = detectedBits;的操作。 - 中断使能在ISR中被意外修改:检查ISR或主程序中是否有代码错误地写入了
I2C_IRQEN_CLR寄存器,禁用了中断。确保中断使能位的修改是受控的。 - CPU全局中断被意外禁用:在ISR或其它地方调用了类似
__disable_irq()的函数后没有重新使能。检查汇编或反汇编代码,确认CPSR或PRIMASK寄存器状态。 - 中断控制器配置问题:确认MCU的中断控制器(如NVIC)中,I2C中断的优先级和使能位是否正确设置。有时需要在I2C传输开始前再次确认使能。
6.2 DMA传输数据错乱或丢失
现象:使用DMA时,发送或接收的数据内容不对,数量不符。根因与解决:
- 数据数量寄存器
I2C_CNT未配置或配置错误:I2C_CNT必须配置为本次传输期望的字节数。这个值决定了I2C控制器何时产生停止条件或释放总线。如果I2C_CNT设为0,或者小于DMA要搬运的数量,传输会提前结束。务必在每次传输前正确设置此寄存器。 - DMA和I2C时钟域不同步:确保DMA控制器和I2C控制器的时钟都已使能并稳定。在低功耗唤醒后的初始化序列中,要等待各模块复位完成(检查
I2C_SYSS.RDONE等位)。 - DMA传输模式配置错误:对于I2C的
DATA寄存器,其地址是固定的。DMA配置应为:- 发送:源地址 = 内存数组地址(递增),目标地址 =
I2C_DATA寄存器地址(固定)。 - 接收:源地址 =
I2C_DATA寄存器地址(固定),目标地址 = 内存数组地址(递增)。 如果地址递增方向搞反,会导致所有数据读写到同一个地址。
- 发送:源地址 = 内存数组地址(递增),目标地址 =
- FIFO阈值与DMA触发不匹配:如果启用了FIFO,
I2C_BUF寄存器中的TXTRSH和RXTRSH设置了DMA请求的触发阈值。例如,TXTRSH设为4,意味着发送FIFO中剩余空间大于等于4个字时,才会触发DMA请求。如果DMA的传输数据量很小(小于阈值),可能无法触发DMA。根据你的数据包大小合理设置阈值。
6.3 低功耗模式下无法被唤醒
现象:配置了I2C_WE唤醒,系统进入低功耗模式后,I2C总线事件无法唤醒系统。根因与解决:
- 未使能全局唤醒:
I2C_SYSC.ENAWAKEUP位是总开关,必须置1。 - 模块时钟被完全关闭:除了
I2C_WE,还需要确保I2C模块在低功耗模式下仍有适当的时钟或电源供应。这通常涉及芯片级的电源/时钟域配置(如CM3的电源控制寄存器,或芯片特定的PRCM模块)。参考你的MCU数据手册中关于“唤醒源配置”和“外设时钟门控”的章节。 STC_WE的时钟限制:如果你只使能了STC_WE作为唤醒源,但I2C_SYSC.ClockActivity配置为不允许关闭功能时钟(值为10或11),则STC_WE无效。检查该配置,或改用其他唤醒源(如AAS_WE)。- 唤醒事件未产生:用逻辑分析仪或示波器检查I2C总线上是否有预期的起始条件或地址匹配事件。可能是主设备根本没发起通信,或者从机地址配置错误。
6.4 多主系统中仲裁丢失处理不当
现象:在多主系统中,本机发送数据时频繁进入AL中断,通信不稳定。根因与解决:
- 未实现退避算法:仲裁丢失后,程序应等待一个随机时间再重试,避免多个主设备立即再次冲突。简单的实现可以读取一个硬件随机数种子或利用系统滴答计时器进行延时。
void handleArbitrationLost(void) { logError("Arbitration Lost"); // 1. 确保发送停止条件,释放总线 HWREG(I2C0_BASE + I2C_CON) |= I2C_CON_STP; // 2. 重置I2C控制器状态(可选,有时需要软复位) // 3. 随机退避 uint32_t backoffTime = getRandomDelay(100, 1000); // 100-1000个系统tick delayTicks(backoffTime); // 4. 重新尝试传输 retryLastTransfer(); } AL_WE在不该使能时使能:在单主系统或从机模式下,务必禁用AL_WE唤醒,否则可能导致系统在异常情况下无法唤醒。
6.5 寄存器访问竞争导致配置失败
现象:在快速连续发起多次I2C传输时,偶尔会出现配置(如修改I2C_SA,I2C_CNT)不生效的情况。根因与解决:根本原因是未尊重ARDY(寄存器访问就绪)状态。在I2C_CON.STT启动传输后,或I2C_CON.I2C_EN被清零后,I2C控制器的内部状态机正在工作,此时写入某些配置寄存器是无效的。最佳实践:在修改I2C_SA,I2C_CNT,I2C_CON(除STT/STP位)等关键寄存器前,等待I2C_IRQSTS.ARDY位置1。
void I2C_SafeConfigure(uint16_t slaveAddr, uint16_t dataCount) { // 等待上一次操作完成,寄存器可访问 while(!(HWREG(I2C0_BASE + I2C_IRQSTS) & I2C_IRQSTS_ARDY)) { // 超时处理 } // 清除ARDY标志 HWREG(I2C0_BASE + I2C_IRQSTS) = I2C_IRQSTS_ARDY; // 现在可以安全配置 HWREG(I2C0_BASE + I2C_SA) = slaveAddr; HWREG(I2C0_BASE + I2C_CNT) = dataCount; // ... 其他配置 }调试这类问题时,逻辑分析仪是你的最佳伙伴。不仅要抓取SDA/SCL波形,如果MCU引脚允许,最好也能抓取中断输出信号和关键的GPIO调试引��。通过波形可以清晰看到起始、停止、地址、数据、ACK/NACK,以及中断触发的时间点,结合代码日志,绝大部分问题都能定位。对于DMA问题,还可以通过监控DMA控制器的状态寄存器来确认传输是否被正确触发和完成。