[FPGA入门笔记](十三):SPI同步串行总线协议及控制器设计

简介

今天购买了AXLINX AX7020的开发板,从今天开始每一个例程都要做文档记录,为自己加油。
本实验,基于ALINX AX7020开发板,芯片为xc7z020clg400-2。开发板输入时钟为50MHz。

一、SPI协议介绍

**SPI(serialperipheralinterface)**是一种同步串行通信协议,由一个主设备和一个或多个从设备组成,主设备启动与从设备的同步通信,从而完成数据的交换。SPI是Motorola公司提出的一种同步串行数据传输标准,是一种高速的,全双工,同步的通信总线,在很多器件中被广泛应用。
SPI是一种高速全双工同步通信总线,标准的SPI仅仅使用4个引脚,常用于主设备和外设(如EEPROM、FLASH、实时时钟和数字信号处理器等器)的通信。
SPI基于主从方式通信,标准的SPI的4根线分别是SSEL(片选,也写作SCS)、SCLK(时钟)、MOSI(主机输出从机输入)和MISO(主机输入从机输出)。这个信号的具体说明如下:
在这里插入图片描述
(1)SSEL:从设备片选使能信号。如果从设备是低电平使能,那么拉低这个引脚,从设备将会被选中,主机和这个被选中的从机进行通信。
(2)SCLK:串行时钟线,作用是Master向Slave传输时钟信号,控制数据交换的时机和速率;
(3)MOSI:在SPI Master上也被称为Tx-channel,作用是SPI主机给SPI从机发送指令或者数据的通道;
(4)MISO:在SPI Master上也被称为Rx-channel,作用是SPI主机接收SPI从机传输过来状态或者数据的通道。

二、SPI总线主要有以下几个特点:

优点:
支持全双工
支持高速
协议支持字长不限于8bit,可以根据应用灵活选择消息字长。
硬件连接简单
缺点:
相比I2C多两条线
没有寻址机制,只能靠片选选择不同的设备
没有回应ACK机制,主设备不知道消息发送是否成功
典型应用仅支持单主控

**1、采用主从模式(Master-Slave)的控制方式,支持单Master多Slave。**SPI规定了两个SPI设备之间通信必须由主设备Master来控制从设备Slave。也就是说,如果FPGA是主机的情况下,不管是FPGA给芯片发送数据还是从芯片中接收数据,写Verilog逻辑的时候片选信号CS与串行时钟信号SCLK必须由FPGA来产生。同时一个Master可以设置多个片选(Chip Select)来控制多个Slave。SPI协议还规定Slave设备的clock由Master通过SCLK管脚提供给Slave,Slave本身不能产生或控制clock,没有clock则Slave不能正常工作。单Master多Slave的典型结构如下图所示
在这里插入图片描述
2、SPI总线在传输数据的同时也传输了时钟信号,所以SPI协议是一种同步(Synchronous)传输协议。Master会根据将要交换的数据产生相应的时钟脉冲,组成时钟信号。时钟信号通过时钟极性(CPOL)和时钟相位(CPHA)控制两个SPI设备何时交换数据以及何时对接收数据进行采样,保证数据在两个设备之间是同步传输的。(1)CPOL:clock polarity,时钟的极性。通信整个过程分为空闲时刻和通信时刻,SCLK在数据发送前后的空闲状态是高电平,那么CPOL为1,否则为0。(2)CPHA:clock phase,时钟的相位。
3、SPI总线协议是一种全双工的串行通信协议,数据传输时高位在前,低位在后。SPI协议规定一个SPI设备不能在数据通信过程中仅仅充当一个发送者(Transmitter)或者接受者(Receiver)。在片选信号CS为0的情况下,每个clock周期内,SPI设备都会发送并接收1 bit数据,相当于有1 bit数据被交换了。数据传输高位在前,低位在后(MSB first)。

三、SPI总线传输的模式:

