基于verilog语言的SPI协议实现

前言

关于SPI协议的基础知识这里就不在叙述了,感兴趣的小伙伴可以自行百度。本文基于verilog语言,实现SPI的四种通信模式,封装成通用模块。
更新时间:2023年7月11日
更新原因:之前的版本存在bug,通用性不够。

模块端口如下:

spi_comm #(
   .CPOL       (0), //时钟极性
   .CPHA       (0), //时钟相位
   .SPI_4P     (0), //1:四线制 0:三线制
   .CMD_BYTE   (3), //指令长度,单位字节
   .DATA_BYTE  (1)  //数据长度,单位字节
)your_instance_name(
   .clk     (clk     ), // input  wire [0            :0] |模块时钟
   .reset   (reset   ), // input  wire [0            :0] |模块复位

   .stert   (stert   ), // input  wire [0            :0] |开始信号
   .cmd     (cmd     ), // input  wire [CMD_BYTE*8-1 :0] |发送命令
   .valid   (valid   ), // output wire [0            :0] |读回数据有效
   .data    (data    ), // output wire [DATA_BYTE*8-1:0] |读回数据
   .done    (done    ), // output wire [0            :0] |结束标志

   .scs_n   (scs_n   ), // output wire [0            :0] |SPI|片选
   .sclk    (sclk    ), // output wire [0            :0] |SPI|时钟
   .sdio    (sdio    ), // output wire [0            :0] |SPI|三线制|数据线
   .mosi    (mosi    ), // output wire [0            :0] |SPI|四线制|主机发送
   .miso    (miso    )  // input  wire [0            :0] |SPI|四线制|主机接收
);

参数说明:

CPOL:配置模块时钟极性
CPHA:配置模块时钟相位
SPI_4P:配置模块的通信接口的引脚数量
CMD_BYTE:配置通信中指令的长度
DATA_BYTE:配置通信中数据的长度
关于CPOL和CPHA的相关说明在这里不做详细的描述,本模块在设计时采用了CPHA模糊化设计,即使CPHA配置错误也能与从机正常通信,但是CPOL必须配置正确。

功能介绍

特点:本模块是一个通用型SPI主机模块,实现SPI的四种通信模式可配置,指令长度可配置,数据长度可配置。

用户端口描述如下

stert :开始通信端口      |上升沿触发
cmd   :本次通信的CMD指令 |该数据会在stert的上升沿被模块锁存直至本次通信结束
valid :返回数据有效标志  |仅在读CMD指令时有效
data  :返回的数据        |仅在读CMD指令时有效
done  :本次通信结束标志

通信端口描述如下

scs_n :片选信号     |低电平有效
sclk  :通信时钟     |和本模块时钟之间的比例为1:8
sdio  :数据线       |当SPI_4P=0时有效,当SPI_4P=1时端口为高阻态
mosi  :主机发送端口 |当SPI_4P=1时有效,当SPI_4P=0时端口为高阻态
miso  :主机接收端口 |当SPI_4P=1时有效

CMD指令构成说明

指令长度是由读写位、地址位和数据位构成的。
CMD={R/W,ADDR,DATA}
CMD命令的最高位为读写位,CMD[CMD_BYTE*8-2 :DATA_BYTE*8]为地址位,CMD[DATA_BYTE*8-1:0]为数据位。
当CMD命令的最高位,即CMD[CMD_BYTE*8-1] = 1 时该指令为读指令,
此时CMD命令的数据位,即CMD[DATA_BYTE*8-1:0]无效,并且该CMD指令会通过valid和data端口返回一个数据。

模块源码

风格1

