TMS320F28003x SPI寄存器级配置与调试实战
2026/9/24 2:36:26 网站建设 项目流程

1. 项目缘起与整体设计思路

1.1 为什么选择TMS320F28003x的SPI外设

TMS320F28003x属于TI C2000系列实时微控制器,主频100MHz,带FPU和TMU,在数字电源、电机驱动、光伏逆变这些场景里用得非常多。我手头这个项目是做三相PFC的数字控制板,主控就是F28003x,板子上挂了EEPROM、外部ADC和一颗隔离型栅极驱动配置芯片,三者都走SPI总线。选SPI而不是I2C或者UART,理由很直接:SPI是全双工同步串行,速率能拉到几十MHz,协议开销几乎为零,适合周期性读取ADC采样值这种对时序敏感的场景。I2C虽然省引脚,但速率和总线仲裁机制在强干扰的功率板上容易出问题;UART是异步的,没有时钟线,长距离传输时波特率误差累积会丢帧。

F28003x内部有多个SPI模块,我这次用的是SPIA和SPIC。SPIA接外部ADC,SPIC接EEPROM和驱动配置芯片,通过不同的片选引脚分时复用。这里有个设计取舍:为什么不用一个SPI挂三个从设备?因为外部ADC的采样率要求高,需要独占总线避免被其他设备的传输阻塞;而EEPROM和配置芯片都是低速设备,共享一条总线完全够用。这种“高速独占、低速共享”的分配思路,在实际项目里比一股脑全挂一条总线要稳妥得多。

1.2 寄存器配置与库函数配置的取舍

TI给C2000提供了driverlib库,很多新手直接调SPI_init()就完事了。但我这个项目对时序要求苛刻,库函数封装了一些默认行为,比如FIFO触发深度、时钟相位极性这些,如果不去翻寄存器手册,出了问题根本不知道从哪查。所以我的做法是:用driverlib做初始化框架,但关键寄存器手动覆写,尤其是SPICCR、SPICTL、SPIBRR、SPIFFTX、SPIFFRX这几个。这样既保留了库函数的可读性,又能在调试时精确控制每一个bit。

具体来说,SPICCR负责时钟极性和字符长度,SPICTL管相位、主从模式和中断使能,SPIBRR决定波特率,SPIFFTX和SPIFFRX是FIFO的发送接收控制。很多人配置SPI不生效,十有八九是SPICCR里的SPICHAR(字符长度)和实际数据宽度对不上,或者SPICTL的CLKPOLARITY设反了。这些细节后面会逐个拆开讲。

1.3 整体调试策略的规划

调试SPI不能上来就写代码,我的习惯是分三步走:第一步,用示波器或者逻辑分析仪确认硬件连线没问题,包括片选、时钟、MOSI、MISO四根线的电平和时序;第二步,写一个最简单的单字节收发测试,不接从设备,只测主设备能不能正常产生时钟和片选信号;第三步,接上真实从设备,逐步增加数据量和速率,观察波形是否畸变。这个顺序看起来笨,但能帮你把问题隔离在最小范围内。我见过太多人一上来就写完整驱动,结果通信失败,既怀疑硬件又怀疑软件,最后查了两天发现是片选引脚配错了复用功能。

2. SPI寄存器核心细节逐位拆解

2.1 SPICCR寄存器:时钟极性与字符长度

SPICCR的全称是SPI Configuration Control Register,地址在SPIA基址+0x0000。这个寄存器里最关键的三个字段是SPICHAR、CLKPOLARITY和SPISWRESET。

SPICHAR占bit0到bit3,决定每次传输的字符长度,范围是1到16位。注意这里写的是“字符长度减一”,比如你要传8位数据,SPICHAR要写7,不是8。这个坑我踩过,当时设成8,结果每次传输多出一个bit,从设备完全不理我。CLKPOLARITY在bit6,决定时钟空闲时的电平。如果从设备要求时钟空闲为低,这一位写0;空闲为高写1。SPISWRESET在bit7,写0会让SPI进入复位状态,配置其他寄存器之前必须先把它清零,配置完再置1。很多人配置不生效就是因为忘了这一步,SPI一直在复位状态,寄存器写进去也没用。

注意:SPISWRESET复位后,SPICCR和SPICTL的其他位才能正常写入。配置顺序一定是先复位、再配置、最后释放复位。