SPI总线传输一共有4中模式,这4种模式分别由时钟极性(CPOL,Clock Polarity)和时钟相位(CPHA,Clock Phase)来定义,其中CPOL参数规定了SCK时钟信号空闲状态的电平,CPHA规定了数据是在SCK时钟的上升沿被采样还是下降沿被采样。这四种模式的时序图如下图所示:
在这里插入图片描述
模式0:CPOL= 0,CPHA=0。SCK串行时钟线空闲是为低电平,数据在SCK时钟的上升沿被采样,数据在SCK时钟的下降沿切换
模式1:CPOL= 0,CPHA=1。SCK串行时钟线空闲是为低电平,数据在SCK时钟的下降沿被采样,数据在SCK时钟的上升沿切换
模式2:CPOL= 1,CPHA=0。SCK串行时钟线空闲是为高电平,数据在SCK时钟的下降沿被采样,数据在SCK时钟的上升沿切换
模式3:CPOL= 1,CPHA=1。SCK串行时钟线空闲是为高电平,数据在SCK时钟的上升沿被采样,数据在SCK时钟的下降沿切换
其中比较常用的模式是模式0和模式3。为了更清晰的描述SPI总线的时序,下面展现了模式0下的SPI时序图
在这里插入图片描述
上图清晰的表明在模式0下,在空闲状态下,SCK串行时钟线为低电平,当SS被主机拉低以后,数据传输开始,数据线MOSI和MISO的数据切换(Toggling)发生在时钟的下降沿(上图的黑色虚线),而数据线MOSI和MISO的数据的采样(Sampling)发生在数据的正中间(上图中的灰色实线)。下图清晰的描述了其他三种模式数据线MOSI和MISO的数据切换(Toggling)位置和数据采样位置的关系图
在这里插入图片描述

四、SPI模块设计

下面我将以模式0为例用Verilog编写SPI通信的代码。
SPI模块的接口定义与整体设计
Verilog编写的SPI模块除了进行SPI通信的四根线以外还要包括一些时钟、复位、使能、并行的输入输出以及完成标志位。其框图如下所示
在这里插入图片描述
其中:I_clk是系统时钟;
I_rst_n是系统复位;
I_tx_en是主机给从机发送数据的使能信号,当I_tx_en为1时主机才能给从机发送数据;
I_rx _en是主机从从机接收数据的使能信号,当I_rx_en为1时主机才能从从机接收数据;
I_data_in是主机要发送的并行数据;
O_data_out是把从机接收回来的串行数据并行化以后的并行数据;
O_tx_done是主机给从机发送数据完成的标志位,发送完成后会产生一个高脉冲;
O_rx_done是主机从从机接收数据完成的标志位,接收完成后会产生一个高脉冲;
I_spi_miso、O_spi_cs、O_spi_sck和O_spi_mosi是标准SPI总线协议规定的四根线;
要想实现上文模式0的时序,最简单的办法还是设计一个状态机。为了方便说明,这里把模式0的时序再在下面贴一遍
在这里插入图片描述
由于是要用FPGA去控制或读写QSPI Flash,所以FPGA是SPI主机,QSPI是SPI从机。
Verilog代码共需要三部分:
(一)串行时钟信号SCLK 片选信号chip select
要实现上文模式0的时序,需要在tx/rx使能信号拉高后,立刻产生片选信号,并且在一个时钟周期(clk)后反转SCLK;
(二)生成主机要发送的并行数据发送:
当FPGA通过SPI总线往QSPI Flash中发送一个字节(8-bit)的数据时,首先FPGA把CS/SS片选信号设置为0,表示准备开始发送数据,整个发送数据过程其实可以分为16个状态:
状态0:MOSI为要发送的数据的最高位,即I_data_in[7] 状态1:MOSI保持不变
状态2:MOSI为要发送的数据的次高位,即I_data_in[6] 状态3:MOSI保持不变
状态4:MOSI为要发送的数据的下一位,即I_data_in[5] 状态5:MOSI保持不变
状态6:MOSI为要发送的数据的下一位,即I_data_in[4] 状态7:MOSI保持不变
状态8:MOSI为要发送的数据的下一位,即I_data_in[3] 状态9:MOSI保持不变
状态10:MOSI为要发送的数据的下一位,即I_data_in[2] 状态11:MOSI保持不变
状态12:MOSI为要发送的数据的下一位,即I_data_in[1] 状态13:MOSI保持不变
状态14:MOSI为要发送的数据的最低位,即I_data_in[0] 状态15:MOSI保持不变
一个字节数据发送完毕以后,产生一个发送完成标志位O_tx_done并把CS/SS信号拉高完成一次发送。通过观察上面的状态可以发现状态编号为奇数的状态要做的操作实际上是一模一样的,所以写代码的时候为了精简代码,可以把状态号为奇数的状态全部整合到一起。
(三)接收从机串行数据并行化,接收:
当FPGA通过SPI总线从QSPI Flash中接收一个字节(8-bit)的数据时,首先FPGA把CS/SS片选信号设置为0,表示准备开始接收数据,整个接收数据过程其实也可以分为16个状态,但是与发送过程不同的是,为了保证接收到的数据准确,必须在数据的正中间采样,也就是说模式0时序图中灰色实线的地方才是代码中锁存数据的地方,所以接收过程的每个状态执行的操作为:
状态0:不锁存MISO上的数据 状态1:锁存MISO上的数据,即O_data_out[7]= MISO
状态2:不锁存MISO上的数据 状态3:锁存MISO上的数据,即O_data_out[6]= MISO
状态4:不锁存MISO上的数据 状态5:锁存MISO上的数据,即O_data_out[5]= MISO
状态6:不锁存MISO上的数据 状态7:锁存MISO上的数据,即O_data_out[4]= MISO
状态8:不锁存MISO上的数据 状态9:锁存MISO上的数据,即O_data_out[3]= MISO
状态10:不锁存MISO上的数据 状态11:锁存MISO上的数据,即O_data_out[2]= MISO
状态12:不锁存MISO上的数据 状态13:锁存MISO上的数据,即O_data_out[1]= MISO
状态14:不锁存MISO上的数据 状态15:锁存MISO上的数据,即O_data_out[0]= MISO
一个字节数据接收完毕以后,产生一个接收完成标志位O_rx_done并把CS/SS信号拉高完成一次数据的接收。通过观察上面的状态可以发现状态编号为偶数的状态要做的操作实际上是一模一样的,所以写代码的时候为了精简代码,可以把状态号为偶数的状态全部整合到一起。而这一点刚好与发送过程的状态刚好相反。

