1. 项目概述与核心价值
在嵌入式系统开发中,SPI(Serial Peripheral Interface)通信的效率直接决定了整个系统的实时性与吞吐量。尤其是在需要与高速ADC、DAC、存储器或显示屏等外设进行大量数据交换的场景下,传统的CPU轮询或中断驱动方式往往会成为性能瓶颈,导致CPU被频繁打断,无法处理其他关键任务。我曾在多个电机控制和工业数据采集项目中,就因为SPI数据传输效率不足而吃过亏,要么是采样率上不去,要么是控制环路周期被迫拉长。
为了解决这个问题,将DMA(Direct Memory Access)技术与SPI结合使用,几乎是高性能嵌入式开发的必由之路。DMA能够在不占用CPU核心的情况下,自动完成内存与外设之间的数据搬运,让CPU得以“解放”出来去处理更复杂的算法和逻辑。而SPI的3线模式,则在某些引脚资源紧张或者需要简化布线的场合下,提供了另一种优化选择,它通过复用数据线,用三根线完成原本四根线的通信任务。
本文将以TI的TMS320F2838x系列高性能微控制器为例,深入剖析其SPI模块如何与DMA协同工作,以及3线模式的配置细节。我不会仅仅罗列寄存器手册的条目,而是结合我实际调试中的经验,重点讲解那些手册里一笔带过、但实际配置时极易出错的“坑”,比如DMA突发大小与FIFO阈值的匹配计算、3线模式下TALK位的精确控制时序等。无论你是正在评估F2838x的架构师,还是正在调试SPI通信的一线工程师,相信这些从实际项目中总结出的细节和心得,都能让你少走弯路。
2. SPI DMA传输机制深度解析
直接内存访问(DMA)对于SPI这类高速串行接口来说,其价值在于将CPU从繁琐的字节搬运工作中解脱出来。想象一下,你需要通过SPI连续读取一个1024点的传感器数据,如果没有DMA,CPU要么在中断服务程序里忙个不停,要么在轮询状态寄存器中空转。而DMA控制器就像一个不知疲倦的搬运工,你只需要告诉它源地址(SPI接收缓冲区)、目标地址(内存数组)和搬运数量,它就能在后台默默完成所有工作,并在完成后通过一个中断通知你,整个过程CPU的参与度极低。
2.1 DMA与SPI FIFO的协同工作原理
TMS320F2838x的SPI模块内置了深度为16字的FIFO(先入先出)缓冲区,这是实现高效DMA传输的基石。FIFO的存在相当于在SPI移位寄存器和系统内存之间设立了一个“蓄水池”或“缓存区”。
传输过程(TX):当我们需要发送大量数据时,DMA控制器的工作不是直接操作SPI的移位寄存器,而是持续地向SPI的发送FIFO(TX FIFO)中填充数据。SPI模块的发送逻辑会自主地从TX FIFO中取出数据,加载到移位寄存器中,再按照设定的时钟速率一位一位地发送出去。DMA只需要确保TX FIFO不会“断粮”即可。
接收过程(RX):反之,在接收数据时,SPI模块将接收到的数据存入接收FIFO(RX FIFO)。DMA控制器则监视着RX FIFO,一旦其中积累的数据达到我们预设的阈值,DMA便自动发起一次传输,将FIFO中的数据批量搬运到指定的内存区域。
这种架构的精妙之处在于,它将高速、连续的串行数据流,转换为了对DMA更友好的、突发式的内存块传输,极大地减少了DMA启动和总线仲裁的开销。
2.2 关键参数计算与避坑指南
手册中给出的计算步骤是准确的,但理解其背后的原因才能避免配置错误。我们以手册中的发送128字为例,拆解每个参数:
- 总传输字数(NUM_WORDS):这是最直观的,就是你需要发送或接收的数据总量,本例中为128。
- 发送FIFO阈值(TXFFIL):这个值决定了TX FIFO剩多少空位时,触发DMA进行下一次填充。手册示例设为8,意味着当TX FIFO中已发送的数据超过8个空位(即剩余空间小于等于8字)时,就会产生一个DMA请求。这里有一个关键限制:为了防止DMA向一个已经满的FIFO写数据导致数据丢失,DMA的突发传输大小(Burst Size)必须满足:
DMA_BURST_SIZE <= 16 - TXFFIL。如果TXFFIL=8,那么DMA一次最多只能搬8个字进来。如果你错误地将DMA突发大小设为10,那么当FIFO只剩6个空位时,DMA试图写入10个字,后4个字就会丢失。 - DMA传输次数(DMA_TRANSFER_SIZE):这个参数告诉DMA控制器,完成整个数据块需要发起多少次传输请求。计算公式为
(NUM_WORDS / TXFFIL) - 1。为什么减1?因为DMA传输计数寄存器通常配置为“传输次数-1”。对于128字,TXFFIL=8,计算得(128/8)-1 = 15。这意味着DMA需要发起16次传输(0到15次),每次在FIFO水位低于阈值时触发,搬送一个“突发”的数据。 - DMA突发大小(DMA_BURST_SIZE):这是DMA单次触发时,连续搬运的字数。计算公式为
(16 - TXFFIL) - 1。同样,减1是因为DMA的突发计数寄存器配置为“突发字数-1”。(16-8)-1 = 7,即每次突发传输8个字。
重要提示:务必避免将TXFFIL设置为0x0或0x10(即0或16)。如果设为0,意味着FIFO一空就请求DMA,但DMA响应和填充需要时间,极易造成FIFO下溢,发送中断。如果设为16,则FIFO永远达不到“低于阈值”的条件(因为FIFO最大深度为16),DMA请求永远不会被触发。
接收方向的配置逻辑类似但有所不同:对于接收FIFO,DMA突发大小必须小于等于RXFFIL,以防止从空的FIFO中读取数据。同时,为了确保DMA能完整地搬走所有数据,建议让突发大小(Burst Size)等于RXFFIL,并且是总传输字数(NUM_WORDS)的整数因子。例如,接收200字,设置RXFFIL=4,那么突发大小=4-1=3(即每次搬4字),传输次数=(200/4)-1=49(共50次传输)。这样,每次FIFO中数据达到4字,DMA就搬走4字,循环50次正好搬完200字,严丝合缝。
3. SPI 3线模式配置与数据收发实战
3线模式是SPI通信的一种变体,它将独立的MISO(主入从出)和MOSI(主出从入)两条数据线合并为一条双向数据线(对于主设备是SPIMOMI,对于从设备是SPISISO)。这节省了一个宝贵的GPIO引脚,在引脚资源受限的紧凑型设计中非常有用,但同时也引入了新的软件控制复杂度。
3.1 3线模式下的引脚与内部连接变化
在标准4线模式下,数据通路是分开的:主机从MOSI发出,从机从MISO发出。在3线模式下:
- 主机:SPISIMO引脚变为双向的SPIMOMI引脚,用于同时发送和接收。SPISOMI引脚不再被SPI模块使用,可复用为普通GPIO。
- 从机:SPISOMI引脚变为双向的SPISISO引脚。SPISIMO引脚被释放。
最关键的一点是,在3线模式下,SPI模块内部的发送和接收路径是短接的。这意味着,主机发送的数据,会立刻被自己接收回来;从机发送的数据,也会被自己接收回来。这部分“自发自收”的数据对应用来说是无用的“垃圾数据”,必须在软件中妥善处理。
3.2 TALK位的核心作用与操作流程
TALK位(SPICTL.1)在3线模式中扮演着数据流方向控制的“闸门”角色,其控制逻辑是3线模式编程的核心,也是最容易出错的地方。
传输数据(主机或从机):
- 设置
TALK = 1。这将打开发送路径,允许数据从SPIDAT/TXBUF通过双向数据引脚输出。 - 将待发送数据写入
SPITXBUF。 - 等待SPI中断标志
INT_FLAG置位,表示一次传输完成。 - 关键步骤:必须从
SPIRXBUF执行一次虚读(Dummy Read)。因为发送的数据同时也被接收了,如果不读走,接收缓冲区会被占用,影响后续操作。读取的数据可以丢弃。
接收数据(以主机为例):
- 设置
TALK = 0。这是与发送模式最根本的区别。关闭发送路径,此时主机不会驱动SPIMOMI引脚。 - 向
SPITXBUF写入一个虚写数据(Dummy Write)。这个操作的目的不是要发送这个数据(因为TALK=0,数据不会输出到引脚),而是为了产生SPI时钟。SPI作为主机,只有在有数据要“发送”时才会产生SCLK。写入虚数据就是为了启动时钟生成,从而触发从机向外发送数据。 - 等待
INT_FLAG置位。 - 从
SPIRXBUF读取的,就是从机发送过来的真实数据。
从机接收数据相对简单,因为从机的时钟由主机提供。从机只需设置TALK = 0(防止意外驱动总线),然后等待主机发起传输,并在INT_FLAG置位后读取SPIRXBUF即可。
实操心得:在3线主模式接收时,最容易忘记的就是设置
TALK=0后再写虚数据。我曾调试过一个传感器读取程序,主机能收到数据,但全是0xFF。排查了半天才发现,在接收流程中误将TALK设为了1,导致主机一直在驱动数据线,压制了从机的输出,从机数据根本送不出来。所以,牢记“接收先闭嘴(TALK=0),再敲门(写虚数据)”。
4. 完整配置流程与代码实例
理解了原理,我们来看如何从零开始,配置一个使用DMA的SPI 3线主设备。以下流程基于TMS320F2838x,但思路通用。
4.1 SPI模块初始化配置
在进行任何功能配置前,一个良好的习惯是先将模块置于复位安全状态。
// 1. 将SPI置于软件复位状态,安全地进行配置 SpiaRegs.SPICCR.bit.SPISWRESET = 0; // 2. 配置SPI基本参数 SpiaRegs.SPICCR.bit.SPICHAR = 7; // 字符长度:8位 (7+1) SpiaRegs.SPICCR.bit.CLKPOLARITY = 0; // 时钟极性:空闲低电平 SpiaRegs.SPICTL.bit.CLK_PHASE = 0; // 时钟相位:数据在第一个边沿采样(模式0) SpiaRegs.SPICTL.bit.MASTER_SLAVE = 1; // 主模式 SpiaRegs.SPICTL.bit.TALK = 1; // 使能发送(初始化为发送使能,后续根据模式调整) SpiaRegs.SPIBRR = 49; // 波特率 = LSPCLK / (49+1)。假设LSPCLK=100MHz,则波特率为2MHz // 3. 启用FIFO增强功能 SpiaRegs.SPIFFTX.bit.SPIFFENA = 1; // 使能FIFO SpiaRegs.SPIFFTX.bit.TXFIFO = 1; // 释放发送FIFO复位 SpiaRegs.SPIFFRX.bit.RXFIFORESET = 1; // 释放接收FIFO复位 SpiaRegs.SPIFFCT.all = 0x0; // FIFO传输延迟设置为0 // 4. 配置3线模式 SpiaRegs.SPIPRI.bit.TRIWIRE = 1; // 使能3线SPI模式 // 5. 配置GPIO复用为SPI 3线主模式引脚 // SPICLK, SPISTE, SPISIMO (此时作为SPIMOMI) 复用为SPI功能 // SPISOMI 引脚可配置为普通GPIO输入或输出 // 6. 将SPI退出复位状态,开始运行 SpiaRegs.SPICCR.bit.SPISWRESET = 1;4.2 DMA控制器配置示例
假设我们需要发送128个16位数据,使用DMA通道1。计算参数:NUM_WORDS=128,TXFFIL=8, 则DMA_TRANSFER_SIZE=15,DMA_BURST_SIZE=7。
// 配置DMA通道1用于SPI发送 // 假设源数据存放在数组 sdataBuffer[128] 中 DMACH1_BASE->CONTROL.bit.SYNC = 0; // 异步传输 DMACH1_BASE->CONTROL.bit.CHINTMODE = 0; // 中断在传输完成后产生 DMACH1_BASE->CONTROL.bit.ONESHOT = 1; // 单次触发模式,每次SPI FIFO请求触发一次突发传输 DMACH1_BASE->CONTROL.bit.CHINTE = 1; // 使能通道中断 DMACH1_BASE->CONTROL.bit.SIZE = 0; // 传输数据单元为16位 DMACH1_BASE->CONTROL.bit.BURST_SIZE = 7; // 突发大小 = 8字 (7+1) DMACH1_BASE->CONTROL.bit.SRC_DIR = 0; // 源地址不递增 DMACH1_BASE->CONTROL.bit.DST_DIR = 0; // 目的地址不递增 DMACH1_BASE->SRC_ADDR_SHADOW = (Uint32)&sdataBuffer[0]; // 源数据起始地址 DMACH1_BASE->DST_ADDR_SHADOW = (Uint32)&SpiaRegs.SPITXBUF; // 目的地址为SPI发送缓冲区 DMACH1_BASE->BURST_COUNT = 15; // 传输次数 = 16次 (15+1) DMACH1_BASE->TRANSFER_COUNT = 7; // 每次传输的单元数 = 8字 (7+1),与BURST_SIZE一致 // 配置DMA触发源为SPA的发送FIFO中断(TXFFINT),当TX FIFO空间>=8时触发 DMACH1_BASE->TRIGGER_SELECT = 58; // 具体触发源编号需查芯片手册,例如58可能对应SPIA TX FIFO // 启动DMA通道 DMACH1_BASE->CONTROL.bit.RUN = 1;4.3 3线模式数据收发代码整合
结合DMA发送和手动接收(假设接收数据量不大),一个完整的3线主设备交互流程如下:
// 第一部分:使用DMA发送一批数据 void SPI_3Wire_DMA_Transmit(Uint16* pData, Uint16 length) { // 确保TALK位为1,打开发送路径 SpiaRegs.SPICTL.bit.TALK = 1; // 根据length计算TXFFIL、DMA_TRANSFER_SIZE、DMA_BURST_SIZE // ... (计算逻辑略) // 配置SPI发送FIFO阈值 SpiaRegs.SPIFFTX.bit.TXFFIL = calculated_txffil; // 配置DMA(如4.2节所示),并启动 // 等待DMA传输完成中断 while(!isDmaTransferComplete); isDmaTransferComplete = 0; // DMA发送完成,但SPI可能还在发送FIFO中最后的几个字 // 等待SPI发送真正空闲 while(SpiaRegs.SPIFFTX.bit.TXFFST != 0); // 重要:读取并丢弃自发自收的垃圾数据 // 因为发送了length个字,所以也会收到length个垃圾字 for(i=0; i<length; i++) { dummy = SpiaRegs.SPIRXBUF; } } // 第二部分:接收从机数据(假设接收20个字) void SPI_3Wire_Receive(Uint16* pBuffer, Uint16 length) { Uint16 i; // 1. 关闭发送路径,主机释放总线 SpiaRegs.SPICTL.bit.TALK = 0; // 2. 为了产生时钟,需要写入虚数据。可以写任意值,如0x0000 // 注意:在3线主接收模式下,写入的数据不会出现在引脚上 for(i=0; i<length; i++) { SpiaRegs.SPITXBUF = 0x0000; // 虚写,启动一次传输 // 等待本次传输完成 while(SpiaRegs.SPISTS.bit.INT_FLAG != 1); // 3. 读取从机发来的真实数据 pBuffer[i] = SpiaRegs.SPIRXBUF; // 清除中断标志(读取SPIRXBUF已自动清除) } // 接收完毕,可根据需要重新打开TALK // SpiaRegs.SPICTL.bit.TALK = 1; }5. 高频问题排查与调试技巧
在实际项目中,配置SPI DMA和3线模式很少能一次成功。下面是我总结的几个常见问题及排查手段。
5.1 DMA传输不启动或数据不完整
- 症状:程序运行后,SPI线上没有波形,或者只发送了部分数据就停止了。
- 排查步骤:
- 检查SPI软件复位位:确认在配置完成后已将
SPISWRESET置1。这是最容易被忽略的一步,该位为0时SPI模块处于复位状态,不会工作。 - 检查DMA触发源:确认DMA的
TRIGGER_SELECT寄存器配置正确,指向了SPI的TXFFINT或RXFFINT。错误的触发源会导致DMA永远等不到启动信号。 - 检查FIFO阈值与中断使能:对于发送,确保
SPIFFTX.bit.TXFFIENA=1(使能TX FIFO中断)且TXFFIL设置合理(非0非16)。对于接收,确保SPIFFRX.bit.RXFFIENA=1且RXFFIL设置合理。 - 验证DMA配置:使用调试器查看DMA通道的
CONTROL.bit.RUN位是否被置起。检查源/目的地址是否正确,传输计数和突发计数是否匹���总数据量。 - 检查时钟与引脚复用:确认外设时钟LSPCLK已使能并正确分频。使用GPIO寄存器查看工具,确认SPI所需的几个引脚已正确复用为外设功能,而非普通GPIO。
- 检查SPI软件复位位:确认在配置完成后已将
5.2 3线模式通信失败
- 症状:主机发送,从机无响应;或主机接收到的全是0xFF/0x00。
- 排查步骤:
- 示波器/逻辑分析仪是王道:这是最直接的诊断工具。查看SPICLK、SPISTE和双向数据线(SPIMOMI/SPISISO)上的波形。
- 有时钟吗?如果主机没有SCLK输出,检查
TALK位和MASTER_SLAVE位,并确认主机是否执行了写SPITXBUF的操作(即使是虚写)。 - 片选信号正确吗?确认SPISTE信号在传输期间有效(低电平有效,除非配置了STEINV反转)。
- 数据线方向对吗?在主机发送阶段,数据线应由主机驱动;在主机接收阶段,主机应释放总线(高阻),由从机驱动。用示波器看数据线在接收阶段是否有变化,如果没有,可能是从机未响应或
TALK位设置错误导致主机仍在驱动。
- 有时钟吗?如果主机没有SCLK输出,检查
- TALK位时序:这是3线模式的核心 bug 来源。严格遵循“发送时TALK=1,接收前TALK=0”的流程。在切换方向时,确保上一次传输已完成(
INT_FLAG=1且已读取垃圾数据)。 - 从机配置:确认从机设备也工作在3线模式,并且其时钟极性(CPOL)和相位(CPHA)与主机完全匹配。很多SPI从设备(如传感器)的3线模式可能有特殊的寄存器需要配置。
- 示波器/逻辑分析仪是王道:这是最直接的诊断工具。查看SPICLK、SPISTE和双向数据线(SPIMOMI/SPISISO)上的波形。
5.3 数据错位或字节序问题
- 症状:接收到的数据与预期不符,但似乎有规律,比如字节顺序颠倒。
- 排查步骤:
- 字符长度(SPICHAR):SPI模块支持1-16位字长。确保主机和从机配置的字长一致。如果你配置为8位(SPICHAR=7),但对方设备是16位,那么数据必然对不上。
- 数据对齐:手册明确指出,发送数据必须左对齐写入,接收数据是右对齐读取。例如,发送8位数据0xAB,在16位的
SPITXBUF中应写入0xAB00。而接收到的16位SPIRXBUF中,有效数据在低8位,需要做掩码处理,如received_data = SpiaRegs.SPIRXBUF & 0x00FF。 - FIFO与DMA的数据宽度:确保DMA配置的数据宽度(16位)与SPI的数据宽度匹配。同时,内存中的数据数组也应按16位(Uint16)对齐,避免访问越界或对齐错误。
调试这类复杂外设交互,分步验证至关重要。不要试图一次性完成所有配置。可以先配置SPI为最简单的4线模式、不用DMA、不用中断,实现基本的字节收发。然后逐步加入FIFO、中断、DMA,最后再切换到3线模式。每增加一个功能,就验证一次,能快速定位问题所在。