1. 项目概述与核心价值
在嵌入式开发中,I2C总线因其简洁的两线制(SDA, SCL)和灵活的多主多从架构,成为了连接各类传感器、EEPROM、RTC等外设的首选。然而,当数据吞吐量增大或系统实时性要求提高时,传统的轮询或中断驱动数据传输方式就会暴露出CPU占用率高、响应延迟等问题。这时,将DMA(直接内存访问)与I2C的FIFO(先进先出缓冲区)结合使用,就成了一种高效的解决方案。这不仅仅是“让数据跑得更快”,更是一种系统资源与性能的精细化管理艺术。
我曾在多个涉及高速数据采集(如图像传感器)或需要与多个从设备进行批量交换的项目中,深刻体会到手动管理每一个字节的繁琐与低效。通过配置I2C的Burst模式,并利用DMA自动搬运FIFO中的数据,CPU得以从繁重的数据搬运工作中解放出来,可以去处理更上层的业务逻辑或进入低功耗模式。本文将以德州仪器(TI)的Tiva™ C系列微控制器(如TM4C129)的I2C模块为例,深入解析其主从模式下,DMA与FIFO协同工作的内部机制、寄存器配置的每一个比特位的含义,以及在实际编程中如何避开那些手册上不会写的“坑”。无论你是正在优化现有驱动,还是从头设计一个高效的外设通信层,这些从芯片手册和实战中提炼的细节,都将为你提供清晰的路径。
2. I2C与DMA、FIFO协同工作原理深度拆解
要理解这套机制,我们不能孤立地看I2C、DMA或FIFO,而要将它们视为一个协同工作的数据流水线。其核心思想是:将数据搬移工作从CPU卸载给DMA,并用FIFO作为数据流速的“缓冲池”和“触发器”。
2.1 核心组件角色定义
- I2C引擎:负责按照I2C协议规范,生成START、STOP、地址、数据比特位和ACK/NACK信号。它是通信协议的最终执行者。
- Tx/Rx FIFO:这是I2C模块内部的硬件缓冲区。通常深度为8字节(具体需查数据手册)。Tx FIFO用于缓存待发送的数据,Rx FIFO用于缓存接收到的数据。它们的关键作用是解耦数据生产(CPU/DMA写入)和消费(I2C引擎发送),或者数据消费(CPU/DMA读取)和生产(I2C引擎接收)。
- DMA控制器 (µDMA):一个独立于CPU的外设,能够在内存(如SRAM中的数组)和I2C的FIFO数据寄存器(
I2CFIFODATA)之间直接搬运数据。它只关心源地址、目的地址和传输量,不关心数据内容。 - 请求信号 (dma_req & dma_sreq):这是连接I2C模块和DMA控制器的“握手信号”。它们是整个自动传输流程的“开关”。
dma_sreq(单次请求):当FIFO需要被服务(例如,Tx FIFO未满但低于某个水平,或Rx FIFO非空)时,此信号有效,请求DMA进行一次单次传输(通常是1字节)。dma_req(突发请求/Burst Request):当满足特定条件(如Tx FIFO空余空间较大,或Rx FIFO中数据量达到一定阈值)时,此信号有效,请求DMA进行一次突发传输(连续传输多个字节)。
2.2 主模式(Master)下的数据传输流程
根据你提供的资料,我们重点分析Burst模式下的逻辑,这是效率最高的模式。
2.2.1 主发送(Master Transmit)流程
假设我们要通过DMA发送一批数据。
- 初始化:CPU配置好I2C主模式时钟、从机地址,并使能FIFO和DMA Burst模式。关键一步是设置
I2CMBLEN(Master Burst Length)寄存器,比如设为8,告知I2C模块“本次Burst传输总共有8个字节”。 - 启动传输:CPU写入
I2CMCS寄存器,发出一个带有START、BURST位使能的命令。I2C引擎开始发送起始条件和从机地址。 - FIFO填充与DMA请求:
- 初始时,Tx FIFO是空的。I2C模块会立即断言
dma_sreq信号,因为FIFO需要数据。 - 关键条件判断:如果
I2CMBLEN >= 4且Tx FIFO的填充水平低于预设的触发值(TXTRIG),那么dma_req信号也会被断言。这告诉DMA控制器:“FIFO很空,你可以一口气给我多发点数据过来”。 - DMA控制器响应
dma_req,启动一次突发传输,将多个字节(具体数量由DMA通道配置决定,通常可以匹配Burst Length)从内存搬运到I2CFIFODATA寄存器,从而快速填充Tx FIFO。
- 初始时,Tx FIFO是空的。I2C模块会立即断言
- 流水线操作:在DMA填充FIFO的同时,I2C引擎已经在从Tx FIFO中取出字节,按照SCL时钟节拍一位位地发送到SDA线上。这是一个并行的过程。
- 请求停止:DMA会持续响应请求,直到累计传输的字节数达到
I2CMBLEN寄存器中设定的值。此时,I2CMBCNT(Burst Count)递减到0,I2C模块停止产生任何DMA请求。但I2C引擎会继续将FIFO中剩余的数据发送完毕。 - 传输结束:最后一个字节发送完成后,I2C引擎根据
I2CMCS中STOP位的设置,决定是否产生停止条件。
注意:这里有一个极易出错的细节。资料中特别提到:
The TXFEIM interrupt mask bit in the I2CMIMR register should be clear (masking the TXFE interrupt) when the master is performing an RX Burst from the RXFIFO and should be unmasked before starting a TX FIFO transfers.这句话的意思是,当主设备正在使用Rx FIFO进行接收Burst时,必须屏蔽Tx FIFO空中断(TXFEIM)。这是因为在Burst模式下,FIFO的控制权很大程度上交给了DMA和硬件状态机,如果此时Tx FIFO空中断被误触发,可能会导致软件错误地尝试向FIFO写数据,干扰DMA操作。在开启Tx FIFO传输前,再根据需要解除屏蔽。这是一个典型的“模式切换”清理工作,容易被忽略。
2.2.2 主接收(Master Receive)流程
接收流程是发送的镜像,但触发逻辑稍有不同。
- 初始化与启动:类似发送,配置为接收模式,设置
I2CMBLEN,发出Burst接收命令。 - FIFO排空与DMA请求:
- 初始时,Rx FIFO为空,无请求。
- 当I2C引擎从从机接收到第一个字节并存入Rx FIFO后,
dma_sreq立即被断言,表示“有数据待取走”。 - 关键条件判断:如果Rx FIFO中累积的数据大于等于4字节,
dma_req信号也会被断言。这告诉DMA:“数据已经攒够一批了,快来一次性搬走”。
- DMA响应:DMA控制器响应请求,将数据从
I2CFIFODATA寄存器搬移到目标内存地址。 - 循环直至完成:此过程持续,直到DMA搬运的总字节数达到
I2CMBLEN设定的值。
2.3 从模式(Slave)下的DMA FIFO机制
从模式的DMA FIFO机制与主模式类似,但触发逻辑由“主设备的请求”来驱动。
- 从发送(Slave Transmit):
- 当主设备向从设备请求数据(读操作)时,如果从设备的Tx FIFO被启用且未满,它会断言
dma_sreq。 - 如果此时FIFO填充水平低于触发值,
dma_req也会被断言,DMA将一批数据从内存搬入Tx FIFO,以备主设备读取。
- 当主设备向从设备请求数据(读操作)时,如果从设备的Tx FIFO被启用且未满,它会断言
- 从接收(Slave Receive):
- 当主设备向从设备发送数据(写操作)时,数据被存入从设备的Rx FIFO。
- 只要Rx FIFO中有数据,
dma_sreq即被断言。 - 当Rx FIFO中的数据量超过触发水平(
RXTRIG)时,dma_req被断言,DMA将数据从FIFO批量搬移到内存。
重要实践建议:资料中特别警告:
Best practice recommends that an application should not switch between the I2CSDR register and TX FIFO or vice versa for successive transactions.这意味着,不要在连续的事务中混合使用I2CSDR(单字节数据寄存器)和Tx FIFO模式。例如,如果你在一次通信中使用FIFO+DMA发送数据,下一次通信最好也沿用此模式,而不是突然改用查询I2CSDR的方式。混合使用可能导致FIFO指针、状态机处于不可预测的状态,引发数据错乱或通信超时。在软件设计上,应为同一从设备固定一种数据传输模式。
3. 关键寄存器配置与命令序列实战解析
理解了原理,我们来看如何用代码实现。TI Tiva™的I2C模块寄存器虽多,但围绕DMA FIFO Burst传输的核心配置集中在几个关键寄存器上。
3.1 核心寄存器功能详解
- I2CMCR (Master Configuration Register):通常写入
0x0000.0010来初始化主模块,并可能使能相关特性。 - I2CMTPR (Master Timer Period Register):用于设置SCL时钟频率。计算公式为:
TPR = (SysClk / (2 * (SCL_LP + SCL_HP) * SCL_CLK)) - 1其中SCL_LP和SCL_HP通常固定为6和4(标准/快速模式)。例如,系统时钟20MHz,目标100kHz标准模式:TPR = (20,000,000 / (2*10*100,000)) - 1 = 9。 - I2CMCS (Master Control/Status Register):这是灵魂寄存器,读写具有不同功能。
- 写操作:发送控制命令。
RUN、START、STOP、ACK、BURST位的组合,决定了状态机的跳转(详见表21-5)。例如,写入0x7(二进制---0-111)表示:RUN=1,START=1,STOP=1,ACK=X,BURST=0,即“发送起始条件,传输单字节数据,然后发送停止条件”。 - 读操作:获取状态。
BUSY、ERROR、BUSBSY(总线忙)等位至关重要,用于查询传输状态。
- 写操作:发送控制命令。
- I2CMBLEN (Master Burst Length):设置一次Burst传输期望的总字节数。DMA请求的生成会以此值为目标。
- I2CFIFOCTL (FIFO Control Register):用于使能Tx/Rx FIFO,并设置它们的触发水平(
TXTRIG,RXTRIG)。触发水平决定了何时断言dma_req。例如,设置TXTRIG=1,意味着当Tx FIFO中剩余空间大于等于7个字节(假设8字节深)时,就可能触发dma_req,以便DMA一次性填充多个字节。 - I2CMIMR (Master Interrupt Mask Register):如前所述,在Burst模式下需要谨慎管理中断屏蔽位,特别是
TXFEIM(Tx FIFO空中断)和RXFFIM(Rx FIFO满中断),避免与DMA操作冲突。
3.2 主模式Burst传输配置步骤
以下是一个配置主设备使用DMA FIFO进行Burst发送的典型步骤序列,结合了资料中的流程图和寄存器描述:
外设时钟与GPIO初始化:使能I2C和对应GPIO端口的时钟,配置SDA和SCL引脚为I2C复用功能,并将SDA引脚配置为开漏输出(这是I2C总线要求)。
// 示例代码框架,基于TivaWare库 SysCtlPeripheralEnable(SYSCTL_PERIPH_I2C0); SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOx); // x根据具体引脚 // 等待外设就绪... GPIOPinConfigure(GPIO_Px_y_I2C0SCL); GPIOPinConfigure(GPIO_Px_z_I2C0SDA); GPIOPinTypeI2C(GPIO_PORTx_BASE, GPIO_PIN_y | GPIO_PIN_z);I2C主模块基础配置:初始化主控器,设置总线速度。
I2CMasterInitExpClk(I2C0_BASE, SysCtlClockGet(), false); // 初始化,false表示不启用高速模式 // 或者手动配置 I2CMTPR配置并启用DMA:这不是I2C寄存器的配置,而是对µDMA控制器的配置。你需要设置一个DMA通道,源地址为内存中的数据数组,目的地址为
I2CFIFODATA寄存器,配置传输大小为字节,并启用该通道。同时,要将该通道的触发源设置为对应的I2C DMA请求(如UDMA_CHANNEL_I2C0TX)。配置I2C FIFO与Burst模式:
// 使能Tx FIFO,并设置触发水平为2(即FIFO中数据少于2个时,可能触发dma_req) HWREG(I2C0_BASE + I2C_O_FIFOCTL) = (2 << I2C_FIFOCTL_TXTRIG_S) | I2C_FIFOCTL_TXFIFO_EN; // 设置本次Burst传输总字节数,例如16字节 HWREG(I2C0_BASE + I2C_O_MBLEN) = 16; // 注意:根据之前提到的注意事项,确保TXFEIM中断被屏蔽(如果之前可能被使能) HWREG(I2C0_BASE + I2C_O_MIMR) &= ~I2C_MIMR_TXFEIM;启动Burst传输:
// 1. 写入从机地址和方向(写) I2CMasterSlaveAddrSet(I2C0_BASE, slaveAddr, false); // 2. 写入I2CMCS寄存器,发起带START的Burst传输命令。 // 查表21-5,对于Idle状态,要发起Burst发送,需要设置 BURST=1, START=1, RUN=0, STOP=0。 // 对应I2CMCS[6:0] = `0b0_1_0_X_0_0_1` (BURST, START, STOP, ACK, HS, QCMD, RUN)。 // 假设ACK位随意(0),则值为: (1<<6) | (1<<1) = 0x42。 HWREG(I2C0_BASE + I2C_O_MCS) = 0x42;写入此命令后,I2C状态机从Idle跳转到Master Transmit状态,并开始发送START条件和地址。由于Tx FIFO为空且
I2CMBLEN>=4,dma_req被断言,DMA开始自动向FIFO灌入数据。等待传输完成:此时CPU可以去做其他任务。可以通过以下方式判断传输结束:
- 查询DMA完成中断:配置DMA通道传输完成中断,这是最高效的方式。
- 查询I2C状态:轮询
I2CMCS寄存器的BUSY位,等待其变为0。同时检查ERROR位确认传输成功。
while(HWREG(I2C0_BASE + I2C_O_MCS) & I2C_MCS_BUSY) { // 等待传输完成 } if(HWREG(I2C0_BASE + I2C_O_MCS) & I2C_MCS_ERROR) { // 错误处理 }
3.3 命令序列流程图解读
资料中的图21-10(Master TRANSMIT of Multiple Data Bytes)和图21-11(Master RECEIVE of Multiple Data Bytes)清晰地描绘了非Burst模式(即CPU轮询I2CMDR)下多字节传输的状态流。而Burst模式可以看作是这些流程的“自动化”版本。
以图21-10为例,其核心循环是“写数据到I2CMDR-> 发送命令(RUN)-> 等待BUSY结束 -> 检查错误”。在Burst模式下,这个“写数据到I2CMDR”的步骤被DMA自动向I2CFIFODATA写数据所替代,而“发送命令”环节,在初始的Burst启动命令后,后续的字节传输由硬件自动管理,直到I2CMBCNT减为0。
理解这些流程图的价值在于,当Burst模式或DMA出现问题时,你可以退回到最基本的轮询模式进行调试,逐字节地控制通信过程,从而隔离问题是在I2C协议层、FIFO配置层还是DMA配置层。
4. 常见问题排查与实战调试技巧
即便理解了所有原理和步骤,实际调试中依然会遇到各种问题。以下是我在项目中总结的一些常见陷阱和排查方法。
4.1 DMA传输未启动或数据不完整
- 症状:程序启动后,I2C总线无活动,或只发送了地址和第一个字节后停止。
- 排查思路:
- 检查DMA通道触发源:确认DMA通道的触发源是否正确设置为对应的I2C TX或RX请求。这是最容易被配错的一步。
- 检查
I2CMBLEN寄存器:确认其值已正确设置为大于0的数。如果为0,I2C模块不会产生任何DMA请求。 - 检查FIFO使能与触发水平:通过读取
I2CFIFOSTATUS寄存器,确认Tx/Rx FIFO是否已使能(TXFE/RXFF位),以及当前FIFO的填充水平。结合触发水平设置,判断是否满足产生dma_req的条件。 - 检查
I2CMCS的BURST位:在发送启动命令时,是否正确地设置了BURST=1且RUN=0?参考表21-5,对于Burst传输,RUN位在启动命令中应为0。 - 验证DMA配置:单独测试DMA通道,配置其触发源为软件触发(Software Transfer),手动启动一次传输,看是否能正确搬运数据。这可以排除DMA本身配置(如地址、数据大小、传输模式)的问题。
4.2 通信错误(ERROR位被置位)
- 症状:传输后检查
I2CMCS寄存器,发现ERROR位为1。 - 排查思路:
- 检查从机地址和ACK:首先检查
ADRACK(地址应答)和DATACK(数据应答)位,确定是从机未应答地址还是未应答数据。用逻辑分析仪或示波器抓取I2C波形是最直接的方法。 - 检查总线竞争:检查
ARBLST(仲裁丢失)位。在多主系统中,可能发生了总线仲裁失败。 - 检查时钟超时:检查
CLKTO位。如果SCL线被从设备长时间拉低,可能导致主设备超时。检查从设备是否正常工作,或调整I2CMCLKOCNT(主时钟低超时计数)寄存器。 - Burst模式下的时序:在Burst模式下,DMA填充FIFO的速度必须跟上I2C发送的速度。如果DMA响应太慢,导致Tx FIFO在下一次SCL时钟边沿到来时为空(Underrun),可能会引发错误。可以尝试提高DMA通道的优先级,或调整FIFO触发水平,让DMA请求更早发出。
- 检查从机地址和ACK:首先检查
4.3 数据错乱或重复
- 症状:接收到的数据与发送的不符,或某些字节重复出现/丢失。
- 排查思路:
- 内存对齐与缓冲区大小:确保DMA源/目标内存地址是字节对齐的(通常要求),并且缓冲区大小至少等于
I2CMBLEN。DMA传输越界会覆盖其他内存数据,导致不可预知的行为。 - 混合模式警告:再次强调,绝对不要在Burst传输未完全结束前,或在一次Burst传输和下一次非Burst传输之间,混合使用
I2CFIFODATA和I2CMDR/I2CSDR寄存器。这会导致FIFO内部状态混乱。 - DMA传输完成中断处理:在DMA传输完成中断服务程序(ISR)中,除了清除中断标志,不要立即启动下一次I2C传输或修改关键的I2C控制寄存器(如
I2CMCS)。应等待当前I2C Burst传输完全结束(BUSY=0)。否则可能打断硬件状态机。
- 内存对齐与缓冲区大小:确保DMA源/目标内存地址是字节对齐的(通常要求),并且缓冲区大小至少等于
4.4 性能优化与进阶技巧
- 双缓冲(Ping-Pong Buffer):对于持续不断的数据流,可以配置两个DMA缓冲区,当DMA正在从缓冲区A搬运数据时,CPU可以处理已经接收满的缓冲区B的数据,反之亦然。这需要利用DMA的“Primary”和“Alternate”控制结构体来无缝切换。
- 动态调整Burst Length:根据每次传输的数据量大小,动态设置
I2CMBLEN。对于短数据包,使用较小的Burst Length甚至单次传输,可以减少不必要的DMA请求开销。对于长数据流,则设置为较大的值(但不要超过FIFO深度太多,以免DMA单次搬运耗时过长)。 - 合理设置FIFO触发水平:
TXTRIG和RXTRIG的设置是一种权衡。设置过低(如0或1),DMA请求会更频繁,可能增加总线开销,但能更快响应FIFO状态。设置过高(如6或7),DMA请求次数少,但需要FIFO积累更多数据/空间才触发,可能增加传输延迟。需要根据系统时钟、I2C速率和DMA延迟来实测调整。 - 利用总线监控功能:一些高级的微控制器(包括Tiva系列)的I2C模块提供
I2CMBMON(主总线监控)寄存器,可以实时查看SDA和SCL线的电平状态,这对于调试总线死锁、信号完整性问题非常有帮助。
调试I2C DMA FIFO问题,一个“从外到内,从简到繁”的策略很有效:首先确保在禁用DMA和FIFO的简单轮询模式下,I2C基本读写功能正常;然后单独启用FIFO,用CPU读写I2CFIFODATA来测试FIFO功能;最后再引入DMA,并仔细核对每一个请求和应答的环节。耐心和细致的寄存器级调试,是驾驭这类复杂外设交互的必经之路。