Verilog笔记——SPI协议

基本概念

        SPI总线传输主要利用四根线:

        SCK(Serial Clock):SCK是串行时钟线,作用是Master向Slave传输时钟信号,控制数据交换的时机和速率;

        MOSI(Master Out Slave in):在SPI Master上也被称为Tx-channel,作用是SPI主机给SPI从机发送数据;

        CS/SS(Chip Select/Slave Select):作用是SPI Master选择与哪一个SPI Slave通信,低电平表示从机被选中(低电平有效);

        MISO(Master In Slave Out):在SPI Master上也被称为Rx-channel,作用是SPI主机接收SPI从机传输过来的数据;

特点:

        1、 采用主从模式(Master-Slave)的控制方式,支持单Master多Slave。写Verilog逻辑的时候片选信号CS与串行时钟信号SCK必须由FPGA来产生。同时一个Master可以设置多个片选(Chip Select)来控制多个Slave。Slave设备的clock由Master通过SCK管脚提供给Slave,Slave本身不能产生或控制clock,没有clock则Slave不能正常工作。单Master多Slave的典型结构如下图所示:

        2、时钟信号通过时钟极性(CPOL)和时钟相位(CPHA)控制两个SPI设备何时交换数据以及何时对接收数据进行采样,保证数据的同步传输。

        3、 SPI总线协议是一种全双工的串行通信协议,数据传输时高位在前,低位在后。SPI协议规定一个SPI设备不能在数据通信过程中仅仅充当一个发送者(Transmitter)或者接受者(Receiver)。在片选信号CS为0的情况下,每个clock周期内,SPI设备都会发送并接收1 bit数据,相当于有1 bit数据被交换了。

        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,模式0的时序图如下:

        空闲状态下,SCK串行时钟线为低电平,当SS被主机拉低以后,数据传输开始,数据线MOSI和MISO的数据切换(Toggling)发生在时钟的下降沿(黑色虚线),而数据线MOSI和MISO的数据的采样(Sampling)发生在数据的正中间(灰色实线)。下图清晰的描述了其他三种模式数据线MOSI和MISO的数据切换(Toggling)位置和数据采样位置的关系图:

SPI协议的FPGA实现

        下面以模式0为例用Verilog编写SPI通信的代码。编写SPI通信的Verilog代码并利用Vivado进行时序仿真。 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的时序,可以设计一个状态机。

        发送:当FPGA通过SPI总线往QSPI Flash中发送一个字节(8-bit)的数据时,首先FPGA把CS/SS片选信号设置为0,表示准备开始发送数据,整个发送数据过程其实可以分为16个状态:

        状态0:SCK为0,MOSI为要发送的数据的最高位,即I_data_in[7]

        状态1:SCK为1,MOSI保持不变

        状态2:SCK为0,MOSI为要发送的数据的次高位,即I_data_in[6]

        状态3:SCK为1,MOSI保持不变

        ······

        状态14:SCK为0,MOSI为要发送的数据的最低位,即I_data_in[0]

        状态15:SCK为1,MOSI保持不变

        一个字节数据发送完毕以后,产生一个发送完成标志位O_tx_done并把CS/SS信号拉高完成一次发送。通过观察上面的状态可以发现状态编号为奇数的状态要做的操作实际上是一模一样的,所以写代码的时候为了精简代码,可以把状态号为奇数的状态全部整合到一起。

        接收:当FPGA通过SPI总线从QSPI Flash中接收一个字节(8-bit)的数据时,首先FPGA把CS/SS片选信号设置为0,表示准备开始接收数据,整个接收数据过程其实也可以分为16个状态,但是与发送过程不同的是,为了保证接收到的数据准确,必须在数据的正中间采样,也就是说模式0时序图中灰色实线的地方才是代码中锁存数据的地方,所以接收过程的每个状态执行的操作为:

        状态0:SCK为0,不锁存MISO上的数据

        状态1:SCK为1,锁存MISO上的数据,即把MISO上的数据赋值给O_data_out[7]

        状态2:SCK为0,不锁存MISO上的数据

        状态3:SCK为1,锁存MISO上的数据

        ······

        状态14:SCK为0,不锁存MISO上的数据

        状态15:SCK为1,锁存MISO上的数据,即把MISO上的数据赋值给O_data_out[0]

         与发送类似,一个字节数据接收完毕以后,产生一个接收完成标志位O_rx_done并把CS/SS信号拉高完成一次数据的接收。同时可以把状态号为偶数的状态全部整合到一起以精简代码。

spi_module模块的代码如下:

