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配置中最容易出错的地方CPOL0时钟空闲时为低电平。CPOL1时钟空闲时为高电平。CPHA0数据在时钟的第一个边沿对于CPOL0是上升沿对于CPOL1是下降沿被采样在下一个边沿切换。CPHA1数据在时钟的第二个边沿被采样在第一个边沿切换。在我的项目中编码器芯片要求CPOL1 CPHA1即时钟空闲时为高电平数据在时钟下降沿被采样。务必与外设数据手册严格对齐否则读到的全是乱码。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 ; 初始化默认发送字节为0xFFACK确认 SPLK #01h, NEW_BYTE_FLAG ; 设置新字节接收标志 SPLK #0, COPIED ; 状态信息复制标志清零然后调用INIT_SPI子程序初始化SPI模块为从机模式。INIT_SPI: LDP #DP_PF1 ; 指向外设帧1数据页 SPLK #00C7h, SPICCR ; 关键先写SW RESET1复位SPI。同时设CPOL1字符长度8位(0x07) SPLK #0000h, SPICTL ; 临时禁用中断和TALK从机模式 SPLK #0040h, SPIPRI ; 设置SPI中断为低优先级映射到CPU INT5 SPLK #0000h, SPISTS ; 清除SPI状态寄存器标志 SPLK #0013h, SPICTL ; 使能TALK使能SPI中断时钟相位CPHA0从机模式 SPLK #0002h, SPIPC1 ; 配置SPICLK引脚功能根据硬件连接 SPLK #0022h, SPIPC2 ; 配置SIMO和SOMI引脚为SPI功能 SPLK #0047h, SPICCR ; 关键释放软件复位(SWRST0)保持其他配置启动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 ; 重复执行下一条指令(VAR11)次 ADD * ; 累加接收缓冲区中的数据字节 AND #0FFh ; 只取低8位 SACL VAR1 ; 保存计算出的校验和 SUB * ; 与接收到的校验和字节比较 BCND STORE_CHECKSUM, EQ ; 相等则跳转 LACL #INVALID_CHKSUM ; 不相等加载无效校验和标识(如0x04) STORE_CHECKSUM: ; 将校验结果(0x00或0x04)存入DATA_OUT缓冲区待下次中断发送给主机注意事项RPT指令后的ADD *会被执行N1次其中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_PTR1 ; 保存累加器ACC高16位 SAR AR7, CONTEXT_MEM_PTR2 ; 保存AR7 SAR AR6, CONTEXT_MEM_PTR3 ; 保存AR6 LDP #IMR/128 LACL IMR LDP #CONTEXT_MEM_PTR SACL CONTEXT_MEM_PTR4 ; 保存当前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 通信协议与消息流设计一个健壮的通信需要简单的应用层协议。我设计了三种消息类型用第一个字节消息类型字节的低两位标识位01消息包含主机下发的参数。位11消息是主机请求从机状态。位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中第一件事就是检查并处理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可以直观看到中断频率和耗时判断是否发生中断风暴。