Menu Close

SPI 通讯协议(4)SPI FLASH (verilog) 工程解析 (spi_phy.v)

在之前的文章中, 我们介绍了SPI 通讯协议,同时给出了一个简单的spi verilog 工程。 通过简单spi verilog 工程,我们可以对一些外设进行通讯。但简单的SPI 工程也有一些缺点。本文主要针对SPI FLASH 这种外设,搭建一个完整的verilog 工程。同时针简单spi 工程中的一些缺点,做一些补充,使得当前的工程可以完成spi flash 的相关操作。

参考文章:

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

 

在之前的简单spi 工程中, 我们实现了基本的spi 通讯,可以帮助大家迅速建立spi 通讯协议的概念,同时也可以做一些外设的应用。但如果想要操作spi flash 这样的外设, 还是有很多工作需要做的。同时,简单spi 工程 的 phy 时钟的速度会受到系统时钟的限制(速度不会很高)。 本文将制作一个全新的spi flash 工程,系统解决这些相关的问题, 同时增加spi flash 所相关的命令(指令),用于spi flash 的操作。

SPI FLASH 系统框图

 

整个spi flash 工程包括:

顶层模块 (top.v)

         时钟模块 (pll  IP)

         按键模块 (key_filter.v) 

         spi 通讯模块 (spi_IF.v)

                  spi 命令模块(spi_cmd.v)

                            spi phy 模块(spi_phy.v)

         uart 接口模块 (pc_uart.v)

                uart 传输模块 (uart_transceiver.v)

组成的。

这里,我们从最顶层的spi_phy.v 模块开始,一层一层向上介绍。 

 

spi_phy.v  模块

 

spi_phy.v 模块是最底层的spi 通讯接口模块, 它主要是用来完成基本的spi 相关的通讯协议。包括cs , clk, mosi, miso 等。

代码介绍

