详解SPI通信协议以及FPGA实现

文章目录


一、SPI简介

前面《详解UART通信协议以及FPGA实现》《UART自适应任意(典型)波特率原理以及FPGA实现》我们实现了UART通信以及自适应任意波特率UART的通信,常见的通信协议还有SPI和IIC。其中SPI 的接口速度可以最高到上百兆,采用 SPI 接口的设备一般兼顾低速通信和上百兆的高速通信。

SPI(Serial Peripheral Interface)是一种同步串行全双工的通信接口,常用于在微控制器、传感器、存储器和其他外部设备之间进行数据交换。例如: EEPROM、RTC(实时时钟)、ADC(模数转换器)、DAC(数模转换器)、LCD、音频 IC、温度和压力等传感器,MMC 或 SD 卡等存储卡等等

二、SPI通信结构

SPI是一个同步的数据总线,由上文《》可以知道,同步通信需要随路时钟,也就是说SPI是用单独的数据线和一个单独的时钟信号来保证发送端和接收端的完美同步。通信结构如下图所示:

  • SCK: SPI通信的随路时钟,时钟由主机产生,接收时钟的为从机。SPI通信结构中有且只有一个主机,但是可以有多个从机。
  • MOSI:(Master output slave input )主机输出,从机输入(数据由主机发送给从机)
  • MISO:(Master input slave output)主机输入,从机输出(数据由从机发送给主机)
  • NSS:(Negative Slave Select)从机片选信号,低电平有效,(由主机发送给从机)

三、多从机模式

  1. 常规模式:主机需要为每个从机提供单独的片选信号。一旦主机(拉低)片选信号,MOSI或者MISO线上的时钟和数据便可用于所选的从机。如果拉低多个片选信号,则MISO线上的数据会被破坏,因为主机无法识别哪个从机正在传输数据。
  1. 菊花链模式:所有从机的片选信号连接在一起,数据从一个从机传播到下一个从机。所有从机同时接收同一SPI时钟。来自主机的数据直接送到第一个从机,该从机将数据提供给下一个从机,依此类推。

四、时钟极性(CPOL)和时钟相位(CPHA)

在SPI中,主机需要配置时钟极性和时钟相位。

  • 在空闲状态期间,CPOL位设置时钟信号的极性。空闲状态是指传输开始时CS为高电平且在向低电平转变的期间,以及传输结束时CS为低电平且在向高电平转变的期间。CPOL = 0:时钟空闲时为低电平;CPHA = 1:时钟空闲时为高电平
  • CPHA位选择时钟相位。根据CPHA位的状态,使用时钟上升沿或下降沿来采样或者发送数据。CPHA = 0:在时钟信号 SCK的第一个跳变沿采样;CPHA = 1:在时钟信号 SCK的第二个跳变沿采样;
  • 主机必须根据从机的要求选择时钟极性和时钟相位。根据CPOL和CPHA位的选择,有四种SPI模式可用。

|-------|------|------|----------|-------------------|
| SPI模块 | CPOL | CPHA | 空闲状态时钟极性 | 采样和发送数据的时钟相位 |
| 0 | 0 | 0 | 低电平 | 数据在第1个沿采样,在第2个沿发送 |
| 1 | 0 | 1 | 低电平 | 数据在第2个沿采样,在第1个沿发送 |
| 2 | 1 | 0 | 高电平 | 数据在第1个沿采样,在第2个沿发送 |
| 3 | 1 | 1 | 高电平 | 数据在第2个沿采样,在第1个沿发送 |

四种传输模式如下所示,传输的开始和结束用绿色虚线表示,**采样沿用橙色**虚线表示,**发送沿用蓝色**虚线表示。

  1. SPI模式0,CPOL = 0,CPHA = 0:CLK空闲状态 = 低电平,数据在在下降沿发送,并在上升沿采样。
  1. SPI模式1,CPOL = 0,CPHA = 1:CLK空闲状态 = 低电平,数据在上升沿发送,并在下降沿采样。

  2. SPI模式2,CPOL = 1,CPHA = 0:CLK空闲状态 = 高电平,数据在上升沿发送,并在下降沿采样。

  3. SPI模式3,CPOL = 1,CPHA = 1:CLK空闲状态 = 高电平,数据在在下降沿发送,并在上升沿采样。