//
// Company: 
// Engineer: Zheng LiGuo
// Create Date: 2023/6/28
// Design Name: common_module
// Module Name: spi_comm
// Project Name: 
// Target Devices: spi_slave
// Tool Versions: vivado 2018.3
// Description: 通用SPI主机模块
//                支持配置项:
//                1.时钟极性
//                2.时钟相位
//                3.几线制
//                4.指令长度 |单位:字节
//                5.数据长度 |单位:字节
// Dependencies: CMD下发模块
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
/*===============================================
//----------- Begin Cut here for spi_comm Template ---//
//===============================================
// 通用SPI主机模块 
//===============================================
spi_comm #(
   .CPOL       (0), //时钟极性
   .CPHA       (0), //时钟相位
   .SPI_4P     (0), //1:四线制 0:三线制
   .CMD_BYTE   (3), //指令长度,单位字节
   .DATA_BYTE  (1)  //数据长度,单位字节
)your_instance_name(
   .clk     (clk     ), // input  wire [0            :0] |模块时钟
   .reset   (reset   ), // input  wire [0            :0] |模块复位

   .stert   (stert   ), // input  wire [0            :0] |开始信号
   .cmd     (cmd     ), // input  wire [CMD_BYTE*8-1 :0] |发送命令
   .valid   (valid   ), // output wire [0            :0] |读回数据有效
   .data    (data    ), // output wire [DATA_BYTE*8-1:0] |读回数据
   .done    (done    ), // output wire [0            :0] |结束标志

   .scs_n   (scs_n   ), // output wire [0            :0] |SPI|片选
   .sclk    (sclk    ), // output wire [0            :0] |SPI|时钟
   .sdio    (sdio    ), // output wire [0            :0] |SPI|三线制|数据线
   .mosi    (mosi    ), // output wire [0            :0] |SPI|四线制|主机发送
   .miso    (miso    )  // input  wire [0            :0] |SPI|四线制|主机接收
);
// INST_TAG_END ------ End spi_comm Template ---------
===============================================*/
`resetall
`timescale 1ns / 1ps
module spi_comm #(
   parameter   [0:0]    CPOL      = 0, //时钟极性
   parameter   [0:0]    CPHA      = 0, //时钟相位
   parameter   [0:0]    SPI_4P    = 0, //1:四线制 0:三线制
   parameter   [3:0]    CMD_BYTE  = 3, //指令长度,单位字节
   parameter   [3:0]    DATA_BYTE = 1  //数据长度,单位字节
)(
   input  wire                      clk   ,  // input  wire [0            :0] |模块时钟
   input  wire                      reset ,  // input  wire [0            :0] |模块复位

   input  wire                      stert ,  // input  wire [0            :0] |开始信号
   input  wire [CMD_BYTE*8-1 :0]    cmd   ,  // input  wire [CMD_BYTE*8-1 :0] |发送命令
   output wire                      valid ,  // output wire [0            :0] |读回数据有效
   output wire [DATA_BYTE*8-1:0]    data  ,  // output wire [DATA_BYTE*8-1:0] |读回数据
   output wire                      done  ,  // output wire [0            :0] |结束标志

   output wire                      scs_n ,  // output wire [0            :0] |SPI|片选
   output wire                      sclk  ,  // output wire [0            :0] |SPI|时钟
   inout  wire                      sdio  ,  // inout  wire [0            :0] |SPI|三线制|数据线
   output wire                      mosi  ,  // output wire [0            :0] |SPI|四线制|主机发送
   input  wire                      miso     // input  wire [0            :0] |SPI|四线制|主机接收
);
//=================================================
// 寄存器&网线定义
//=================================================
   reg                     resv_r;                    //接收引脚缓存器
   reg                     flag_rd   = 'd0;           //读操作标志寄存器
   reg  [2           :0]   stert_r3  = 'd0;           //开始信号缓存器
   reg  [CMD_BYTE*8-1:0]   cmd_r     = 'd0;           //CMD命令缓存器
   reg  [3:0]              NOW_S     = 'd1;           //状态机当前状态
   reg  [3:0]              NEX_S     = 'd1;           //状态机下一状态
   reg  [7:0]              time_cnt  = 'd0;           //时钟计数器
   reg  [5:0]              send_cnt  = CMD_BYTE*8-1;  //bit计数器
   reg                     flag_cnt1 = 'd0;           //bit交换完成标志寄存器
   reg  [DATA_BYTE*8-1:0]  data_r    = 'd0;           //读模式下接收数据寄存器
   wire                    stert_u;                   //开始信号上升沿标志
