SPI中断与FIFO机制深度解析:从轮询到DMA的嵌入式通信优化

发布时间:2026/7/26 20:05:57
SPI中断与FIFO机制深度解析:从轮询到DMA的嵌入式通信优化 1. SPI接口核心原理与中断机制深度解析SPI全称Serial Peripheral Interface几乎是我在嵌入式开发生涯中打交道最多的通信协议之一。它不像I2C那样需要复杂的地址寻址和应答机制也不像UART那样依赖精确的波特率匹配。SPI的核心魅力在于其“简单粗暴”的高效——一个主设备通过时钟线SPICLK同步指挥着数据在MOSI主出从入和MISO主入从出线上流动片选线SPIEN/CS则像点名一样决定哪个从设备参与这场对话。这种全双工、同步通信的特性使得它在需要高速、实时数据交换的场景比如驱动TFT屏幕、读取高速ADC或连接Flash存储器时成为不二之选。但“简单”并不意味着可以轻视。很多新手在刚接触SPI时最容易栽在“轮询”这个坑里。他们写个死循环不停地去读状态寄存器看看数据来了没有或者发送寄存器空了没有。在小数据量、低频率下这似乎没问题。可一旦系统复杂起来多个任务并行这种“忙等待”会无情地吞噬宝贵的CPU周期让系统响应变得迟钝。这时中断机制的价值就凸显出来了。它允许CPU去处理其他任务只有当SPI接口有“事”要汇报比如数据收满了或者发完了时才通过一个中断信号“打断”CPU让它来紧急处理一下。这就像你雇了个助理平时你只管处理核心业务只有当他整理好一摞文件RX_FULL或者需要你签新文件TX_EMPTY时他才来敲你的门效率自然高得多。在SPI的中断世界里有几个关键“事件”是我们必须熟记于心的。TX_EMPTY发送寄存器空和RX_FULL接收寄存器满是最常用的一对。当发送寄存器或FIFO的数据被搬空TX_EMPTY事件就会触发这是在告诉你“主机快给我下一批数据我要断粮了”而RX_FULL则相反它说“主机我这儿数据堆满了你快来取走不然新数据没地方放了”处理这两个中断的流程是标准动作进入中断服务程序后先读取SPI_IRQSTATUS寄存器确认是谁触发了中断然后进行相应的数据读写操作写TX寄存器以补充数据读RX寄存器以清空数据最后至关重要的一步是向SPI_IRQSTATUS寄存器对应的状态位写1以清除中断标志。如果不做这一步中断线会一直保持有效状态导致CPU反复进入中断甚至可能卡死。这个“清除”操作手册里反复强调是很多中断异常问题的根源。除了这两个还有两个“异常”事件需要警惕。TX_UNDERFLOW发送下溢通常发生在从机模式下主设备时钟来了但从机的发送寄存器里却没有准备好的数据可以发出这时就会触发下溢。而RX_OVERFLOW接收溢出则更严重它意味着接收端寄存器或FIFO已经满了但新的数据又来了导致旧数据被覆盖而丢失。在从机模式下使用FIFO时手册明确指出RX_OVERFLOW事件不应出现一旦出现就标志着发生了数据丢失的错误。对于这两个事件中断服务程序中通常不需要进行数据读写操作来清除源头但必须记得清除它们的状态位否则中断线无法解除断言。1.1 中断与轮询的抉择何时用谁理解了中断机制我们还得回到一个根本问题什么时候该用中断什么时候用轮询这不是非黑即白的选择而是一个权衡。中断驱动这是处理异步、不可预测事件的理想方式。当数据到达时间不确定或者你不希望CPU在等待上浪费资源时中断是首选。例如一个SPI从设备随时可能被主设备访问或者在一个多任务RTOS中你希望SPI传输不阻塞高优先级任务。它的代价是中断上下文切换的开销以及编写相对复杂的中断服务程序ISRISR必须尽可能短小精悍。轮询简单、直接、可预测。当你进行的是连续的、批量的数据传输并且传输间隔和时长你完全可控时轮询往往更高效。比如在系统初始化阶段向一个SPI Flash写入一大段配置数据。你只需要在一个循环里检查状态位然后读写数据。它没有中断开销代码也直观。但缺点就是CPU被独占在传输期间无法响应其他事件。在CC323x的SPI驱动中这两种模式都得到了支持。通过配置SPI_IRQENABLE中断使能寄存器你可以自由选择让哪些事件触发硬件中断。如果某个事件的中断使能位被禁用那么即使该事件发生也不会拉起中断线但你依然可以通过软件定期读取SPI_IRQSTATUS寄存器来“轮询”检查事件是否发生。这种灵活性让开发者可以根据具体应用场景做出最合适的设计。2. FIFO缓冲区提升SPI吞吐量的关键引擎如果说中断机制是把CPU从“忙等”中解放出来那么FIFOFirst In, First Out缓冲区就是为高速数据流修建的“缓冲水库”。没有FIFO的SPI就像只有一个水杯在接水龙头的水接满一杯RX_FULL就得赶紧拿走CPU读取否则水就溢出了RX_OVERFLOW。发送也是倒空一杯TX_EMPTY就得赶紧续上。而FIFO的存在相当于给你配备了一个有刻度的水桶。CC323x的SPI模块内置了最大64字节的FIFO缓冲区这个缓冲区可以灵活配置。你可以将它作为一个64字节的统一缓冲区使用也可以将其分割成两个独立的32字节缓冲区分别用于发送和接收。这通过SPI_CHCONF寄存器中的FFEWFIFO使能用于发送和FFERFIFO使能用于接收位来控制。FIFO带来的最直接好处是减少中断频率提升传输效率。想象一下如果没有FIFO每接收1个字节假设字长为8位就会产生一次RX_FULL中断CPU要频繁地进行上下文切换。而有了FIFO我们可以设置一个阈值比如当FIFO中积累了16个字节AFL15时才产生一次中断通知CPU来批量读取。这大大降低了CPU的中断处理负担让它可以更连续地处理其他任务同时也为DMA直接内存访问的介入创造了条件。2.1 FIFO阈值配置的艺术AEL与AFLFIFO的核心配置在于两个阈值AELAlmost Empty Level几乎空电平和AFLAlmost Full Level几乎满电平。它们定义在SPI_XFERLEVEL寄存器中是协调SPI硬件与CPU或DMA工作节奏的“指挥棒”。AFL接收方向这个值定义了“什么时候通知主机来取数据”。手册里的描述很关键“If the user wants an interrupt or a DMA read request to be issued during a receive operation when the data buffer holds at least n bytes, then the buffer SPI_XFERLEVEL[AFL] must be set with n-1.”举个例子我希望当接收FIFO里至少有8个字节数据时就产生中断或DMA请求那么我应该设置AFL 8 - 1 7。这样当FIFO中数据量达到或超过8字节时RX_FULL中断或DMA读请求就会被触发。AEL发送方向这个值定义了“什么时候通知主机来填数据”。规则类似“If the user wants an interrupt or a DMA write request to be issued during a transmit operation when the data buffer is able to receive n bytes, then the buffer SPI_XFERLEVEL[AEL] must be set with n-1.”如果我希望当发送FIFO空余空间至少有4个字节时就请求新数据那么设置AEL 4 - 1 3。这里有一个非常重要的实操陷阱在中断模式下一旦RX_FULL中断被触发你必须连续、准确地读取足够次数的数据直到FIFO中的数据量低于AFL阈值下一次RX_FULL中断才会再次产生。手册原文警告“When FIFO is enabled, no new RX_full event is asserted until the local host has not performed the number of reads into the receive register defined by MCSPI_XFERLEVEL[AFL]. The local host must perform the correct number of reads.” 如果你只读了一部分数据FIFO水位仍在阈值之上那么即使有新数据进来也不会产生新中断可能导致数据“滞留”。在DMA模式下同理DMA控制器必须完成设定次数的读取后请求线才会解除断言。2.2 字计数器WCNT与EOW中断在FIFO模式下CC323x的SPI还提供了一个非常实用的功能字计数器WCNT和与之关联的EOWEnd-of-Word Count中断。这个功能特别适合需要精确控制传输数据总量的场景。你可以在SPI_XFERLEVEL[WCNT]中设置一个期望传输的总字数比如1024。当SPI控制器实际完成的数据传输数量达到这个设定值时就会触发EOW中断。这相当于给整个传输过程设置了一个“终点线”提醒。触发EOW中断后该通道的SPI传输会自动停止直到你重新加载WCNT值并再次使能通道。这个机制在需要发送固定长度数据包如通信协议帧或进行定长数据采集时非常有用可以避免复杂的软件计数逻辑减少出错概率。3. CC323x SPI驱动配置与实战代码剖析理论说得再多不如一行代码来得实在。我们以TI的SimpleLink CC323x SDK为例看看如何将上述原理落地。CC323x的驱动库对硬件寄存器进行了封装提供了更易用的API但理解其背后的寄存器操作对于调试和优化至关重要。3.1 基础初始化与主模式轮询传输首先无论哪种模式基础的模块初始化三部曲是必不可少的使能外设时钟任何外设要工作必须先给它“供电”时钟。PRCMPeripheralClkEnable(PRCM_GSPI, PRCM_RUN_MODE_CLK);配置引脚复用将芯片内部的SPI信号映射到具体的物理引脚上。// 例如将SPI主设备的CLK、MOSI、MISO、CS分别映射到指定引脚 PinTypeSPI(PIN_01, PIN_MODE_7); // CLK PinTypeSPI(PIN_02, PIN_MODE_7); // MOSI PinTypeSPI(PIN_03, PIN_MODE_7); // MISO PinTypeSPI(PIN_04, PIN_MODE_7); // CS (软件控制时)软件复位模块确保SPI控制器从一个确定的初始状态开始。SPIReset(GSPI_BASE);接下来我们配置一个主设备以100kHz的速率采用轮询方式进行通信。这是最基础的用法适合简单的、小批量的数据交换。// 1. 配置SPI参数 SPIConfigSetExpClk(GSPI_BASE, PRCMPeripheralClockGet(PRCM_GSPI), // 获取SPI模块时钟频率 100000, // 比特率100 kHz SPI_MODE_MASTER, // 主模式 SPI_SUB_MODE_0, // 时钟极性(CPOL)0, 相位(CPHA)0 (SPI_SW_CTRL_CS | SPI_4PIN_MODE | SPI_TURBO_OFF | SPI_CS_ACTIVEHIGH | SPI_WL_8)); // 软件控制CS、4线模式、关闭Turbo、CS高有效、字长8位 // 2. 使能SPI通道 SPIEnable(GSPI_BASE); // 3. 使能片选拉低CS线选中从设备 SPICSEnable(GSPI_BASE); // 4. 轮询方式发送一个字节数据 uint8_t txData 0x55; uint8_t rxData; SPIDataPut(GSPI_BASE, txData); // 数据放入发送寄存器/FIFO // 5. 轮询等待并读取接收到的数据 // 注意SPIDataGet是阻塞函数会等待一次传输完成 SPIDataGet(GSPI_BASE, rxData); // 6. 禁用片选拉高CS线结束本次通信 SPICSDisable(GSPI_BASE);关键点解析SPI_SUB_MODE_0对应CPOL0, CPHA0。这是最常用的SPI时钟模式数据在时钟的第一个边沿上升沿采样。务必与从设备的模式匹配。SPI_SW_CTRL_CS意味着片选信号由软件通过SPICSEnable/Disable控制。你也可以配置为硬件自动控制。SPIDataPut和SPIDataGet在轮询模式下是阻塞的。SPIDataPut会等待发送寄存器可用SPIDataGet会等待接收寄存器数据有效。对于连续传输你需要循环调用它们。3.2 从模式中断驱动传输实战从设备的中断模式配置更为典型因为它需要随时响应主设备的召唤。以下是配置一个SPI从设备并启用TX_EMPTY和RX_FULL中断的步骤// 1. 全局中断使能通常在主函数初始化时完成 IntMasterEnable(); // 2. 配置SPI为从模式 SPIConfigSetExpClk(GSPI_BASE, PRCMPeripheralClockGet(PRCM_GSPI), SPI_IF_BIT_RATE, // 从设备速率由主设备时钟决定 SPI_MODE_SLAVE, // 从模式 SPI_SUB_MODE_0, (SPI_HW_CTRL_CS | // 硬件控制CS由主设备驱动 SPI_4PIN_MODE | SPI_TURBO_OFF | SPI_CS_ACTIVEHIGH | SPI_WL_8)); // 3. 注册中断服务函数 SPIIntRegister(GSPI_BASE, SlaveSPIIntHandler); // 4. 使能特定的中断源发送空和接收满 SPIIntEnable(GSPI_BASE, SPI_INT_TX_EMPTY | SPI_INT_RX_FULL); // 5. 使能SPI通道 SPIEnable(GSPI_BASE);中断服务函数ISR是中断处理的核心其编写有严格的规范void SlaveSPIIntHandler(void) { uint32_t uiStatus; uint32_t uiRxData; // 第一步读取中断状态寄存器明确是哪个事件触发了中断 uiStatus SPIIntStatus(GSPI_BASE, true); // true表示读取原始中断状态 // 第二步清除已处理的中断标志位这是防止中断重入的关键。 SPIIntClear(GSPI_BASE, uiStatus); // 清除我们读到的所有状态位 // 第三步根据状态位执行相应操作 if(uiStatus SPI_INT_TX_EMPTY) { // 发送寄存器/FIFO空了需要填充新数据 // 这里应该从一个发送缓冲区中获取下一个要发送的数据 uint8_t nextTxByte GetNextTxByteFromBuffer(); SPIDataPutNonBlocking(GSPI_BASE, nextTxByte); // 非阻塞写入 } if(uiStatus SPI_INT_RX_FULL) { // 接收寄存器/FIFO满了需要读取数据 // 使用非阻塞读取避免在FIFO为空时阻塞 while(SPIDataGetNonBlocking(GSPI_BASE, uiRxData)) { // 成功读取到一个数据存入接收缓冲区 StoreRxDataToBuffer((uint8_t)uiRxData); } // 注意在FIFO模式下可能需要循环读取直到FIFO为空或低于AFL阈值 } // 注意TX_UNDERFLOW和RX_OVERFLOW中断如果使能了也需要判断和清除状态位 // 但通常不需要进行数据读写操作仅作错误标志处理或日志记录。 if(uiStatus SPI_INT_TX_UNDERFLOW) { // 处理发送下溢错误可能是从设备响应不及时 HandleTxUnderflowError(); } if(uiStatus SPI_INT_RX_OVERFLOW) { // 处理接收溢出错误数据已丢失需检查接收速率是否过快 HandleRxOverflowError(); } }中断服务程序编写要点快进快出ISR中不要做复杂运算、不要调用可能阻塞的函数如某些printf。尽快处理硬件交互设置标志位让主循环或其他任务去做后续处理。状态读取与清除先读SPIIntStatus再立刻用该值清除SPIIntClear。顺序很重要且清除的是你读到的状态位避免误清除其他未处理的中断。非阻塞操作在ISR中优先使用SPIDataPutNonBlocking和SPIDataGetNonBlocking。它们会立即返回成功或失败状态避免在FIFO已满/空时阻塞在ISR中。FIFO批量处理在RX_FULL中断中不要只读一个数据。应该用while循环配合非阻塞读尽可能多地把FIFO中的数据读空直到读不到为止。这符合FIFO的设计初衷也减少了中断次数。3.3 FIFO与DMA的联合配置对于大数据量传输中断配合FIFO仍可能给CPU带来负担。此时DMA直接内存访问是终极解决方案。DMA控制器可以在不打扰CPU的情况下自动在内存和SPI FIFO之间搬运数据。CC323x的SPI模块支持独立的DMA读写请求线。配置DMA的基本思路禁用相关中断当使用DMA处理数据传输时应禁用TX_EMPTY和RX_FULL等数据中断避免冲突。SPI_IRQENABLE寄存器中对应的使能位清0。配置FIFO阈值在SPI_XFERLEVEL寄存器中设置AEL和AFL。这两个值直接决定了DMA请求触发的时机。例如设置AEL7几乎空等级为8字节当发送FIFO剩余空间8字节时会触发DMA写请求设置AFL15几乎满等级为16字节当接收FIFO数据量16字节时触发DMA读请求。使能DMA请求在SPI_CHCONF寄存器中设置DMARW1使能DMA写请求DMAR1使能DMA读请求。配置DMA控制器这通常在SDK的DMA驱动中完成。你需要为DMA通道设置源地址对于发送是内存对于接收是SPI_RX寄存器、目的地址对于发送是SPI_TX寄存器对于接收是内存、传输数据量、传输宽度与SPI字长匹配等。并链接好DMA完成中断以便在整块数据传输完毕后通知CPU。启动传输使能SPI通道并启动DMA传输。之后SPI和DMA会协同工作直到设定的数据量传输完成通过DMA中断通知CPU。一个常见的DMA发送配置伪代码逻辑// 假设使用SDK的UDMA驱动 // 1. 配置SPI FIFO和DMA使能 HWREG(GSPI_BASE SPI_O_XFERLEVEL) (15 8) | (7 0); // AFL15, AEL7 HWREG(GSPI_BASE SPI_O_CHCONF) | (1 15) | (1 14); // 使能DMAR和DMARW // 2. 配置DMA通道以发送为例 // 设置DMA通道属性源地址递增内存目的地址固定SPI_TX数据大小等 uDMAChannelTransferSet(UDMA_CHANNEL_SPI_TX, ...); // 3. 使能DMA通道和SPI uDMAChannelEnable(UDMA_CHANNEL_SPI_TX); SPIEnable(GSPI_BASE); // 4. 在DMA传输完成中断中进行后续处理如关闭通道、通知任务等4. 寄存器级调试与常见问题排查实录即使使用了高级的驱动库在遇到棘手的通信问题时深入寄存器层面进行调试仍然是工程师的必备技能。CC323x的SPI寄存器映射清晰通过读取这些寄存器可以精准定位问题所在。4.1 关键状态寄存器解读SPI_IRQSTATUS (偏移 0x118)这是诊断中断问题的第一站。当通信异常或中断不触发时首先读取这个寄存器。它能告诉你TX_EMPTY、RX_FULL、TX_UNDERFLOW、RX_OVERFLOW、EOW等事件是否真实发生。如果这里显示有状态位被置1但CPU没进入中断那问题可能出在中断使能SPI_IRQENABLE或系统级的NVIC中断配置上。SPI_CHSTAT (偏移 0x130)这个寄存器反映了FIFO和传输的实时状态。TXFFE/TXFFF发送FIFO空/满状态。如果你发现数据发不出去检查TXFFF是否一直为1满这可能意味着数据没有成功写入FIFO或者SPI时钟根本没启动。RXFFE/RXFFF接收FIFO空/满状态。如果收不到数据但RXFFE为0非空说明数据可能已经收到FIFO里了只是你的程序没去读。EOT传输结束标志。在从设备模式下这个标志可以帮助判断一次SPI传输是否完成。SPI_SYSSTATUS (偏移 0x114)重点关注RESETDONE位。在进行软件复位SPI_SYSCONFIG[SOFTRESET]或模块初始化后必须轮询此位直到它变为1才能进行后续配置。忽略这一步是很多初始化失败的原因。4.2 典型问题排查清单以下是我在多年调试中总结的一些常见问题及其排查思路整理成表格方便速查问题现象可能原因排查步骤与解决方法完全无通信时钟线无波形1. SPI模块时钟未使能。2. 引脚复用配置错误。3. 主/从模式配置错误。4. 通道未使能SPI_CHCTRL[EN]0。1. 确认PRCMPeripheralClkEnable已调用。2. 用万用表或示波器检查物理引脚或用SDK的引脚配置工具复查。3. 检查SPI_MODULCTRL[MS]位主/从选择。4. 检查SPI_CHCTRL[EN]寄存器是否为1。能发送但接收不到数据或数据全为0/FF1. 从设备未正确响应。2. 时钟极性(CPOL)和相位(CPHA)不匹配。3. 接收中断未使能或中断服务程序未读数据。4. 接收FIFO溢出RX_OVERFLOW。1. 检查从设备电源、片选、接线。2.这是最常见原因用示波器同时抓取CLK和MISO/MOSI对照从设备手册确认采样边沿是否正确。调整SPI_CHCONF[POL]和[PHA]。3. 检查SPI_IRQENABLE[RX_FULL_ENABLE]并在ISR中确认调用了SPIDataGet。4. 读取SPI_IRQSTATUS检查RX_OVERFLOW位。如果发生检查接收处理速度是否跟不上发送速度考虑增大FIFO或使用DMA。中断频繁触发甚至卡死1. 中断状态位未清除。2. FIFO阈值AEL/AFL设置过小。3. 在ISR中进行了阻塞操作。1.绝对重点确保在ISR中读取状态后立即向SPI_IRQSTATUS对应位写1清除。2. 适当增大AFL接收和减小AEL发送的值降低中断频率。3. 检查ISR代码确保只进行必要的、非阻塞的寄存器操作。DMA传输不启动或数据错误1. DMA请求未使能SPI_CHCONF[DMAR]/[DMARW]。2. FIFO阈值设置不当DMA请求未触发。3. DMA通道源/目的地址、数据量配置错误。4. DMA与SPI中断冲突。1. 确认SPI_CHCONF寄存器中DMA使能位已置1。2. 确认SPI_XFERLEVEL中AEL/AFL值已根据DMA突发传输大小合理设置。3. 使用调试器检查DMA控制器的配置寄存器确保地址、传输大小正确且传输宽度8/16/32位与SPI字长匹配。4. 确保在启用DMA请求后已禁用对应的SPI数据中断TX_EMPTY, RX_FULL。从设备模式下数据发送不及时触发TX_UNDERFLOW1. 主设备时钟已开始但从设备TX FIFO/寄存器未提前准备好数据。2. 中断响应太慢来不及填充数据。1. 在从设备使能通道后、主设备发起传输前就预先向发送寄存器写入第一个数据。2. 优化ISR性能或者考虑使用更大的发送FIFO并设置合适的AEL让CPU/DMA有更充裕的时间准备数据。切换主从模式后功能异常模块状态未完全复位。手册明确警告在切换主从模式前必须对SPI模块进行软件复位SPI_SYSCONFIG[SOFTRESET]并等待SPI_SYSSTATUS[RESETDONE]置位。4.3 调试技巧与心得示波器/逻辑分析仪是你的最佳搭档软件层面的调试总有局限。当通信异常时第一时间用示波器查看SPICLK、MOSI、MISO、CS四条线的实际波形。检查时钟频率是否正确、数据是否对齐、CS信号是否符合预期。逻辑分析仪配合SPI解码功能能直观地看到每个字节的传输值极大提升调试效率。从最简单配置开始遇到复杂问题先剥离所有高级功能。关闭FIFO、关闭中断、关闭DMA用最基本的轮询模式、8位数据、低速时钟进行测试。确保底层通信是通的再逐一叠加功能FIFO-中断-DMA这样能快速定位问题出现在哪个环节。善用SDK的示例代码TI的SDK通常提供了丰富的示例spi_master_example,spi_slave_example,spi_dma_example。这些代码是经过验证的起点。在你自己编写驱动前先让示例代码跑起来然后用你的硬件和配置去逐步修改它比从头写要稳妥得多。注意字长与数据对齐CC323x的SPI数据寄存器是32位的但数据总是右对齐在低位上。当你使用16位或8位字长时写入SPI_TX和从SPI_RX读取的数据都要注意高低位。确保你的数据缓冲区、DMA配置的传输宽度与SPI_CHCONF[WL]设置一致。关于Turbo模式SPI_CHCONF[TURBO]位用于在多字连续传输时最大化吞吐量。在单次字传输时建议关闭。启用Turbo模式时需要仔细阅读手册关于CS信号行为的描述确保从设备能兼容这种时序。SPI接口的深度优化就是在可靠性、实时性和吞吐量之间寻找最佳平衡点。理解每一个状态位、每一个配置寄存器背后的含义才能在设计时做出合理选择在调试时快速定位根因。从轮询到中断再到FIFO与DMA技术的层层递进背后是对系统资源更精细的掌控。希望这些从实际项目中沉淀下来的细节和“坑点”能让你在下一场与SPI的“对话”中更加游刃有余。