五、SPI通信过程

  1. 主机在访问从机前,先将NSS信号拉低,选择对应的从机;当主机要结束本次通信时,再拉高NSS信号。
  2. 主机发送SCLK时钟信号
  3. 主机按照配置的时钟极性和时钟相位,将数据一位一位通过MOSI发送给从机
  4. 从机根据主机配置的时钟极性和时钟相位,在对应位置一位一位的接收数据

六、实现SPI主机发送程序

6.1 波形图分析

  1. 当前没有发送数据时,spi_en为低,当上有请求发送数据时,则拉高spi_en信号以及缓存待发送数据
  2. 然后拉低cs信号,开始时钟计数
  3. 每次计数一半周期时,拉高第一时钟沿信号,计数一个周期时,拉高第二时钟沿信号。
  4. 如果CPOL=0,则时钟空闲时为低电平,第一时钟沿为上升沿。如果CPOL=1,则相反
  5. 每次第一时钟沿来临时,发送的bit计数器累加
  6. 如果CPHA=0时,数据采样在第一时钟沿(上升沿),采样数据如上面红线所示。例如发送的数据为10010110,则采样的数据也为10010110。
  7. 如果CPHA=1时,数据采样在第二个时钟沿(下降沿),采样数据如下面红线所示,例如发送的数据为10010110,则采样的数据也为10010110。

6.2 Verilog代码

