STM32与FPGA的SPI通信实战从硬件连接到联合调试第一次尝试让STM32和FPGA通过SPI通信时我遇到了一个令人抓狂的问题——明明代码看起来没问题但数据就是传不过去。后来发现是时钟相位配置错了这个教训让我意识到嵌入式开发中细节决定成败。本文将带你完整实现STM32F103与Xilinx Spartan-6 FPGA的SPI通信避开那些我踩过的坑。1. 硬件准备与连接在开始编程前正确的硬件连接是成功的第一步。我建议使用杜邦线连接两块开发板时尽量选择短而质量好的线材长线可能引入信号完整性问题。所需硬件清单STM32F103开发板如Blue PillXilinx Spartan-6 FPGA开发板如Digilent Atlys杜邦线若干USB转串口模块用于调试输出引脚连接对照表STM32引脚FPGA引脚SPI信号线备注PB13对应SCK引脚SCK时钟线必须直连PB14对应MISO引脚MISO主入从出PB15对应MOSI引脚MOSI主出从入PC3对应CS引脚CS_N片选低电平有效注意不同开发板的引脚编号可能不同请根据实际板卡原理图调整。我曾因为看错引脚定义浪费了半天时间调试。连接时特别注意共地连接必不可少将两块板子的GND引脚相连如果使用3.3V和5V混合系统需要电平转换建议在SCK线上串联一个22-100Ω的电阻可减少振铃现象2. STM32主机配置详解STM32作为SPI主机需要正确初始化外设。下面这段代码是我在多个项目中验证过的稳定配置采用SPI模式3CPOL1, CPHA1这也是大多数FPGA SPI从机兼容的模式。// bsp_spi_fpga.h 头文件关键定义 #define FPGA_SPIx SPI2 #define FPGA_SPI_CS_PORT GPIOC #define FPGA_SPI_CS_PIN GPIO_Pin_3 void SPI_FPGA_Init(void) { SPI_InitTypeDef SPI_InitStructure; GPIO_InitTypeDef GPIO_InitStructure; // 启用SPI2和GPIOC时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); // 配置CS引脚为推挽输出 GPIO_InitStructure.GPIO_Pin FPGA_SPI_CS_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(FPGA_SPI_CS_PORT, GPIO_InitStructure); // 配置其他SPI引脚为复用功能 GPIO_InitStructure.GPIO_Pin GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_Init(GPIOB, GPIO_InitStructure); // SPI参数配置 SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode SPI_Mode_Master; SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL SPI_CPOL_High; // 时钟极性高 SPI_InitStructure.SPI_CPHA SPI_CPHA_2Edge; // 第二个边沿采样 SPI_InitStructure.SPI_NSS SPI_NSS_Soft; // 软件控制片选 SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_32; SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(FPGA_SPIx, SPI_InitStructure); SPI_Cmd(FPGA_SPIx, ENABLE); SPI_FPGA_CS_HIGH(); // 初始时取消片选 }关键参数解析SPI_BaudRatePrescaler_32设置SPI时钟为PCLK1/32约2.25MHz适合初次调试软件控制片选SPI_NSS_Soft比硬件控制更灵活模式3CPOL1,CPHA1意味着空闲时SCK为高电平数据在第二个边沿下降沿采样数据收发函数需要处理超时避免死等uint8_t SPI_FPGA_SendByte(uint8_t byte) { uint16_t timeout SPIT_FLAG_TIMEOUT; while (SPI_I2S_GetFlagStatus(FPGA_SPIx, SPI_I2S_FLAG_TXE) RESET) { if((timeout--) 0) return 0xFF; } SPI_I2S_SendData(FPGA_SPIx, byte); timeout SPIT_FLAG_TIMEOUT; while (SPI_I2S_GetFlagStatus(FPGA_SPIx, SPI_I2S_FLAG_RXNE) RESET) { if((timeout--) 0) return 0xFF; } return SPI_I2S_ReceiveData(FPGA_SPIx); }3. FPGA从机Verilog实现FPGA作为SPI从机需要精确同步时钟边沿。下面这段Verilog代码实现了模式3的从机逻辑包含数据收发和简单状态指示。module spi_slave ( input clk, // 系统时钟如50MHz input rst_n, // 低电平复位 input CS_N, // 片选低有效 input SCK, // SPI时钟 input MOSI, // 主出从入 output reg MISO, // 主入从出 output reg led, // 状态LED output reg led1 // 状态LED ); reg [7:0] tx_data 8b0001_1000; // 固定发送数据 reg [7:0] rx_data; // 接收数据寄存器 reg rx_flag; // 接收完成标志 reg [2:0] bit_cnt; // 位计数器 // 时钟边沿检测 reg SCK_r, SCK_rise, SCK_fall; always (posedge clk or negedge rst_n) begin if(!rst_n) begin SCK_r 1b0; SCK_rise 1b0; SCK_fall 1b0; end else begin SCK_r SCK; SCK_rise (~SCK_r SCK); SCK_fall (SCK_r ~SCK); end end // 接收逻辑上升沿采样 always (posedge clk or negedge rst_n) begin if(!rst_n) begin rx_data 8h00; bit_cnt 3d0; rx_flag 1b0; end else if(!CS_N SCK_rise) begin rx_data {rx_data[6:0], MOSI}; if(bit_cnt 3d7) begin rx_flag 1b1; bit_cnt 3d0; end else begin rx_flag 1b0; bit_cnt bit_cnt 1b1; end end else begin rx_flag 1b0; end end // 发送逻辑下降沿更新 always (posedge clk or negedge rst_n) begin if(!rst_n) begin MISO 1b0; end else if(!CS_N SCK_fall) begin case(bit_cnt) 3d0: MISO tx_data[7]; 3d1: MISO tx_data[6]; 3d2: MISO tx_data[5]; 3d3: MISO tx_data[4]; 3d4: MISO tx_data[3]; 3d5: MISO tx_data[2]; 3d6: MISO tx_data[1]; 3d7: MISO tx_data[0]; endcase end end // LED状态指示 always (posedge clk or negedge rst_n) begin if(!rst_n) begin led 1b0; led1 1b0; end else if(rx_flag) begin led (rx_data 8d123); led1 (rx_data 8d245); end end endmodule关键设计要点使用主时钟如50MHz同步SPI信号避免亚稳态精确检测SCK的上升沿和下降沿接收数据在上升沿采样发送数据在下降沿更新简单的LED状态指示验证数据传输正确性4. 联合调试与问题排查即使代码看起来完美实际调试中仍可能遇到各种问题。以下是常见问题及解决方法问题1无数据通信检查硬件连接特别是GND是否共地确认片选信号CS_N是否有效拉低用示波器检查SCK是否有时钟输出问题2数据错位确认STM32和FPGA的SPI模式一致同为模式3检查MSB/LSB传输顺序是否匹配验证时钟极性(CPOL)和相位(CPHA)设置问题3偶发性通信失败降低SPI时钟频率测试如设为SPI_BaudRatePrescaler_256检查电源稳定性适当增加去耦电容缩短连接线长度或使用屏蔽线调试技巧先单独测试STM32的SPI输出用逻辑分析仪抓取波形FPGA端添加ILA核实时监测内部信号从简单数据如0xAA、0x55开始测试逐步复杂化// 调试用测试代码 void Test_SPI_Communication(void) { uint8_t sent_data[] {0x55, 0xAA, 0x01, 0x80}; uint8_t received; for(int i0; isizeof(sent_data); i) { SPI_FPGA_CS_LOW(); received SPI_FPGA_SendByte(sent_data[i]); SPI_FPGA_CS_HIGH(); printf(Sent: 0x%02X, Received: 0x%02X\n, sent_data[i], received); Delay(100000); } }5. 性能优化与高级应用基础通信实现后可以考虑以下优化方向1. 提高传输速率逐步增加SPI时钟频率如升至SPI_BaudRatePrescaler_4FPGA端使用跨时钟域处理技术优化PCB布局减少信号反射2. 增加协议可靠性添加CRC校验字段实现重传机制设计简单的握手机制3. 扩展应用场景使用DMA传输减少CPU开销// STM32 DMA配置示例 void SPI_DMA_Init(void) { DMA_InitTypeDef DMA_InitStructure; // 启用DMA时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 配置DMA发送通道 DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)(SPI2-DR); DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)tx_buffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize BUFFER_SIZE; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode DMA_Mode_Normal; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel5, DMA_InitStructure); SPI_I2S_DMACmd(SPI2, SPI_I2S_DMAReq_Tx, ENABLE); }4. 多从机系统设计使用多个片选信号设计菊花链连接方式实现动态设备枚举实际项目中我曾用这套SPI通信框架实现了STM32与FPGA之间的高速数据采集系统稳定传输速率达到8Mbps。关键是在原型阶段就建立了完善的调试机制这比事后解决问题要高效得多。