通信篇-STM32与FPGA之间的高速SPI通信

本文作者也只是一个刚接触嵌入式不久的学生,如果有错误的地方可以指出来,我会虚心学习的。

一、硬件资源

  • 主控制器: STM32F407ZGT6(作为SPI主机)
  • FPGA: EP4CE6F17C8(作为SPI从机)
  • 通信接口: SPI(42MHz最大值)

二、SPI通信简介

不太了解SPI通信协议的可以去看看这一篇博客,讲的还是很详尽的

[SPI协议详解(图文并茂+超详细)]

三、stm32端spi具体实现(基于标准外设库V1.8.1)

引脚初始化以及时钟数的开启

  1. 本次工程采用硬件spi,以此来尽可能达到理论上的最大值.
  2. 使用SPI1,挂载在APB2上.APB2stm32f407zgt6这块板子上的时钟频率为84MHZ.
  3. 具体代码如下:
// 引脚初始化
    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的配置(只列出一部分用上了的)

  1. SPI通信方向:双线全双工-SPI_Direction_2Lines_FullDuplex
  2. SPI工作模式:主机-SPI_Mode_Master
  3. SPI数据传输宽度:16Bit-SPI_DataSize_16b
  4. SPI时钟极性:High(空闲高电平)-SPI_CPOL_High
  5. SPI时钟相位:第二个时钟边沿采集数据-SPI_CPHA_2Edge
  6. 片选信号控制方式:软件控制-SPI_NSS_Soft
  7. SPI 波特率预分频器:2-SPI_BaudRatePrescaler_2
  8. 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的实现进行拆分

    1. 模块以及端口的定义
    2. 对CS、SCK信号进行滤波操作
    3. 对CS、SCK信号进行边沿捕捉
    4. 利用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. 第1个16位数据的传输:
    请添加图片描述

  2. 第2个16位数据的传输:
    请添加图片描述

  3. 第3个16位数据的传输:
    请添加图片描述

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

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