c 复制代码
`timescale 1ns/1ns
module spi_master_tx#
(
    parameter SYS_CLK_FREQ = 50000000,                  //输入时钟频率50M
    parameter SPI_CLK_FREQ = 500000,                    //输出spi时钟500K
    parameter CPOL = 1'b0,                              //时钟极性设置为0
    parameter CPHA = 1'b0                               //时钟相位设置为0
)   
(
    input                                               sys_clk ,       //输入系统时钟
    input                                               rst_n   ,       //系统复位
    input                                               spi_tx_req  ,   //待发送数据请求
    input           [7:0]                               spi_tx_data ,   //待发送数据
    output  reg                                         spi_cs  ,       //cs片选信号
    output                                              spi_clk ,       //主机发送的spi时钟
    output                                              spi_busy,       //spi繁忙信号
    output                                              spi_mosi        //mosi
  
);

    localparam      [9:0]                               spi_clk_cnt_max = SYS_CLK_FREQ / SPI_CLK_FREQ;  //一个spi时钟需要计数的最大值
    localparam      [9:0]                               spi_clk_cnt_max_div2    = spi_clk_cnt_max/2;    //半个时钟周期需要计数的最大值

    reg             [9:0]                               clk_div_cnt ;             //spi时钟计数器
    reg                                                 spi_en  ;                 //spi发送使能
    reg             [7:0]                               spi_tx_data_reg ;         //缓存待发送的数据
    reg                                                 clk1_en ;                 //第一时钟边沿
    reg                                                 clk2_en ;                 //第二时钟边沿
    reg                                                 spi_clk_temp    ;         //spi时钟
    reg             [3:0]                               tx_cnt  ;                 //发送的bit计数器
    reg                                                 spi_strobe_en   ;         //spi发送数据使能有效范围
    wire                                                strobe  ;                 //spi发送数据使能信号

//如果时钟极性=1,则取反时钟信号
assign spi_clk = (CPOL == 1'b1)?  ~spi_clk_temp : spi_clk_temp;
//先发送高位数据
assign spi_mosi = spi_tx_data_reg[7];
//如果时钟相位=1,则采样数据才第二时钟沿,发送数据在第一时钟沿
assign strobe = (CPHA == 1'b1)? clk1_en&spi_strobe_en : clk2_en&spi_strobe_en;
//spi繁忙信号就=spi发送使能信号
assign spi_busy = spi_en;

//等待上游给出发送请求和发送数据。发送完成8个bit,则本次发送完成。
always @(posedge sys_clk or negedge rst_n) begin
    if((rst_n == 1'b0) || (clk1_en == 1'b1 && tx_cnt == 4'd8)) begin
        spi_en <= 1'b0;
        spi_tx_data_reg <= 'd0;
    end
    else if((spi_tx_req == 1'b1) && (spi_en == 1'b0))begin
        spi_en <= 1'b1;
        spi_tx_data_reg <= spi_tx_data;
    end
    else if(spi_en == 1'b1 && strobe)
        spi_tx_data_reg <= {spi_tx_data_reg[6:0],1'b0};
    else
        spi_tx_data_reg <= spi_tx_data_reg;
end

//发送开始前,拉低cs信号
always @(posedge sys_clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        spi_cs <= 1'b1;
    else if(spi_en == 1'b1)
        spi_cs <= 1'b0;
    else
        spi_cs <= 1'b1;
end

//当时钟计数器计数到一半时,拉高第一时钟沿信号,计数完成一个时钟周期时,拉高第二时钟信号
always @(posedge sys_clk or negedge rst_n) begin
    if((rst_n == 1'b0) || (spi_cs == 1'b1))begin
        clk_div_cnt <= 0;
        clk1_en <= 1'b0;
        clk2_en <= 1'b0;
    end
    else if(clk_div_cnt == spi_clk_cnt_max - 1)begin
        clk_div_cnt <= 0;
        clk2_en <= 1'b1;
    end
    else if(clk_div_cnt == spi_clk_cnt_max_div2 - 1)begin
        clk1_en <= 1'b1;
        clk_div_cnt <= clk_div_cnt + 1'b1;
    end
    else begin   
        clk_div_cnt <= clk_div_cnt + 1'b1;
        clk1_en <= 1'b0;
        clk2_en <= 1'b0;
    end
end

//每次第一时钟沿上来,则表示发送完一个数
always @(posedge sys_clk or negedge rst_n) begin
    if((rst_n == 1'b0)||(spi_en == 1'b0))
        tx_cnt <= 'd0;
    else if(clk1_en == 1'b1)
        tx_cnt <= tx_cnt + 1'b1;
    else
        tx_cnt <= tx_cnt;
end

        
always @(posedge sys_clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        spi_strobe_en  <= 1'b0;
    else if(tx_cnt < 4'd8)
        if(clk1_en == 1'b1 )
            spi_strobe_en  <= 1'b1;
        else
            spi_strobe_en  <= spi_strobe_en;
    else
        spi_strobe_en  <= 1'b0;
end
    

always @(posedge sys_clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        spi_clk_temp <= 1'b0;
    else if(clk2_en == 1'b1)
        spi_clk_temp <= 1'b0;
    else if((clk1_en == 1'b1)&&(tx_cnt <4'd8))
        spi_clk_temp <= 1'b1;
    else
        spi_clk_temp <= spi_clk_temp;
end

endmodule

6.3 发送数据控制模块

简单设置发送数据控制模块,发送数据从1开始累加,代码如下:

c 复制代码
`timescale 1ns / 1ps

module spi_top
(
    input                                               sys_clk ,   //输入时钟
    input                                               rst_n   ,   //系统复位
    output                                              spi_clk ,   //SPI发送时钟
    output                                              spi_mosi    //SPI发送数据
    );

    wire                                                spi_cs  ;
    wire                                                spi_busy    ;                                  //SPI忙信号
    reg                                                 spi_tx_req  ;                                  //SPI发送req信号,有发送需求时拉高
    reg             [7:0]                               spi_tx_data ;                                  //待发送数据存储
    reg             [1:0]                               state ;                                        //状态机

//spi send state machine
always @(posedge sys_clk) begin
    if(!rst_n) begin                                  
        spi_tx_req  <= 1'b0;
        spi_tx_data <= 8'd0;
        state <= 2'd0;
    end
    else begin
        case(state)
        0:if(!spi_busy)begin                            //总线不忙启动传输
           spi_tx_req  <= 1'b1;                         //req信号拉高,开始传输
           spi_tx_data <= spi_tx_data + 1'b1;          //测试数据
           state <= 2'd1;
        end
        1:if(spi_busy)begin                             //如果spi总线忙,清除spi_tx_req
           spi_tx_req  <= 1'b0;
           state <= 2'd0;
        end
        default:state <= 2'd0;
        endcase
    end
