SPI从机驱动开发:中断与DMA模式详解及Kinetis SDK实践
1. SPI从机驱动开发:中断与DMA模式详解及Kinetis SDK实践
在嵌入式系统开发中,SPI(Serial Peripheral Interface)通信是连接传感器、存储器、显示屏等外设的基石。作为从机设备,如何高效、可靠地响应主机的数据请求,同时又不拖累主CPU的性能,是每个嵌入式工程师都会面临的挑战。尤其是在处理实时数据流或大数据块传输时,简单的轮询方式往往力不从心。这时,中断和DMA(直接内存访问)这两种异步处理机制就成了提升系统效率的关键。飞思卡尔(现恩智浦)的Kinetis SDK为SPI从机驱动提供了这两种模式的完整实现,但官方API手册更像是一本字典,告诉你每个函数“是什么”,却很少解释“为什么”要这么用,以及在实际项目中“怎么用”才能避开那些坑。今天,我就结合自己多年在Kinetis平台上的踩坑经验,为你彻底拆解SPI从机驱动中中断与DMA模式的原理、SDK的实现细节,以及那些手册里不会写的实战技巧。
2. 核心机制解析:中断与DMA的本质区别与选型考量
在深入代码之前,我们必须先搞清楚中断和DMA到底在解决什么问题。很多人知道DMA更快,但为什么快?快在哪儿?什么情况下该用中断,什么情况下必须上DMA?理解这些,你才能做出正确的技术选型,而不是盲目套用。
2.1 中断驱动模式:事件驱动的精准响应
中断模式的本质是事件通知。当SPI从机的接收缓冲区(RX FIFO)有数据到达,或者发送缓冲区(TX FIFO)为空需要填充新数据时,SPI外设会触发一个硬件中断信号。CPU接收到这个信号后,会暂停当前正在执行的任务(保存现场),跳转到预先设置好的中断服务程序(ISR)中,由ISR来完成数据的读取或写入操作,操作完成后CPU再恢复原来的任务。
它的工作流程可以类比为一个高效的餐厅服务员(CPU):服务员原本在收拾桌子(执行主程序任务),这时后厨铃响了(SPI中断触发),告诉他有一道菜好了(数据接收完成)或者需要备料(发送缓冲区空)。服务员立刻停下手中的活(保存上下文),跑去后厨处理这个紧急事件(执行ISR),处理完后再回来继续收拾桌子(恢复上下文)。这个过程确保了事件能得到及时响应。
在Kinetis SDK中,中断模式的核心结构体是spi_slave_state_t。这个结构体就像是服务员的“待办事项清单”,记录了当前传输的状态(status)、缓冲区指针(sendBuffer,receiveBuffer)、还剩多少字节要处理(remainingSendByteCount,remainingReceiveByteCount)等关键信息。ISR就根据这个清单来决定当前是该送菜还是该接菜。
中断模式的优势在于实时性高、响应延迟确定。因为ISR的优先级可以设置,高优先级的SPI通信可以打断低优先级任务,确保关键数据不被延误。但它也有明显的缺点:每传输一个或几个字节(取决于FIFO深度)就要打断CPU一次。如果SPI时钟频率很高,或者单次传输数据量很大,CPU就会频繁地在主程序和ISR之间切换,产生大量的上下文保存与恢复开销,导致整体系统效率下降,其他任务的实时性也会受到影响。
2.2 DMA驱动模式:解放CPU的“自动驾驶”
DMA模式的本质是硬件代理。你可以把它想象成在CPU和SPI外设之间安排了一个专门的“搬运工”(DMA控制器)。在传输开始前,CPU只需要告诉这个搬运工三件事:源地址在哪里(内存或外设)、目的地址在哪里(外设或内存)、要搬多少东西(传输字节数)。然后CPU就可以去处理其他任务了,搬运工会自动在SPI外设和内存之间搬运数据,完全不需要CPU介入。只有当一整批数据全部搬完,DMA控制器才会产生一个完成中断,通知CPU“活儿干完了”。
Kinetis SDK的DMA模式驱动结构体是spi_dma_slave_state_t。它继承了中断模式状态结构体的基本字段,并增加了两个关键成员:dmaReceive和dmaTransmit,分别对应接收和发送所使用的DMA通道句柄。这揭示了DMA模式的一个底层细节:接收和发送通常是两个独立的DMA通道在并行工作,从而实现SPI全双工通信的真正硬件并发。
DMA模式最大的优势就是极大地解放了CPU。在长达数KB的数据传输过程中,CPU可以完全专注于应用层逻辑计算,系统吞吐量得到质的提升。但它并非没有代价:DMA传输的延迟通常比中断大。因为DMA请求的仲裁、通道的启动都需要时间,对于极少量数据(比如几个字节)的零星传输,DMA的配置开销可能比直接中断处理还要大。此外,DMA需要占用额外的系统总线带宽,在总线繁忙时可能引发竞争。
2.3 实战选型指南:何时用中断,何时用DMA?
根据上面的分析,我们可以得出一个清晰的选型矩阵:
| 传输场景 | 推荐模式 | 理由与注意事项 |
|---|---|---|
| 低频、小数据包(< 32字节) | 中断模式 | 配置简单,响应及时,避免DMA通道配置和启动的开销。例如,读取一个温度传感器的一次16位读数。 |
| 高频、大数据块(> 100字节)或连续流 | DMA模式 | 能显著降低CPU占用率,提升系统整体性能。例如,向LCD屏刷新一帧图像数据,或从SPI Flash连续读取数据。 |
| 实时性要求极高,延迟必须确定 | 中断模式(配合高优先级) | DMA传输的启动和完成中断响应时间存在一定波动,而高优先级中断的延迟是可控的。 |
| 系统总线负载重,有多个DMA设备 | 需谨慎评估 | 多个DMA设备可能竞争总线带宽,反而导致性能下降。需要合理分配DMA通道优先级和仲裁策略。 |
| 代码复杂度与资源考量 | 中断模式更简单 | DMA模式需要额外初始化DMA控制器,管理通道资源,代码量和复杂度更高。 |
一个关键提示:Kinetis SDK的API参考手册中明确提到,不建议在同一个运行时应用中混合使用中断驱动和DMA驱动的SPI驱动。这是因为SPI的中断向量(IRQHandler)只能指向一个处理函数。如果你在一个工程里既用了
SPI_DRV_SlaveInit(中断),又用了SPI_DRV_DmaSlaveInit(DMA),中断向量该指向SPI_DRV_SlaveIRQHandler还是SPI_DRV_DmaSlaveIRQHandler呢?这会导致冲突。正确的做法是,为不同的SPI实例(Instance)分配不同的模式,或者在整个应用生命周期内只使用一种模式。
3. Kinetis SDK SPI从机驱动深度剖析
理解了原理,我们再来啃SDK的代码。手册里给出了函数原型和结构体定义,但把这些碎片拼成一幅能工作的图景,需要理解其内在的设计逻辑。
3.1 驱动状态管理:spi_slave_state_t与spi_dma_slave_state_t
这两个结构体是驱动运行的核心,它们记录了传输的上下文。以spi_slave_state_t为例,几个关键字段的协同工作方式如下:
isTransferInProgress (volatile bool): 这是一个“传输进行中”的标志位。volatile关键字至关重要,它告诉编译器这个变量可能被ISR(中断上下文)和主程序(线程上下文)同时修改,禁止对其进行激进的优化(如缓存到寄存器),确保双方都能读到最新的值。当调用SPI_DRV_SlaveTransfer启动非阻塞传输时,驱动会将其设为true;在ISR中完成所有字节传输后,会将其设为false。remainingSendByteCount和remainingReceiveByteCount (volatile int32_t): 这两个计数器在���输开始时被设置为总字节数。在ISR中,每成功发送或接收一个字节,对应的计数器就减1。它们同样被声明为volatile。主程序可以通过查询这些值(结合状态函数)来了解传输进度。sendBuffer和receiveBuffer (const uint8_t*, uint8_t*): 指向用户数据缓冲区的指针。这里有一个非常重要的设计:sendBuffer是const指针,意味着驱动承诺不会修改你的发送数据源;而receiveBuffer是非const的,因为驱动要向里面写入数据。如果只想发送,可以将receiveBuffer设为NULL;如果只想接收,可以将sendBuffer设为NULL。驱动内部会检查这些指针并做出相应处理。dummyPattern (uint32_t): 当sendBuffer为NULL(即纯接收模式)时,SPI从机在MOSI线上需要输出什么数据来维持时钟?答案就是dummyPattern(哑元模式)。通常设置为0x00或0xFF。这个值需要在初始化时的用户配置结构体spi_slave_user_config_t中指定。
spi_dma_slave_state_t在继承上述字段的基础上,增加了DMA通道句柄。DMA驱动的状态机更为复杂,因为它涉及DMA控制器和SPI外设两个硬件模块的协同。其isTransferInProgress标志位通常由DMA传输完成中断来清除。
3.2 用户配置结构:时钟相位与极性的“四模式”
无论是中断还是DMA驱动,初始化时都需要传入一个用户配置结构体(spi_slave_user_config_t或spi_dma_slave_user_config_t)。其中最关键的两个参数是polarity(时钟极性CPOL)和phase(时钟相位CPHA)。它们共同定义了SPI的四种通信模式,必须与SPI主机严格匹配,否则数据采样会错位,导致通信完全失败。
- CPOL (Clock Polarity):
kSpiClockPolarity_ActiveHigh(CPOL=0): 时钟空闲时为低电平。kSpiClockPolarity_ActiveLow(CPOL=1): 时钟空闲时为高电平。
- CPHA (Clock Phase):
kSpiClockPhase_FirstEdge(CPHA=0): 数据在时钟的第一个边沿(上升沿或下降沿)被采样。kSpiClockPhase_SecondEdge(CPHA=1): 数据在时钟的第二个边沿被采样。
常见的模式组合如下:
- 模式0 (CPOL=0, CPHA=0): 空闲低电平,数据在上升沿采样。这是最常用的模式。
- 模式1 (CPOL=0, CPHA=1): 空闲低电平,数据在下降沿采样。
- 模式2 (CPOL=1, CPHA=0): 空闲高电平,数据在下降沿采样。
- 模式3 (CPOL=1, CPHA=1): 空闲高电平,数据在上升沿采样。
实操心得:很多SPI设备(如Flash芯片、传感器)的datasheet会明确要求使用哪种模式。如果你手头没有示波器或逻辑分析仪,调试SPI通信的第一步就是确认主从双方的模式是否一致。一个快速验证的方法是:如果通信完全无反应,首先检查片选(CS)和时钟(SCK)线是否有波形;如果有时钟但数据不对,十有八九是CPOL和CPHA设错了。
3.3 初始化的完整流程与陷阱规避
手册里给的初始化代码示例是骨架,在实际项目中,我们需要用肌肉和神经把它填充起来。下面是一个更健壮、带错误处理的DMA模式从机初始化示例:
#include "fsl_spi_dma.h" #include "fsl_dma_manager.h" // 使用DMA管理器可以简化通道分配 spi_status_t SPI_Slave_DMA_Init(uint32_t instance) { spi_status_t spiStatus = kStatus_SPI_Error; dma_channel_config_t dmaRxConfig, dmaTxConfig; spi_dma_slave_state_t spiDmaSlaveState; spi_dma_slave_user_config_t slaveDmaUserConfig; /* 1. 配置SPI从机参数 */ slaveDmaUserConfig.polarity = kSpiClockPolarity_ActiveHigh; // CPOL=0 slaveDmaUserConfig.phase = kSpiClockPhase_FirstEdge; // CPHA=0 slaveDmaUserConfig.direction = kSpiMsbFirst; // 高位先行 slaveDmaUserConfig.dummyPattern = 0xFF; // 纯接收时发送0xFF #if FSL_FEATURE_SPI_16BIT_TRANSFERS slaveDmaUserConfig.bitCount = kSpi8BitMode; // 使用8位传输模式 #endif /* 2. 初始化DMA控制器(全局一次即可)*/ if (DMA_DRV_Init(&dmaState) != kStatus_DMA_Success) { // 打印错误日志 return kStatus_SPI_Error; } /* 3. 为SPI接收和发送分配DMA通道 */ // 通常接收通道请求高于发送,因为数据不及时读取可能丢失 if (DMA_DRV_RequestChannel(&dmaRxConfig, kDmaRequestMux0SPI1_Rx) != kStatus_DMA_Success) { // 处理通道申请失败 return kStatus_SPI_Error; } if (DMA_DRV_RequestChannel(&dmaTxConfig, kDmaRequestMux0SPI1_Tx) != kStatus_DMA_Success) { DMA_DRV_ReleaseChannel(dmaRxConfig.channelNumber); return kStatus_SPI_Error; } /* 4. 初始化SPI DMA从机驱动 */ spiStatus = SPI_DRV_DmaSlaveInit(instance, &spiDmaSlaveState, &slaveDmaUserConfig); if (spiStatus != kStatus_SPI_Success) { DMA_DRV_ReleaseChannel(dmaRxConfig.channelNumber); DMA_DRV_ReleaseChannel(dmaTxConfig.channelNumber); // 打印SPI初始化失败信息 } // 保存通道配置到全局变量,供后续传输函数使用 g_spiDmaRxChannel = dmaRxConfig.channelNumber; g_spiDmaTxChannel = dmaTxConfig.channelNumber; return spiStatus; }这里有几个容易踩坑的地方:
- DMA通道资源管理:DMA通道是系统稀缺资源。上述代码中,如果SPI初始化失败,必须释放已经申请到的DMA通道,否则会造成资源泄漏。在项目后期,可能会出现“某个功能突然不工作”的灵异现象,排查起来非常困难。
FSL_FEATURE_SPI_HAS_DMA_SUPPORT宏:这个宏定义在芯片特定的头文件里。在包含fsl_spi_dma.h之前,必须先包含你的芯片型号对应的头文件(如MKL25Z4.h),否则这个宏可能未定义,导致DMA相关的代码被错误地编译或忽略。- 中断向量重定向:如手册所述,DMA驱动有自己的中断处理函数
SPI_DRV_DmaSlaveIRQHandler。你需要在启动文件或中断管理器中,确保SPI对应的中断向量指向这个函数,而不是默认的SPI_DRV_SlaveIRQHandler。通常,SDK的fsl_spi_dma_irq.c文件已经提供了这个函数,你只需要确保该文件被加入工程编译即可。
4. 阻塞与非阻塞传输的实战应用
SDK提供了阻塞(Blocking)和非阻塞(Non-blocking)两种传输接口。选择哪一种,取决于你的系统架构和实时性要求。
4.1 阻塞传输:简单直接的“等结果”
阻塞函数,例如SPI_DRV_SlaveTransferBlocking,它的行为非常直观:函数被调用后,程序会“卡”在这个函数里,直到传输完成或超时,函数才会返回。在此期间,CPU无法执行其他任务。
#define SPI_SLAVE_INSTANCE 1 #define TRANSFER_SIZE 256 #define TIMEOUT_MS 1000 uint8_t txBuffer[TRANSFER_SIZE]; uint8_t rxBuffer[TRANSFER_SIZE]; spi_status_t result; // 填充要发送的数据 // ... result = SPI_DRV_SlaveTransferBlocking(SPI_SLAVE_INSTANCE, txBuffer, rxBuffer, TRANSFER_SIZE, TIMEOUT_MS); if (result == kStatus_SPI_Success) { // 处理接收到的数据 rxBuffer } else if (result == kStatus_SPI_Timeout) { // 处理超时,可能是主机未启动传输或通信故障 } else { // 处理其他错误 (kStatus_SPI_Error, kStatus_SPI_Busy) }阻塞调用的适用场景:
- 简单的单任务系统:没有复杂的多任务调度,CPU可以“专心”等待SPI传输。
- 初始化阶段的配置:例如在启动时从SPI EEPROM读取配置参数。
- 对实时性要求不高的后台任务。
它的致命缺点就是会“阻塞”整个线程。在RTOS(实时操作系统)环境中,如果一个高优先级任务长时间阻塞在一个SPI传输上,会导致低优先级任务完全得不到执行,甚至看门狗超时复位。
4.2 非阻塞传输:异步处理的“协作式”
非阻塞函数,如SPI_DRV_SlaveTransfer,是更高级、更常用的方式。调用它只会启动传输,然后函数立即返回。传输过程在后台由中断或DMA进行。你需要通过其他方式(如查询状态、等待信号量)来获知传输是否完成。
static volatile bool g_spiTransferDone = false; static event_t g_spiTransferEvent; // 在某个初始化函数中创建事件 OSA_EventCreate(&g_spiTransferEvent, kEventAutoClear); // 传输启动函数 void Start_SPI_Transfer(void) { spi_status_t result; result = SPI_DRV_SlaveTransfer(SPI_SLAVE_INSTANCE, txBuffer, rxBuffer, TRANSFER_SIZE); if (result != kStatus_SPI_Success) { // 处理启动失败 return; } // 传输已启动,函数立即返回,CPU可以去做别的事了 } // 等待传输完成的函数(可以在另一个任务中调用) void Wait_For_SPI_Transfer(void) { // 方式1:简单查询(浪费CPU周期,不推荐在RTOS中长时间使用) // while(SPI_DRV_SlaveGetTransferStatus(SPI_SLAVE_INSTANCE, NULL) == kStatus_SPI_Busy); // 方式2:基于事件等待(高效,释放CPU) osa_status_t osaStatus; osaStatus = OSA_EventWait(&g_spiTransferEvent, kEventFlags, false, osaWaitForever_c); if (osaStatus == kStatus_OSA_Success) { // 传输完成事件已收到 Process_Received_Data(); } } // --- 在SPI的传输完成中断服务程序(ISR)中 --- void SPI1_IRQHandler(void) { SPI_DRV_SlaveIRQHandler(SPI_SLAVE_INSTANCE); // 调用SDK的IRQ Handler // SDK的Handler处理完数据后,会检查传输是否完成 // 如果完成,我们需要设置完成标志或发送事件 uint32_t framesTransferred; if (SPI_DRV_SlaveGetTransferStatus(SPI_SLAVE_INSTANCE, &framesTransferred) == kStatus_SPI_Success) { g_spiTransferDone = true; // 设置全局标志 // 或者更好的是,发送一个RTOS事件或信号量 OSA_EventSet(&g_spiTransferEvent, kEventFlags); } }非阻塞调用的核心优势在于“异步”。它完美契合了RTOS或事件驱动型应用。主任务启动传输后,可以调用OSA_TimeDelay让出CPU,或者去处理其他就绪的任务。当传输在后台完成时,通过中断触发一个事件,唤醒等待该事件的任务来处理数据。这样,系统的响应性和吞吐量都得到了保障。
重要注意事项:手册示例中的
while(kStatus_SPI_Success != SPI_DRV_SlaveGetTransferStatus(...));是一种忙等待(Busy Waiting),在裸机程序中勉强可用,但在RTOS中这是绝对要避免的!它会独占CPU,使任务调度器无法工作。正确的做法是使用事件(Event)、信号量(Semaphore)或消息队列(Message Queue)进行任务同步。
5. 高级话题:DMA传输的“最后一字节”问题与IRQ Handler
如果你仔细阅读Kinetis SDK手册中关于SPI_DRV_DmaSlaveIRQHandler的描述,会发现一句关键的话:“This handler is used when the hasExtraByte flag is set to retrieve the received last byte.” 这指向了SPI DMA传输中一个经典的硬件边界情况。
5.1 问题根源:SPI时钟与DMA请求的时序
在典型的SPI DMA接收场景中,DMA控制器通常被配置为:每当SPI接收数据寄存器(SPI_D)中有新数据(即SPI_RX_FULL标志置位)时,就产生一个DMA请求,将数据从SPI_D搬移到内存。
假设要接收N个字节。理想情况下,第N个字节被SPI接收完成,触发最后一次DMA请求,DMA搬运完这第N个字节后,计数器归零,触发DMA传输完成中断。一切完美。
但在某些芯片的SPI模块实现中,可能存在这样的时序:第N个字节的最后一个时钟边沿(完成接收)与DMA请求的产生之间,存在一个极小的延迟。或者,DMA通道在搬运完第N个字节后,需要几个时钟周期来更新其内部状态并产生完成中断。
就在这个微小的窗口期内,如果SPI主机紧接着又开始了下一次传输(发出了第N+1个时钟脉冲),SPI模块会继续工作,将新数据(第N+1个字节)放入接收寄存器。然而,DMA的传输计数器已经为0,它不会再响应这个新的DMA请求。这个“多余”的字节就留在了SPI_D寄存器中,没有被DMA搬走。如果不处理,它要么会被下一次传输覆盖丢失,要么会触发SPI溢出错误。
5.2 SDK的解决方案:hasExtraByte标志与专用IRQ Handler
Kinetis SDK的DMA驱动通过spi_dma_slave_state_t结构体中的hasExtraByte标志来处理这个问题。其逻辑大致如下:
- 在DMA传输配置阶段,驱动可能会预先计算并设置
hasExtraByte = true,或者在某些硬件条件下自动判定。 - DMA传输完成中断触发后,驱动的标准处理流程结束。
- 但是,如果
hasExtraByte标志为真,驱动会知道“可能还有一个字节残留在SPI FIFO或寄存器中”。 - 此时,
SPI_DRV_DmaSlaveIRQHandler这个函数就会被调用(通常由DMA完成中断服务程序在最后调用)。它的任务就是手动检查SPI接收状态寄存器,如果发现还有数据,就将其读取出来,放到接收缓冲区的末尾。
这就是为什么DMA驱动有自己独立的IRQ Handler。对于纯中断驱动,每个字节的接收都会触发中断,不存在“漏字节”的问题,所以它的SPI_DRV_SlaveIRQHandler逻辑相对单纯。
给你的实践建议是:在使用SPI DMA从机接收时,务必确保你的工程包含了fsl_spi_dma_irq.c文件,并且SPI的中断向量正确指向了DMA版本的Handler。不要尝试自己去实现这个逻辑,因为其中涉及对硬件状态非常精细的判断,SDK已经为你处理好了这些边界情况。
6. 调试技巧与常见问题排查实录
理论再完美,代码一跑就崩。下面是我在调试SPI从机驱动时积累的一些“血泪”经验,希望能帮你快速定位问题。
6.1 通信完全无反应(没波形)
这是最让人头疼的情况,软件似乎运行了,但用逻辑分析仪在SCK、MOSI、MISO线上根本看不到任何信号。
- 检查清单:
- 引脚复用配置:这是新手第一坑!Kinetis芯片的引脚功能是可复用的。你不仅要用
SPI_DRV_SlaveInit初始化SPI模块,还必须通过PORT模块的寄存器,将对应的SCK、MOSI、MISO、CS引脚功能配置为SPI模式。例如,对于SPI1的SCK引脚(假设是PTE1),你需要:PORT_SetPinMux(PORTE, 1U, kPORT_MuxAlt2);(具体Alt值查芯片参考手册)。 - 时钟门控:SDK的初始化函数会打开SPI模块的时钟,但确保整个芯片的SPI模块时钟源(例如Bus Clock)是使能的。
- 从机选择(CS):确认主机的片选信号已经正确连接到从机,并且电平有效(通常是低电平选中)。有些SPI驱动需要CS信号来触发传输。
- 主从关系:再次确认你的代码初始化的是从机(Slave)驱动,而不是主机(Master)。从机是不会主动产生时钟的,必须等待主机发起传输。
- 引脚复用配置:这是新手第一坑!Kinetis芯片的引脚功能是可复用的。你不仅要用
6.2 有时钟,但数据不对或全是0xFF/0x00
逻辑分析仪能看到SCK在跳动,但MOSI/MISO上的数据乱七八糟,或者始终是高电平(0xFF)或低电平(0x00)。
- 排查步骤:
- CPOL/CPHA模式:这是最常见的原因!用逻辑分析仪抓取一个完整的字节传输波形。观察SCK空闲时的电平(CPOL),以及数据是在SCK的哪个边沿变化的,哪个边沿稳定的(CPHA)。与你的代码配置进行比对。模式不匹配,数据位会整体错位。
- 数据位序(MSB/LSB):检查
direction配置是kSpiMsbFirst还是kSpiLsbFirst,是否与主机匹配。 - 缓冲区指针:检查你的
sendBuffer和receiveBuffer指针是否有效。如果sendBuffer是NULL,从机会一直发送dummyPattern(默认0x00)。如果你期望发送特定数据却看到0x00,可能就是指针错了。 - 传输大小:确认
transferByteCount参数设置正确。设小了会导致数据截断,设大了会导致程序访问非法的缓冲区内存,可能引发硬件错误(HardFault)。
6.3 中断/DMA不触发,传输卡住
调用了非阻塞传输函数,但等待的事件永远等不到,程序卡死。
- 深度排查:
- 中断优先级与全局开关:在RTOS中,检查SPI或DMA中断的优先级是否被设置得太低,被其他高优先级中断一直抢占。确认全局中断是否使能(
__enable_irq())。 - 中断向量表:这是DMA模式的特有坑。确认链接脚本是否正确包含了中断向量表,并且SPI中断的入口地址指向了
SPI_DRV_DmaSlaveIRQHandler(对于DMA模式)或SPI_DRV_SlaveIRQHandler(对于中断模式)。一个检查方法是,在Handler函数入口处设置一个断点,看能否进入。 - DMA通道请求源(MUX)配置:对于DMA模式,确保你为DMA通道配置的请求源(Request Source)与当前使用的SPI实例匹配。例如,SPI1的接收请求可能是
kDmaRequestMux0SPI1_Rx。配置错误会导致SPI无法触发DMA请求。 - 超时处理:即使是阻塞调用,也务必设置一个合理的超时时间。如果主机永远不发起传输,你的从机程序就会永远阻塞。超时后,可以尝试调用
SPI_DRV_SlaveAbortTransfer来安全地终止本次传输,并重置SPI状态。
- 中断优先级与全局开关:在RTOS中,检查SPI或DMA中断的优先级是否被设置得太低,被其他高优先级中断一直抢占。确认全局中断是否使能(
6.4 性能达不到预期
感觉用了DMA,但CPU占用率还是很高,或者传输速度上不去。
- 优化方向:
- SPI时钟频率:从机的SCK时钟由主机提供,但从机的总线时钟(例如SPI模块的时钟)必须足够快,以处理主机送来的数据。检查从机SPI模块的输入时钟分频配置,确保它高于主机SCK频率。
- DMA缓冲区与内存:确保DMA使用的源/目标缓冲区位于支持DMA访问的内存区域(通常是RAM)。如果缓冲区被错误地定义在Flash或低速内存中,DMA性能会大打折扣。
- 总线竞争:如果系统中有多个主设备(如CPU、另一个DMA控制器、以太网)同时访问内存或外设总线,会产生仲裁延迟。可以尝试调整DMA通道的优先级,或者优化数据流,减少并发访问。
- 中断风暴:对于中断模式,如果单次传输字节数少但频率极高,会导致中断频率过高。考虑增大SPI的接收FIFO深度(如果硬件支持),让FIFO半满或全满时再产生中断,以减少中断次数。
开发SPI从机驱动,尤其是用好中断和DMA,是一个从理解协议、熟悉硬件到掌握SDK的完整过程。它没有太多“黑魔法”,更多的是对细节的把握和严谨的调试。希望这篇结合了原理、SDK源码分析和实战踩坑经验的总结,能成为你手边一份有用的参考。当你下次再面对SPI通信难题时,不妨顺着“电源时钟->引脚复用->模式匹配->缓冲区->中断/DMA配置”这个链条逐一排查,问题往往就能迎刃而解。