五、SPI模块Verilog代码

思路理清楚以后就可以直接编写Verilog代码了,spi_master模块的代码如下:

`timescale 1ns/1ps
//this spi module is the mode 0; 该模块遵从模式0的时序
module spi_master#(
    localparam  DATA_WIDTH = 8
    )(
    input                       clk,
    input                       rst_n,
    input                       i_tx_en,        //主机给从机发送数据的使能信号
    input                       i_rx_en,        //主机接收从机数据的使能信号
    
    input   [DATA_WIDTH-1:0]    i_data_in,      //主机要发送的并行数据
    output  [DATA_WIDTH-1:0]    o_data_out,     //接收回来的从机串行数据并行化以后的并行数据
    
    output                      o_tx_done,      //发送数据完成的标志位
    output                      o_rx_done,      //接收数据完成的标志位
    SPI协议的标准四线
    output                      o_spi_cs,       //chip select 片选
    output                      o_spi_clk,      //串行时钟线
    input                       i_spi_miso,     //SPI主机给SPI从机发送指令或者数据的通道
    output                      o_spi_mosi      //SPI主机接收SPI从机传输过来状态或者数据的通道
    );
    
    
    //串行时钟线SCLK  片选 chip select
    reg     sclk;       
    reg     mode_clk;   //该信号用来使得 SCLK信号满足模式0要求 
    reg     spi_cs;     //片选 chip select
    
    always@(posedge clk or negedge rst_n)
    begin
        if(!rst_n)
            begin
                sclk     <= 1'b0;
                mode_clk <= 1'b0;
                spi_cs   <= 1'b1;
            end
        else begin
            if(i_tx_en|i_rx_en)
                begin
                    spi_cs   <= 1'b0;
                    if(mode_clk==1'b0)
                        begin
                        sclk     <= 1'b0;
                        mode_clk <= 1'b1;
                        end
                    else
                        sclk <= ~sclk;
                end
            else
                begin
                    sclk     <= 1'b0;
                    mode_clk <= 1'b0;
                    spi_cs   <= 1'b1;
                end
            end
    end
    assign  o_spi_clk = sclk;
    assign  o_spi_cs  = spi_cs  ;
    
    //主机要发送的并行数据
    reg     [3:0]       state_tx;
    reg                 tx_done ;
    reg                 spi_mosi;
    always@(posedge clk or negedge rst_n)
    begin
        if(!rst_n)
            begin
                state_tx <= 4'd0;
                spi_mosi <= 1'b0;
                tx_done  <= 1'b0;
            end
        else if(i_tx_en) // 发送使能信号打开的情况下
            begin
                case(state_tx)
                    4'd1, 4'd3 , 4'd5 , 4'd7  , 
                    4'd9, 4'd11, 4'd13, 4'd15 : //整合奇数状态
                        begin
                            state_tx  <=  state_tx + 1'b1;
                            spi_mosi  <=  spi_mosi  ;
                            tx_done   <=  1'b0      ;
                        end
                    4'd0:    // 发送第7位
                        begin
                            state_tx  <=  state_tx + 1'b1;
                            spi_mosi  <=  i_data_in[7]  ;
                            tx_done   <=  1'b0          ;
                        end
                    4'd2:    // 发送第6位
                        begin
                            state_tx  <=  state_tx + 1'b1;
                            spi_mosi  <=  i_data_in[6]  ;
                            tx_done   <=  1'b0          ;
                        end
                    4'd4:    // 发送第5位
                        begin
                            state_tx  <=  state_tx + 1'b1;
                            spi_mosi  <=  i_data_in[5]  ;
                            tx_done   <=  1'b0          ;
                        end
                    4'd6:    // 发送第4位
                        begin
                            state_tx  <=  state_tx + 1'b1;
                            spi_mosi  <=  i_data_in[4]  ;
                            tx_done   <=  1'b0          ;
                        end
                    4'd8:    // 发送第3位
                        begin
                            state_tx  <=  state_tx + 1'b1;
                            spi_mosi  <=  i_data_in[3]  ;
                            tx_done   <=  1'b0          ;
                        end
                    4'd10:    // 发送第2位
                        begin
                            state_tx  <=  state_tx + 1'b1;
                            spi_mosi  <=  i_data_in[2]  ;
                            tx_done   <=  1'b0          ;
                        end
                    4'd12:    // 发送第1位
                        begin
                            state_tx  <=  state_tx + 1'b1;
                            spi_mosi  <=  i_data_in[1]  ;
                            tx_done   <=  1'b0          ;
                        end
                    4'd14:    // 发送第0位
                        begin
                            state_tx  <=  state_tx + 1'b1;
                            spi_mosi  <=  i_data_in[0]  ;
                            tx_done   <=  1'b1          ;
                        end
                    default:
                        begin
                            state_tx  <= 4'd0;
                            spi_mosi  <= 1'b0;
                            tx_done   <= 1'b0;
                        end
                endcase
            end
    end
    
    assign o_spi_mosi = spi_mosi;
    assign o_tx_done  = tx_done ;
    
    //接收回来的从机串行数据并行化以后的并行数据
    reg     [3:0]               state_rx;
    reg     [DATA_WIDTH-1:0]    data_out;
    reg                         rx_done ;
    always@(posedge clk or negedge rst_n)
    begin
        if(!rst_n)
            begin
                state_rx <= 4'd0;
                data_out <= {DATA_WIDTH{1'b0}};
                rx_done  <= 1'b0;
            end
        else if(i_rx_en) //接收使能信号打开的情况下
            begin
                case(state_rx)
                    4'd0, 4'd2 , 4'd4 , 4'd6  , 
                    4'd8, 4'd10, 4'd12, 4'd14 : //整合偶数状态
                        begin
                            state_rx  <=  state_rx + 1'b1;
                            data_out  <=  data_out;
                            rx_done   <=  1'b0;
                        end
                    4'd1:    // 接收第7位
                        begin                       
                            state_rx    <=  state_rx + 1'b1;
                            data_out[7] <=  i_spi_miso;
                            rx_done     <=  1'b0;
                        end
                    4'd3:    // 接收第6位
                        begin                       
                            state_rx    <=  state_rx + 1'b1;
                            data_out[6] <=  i_spi_miso;
                            rx_done     <=  1'b0;
                        end
                    4'd5:    // 接收第5位
                        begin                       
                            state_rx    <=  state_rx + 1'b1;
                            data_out[5] <=  i_spi_miso;
                            rx_done     <=  1'b0;
                        end
                    4'd7:    // 接收第4位
                        begin                       
                            state_rx    <=  state_rx + 1'b1;
                            data_out[4] <=  i_spi_miso;
                            rx_done     <=  1'b0;
                        end
                    4'd9:    // 接收第3位
                        begin                       
                            state_rx    <=  state_rx + 1'b1;
                            data_out[3] <=  i_spi_miso;
                            rx_done     <=  1'b0;
                        end
                    4'd11:    // 接收第2位
                        begin                       
                            state_rx    <=  state_rx + 1'b1;
                            data_out[2] <=  i_spi_miso;
                            rx_done     <=  1'b0;
                        end
                    4'd13:    // 接收第1位
                        begin                       
                            state_rx    <=  state_rx + 1'b1;
                            data_out[1] <=  i_spi_miso;
                            rx_done     <=  1'b0;
                        end
                    4'd15:    // 接收第0位
                        begin                       
                            state_rx    <=  state_rx + 1'b1;
                            data_out[0] <=  i_spi_miso;
                            rx_done     <=  1'b1;
                        end
                    default:
                        begin
                            state_rx  <=  state_rx + 1'b1;
                            data_out  <=  data_out;
                            rx_done   <=  1'b0;
                        end
                endcase
            end
    end
    
    assign  o_data_out = data_out;
    assign  o_rx_done  = rx_done ; 

endmodule

测试代码:

`timescale 1ns/1ps
module tb_spi_master;
    localparam  DATA_WIDTH = 8;
    reg                         clk;
    reg                         rst_n;
    reg                         i_tx_en;
    reg                         i_rx_en;
    reg     [DATA_WIDTH-1:0]    i_data_in;      //主机要发送的并行数据
    wire    [DATA_WIDTH-1:0]    o_data_out;     //接收回来的从机串行数据并行化以后的并行数据
    
    wire                        o_tx_done;      //发送数据完成的标志位
    wire                        o_rx_done;      //接收数据完成的标志位
    SPI协议的标准四线
    wire                        o_spi_cs;       //chip select 片选
    wire                        o_spi_clk;      //串行时钟线
    reg                         i_spi_miso;     //SPI主机给SPI从机发送指令或者数据的通道
    wire                        o_spi_mosi;     //SPI主机接收SPI从机传输过来状态或者数据的通道
    
    
    reg     [3:0]    count;
    reg     [DATA_WIDTH-1:0]    miso_buf;
    
    
    always  #10  clk = ~clk;
    
    initial
    begin
        clk        = 0;
        rst_n      = 0;
        i_tx_en    = 0;
        i_rx_en    = 0;
        i_spi_miso = 0;
        count      = 4'd0;
        miso_buf   = 8'b1001_1010;
        i_data_in  = 8'b0101_0011;

        #100
        rst_n      = 1;
        i_tx_en    = 1;
        i_rx_en    = 1;
        miso_data;
    end
    
    task miso_data;
    begin
        repeat(10000)
        begin
            @(negedge o_spi_clk or negedge o_spi_cs);
            if(count==4'd7)
                begin
                    count <= 4'd0;
                    i_spi_miso <= 1'b0;
                end
            else
                begin
                    count <= count  +  1'b1 ;
                    i_spi_miso <= miso_buf[count];
                end
        end
    end
    endtask
    
    spi_master uut //Instantiate the Unit Under Test (UUT)
    (
    .clk(clk),
    .rst_n(rst_n)       ,
    .i_tx_en(i_tx_en)   ,          //主机给从机发送数据的使能信号
    .i_rx_en(i_rx_en)   ,          //主机接收从机数据的使能信号
    
    .i_data_in(i_data_in),         //主机要发送的并行数据
    .o_data_out(o_data_out),       //接收回来的从机串行数据并行化以后的并行数据
    
    .o_tx_done(o_tx_done),         //发送数据完成的标志位
    .o_rx_done(o_rx_done),         //接收数据完成的标志位
    SPI协议的标准四线
    .o_spi_cs(o_spi_cs),           //chip select 片选
    .o_spi_clk(o_spi_clk),         //串行时钟线
    .i_spi_miso(i_spi_miso),       //SPI主机给SPI从机发送指令或者数据的通道
    .o_spi_mosi(o_spi_mosi)        //SPI主机接收SPI从机传输过来状态或者数据的通道
    );
endmodule

时序仿真波形
在这里插入图片描述

参考博客:

一、https://blog.csdn.net/weixin_30851409/article/details/97402076
这一篇的总体设计思想很好,本文主要参考了这篇文章,但是,这篇文章的代码没有实现全双工通信。
在这里插入图片描述
二、https://blog.csdn.net/IamSarah/article/details/76269737
这篇文章产生的SPI输出信号有一个时钟周期的延时,原因是在设计的时候SPI数据输出依赖于SCLK,这样就导致输出数据比SCLK晚一拍,设计不合理。应该将SCLK和输入输出数据分别设计。
在这里插入图片描述

整理不易,点个赞呗~~!!!

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值