STM32与FPGA的SPI通信:从模式配置到数据交互实战
2026/4/18 13:57:49 网站建设 项目流程

1. SPI通信基础与硬件选型

SPI(Serial Peripheral Interface)是嵌入式领域最常用的同步串行通信协议之一,尤其适合短距离高速数据传输。我第一次接触STM32与FPGA的SPI通信是在一个工业控制器项目中,需要实时传输传感器数据到FPGA进行预处理。当时最大的困惑是:为什么同样的接线方式,别人的板子能跑通,我的却只能收到乱码?后来发现是时钟相位配置错误。

物理层连接只需要4根线:

  • SCK(Serial Clock):时钟信号,由主机产生
  • MOSI(Master Out Slave In):主机输出从机输入
  • MISO(Master In Slave Out):主机输入从机输出
  • CS(Chip Select):片选信号(低电平有效)

实际项目中我推荐使用屏蔽双绞线,特别是当通信距离超过15cm时。曾经有个电机控制项目因为电磁干扰导致SPI误码率飙升,换成带屏蔽层的线缆后问题立刻解决。对于STM32和Xilinx FPGA的组合,需要注意两者IO电平的匹配——STM32通常是3.3V,而部分老款FPGA可能是5V电平,需要加电平转换芯片如TXB0108。

2. STM32主机配置详解

2.1 硬件初始化陷阱

使用STM32CubeMX配置SPI外设时,新手常会忽略几个关键点:

  1. 时钟分频:SPI1挂载在APB2总线(72MHz),SPI2/3在APB1(36MHz)。我曾因为没注意这个细节,导致实际波特率比预期慢一倍
  2. DMA配置:连续传输大量数据时一定要启用DMA,否则CPU利用率会飙升。分享一个实测数据:
传输模式1KB数据传输时间CPU占用率
轮询2.8ms100%
中断2.6ms85%
DMA0.3ms<5%

初始化代码要特别注意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模式3(CPOL=1, CPHA=1)是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 <= 1'b0; sck_r1 <= 1'b0; 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 <= 3'd0; end else if(sck_rising && !CS_N) begin // 必须包含CS条件 case(rxd_state) // 状态转移逻辑... endcase end end

3.2 动态响应优化

当FPGA需要返回不同数据时,可以采用流水线响应设计。比如在电机控制项目中,我这样实现实时状态反馈:

reg [7:0] status_reg; always @(posedge clk) begin if(sensor_alert) status_reg <= 8'hAA; // 异常代码 else status_reg <= {4'd0, speed[3:0]}; // 速度值 end // 发送逻辑 always @(posedge clk or negedge rst_n) begin if(!rst_n) begin txd_state <= 3'd0; end else if(sck_falling && !CS_N) begin case(txd_state) 3'd0: MISO <= status_reg[7]; // 其他位... endcase end end

4. 联合调试实战案例

4.1 交叉验证方法

建议分三个阶段验证:

  1. 单机测试:先用STM32自发自收验证硬件通路
  2. 静态测试:固定发送0x55/0xAA等特征码
  3. 动态测试:递增数据模式(如0x00-0xFF)

分享一个实用的调试脚本(Python + pySerial):

import serial ser = serial.Serial('COM3', 115200, timeout=1) 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(f"Error 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 i=0; i<len; i++) { crc ^= data[i]; } return crc; }

FPGA端用类似的逻辑验证,当校验失败时通过特定引脚触发示波器捕获,能极大提高调试效率。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询