end   

//例化SPI Master发送驱动器

spi_master_tx#(
    .SYS_CLK_FREQ ( 50000000 ),
    .SPI_CLK_FREQ ( 500000 ),
    .CPOL         ( 1'b0 ),
    .CPHA         ( 1'b0 )
)u_spi_master_tx(
    .sys_clk      ( sys_clk      ),
    .rst_n        ( rst_n        ),
    .spi_tx_req   ( spi_tx_req   ),
    .spi_tx_data  ( spi_tx_data  ),
    .spi_cs       ( spi_cs       ),
    .spi_clk      ( spi_clk      ),
    .spi_busy     ( spi_busy     ),
    .spi_mosi     ( spi_mosi     )
);

endmodule

6.4 仿真代码编写以及仿真结果分析

tb代码如下:

c 复制代码
`timescale 1ns / 1ps
module master_spi_tb;

reg             sys_clk;               //系统时钟
reg     rst_n;  
wire spi_clk;
wire spi_mosi;

spi_top u_spi_top(
.sys_clk(sys_clk),
.rst_n(rst_n),
.spi_clk(spi_clk),
.spi_mosi(spi_mosi)
);

initial begin
    sys_clk  = 1'b0;                              //设置时钟基础值
    rst_n = 1'b0;                              //低电平复位
    #100;
    rst_n = 1'b1;                             //复位释放

 #2000000 $finish;
end
 
always #10 sys_clk = ~sys_clk;     //产生主时钟

endmodule

设置CPOL=0,CPHA=0观看仿真结果,我们随便选取一个数据,例如发送8'd37,结果如下:

8'd37的二进制为00100101,所以采集数据在时钟上升沿,采集到的数据也为00100101,结果正确。

现在修改CPOL=0,CPHA=1观看同样的结果,如下:

采集数据在时钟下升沿,采集到的数据也为00100101,结果正确。

七、Verilog实现SPI从机接收程序

7.1 波形图分析

  1. 接收端首先检测cs线是否拉低,如果拉低了则进入接收状态,拉高rx_en。
  2. 然后根据CPOL和CPHA的数值来找到采集信号的沿。
  3. 每次采集沿到来时,采集数据并进行串并转换。

7.2 Verilog代码

c 复制代码
`timescale 1ns / 1ns
module spi_slave_rx#
(
    parameter                                           BITS_LEN    = 8,    //设置接收bit长度
    parameter                                           CPOL    = 1'b0,     //时钟极性
    parameter                                           CPHA    = 1'b1      //时钟相位
)
(
    input                                               sys_clk ,
    input                                               rst_n   ,
    input                                               spi_cs  ,           //spi片选信号
    input                                               spi_clk ,           //spi_clk
    input                                               spi_mosi    ,       //spi_mosi
    output  reg                                         rx_data_valid   ,   //接收到的数据有效信号
    output  reg     [BITS_LEN - 1:0]                    rx_data             //接收到的信号
);

    reg             [3:0]                               spi_cs_reg  ;       //打四拍
    reg             [3:0]                               spi_clk_reg ;       //打四拍
    reg             [3:0]                               spi_mosi_reg    ;   //打四拍
    reg                                                 cap ;               //采集时刻信号
    reg                                                 spi_clk_pos ;       //上升沿
    reg                                                 spi_clk_neg ;       //下降沿
    wire                                                rx_en   ;           //接收使能信号
    reg             [4:0]                               rx_bit_cnt  ;       //接收bit计数器

assign rx_en  = (~spi_cs_reg[3]);   

//将 cs,spi_clk,mosi信号都打三拍消除亚稳态
always @(posedge sys_clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        spi_cs_reg <= 3'd0;
    else
        spi_cs_reg <= {spi_cs_reg[2:0],spi_cs};
end

always @(posedge sys_clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        spi_clk_reg <= 3'd0;
    else
        spi_clk_reg <= {spi_clk_reg[2:0],spi_clk};
end
always @(posedge sys_clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        spi_mosi_reg <= 3'd0;
    else
        spi_mosi_reg <= {spi_mosi_reg[2:0],spi_mosi};
end

//spi_clk上升沿
always @(posedge sys_clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        spi_clk_pos <= 1'b0;
    else if (spi_clk_reg[2] == 1'b0 && spi_clk_reg[1] == 1'b1)
        spi_clk_pos <= 1'b1;
    else
        spi_clk_pos <= 1'b0;
end

//spi_clk下降沿
always @(posedge sys_clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        spi_clk_neg <= 1'b0;
    else if (spi_clk_reg[2] == 1'b1 && spi_clk_reg[1] == 1'b0)
        spi_clk_neg <= 1'b1;
    else
        spi_clk_neg <= 1'b0;
end

//根据CPOL CPHA来确定采集信号位置
always @(posedge sys_clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        cap <= 1'b0;
    else if(CPOL == 1'b0 && CPHA == 1'b0)
        cap <= spi_clk_pos;
    else if(CPOL == 1'b0 && CPHA == 1'b1)
        cap <= spi_clk_neg;
    else if(CPOL == 1'b1 && CPHA == 1'b0)
        cap <= spi_clk_neg;
    else if(CPOL == 1'b1 && CPHA == 1'b1)
        cap <= spi_clk_pos;
    else
        cap <= 1'b0;
end

//每次采集一次,接收bit计数器累加一次
always @(posedge sys_clk or negedge rst_n) begin
    if(rst_n == 1'b0)begin
        rx_bit_cnt <= 'd0;  
        rx_data_valid <= 1'b0;
    end
    else if((rx_en == 1'b1) && (cap == 1'b1) && (rx_bit_cnt < BITS_LEN))begin
        rx_bit_cnt <= rx_bit_cnt +1'b1;
        rx_data_valid <= 1'b0;
    end
    else if((rx_en == 1'b0) || (rx_bit_cnt == BITS_LEN))begin
        rx_bit_cnt <= 'd0;
        rx_data_valid <= 1'b1;
    end
    else begin
        rx_bit_cnt <= rx_bit_cnt;
        rx_data_valid <= 1'b0;
    end
end

//每次采集时刻到来,进行串并转换
always @(posedge sys_clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        rx_data <= 'd0;
    else if(rx_en == 1'b1 && cap == 1'b1)
        rx_data <= {rx_data[BITS_LEN -2 : 0],spi_mosi_reg[3]};
    else if(rx_en == 1'b0)
        rx_data <= 'd0;
    else
        rx_data <= rx_data;
end

endmodule

7.3 仿真代码以及仿真结果分析

编写tb文件如下:

c 复制代码
`timescale 1ns / 1ps

module tb_spi_slave_rtx();

localparam  BYTES = 8;
localparam  TCNT  = BYTES*8*2-1;

localparam  CPOL = 1;
localparam  CPHA = 1;

reg I_clk; //系统时钟
reg [7:0] i;//计数器,用于产生SPI时钟数量
reg I_rstn; //系统复位
reg I_spi_clk;//SPI时钟
reg I_spi_ss; //SPI的Slave选通信号
reg [3:0]bit_cnt; //bit计数器
reg [7:0]spi_tx_buf; //发送缓存(移位寄存器)
reg [7:0]spi_tx_buf_r; //发送化缓存,用于产生测试数据
reg first_data_flag; //是否一个时钟改变数据

wire O_spi_rvalid; //SPI 数据接收有效,当该信号有效代表接收到一个有效数据
wire [7:0]O_spi_rdata; //SPI读数据
wire I_spi_rx;//SPI数据总线

//tb模拟的SPI测试数据接到I_spi_rx
assign I_spi_rx = spi_tx_buf[7];

//例化SPI 接收模块

spi_slave_rx#(
    .BITS_LEN       ( 8     ),
    .CPOL           ( CPOL  ),
    .CPHA           ( CPHA  )
)u_spi_slave_rx(
    .sys_clk        ( I_clk         ),
    .rst_n          ( I_rstn        ),
    .spi_cs         ( I_spi_ss      ),
    .spi_clk        ( I_spi_clk     ),
    .spi_mosi       ( I_spi_rx      ),
    .rx_data_valid  ( O_spi_rvalid  ),
    .rx_data        ( O_spi_rdata   )
);


initial begin
    I_clk  = 1'b0;
    I_rstn = 1'b0;
    #100;
    I_rstn = 1'b1;
end

always #10   I_clk  = ~I_clk;   //时钟信号翻转,产生系统时钟

initial begin
    #100;
    i = 0;
    
    forever begin
        I_spi_clk = CPOL; //设置时钟极性
        I_spi_ss  = 1; // 设置SPI的SS控制信号
        #2000;
        I_spi_ss  = 0;
        for(i=0;i<TCNT;i=i+1) #1000 I_spi_clk = ~ I_spi_clk; //产生SPI时钟
        #2000;
        I_spi_ss  = 1;

    end
end

initial begin
        #100;
        bit_cnt = 0;
        first_data_flag =0;
        spi_tx_buf[7:0] = 8'ha0;
        spi_tx_buf_r[7:0] = 8'ha0;
    forever begin
//spi ss 控件用于启用传输
        wait(I_spi_ss);//spi ss 
        bit_cnt = 0;
        spi_tx_buf[7:0] = 8'ha0;
        spi_tx_buf_r[7:0] = 8'ha0;

        if((CPHA == 1 && CPOL ==0)||(CPHA == 1 && CPOL ==1))//第一个时钟沿改变数据的情况
            first_data_flag = 1; //设置first_data_flag=1 下面的发送时序对应情况跳过第一个沿

//ss低时开始数据传输          
        wait(!I_spi_ss);

        while(!I_spi_ss)begin


//COPL=0 CPHA=0默认SCLK为低电平,对于发送方,在对于第1个bit数据提前放到总线
            if(CPHA == 0 && CPOL ==0)begin 
             @(negedge I_spi_clk)  begin //每个时钟的下降沿更新需要发送的BIT
                if(bit_cnt == 7)begin//连续发送过程中,8bits 发送完毕后更新数据
                    bit_cnt = 0;
                    spi_tx_buf_r = spi_tx_buf_r + 1'b1;//产生新的测试数据
                    spi_tx_buf = spi_tx_buf_r;//重新跟新发送寄存器
                end
                else begin 
                    spi_tx_buf = {spi_tx_buf[6:0],1'b0};//数据移位,更新数据
                    bit_cnt = bit_cnt + 1'b1;//SPI发送位数计数器
                end
             end
            end

//CPHA=0 COPL=1 默认SCLK为高电平,对于发送方,在对于第1个bit数据提前放到总线
            if(CPHA == 0 && CPOL ==1)begin 
             @(posedge I_spi_clk)  begin //每个时钟的上升沿更新需要发送的BIT
                if(bit_cnt == 7)begin //连续发送过程中,8bits 发送完毕后更新数据
                    bit_cnt = 0;
                    spi_tx_buf_r = spi_tx_buf_r + 1'b1;//产生新的测试数据
                    spi_tx_buf = spi_tx_buf_r; //重新跟新发送寄存器
                end
                else begin
                    spi_tx_buf = {spi_tx_buf[6:0],1'b0};//数据移位,更新数据
                    bit_cnt = bit_cnt + 1'b1;//SPI发送位数计数器
                end
             end
            end

//CPHA=1 COPL=0 默认SCLK为低电平,对于发送方,在第1个SCLK的跳变沿更新
            if(CPHA == 1 && CPOL ==0)begin 
             @(posedge I_spi_clk)  begin
                if(first_data_flag == 1'b1)begin //第一个时钟沿,由于前面已经提前初始化第一个需要发送的数据,因此,这里跳过第一个跳变沿沿
                    first_data_flag = 0;
                    //spi_tx_buf[7:0] = 8'ha0;//也可以在第一个跳变沿初始化第一个发送的数据
                end
                else begin
                    if(bit_cnt == 7)begin
                        bit_cnt = 0;
                        spi_tx_buf_r = spi_tx_buf_r + 1'b1;//产生新的测试数据
                        spi_tx_buf = spi_tx_buf_r;//重新跟新发送寄存器
                    end
                    else begin 
                        spi_tx_buf = {spi_tx_buf[6:0],1'b0}; //数据移位,更新数据
                        bit_cnt = bit_cnt + 1'b1;//SPI发送位数计数器
                    end
                end
             end
            end

//CPHA=1 COPL=1 默认SCLK为高电平,对于发送方,在第1个SCLK的跳变沿更新
            if(CPHA == 1 && CPOL ==1)begin 
             @(negedge I_spi_clk)  begin
                if(first_data_flag == 1'b1)begin //第一个时钟沿,由于前面已经提前初始化第一个需要发送的数据,因此,这里跳过第一个跳变沿沿
                    first_data_flag = 0;
                    //spi_tx_buf[7:0] = 8'ha0;//也可以在第一个跳变沿初始化第一个发送的数据
                end
                else begin
                    if(bit_cnt == 7)begin
                        bit_cnt = 0;
                        spi_tx_buf_r = spi_tx_buf_r + 1'b1;//产生新的测试数据
                        spi_tx_buf = spi_tx_buf_r;//重新跟新发送寄存器
                    end
                    else begin 
                        spi_tx_buf = {spi_tx_buf[6:0],1'b0};//数据移位,更新数据
                        bit_cnt = bit_cnt + 1'b1;//SPI发送位数计数器
                    end
                end
             end
            end

        end
    end
end

endmodule

测试结果如下图所示,上游发送数据a0,接收模块接收到的也是a0 。

八、SPI回环测试

8.1 顶层代码

将spi发送端和spi接收端接起来,通过顶层模拟发送数据。

顶层代码:

c 复制代码
`timescale 1ns / 1ps

module spi_top
(
    input                                               sys_clk ,   //输入时钟
    input                                               rst_n   ,   //系统复位
    input                                               spi_clk_in  ,
    input                                               spi_miso    ,
    output                                              spi_clk ,   //SPI发送时钟
    output                                              spi_mosi    //SPI发送数据

    );

    reg                                                 spi_cs  ;
    wire                                                spi_busy    ;                                  //SPI忙信号
    reg                                                 spi_tx_req  ;                                  //SPI发送req信号,有发送需求时拉高
    reg             [7:0]                               spi_tx_data ;                                  //待发送数据存储
    reg             [1:0]                               state ;     //状态机
    reg             [10:0]                              delay_cnt   ;
    wire                                                rx_data_valid   ;
    wire            [7:0]                               rx_data ;

always @(posedge sys_clk or negedge rst_n) begin
    if(rst_n == 1'b0)
        delay_cnt <= 'd0;
    else if(delay_cnt[10] == 1'b1)
        delay_cnt <= 'd0;
    else
        delay_cnt <= delay_cnt +1'b1;   
end

//spi send state machine
always @(posedge sys_clk) begin
    if(!rst_n) begin                                  
        spi_tx_req  <= 1'b0;
        spi_tx_data <= 8'd0;
        state <= 2'd0;
         spi_cs <= 1'b1;
    end
    else begin
        case(state)
        0:if((delay_cnt[10]==1'b1) &&(spi_busy == 1'b0))begin
            spi_cs <= 1'b1;
            state <= 2'd1;
        end

        1:if((delay_cnt[10]==1'b1) &&(spi_busy == 1'b0))begin
            spi_cs <= 1'b0;
            state <= 2'd2;
        end
        2:if((delay_cnt[10]==1'b1) &&(spi_busy == 1'b0))begin                            //总线不忙启动传输
           spi_tx_req  <= 1'b1;                         //req信号拉高,开始传输
           spi_tx_data <= spi_tx_data + 1'b1;          //测试数据
           state <= 2'd3;
        end
        3:if(spi_busy)begin                             //如果spi总线忙,清除spi_tx_req
           spi_tx_req  <= 1'b0;
           state <= 2'd0;
        end
        default:state <= 2'd0;
        endcase
    end
end   

//例化SPI Master发送驱动器

spi_master_tx#(
    .SYS_CLK_FREQ ( 50000000 ),
    .SPI_CLK_FREQ ( 500000 ),
    .CPOL         ( 1'b0 ),
    .CPHA         ( 1'b1 )
)u_spi_master_tx(
    .sys_clk      ( sys_clk      ),
    .rst_n        ( rst_n        ),
    .spi_tx_req   ( spi_tx_req   ),
    .spi_tx_data  ( spi_tx_data  ),
    .spi_cs       (                ),
    .spi_clk      ( spi_clk      ),
    .spi_busy     ( spi_busy     ),
    .spi_mosi     ( spi_mosi     )
);

spi_slave_rx#(
    .BITS_LEN       ( 8 ),
    .CPOL           ( 1'b0 ),
    .CPHA           ( 1'b1 )
)u_spi_slave_rx(
    .sys_clk        ( sys_clk        ),
    .rst_n          ( rst_n          ),
    .spi_cs         ( spi_cs         ),
    .spi_clk        ( spi_clk_in     ),
    .spi_mosi       ( spi_miso       ),
    .rx_data_valid  ( rx_data_valid  ),
    .rx_data        ( rx_data        )
);

ila_0 u_ila (
	.clk(sys_clk), // input wire clk
	.probe0(rx_data_valid), // input wire [0:0]  probe0  
	.probe1(rx_data) // input wire [7:0]  probe1
);

endmodule

8.2 仿真代码以及仿真结果分析

c 复制代码
`timescale 1ns / 1ps
module master_spi_tb;

reg             sys_clk;               //系统时钟
reg     rst_n;  
wire spi_clk;
wire spi_mosi;

spi_top u_spi_top(
.sys_clk(sys_clk),
.rst_n(rst_n),
.spi_clk(spi_clk),
.spi_mosi(spi_mosi),
.spi_clk_in(spi_clk),
.spi_miso(spi_mosi)
);

initial begin
    sys_clk  = 1'b0;                          //设置时钟基础值
    rst_n = 1'b0;                             //低电平复位
    #100;
    rst_n = 1'b1;                             //复位释放

 #2000000 $finish;
end
 
always #10 sys_clk = ~sys_clk;     //产生主时钟

endmodule

仿真结果如下:

顶层模拟发送累加数字给发送端,发送端再连接到接收端,数据接收一致。

8.3 上板验证

我们例化一个ILA,抓取spi_rx模块里的rx_data和rx_data_valid信号,并且打开捕获模式。具体捕获模式原理以及怎么使用,请看Vivado ILA Capture Control 模式与 Advanced Trigger的功能使用以及TSM(触发状态机)的编写,配置图如下:

下载后,我们捕获模式设置BASIC,触发条件选择rx_data_valid。观察波形,发现是连续累加的数据,表面SPI回环成功。

参考

《introduction-to-spi-interface》

相关推荐
fei_sun10 小时前
【Verilog】第一章作业
fpga开发·verilog
深圳市雷龙发展有限公司longsto10 小时前
基于FPGA(现场可编程门阵列)的SD NAND图片显示系统是一个复杂的项目,它涉及硬件设计、FPGA编程、SD卡接口、NAND闪存控制以及图像显示等多个方面
fpga开发
9527华安15 小时前
FPGA实现PCIE3.0视频采集转10G万兆UDP网络输出,基于XDMA+GTH架构,提供工程源码和技术支持
网络·fpga开发·udp·音视频·xdma·pcie3.0·万兆网
able陈15 小时前
为什么verilog中递归函数需要定义为automatic?
fpga开发
fei_sun16 小时前
【Verilog】第二章作业
fpga开发·verilog
碎碎思16 小时前
如何使用 Vivado 从源码构建 Infinite-ISP FPGA 项目
fpga开发·接口隔离原则
江山如画,佳人北望19 小时前
fpga-状态机的设计及应用
fpga开发
晓晓暮雨潇潇20 小时前
Xilinx IP核(3)XADC IP核
fpga开发·vivado·xadc·ip核
CWNULT20 小时前
AMD(Xilinx) FPGA配置Flash大小选择
fpga开发
碎碎思1 天前
很能体现FPGA硬件思维的一道面试题
fpga开发