1. 项目缘起为什么要在FPGA和STM32之间折腾SPI最近在做一个嵌入式数据采集系统的原型验证遇到了一个挺有意思的挑战前端的高速ADC采样数据由FPGA实时接收并做初步处理但后续的复杂算法、数据打包和网络上传任务又需要一颗灵活且生态成熟的MCU来完成。这个架构很常见对吧FPGA负责“硬”实时和高速并行MCU负责“软”逻辑和上层协议。但问题来了这两者之间怎么高效、可靠地“对话”最直接的方案就是SPI。它简单、全双工、速度够快而且FPGA和STM32都原生支持。听起来像是把线一连配置一下就能跑起来的事儿。但真上手了才发现从“能通信”到“稳定、高效、不出错地通信”中间隔着一堆细节和坑。特别是当FPGA作为主机MasterSTM32作为从机Slave时时序的主动权在FPGA手里STM32处于被动响应状态这种主从关系下的协同比两个MCU对等通信要更考验设计。网上能找到的例程要么是STM32作主机的要么是讲个大概原理。关于FPGA作为SPI主机特别是如何模拟一个“听话”又“健壮”的SPI主机去驱动STM32从机并处理各种边界情况的完整实践并不多。所以我把这次从零搭建、调试到最终稳定的全过程梳理出来重点不是讲SPI协议本身而是分享在这种异构架构下让两者默契配合的那些设计思路、调试方法和避坑经验。2. 通信框架设计主从角色的核心差异与设计要点在开始写代码之前必须把通信的“规矩”定好。FPGA作为主机意味着它要掌控通信的发起、时钟的生成和结束。STM32作为从机则必须严格遵循主机发出的时钟和片选信号进行响应。这个根本性的差异决定了我们双方的设计重点完全不同。2.1 FPGA侧设计哲学做一个“体贴”的主机FPGA这边的SPI控制器需要我们用硬件描述语言比如Verilog从头实现。我们的目标不是实现一个最简功能的SPI而是实现一个对从机“友好”的、易于上层逻辑控制的SPI主机。首先通信时序必须绝对规范。SPI有四种模式CPOL和CPHA的组合我选择的是最常用的Mode 0CPOL0 CPHA0和Mode 3CPOL1 CPHA1。这里我以Mode 0为例说明时钟空闲时为低电平在时钟的上升沿采样数据下降沿更新数据。在FPGA中我们需要一个状态机来精确控制片选CS_N的拉低、时钟SCLK的生成、数据MOSI的移出以及从机数据MISO的采样。一个常见的错误是SCLK的占空比不稳定或者在数据位之间插入不必要的毛刺这都会导致STM32采样错误。其次必须设计好流控接口。一个简单的SPI发送模块可能只有一个“发送使能”和“发送数据”接口。但在实际系统中FPGA往往需要发送不定长的数据包或者等待STM32处理完上一包数据后再发送下一包。因此我设计了一个基于FIFO先入先出队列的接口。上层逻辑只需将数据写入FIFOSPI主控状态机自动从FIFO中读取数据并发送。同时状态机还会输出“繁忙”、“发送完成”、“FIFO空/满”等状态信号方便上层逻辑进行流控。例如当检测到STM32通过某种方式比如拉高某个GPIO表示“缓冲区满”时上层逻辑就暂停向FIFO写数据。再者要加入超时和错误恢复机制。理想情况下一次传输应该在预定数量的时钟后结束。但如果STM32卡死或线路干扰导致MISO线异常FPGA的接收状态可能会挂起。因此我的设计里包含了一个计数器在一次传输启动后如果超过预期时钟周期数比如数据位宽*2仍未收到“传输完成”信号则强制结束本次传输拉高片选并产生一个错误标志通知系统进行干预或重试。2.2 STM32侧配置要点当好一个“本分”的从机STM32这边相对“省心”因为ST的HAL库或LL库已经提供了成熟的SPI从机驱动。但“省心”不代表可以随意配置几个关键点配置错了通信必然失败。第一SPI从机模式的初始化必须与主机严格匹配。这包括数据大小Data Size 通常是8位或16位、时钟极性相位CPOL/CPHA、数据顺序MSB/LSB First。这些参数必须和FPGA侧生成的时序一模一样。我遇到过因为STM32默认是MSB先行而FPGA代码写成了LSB先行导致数据位完全反序的坑。第二注意从机的数据准备与响应时机。在从机模式下STM32无法主动发起传输。它必须在片选有效后随时准备好在主机时钟的驱动下接收和发送数据。通常我们使用中断或DMA来响应。对于小数据量或低速通信用中断模式HAL_SPI_TransmitReceive_IT即可。当主机开始产生时钟STM32的SPI外设会自动接收数据并触发中断在中断服务程序里我们可以读取接收到的数据并准备下一字节要发送的数据如果需要回传。第三充分利用STM32的硬件NSS片选管理。STM32的SPI外设可以配置为硬件NSS模式即片选引脚由硬件自动管理。当FPGA拉低该引脚时STM32的SPI外设自动进入从机就绪状态拉高时自动复位内部状态。这比用软件GPIO模拟片选响应要可靠和及时得多强烈建议启用。需要确保在CubeMX中正确配置NSS引脚为“硬件输入”模式。第四DMA是提升性能和处理大数据的关键。如果FPGA需要连续发送大量数据比如一帧图像使用中断每个字节都处理一次开销太大可能导致数据丢失。此时必须启用SPI的DMA传输。在从机模式下配置DMA接收可以实现在FPGA连续发送时钟时STM32后台自动将数据搬运到指定的内存缓冲区完全不需要CPU干预。等一帧数据收满再触发DMA传输完成中断通知CPU来处理整个缓冲区。这是实现高速稳定传输的标配。3. 协议层设计让数据流动变得有意义物理层和链路层打通了只能保证比特流能过去。但要真正完成“通信”我们必须定义一套双方都能理解的“语言”这就是应用层协议。对于FPGA和STM32之间的SPI通信我设计了一个简单但足够健壮的“帧协议”。3.1 帧结构定义一个完整的通信帧由以下几部分组成帧头Header1-2个字节的固定值如0xAA、0x55。用于在数据流中标识一帧的开始。FPGA在发送有效数据前先发送帧头。STM32在接收时持续检测数据流只有收到正确的帧头序列才认为一帧开始此后的数据存入缓冲区。长度域Length1-2个字节指明本帧中“数据载荷”部分的字节数。这解决了变长数据包的传输问题。STM32收到长度后就知道还要接收多少字节才算一帧结束。命令/类型域Command/Type1个字节指示这帧数据的用途。例如0x01代表传感器数据0x02代表FPGA发给STM32的配置参数0x03代表STM32对上一命令的应答。数据载荷Data Payload实际要传输的数据长度由“长度域”指定。校验和Checksum1个字节通常是对帧头、长度、命令、数据载荷所有字节进行累加和或CRC8计算的结果。STM32收到完整帧后自己计算一遍校验和与帧尾的校验和比对。如果不一致则丢弃该帧并通过其他方式如后续会提到的通知FPGA重发。一个典型的帧看起来像这样[0xAA][长度L][命令C][数据1]...[数据L][校验和]。3.2 双向通信与流控机制SPI是全双工的意味着主机发送数据的同时从机也在回送数据。我们可以利用这一点实现简单的应答机制。FPGA - STM32下行FPGA发送的“命令帧”中数据载荷可以是具体的指令或数据。STM32收到并校验正确后可以在紧接着的下一次SPI传输中FPGA再次发起通信时通过MISO线回送一个“应答帧”。应答帧可以很简单比如一个状态字节0x00表示成功接收0x01表示校验错误请求重发0xFF表示缓冲区满请暂停发送。STM32 - FPGA上行当STM32有数据要主动上报给FPGA时比如处理完的数据包它无法主动发起SPI传输。这时需要一种“敲门”机制。我的做法是让STM32控制一个GPIO引脚比如IRQ_PIN。当STM32准备好数据后拉低这个引脚。FPGA端持续监控这个引脚一旦发现其被拉低就主动发起一次SPI读取操作。在这次操作中FPGA发送一个“读取请求”命令帧可能数据载荷为空STM32则在主机时钟驱动下将准备好的数据通过MISO发送出去。发送完成后STM32再拉高IRQ_PIN。这种“主机轮询从机中断提示”的方式巧妙地解决了SPI从机无法主动发起通信的问题实现了双向数据流控。4. FPGA端SPI主机模块的Verilog实现细节理论说完了来看干货。以下是我用Verilog实现的一个增强型SPI主机模块的核心部分。它包含一个清晰的状态机并集成了前面提到的流控和超时机制。module spi_master #( parameter DATA_WIDTH 8, // 数据位宽 parameter CPOL 1b0, // 时钟极性 parameter CPHA 1b0, // 时钟相位 parameter CLK_DIV 10 // 系统时钟分频系数决定SCLK频率 )( input wire clk, // 系统时钟 input wire rst_n, // 异步复位低有效 // 用户控制接口 input wire start_i, // 启动一次传输脉冲 input wire [DATA_WIDTH-1:0] data_tx_i, // 待发送数据 output reg [DATA_WIDTH-1:0] data_rx_o, // 接收到的数据 output reg busy_o, // 模块忙标志 output reg done_o, // 单次传输完成脉冲 output reg error_o, // 传输错误标志如超时 // SPI物理接口 output reg sclk_o, output reg mosi_o, input wire miso_i, output reg cs_n_o ); // 状态定义 localparam S_IDLE 3d0; localparam S_ASSERT_CS 3d1; localparam S_CLK_LOW 3d2; localparam S_CLK_HIGH 3d3; localparam S_DEASSERT_CS 3d4; localparam S_ERROR 3d5; reg [2:0] state_r, next_state; reg [7:0] clk_cnt; // 时钟分频计数器 reg [3:0] bit_cnt; // 位计数器根据DATA_WIDTH调整宽度 reg [DATA_WIDTH-1:0] shift_tx_r; // 发送移位寄存器 reg [DATA_WIDTH-1:0] shift_rx_r; // 接收移位寄存器 reg [15:0] timeout_cnt; // 超时计数器 reg sclk_en; // SCLK使能信号 // 时钟分频与SCLK生成 always (posedge clk or negedge rst_n) begin if (!rst_n) begin clk_cnt 8d0; sclk_o CPOL; // 空闲状态时钟电平 end else begin if (state_r ! S_IDLE) begin if (clk_cnt CLK_DIV - 1) begin clk_cnt 8d0; sclk_o ~sclk_o; // 翻转SCLK end else begin clk_cnt clk_cnt 1b1; end end else begin clk_cnt 8d0; sclk_o CPOL; // 空闲时回到空闲电平 end end end // 主状态机 always (posedge clk or negedge rst_n) begin if (!rst_n) begin state_r S_IDLE; busy_o 1b0; done_o 1b0; error_o 1b0; cs_n_o 1b1; bit_cnt 0; shift_tx_r 0; shift_rx_r 0; data_rx_o 0; timeout_cnt 0; end else begin done_o 1b0; // 完成脉冲默认清零 error_o 1b0; case (state_r) S_IDLE: begin cs_n_o 1b1; busy_o 1b0; if (start_i) begin state_r S_ASSERT_CS; shift_tx_r data_tx_i; // 锁存待发送数据 bit_cnt DATA_WIDTH; // 初始化位计数器 timeout_cnt 16hFFFF; // 初始化超时计数器 end end S_ASSERT_CS: begin cs_n_o 1b0; // 拉低片选 busy_o 1b1; // 等待至少半个SCLK周期满足从机建立时间 if (clk_cnt (CLK_DIV1)) begin state_r (CPHA 0) ? S_CLK_HIGH : S_CLK_LOW; end timeout_cnt timeout_cnt - 1b1; if (timeout_cnt 0) state_r S_ERROR; end S_CLK_LOW: begin mosi_o shift_tx_r[DATA_WIDTH-1]; // 在时钟低电平时更新数据(Mode 0) if (sclk_o 1b1 clk_cnt 0) begin // 检测SCLK下降沿 state_r S_CLK_HIGH; end timeout_cnt timeout_cnt - 1b1; if (timeout_cnt 0) state_r S_ERROR; end S_CLK_HIGH: begin if (sclk_o 1b0 clk_cnt 0) begin // 检测SCLK上升沿 shift_rx_r {shift_rx_r[DATA_WIDTH-2:0], miso_i}; // 上升沿采样 shift_tx_r {shift_tx_r[DATA_WIDTH-2:0], 1b0}; // 左移发送数据 bit_cnt bit_cnt - 1b1; if (bit_cnt 1) begin // 所有位传输完毕 state_r S_DEASSERT_CS; end else begin state_r S_CLK_LOW; end end timeout_cnt timeout_cnt - 1b1; if (timeout_cnt 0) state_r S_ERROR; end S_DEASSERT_CS: begin cs_n_o 1b1; // 拉高片选结束传输 data_rx_o shift_rx_r; // 输出接收到的完整数据 done_o 1b1; // 产生完成脉冲 state_r S_IDLE; end S_ERROR: begin error_o 1b1; // 报告错误 cs_n_o 1b1; // 确保片选释放 state_r S_IDLE; end default: state_r S_IDLE; endcase end end endmodule关键点解析与避坑经验CPHA的处理代码中通过(CPHA 0) ? S_CLK_HIGH : S_CLK_LOW来区分Mode 0和Mode 3。在Mode 0CPHA0下第一个数据位在片选有效后、第一个时钟边沿之前就需要准备好因此跳转到S_CLK_HIGH状态后会立即或在半个分频周期后产生第一个上升沿。而在Mode 3下第一个时钟边沿是下降沿。采样与更新时机在S_CLK_HIGH状态中我们检测下降沿sclk_o 1b0作为状态转换条件并在此时进行采样。这是因为在S_CLK_HIGH状态下SCLK为高当分频计数器归零SCLK翻转为低的瞬间就是一个下降沿。对于Mode 0数据在时钟上升沿采样所以我们的采样逻辑shift_rx_r ...放在检测到下降沿之后、状态转换之前是合适的确保了采样点发生在SCLK高电平期间。数据更新mosi_o赋值则在S_CLK_LOW状态开始时进行。超时机制timeout_cnt在每个非空闲状态递减如果减到0则跳转到错误状态。这是一个非常重要的安全机制防止因为从机无响应或线路故障导致FPGA状态机死锁。超时阈值需要根据系统时钟和SPI时钟频率合理设置。片选管理在S_ASSERT_CS和S_DEASSERT_CS状态我特意留出了时间通过clk_cnt判断。拉低片选后等待一段时间再产生时钟给STM32足够的准备时间传输结束后先拉高片选再回到空闲状态。很多通信不稳定的问题就出在片选和时钟的边沿太近。5. STM32端SPI从机配置与DMA接收实战FPGA那边是精确的“发令员”STM32这边就要做好“运动员”。我们使用STM32CubeIDE和HAL库来配置。第一步CubeMX图形化配置选择你的SPI外设例如SPI2。Mode选择“Transmit and Receive”全双工。Data Size选择“8 bits”或“16 bits”与FPGA一致。Frame Format选择“Motorola”标准SPI格式。NSS这是关键选择“Hardware Input”。这会将对应的引脚通常是NSS配置为硬件片选输入。FPGA的CS_N线就连接到这里。Baud Rate从机模式下此设置无效时钟由主机提供但建议预设一个大于主机时钟的频率。Clock Polarity和Clock Phase根据FPGA的模式选择例如Mode 0对应“Low”和“1 Edge”。开启SPI中断和/或DMA。如果使用中断模式在NVIC设置中使能SPI全局中断。如果使用DMA模式推荐用于大数据量在“DMA Settings”标签页为SPI_RX添加一个DMA流如DMA1 Stream 0方向Peripheral To Memory模式Circular循环或Normal普通并设置好内存地址递增。第二步编写中断/DMA接收代码以下是使用DMA循环模式接收的示例代码片段。这种方式可以无缝接收FPGA连续发来的数据流。// 在main.c或专用通信模块中 #define SPI_RX_BUFFER_SIZE 256 uint8_t spiRxBuffer[SPI_RX_BUFFER_SIZE]; // DMA接收缓冲区 void SPI_Init_With_DMA(void) { // ... CubeMX生成的初始化代码之后 // 启动SPI DMA接收 if (HAL_SPI_Receive_DMA(hspi2, spiRxBuffer, SPI_RX_BUFFER_SIZE) ! HAL_OK) { Error_Handler(); } // 此时SPI2会在硬件NSS片选为低时自动接收数据到spiRxBuffer // 当缓冲区收满一半或全部时会触发DMA中断 } // DMA传输完成一半或全部的中断回调函数 void HAL_SPI_RxHalfCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI2) { // 处理 spiRxBuffer[0] ~ spiRxBuffer[SIZE/2-1] 之间的数据 process_spi_data(spiRxBuffer, SPI_RX_BUFFER_SIZE/2); } } void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI2) { // 处理 spiRxBuffer[SIZE/2] ~ spiRxBuffer[SIZE-1] 之间的数据 process_spi_data(spiRxBuffer SPI_RX_BUFFER_SIZE/2, SPI_RX_BUFFER_SIZE/2); // 注意DMA是循环模式所以不需要重新启动。处理完的数据会被新数据覆盖。 } } // 数据处理函数实现帧解析 void process_spi_data(uint8_t* data, uint16_t len) { static uint8_t frame_state 0; static uint16_t data_index 0; static uint8_t frame_len 0; static uint8_t frame_cmd 0; static uint8_t frame_buffer[128]; static uint8_t calc_checksum 0; for (int i 0; i len; i) { uint8_t byte data[i]; switch (frame_state) { case 0: // 寻找帧头1 if (byte 0xAA) frame_state 1; break; case 1: // 寻找帧头2如果使用双字节帧头 if (byte 0x55) { frame_state 2; calc_checksum 0xAA 0x55; // 初始化校验和 } else { frame_state 0; // 同步失败重新开始 } break; case 2: // 获取长度 frame_len byte; calc_checksum byte; data_index 0; // 简单检查长度有效性 if (frame_len sizeof(frame_buffer)) { frame_state 0; break; } frame_state 3; break; case 3: // 获取命令 frame_cmd byte; calc_checksum byte; frame_state 4; break; case 4: // 接收数据载荷 frame_buffer[data_index] byte; calc_checksum byte; if (data_index frame_len) { frame_state 5; } break; case 5: // 接收并校验校验和 if (calc_checksum byte) { // 校验成功处理完整帧 handle_spi_frame(frame_cmd, frame_buffer, frame_len); } else { // 校验失败丢弃或记录错误 } frame_state 0; // 无论成功与否回到初始状态 break; default: frame_state 0; break; } } }避坑经验DMA缓冲区溢出使用循环DMA时数据处理速度必须快于数据接收速度。如果process_spi_data函数处理太慢可能导致数据被覆盖。解决方案是使用双缓冲区Ping-Pong Buffer或者增大DMA缓冲区并确保处理逻辑高效。中断优先级如果系统中有其他高优先级中断如USB、以太网可能会打断SPI DMA中断导致数据流解析出错。需要合理配置中断优先级。片选毛刺硬件连接上SPI的片选CS_N、时钟SCLK线最好串联一个几十欧姆的电阻并尽量缩短走线可以减少反射和毛刺提高在较高时钟频率下的稳定性。6. 联合调试与问题排查从“不通”到“稳定”即使双方代码都写好了第一次联调往往也不会一帆风顺。下面是我总结的调试步骤和常见问题排查清单。调试第一步静态检查与信号测量电气连接用万用表确认VCC、GND、MOSI、MISO、SCLK、CS_N这几根线没有接错、虚焊或短路。电平匹配确认FPGA的IO bank电压与STM32的引脚电压匹配通常是3.3V。如果不匹配需要电平转换。上电顺序确保双方同时上电或FPGA先上电。STM32先上电而FPGA的IO处于高阻态可能导致SPI线被意外拉高或拉低。调试第二步分步验证先单向后双向FPGA单发STM32监听先将STM32的MISO引脚配置为输入上拉或直接悬空让FPGA循环发送一个固定的字节如0x55或0xAA。用逻辑分析仪或示波器同时抓取FPGA的MOSI、SCLK、CS_N信号。看时序检查SCLK频率、占空比、CPOL/CPHA是否与STM32配置一致。检查CS_N在数据位传输前后是否有足够保持时间。看数据检查MOSI线上移出的数据位是否正确MSB/LSB顺序。STM32侧在中断或DMA回调中设置断点或者简单地将接收到的数据通过串口打印出来看是否能收到FPGA发送的固定模式。STM32单发FPGA监听将FPGA的MOSI线暂时断开或上拉让STM32在收到片选后发送一个固定数据。修改FPGA代码将接收到的数据通过LED或虚拟IO输出检查是否正确。双向通信将线路恢复进行完整的双向通信测试。先测试短帧再测试长帧。常见问题与解决方案现象可能原因排查方法STM32完全收不到数据1. 片选信号问题2. 时钟极性/相位不匹配3. STM32 SPI未使能或配置错误4. 硬件连接问题1. 用示波器看CS_N在传输期间是否被FPGA拉低。2. 核对双方CPOL/CPHA设置用示波器看SCLK空闲电平和第一个边沿。3. 检查STM32的SPI外设时钟是否开启__HAL_SPI_ENABLE()是否调用。4. 重新检查接线。收到数据但全是0或0xFF1. MISO/MOSI线接反2. 数据位顺序不匹配3. 从机未及时输出数据1. 交换MISO和MOSI线试试。2. 检查双方MSB/LSB设置。3. STM32在从机模式下必须在SCLK驱动下输出数据。检查发送缓冲寄存器DR是否在正确时机被写入。数据错位偶尔正确1. 时钟频率过高建立保持时间不足2. 片选信号有毛刺或建立时间不够3. 中断/DMA响应太慢数据丢失1. 降低SPI时钟频率调整FPGA的CLK_DIV参数。2. 在FPGA代码中增加片选有效到第一个时钟沿的延迟S_ASSERT_CS状态。3. 优化STM32中断服务程序减少处理时间或检查DMA优先级和缓冲区是否溢出。长数据包传输出错1. 软件流控缺失缓冲区溢出2. 帧同步丢失没有帧头或帧头被干扰3. 校验和错误1. 实现前面提到的硬件流控IRQ_PIN或软件应答机制。2. 增强帧同步算法例如使用更独特的帧头如0xAA55或增加帧间空闲时间。3. 改用CRC校验并检查硬件线路是否有噪声。一个真实的调试案例我曾遇到STM32只能收到第一个字节后续字节全是0的问题。用逻辑分析仪抓取发现FPGA发出的SCLK、MOSI、CS_N信号完全正常。问题出在STM32侧。我使用的是HAL库的HAL_SPI_Receive_IT中断模式。调试发现在第一个字节接收完成后中断服务程序里没有及时为下一个字节准备数据即重新调用HAL_SPI_Receive_IT。在从机模式下SPI DR数据寄存器必须在下一个时钟周期到来前被写入新的待发送数据即使我们只想接收也需要写入一个哑元数据如0x00否则MISO线会保持之前的状态或高阻导致FPGA采样到错误数据。解决方案是在HAL_SPI_RxCpltCallback回调函数中立即启动下一次接收。或者更简单直接地切换到DMA模式让硬件自动处理数据的连续搬运。7. 性能优化与进阶思考当基本通信稳定后可以考虑一些优化和扩展。1. 时钟速度与布线SPI的极限时钟频率受限于最慢设备的特性、PCB走线长度和噪声环境。对于STM32F4系列SPI从机模式在理论上可以支持到几十MHz但实际应用中如果FPGA和STM32不在同一块板卡上通过排线连接建议保守一点先从1-2MHz开始测试逐步提高。良好的接地和较短的走线是高速通信的基础。2. DMA与双缓冲对于持续的数据流DMA是必须的。更进一步可以使用双缓冲机制DMA配置为循环模式填充两个缓冲区A和B。当A半满或全满时触发中断CPU处理A的数据同时DMA继续向B填充数据。这样可以几乎避免任何数据丢失的风险。3. 错误注入与鲁棒性测试系统稳定后可以主动进行一些“破坏性”测试比如随机拔插通信线、在电源上引入短时毛刺、让FPGA发送错误格式的数据包等观察系统的恢复能力。根据测试结果完善超时重发、错误计数、自动复位等机制。4. 协议扩展本文定义的帧协议比较简单。在实际复杂系统中可能需要支持分包、重组、重传、确认ACK/NACK、窗口滑动等更复杂的通信机制。可以将SPI仅仅看作一个可靠的数据链路层在其之上运行一个简化的自定义协议或甚至移植一个轻量级的通信协议栈。FPGA与STM32的SPI主从通信是嵌入式系统中一种经典且高效的异构协同方式。打通它就像是给两个各有所长的“大脑”建立了高速的神经连接。整个过程从时序设计、协议制定、代码实现到联合调试是对数字电路基础、微控制器外设理解和系统调试能力的综合锻炼。最深的体会是“稳定”远比“跑通”更重要。多考虑一些边界情况多加入一些保护机制在实验室里多模拟一些异常最终的产品才能在现场复杂的环境下可靠运行。希望这篇基于实战的总结能帮你绕过我踩过的那些坑更顺畅地搭建起属于你自己的FPGA-STM32通信桥梁。