Menu Close

SPI 通讯协议(2)简单的spi verilog 模块及仿真

当我们理解了SPI的通讯协议后, 本文使用Verilog 语言来编写一个简单的SPI通讯模块,用这个模块展示一个完整的SPI通讯方式。

%title插图%num

图1

 SPI模块(Verilog 代码 :默认 CPOL = 0  CPHA = 0)

  • 设计思路

设计思路由简单的SPI收发工程到完善的SPI工程。接下来,由下面的三个步骤进行说明。

  1. 设计一个SPI发送移位寄存器
  2. 设计完整的SPI接收,发送的工程
  3. 在完整的SPI收发工程上,包装了一个上层使用接口

 

  • SPI发送移位寄存器代码实现

实现简单的MOSI发送。

工程代码:

module test_spi(
    input clk,
    
    input ce,           //片选
    output spi_clk,     //时钟
    output reg mosi = 1,       
    input  miso,
    
    input  reset
);
    
reg [7:0] data = 8'hd3;            //通过MOSI发送的数据,可以修改
always @ (posedge clk)
if(~ce)                            //当片选为低时有效
    data <= {data[7:0], 1'b1};     //由高位开始发送,之后每过一个时钟周期,数据移位
    
always @ (posedge clk)
if(~ce)
    mosi <= data[7];              //与时钟同步发送数据
    
assign spi_clk = ~clk;

仿真文件:

`timescale 1ns / 1ps



module test_spi_sim(

    );
    
reg reset = 0;
reg clk = 0;
always #5 clk = ~clk;

reg ce = 1;
initial
begin
ce = 1;
#200; 
@(posedge clk);
ce = 0;
@(posedge clk);
@(posedge clk);
@(posedge clk);
@(posedge clk);
@(posedge clk);
@(posedge clk);
@(posedge clk);
@(posedge clk);

ce = 1;

#400;
$stop;
end

wire spi_clk;
wire mosi;    
wire miso = mosi;
test_spi  test_spi_inst  
(
    .clk    (clk),

    .ce     (ce),
    .spi_clk(spi_clk),
    .mosi   (mosi),
    .miso   (miso),

    .reset (reset)
);

    
endmodule

 

仿真波形图如图2所示:

%title插图%num

图2 仿真波形图

通过图2可以看出,数据8’hd3被正确发送。在片选ce为低的期间,在spi_clk的下降沿,mosi更换数据;同时,我们也发现, spi_clk 一直没有停止, miso 也没有接收的逻辑。

  • 完整SPI收发器代码实现

在完整SPI收发器工程中,改进了上一个工程的问题。主要改进了在片选cs为高的时候,时钟不能翻转,以及增加了miso的接收逻辑。

工程代码:

module test_spi(
    input clk,
    
    input ce,
    output spi_clk,
    output reg mosi = 1,
    input miso,
    
    input  reset
);
    
reg [7:0] tx_data = 8'h6a;           //通过MOSI发送的数据,可以修改
always @ (posedge clk)
if(~ce)
    tx_data <= {tx_data[7:0], 1'b1};
    
always @ (posedge clk)
if(~ce)
    mosi <= tx_data[7];

    
assign spi_clk = ce ? 1'b0 : ~clk;    //通过片选是否有效来控制spi_clk



reg [7:0] rx_data = 8'hff;            //miso使用spi_clk采样数据
always @ (posedge spi_clk) 
rx_data <= {rx_data[6:0], miso};
   
endmodule

仿真结果,如图3所示:

%title插图%num

图3 仿真波形图

根据图3可以看出,SPI基本功能已经实现(默认CPOL = 0,CPHA = 0),接下来将在此基础上增加上层模块的接口。

 

  • 有上层使用接口的SPI收发代码实现

工程代码:

`timescale 1ns / 1ps



module simple_spi
(
    input phy_clk, 

    input [7:0] spi_din,               // SPI需要发送的数据
    input spi_data_vld,                // SPI数据 有效, 一个时钟宽度(phy_clk)

    output reg [7:0] spi_dout = 8'hff, // SPI接收到的数据
    output reg spi_data_rdy = 0,       // SPI接收数据有效 , 一个时钟宽度(phy_clk)

    output reg spi_cs = 1,             // SPI片选 pin
    output reg spi_clk = 0,            // SPI时钟 pin
    output reg spi_mosi = 1,           // SPI主设备输出数据(从设备接收数据) pin
    input spi_miso,                    // SPI主设备接收数据(从设备输出数据) pin

    input rst_n
);

reg [7:0] tx_data = 8'hff;
reg [3:0] tx_cnt = 0;
reg [1:0] tx_st = 0;
always @ (posedge phy_clk or negedge rst_n)
if(!rst_n)
begin
    spi_mosi <= 1;
    spi_cs <= 1;
    spi_clk <= 0;
    tx_st <= 0;
end
else case (tx_st)
0:
begin
    tx_cnt <= 0;

    spi_mosi <= 1;
    spi_cs <= 1;
    spi_clk <= 0;

    if(spi_data_vld)
    begin
        tx_data <= spi_din;
        tx_st <= 1;
    end
    else tx_data <= 8'hff;
end
1:
begin
    spi_cs <= 0;
    tx_st <= 2;
end
2:
begin
    spi_clk <= 0;
    spi_mosi <= tx_data[7];
    tx_data <= {tx_data[6:0], 1'b1};

    if(tx_cnt == 8)
        tx_st <= 0;
    else
        tx_st <= 3;
end
3:
begin
    spi_clk <= 1;

    tx_cnt <= tx_cnt + 1;
    tx_st <= 2;
end
default : tx_st <= 0;
endcase
//=======================================================================================
reg spi_clk_r = 0;
always @ (posedge phy_clk ) 
    spi_clk_r <= spi_clk;

wire rx_pos_clk = ({spi_clk_r, spi_clk} == 2'b01) ? 1'b1 : 1'b0;

reg [2:0] rx_cnt = 0;
always @ (posedge phy_clk ) 
if(spi_cs)
begin
    spi_data_rdy <= 0;
    rx_cnt <= 0;
end
else if(rx_pos_clk)
begin
    spi_dout <= {spi_dout[6:0], spi_miso};
    rx_cnt <= rx_cnt + 1;
    if(rx_cnt == 7) spi_data_rdy <= 1;
    else spi_data_rdy <= 0;
end
else spi_data_rdy <= 0;
//=======================================================================================

endmodule

详细分析上述工程代码如下:

接口信号

input phy_clk,                                  // SPI模块 操作时钟

input [7:0] spi_din,                         // SPI需要发送的数据
input spi_data_vld,                         // SPI数据 有效, 一个时钟宽度(phy_clk)

系统上层代码 可以使用这两个信号, 将需要发送的数据传送到 当前这个模块, 用于数据发送。

output reg [7:0] spi_dout = 8’hff, // SPI接收到的数据
output reg spi_data_rdy = 0,        // SPI接收数据有效 , 一个时钟宽度(phy_clk)

系统上层代码 可以使用这两个信号, 将当前模块所接收到的SPI数据发送给上层, 供上层模块使用。

output reg spi_cs = 1,                      // SPI片选 pin
output reg spi_clk = 0,                   // SPI时钟 pin
output reg spi_mosi = 1,                // SPI主设备输出数据(从设备接收数据) pin
input spi_miso,                               // SPI主设备接收数据(从设备输出数据) pin

这 4 个信号为SPI的接口信号, 用于连接其他SPI的设备(pin )

input rst_n                                       // 模块 reset 信号

 

发送状态机

复位状态: 状态机复位, spi_mosi  = 1 , 保持高电平;  spi_cs = 1,一般情况下SPI的从设备都是cs = 0 有效,但用户需要注意, 也有一些从设备是cs = 1 有效的。 如果遇到这样的芯片,注意修改当前缺省值。  spi_clk = 0, 对应使用的是 CPOL = 0 (标准协议), 如果用户使用 CPOL = 1 的标准, 需要修改这个值。

状态0:等待上层模块发送命令(那些数据需要SPI模块发送出去), 同时保持 cs = 1, spi_clk = 0 ; 如果得到上层的命令,将数据所存为tx_data

状态1:spi_cs = 0 , 片选使能

状态2:产生 spi_clk = 0,  在spi_clk = 0 (低电平期间) 将tx_data 的最高位 赋值给 spi_mosi , 同时 将tx_data 做 逻辑左移,最后一位补一; 如果计数器 (tx_cnt == 8) ,表示当前的数据已经发送完毕了,跳转状态机 到 0; 如果计数器 没有加到 8, 跳转到状态机 3

状态3: 产生 spi_clk = 1 ,同时 计数器加一 , 表示tx_data 已经发送 了 1 bit , 跳转状态机到2;

接收代码

reg spi_clk_r = 0;
always @ (posedge phy_clk )
spi_clk_r <= spi_clk;

wire rx_pos_clk = ({spi_clk_r, spi_clk} == 2'b01) ? 1'b1 : 1'b0;

得到 spi_clk 的上升沿, 因为当前代码为(CPOL = 0, CPHA = 0) 表示时钟上升沿接收数据。

 

reg [2:0] rx_cnt = 0;
always @ (posedge phy_clk )
if(spi_cs)
begin
    spi_data_rdy <= 0;
    rx_cnt <= 0;
end
else if(rx_pos_clk)
begin
    spi_dout <= {spi_dout[6:0], spi_miso};
    rx_cnt <= rx_cnt + 1;
    if(rx_cnt == 7) spi_data_rdy <= 1;
    else spi_data_rdy <= 0;
end
else spi_data_rdy <= 0;

 

在片选无效时, 接收计数器(rx_cnt) 清零。  每当得到一个spi_clk 的上升沿(rx_pos_clk) ,  spi_miso (其他设备发送, 当前模块接收)信号被存储到spi_dout 中,同时计数器(rx_cnt) 加一。当接收8-bit后, spi_data_rdy = 1 , 通知上层模块 , SPI模块得到一个byte的数据了;其他情况 spi_data_rdy  = 0

仿真工程代码:

`timescale 1ns / 1ps


module simple_spi_sim(

);

reg clk = 0;
always clk = #10 ~clk;

reg rst_n = 0;


reg [7:0] spi_din = 8'hff;
reg spi_data_vld = 0;

initial
begin
    spi_data_vld = 0;
    rst_n = 0;
    #200;
    rst_n = 1;

    #400;
    @(posedge clk);
    spi_din = 8'h5d;
    spi_data_vld = 1;
    @(posedge clk);
    spi_data_vld = 0;

    #400;
    @(posedge clk);
    spi_din = 8'h3c;
    spi_data_vld = 1;
    @(posedge clk);
    spi_data_vld = 0;

    #400;
    @(posedge clk);
    spi_din = 8'h47;
    spi_data_vld = 1;
    @(posedge clk);
    spi_data_vld = 0;


    #200000;
    $stop;
end
wire [7:0] spi_dout;
wire spi_data_rdy;


wire spi_cs;
wire spi_clk;
wire spi_data;
simple_spi simple_spi_inst
(
    .phy_clk      (clk),

    .spi_din      (spi_din),      // SPI需要发送的数据
    .spi_data_vld (spi_data_vld), // SPI数据 有效, 一个时钟宽度(phy_clk)

    .spi_dout     (spi_dout),     // SPI接收到的数据
    .spi_data_rdy (spi_data_rdy), // SPI接收数据有效 , 一个时钟宽度(phy_clk)

    .spi_cs       (spi_cs),       // SPI片选 pin
    .spi_clk      (spi_clk),      // SPI时钟 pin
    .spi_mosi     (spi_data),     // SPI 主设备输出数据(从设备接收数据) pin
    .spi_miso     (spi_data),     // SPI主设备接收数据(从设备输出数据) pin

    .rst_n        (rst_n)
);

//==================================================================
endmodule

 

仿真波形如图4所示:

%title插图%num

图4 仿真波形

spi_din, spi_data_vld 为仿真上层模块数据信号, 当前仿真模块 仿真上层模块 发送数据 为 8’h5d,  8’h3c, 8’h47。(蓝色信号)

spi_dout, spi_data_rdy 为上层模块接收到的信号(从其他设备发送过来, 当前仿真程序 是将 spi_mosi 和 spi_miso 信号连接, loopback 回环测试) (浅粉色)

spi_cs, spi_clk, spi_data 代表SPI模块 pin , 其中spi_data 是将spi_mosi 和 spi_miso 连接。回环测试。

图中我们可以看出:

首先是cs 信号 为 0, 然后 发送spi_clk 时钟信号, 同时配合spi_clk  发送spi_data 信号,这里我们可以看出, spi_data 是在spi_clk 下降沿变化数据的。当 8bit 发送完成, cs = 1 。 表示当前操作结束。

接收模块在cs = 1 时 ,不做任何动作; 在cs = 0 时, 每当spi_clk时钟上升沿时, 采样一个bit 数据。 当 8-bit 数据接收到后, 产生spi_data_rdy 信号。

 

总结

由上面3个SPI实验工程,从简单到复杂的实现过程中,编写一个实现基本功能的SPI模块 还是比较简单使用的, 可以用来处理一些SPI的设备。 可以帮助大家迅速理解SPI的协议。

不足之处是spi_clk 时钟 最大只能是当前时钟(phy_clk)的 二分之一, 甚至更低, 对于高速SPI设备 不能发挥出SPI的效率。 同时 ,不是很容易添加复杂的spi 操作 向SPI FLASH 操作等等。之后的文章会针对不足进行改进。

其实当大家理解了SPI协议后, 会发现SPI协议要比 I2C, UART协议还要简单 。 SPI协议 其实就是将数据移位输出(发送),移位输入(接收),同时配合上cs, clk 信号即可。

 

SPI系列文章链接:

SPI 通讯协议 及 SPI 相关工程 详解

 

Posted in FPGA, FPGA, IP开发, RISC-V, RISC-V IPcore设计, RISC-V 教案, 元器件, 教材与教案, 文章, 资料区

发表评论

相关链接