module spi_module
(
    input               I_clk       , // 全局时钟50MHz
    input               I_rst_n     , // 复位信号,低电平有效
    input               I_rx_en     , // 读使能信号
    input               I_tx_en     , // 发送使能信号
    input        [7:0]  I_data_in   , // 要发送的数据
    output  reg  [7:0]  O_data_out  , // 接收到的数据
    output  reg         O_tx_done   , // 发送一个字节完毕标志位
    output  reg         O_rx_done   , // 接收一个字节完毕标志位
    
    // 四线标准SPI信号定义
    input               I_spi_miso  , // SPI串行输入,用来接收从机的数据
    output  reg         O_spi_sck   , // SPI时钟
    output  reg         O_spi_cs    , // SPI片选信号
    output  reg         O_spi_mosi    // SPI输出,用来给从机发送数据          
);
 
reg [3:0]   R_tx_state      ; 
reg [3:0]   R_rx_state      ;
 
always @(posedge I_clk or negedge I_rst_n)
begin
    if(!I_rst_n)
        begin
            R_tx_state  <=  4'd0    ;
            R_rx_state  <=  4'd0    ;
            O_spi_cs    <=  1'b1    ;
            O_spi_sck   <=  1'b0    ;
            O_spi_mosi  <=  1'b0    ;
            O_tx_done   <=  1'b0    ;
            O_rx_done   <=  1'b0    ;
            O_data_out  <=  8'd0    ;
        end 
    else if(I_tx_en) // 发送使能信号打开的情况下
        begin
            O_spi_cs    <=  1'b0    ; // 把片选CS拉低
            case(R_tx_state)
                4'd1, 4'd3 , 4'd5 , 4'd7  , 
                4'd9, 4'd11, 4'd13, 4'd15 : //整合奇数状态
                    begin
                        O_spi_sck   <=  1'b1                ;
                        R_tx_state  <=  R_tx_state + 1'b1   ;
                        O_tx_done   <=  1'b0                ;
                    end
                4'd0:    // 发送第7位
                    begin
                        O_spi_mosi  <=  I_data_in[7]        ;
                        O_spi_sck   <=  1'b0                ;
                        R_tx_state  <=  R_tx_state + 1'b1   ;
                        O_tx_done   <=  1'b0                ;
                    end
                4'd2:    // 发送第6位
                    begin
                        O_spi_mosi  <=  I_data_in[6]        ;
                        O_spi_sck   <=  1'b0                ;
                        R_tx_state  <=  R_tx_state + 1'b1   ;
                        O_tx_done   <=  1'b0                ;
                    end
                4'd4:    // 发送第5位
                    begin
                        O_spi_mosi  <=  I_data_in[5]        ;
                        O_spi_sck   <=  1'b0                ;
                        R_tx_state  <=  R_tx_state + 1'b1   ;
                        O_tx_done   <=  1'b0                ;
                    end 
                4'd6:    // 发送第4位
                    begin
                        O_spi_mosi  <=  I_data_in[4]        ;
                        O_spi_sck   <=  1'b0                ;
                        R_tx_state  <=  R_tx_state + 1'b1   ;
                        O_tx_done   <=  1'b0                ;
                    end 
                4'd8:    // 发送第3位
                    begin
                        O_spi_mosi  <=  I_data_in[3]        ;
                        O_spi_sck   <=  1'b0                ;
                        R_tx_state  <=  R_tx_state + 1'b1   ;
                        O_tx_done   <=  1'b0                ;
                    end                            
                4'd10:    // 发送第2位
                    begin
                        O_spi_mosi  <=  I_data_in[2]        ;
                        O_spi_sck   <=  1'b0                ;
                        R_tx_state  <=  R_tx_state + 1'b1   ;
                        O_tx_done   <=  1'b0                ;
                    end 
                4'd12:    // 发送第1位
                    begin
                        O_spi_mosi  <=  I_data_in[1]        ;
                        O_spi_sck   <=  1'b0                ;
                        R_tx_state  <=  R_tx_state + 1'b1   ;
                        O_tx_done   <=  1'b0                ;
                    end 
                4'd14:    // 发送第0位
                    begin
                        O_spi_mosi  <=  I_data_in[0]        ;
                        O_spi_sck   <=  1'b0                ;
                        R_tx_state  <=  R_tx_state + 1'b1   ;
                        O_tx_done   <=  1'b1                ;
                    end
                default:R_tx_state  <=  4'd0                ;   
            endcase 
        end
    else if(I_rx_en) // 接收使能信号打开的情况下
        begin
            O_spi_cs    <=  1'b0        ; // 拉低片选信号CS
            case(R_rx_state)
                4'd0,4'd2,4'd4,4'd6, 
                4'd8,4'd10,4'd12,4'd14: //整合偶数状态
                begin
                        O_spi_sck<=1'b0;
                        R_rx_state<=  R_rx_state + 1'b1   ;
                        O_rx_done<=  1'b0;
                    end
                4'd1:    // 接收第7位
                    begin                       
                        O_spi_sck       <=  1'b1                ;
                        R_rx_state      <=  R_rx_state + 1'b1   ;
                        O_rx_done       <=  1'b0                ;
                        O_data_out[7]   <=  I_spi_miso          ;   
                    end
                4'd3:    // 接收第6位
                    begin
                        O_spi_sck       <=  1'b1                ;
                        R_rx_state      <=  R_rx_state + 1'b1   ;
                        O_rx_done       <=  1'b0                ;
                        O_data_out[6]   <=  I_spi_miso          ; 
                    end
                4'd5:    // 接收第5位
                    begin
                        O_spi_sck       <=  1'b1                ;
                        R_rx_state      <=  R_rx_state + 1'b1   ;
                        O_rx_done       <=  1'b0                ;
                        O_data_out[5]   <=  I_spi_miso          ; 
                    end 
                4'd7:    // 接收第4位
                    begin
                        O_spi_sck       <=  1'b1                ;
                        R_rx_state      <=  R_rx_state + 1'b1   ;
                        O_rx_done       <=  1'b0                ;
                        O_data_out[4]   <=  I_spi_miso          ; 
                    end 
                4'd9:    // 接收第3位
                    begin
                        O_spi_sck       <=  1'b1                ;
                        R_rx_state      <=  R_rx_state + 1'b1   ;
                        O_rx_done       <=  1'b0                ;
                        O_data_out[3]   <=  I_spi_miso          ; 
                    end                            
                4'd11:    // 接收第2位
                    begin
                        O_spi_sck       <=  1'b1                ;
                        R_rx_state      <=  R_rx_state + 1'b1   ;
                        O_rx_done       <=  1'b0                ;
                        O_data_out[2]   <=  I_spi_miso          ; 
                    end 
                4'd13:    // 接收第1位
                    begin
                        O_spi_sck       <=  1'b1                ;
                        R_rx_state      <=  R_rx_state + 1'b1   ;
                        O_rx_done       <=  1'b0                ;
                        O_data_out[1]   <=  I_spi_miso          ; 
                    end 
                4'd15:    // 接收第0位
                    begin
                        O_spi_sck       <=  1'b1                ;
                        R_rx_state      <=  R_rx_state + 1'b1   ;
                        O_rx_done       <=  1'b1                ;
                        O_data_out[0]   <=  I_spi_miso          ; 
                    end
                default:R_rx_state  <=  4'd0                    ;   
            endcase 
        end    
    else
        begin
            R_tx_state  <=  4'd0    ;
            R_rx_state  <=  4'd0    ;
            O_tx_done   <=  1'b0    ;
            O_rx_done   <=  1'b0    ;
            O_spi_cs    <=  1'b1    ;
            O_spi_sck   <=  1'b0    ;
            O_spi_mosi  <=  1'b0    ;
            O_data_out  <=  8'd0    ;
        end      
