Menu Close

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

当我们了解了spi 的通讯协议后, 我们使用verilog 语言,编写一个简单的spi 通讯模块。 这个模块将会向大家展示一个完整的spi 通讯方式。

相关参考文章:

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

 

一个简单的 spi 模块(verilog 代码 : CPOL = 0  CPHA = 0)

`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company: Fraser Innovation Inc
// Engineer: WilliamG
// 
// Create Date: 04/23/2021 10:11:52 AM
// Design Name: simple spi
// Module Name: simple_spi
// Project Name:
// Target Devices: 
// Tool Versions: 
// Description: 
// 
// Dependencies: 
// 
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
// 
//////////////////////////////////////////////////////////////////////////////////


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) 加一。当接收8bit后, spi_data_rdy = 1 , 通知上层模块 , spi 模块得到一个byte的数据了;其他情况 spi_data_rdy  = 0

 

创建仿真工程

仿真工程代码

`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company: Fraser Innovation Inc
// Engineer: WilliamG
// 
// Create Date: 04/23/2021 10:12:11 AM
// Design Name: smple spi simulation
// Module Name: simple_spi_sim
// Project Name: 
// Target Devices: 
// Tool Versions: 
// Description: 
// 
// Dependencies: 
// 
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
// 
//////////////////////////////////////////////////////////////////////////////////

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

 

仿真波形

 

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 数据。 当 8bit 数据接收到后, 产生spi_data_rdy 信号。

 

总结

一个简单的spi 模块 还是比较简单使用的, 可以用来处理一些spi 的设备。 优点是模块比较简单, 容易理解。可以帮助大家迅速理解spi 的协议。 缺点是spi_clk 时钟 最大只能是当前时钟(phy_clk)的 二分之一, 甚至更低, 对于高速spi 设备 不能发挥出spi的效率。 同时 ,不是很容易添加复杂的spi 操作 向spi flash 操作等等。

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

 

Posted in FPGA, FPGA 教材教案, Verilog, Verilog, 教材与教案, 文章

发表回复

相关链接