//=================================================
// 状态机状态定义
//=================================================
   localparam  [3:0]    S0_0 = 4'b0001;   //空闲状态
   localparam  [3:0]    S0_1 = 4'b0010;   //跳转至发送
   localparam  [3:0]    S0_2 = 4'b0100;   //发送命令
   localparam  [3:0]    S0_3 = 4'b1000;   //跳转回空闲
//=================================================
// 时钟计数器标志定义
//=================================================
   localparam  [7:0]    flag_time1 = 'd2;   //跳出跳转态时刻(跳转至发送)
   localparam  [7:0]    flag_time2 = 'd8;   //交换一个bit时间宽度
   localparam  [7:0]    flag_time3 = 'd3   //CPHA=0时接收bit时刻
   localparam  [7:0]    flag_time4 = 'd6;   //CPHA=1时接收bit时刻
   localparam  [7:0]    flag_time5 = 'd5;   //SPI时钟脉宽
//===============================================
// 同步端口信号到本地时钟
//===============================================
   always @(posedge clk) begin 
      if(SPI_4P) resv_r <= miso;
      else       resv_r <= sdio;
   end 
//===============================================
// 进程状态机
//===============================================
   always @(posedge clk) begin 
      if(reset) NOW_S <= S0_0 ;
      else      NOW_S <= NEX_S;
   end 
   always @(*) begin 
      case(NOW_S)
         S0_0    : begin if(stert_r3[2:1] == 2'b01) NEX_S = S0_1; else NEX_S = S0_0; end 
         S0_1    : begin if(time_cnt == flag_time1) NEX_S = S0_2; else NEX_S = S0_1; end 
         S0_2    : begin if(flag_cnt1             ) NEX_S = S0_3; else NEX_S = S0_2; end 
         S0_3    : begin if(time_cnt == flag_time1) NEX_S = S0_0; else NEX_S = S0_3; end 
         default : begin                            NEX_S = S0_0;                    end 
      endcase 
   end 
   always @(posedge clk) begin 
      case(NOW_S)
         S0_0    : begin 
            stert_r3  <= {stert_r3[1:0],stert}; 
            time_cnt  <= 'd0;
            send_cnt  <= CMD_BYTE*8-1;
            flag_cnt1 <= 'd0;
            data_r    <= 'd0;
            if(stert_r3[1:0] == 2'b01) cmd_r   <= cmd;
            else                       cmd_r   <= cmd_r;
            if(cmd_r[CMD_BYTE*8-1])    flag_rd <= 'd1;
            else                       flag_rd <= 'd0;
         end 
         S0_1    : begin 
            if(time_cnt == flag_time1) time_cnt <= 'd0;
            else                       time_cnt <= time_cnt+1;
         end 
         S0_2    : begin 
            if(time_cnt == flag_time2)                      time_cnt  <= 'd0;
            else                                            time_cnt  <= time_cnt+1;
            if(time_cnt == flag_time2)                      send_cnt  <= send_cnt-1;
            else                                            send_cnt  <= send_cnt;
            if(send_cnt == 'd0 && time_cnt == flag_time2-1) flag_cnt1 <= 'd1;
            else                                            flag_cnt1 <= 'd0;
            if(flag_rd && send_cnt<(DATA_BYTE*8)) begin 
               if(CPHA) begin 
                  if(time_cnt==flag_time4)   data_r[send_cnt] <= resv_r;
                  else                       data_r <= data_r;
               end else begin 
                  if(time_cnt==flag_time3)   data_r[send_cnt] <= resv_r;
                  else                       data_r <= data_r;
               end 
            end else 
               data_r <= data_r;
         end 
         S0_3    : begin 
            if(time_cnt == flag_time1)                   time_cnt <= 'd0;
            else                                         time_cnt <= time_cnt+1;
            if(NOW_S == S0_3 && time_cnt == flag_time1)  flag_rd  <= 'd0;
            else                                         flag_rd  <= flag_rd;
            if(NOW_S == S0_3 && time_cnt == flag_time1)  cmd_r    <= 'd0;
            else                                         cmd_r    <= cmd_r;
         end 
         default : begin 
            flag_rd   <= 'd0;
            time_cnt  <= 'd0;
            send_cnt  <= CMD_BYTE*8-1;
            flag_cnt1 <= 'd0;
            data_r    <= 'd0;
         end 
      endcase 
   end 

//===============================================
// 输入输出端口
//===============================================
   assign   valid = flag_rd ? ((NOW_S == S0_3) ? 1'b1 : 1'b0) : 1'b0;
   assign   data  = data_r;
   assign   done  = (NOW_S == S0_3 && time_cnt==flag_time1) ? 'd1 : 'd0;
   assign   scs_n = (NOW_S != S0_0) ? 'd0 : 'd1;
   assign   sclk  = (CPOL  == 'd0 ) ? (NOW_S == S0_2 ? ((time_cnt>=flag_time3 && time_cnt<=flag_time5) ? 'd1 : 'd0) : 'd0) :
                                      (NOW_S == S0_2 ? ((time_cnt>=flag_time3 && time_cnt<=flag_time5) ? 'd0 : 'd1) : 'd1) ;
   assign   sdio  = (SPI_4P) ? 'dz : ((NOW_S == S0_2) ? (flag_rd ? (send_cnt >= (DATA_BYTE*8) ? cmd_r[send_cnt] : 'dz) : cmd_r[send_cnt] ) : 'dz);
   assign   mosi  = (SPI_4P) ? ((NOW_S == S0_2) ? (flag_rd ? (send_cnt >= (DATA_BYTE*8) ? cmd_r[send_cnt] : 'dz) : cmd_r[send_cnt] ) : 'dz) : 'dz;

endmodule

风格2

//
// Company: 
// Engineer: Zheng LiGuo
// Create Date: 2023/6/28
// Design Name: common_module
// Module Name: spi_comm
// Project Name: 
// Target Devices: spi_slave
// Tool Versions: vivado 2018.3
// Description: 通用SPI主机模块
//                支持配置项:
//                1.时钟极性
//                2.时钟相位
//                3.几线制
//                4.指令长度 |单位:字节
//                5.数据长度 |单位:字节
// Dependencies: CMD下发模块
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
/*===============================================
//----------- Begin Cut here for spi_comm Template ---//
//===============================================
// 通用SPI主机模块 
//===============================================
spi_comm #(
   .CPOL       (0), //时钟极性
   .CPHA       (0), //时钟相位
   .SPI_4P     (0), //1:四线制 0:三线制
   .CMD_BYTE   (3), //指令长度,单位字节
   .DATA_BYTE  (1)  //数据长度,单位字节
)your_instance_name(
   .clk     (clk     ), // input  wire [0            :0] |模块时钟
   .reset   (reset   ), // input  wire [0            :0] |模块复位

   .stert   (stert   ), // input  wire [0            :0] |开始信号
   .cmd     (cmd     ), // input  wire [CMD_BYTE*8-1 :0] |发送命令
   .valid   (valid   ), // output wire [0            :0] |读回数据有效
   .data    (data    ), // output wire [DATA_BYTE*8-1:0] |读回数据
   .done    (done    ), // output wire [0            :0] |结束标志

   .scs_n   (scs_n   ), // output wire [0            :0] |SPI|片选
   .sclk    (sclk    ), // output wire [0            :0] |SPI|时钟
   .sdio    (sdio    ), // output wire [0            :0] |SPI|三线制|数据线
   .mosi    (mosi    ), // output wire [0            :0] |SPI|四线制|主机发送
   .miso    (miso    )  // input  wire [0            :0] |SPI|四线制|主机接收
);
// INST_TAG_END ------ End spi_comm Template ---------
===============================================*/
`resetall
`timescale 1ns / 1ps
module spi_comm #(
   parameter   [0:0]    CPOL      = 0, //时钟极性
   parameter   [0:0]    CPHA      = 0, //时钟相位
   parameter   [0:0]    SPI_4P    = 0, //1:四线制 0:三线制
   parameter   [3:0]    CMD_BYTE  = 3, //指令长度,单位字节
   parameter   [3:0]    DATA_BYTE = 1  //数据长度,单位字节
)(
   input  wire                      clk   ,  // input  wire [0            :0] |模块时钟
   input  wire                      reset ,  // input  wire [0            :0] |模块复位

   input  wire                      stert ,  // input  wire [0            :0] |开始信号
   input  wire [CMD_BYTE*8-1 :0]    cmd   ,  // input  wire [CMD_BYTE*8-1 :0] |发送命令
   output wire                      valid ,  // output wire [0            :0] |读回数据有效
   output wire [DATA_BYTE*8-1:0]    data  ,  // output wire [DATA_BYTE*8-1:0] |读回数据
   output wire                      done  ,  // output wire [0            :0] |结束标志

   output wire                      scs_n ,  // output wire [0            :0] |SPI|片选
   output wire                      sclk  ,  // output wire [0            :0] |SPI|时钟
   inout  wire                      sdio  ,  // inout  wire [0            :0] |SPI|三线制|数据线
   output wire                      mosi  ,  // output wire [0            :0] |SPI|四线制|主机发送
   input  wire                      miso     // input  wire [0            :0] |SPI|四线制|主机接收
);
//=================================================
// 寄存器&网线定义
//=================================================
   reg                     resv_r;                    //接收引脚缓存器
   reg                     flag_rd   = 'd0;           //读操作标志寄存器
   reg  [2           :0]   stert_r3  = 'd0;           //开始信号缓存器
   reg  [CMD_BYTE*8-1:0]   cmd_r     = 'd0;           //CMD命令缓存器
   reg  [3:0]              NOW_S     = 'd1;           //状态机当前状态
   reg  [3:0]              NEX_S     = 'd1;           //状态机下一状态
   reg  [7:0]              time_cnt  = 'd0;           //时钟计数器
   reg  [5:0]              send_cnt  = CMD_BYTE*8-1;  //bit计数器
   reg                     flag_cnt1 = 'd0;           //bit交换完成标志寄存器
   reg  [DATA_BYTE*8-1:0]  data_r    = 'd0;           //读模式下接收数据寄存器
   wire                    stert_u;                   //开始信号上升沿标志
//=================================================
// 状态机状态定义
//=================================================
   localparam  [3:0]    S0_0 = 4'b0001;   //空闲状态
   localparam  [3:0]    S0_1 = 4'b0010;   //跳转至发送
   localparam  [3:0]    S0_2 = 4'b0100;   //发送命令
   localparam  [3:0]    S0_3 = 4'b1000;   //跳转回空闲
//=================================================
// 时钟计数器标志定义
//=================================================
   localparam  [7:0]    flag_time1 = 'd2;   //跳出跳转态时刻(跳转至发送)
   localparam  [7:0]    flag_time2 = 'd8;   //交换一个bit时间宽度
   localparam  [7:0]    flag_time3 = 'd5;   //跳出跳转态时刻(跳转回空闲)
   localparam  [7:0]    flag_time4 = 'd3;   //CPHA=0时接收bit时刻
   localparam  [7:0]    flag_time5 = 'd6;   //CPHA=1时接收bit时刻
//===============================================
// 同步端口信号到本地时钟
//===============================================
   always @(posedge clk) begin 
      if(SPI_4P) resv_r <= miso;
      else       resv_r <= sdio;
   end 
//===============================================
// 获取开始信号上升沿 |缓存CMD |产生本地开始信号
//===============================================
   always @(posedge clk) stert_r3 <= {stert_r3[1:0],stert}; 
   always @(posedge clk) begin 
      if(stert_r3[1:0] == 2'b01)
         cmd_r <= cmd;
      else 
         cmd_r <= cmd_r;
   end 
   assign stert_u = (stert_r3[2:1] == 2'b01) ? 1'b1 : 1'b0;
   always @(posedge clk) begin 
      if(stert_r3[1:0] == 2'b01) begin 
         if(cmd[CMD_BYTE*8-1])
            flag_rd <= 'd1;
         else 
            flag_rd <= 'd0;
      end else if(NOW_S == S0_3 && time_cnt == flag_time3)
         flag_rd <= 'd0;
      else 
         flag_rd <= flag_rd;
   end 
//===============================================
// 进程状态机
//===============================================
   always @(posedge clk) begin 
      if(reset) NOW_S <= S0_0 ;
      else      NOW_S <= NEX_S;
   end 
   always @(*) begin 
      case(NOW_S)
         S0_0    : begin if(stert_u               ) NEX_S = S0_1; else NEX_S = S0_0; end 
         S0_1    : begin if(time_cnt == flag_time1) NEX_S = S0_2; else NEX_S = S0_1; end 
         S0_2    : begin if(flag_cnt1             ) NEX_S = S0_3; else NEX_S = S0_2; end 
         S0_3    : begin if(time_cnt == flag_time3) NEX_S = S0_0; else NEX_S = S0_3; end 
         default : begin                            NEX_S = S0_0;                    end 
      endcase 
   end 
//===============================================
// 时钟计数器
//===============================================
   always @(posedge clk) begin 
      case(NOW_S)
         S0_0    : begin                            time_cnt <= 'd0;                              end 
         S0_1    : begin if(time_cnt == flag_time1) time_cnt <= 'd0; else time_cnt <= time_cnt+1; end 
         S0_2    : begin if(time_cnt == flag_time2) time_cnt <= 'd0; else time_cnt <= time_cnt+1; end  
         S0_3    : begin if(time_cnt == flag_time3) time_cnt <= 'd0; else time_cnt <= time_cnt+1; end  
         default : begin                            time_cnt <= 'd0;                              end 
      endcase 
   end 
//===============================================
// 比特计数
//===============================================
   always @(posedge clk) begin 
      if(NOW_S == S0_2) begin 
         if(time_cnt == flag_time2)
            send_cnt <= send_cnt-1;
         else 
            send_cnt <= send_cnt;
      end else 
         send_cnt <= CMD_BYTE*8-1;
   end 
   //============================================
   // bit交换完成标志
   //============================================
   always @(posedge clk) begin 
      if(NOW_S == S0_2) begin 
         if(send_cnt == 'd0 && time_cnt == flag_time2-1)
            flag_cnt1 <= 'd1;
         else 
            flag_cnt1 <= 'd0;
      end else 
         flag_cnt1 <= 'd0;
   end 
//===============================================
// 读命令下接收bit
//===============================================
   always @(posedge clk) begin 
      if(flag_rd) begin 
         if(send_cnt<(DATA_BYTE*8)) begin 
            if(CPHA) begin 
               if(time_cnt==flag_time5)
                  data_r[send_cnt] <= resv_r;
               else 
                  data_r <= data_r;
            end else begin 
               if(time_cnt==flag_time4)
                  data_r[send_cnt] <= resv_r;
               else 
                  data_r <= data_r;
            end 
         end else 
            data_r <= data_r;
      end else 
         data_r <= 'd0;
   end 
//===============================================
// 输入输出端口
//===============================================
   assign   valid = flag_rd ? ((NOW_S == S0_3) ? 1'b1 : 1'b0) : 1'b0;
   assign   data  = data_r;
   assign   done  = (NOW_S == S0_3 && time_cnt==flag_time3) ? 'd1 : 'd0;
   assign   scs_n = (NOW_S != S0_0) ? 'd0 : 'd1;
   assign   sclk  = (CPOL  == 'd0 ) ? (NOW_S == S0_2 ? ((time_cnt>=flag_time4 && time_cnt<=flag_time3) ? 'd1 : 'd0) : 'd0) :
                                      (NOW_S == S0_2 ? ((time_cnt>=flag_time4 && time_cnt<=flag_time3) ? 'd0 : 'd1) : 'd1) ;
   assign   sdio  = (SPI_4P) ? 'dz : ((NOW_S == S0_2) ? (flag_rd ? (send_cnt >= (DATA_BYTE*8) ? cmd_r[send_cnt] : 'dz) : cmd_r[send_cnt] ) : 'dz);
   assign   mosi  = (SPI_4P) ? ((NOW_S == S0_2) ? (flag_rd ? (send_cnt >= (DATA_BYTE*8) ? cmd_r[send_cnt] : 'dz) : cmd_r[send_cnt] ) : 'dz) : 'dz;

endmodule

结束语

由于模块的通用性,这次更新删除了仿真部分
很抱歉之前的版本存在bug。也是我最近的项目需要用到SPI通信时才发现的。希望读者能给我反馈使用和阅读时发现的错误。也欢迎各位读者和我交流verilog知识。

你可能感兴趣的:(FPGA,verilog,通信协议,fpga开发,编辑器)