通信篇-STM32与FPGA之间的高速SPI通信(含源码和波形分析)
通信篇-STM32与FPGA之间的高速SPI通信
本文作者也只是一个刚接触嵌入式不久的学生,如果有错误的地方可以指出来,我会虚心学习的。
一、硬件资源
- 主控制器: STM32F407ZGT6(作为SPI主机)
- FPGA: EP4CE6F17C8(作为SPI从机)
- 通信接口: SPI(42MHz最大值)
二、SPI通信简介
不太了解SPI通信协议的可以去看看这一篇博客,讲的还是很详尽的
三、stm32端spi具体实现(基于标准外设库V1.8.1)
引脚初始化以及时钟数的开启
- 本次工程采用硬件spi,以此来尽可能达到理论上的最大值.
- 使用SPI1,挂载在APB2上.APB2在stm32f407zgt6这块板子上的时钟频率为84MHZ.
- 具体代码如下:
// 引脚初始化
GPIO_InitTypeDef GPIO_InitStructure;
// GPIOB时钟的开启
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);/b
// CS片选信号软件操作
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(GPIOB, &GPIO_InitStructure);
// 引脚复用
GPIO_PinAFConfig(GPIOB, GPIO_PinSource3, GPIO_AF_SPI1); // SCK
GPIO_PinAFConfig(GPIOB, GPIO_PinSource4, GPIO_AF_SPI1); // MISO
GPIO_PinAFConfig(GPIOB, GPIO_PinSource5, GPIO_AF_SPI1); // MOSI
SPI的配置(只列出一部分用上了的)
- SPI通信方向:双线全双工-SPI_Direction_2Lines_FullDuplex
- SPI工作模式:主机-SPI_Mode_Master
- SPI数据传输宽度:16Bit-SPI_DataSize_16b
- SPI时钟极性:High(空闲高电平)-SPI_CPOL_High
- SPI时钟相位:第二个时钟边沿采集数据-SPI_CPHA_2Edge
- 片选信号控制方式:软件控制-SPI_NSS_Soft
- SPI 波特率预分频器:2-SPI_BaudRatePrescaler_2
- SPI数据传输首位数据:MSB(高位)-SPI_FirstBit_MSB
简单介绍一下这个配置具体的时序吧
首先,STM32调用发送函数的时候,传输开始,CS片选信号拉低,数据写入寄存器,时钟信号开始生成(空闲电平为高)。在第一个下降沿,以及后面的每一个下降沿,STM32会从高位开始移出一位数据置于MOSI(主发从收)线上。并且,STM32会在每一个上升沿采集MISO(主收从发)线上读取一位数据,同样,也是高位先行。最后,十六位数据传输完成的时候,CS片选信号拉高,传输结束
具体代码如下:
// SPI配置
// SPI时钟的开启
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
SPI_InitTypeDef SPI_InitStructure;
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
SPI_InitStructure.SPI_DataSize = SPI_DataSize_16b;
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_2;
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(SPI1, &SPI_InitStructure);
SPI_Cmd(SPI1, ENABLE);
SPI_CS_HIGH();
当然,虽然我上面两串代码是分开给的,但是用的时候最好还是写在一个函数里,毕竟都是用来做初始化的
数据的读取和发送
好吧,32端我觉得也不需要过多解释,直接看代码吧。
// 宏定义以及数据收发
// PB6 - Chip select signal PB6
#define SPI_CS_GPIO_PORT GPIOB
#define SPI_CS_GPIO_PIN GPIO_Pin_6
// - Chip select control macros
#define SPI_CS_LOW() GPIO_ResetBits(SPI_CS_GPIO_PORT, SPI_CS_GPIO_PIN)
#define SPI_CS_HIGH() GPIO_SetBits(SPI_CS_GPIO_PORT, SPI_CS_GPIO_PIN)
void MySPI1_W_SS(uint8_t BitValue)
{
if(BitValue)
SPI_CS_HIGH();
else
SPI_CS_LOW();
}
uint16_t SPI1_ReadWriteHalfWord(uint16_t TxData)
{
while(SPI_I2S_GetFlagStatus(SPI1, SPI_FLAG_TXE) == RESET);
SPI_I2S_SendData(SPI1, TxData);
while(SPI_I2S_GetFlagStatus(SPI1, SPI_FLAG_RXNE) == RESET);
return SPI_I2S_ReceiveData(SPI1);
}
u16 send_halfword[100];
u16 recv_halfword[100];
void FPGA_SPI1(u8 num, const u32* spi1_sending_datas)
{
MySPI1_Start();
recv_halfword[i] = SPI1_ReadWriteHalfWord(send_halfword[i]);
MySPI1_Stop();
}
FPGA端SPI的底层实现
-
技术要点: FPGA作为从机时,系统时钟必须远高于SPI时钟,一般建议5倍以上以满足采样定理。
-
注意事项: CS信号的滤波至关重要,否则在高速下容易出现误触发。
接下来我们对SPI的实现进行拆分
- 模块以及端口的定义
- 对CS、SCK信号进行滤波操作
- 对CS、SCK信号进行边沿捕捉
- 利用CS、SCK信号的边沿做出对应操作,如接收、发送操作
-
模块以及端口定义
// 16Bit数据SPI传输FPGA端实现
module spi_driver #(
parameter WIDTH = 16
)(
input [WIDTH-1:0] tx_data , // 发送数据缓冲区
output reg [WIDTH-1:0] rx_data , // 接收数据缓冲区
input clk , // 系统时钟
input rstn , // 复位信号
input CS_N , // 片选信号
input SCK , // 时钟信号
input MOSI , // 主发从收数据线
output reg MISO , // 主收从发数据线
output reg mark // 16bit数据传输完成标志
);
endmodule
- 对CS、SCK信号的二级滤波
// CS片选信号的二级滤波
reg [1:0] CS_N_filter;
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
CS_N_filter <= 2'b11;
end else begin
CS_N_filter <= {CS_N_filter[0], CS_N};
end
end
// 获得滤波后的CS片选信号
reg CS_N_stable;
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
CS_N_stable <= 1'b1;
end
else begin
if(CS_N_filter == 'b00) begin
CS_N_stable <= 0;
end
else if(CS_N_filter == 'b11) begin
CS_N_stable <= 1;
end
end
end
// SCK时钟信号的二级滤波
reg [1:0] SCK_filter;
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
SCK_filter <= 2'b11;
end else begin
SCK_filter <= {SCK_filter[0], SCK};
end
end
// 获得滤波后的SCK时钟信号
reg SCK_stable;
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
SCK_stable <= 1'b1;
end else begin
if(SCK_filter == 'b00) begin
SCK_stable <= 0;
end else if(SCK_filter == 'b11) begin
SCK_stable <= 1;
end
end
end
- 对CS、SCK信号的边沿捕捉
// CS片选控制信号边沿捕捉
reg CS_N1, CS_N2;
wire CS_N_n, CS_N_p;
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
CS_N1 <= 'd0;
CS_N2 <= 'd0;
end else begin
CS_N1 <= CS_N_stable;
CS_N2 <= CS_N1;
end
end
assign CS_N_n = (~CS_N1 & CS_N2) ? 1'b1 : 1'b0; // 下降沿
assign CS_N_p = (CS_N1 & ~CS_N2) ? 1'b1 : 1'b0; // 上升沿
// SCK时钟信号边沿捕捉
reg sck_r0, sck_r1;
wire sck_n, sck_p;
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
sck_r0 <= 1'b0;
sck_r1 <= 1'b0;
end else if (CS_N_stable) begin // 片选无效时复位
sck_r0 <= 1'b1;
sck_r1 <= 1'b1;
end else begin
sck_r0 <= SCK_stable;
sck_r1 <= sck_r0;
end
end
assign sck_n = (~sck_r0 & sck_r1) ? 1'b1 : 1'b0; // 下降沿
assign sck_p = (sck_r0 & ~sck_r1) ? 1'b1 : 1'b0; // 上升沿
- 接收、发送操作
// spi 16bit数据发送
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
MISO <= 1'bz; // 添加MISO复位
end else if (sck_p && !CS_N) begin
if (width_cnt_p <= WIDTH) begin
MISO <= tx_data[width_cnt_p];
end else if (CS_N) begin // CS_N 无效时
MISO <= 1'bz; // 高阻(若引脚配置为开漏),或 1'b1(固定电平)
end
end
end
// spi 16bit数据接收
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
rx_data <= 'd0;
end else if (sck_n && !CS_N) begin
if (width_cnt_n <= WIDTH - 1) begin
rx_data[width_cnt_n] <= MOSI;
end
end
end
// 其实还有一种写法,在时钟的一个边沿同时进行读取和放入数据,不过为了时序更严谨我把它分开了
reg [$clog2(WIDTH):0] width_cnt_n;
reg [$clog2(WIDTH):0] width_cnt_p;
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
width_cnt_n <= WIDTH - 1; // 初始设为无效值(16)
end
else if(CS_N_p) begin
width_cnt_n <= WIDTH - 1;
end else if (sck_n && !CS_N) begin // SCK下降沿
if (width_cnt_n > 0) begin
width_cnt_n <= width_cnt_n - 1;
end else if (width_cnt_n == 0) begin
width_cnt_n <= WIDTH - 1; // 传输完成后设为无效值,避免覆盖
end
end
end
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
width_cnt_p <= WIDTH - 1; // 初始设为无效值(16)
end else if(CS_N_p) begin
width_cnt_p <= WIDTH - 1;
end else if (sck_p && !CS_N) begin // SCK下降沿
if (width_cnt_p > 0) begin
width_cnt_p <= width_cnt_p - 1;
end else if (width_cnt_p == 0) begin
width_cnt_p <= WIDTH - 1; // 传输完成后设为无效值,避免覆盖
end
end
end
通信验证
接下来就是我们的验证环节了
- stm32端发送与LCD显示


如图,发送4个16位数据,分别为1,12,123,1234.
然后,在LCD屏幕上显示接收到的4个16位数据.
-
FPGA端的数据发送以及模块实例化
!

如图,发送4个16位数据,分别为4321,321,21,1.
至于数据接收,我们用singal tap来观察它是否正确. -
singal tap波形抓取,以及LCD显示

-
第1个16位数据的传输:

-
第2个16位数据的传输:

-
第3个16位数据的传输:

-
第4个16位数据的传输:

由singal tap我们可以看出,片选信号拉低后,SCK信号开始生成。在SCK的上升沿和下降沿,MOSI、MISO也都有所操作。当每一组16位传输结束后,rx_data寄存器里的值正好也是对应stm32的发送数据。
大家也可以观察到一个问题,每16个SCK周期之间,都会有一个延时,这是由于此时并没有加上DMA,所以数据从寄存器到数组的搬运操作是CPU来执行,除此之外,CPU还得做标志位清除的工作,这样极大程度上降低了SPI的传输速率。
当然,后面我也会更新关于SPI+DMA的相关博客。
更多推荐


所有评论(0)