STM32与FPGA的SPI通信:从模式配置到数据交互实战

张开发
2026/4/18 13:57:51 15 分钟阅读

分享文章

STM32与FPGA的SPI通信:从模式配置到数据交互实战
1. SPI通信基础与硬件选型SPISerial Peripheral Interface是嵌入式领域最常用的同步串行通信协议之一尤其适合短距离高速数据传输。我第一次接触STM32与FPGA的SPI通信是在一个工业控制器项目中需要实时传输传感器数据到FPGA进行预处理。当时最大的困惑是为什么同样的接线方式别人的板子能跑通我的却只能收到乱码后来发现是时钟相位配置错误。物理层连接只需要4根线SCKSerial Clock时钟信号由主机产生MOSIMaster Out Slave In主机输出从机输入MISOMaster In Slave Out主机输入从机输出CSChip Select片选信号低电平有效实际项目中我推荐使用屏蔽双绞线特别是当通信距离超过15cm时。曾经有个电机控制项目因为电磁干扰导致SPI误码率飙升换成带屏蔽层的线缆后问题立刻解决。对于STM32和Xilinx FPGA的组合需要注意两者IO电平的匹配——STM32通常是3.3V而部分老款FPGA可能是5V电平需要加电平转换芯片如TXB0108。2. STM32主机配置详解2.1 硬件初始化陷阱使用STM32CubeMX配置SPI外设时新手常会忽略几个关键点时钟分频SPI1挂载在APB2总线72MHzSPI2/3在APB136MHz。我曾因为没注意这个细节导致实际波特率比预期慢一倍DMA配置连续传输大量数据时一定要启用DMA否则CPU利用率会飙升。分享一个实测数据传输模式1KB数据传输时间CPU占用率轮询2.8ms100%中断2.6ms85%DMA0.3ms5%初始化代码要特别注意GPIO的复用功能映射。有次调试发现MOSI没输出最后发现是GPIO_AF5没配置正确// 正确的GPIO初始化示例以STM32F4为例 GPIO_InitStruct.Pin GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_VERY_HIGH; GPIO_InitStruct.Alternate GPIO_AF5_SPI1; HAL_GPIO_Init(GPIOA, GPIO_InitStruct);2.2 模式3的特殊处理SPI模式3CPOL1, CPHA1是FPGA最常用的模式此时空闲时SCK保持高电平数据在时钟第二个边沿下降沿采样在STM32中需要这样配置hspi1.Init.CLKPolarity SPI_POLARITY_HIGH; hspi1.Init.CLKPhase SPI_PHASE_2EDGE;遇到过最坑的问题是某些FPGA型号对建立时间setup time要求严格。当STM32运行在72MHz时建议将SPI分频设置为至少8分频即9MHz否则可能出现时序违例。可以通过示波器测量SCK与MOSI的相位关系来验证测量要点MOSI数据变化应在SCK下降沿之后保持稳定直到下一个下降沿到来3. FPGA从机Verilog实现3.1 双缓冲设计技巧FPGA作为从机时核心是准确捕捉时钟边沿。我推荐使用双寄存器同步技术避免亚稳态// 边沿检测标准写法 always (posedge clk or negedge rst_n) begin if(!rst_n) begin sck_r0 1b0; sck_r1 1b0; end else begin sck_r0 SCK; sck_r1 sck_r0; end end assign sck_rising (~sck_r1 sck_r0); assign sck_falling (sck_r1 ~sck_r0);数据接收状态机要注意启动条件。有次调试发现接收错位原因是没判断CS信号always (posedge clk or negedge rst_n) begin if(!rst_n) begin rxd_state 3d0; end else if(sck_rising !CS_N) begin // 必须包含CS条件 case(rxd_state) // 状态转移逻辑... endcase end end3.2 动态响应优化当FPGA需要返回不同数据时可以采用流水线响应设计。比如在电机控制项目中我这样实现实时状态反馈reg [7:0] status_reg; always (posedge clk) begin if(sensor_alert) status_reg 8hAA; // 异常代码 else status_reg {4d0, speed[3:0]}; // 速度值 end // 发送逻辑 always (posedge clk or negedge rst_n) begin if(!rst_n) begin txd_state 3d0; end else if(sck_falling !CS_N) begin case(txd_state) 3d0: MISO status_reg[7]; // 其他位... endcase end end4. 联合调试实战案例4.1 交叉验证方法建议分三个阶段验证单机测试先用STM32自发自收验证硬件通路静态测试固定发送0x55/0xAA等特征码动态测试递增数据模式如0x00-0xFF分享一个实用的调试脚本Python pySerialimport serial ser serial.Serial(COM3, 115200, timeout1) def spi_test(test_pattern): ser.write(test_pattern) response ser.read(len(test_pattern)) for i in range(len(test_pattern)): if response[i] ! test_pattern[i]: print(fError at byte {i}: sent {hex(test_pattern[i])}, got {hex(response[i])}) # 测试用例 spi_test(bytes([0x55]*128)) # 交替比特 spi_test(bytes(range(256))) # 全模式4.2 常见故障排查表现象可能原因解决方法能发不能收MISO线路断路检查PCB走线或飞线连接偶尔数据错误时序裕量不足降低SPI时钟频率CS信号无效GPIO配置错误确认CS引脚输出模式为推挽输出仅首字节正确CS信号释放过早确保CS在传输全程保持低电平FPGA收不到任何数据时钟极性/相位不匹配核对双方CPOL/CPHA设置记得有一次遇到STM32发送正常但FPGA收不到数据最后发现是PCB设计时把MOSI和MISO画反了。现在我的检查清单里一定会包含交叉验证接线顺序这一项。在完成基础通信后可以尝试添加CRC校验。我常用的简单校验方法是XOR累加// STM32端发送校验 uint8_t add_checksum(uint8_t *data, uint8_t len) { uint8_t crc 0; for(int i0; ilen; i) { crc ^ data[i]; } return crc; }FPGA端用类似的逻辑验证当校验失败时通过特定引脚触发示波器捕获能极大提高调试效率。

更多文章