STM32与FPGA的SPI全双工通信实战指南
在嵌入式系统开发中,处理器与可编程逻辑器件的高效通信是一个常见需求。SPI(Serial Peripheral Interface)作为一种高速全双工同步串行通信协议,因其简单高效的特性,成为STM32等微控制器与FPGA之间数据交互的理想选择。本文将基于STM32F103和Xilinx Spartan-6 FPGA平台,深入讲解SPI通信的实现细节,提供可直接应用于项目的完整代码方案。
1. 硬件平台与通信基础
1.1 硬件选型与连接
我们选择的硬件组合是:
- 主设备:STM32F103C8T6(72MHz主频,ARM Cortex-M3内核)
- 从设备:Xilinx Spartan-6 XC6SLX9 FPGA
SPI物理连接需要四根信号线:
- SCK(Serial Clock):时钟信号,由主设备产生
- MOSI(Master Out Slave In):主设备输出,从设备输入
- MISO(Master In Slave Out):主设备输入,从设备输出
- CS(Chip Select):片选信号,低电平有效
注意:STM32的硬件SPI外设通常固定分配了MOSI/MISO/SCK引脚,但CS引脚可以自由选择任意GPIO,这为电路设计提供了灵活性。
1.2 SPI模式详解
SPI通信有四种工作模式,由时钟极性(CPOL)和时钟相位(CPHA)两个参数决定:
| 模式 | CPOL | CPHA | 空闲时钟电平 | 数据采样边沿 |
|---|---|---|---|---|
| 0 | 0 | 0 | 低电平 | 奇数边沿(上升沿) |
| 1 | 0 | 1 | 低电平 | 偶数边沿(下降沿) |
| 2 | 1 | 0 | 高电平 | 奇数边沿(下降沿) |
| 3 | 1 | 1 | 高电平 | 偶数边沿(上升沿) |
本实验采用模式3(CPOL=1,CPHA=1),这是许多FPGA SPI从机实现的首选模式。在这种模式下:
- 空闲时SCK保持高电平
- 数据在SCK的上升沿采样
- 数据在SCK的下降沿切换
2. STM32主设备配置
2.1 硬件初始化
STM32标准库提供了完善的SPI外设支持。以下是关键初始化代码:
void SPI_FPGA_Init(void) { SPI_InitTypeDef SPI_InitStructure; GPIO_InitTypeDef GPIO_InitStructure; // 使能SPI2时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE); // 配置SPI引脚 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); // 配置CS引脚(PC3)为普通输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOC, &GPIO_InitStructure); SPI_FPGA_CS_HIGH(); // 初始化为高电平 // 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_4; // 18MHz SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI2, &SPI_InitStructure); SPI_Cmd(SPI2, ENABLE); }2.2 数据收发实现
SPI是全双工通信,发送和接收同时进行。以下是字节收发函数:
uint8_t SPI_FPGA_SendByte(uint8_t byte) { // 等待发送缓冲区空 while(SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET); // 发送数据 SPI_I2S_SendData(SPI2, byte); // 等待接收缓冲区非空 while(SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET); // 返回接收到的数据 return SPI_I2S_ReceiveData(SPI2); }提示:STM32的SPI数据寄存器是16位的,但我们可以只使用低8位进行字节传输。高位数据在8位模式下会被忽略。
3. FPGA从机Verilog实现
3.1 SPI从机模块设计
FPGA端的SPI从机需要精确同步主设备的时钟和数据。以下是Verilog核心代码:
module spi_slave ( input clk, // FPGA系统时钟 input rst_n, // 复位信号 input CS_N, // 片选信号 input SCK, // SPI时钟 input MOSI, // 主出从入 output reg MISO, // 主入从出 output reg led // 状态指示灯 ); // 时钟边沿检测 reg sck_r0, sck_r1; always @(posedge clk or negedge rst_n) begin if(!rst_n) {sck_r0, sck_r1} <= 2'b11; else {sck_r0, sck_r1} <= {SCK, sck_r0}; end wire sck_rising = ~sck_r0 & sck_r1; // 上升沿检测 wire sck_falling = sck_r0 & ~sck_r1; // 下降沿检测 // 接收逻辑(上升沿采样) reg [7:0] rxd_data; reg [2:0] rxd_bit_cnt; always @(posedge clk or negedge rst_n) begin if(!rst_n) begin rxd_data <= 8'h00; rxd_bit_cnt <= 3'd0; end else if(!CS_N && sck_rising) begin rxd_data <= {rxd_data[6:0], MOSI}; rxd_bit_cnt <= rxd_bit_cnt + 1'b1; end end // 发送逻辑(下降沿切换) reg [7:0] txd_data = 8'b00011000; // 固定返回0x18 reg [2:0] txd_bit_cnt; always @(posedge clk or negedge rst_n) begin if(!rst_n) begin MISO <= 1'b0; txd_bit_cnt <= 3'd0; end else if(!CS_N && sck_falling) begin MISO <= txd_data[7 - txd_bit_cnt]; txd_bit_cnt <= txd_bit_cnt + 1'b1; end else if(CS_N) begin MISO <= 1'b0; txd_bit_cnt <= 3'd0; end end // 接收数据处理 always @(posedge clk or negedge rst_n) begin if(!rst_n) led <= 1'b0; else if(rxd_bit_cnt == 3'd7 && !CS_N && sck_rising) begin led <= (rxd_data == 8'd123); // 收到123时点亮LED end end endmodule3.2 时序分析与优化
在FPGA实现中,需要特别注意:
- 时钟同步:使用双寄存器同步SCK信号,避免亚稳态
- 边沿检测:准确识别SCK的上升沿和下降沿
- 位计数器:确保8位数据的完整收发
- CS信号处理:片选无效时重置状态机
4. 系统联调与问题排查
4.1 调试流程
硬件检查:
- 确认所有连接线正确无误
- 检查电源和地线连接
- 测量SCK信号是否正常产生
分模块验证:
- 先单独测试STM32的SPI输出
- 再验证FPGA的信号接收
- 最后进行全双工通信测试
逻辑分析仪抓取: 典型的SPI模式3时序应显示:
- CS拉低后,SCK从高电平开始
- MOSI数据在SCK下降沿切换
- MISO数据在SCK上升沿采样
4.2 常见问题与解决方案
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 无任何通信 | 硬件连接错误 | 检查线路连通性,确认电源正常 |
| 只能发送不能接收 | CPOL/CPHA不匹配 | 检查两端模式设置是否一致 |
| 数据错位 | 相位错误 | 调整采样边沿,或检查位序(MSB/LSB) |
| 偶尔数据错误 | 时序不满足 | 降低SPI时钟频率,增加建立保持时间 |
| CS信号异常 | 软件控制不当 | 确保CS在传输前后有足够延时 |
4.3 性能优化技巧
时钟分频选择:
// STM32 SPI时钟分频选项 typedef enum { SPI_BaudRatePrescaler_2 = 0x00, // 36MHz @72MHz PCLK SPI_BaudRatePrescaler_4 = 0x08, // 18MHz SPI_BaudRatePrescaler_8 = 0x10, // 9MHz SPI_BaudRatePrescaler_16 = 0x18, // 4.5MHz SPI_BaudRatePrescaler_32 = 0x20, // 2.25MHz SPI_BaudRatePrescaler_64 = 0x28, // 1.125MHz SPI_BaudRatePrescaler_128 = 0x30, // 562.5kHz SPI_BaudRatePrescaler_256 = 0x38 // 281.25kHz } SPIBaudRate;DMA传输:对于大数据量传输,可以配置SPI DMA:
void SPI_DMA_Config(void) { DMA_InitTypeDef DMA_InitStructure; // 使能DMA时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 配置TX DMA DMA_DeInit(DMA1_Channel4); 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_Channel4, &DMA_InitStructure); // 配置RX DMA类似... // 使能SPI DMA SPI_I2S_DMACmd(SPI2, SPI_I2S_DMAReq_Tx | SPI_I2S_DMAReq_Rx, ENABLE); }双缓冲技术:在FPGA端实现双缓冲机制,避免数据覆盖。