1. 项目概述与核心价值
在嵌入式系统开发中,I2C总线因其简洁的两线制(SDA、SCL)和主从多设备架构,成为了连接传感器、EEPROM、RTC等低速外设的首选协议。然而,当数据吞吐量增大或系统实时性要求提高时,传统的字节级轮询或中断处理方式会大量占用CPU资源,成为系统性能的瓶颈。这时,I2C控制器内置的FIFO(先进先出)缓冲区及其配套的中断、DMA(直接内存访问)和Draining(排空)机制,就成为了提升效率、解放CPU的关键武器。
我接触过不少项目,从简单的温湿度采集到复杂的多传感器融合系统,但凡涉及到批量数据传输,如果没用好FIFO和DMA,系统负载很容易就上去了,主循环响应变慢,甚至出现数据丢失。TI的I2C控制器模块提供了一个相当典型且功能完善的案例,其FIFO管理逻辑清晰,但手册中的描述往往分散在各个章节,初次接触容易摸不着头脑。本文旨在将这些碎片化的知识串联起来,结合我实际调试中的踩坑经验,深入解析FIFO管理的三大核心操作模式(中断、轮询、DMA)以及用于处理“零头”数据的Draining机制。理解这些机制,不仅能帮你写出更高效、更稳定的I2C驱动,更能让你在遇到通信异常时,快速定位问题是出在硬件、配置还是软件流程上。
2. FIFO管理机制总览与设计思路
在深入细节之前,我们得先建立起对TI I2C控制器FIFO管理的整体认知。这个模块内部集成了两个独立的32字节FIFO,分别用于发送(TX)和接收(RX)。FIFO的核心思想是批处理和异步通知,其管理逻辑围绕几个关键寄存器展开,尤其是I2C_BUF(缓冲区配置)和I2C_BUFSTAT(缓冲区状态)。
2.1 核心寄存器与阈值控制
整个FIFO管理的“开关”和“刻度”都集中在I2C_BUF寄存器。其中,RXTRSH和TXTRSH这两个字段是灵魂所在。它们定义了触发事件的阈值水平。
RXTRSH(Receive Threshold): 接收FIFO阈值。当RX FIFO中的数据量大于这个阈值时,才会满足触发条件(产生RRDY中断或DMA请求)。例如,设置RXTRSH = 7,意味着当FIFO中存有8个或更多字节数据时,才会通知CPU或DMA来取数据。TXTRSH(Transmit Threshold): 发送FIFO阈值。其逻辑与接收相反,当TX FIFO中的数据量低于这个阈值(即变得更空)时,才会触发事件(产生XRDY中断或DMA请求)。例如,设置TXTRSH = 15,意味着当FIFO中剩余空间大于等于16字节(即数据量少于16字节)时,就会通知CPU或DMA来填充数据。
这里有一个非常重要的细节:阈值设定与触发条件的关系是“阈值+1”。手册中多次提到“RXTRSH + 1”或“TXTRSH + 1”,这是因为寄存器存储的是阈值N,而触发发生在“FIFO深度 > N”(接收)或“FIFO深度 <= N”(发送,更空)的时刻。理解这一点,对于后续计算DMA传输长度、理解中断频率至关重要。
2.2 三种操作模式的选择逻辑
TI I2C控制器提供了三种FIFO操作模式,选择哪种取决于你的系统对实时性、CPU占用率和开发复杂度的权衡。
- 中断模式 (Interrupt Mode): 通过使能
I2C_IRQENABLE_SET寄存器中的RRDY_IE和XRDY_IE来开启。当FIFO达到阈值条件时,控制器向CPU发起硬件中断。这是最常用的平衡方案,既能及时响应,又避免了轮询的CPU空转。你需要编写中断服务程序(ISR)来读写I2C_DATA寄存器。 - 轮询模式 (Polling Mode): 关闭上述中断使能位,CPU定期主动查询
I2C_IRQSTATUS_RAW寄存器中的RRDY或XRDY标志位。这种方式实现简单,没有中断上下文切换的开销,但会持续占用CPU时间。适用于对实时性要求不高、或系统负载极轻的场景。 - DMA模式 (DMA Mode): 通过设置
I2C_BUF中的RDMA_EN/XDMA_EN以及相应的DMA使能寄存器来开启。当FIFO达到阈值条件时,控制器直接向DMA控制器发起请求,由DMA引擎在内存和I2C_DATA寄存器之间搬运数据,完全解放CPU。这是处理大批量、高带宽数据传输的理想选择,但需要正确配置DMA通道,并处理好与Draining机制的配合。
注意:模式是互斥的。对于RX或TX方向,你只能选择其中一种方式来服务FIFO。例如,使能了DMA RX,那么
RRDY中断就会被强制拉低无效。如果同时使能了中断和DMA,行为是未定义的,很可能导致数据错乱。
2.3 Draining机制的必要性
这是最容易出问题的地方。假设你设置RXTRSH = 7(阈值8),并且使用DMA,每次DMA传输固定搬运8字节。如果总传输长度是20字节,那么DMA会完美地搬运前16字节(2次请求)。但剩下的4字节(20 - 16)不足以再次触发DMA请求(因为FIFO深度4 < 阈值8),这4个字节就会一直残留在FIFO里,导致传输不完整。
Draining机制就是为解决这个“零头”问题而生的。当一次I2C传输结束(检测到Stop信号)后,如果RX FIFO中仍有数据但未达到阈值,或者TX方向仍有数据需要发送但不足一个阈值量,控制器就会产生一个特殊的排空中断:RDR(Receive Draining) 或XDR(Transmit Draining)。此时,CPU需要介入,读取I2C_BUFSTAT.RXSTAT或I2C_BUFSTAT.TXSTAT来获知剩余字节数,然后手动读取或写入这些数据,或者重新配置DMA进行一次小规模传输。如果没有启用或正确处理Draining,就可能触发AERR(Access Error) 中断,或者数据永远滞留在FIFO中。
3. 中断模式下的FIFO管理详解
中断模式是理解所有机制的基础。我们分别从接收和发送两个方向,拆解其工作流程和关键陷阱。
3.1 接收中断 (RRDY) 流程与配置要点
接收流程的目标是高效地从RX FIFO中取出数据。核心中断是RRDY(Receive Ready)。
工作流程:
- 初始化:配置
I2C_BUF.RXTRSH(例如设为7),使能I2C_IRQENABLE_SET.RRDY_IE = 1。 - 触发:当从设备返回数据,使得RX FIFO中的数据字节数大于
RXTRSH(即 >=RXTRSH + 1)时,RRDY标志位在I2C_IRQSTATUS_RAW中被置位,并向CPU产生中断。 - 响应:CPU进入中断服务程序(ISR)。
- 读取状态:ISR首先读取
I2C_IRQSTATUS_RAW寄存器,确定是RRDY中断。 - 批量读取:从
I2C_DATA寄存器连续读取RXTRSH + 1字节的数据。这是最高效的方式,因为一次中断处理尽可能多的数据。 - 清除中断:向
I2C_IRQSTATUS_RAW.RRDY位写入1,清除中断标志。注意:如果清除中断后,FIFO中的数据量仍然高于阈值,RRDY标志会立即再次被置位,可能马上触发下一次中断。 - 循环:重复步骤2-6,直到所有数据接收完成。
- 处理零头:如果最后一批数据不足
RXTRSH + 1字节,RRDY中断将永远不会被触发。此时需要依靠Draining机制(见第5章)或切换到轮询模式读取最后几个字节。
关键配置与陷阱:
- 阈值选择:
RXTRSH的选择是一场权衡。设得太小(如0),每收到1字节就中断,效率低下,等同于无FIFO。设得太大(如31),中断频率低,但每次中断处理的数据量大,可能导致ISR执行时间过长,且最后一次传输零头数据的概率和量都会增大。通常根据总传输长度和系统中断响应能力,选择8或16是一个不错的起点。 - 中断清除时机:务必在读取完数据后再清除
RRDY中断标志。如果先清除标志,但在读取数��过程中,新的数据又涌入FIFO并再次满足了阈值条件,可能会丢失这次中断事件(取决于中断控制器是边沿触发还是电平触发)。安全的做法是:读数据 -> 清除标志。 - FIFO深度查询:在ISR中,可以通过读取
I2C_BUFSTAT.RXSTAT来实时知道FIFO中有多少字节,而不是僵化地只读RXTRSH+1个字节。这在处理不定长数据或担心数据堆积时非常有用。
3.2 发送中断 (XRDY) 流程与核心差异
发送流程的目标是及时向TX FIFO中补充数据,避免下溢(Underflow)。核心中断是XRDY(Transmit Ready)。
工作流程:
- 初始化:配置
I2C_BUF.TXTRSH(例如设为0),使能I2C_IRQENABLE_SET.XRDY_IE = 1。注意:对于Slave发送模式,手册强烈建议设置TXTRSH = 0(阈值1),原因后述。 - 触发:当TX FIFO中的数据字节数小于等于
TXTRSH(即 FIFO 空余空间 >=TXTRSH + 1)时,XRDY标志位置位,产生中断。特别地,当FIFO完全为空时,一定会触发XRDY。 - 响应:CPU进入ISR。
- 批量写入:向
I2C_DATA寄存器连续写入TXTRSH + 1字节的数据。 - 清除中断:写入1清除
I2C_IRQSTATUS_RAW.XRDY标志。 - 循环:重复步骤2-5,直到所有数据发送完成。
- 处理零头:同接收,最后不足一个阈值的数据需要Draining机制处理。
发送模式的特殊考量:
- Slave发送模式的陷阱:这是重点!当I2C作为从设备发送器时,主设备随时可能通过发送NACK来终止读取。如果
TXTRSH设置得较大(比如15),那么当FIFO为空时,控制器会请求CPU/DMA填充16字节的数据。但如果主设备只读了5字节就发送NACK停止,那么剩下的11字节数据就永远留在FIFO里,成为“死数据”。因此,手册明确建议在Slave TX模式下,设置TXTRSH = 0(阈值1),这样主设备每请求一个字节,FIFO空就触发一次中断,CPU只准备下一个字节,避免了数据残留。如果不得不使用大阈值,则必须在传输结束后,检查NACK中断并手动清除TX FIFO(使用I2C_BUF.TXFIFO_CLR)。 - 下溢(XUDF):如果CPU没有及时响应
XRDY中断来补充数据,导致移位寄存器为空且FIFO也为空,但I2C_CNT显示还有数据要发送,就会发生下溢。XUDF标志位会被置位,控制器会拉低SCL线以挂起总线,等待数据。这通常意味着你的中断响应太慢或系统负载过重。
4. DMA模式下的高效数据传输
DMA模式将CPU从繁重的数据搬运工作中彻底解放出来,是实现高效大数据量传输的关键。其核心思想是将FIFO的阈值触发信号 (RRDY/XRDY) 直接转换为DMA请求信号 (I2C_DMA_RX/I2C_DMA_TX)。
4.1 DMA接收配置与流程
- 外设端配置:
- 设置
I2C_BUF.RXTRSH为期望的阈值(如7)。 - 设置
I2C_BUF.RDMA_EN = 1,启用DMA接收通道。 - 设置
I2C_DMARXENABLE_SET.DMARX_ENABLE_SET = 1,使能DMA请求生成。
- 设置
- DMA控制器配置:
- 源地址 (Source Address):固定为
I2C_DATA寄存器的地址。 - 目的地址 (Destination Address):指向内存中接收缓冲区的地址。
- 传输长度 (Transfer Size):通常配置为
(RXTRSH + 1) * N,其中N是整数,表示期望的DMA传输次数。但更关键的是同步类型。 - 同步类型 (Synchronization):必须配置为外设同步。即每次传输都由I2C控制器产生的
I2C_DMA_RX请求来触发。 - 单次传输大小 (Element Size):设置为1字节(8位)。
- 源地址 (Source Address):固定为
- 工作流程:
- I2C开始接收数据,当RX FIFO中数据量 >
RXTRSH时,I2C_DMA_RX请求信号有效。 - DMA控制器收到请求,执行一次从
I2C_DATA到目标内存的1字节传输。 - 关键点:DMA控制器需要被配置为,每收到一次请求,就传输
RXTRSH + 1个字节(一个数据块)。这通常通过DMA控制器的“帧(Frame)”或“突发(Burst)”概念来实现。例如,设置DMA每次触发传输一个包含8个元素的数组。 - 传输完成后,DMA控制器可能需要在完成中断中,清除I2C的DMA请求使能位 (
I2C_DMARXENABLE_CLR),或者由I2C在传输结束时自动处理。
- I2C开始接收数据,当RX FIFO中数据量 >
- Draining处理:对于最后不足一个阈值的零头数据,DMA模式同样需要处理。当I2C传输结束,且RX FIFO中有剩余数据时,会触发
RDR中断。此时,CPU需要读取I2C_BUFSTAT.RXSTAT获得剩余字节数,然后重新配置DMA通道,将传输长度改为这个剩余值,并启动一次性的DMA传输来排空FIFO。或者,CPU直接手动读取这些字节。
4.2 DMA发送配置与流程
- 外设端配置:
- 设置
I2C_BUF.TXTRSH(主模式可根据需要设置,从模式建议为0)。 - 设置
I2C_BUF.XDMA_EN = 1。 - 设置
I2C_DMATXENABLE_SET.DMATX_ENABLE_SET = 1。
- 设置
- DMA控制器配置:
- 源地址:指向内存中发送缓冲区的地址。
- 目的地址:固定为
I2C_DATA寄存器的地址。 - 传输长度:总发送字节数。
- 同步类型:外设同步,由
I2C_DMA_TX请求触发。 - 单次传输大小:1字节。
- 工作流程:
- 初始时TX FIFO为空,
I2C_DMA_TX请求立即有效。 - DMA控制器收到请求,将
TXTRSH + 1字节的数据写入I2C_DATA。 - I2C控制器开始发送数据,当FIFO中数据量 <=
TXTRSH时,再次产生DMA请求。 - 循环直到所有数据写入FIFO。
- 初始时TX FIFO为空,
- 阈值高低的影响:手册中的图15-12和15-13清晰地展示了
TXTRSH高低的影响。- 高阈值 (High Threshold):例如
TXTRSH=15。DMA请求在FIFO较空时(数据量<=15)就触发,允许DMA一次性写入16字节。这能更快地填满FIFO,减少总线空闲,提高吞吐率,但可能增加数据传输的延迟(因为要攒够一定数据才发送)。 - 低阈值 (Low Threshold):例如
TXTRSH=0。DMA请求仅在FIFO完全为空时触发,每次只写入1字节。这保证了最快的响应速度,但DMA请求频率极高,可能增加系统总线竞争,降低整体效率。在Slave发送模式下,必须使用低阈值(0)来避免数据残留问题。
- 高阈值 (High Threshold):例如
4.3 DMA模式下的常见问题排查
- DMA传输卡住,不完成:首先检查DMA请求信号是否正常产生。用逻辑分析仪或示波器查看
I2C_DMA_TX/RX信号线。如果没有请求,检查I2C的DMA使能位 (RDMA_EN/XDMA_EN) 和请求使能位 (DMARX_ENABLE_SET等) 是否正确设置。其次,检查DMA通道的源/目的地址、传输长度配置是否正确,特别是地址是否自增(Increment)。 - 数据错位或丢失:确保DMA的传输数据宽度(Element Size)与I2C数据寄存器宽度(8位)匹配。检查内存缓冲区对齐问题。在DMA完成中断中,确认DMA传输的字节数与I2C
I2C_CNT寄存器设定的值是否一致。 - Draining中断未触发,数据残留:确认
I2C_IRQENABLE_SET.RDR_IE或XDR_IE是否已使能。检查传输总长度是否确实是FIFO阈值的整数倍。如果不是,必须使能Draining中断并正确处理。 - Access Error (AERR) 中断:这是Draining机制未正确处理的最直接表现。当DMA试图读取空RX FIFO,或��入满TX FIFO时,就会触发此中断。这通常发生在传输末尾,DMA被配置的传输长度大于实际可用的数据量(对于RX)或剩余空间(对于TX)。解决方案就是正确启用和处理
RDR/XDR中断。
5. Draining机制:处理“零头”数据的艺术
Draining机制是FIFO管理中最精巧也最容易疏忽的一环。它的存在,确保了任何非阈值整数倍的传输都能被完整处理。
5.1 接收排空 (RDR) 机制
触发条件:一次I2C接收传输结束(检测到Stop条件),且此时RX FIFO中的数据量大于0但小于等于RXTRSH(即未达到常规的RRDY或DMA请求触发条件)。
软件响应流程:
I2C_IRQSTATUS_RAW.RDR标志位置1,如果已使能 (RDR_IE=1),则产生中断。- CPU在中断服务程序中,读取
I2C_BUFSTAT.RXSTAT寄存器。这个寄存器的值就是FIFO中剩余的、待读取的字节数。 - 根据当前模式采取行动:
- 中断模式:CPU直接从
I2C_DATA寄存器连续读取RXSTAT次。 - DMA模式:CPU需要重新编程DMA控制器。将DMA传输长度修改为
RXSTAT的值,然后启动这次特殊的DMA传输。这次传输完成后,整个接收过程才算结束。
- 中断模式:CPU直接从
- 清除
RDR中断标志位。
5.2 发送排空 (XDR) 机制
触发条件:在I2C主发送模式下,所有待发送数据已通过DMA或中断写入流程提交,但最后剩余待写入TX FIFO的字节数小于TXTRSH + 1。
软件响应流程:
I2C_IRQSTATUS_RAW.XDR标志位置1,如果已使能 (XDR_IE=1),则产生中断。- CPU读取
I2C_BUFSTAT.TXSTAT寄存器。这个值表示还需要向TX FIFO写入多少字节,才能完成整个数据块的发送。 - 根据当前模式采取行动:
- 中断模式:CPU直接向
I2C_DATA寄存器连续写入TXSTAT个字节。 - DMA模式:重新配置DMA,发起一次长度为
TXSTAT的传输。
- 中断模式:CPU直接向
- 清除
XDR中断标志位。
5.3 实践中的注意事项与优化技巧
- 使能时机:
RDR_IE和XDR_IE通常不需要一直开启。可以在启动一次传输前开启,在传输完成确认后关闭。这可以减少不必要的中断。 - Slave发送模式的例外:如前所述,Slave发送模式无法使用Draining机制,因为主机随时可能停止读取,传输长度未知。因此,在Slave TX模式下,要么设置
TXTRSH=0,要么在收到NACK中断后,手动清除TX FIFO (I2C_BUF.TXFIFO_CLR)。 - 状态寄存器的原子性:在读取
RXSTAT/TXSTAT和实际操作FIFO之间,如果发生了更高优先级的中断并操作了I2C,状态可能改变。在可靠性要求高的场合,可以考虑在操作期间短暂关闭全局中断或使用信号量保护。 - 计算替代方案:手册提到,在主模式下,CPU可以通过计算
DCOUNT(数据计数器)模除TXTRSH/RXTRSH来提前知道剩余字节数,从而可能避免使用Draining中断。但这需要软件维护精确的计数,在复杂的中断和DMA交互场景下容易出错,通常直接使用硬件Draining机制更为可靠。
6. 实战配置步骤与问题排查实录
结合手册第15.2.14节的编程指南,我将一个完整的I2C主模式DMA传输配置流程细化,并附上常见问题。
6.1 完整的主模式DMA传输配置流程
以下以主设备发送器,使用DMA TX为例,目标速率100kbps:
模块基础配置:
// 1. 配置时钟预分频器,得到约12MHz的模块功能时钟(根据系统时钟计算) I2C_PSC = (SystemClockInMHz / 12) - 1; // 2. 配置SCL高低时间,得到100kbps (12MHz模块时钟下,SCLL=SCLH=60) I2C_SCLL = 60; I2C_SCLH = 60; // 3. 配置自身地址(主模式通常可忽略,或配置为不冲突的地址) I2C_OA = 0x00; // 4. 使能I2C模块 I2C_CON |= I2C_EN;FIFO与DMA配置:
// 5. 配置TX FIFO阈值,假设设为7(即阈值8) I2C_BUF.TXTRSH = 7; // 6. 使能TX DMA通道 I2C_BUF.XDMA_EN = 1; // 7. 使能TX DMA请求生成 I2C_DMATXENABLE_SET = 0x1; // 设置DMATX_ENABLE_SET位 // 8. 使能XDR中断,用于处理零头数据 I2C_IRQENABLE_SET.XDR_IE = 1; // 9. 配置DMA控制器: // - 源地址: 发送数据缓冲区地址 // - 目的地址: &I2C_DATA // - 传输长度: 总发送字节数 // - 同步模式: 外设触发,触发源为I2C_DMA_TX // - 单次传输大小: 8位 // - 传输模式: 每次触发传输 (TXTRSH+1)=8 个字节 DMA_ConfigureChannel(I2C_TX_CH, ...);启动传输:
// 10. 配置从设备地址和传输字节数 I2C_SA = SlaveAddress; I2C_CNT = TotalBytesToSend; // 11. 等待总线空闲 while(I2C_IRQSTATUS_RAW.BB); // 12. 配置为主发送模式,并产生起始条件 I2C_CON |= (MST | TRX | STT); // 13. 启动DMA传输 DMA_StartChannel(I2C_TX_CH);处理Draining中断(XDR):
void I2C_ISR(void) { if(I2C_IRQSTATUS_RAW.XDR) { uint8_t remaining_bytes = I2C_BUFSTAT.TXSTAT; // 重新配置DMA,进行一次性小规模传输 DMA_Reconfigure(I2C_TX_CH, remaining_bytes); DMA_StartChannel(I2C_TX_CH); // 或者,直接CPU写入剩余字节 // for(int i=0; i<remaining_bytes; i++) I2C_DATA = tx_buffer[offset++]; I2C_IRQSTATUS_RAW.XDR = 1; // 写1清除中断标志 } // ... 处理其他中断 }
6.2 常见问题排查速查表
| 现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 数据发送/接收不完整,总是少最后几个字节 | Draining机制未启用或未正确处理。 | 1. 检查RDR_IE/XDR_IE是否使能。2. 在传输结束中断中,检查 RDR/XDR标志位是否置位。3. 确保在 RDR/XDRISR中正确读取了RXSTAT/TXSTAT并处理了剩余数据。 |
| 触发AERR (Access Error) 中断 | 软件试图读取空RX FIFO或写入满TX FIFO。 | 1. 在DMA模式下,通常是Draining问题(见上一条)。 2. 在中断模式下,检查中断服务程序读写 I2C_DATA的次数是否超过了FIFO中实际可用的数据量。可以通过查询RXSTAT/TXSTAT来动态决定读写次数,而非固定THRSH+1。 |
| DMA传输启动后毫无动静 | DMA请求未产生或DMA配置错误。 | 1. 确认I2C_BUF.XDMA_EN和I2C_DMATXENABLE_SET已正确设置。2. 用调试器或指示灯检查 I2C_DMA_TX信号是否变高。3. 检查DMA通道的触发源选择是否正确映射到I2C的DMA请求线。 4. 检查DMA通道是否已使能。 |
| Slave发送时,主设备收到错误数据或NACK | Slave TX FIFO配置不当,导致数据残留。 | 1.确保在Slave发送模式下,设置TXTRSH = 0。2. 如果必须用大阈值,则在检测到NACK中断后,执行 I2C_BUF.TXFIFO_CLR = 1来清空FIFO。 |
| 中断频率异常高,系统负载大 | FIFO阈值 (RXTRSH/TXTRSH) 设置过小。 | 适当增大阈值。权衡中断处理开销和单次中断处理的数据量。对于大数据量传输,优先考虑DMA模式。 |
| 总线被锁死,SCL线持续为低 | 发生下溢 (XUDF) 或仲裁丢失 (AL)。 | 1. 检查XUDF标志。如果是,说明CPU/DMA未能及时补充发送数据,优化数据供给流程。2. 检查 AL标志。如果是,说明多主竞争总线失败,检查总线冲突或软件启动传输时未检查BB(Bus Busy)标志。 |
| 使能DMA后,对应的RRDY/XRDY中断不再产生 | 这是正常现象。一旦使能RDMA_EN/XDMA_EN,对应的RRDY/XRDY中断标志位会被硬件强制拉低,中断不会产生,取而代之的是DMA请求信号。确保你使用的是DMA请求,而不是等待中���。 |
调试I2C FIFO相关问题时,I2C_BUFSTAT寄存器是你的最佳朋友。时刻关注RXSTAT和TXSTAT,可以清晰了解FIFO的实时状态。结合逻辑分析仪查看SCL/SDA波形和DMA请求信号,几乎能定位所有硬件交互层面的问题。最后,仔细阅读芯片勘误表(Errata),有些FIFO或DMA相关的异常行为可能是硅片bug,需要通过软件 workaround 来解决。