2.2 SPICTL寄存器:相位、主从与中断控制

SPICTL是SPI Operation Control Register,地址偏移0x0001。bit0是SPIINTENA,使能SPI中断;bit1是TALK,主模式下必须置1才能发送数据,从模式下置1才能接收;bit2是MASTER_SLAVE,1为主模式,0为从模式;bit3是CLOCK_PHASE,决定数据在时钟的哪个边沿采样;bit4是OVERRUNINTENA,溢出中断使能。

CLOCK_PHASE和CLKPOLARITY组合起来就是SPI的四种模式。Mode0是CPOL=0、CPHA=0,数据在时钟上升沿采样、下降沿切换;Mode1是CPOL=0、CPHA=1;Mode2是CPOL=1、CPHA=0;Mode3是CPOL=1、CPHA=1。大部分SPI Flash和EEPROM用Mode0或Mode3,具体要看从设备手册。我这次用的外部ADC是Mode1,配置芯片是Mode0,所以两条SPI总线的SPICTL值是不一样的,不能照抄。

2.3 SPIBRR寄存器:波特率的精确计算

SPIBRR是SPI Baud Rate Register,地址偏移0x0004。波特率的计算公式分两种情况:当SPIBRR的值在3到127之间时,波特率等于LSPCLK除以(SPIBRR+1);当SPIBRR小于3时,波特率等于LSPCLK除以4。LSPCLK是低速外设时钟,默认是SYSCLKOUT除以4,也就是100MHz除以4等于25MHz。

假设我要把SPIA配到10MHz,计算过程是:SPIBRR+1 = 25MHz / 10MHz = 2.5,取整后SPIBRR = 1.5,但SPIBRR必须是整数,所以要么选SPIBRR=1得到12.5MHz,要么选SPIBRR=2得到8.33MHz。实际项目中我选了SPIBRR=2,因为8.33MHz对外部ADC来说足够,而且留了余量避免信号完整性问题。这里有个经验:SPI速率不要贴着从设备的最高速率配,留20%到30%的余量,尤其是板子上有功率器件开关噪声的时候。

2.4 SPIFFTX与SPIFFRX:FIFO的发送与接收管理

SPIFFTX是FIFO Transmit Register,地址偏移0x000A。bit13是SPIFFENA,使能FIFO增强功能;bit14是TXFIFO复位;bit8到bit12是TXFFIL,发送FIFO中断触发深度。SPIFFRX是FIFO Receive Register,地址偏移0x000B,bit13是RXFFIENA,接收FIFO中断使能;bit8到bit12是RXFFIL,接收FIFO中断触发深度。

FIFO深度是16级。我一般把TXFFIL设成8,RXFFIL设成8,这样发送到一半就触发中断填充下一批数据,接收收到一半就触发中断读取,避免溢出。如果不用FIFO,每次收发一个字节都要进中断,CPU开销太大,在100MHz主频下虽然扛得住,但会影响其他实时任务的响应。用了FIFO之后,中断频率降到原来的八分之一,CPU有更多时间处理控制算法。

3. 完整实操流程与关键环节实现

3.1 硬件连线检查与引脚复用配置

F28003x的SPI引脚是GPIO复用的,SPIA的默认引脚是GPIO16到GPIO19,分别对应SPICLK、SPISIMO、SPISOMI和SPISTE。但实际板子上可能用了其他GPIO,需要通过GPIO_setPinConfig()函数把引脚配成SPI功能。我这次SPIA用的是GPIO58到GPIO61,因为GPIO16到GPIO19被PWM模块占用了。配置代码是这样的:

GPIO_setPinConfig(GPIO_58_SPICLKA); GPIO_setPinConfig(GPIO_59_SPISIMOA); GPIO_setPinConfig(GPIO_60_SPISOMIA); GPIO_setPinConfig(GPIO_61_SPISTEA);

片选引脚我单独用了一个普通GPIO,因为从设备需要软件片选,硬件片选在F28003x上只有SPISTE一个,不够用。软件片选的好处是灵活,可以在传输前后精确控制片选拉低和拉高的时刻,缺点是占用CPU时间。对于低速设备无所谓,对于高速ADC,我用了SPISTE硬件片选,因为时序要求严格,软件控制容易引入抖动。

3.2 SPI初始化代码的完整实现

