1. 项目概述
在嵌入式系统开发中,处理器与外围芯片(如传感器、存储器、显示模块)之间的可靠数据交换是核心任务之一。串行外设接口(SPI)因其高速、全双工和硬件简单的特性,成为了这类短距离通信的首选方案。最近,我在一个基于TI TMS320F240 DSP的运动控制项目中,需要实现与一个高精度编码器接口芯片的实时数据同步。这个场景对通信的实时性和可靠性要求极高,任何数据丢失或延迟都可能导致控制环路失稳。经过一番折腾,我最终基于F240的SPI模块,打磨出了一套稳定高效的点对点通信与中断服务程序。今天,我就把从硬件连接到软件中断处理的完整实现过程,以及那些调试手册里不会写的“坑”和技巧,系统地分享出来。无论你是刚开始接触DSP的SPI,还是正在为通信稳定性头疼,相信这篇近万字的实战总结都能给你带来直接的帮助。
TMS320F240的SPI模块功能相当完整,支持主从模式、可编程时钟极性与相位、以及多种数据长度。但手册上往往只给出了寄存器描述,如何将这些寄存器配置组合起来,构建一个既能快速响应数据、又能稳健处理异常(如接收溢出)的通信链路,才是工程实践中的难点。本文将围绕点对点通信这一最基础的拓扑,深入拆解**中断服务例程(ISR)**的设计哲学、代码实现细节,并提供一个经过实际项目验证的、可直接复用的代码框架。
2. SPI通信核心原理与F240模块配置
在动手写代码之前,我们必须吃透SPI的工作原理和F240片上模块的特殊性。这决定了我们配置寄存器的每一个比特位,以及中断服务程序(ISR)的每一个操作步骤。
2.1 SPI总线工作原理与主从时序
SPI是一种同步、全双工的串行通信协议。它通常需要四根线:
- SCLK (Serial Clock):由主机产生的同步时钟信号。
- MOSI/SIMO (Master Out Slave In):主机输出、从机输入的数据线。
- MISO/SOMI (Master In Slave Out):主机输入、从机输出的数据线。
- SS/SPISTE (Slave Select):从机片选信号,低电平有效。在点对点通信中,此信号有时可接固定电平。
其核心是一个环形移位寄存器。通信开始时,主机通过SCLK时钟边沿,将自己移位寄存器中的数据一位一位地从MOSI线移出,同时从机也将自己寄存器中的数据从MISO线移出。经过8个或16个时钟周期后,主机和从机的移位寄存器内容完成交换。因此,一次SPI通信本质上是主机和从机交换了一个寄存器的值。
时钟极性(CPOL)和时钟相位(CPHA)共同定义了数据采样的边沿。这是SPI配置中最容易出错的地方:
- CPOL=0:时钟空闲时为低电平。
- CPOL=1:时钟空闲时为高电平。
- CPHA=0:数据在时钟的第一个边沿(对于CPOL=0是上升沿,对于CPOL=1是下降沿)被采样,在下一个边沿切换。
- CPHA=1:数据在时钟的第二个边沿被采样,在第一个边沿切换。
在我的项目中,编码器芯片要求CPOL=1, CPHA=1,即时钟空闲时为高电平,数据在时钟下降沿被采样。务必与外设数据手册严格对齐,否则读到的全是乱码。
2.2 TMS320F240 SPI模块寄存器精讲
F240的SPI通过一组内存映射寄存器控制。理解每个寄存器的作用是编写稳定驱动的基础。
SPICCR (SPI配置控制寄存器):
- 位7 (SW RESET):软件复位位。这是配置的起点和终点。任何对SPI的重大配置更改前,必须先置1复位模块,配置完成后再清零以启动模块。这是一个关键的安全操作,可以避免模块处于不确定状态。
- 位6 (CLOCK POLARITY):对应CPOL。决定了SPICLK引脚在空闲时的电平状态。
- 位5-0 (SPI CHAR3-0):设置字符长度(1到16位)。我们常用8位,即设置为
0111b(十进制7)。注意,写入SPICCR的值需要是(字符长度-1)。
SPICTL (SPI操作控制寄存器):
- 位4 (MASTER/SLAVE):主从模式选择。1为主机,0为从机。
- 位3 (TALK):发送使能。对于从机,必须使能TALK才能将数据输出到SOMI引脚,否则SOMI呈高阻态,主机将无法收到从机数据。这是一个常见的“主机能发,从机无回”问题的根源。
- 位2 (SPI INT ENA):SPI中断使能。我们通常开启它,利用中断处理数据,而非轮询。
- 位1-0 (CLOCK PHASE):对应CPHA。与SPICCR.6共同决定时序。
SPISTS (SPI状态寄存器):
- 位7 (RECEIVER OVERRUN FLAG):接收溢出标志。这是可靠性设计的重中之重。当CPU尚未读取SPIBUF(接收缓冲器)中的数据,而新的数据已接收完毕并准备覆盖SPIBUF时,此位置1。如果不处理,会导致数据丢失。在中断服务程序中,第一件事就是检查并清除此标志。
- 位6 (SPI INT FLAG):SPI中断标志。当一次数据传输完成(发送和接收同时完成)时,硬件置位。读取SPIBUF会自动清除此标志,这是硬件机制,无需软件操作。
- 位5 (TX BUF FULL FLAG):发送缓冲器满标志。当数据写入SPIDAT后置位,数据开始移位后清零。我们通常不依赖此标志,而是依靠SPI INT FLAG。
SPIBRR (SPI波特率寄存器):仅主机模式有效。设置SPI时钟波特率。计算公式为:当SPIBRR = 3 到 127时,波特率 = CLKOUT / (SPIBRR + 1)。需要根据从机最高时钟频率和系统时钟来设置。
SPIDAT (SPI数据寄存器):16位的移位寄存器。写入的数据会被移出,同时移入的数据会出现在该寄存器的高位(取决于字符长度)。向SPIDAT写入数据会立即启动一次传输(主机模式)或为下一次传输准备数据(从机模式)。
SPIBUF (SPI缓冲器):这是读取接收数据的关键。当一次传输完成,SPIDAT中接收到的数据会被自动复制到SPIBUF。读取SPIBUF的操作会清除SPISTS.6(中断标志)。因此,在中断服务程序中,我们必须读取SPIBUF,即使你不关心接收到的数据,也要读一下以清除中断标志,否则会陷入连续中断。
2.3 主从模式下的配置流程差异
主机初始化流程(关键顺序): 配置主机的顺序尤为重要,错误的顺序可能导致在SPI模块激活瞬间产生多余的时钟边沿,干扰从机。
- 将SPICCR的SW RESET置1,复位模块。
- 配置SPICCR,包括时钟极性(CPOL)和字符长度。此时SPICLK引脚为输入,状态由CPOL决定。
- 配置SPIPC1寄存器,将SPICLK引脚功能设置为SPI(而非通用IO)。
- 此时,SPICLK引脚变为输出,并输出由CPOL决定的空闲电平。
- 配置SPICTL,设置为主机模式、时钟相位、使能TALK和中断。
- 配置SPIBRR,设置波特率。
- 将SPICCR的SW RESET清零,释放复位,SPI模块开始工作。
- 向SPIDAT写入第一个数据,启动传输。
从机初始化流程: 从机配置相对简单,因为它不产生时钟。
- 将SPICCR的SW RESET置1。
- 配置SPICCR(时钟极性、字符长度)。
- 配置SPICTL,设置为从机模式、时钟相位、使能TALK(如果需要发送数据)和中断。
- 将SPICCR的SW RESET清零。
- 如果需要主动发送数据,预先写入SPIDAT。从机的传输由主机时钟触发。
注意:在从机模式下,即使你不打算发送数据,只是接收,有时也需要使能TALK并向SPIDAT写入一个哑元数据(如0xFF),因为SPI是全双工的,主机在接收数据的同时,从机也必须移出一些东西。否则主机的MISO线可能处于不确定状态。
3. 中断系统架构与GISR/SISR设计模式
在实时性要求高的嵌入式系统中,使用中断而非轮询来处理SPI通信是必然选择。F240的中断系统分为两级:外设级中断(如SPI)和CPU内核级中断(INT1-INT6)。SPI中断属于外设中断,它需要被映射到某一个CPU内核中断线上(通常是低优先级的INT5或高优先级的INT1)。我们的代码需要处理这两级中断。
3.1 中断向量表与GISR的三种方法
CPU中断向量表位于内存特定区域(如0x0000-0x003F)。当INT5中断发生时,CPU会跳转到向量表中INT5对应的地址执行。我们需要在这个地址放置一条跳转指令,指向我们的中断服务程序入口。
根据系统中同时使用的中断源数量,有三种组织通用中断服务例程(GISR)的方法:
方法1:单一外设中断如果INT5中断线上只连接了SPI这一个外设中断源,那么事情最简单。CPU向量表直接跳转到SPI_ISR。
.sect "vectors" RESET B START ; 复位向量 B PHANTOM ; INT1 未用 B PHANTOM ; INT2 未用 B PHANTOM ; INT3 未用 B PHANTOM ; INT4 未用 B SPI_ISR ; INT5, SPI中断这种方法下,SPI_ISR需要自己完成上下文保存和恢复。
方法2:多外设中断,软件查询如果INT5上挂载了多个外设(例如SPI和SCI),那么CPU向量表跳转到一个通用的GISR5。GISR5首先保存上下文,然后依次读取外设中断向量寄存器(PIVR)和外设中断标志,判断是哪个外设产生的中断,再跳转到对应的特定中断服务例程(SISR),如SPI_ISR或SCI_ISR。最后恢复上下文返回。这是最通用、最标准的方法。
方法3:条件性直接跳转这是原文中提到的一种优化情况。如果SPI是INT5上唯一活跃的中断,即使SCI也挂在INT5上,但只要将SCI配置为使用INT1中断,那么对于INT5来说,SPI仍然是唯一的中断源。此时可以像方法1一样,CPU向量表直接跳转到SPI_ISR,省去了GISR5中的查询跳转开销,减少了中断延迟。
在我的项目中,系统较为简单,INT5只用于SPI,因此我采用了方法1。代码简洁,中断响应最快。
3.2 特定中断服务例程(SISR)的黄金法则
SPI_ISR作为SISR,其设计必须遵循几个核心原则,以确保快速、正确、可重入。
上下文保存与恢复(Context Save/Restore): 中断会打断任何正在执行的程序。为了在中断返回后能让被中断的程序无缝继续运行,必须保存和恢复所有被中断程序使用的寄存器。对于F240,这至少包括:
- 累加器ACC
- 辅助寄存器ARx(常用AR6, AR7作为临时寄存器)
- 状态寄存器ST0, ST1
- 当前数据页DP 保存时,通常压入软件堆栈或指定的内存区域。恢复时按相反顺序弹出。这是中断服务程序不可省略的“开场白”和“结束语”。
第一时间读取SPIBUF: 如前所述,读取SPIBUF有两个关键作用:获取接收到的数据,以及清除SPI中断标志(SPISTS.6)。如果忘记读取,中断标志会一直保持,导致CPU不断进入中断,形成“中断风暴”,系统瞬间崩溃。因此,在保存上下文后,应尽快执行
LACC SPIBUF。溢出(Overrun)检测与处理: 这是通信可靠性的“守门员”。在读取SPIBUF之前或之后,应立即检查SPISTS.7(溢出标志)。如果置位,说明上一帧数据还未被主程序取走,新数据就来了,导致旧数据被覆盖。处理策略通常是:
- 清除溢出标志(向SPISTS写入0即可)。
- 丢弃当前SPIBUF中的数据(因为可能已损坏),或者根据应用需求做特殊处理(如记录错误计数)。
- 直接恢复上下文并返回,不再执行本次中断的正常数据处理流程。这样可以避免基于错误数据进行后续操作。
为下一次传输准备数据: SPI是全双工且自动连续的。当本次传输完成中断发生时,主机和从机已经交换了数据。为了在下一个时钟周期到来时能发送新的数据,必须在本次中断结束前,将待发送的数据写入SPIDAT。对于从机,这就是把要回复给主机的数据写入;对于主机,则是写入下一帧要发送的数据。这是一个“预加载”机制。
中断使能位的操作: 在ISR内部,有时需要临时屏蔽自身中断(例如,正在处理接收缓冲区时,不希望被新的接收中断打断)。可以通过操作中断屏蔽寄存器(IMR)的对应位来实现。但需注意,在中断服务程序内部,CPU会自动置位INTM(全局中断屏蔽),此时即使IMR对应位使能,也不会响应新的同级或低级中断。通常我们在ISR开始保存上下文后,就可以安全地操作IMR来管理外设中断了。
4. 点对点通信实战:从机代码逐行解析
下面,我将结合一个实际的从机代码示例,详细拆解每一个步骤。这个从机需要响应主机的三种命令:发送新参数并接收状态、仅发送新参数、仅请求状态信息。
4.1 系统与SPI初始化
初始化是稳定的基石。代码从START标签开始。
START: CLRC SXM ; 清除符号扩展模式,简化计算 SPLK #06Fh, WDCR ; 看门狗控制:清除标志、禁用看门狗、设置溢出时间 SPLK #07h, RTICR ; 实时中断控制:清除标志、设置溢出时间 ; 系统时钟配置(根据外部晶振频率) SPLK #00B1h, CKCR1 ; PLL配置,倍频 SPLK #00C3h, CKCR0 ; 系统时钟分频 LACL SYSSR ; 清除系统状态寄存器中的复位标志位 AND #00FFh SACL SYSSR接下来初始化应用变量和缓冲区指针。DATA_IN是接收缓冲区,DATA_OUT是发送缓冲区。我使用指针DATA_IN_PTR和DATA_OUT_PTR来管理这些循环缓冲区。
LDP #DATA_IN_PTR SPLK #DATA_IN, DATA_IN_PTR ; 接收缓冲区指针指向起始位置 SPLK #DATA_OUT, DATA_OUT_PTR ; 发送缓冲区指针指向起始位置 SPLK #0FFH, DATA_OUT ; 初始化默认发送字节为0xFF(ACK确认) SPLK #01h, NEW_BYTE_FLAG ; 设置新字节接收标志 SPLK #0, COPIED ; 状态信息复制标志清零然后调用INIT_SPI子程序初始化SPI模块为从机模式。
INIT_SPI: LDP #DP_PF1 ; 指向外设帧1数据页 SPLK #00C7h, SPICCR ; 关键!先写SW RESET=1复位SPI。同时设CPOL=1,字符长度8位(0x07) SPLK #0000h, SPICTL ; 临时禁用中断和TALK,从机模式 SPLK #0040h, SPIPRI ; 设置SPI中断为低优先级(映射到CPU INT5) SPLK #0000h, SPISTS ; 清除SPI状态寄存器标志 SPLK #0013h, SPICTL ; 使能TALK,使能SPI中断,时钟相位CPHA=0,从机模式 SPLK #0002h, SPIPC1 ; 配置SPICLK引脚功能(根据硬件连接) SPLK #0022h, SPIPC2 ; 配置SIMO和SOMI引脚为SPI功能 SPLK #0047h, SPICCR ; 关键!释放软件复位(SWRST=0),保持其他配置,启动SPI RET实操心得:SPI初始化的顺序,特别是
SPICCR的两次写入(先复位,后释放),以及SPICTL中TALK位的使能时机,必须严格按照数据手册的推荐流程。我曾因为先使能TALK后配置引脚功能,导致SOMI引脚输出异常,浪费了半天调试时间。
4.2 主循环与消息读取函数
初始化完成后,开启全局中断,进入主循环。主循环不做繁重工作,只是等待标志位。
CLRC INTM ; 开启全局中断 MAIN: LDP #NEW_BYTE_FLAG MAR *, AR5 LAR AR5, NEW_BYTE_FLAG ; AR5 = NEW_BYTE_FLAG值 BANZ MAIN,* ; 如果NEW_BYTE_FLAG为0,则循环等待;否则跳转NEW_BYTE_FLAG是SPI_ISR和主程序RD_MSG之间的握手信号。当SPI_ISR收到一个完整字节并存入缓冲区后,会将此标志清零,从而触发BANZ指令跳出循环,执行RD_MSG。
RD_MSG函数是应用层协议解析的核心。它首先屏蔽SPI中断,防止在处理过程中SPI_ISR修改缓冲区指针,造成数据错乱。
RD_MSG: LDP #IMR/128 LACL IMR AND #0FFEFh ; 屏蔽SPI中断(INT5) SACL IMR LDP #NEW_BYTE_FLAG SPLK #1, NEW_BYTE_FLAG ; 重置标志,为下一次接收做准备 ; ... (协议解析代码) LDP #IMR/128 ; 解析完成后,重新使能SPI中断 LACL IMR OR #010h SACL IMR协议解析部分根据消息类型字节(接收到的第一个字节)进行分支:
- 状态请求:如果主机只请求状态,则从机将状态信息(预先存储在
STATUS_INFO区域)复制到DATA_OUT发送缓冲区。这里使用COPIED标志确保在一次请求中只复制一次状态信息,避免重复发送。 - 参数下发:如果消息包含新参数,则在校验和验证通过后,将参数从接收缓冲区
DATA_IN复制到应用变量区(如PARAMS)。 - 校验和请求:主机在发送完自己的消息后,可能会发送特定的“校验和请求”字节(如0x08),以“填充”时钟,从而读取从机的回复。
RD_MSG识别到此字节后直接丢弃,不进行任何处理。
校验和计算是通信可靠性的关键一环。我采用最简单的字节累加和(仅取低8位)的方式:
VERIFY_CHECKSUM: LACL *- ; 获取接收到的校验和字节 SUB #02H ; 调整指针,准备计算 SACL VAR1 ; VAR1 = 需要累加的字节数 LACC #0 ; 累加器清零 RPT VAR1 ; 重复执行下一条指令(VAR1+1)次 ADD *+ ; 累加接收缓冲区中的数据字节 AND #0FFh ; 只取低8位 SACL VAR1 ; 保存计算出的校验和 SUB * ; 与接收到的校验和字节比较 BCND STORE_CHECKSUM, EQ ; 相等则跳转 LACL #INVALID_CHKSUM ; 不相等,加载无效校验和标识(如0x04) STORE_CHECKSUM: ; 将校验结果(0x00或0x04)存入DATA_OUT缓冲区,待下次中断发送给主机注意事项:
RPT指令后的ADD *+会被执行N+1次,其中N是VAR1的值。因此,在计算需要累加的字节数时(VAR1),要特别注意是“字节数”还是“循环次数”。这里SUB #02H的调整就是为了让循环次数等于参数字节数。这是汇编编程中常见的“差一错误”(Off-by-one error)易发点。
4.3 中断服务例程SPI_ISR深度剖析
这是整个通信系统的引擎。我们逐段分析一个健壮的SPI_ISR实现。
第一部分:上下文保存
SPI_ISR: SST #0, ST0_TEMP ; 保存状态寄存器ST0 SST #1, ST1_TEMP ; 保存状态寄存器ST1 LDP #CONTEXT_MEM_PTR ; 切换到上下文保存区的数据页 SACL CONTEXT_MEM_PTR ; 保存累加器ACC低16位 SACH CONTEXT_MEM_PTR+1 ; 保存累加器ACC高16位 SAR AR7, CONTEXT_MEM_PTR+2 ; 保存AR7 SAR AR6, CONTEXT_MEM_PTR+3 ; 保存AR6 LDP #IMR/128 LACL IMR LDP #CONTEXT_MEM_PTR SACL CONTEXT_MEM_PTR+4 ; 保存当前IMR值(可选)我将上下文保存在一块固定的内存区域CONTEXT_MEM。保存IMR是为了在复杂的中断嵌套环境中能精确恢复中断屏蔽状态,在简单的点对点通信中非必需。
第二部分:溢出检测与处理
OVER_RUN: LDP #DP_PF1 ; 指向包含SPI寄存器的数据页 BIT SPISTS, 8 ; 测试SPISTS.7(溢出标志位) BCND CLEAR_FLAG, TC ; 如果置位,跳转到清除流程 ; 无溢出,继续正常处理 B NEXT_TX CLEAR_FLAG: SPLK #0H, SPISTS ; 向SPISTS写0即可清除溢出标志 B SPI_DONE ; 跳转到上下文恢复,不处理本次数据这是最重要的安全阀。一旦检测到溢出,意味着上一帧数据应用程序还没取走,SPIBUF就被新数据覆盖了。此时最安全的做法是清除标志,丢弃当前帧(不读取SPIBUF),直接退出中断。你可以增加一个错误计数器,以便在调试时发现此类问题。
第三部分:加载下一次发送数据
NEXT_TX: LDP #DATA_OUT_PTR LAR AR6, DATA_OUT_PTR ; AR6指向待发送数据缓冲区 MAR *,AR6 LACL *- ; 从缓冲区取一个字节到ACC LDP #DP_PF1 SACL SPIDAT ; 将数据写入SPIDAT,为**下一次**传输准备 LDP #DATA_OUT_PTR LACC #DATA_OUT SUB DATA_OUT_PTR BCND READ_SPI, EQ ; 如果指针回到缓冲区起点,说明发完了,不再递增指针 SAR AR6, DATA_OUT_PTR ; 更新发送缓冲区指针关键点:SPIDAT的写入操作,加载的是接下来要发送的数据,而不是对刚刚完成的那次传输的响应。本次传输收到的数据还在SPIBUF里。这个“预加载”机制是SPI全双工连续通信的基础。代码中还包含了一个判断:如果发送指针已经回到缓冲区起始点(DATA_OUT),说明所有预定数据已发送完毕,此时指针不再递增,下次中断会继续发送DATA_OUT处的默认字节(通常是ACK的0xFF)。
第四部分:读取本次接收数据并触发处理
READ_SPI: LDP #DP_PF1 LACC SPIBUF ; **读取SPIBUF!清除中断标志,获取数据** LDP #DATA_IN_PTR LAR AR7, DATA_IN_PTR ; AR7指向接收缓冲区 MAR *, AR7 SACL *+ ; 将接收到的字节存入缓冲区,指针后移 SAR AR7, DATA_IN_PTR ; 保存更新后的接收缓冲区指针 LDP #NEW_BYTE_FLAG SPLK #0, NEW_BYTE_FLAG ; **清零标志,通知主循环RD_MSG有新数据待处理**LACC SPIBUF一举两得。NEW_BYTE_FLAG清零是通知机制的核心。
第五部分:上下文恢复与返回
SPI_DONE: LDP #CONTEXT_MEM_PTR ; ... 按照保存的相反顺序,恢复AR6, AR7, ACC, ST1, ST0 ... LST #1, ST1_TEMP LST #0, ST0_TEMP CLRC INTM ; 清除中断屏蔽位(在ISR末尾,CPU会执行RET,有些编译器会自动处理INTM) RET ; 中断返回恢复上下文后,用CLRC INTM重新开启全局中断(如果之前被硬件自动关闭),然后RET返回。RET指令会从堆栈恢复程序计数器,CPU继续执行被中断的任务。
5. 主机端实现与联合调试技巧
从机代码稳定后,需要主机来配合测试。主机可以是另一个F240,也可以是其他MCU。这里以F240作为主机为例,说明关键差异和调试方法。
5.1 主机初始化与数据传输触发
主机初始化流程在2.3节已简述,顺序要求严格。主机的SPI_ISR逻辑与从机类似,但通常更简单,因为它主导通信流程。主机通常在需要发送数据时(例如,响应定时器或外部事件),主动向SPIDAT写入第一个字节来启动传输。
在我的测试代码中,我使用了一个外部中断(XINT1,连接到一个按钮)来手动触发一次消息发送序列,这非常利于调试。
XINT1_ISR: ; 外部中断1服务程序 ; 保存上下文... LDP #TEST_MSG_PTR LAR AR6, TEST_MSG_PTR MAR *, AR6 LACL *+ ; 从测试消息数组中取一个字节 SAR AR6, TEST_MSG_PTR ; 更新指针 LDP #DP_PF1 SACL SPIDAT ; 写入SPIDAT,启动SPI传输! ; ... 去抖动处理,清除中断标志 ... ; 恢复上下文并返回主机写入SPIDAT后,时钟自动产生,数据开始移出。同时,主机的SPI_ISR也会在传输完成后被触发,用于读取从机的回复并准备下一个要发送的字节。
5.2 通信协议与消息流设计
一个健壮的通信需要简单的应用层协议。我设计了三种消息类型,用第一个字节(消息类型字节)的低两位标识:
- 位0=1:消息包含主机下发的参数。
- 位1=1:消息是主机请求从机状态。
- 位0和位1都=1:既下发参数,也请求状态。
消息基本格式为:[类型][长度][数据...][校验和]。 例如,主机发送参数:[0x03][0x06][0x02][0x03][0x01][0x0F][校验和]。 从机回复:对于参数消息,从机计算校验和后,回复0x00(成功)或0x04(失败)。对于状态请求,从机回复状态数据+校验和状态。
处理消息长度不匹配是一个关键技巧。SPI是全双工,主从机必须交换相同数量的字节。如果主机消息长,从机消息短,从机需要在发完自己的数据后,继续发送“填充字节”(如0xFF,即ACK),直到主机传输结束。反之,如果主机消息短,从机消息长,主机在发完自己的数据后,需要发送“哑元请求字节”(如0x08,即校验和请求),以提供时钟来读取从机的剩余数据。代码中的RD_MSG函数对0x08字节的特殊处理,以及SPI_ISR中发送默认0xFF的机制,正是为了应对这种情况。
5.3 调试实战与常见问题排查
调试SPI通信,逻辑分析仪或示波器是必备工具。以下是我踩过的坑和解决方法:
问题:主机能发送,示波器看到时钟和数据,但从机无反应,收不到中断。
- 排查:首先检查硬件连接,SCLK、MOSI、MISO是否交叉连接(主机MOSI接从机MOSI?不对!应该是主机MOSI接从机MISO,主机MISO接从机MOSI)。这是最容易接错的地方。
- 排查:检查从机SPICTL的
TALK位是否使能。如果从机不需要发送数据,也要使能TALK并预写一个数据(如0xFF)到SPIDAT,否则主机的MISO线可能浮空。 - 排查:检查双方CPOL和CPHA设置是否完全一致。用示波器测量SCLK和MOSI的时序,对照数据手册看采样边沿是否正确。
问题:能进入一次中断,但之后卡死,或进入中断风暴。
- 排查:百分之九十的原因是忘记在ISR中读取SPIBUF。读取SPIBUF是清除SPI中断标志(SPISTS.6)的唯一方法。不读取,标志位一直为1,导致CPU不断响应中断。在ISR开始处加一句
LACC SPIBUF,即使你不用这个数据,也要读出来。 - 排查:检查中断向量表配置是否正确。确认CPU的INT5向量是否准确跳转到了你的
SPI_ISR入口。 - 排查:在ISR末尾,是否正确地恢复了上下文并使用了
RET指令返回?错误的堆栈操作会导致返回地址错误,程序跑飞。
- 排查:百分之九十的原因是忘记在ISR中读取SPIBUF。读取SPIBUF是清除SPI中断标志(SPISTS.6)的唯一方法。不读取,标志位一直为1,导致CPU不断响应中断。在ISR开始处加一句
问题:数据偶尔出错,特别是多字节传输时。
- 排查:检查溢出标志。在ISR中第一件事就是检查并处理SPISTS.7。数据出错很可能是因为主程序处理速度跟不上SPI接收速度,导致溢出发生,数据被覆盖。
- 排查:提高
RD_MSG函数的执行效率,或者增大接收缓冲区。确保NEW_BYTE_FLAG被清零后,主循环能尽快调用RD_MSG处理数据。 - 排查:降低SPI波特率。过高的波特率可能导致在中断服务程序执行期间,下一个字节已经传输完成,从而增加溢出风险。尤其是在使用较低系统时钟频率时。
问题:主机发送正常,但从机回复的数据主机读回来全是0xFF或固定值。
- 排查:检查从机
SPI_ISR中的NEXT_TX部分。确认DATA_OUT_PTR指针管理逻辑正确,是否在发送完有效数据后,指针没有正确复位或指向了错误的数据。主机读到的固定值很可能就是从机DATA_OUT缓冲区起始位置的默认值。 - 排查:用示波器同时测量主机的MISO线和从机的SOMI线,看从机是否确实输出了期望的数据波形。如果没有,回头检查从机
SPIDAT的写入操作和TALK使能。
- 排查:检查从机
问题:通信几轮后突然停止。
- 排查:检查发送和接收缓冲区的指针管理。在
RD_MSG和SPI_ISR中,指针的增加和重置逻辑必须严密,防止指针越界,覆盖了其他内存区域。可以在指针更新后加入边界检查断言。 - 排查:在
SPI_ISR和RD_MSG中操作同一缓冲区时,是否正确地用IMR屏蔽了SPI中断?如果不屏蔽,SPI_ISR可能在RD_MSG修改指针的过程中触发,导致指针处于不一致状态。
- 排查:检查发送和接收缓冲区的指针管理。在
调试时,可以在关键位置设置IO引脚电平翻转,用示波器观察程序执行流程和时间。例如,在SPI_ISR入口和出口翻转一个GPIO,可以直观看到中断频率和耗时,判断是否发生中断风暴。