在嵌入式系统开发中,STM32与FPGA的协同工作越来越常见,而SPI(Serial Peripheral Interface)作为两者间高效通信的首选协议之一,其稳定性和灵活性备受开发者青睐。本文将带您完成一个完整的STM32F103与Xilinx Spartan-6 FPGA的SPI通信项目,从硬件连接到软件实现,再到联调技巧,手把手解决实际开发中可能遇到的各种问题。
在开始项目前,请确保您已准备好以下硬件设备:
SPI通信需要正确连接以下四根信号线(以STM32作为主机,FPGA作为从机为例):
| STM32引脚 | FPGA引脚 | 信号类型 | 备注 |
|---|---|---|---|
| PB13 (SCK) | 对应SCK输入 | 时钟信号 | 确保电平匹配 |
| PB14 (MISO) | FPGA的MISO | 主入从出 | 数据输入到STM32 |
| PB15 (MOSI) | FPGA的MOSI | 主出从入 | 数据输出到FPGA |
| PC3 (CS) | FPGA的CS_N | 片选信号 | 低电平有效 |
注意:不同开发板的引脚定义可能有所差异,请根据实际使用的开发板原理图进行调整。若使用其他型号STM32芯片,SPI2的引脚位置也可能不同。
STM32F103的IO口工作电压为3.3V,而Xilinx Spartan-6 FPGA通常也支持3.3V电平,但需要注意:
常见问题排查:若通信不稳定,首先检查所有连接线是否接触良好,然后用万用表测量各信号线的电压是否符合预期。
STM32标准库提供了完善的SPI配置接口,以下是针对STM32F103 SPI2的初始化代码示例:
c复制void SPI2_Init(void)
{
SPI_InitTypeDef SPI_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
// 使能SPI2和GPIO时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
// 配置SPI引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_15; // SCK和MOSI
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14; // MISO
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOB, &GPIO_InitStructure);
// 配置CS引脚(普通IO)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOC, &GPIO_InitStructure);
GPIO_SetBits(GPIOC, GPIO_Pin_3); // 初始置高
// SPI参数配置(模式3)
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_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(SPI2, &SPI_InitStructure);
SPI_Cmd(SPI2, ENABLE);
}
SPI有四种工作模式,由CPOL(时钟极性)和CPHA(时钟相位)决定:
| 模式 | CPOL | CPHA | 空闲时钟 | 数据采样边沿 |
|---|---|---|---|---|
| 0 | 0 | 0 | 低电平 | 奇数边沿(上升沿) |
| 1 | 0 | 1 | 低电平 | 偶数边沿(下降沿) |
| 2 | 1 | 0 | 高电平 | 奇数边沿(下降沿) |
| 3 | 1 | 1 | 高电平 | 偶数边沿(上升沿) |
本实验采用模式3,这是许多FPGA SPI从机实现中最常用的模式。关键参数说明:
可靠的SPI通信需要完善的收发函数,下面是一个带超时检测的实现:
c复制#define SPI_TIMEOUT 1000
uint8_t SPI2_SendByte(uint8_t byte)
{
uint16_t timeout = SPI_TIMEOUT;
// 等待发送缓冲区空
while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET) {
if ((timeout--) == 0) return 0xFF;
}
// 发送数据
SPI_I2S_SendData(SPI2, byte);
timeout = SPI_TIMEOUT;
// 等待接收完成
while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET) {
if ((timeout--) == 0) return 0xFF;
}
return SPI_I2S_ReceiveData(SPI2);
}
void SPI2_WriteBytes(uint8_t* pData, uint16_t len)
{
GPIO_ResetBits(GPIOC, GPIO_Pin_3); // CS拉低
for (uint16_t i = 0; i < len; i++) {
SPI2_SendByte(pData[i]);
}
GPIO_SetBits(GPIOC, GPIO_Pin_3); // CS拉高
}
FPGA作为SPI从机,需要准确检测时钟边沿并同步处理数据。以下是完整的SPI从机Verilog代码:
verilog复制module spi_slave (
input wire clk, // FPGA系统时钟
input wire rst_n, // 复位信号(低有效)
input wire CS_N, // 片选信号(低有效)
input wire SCK, // SPI时钟
input wire MOSI, // 主出从入
output reg MISO, // 主入从出
output reg [7:0] rxd_data, // 接收数据寄存器
output reg rxd_flag // 数据接收完成标志
);
// 时钟边沿检测
reg SCK_r0, SCK_r1;
wire SCK_rising, SCK_falling;
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);
// 接收状态机
reg [2:0] bit_cnt;
reg [7:0] shift_reg;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
bit_cnt <= 3'd0;
shift_reg <= 8'h00;
rxd_flag <= 1'b0;
end else if (CS_N) begin
bit_cnt <= 3'd0;
rxd_flag <= 1'b0;
end else if (SCK_rising) begin // 模式3在上升沿采样
shift_reg <= {shift_reg[6:0], MOSI};
if (bit_cnt == 3'd7) begin
rxd_data <= {shift_reg[6:0], MOSI};
rxd_flag <= 1'b1;
bit_cnt <= 3'd0;
end else begin
bit_cnt <= bit_cnt + 1'b1;
rxd_flag <= 1'b0;
end
end else begin
rxd_flag <= 1'b0;
end
end
// 发送逻辑
reg [7:0] txd_data = 8'h1C; // 默认发送数据
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
MISO <= 1'b0;
end else if (CS_N) begin
MISO <= 1'b0;
end else if (SCK_falling) begin // 模式3在下降沿更新数据
case (bit_cnt)
3'd0: MISO <= txd_data[7];
3'd1: MISO <= txd_data[6];
3'd2: MISO <= txd_data[5];
3'd3: MISO <= txd_data[4];
3'd4: MISO <= txd_data[3];
3'd5: MISO <= txd_data[2];
3'd6: MISO <= txd_data[1];
3'd7: MISO <= txd_data[0];
default: MISO <= 1'b0;
endcase
end
end
endmodule
在FPGA中准确检测SPI时钟边沿是通信可靠的关键。我们采用两级寄存器同步法:
~SCK_r1 & SCK_r0)SCK_r1 & ~SCK_r0)这种方法有效消除了亚稳态问题,是FPGA设计中处理异步信号的常用技术。
SPI通信是同步串行协议,需要精确控制数据位顺序:
在实际调试中,以下几个工具和技术非常有用:
逻辑分析仪:观测SCK、MOSI、MISO、CS信号的实时波形
FPGA内部SignalTap:Xilinx ChipScope或Intel SignalTap
STM32串口打印:输出调试信息和通信结果
以下是开发中可能遇到的典型问题及解决方法:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 通信完全无反应 | 硬件连接错误 | 检查所有连线,确认电源正常 |
| 数据错位或错误 | SPI模式不匹配 | 确保STM32和FPGA使用相同SPI模式 |
| 偶尔数据错误 | 时序不满足 | 降低SPI时钟频率,增加建立保持时间 |
| CS信号无效 | 引脚配置错误 | 确认CS引脚方向设置为输出 |
| FPGA不响应 | 复位信号问题 | 检查FPGA复位电路和信号 |
当系统需要更高通信速率时,可以考虑以下优化措施:
提高SPI时钟频率:
优化FPGA时序:
批量传输优化:
信号完整性改进:
通过多个CS信号控制,可以实现一个STM32主机与多个FPGA从机的通信:
c复制#define FPGA1_CS_PIN GPIO_Pin_3
#define FPGA2_CS_PIN GPIO_Pin_4
#define FPGA3_CS_PIN GPIO_Pin_5
void SelectDevice(uint8_t dev_id)
{
// 所有CS先置高
GPIO_SetBits(GPIOC, FPGA1_CS_PIN | FPGA2_CS_PIN | FPGA3_CS_PIN);
// 根据设备ID选择对应的CS
switch (dev_id) {
case 1: GPIO_ResetBits(GPIOC, FPGA1_CS_PIN); break;
case 2: GPIO_ResetBits(GPIOC, FPGA2_CS_PIN); break;
case 3: GPIO_ResetBits(GPIOC, FPGA3_CS_PIN); break;
}
}
在基本SPI通信基础上,可以定义更复杂的数据协议:
示例协议格式:
| 字段 | 长度 | 说明 |
|---|---|---|
| 帧头 | 1字节 | 固定0xAA |
| 命令 | 1字节 | 操作指令码 |
| 数据长度 | 1字节 | 后续数据字节数 |
| 数据 | N字节 | 有效载荷 |
| CRC | 1字节 | 校验和 |
| 帧尾 | 1字节 | 固定0x55 |
对于需要传输大量数据的应用(如图像、音频),可采用以下技术:
verilog复制// FPGA端流控制示例
module spi_stream (
input wire clk,
input wire rst_n,
input wire CS_N,
input wire SCK,
input wire MOSI,
output wire MISO,
output wire READY
);
reg [7:0] buffer[0:255];
reg [7:0] wr_ptr;
reg [7:0] rd_ptr;
reg buffer_full;
assign READY = ~buffer_full; // 高表示可以接收数据
// 接收数据写入缓冲区
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
wr_ptr <= 8'd0;
buffer_full <= 1'b0;
end else if (rxd_flag && !buffer_full) begin
buffer[wr_ptr] <= rxd_data;
wr_ptr <= wr_ptr + 1;
if (wr_ptr == 8'd255) buffer_full <= 1'b1;
end
end
// 处理数据读出缓冲区
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rd_ptr <= 8'd0;
end else if (process_en && rd_ptr != wr_ptr) begin
process_data <= buffer[rd_ptr];
rd_ptr <= rd_ptr + 1;
if (buffer_full) buffer_full <= 1'b0;
end
end
endmodule