初始化分四步:复位SPI、配置时钟和字符长度、配置相位和主从模式、配置FIFO和波特率。下面是我实际项目里的代码,基于driverlib但做了寄存器级覆写:

void SPI_A_Init(void) { // 第一步:复位SPI SPI_resetModule(SPIA_BASE); // 第二步:配置SPICCR // SPICHAR=7 (8位), CLKPOLARITY=0 (空闲低), SPISWRESET=0 (保持复位) HWREGH(SPIA_BASE + SPI_O_CCR) = 0x0007; // 第三步:配置SPICTL // MASTER=1, TALK=1, CLOCK_PHASE=1 (Mode1), SPIINTENA=0 HWREGH(SPIA_BASE + SPI_O_CTL) = 0x000E; // 第四步:配置SPIBRR // LSPCLK=25MHz, 目标8.33MHz, SPIBRR=2 HWREGH(SPIA_BASE + SPI_O_BRR) = 0x0002; // 第五步:配置FIFO // TXFFIL=8, RXFFIL=8, 使能FIFO HWREGH(SPIA_BASE + SPI_O_FFTX) = 0xE008; HWREGH(SPIA_BASE + SPI_O_FFRX) = 0x6008; // 第六步:释放复位 HWREGH(SPIA_BASE + SPI_O_CCR) |= 0x0080; }

这段代码里,SPICTL写0x000E,二进制是0000 0000 0000 1110,bit1到bit3都是1,对应TALK=1、MASTER=1、CLOCK_PHASE=1。SPIFFTX写0xE008,bit13到bit15是111,使能FIFO增强和复位;bit3到bit7是00001,对应TXFFIL=8。SPIFFRX写0x6008,bit13和bit14是11,使能接收FIFO中断和复位;bit3到bit7是00001,对应RXFFIL=8。

3.3 数据收发函数的编写与调试

发送一个字节并接收返回值的函数:

uint16_t SPI_A_TransferByte(uint16_t txData) { // 等待发送FIFO有空位 while (SPI_getTxFIFOStatus(SPIA_BASE) == SPI_FIFO_FULL); // 写入发送FIFO SPI_writeDataNonBlocking(SPIA_BASE, txData); // 等待接收FIFO有数据 while (SPI_getRxFIFOStatus(SPIA_BASE) == SPI_FIFO_EMPTY); // 读取接收FIFO return SPI_readDataNonBlocking(SPIA_BASE); }

这个函数是阻塞式的,适合调试阶段用。实际项目里我改成了中断方式,发送和接收都在FIFO中断里处理,主循环只负责搬运数据。调试时先用阻塞式确认通信正常,再切换到中断方式,这样出了问题容易定位。

3.4 逻辑分析仪抓取波形的关键观察点

接上逻辑分析仪之后,我重点看四个东西:片选拉低的时刻是否在时钟开始之前、时钟的极性和相位是否和从设备手册一致、MOSI上的数据在时钟边沿是否稳定、MISO上的数据是否在正确的边沿被采样。我这次遇到一个典型问题:片选拉低和第一个时钟沿之间只有10ns,从设备要求至少50ns的建立时间,导致第一个bit经常读错。解决办法是在片选拉低之后加一个短延时,用DEVICE_DELAY_US(1)就够了,1微秒的延时对8.33MHz的SPI来说完全可以接受。

4. 常见问题排查与实战避坑指南

4.1 SPI通信完全不生效的排查顺序

通信不生效是最常见的问题,我的排查顺序是:先查引脚复用有没有配对,再查SPI有没有从复位状态释放,然后查片选有没有正常拉低,最后查时钟有没有输出。这四步能覆盖90%的问题。具体来说,用万用表测片选引脚,传输时应该看到电平翻转;用示波器测SPICLK,应该有方波输出。如果片选不动,检查GPIO方向寄存器是不是设成了输出;如果时钟不动,检查SPICCR的SPISWRESET是不是忘了置1。

4.2 数据错位的三种典型原因

数据错位表现为读回来的值整体偏移一位或者几位。原因一:SPICHAR设错了,比如实际传8位但SPICHAR设成了7,导致每次少传一位。原因二:CLOCK_PHASE设反了,数据在错误的边沿被采样。原因三:从设备的片选建立时间不够,第一个bit丢失。这三种情况我都遇到过,最隐蔽的是第三种,因为波形看起来完全正常,只是第一个bit的值偶尔不对。解决办法就是在片选拉低后加延时,或者降低SPI速率。

4.3 FIFO溢出与中断丢失的处理

FIFO溢出表现为接收数据丢失或者中断进不去。SPIFFRX的bit15是RXFFOVF,溢出标志位,一旦置1说明接收FIFO满了但CPU没及时读走。处理方法是:在接收中断里一次性把FIFO里的数据全部读出来,而不是只读一个。我一般用while循环读,直到SPI_getRxFIFOStatus返回EMPTY为止。另外,RXFFIL不要设得太高,设成8比较稳妥,设成14的话只剩两个空位,稍微有点延迟就溢出了。

4.4 高速SPI的信号完整性优化

SPI速率超过10MHz之后,信号完整性问题开始显现。我这次把SPIA配到8.33MHz,波形还算干净,但之前试过12.5MHz,MOSI上有明显的过冲和振铃。优化手段有三个:一是在MOSI和SPICLK上串22欧姆的电阻,抑制反射;二是缩短走线长度,SPI走线不要超过10厘米;三是降低驱动强度,F28003x的GPIO驱动强度可以通过GPADAT寄存器配置,从默认的8mA降到4mA,边沿变缓,过冲减小。这三个手段我全用上了,12.5MHz也能稳定通信。

4.5 常见问题速查表

现象可能原因排查方法解决办法
完全无通信引脚复用未配置测引脚电平调用GPIO_setPinConfig
完全无通信SPI处于复位状态读SPICCR bit7置位SPISWRESET
时钟无输出主从模式配错读SPICTL bit2设MASTER=1
数据错位SPICHAR设错核对数据宽度SPICHAR=宽度-1
数据错位相位极性不匹配对照从设备手册调整CPOL/CPHA
第一个bit错误片选建立时间不足测片选到时钟延时加1us延时
数据丢失FIFO溢出读SPIFFRX bit15降低RXFFIL,及时读FIFO
高速振铃信号反射示波器看过冲串22欧姆电阻

5. 调试心得与扩展思考

5.1 寄存器级调试的价值

用库函数能快速跑通,但寄存器级调试能让你真正理解外设的行为。我这次把SPICCR、SPICTL、SPIBRR、SPIFFTX、SPIFFRX五个寄存器的每一位都翻了一遍手册,虽然花了大半天时间,但后面遇到问题时排查速度比之前快了好几倍。比如有一次接收数据偶尔多一个字节,我直接读SPIFFRX发现RXFFOVF置1了,立刻定位到是FIFO溢出,而不是去怀疑从设备或者硬件连线。这种“知道自己在配什么”的感觉,是库函数给不了的。

5.2 多从设备共享总线的片选管理

SPIC和SPIA不同,SPIC上挂了EEPROM和配置芯片两个从设备。片选管理我用的是软件片选,两个GPIO分别控制。关键点是:切换从设备时,必须先拉高当前片选,等至少一个时钟周期,再拉低另一个片选。如果两个片选同时为低,两个从设备会同时驱动MISO线,导致总线冲突。我一开始没注意这个,EEPROM读出来的数据全是0xFF,查了半天才发现是片选切换太快,两个设备同时在线。

5.3 从SPI扩展到其他外设的通用思路

SPI调通之后,我发现这套寄存器级调试的方法可以复用到I2C、UART、CAN这些外设上。核心思路是一样的:先复位外设、再配置控制寄存器、然后配置速率和中断、最后释放复位。区别只是寄存器的名字和字段不同。所以花时间搞懂一个外设的寄存器,对理解整个芯片的外设架构都有帮助。我后面调I2C的时候,直接套用SPI的调试流程,半天就跑通了。

5.4 后续可以继续深挖的方向

这个项目目前SPI跑在8.33MHz,数据量不大,FIFO中断方式完全够用。如果后续要接更高速度的ADC,比如20MHz以上的,可以考虑用DMA来搬运SPI数据,进一步降低CPU开销。F28003x的DMA支持外设触发,SPI的接收FIFO可以触发DMA搬运,这样CPU只需要在DMA传输完成中断里处理数据就行。另外,SPI的TI模式(多数据线模式)也可以研究一下,用四根数据线并行传输,速率能翻四倍,适合大数据量的场景。这些等当前项目稳定之后再折腾,先把基础打牢。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询