end
 
endmodule

对发送部分进行测试,tb文件:

`timescale 1ns / 1ps
//
// Company: 
// Engineer: 
// 
// Create Date: 2024/03/30 19:45:22
// Design Name: 
// Module Name: spi_module_tb
// Project Name: 
// Target Devices: 
// Tool Versions: 
// Description: 
// 
// Dependencies: 
// 
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
// 
//


module spi_module_tb();
reg I_clk;  
    reg I_rst_n;  
    reg I_rx_en;  
    reg I_tx_en;  
    reg [7:0] I_data_in;  
    reg I_spi_miso;  
 
    // Outputs  
    wire [7:0] O_data_out;  
    wire O_tx_done;  
    wire O_rx_done;  
    wire O_spi_sck;  
    wire O_spi_cs;  
    wire O_spi_mosi; 
    
    spi_module uut(
        .I_clk(I_clk),
        .I_rst_n(I_rst_n),  
        .I_rx_en(I_rx_en),   
        .I_tx_en(I_tx_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),  
        .I_spi_miso(I_spi_miso),   
        .O_spi_sck(O_spi_sck),   
        .O_spi_cs(O_spi_cs),   
        .O_spi_mosi(O_spi_mosi) 
    );
    
     initial begin  
        // Initialize Inputs  
        I_clk = 0;  
        I_rst_n = 0;  
        I_rx_en = 0;  
        I_tx_en = 1;  
        I_data_in = 8'h00;  
        I_spi_miso = 0;  
 
        // Wait 100 ns for global reset to finish  
        #100;  
        I_rst_n = 1;    
 
    end  
 
    always #10 I_clk = ~I_clk ;  
    always @(posedge I_clk or negedge I_rst_n)  
    begin  
 if(!I_rst_n)  
            I_data_in <= 8'h00;  
 else if(I_data_in == 8'hff)  
            begin  
                I_data_in <= 8'hff;  
                I_tx_en <= 0;  
            end  
 else if(O_tx_done)  
            I_data_in <= I_data_in + 1'b1 ;              
    end  
endmodule

未完待续······

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
SPI协议是一种串行通信协议,常用于数字信号处理器和外围设备之间的通信。以下是使用Verilog实现SPI协议的示例代码: module spi_master( input clk, // 时钟信号 input rst, // 复位信号 input [7:0] data_in, // 输入数据信号 output [7:0] data_out, // 输出数据信号 output cs_n, // 片选信号 output sclk, // 时钟信号 output mosi, // 主机输出从机输入信号 input miso // 主机输入从机输出信号 ); parameter IDLE = 2'h0; // 空闲状态 parameter WRITE = 2'h1; // 写状态 parameter READ = 2'h2; // 读状态 reg [1:0] state; // 状态寄存器 reg [7:0] data; // 数据寄存器 reg [7:0] shift_reg; // 移位寄存器 reg [2:0] shift_cnt; // 移位计数器 assign sclk = state == WRITE || state == READ; // 写或读状态下输出时钟信号 assign mosi = shift_reg[7]; // 输出移位寄存器的MSB always @(posedge clk or posedge rst) begin if (rst) begin state <= IDLE; data <= 8'h00; shift_reg <= 8'h00; shift_cnt <= 3'h0; end else begin case (state) IDLE: begin cs_n <= 1'b1; // 空闲状态下片选信号为高电平 data_out <= 8'h00; // 输出数据信号为0 if (data_in != 8'h00) begin // 如果输入数据不为0,进入写状态 state <= WRITE; data <= data_in; // 存储输入数据 shift_reg <= {1'b1, 1'b1, data}; // 移位寄存器为起始位、模式字和数据 shift_cnt <= 3'h0; end else if (miso == 1'b0) begin // 如果输入数据为0且有从机输出数据,进入读状态 state <= READ; shift_reg <= 8'h00; // 移位寄存器为0 shift_cnt <= 3'h0; end end WRITE: begin cs_n <= 1'b0; // 写状态下片选信号为低电平 data_out <= 8'h00; // 输出数据信号为0 shift_cnt <= shift_cnt + 1; // 移位计数器加1 if (shift_cnt == 3'h7) begin // 移位计数器达到7,进入空闲状态 state <= IDLE; shift_reg <= 8'h00; shift_cnt <= 3'h0; end end READ: begin cs_n <= 1'b0; // 读状态下片选信号为低电平 data_out <= shift_reg[0]; // 输出移位寄存器的LSB shift_reg <= {shift_reg[6:0], miso}; // 移位寄存器左移一位,将从机输出数据存储到LSB shift_cnt <= shift_cnt + 1; // 移位计数器加1 if (shift_cnt == 3'h7) begin // 移位计数器达到7,进入空闲状态 state <= IDLE; shift_reg <= 8'h00; shift_cnt <= 3'h0; end end endcase end end endmodule 在此代码中,我们实现了一个SPI主机模块,可以与外围设备进行通信。主机通过CLK和RST输入时钟和复位信号,通过DATA_IN输入数据,通过DATA_OUT输出数据,通过CS_N输出片选信号,通过SCLK和MOSI输出时钟和主机输出从机输入信号,通过MISO输入主机输入从机输出信号。主机通过状态机控制SPI通信的过程,具体实现如下: - 空闲状态下,主机等待输入数据或从机输出数据。如果输入数据不为0,主机将进入写状态;如果从机输出数据有效,主机将进入读状态。 - 写状态下,主机将发送一个8位字节,包括起始位、模式字和数据。主机在每个时钟周期中将移位寄存器左移一位,并将下一个位存储在MSB中,直到发送完8位字节。然后,主机进入空闲状态。 - 读状态下,主机将接收一个8位字节,包括从机输出的数据。主机在每个时钟周期中将移位寄存器左移一位,并将从机输出的数据存储到LSB中,直到接收完8位字节。然后,主机进入空闲状态。 这是实现SPI协议的基本框架,实际应用中可能需要根据具体需求进行一些改进。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值