`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company: Fraser Innovation Inc
// Engineer: WilliamG
//
// Create Date: 2020/06/22 09:12:51
// Design Name:
// Module Name: spi_phy.v
// Project Name:
// Target Devices:
// Tool Versions:
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//////////////////////////////////////////////////////////////////////////////////
module spi_phy
(
input phy_clk, // spi 输出时钟

input spi_cs_cmd, // 片选信号指令,

input [7:0] flash_din, // 需要发送的spi 数据
input flash_din_rdy, // 需要发送的spi 数据 enable 一个时钟宽度
output reg flash_data_done = 0, // 当前数据已经发送完毕

output reg [7:0] flash_dout = 8'hff, // 读取的flash数据
output reg flash_dout_vld = 0, // flash数据的使能信号

output reg spi_cs = 1, // SPI 片选信号
output spi_clk, // SPI 时钟信号
output spi_mosi, // SPI 发送数据线
input spi_miso, // SPI 接收数据线

input rst_n
);
//=======================================================================================
// SPI 输出时钟使能逻辑, 当数据rdy 后,使能时钟, 当发送结束后, 停止时钟
reg clk_stop = 1;
always @ (posedge phy_clk or negedge rst_n)
if(!rst_n) clk_stop <= 1;
else
begin
if(flash_data_done) clk_stop <= 1;
else if(flash_din_rdy) clk_stop <= 0;
end


// 时钟输出选择信号, 片选有效, 并且时钟使能有效
wire spi_clk_ce = (~clk_stop);

`define XILINX_FPGA
`ifdef XILINX_FPGA

ODDR #(
.DDR_CLK_EDGE("SAME_EDGE"), // "OPPOSITE_EDGE" or "SAME_EDGE"
.INIT(1'b0), // Initial value of Q: 1'b0 or 1'b1
.SRTYPE("ASYNC") // Set/Reset type: "SYNC" or "ASYNC"
) SPI_CLK_ODDR_inst (
.Q (spi_clk), // 1-bit DDR output
.C (~phy_clk), // 1-bit clock input
.CE (spi_clk_ce), // 1-bit clock enable input
.D1 (1'b1), // 1-bit data input (positive edge)
.D2 (1'b0), // 1-bit data input (negative edge)
.R (1'b0), // 1-bit reset
.S (1'b0) // 1-bit set
);
`else
assign spi_clk = spi_clk_ce ? ~phy_clk : 0;
`endif
//=======================================================================================
// 同步 negedge reset 信号
reg phy_reset_neg = 0;
always @ (posedge phy_clk )
phy_reset_neg <= (!rst_n) | spi_cs_cmd;


reg [7:0] flash_data_r = 8'hff;
reg [2:0] tx_cnt = 0;
reg tx_st = 0;
always @ (posedge phy_clk)
if(phy_reset_neg)
begin
flash_data_done <= 0;
flash_data_r <= 8'hff;
tx_st <= 0;
tx_cnt <= 0;
end
else case (tx_st)
0:
begin
flash_data_done <= 0;
tx_cnt <= 0;

if(flash_din_rdy )
begin
flash_data_r <= flash_din;
tx_st <= 1;
end
end
1:
begin
flash_data_r <= {flash_data_r[6:0], 1'b1};
tx_cnt <= tx_cnt + 1;

if(tx_cnt == 6) flash_data_done <= 1;
else flash_data_done <= 0;

if(tx_cnt == 7)
tx_st <= 0;
end
endcase

assign spi_mosi = flash_data_r[7];
//=======================================================================================
// 同步 posedge reset 信号
reg phy_reset = 0;
always @ (posedge phy_clk )
phy_reset <= (!rst_n) | flash_data_done | spi_cs_cmd;


reg [3:0] rx_cnt = 0;
always @ (posedge phy_clk )
if( phy_reset )
begin
flash_dout <= 8'hff;
flash_dout_vld <= 0;
rx_cnt <= 0;
end
else
begin
rx_cnt <= rx_cnt + 1;

flash_dout <= {flash_dout[6:0], spi_miso};
if(rx_cnt == 7)
flash_dout_vld <= 1;

if(flash_dout_vld) flash_dout_vld <= 0;

end

//=======================================================================================
always @ (posedge phy_clk or negedge rst_n)
if(!rst_n)
begin
spi_cs <= 1;
end
else
begin
spi_cs <= spi_cs_cmd;
end
//=======================================================================================

endmodule



代码介绍

module spi_phy
(
input phy_clk, // spi 输出时钟

input spi_cs_cmd, // 片选信号指令,

input [7:0] flash_din, // 需要发送的spi 数据
input flash_din_rdy, // 需要发送的spi 数据 enable 一个时钟宽度
output reg flash_data_done = 0, // 当前数据已经发送完毕

output reg [7:0] flash_dout = 8'hff, // 读取的flash数据
output reg flash_dout_vld = 0, // flash数据的使能信号

output reg spi_cs = 1, // SPI 片选信号
output spi_clk, // SPI 时钟信号
output spi_mosi, // SPI 发送数据线
input spi_miso, // SPI 接收数据线

input rst_n
);

其中 spi_cs_cmd 是用来控制 SPI CS 信号的。

flash_din 是上层模块发送的数据(准备由spi_phy.v 发送到 mosi 上的),

flash_din_rdy 配合flash_din ,flash_din_rdy 是一个时钟宽度。

flash_data_done 信号 由spi_phy.v 模块产生, 返回给上层模块,用于通知上层模块 ,当前的数据已经发送完毕

 

flash_dout 信号由spi_phy.v 模块产生, 表示从miso 信号线上(其他spi 设备)接收到8bit数据, 准备送给上层模块

flash_dout_vld 信号表示当前的flash_dout 信号有效, 这个信号只有一个时钟宽度。

 

spi_cs       连接到其他SPI设备 的片选信号

spi_clk      连接到其他SPI设备 的时钟信号

spi_mosi  连接到其他SPI设备 的数据发送信号

spi_miso  连接到其他SPI设备 的数据接收信号

 

// SPI 输出时钟使能逻辑, 当数据rdy 后,使能时钟, 当发送结束后, 停止时钟
reg clk_stop = 1;
always @ (posedge phy_clk or negedge rst_n)
if(!rst_n) clk_stop <= 1;
else
begin
if(flash_data_done) clk_stop <= 1;
else if(flash_din_rdy) clk_stop <= 0;
end


// 时钟输出选择信号, 片选有效, 并且时钟使能有效
wire spi_clk_ce = (~clk_stop);

`define XILINX_FPGA
`ifdef XILINX_FPGA

ODDR #(
.DDR_CLK_EDGE("SAME_EDGE"), // "OPPOSITE_EDGE" or "SAME_EDGE"
.INIT(1'b0), // Initial value of Q: 1'b0 or 1'b1
.SRTYPE("ASYNC") // Set/Reset type: "SYNC" or "ASYNC"
) SPI_CLK_ODDR_inst (
.Q (spi_clk), // 1-bit DDR output
.C (~phy_clk), // 1-bit clock input
.CE (spi_clk_ce), // 1-bit clock enable input
.D1 (1'b1), // 1-bit data input (positive edge)
.D2 (1'b0), // 1-bit data input (negative edge)
.R (1'b0), // 1-bit reset
.S (1'b0) // 1-bit set
);
`else
assign spi_clk = spi_clk_ce ? ~phy_clk : 0;
`endif

 

clk_stop 表示时钟停止信号, clk_stop = 1 ,  spi_clk 停止时钟输出; clk_stop = 0, spi_clk 正常输出。当8bit 数据发送完毕后(flash_data_done),clk_stop = 1, 时钟停止输出; 当新数据准备好(flash_din_rdy),clk_stop = 0, 时钟正常输出

接下来的逻辑中xilinx 工程使用的是 ODDR, 这个源语 实现的效果和 assign spi_clk = spi_clk_ce ? ~phy_clk : 0; 是一样的。但在xilinx 6 系列之后的芯片中, xilinx 建议使用ODDR 方式,作为时钟输出,这样可以减少外设对时钟的负载, 这样时钟不容易变形, 系统约束效果也是比较好的。 

 

mosi 发送逻辑

// 同步 negedge reset 信号
reg phy_reset_neg = 0;
always @ (posedge phy_clk )
phy_reset_neg <= (!rst_n) | spi_cs_cmd;


reg [7:0] flash_data_r = 8'hff;
reg [2:0] tx_cnt = 0;
reg tx_st = 0;
always @ (posedge phy_clk)
if(phy_reset_neg)
begin
flash_data_done <= 0;
flash_data_r <= 8'hff;
tx_st <= 0;
tx_cnt <= 0;
end
else case (tx_st)
0:
begin
flash_data_done <= 0;
tx_cnt <= 0;

if(flash_din_rdy )
begin
flash_data_r <= flash_din;
tx_st <= 1;
end
end
1:
begin
flash_data_r <= {flash_data_r[6:0], 1'b1};
tx_cnt <= tx_cnt + 1;

if(tx_cnt == 6) flash_data_done <= 1;
else flash_data_done <= 0;

if(tx_cnt == 7)
tx_st <= 0;
end
endcase

assign spi_mosi = flash_data_r[7];



系统复位信号rst_n 和 片选有效spi_cs_cmd 这两个信号 一起生成同步rest 信号 : phy_reset_neg , 这个信号(phy_reset_neg )用来同步复位 发送状态机。
发送状态机: 有2个状态。 状态0: 接收上层模块的数据, 状态1: 用来发送当前的数据(从上层模块接收到的), 当8bit 数据发送完毕, 产生flash_data_done 信号, 用于通知上层模块当前数据发送完毕,可以接收新的数据了。(flash_data_done 和发送的最后一个bit同时产生)

miso 接收逻辑

// 同步 posedge reset 信号
reg phy_reset = 0;
always @ (posedge phy_clk )
phy_reset <= (!rst_n) | flash_data_done | spi_cs_cmd;


reg [3:0] rx_cnt = 0;
always @ (posedge phy_clk )
if( phy_reset )
begin
flash_dout <= 8'hff;
flash_dout_vld <= 0;
rx_cnt <= 0;
end
else
begin
rx_cnt <= rx_cnt + 1;

flash_dout <= {flash_dout[6:0], spi_miso};
if(rx_cnt == 7)
flash_dout_vld <= 1;

if(flash_dout_vld) flash_dout_vld <= 0;
end

 

rst_nflash_data_donespi_cs_cmd 信号一起产生 phy_reset 信号。 即: 发送、接收结束, 片选失效, 系统复位 这3中情况 ,都会产生 phy_reset 信号,用于复位 接收逻辑

每当时钟上升沿时 , 从spi_miso 上接收1bit 数据, 当8bit 数据接收完毕, 产生flash_dout_vld 信号,将flash_doutflash_dout_vld 一起送给上层模块,表示当前接收到了1byte 数据。

 

spi_cs 信号

always @ (posedge phy_clk or negedge rst_n)
if(!rst_n)
begin
spi_cs <= 1;
end
else
begin
spi_cs <= spi_cs_cmd;
end

上层模块发送的spi_cs_cmd 用来驱动spi_cs 信号。

上述模块仿真:

到此, 上层模块可以控制 spi_cs 信号, 发送 8bit 数据,让spi_phy.v 模块发送到spi_mosi 上, 接收spi_miso 信号解码后的8bit 数据了

Posted in FPGA, FPGA 教材教案, IC, RISC-V, RISC-V 外设, 教材与教案, 文章

发表评论

您的电子邮箱地址不会被公开。

Leave the field below empty!

相关链接