TMS320F2838x SPI DMA与3线模式配置详解及避坑指南 📅 2026/7/19 12:17:51 1. 项目概述与核心价值在嵌入式系统开发中SPISerial Peripheral Interface通信的效率直接决定了整个系统的实时性与吞吐量。尤其是在需要与高速ADC、DAC、存储器或显示屏等外设进行大量数据交换的场景下传统的CPU轮询或中断驱动方式往往会成为性能瓶颈导致CPU被频繁打断无法处理其他关键任务。我曾在多个电机控制和工业数据采集项目中就因为SPI数据传输效率不足而吃过亏要么是采样率上不去要么是控制环路周期被迫拉长。为了解决这个问题将DMADirect 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点的传感器数据如果没有DMACPU要么在中断服务程序里忙个不停要么在轮询状态寄存器中空转。而DMA控制器就像一个不知疲倦的搬运工你只需要告诉它源地址SPI接收缓冲区、目标地址内存数组和搬运数量它就能在后台默默完成所有工作并在完成后通过一个中断通知你整个过程CPU的参与度极低。2.1 DMA与SPI FIFO的协同工作原理TMS320F2838x的SPI模块内置了深度为16字的FIFO先入先出缓冲区这是实现高效DMA传输的基石。FIFO的存在相当于在SPI移位寄存器和系统内存之间设立了一个“蓄水池”或“缓存区”。传输过程TX当我们需要发送大量数据时DMA控制器的工作不是直接操作SPI的移位寄存器而是持续地向SPI的发送FIFOTX FIFO中填充数据。SPI模块的发送逻辑会自主地从TX FIFO中取出数据加载到移位寄存器中再按照设定的时钟速率一位一位地发送出去。DMA只需要确保TX FIFO不会“断粮”即可。接收过程RX反之在接收数据时SPI模块将接收到的数据存入接收FIFORX 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。如果TXFFIL8那么DMA一次最多只能搬8个字进来。如果你错误地将DMA突发大小设为10那么当FIFO只剩6个空位时DMA试图写入10个字后4个字就会丢失。DMA传输次数DMA_TRANSFER_SIZE这个参数告诉DMA控制器完成整个数据块需要发起多少次传输请求。计算公式为(NUM_WORDS / TXFFIL) - 1。为什么减1因为DMA传输计数寄存器通常配置为“传输次数-1”。对于128字TXFFIL8计算得(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最大深度为16DMA请求永远不会被触发。接收方向的配置逻辑类似但有所不同对于接收FIFODMA突发大小必须小于等于RXFFIL以防止从空的FIFO中读取数据。同时为了确保DMA能完整地搬走所有数据建议让突发大小Burst Size等于RXFFIL并且是总传输字数NUM_WORDS的整数因子。例如接收200字设置RXFFIL4那么突发大小4-13即每次搬4字传输次数(200/4)-149共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。这个操作的目的不是要发送这个数据因为TALK0数据不会输出到引脚而是为了产生SPI时钟。SPI作为主机只有在有数据要“发送”时才会产生SCLK。写入虚数据就是为了启动时钟生成从而触发从机向外发送数据。等待INT_FLAG置位。从SPIRXBUF读取的就是从机发送过来的真实数据。从机接收数据相对简单因为从机的时钟由主机提供。从机只需设置TALK 0防止意外驱动总线然后等待主机发起传输并在INT_FLAG置位后读取SPIRXBUF即可。实操心得在3线主模式接收时最容易忘记的就是设置TALK0后再写虚数据。我曾调试过一个传感器读取程序主机能收到数据但全是0xFF。排查了半天才发现在接收流程中误将TALK设为了1导致主机一直在驱动数据线压制了从机的输出从机数据根本送不出来。所以牢记“接收先闭嘴TALK0再敲门写虚数据”。4. 完整配置流程与代码实例理解了原理我们来看如何从零开始配置一个使用DMA的SPI 3线主设备。以下流程基于TMS320F2838x但思路通用。4.1 SPI模块初始化配置在进行任何功能配置前一个良好的习惯是先将模块置于复位安全状态。// 1. 将SPI置于软件复位状态安全地进行配置 SpiaRegs.SPICCR.bit.SPISWRESET 0; // 2. 配置SPI基本参数 SpiaRegs.SPICCR.bit.SPICHAR 7; // 字符长度8位 (71) 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 / (491)。假设LSPCLK100MHz则波特率为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_WORDS128,TXFFIL8, 则DMA_TRANSFER_SIZE15,DMA_BURST_SIZE7。// 配置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字 (71) 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次 (151) DMACH1_BASE-TRANSFER_COUNT 7; // 每次传输的单元数 8字 (71)与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(i0; ilength; 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(i0; ilength; 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.TXFFIENA1使能TX FIFO中断且TXFFIL设置合理非0非16。对于接收确保SPIFFRX.bit.RXFFIENA1且RXFFIL设置合理。验证DMA配置使用调试器查看DMA通道的CONTROL.bit.RUN位是否被置起。检查源/目的地址是否正确传输计数和突发计数是否匹总数据量。检查时钟与引脚复用确认外设时钟LSPCLK已使能并正确分频。使用GPIO寄存器查看工具确认SPI所需的几个引脚已正确复用为外设功能而非普通GPIO。5.2 3线模式通信失败症状主机发送从机无响应或主机接收到的全是0xFF/0x00。排查步骤示波器/逻辑分析仪是王道这是最直接的诊断工具。查看SPICLK、SPISTE和双向数据线SPIMOMI/SPISISO上的波形。有时钟吗如果主机没有SCLK输出检查TALK位和MASTER_SLAVE位并确认主机是否执行了写SPITXBUF的操作即使是虚写。片选信号正确吗确认SPISTE信号在传输期间有效低电平有效除非配置了STEINV反转。数据线方向对吗在主机发送阶段数据线应由主机驱动在主机接收阶段主机应释放总线高阻由从机驱动。用示波器看数据线在接收阶段是否有变化如果没有可能是从机未响应或TALK位设置错误导致主机仍在驱动。TALK位时序这是3线模式的核心 bug 来源。严格遵循“发送时TALK1接收前TALK0”的流程。在切换方向时确保上一次传输已完成INT_FLAG1且已读取垃圾数据。从机配置确认从机设备也工作在3线模式并且其时钟极性CPOL和相位CPHA与主机完全匹配。很多SPI从设备如传感器的3线模式可能有特殊的寄存器需要配置。5.3 数据错位或字节序问题症状接收到的数据与预期不符但似乎有规律比如字节顺序颠倒。排查步骤字符长度SPICHARSPI模块支持1-16位字长。确保主机和从机配置的字长一致。如果你配置为8位SPICHAR7但对方设备是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线模式。每增加一个功能就验证一次能快速定位问题所在。