Menu Close

SPI 通讯协议(5)SPI FLASH (verilog) 工程解析 (spi_cmd.v)

spi_phy.v 的上层模块是spi_cmd.v.  spi_cmd.v模块主要是负责命令(指令)的调度。 上层的系统模块可以将相关的spi 命令(指令)写入到spi_fifo_wr (ip 核)中, spi_cmd.v 模块通过读取spi_fifo_wr 的内容,得到相关的命令(指令),然后产生spi_phy.v 所需要的相关控制信号, 数据将通过spi_phy.v 模块发送到其他外设(spi 设备); 同时 ,从spi_phy.v 模块中读取到的数据,也会被存储到spi_cmd.v 模块中的spi_fifo_rd (ip核)中, 供spi_cmd.v 模块之上的系统模块调用。

参考文章:

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

 

spi_cmd.v 模块介绍

%title插图%num

 

spi_cmd.v 模块 下层连接spi_phy.v,上层模块为SPI_IF.V  。  spi_cmd.v 和 spi_phy.v 模块都是使用的phy_clk 时钟的, 他们的上层模块将使用系统时钟(sys_clk)。使用fifo 作为两个时钟域的接口是比较适合的。spi_cmd.v 模块 负责将上层模块的指令(fifo 中) 正确的发送给spi_phy.v 模块, 同时将spi_phy.v 模块中读到的数据写入(spi_fifo_rd) 中, 供上层模块读取。

代码介绍:

`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company: Fraser Innovation Inc
// Engineer: WilliamG
// 
// Create Date: 2020/06/22 10:58:53
// Design Name: 
// Module Name: spi_cmd.v
// Project Name: 
// Target Devices: 
// Tool Versions: 
// Description: 
// 
// Dependencies: 
// 
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
// 
//////////////////////////////////////////////////////////////////////////////////

/*
flash command
02h page program
20h subsector erase
d8h sector erase
c7h chip erase
9fh read ID
03h read
06h write enable
04h write disable
05h read status register 

*/
module spi_cmd #
(
    parameter SPI_MODE = "FLASH"
)
(
    input  phy_clk,

// phy interface 
    output spi_cs,   //SPI片选 
    output spi_clk,  //SPI时钟 
    output spi_mosi, //SPI主器件输出从器件输入
    input  spi_miso, //SPI主器件输入从器件输出

// 系统时钟域
    input  sys_clk,
    input  [17:0] spi_din,
    input  spi_wr,

    input  spi_rd,
    output [7:0] spi_dout,
    output spi_empty,

    input  spi_cmd_vld,
    output spi_cmd_ack,

    input  rd_dev_state_en,    // for spi flash 
    output [7:0] rd_dev_state, // read spi flash 05 state register, this value cannot be written to fifo

    input  rst_n
);

//=======================================================================================
reg [1:0] phy_cmd_vld = 0;
always @ (posedge phy_clk or negedge rst_n)
if(!rst_n) phy_cmd_vld <= 0;
else phy_cmd_vld <= {phy_cmd_vld[0], spi_cmd_vld};

wire pos_cmd_vld = (phy_cmd_vld == 2'b01) ? 1'b1 : 1'b0;


reg phy_cmd_ack = 0;
reg [7:0] phy_cmd_ack_r = 0;
always @ (posedge phy_clk )
if(phy_cmd_ack) phy_cmd_ack_r <= 8'hff;
else phy_cmd_ack_r <= {phy_cmd_ack_r[6:0], 1'b0};

assign spi_cmd_ack = phy_cmd_ack_r[7];
//=======================================================================================
/*
定义 写fifo ; 18bit: 
bit[17] == 0, bit[7:0] 是数据, bit[16:8] 无用; 
bit[17] == 1 作为flash 的读标识 和 结束标识。 剩余的bit[16:0] 读spi 的 byte 数量; 

举例 : 18'h2_0000 表示当前命令没有需要从flash 读取的数据。
18'h2_0003 表示当前命令需要从flash 读取3个数据。
每次flash 操作 都必须使用 18'b1x_xxxx_xxxx_xxxx_xxxx 作为最后一条命令,
即使没有要从flash读取数据,也要写入fifo 18'h2_0000 作为 本次命令传输结束。

举例: 
读 spi flash ID
18'h0_009f // 发送READ-ID 命令
18'h2_0003 // 结束符,从flash 中读取3 bytes 数据

写 spi flash 0x20_0000 0x01,0x02,0x03 
18'h0_0006 // write enable command
18'h2_0000 // 结束符,从flash 中读取0 bytes 数据

18'h0_0002 // write enable command

18'h0_0020 // flash 地址 最高8bit
18'h0_0000 // flash 地址 中间8bit
18'h0_0000 // flash 地址 最低8bit

18'h0_0001 // flash 写数据 0x01
18'h0_0002 // flash 写数据 0x02
18'h0_0003 // flash 写数据 0x03

18'h2_0000 // 结束符,从flash 中读取0 bytes 数据

擦除扇区 0x80_0000
18'h0_0020 // erase command

18'h0_0080 // flash 地址 最高8bit
18'h0_0000 // flash 地址 中间8bit
18'h0_0000 // flash 地址 最低8bit

18'h2_0000 // 结束符,从flash 中读取0 bytes 数据


*/

reg wfifo_rd = 0;

wire [17:0] wfifo_dout;
wire wfifo_empty;
SPI_FIFO_WR SPI_FIFO_WR_inst
(
    .wr_clk (sys_clk),     // input wire wr_clk
    .din    (spi_din),     // input wire [17 : 0] din
    .wr_en  (spi_wr),      // input wire wr_en
    .full   (),            // output wire full

    .rd_clk (phy_clk),     // input wire rd_clk
    .rd_en  (wfifo_rd),    // input wire rd_en
    .dout   (wfifo_dout),  // output wire [17 : 0] dout
    .valid  (),            // output wire valid
    .empty  (wfifo_empty), // output wire empty

    .rst    (!rst_n)       // input wire rst
);

//=======================================================================================
reg [31:0] rd_len = 0;
reg spi_rev_en = 0;

reg spi_cs_cmd = 1;
reg [7:0] flash_din = 8'hff;
reg flash_din_rdy = 0;

wire [7:0] flash_dout;
wire flash_dout_vld;

wire flash_data_done;

reg [31:0] spi_cnt = 0;
reg [2:0] spi_st = 0;
always @ (posedge phy_clk or negedge rst_n)
if(!rst_n)
begin
    spi_rev_en <= 0;
    phy_cmd_ack <= 0;
    wfifo_rd <= 0;

    flash_din_rdy <= 0;
    spi_cs_cmd <= 1;
    spi_st <= 0;
end
else case(spi_st)
0:
begin
    phy_cmd_ack <= 0;
    spi_cs_cmd <= 1;
    spi_rev_en <= 0;
    spi_cnt <= 0;
    flash_din <= 8'hff;

    wfifo_rd <= 0;
    if(pos_cmd_vld)
    begin
        spi_st <= 1;
    end
end
1:
begin
    if(!wfifo_empty)
    begin
        wfifo_rd <= 1;
        spi_st <= 2;
    end
end
2:
begin
    spi_cs_cmd <= 0;
    wfifo_rd <= 0;
    spi_st <= 3;
end
3:
begin
    flash_din <= wfifo_dout[7:0];
    flash_din_rdy <= 1;
    wfifo_rd <= 1;
    spi_st <= 4;
end
4:
begin
    wfifo_rd <= 0;
    flash_din_rdy <= 0;
    spi_st <= 5;
end
5:
begin
    flash_din_rdy <= 0;
    if(flash_data_done)
    begin
        if(wfifo_dout[17])
        begin
            rd_len <= wfifo_dout[16:0];
            spi_cnt <= 1;

            flash_din <= 8'hff;

            if(wfifo_dout[16:0] == 0) spi_st <= 7;
            else 
            begin
                flash_din_rdy <= 1;
                spi_st <= 6;
            end
        end
        else
        begin
            flash_din <= wfifo_dout[7:0];
            flash_din_rdy <= 1;
            wfifo_rd <= 1;
            spi_st <= 4;
        end
    end
end

6:
begin
    spi_rev_en <= 1;
    flash_din_rdy <= 0;
    if(flash_data_done)
    begin
        if(spi_cnt == rd_len) 
            spi_st <= 7;
        else 
        begin
            spi_cnt <= spi_cnt + 1;
            flash_din <= 8'hff;
            flash_din_rdy <= 1;
        end
    end
end
7:
begin
    spi_cs_cmd <= 1;
    spi_rev_en <= 0;
    flash_din_rdy <= 0;
    phy_cmd_ack <= 1;
    spi_st <= 0;
end
default : spi_st <= 0;
endcase

reg [7:0] rx_fifo_din = 0;
reg rx_fifo_we = 0;

//=======================================================================================
// 系统上层程序 发送 rd_dev_state_en, 想要得到 rd_dev_state。 从phy_clk 时钟域 转换到 sys_clk 时钟域
reg [7:0] rd_dev_state_r = 0;
always @ (posedge sys_clk)
if(rd_dev_state_en)
    rd_dev_state_r <= rx_fifo_din;
else
    rd_dev_state_r <= 0;

assign rd_dev_state = rd_dev_state_r;

// 从sys_clk 时钟域 转换到 phy_clk 时钟域 
reg rd_dev_state_en_phy = 0;
always @ (posedge phy_clk)
rd_dev_state_en_phy <= rd_dev_state_en;

//=======================================================================================
// 05 寄存器读取的内容,没有被写进fifo, 当然也是可以写入的, 再由上层程序读取。
always @ (posedge phy_clk)
if(flash_dout_vld & spi_rev_en)
begin
    rx_fifo_din <= flash_dout;
    rx_fifo_we <= (SPI_MODE == "FLASH") ? ~rd_dev_state_en_phy : 1'b1;
//    rx_fifo_we <= 1'b1;
end
else rx_fifo_we <= 0;

wire rx_fifo_full;
SPI_FIFO_RD SPI_FIFO_RD_inst
(
    .wr_clk (phy_clk),      // input wire wr_clk
    .din    (rx_fifo_din),  // input wire [7 : 0] din
    .wr_en  (rx_fifo_we),   // input wire wr_en
    .full   (rx_fifo_full), // output wire full

    .rd_clk (sys_clk),      // input wire rd_clk
    .rd_en  (spi_rd),       // input wire rd_en
    .dout   (spi_dout),     // output wire [7 : 0] dout
    .valid  (),             // output wire valid
    .empty  (spi_empty),    // output wire empty

    .rst    (!rst_n)        // input wire rst
);

//=======================================================================================
// spi_phy 驱动模块

spi_phy spi_phy_inst
(
    .phy_clk         (phy_clk), 

    .spi_cs_cmd      (spi_cs_cmd),

    .flash_din       (flash_din),
    .flash_din_rdy   (flash_din_rdy),
    .flash_data_done (flash_data_done),

    .flash_dout      (flash_dout),
    .flash_dout_vld  (flash_dout_vld),

    .spi_cs          (spi_cs),   // SPI片选  
    .spi_clk         (spi_clk),  // SPI 时钟
    .spi_mosi        (spi_mosi), // SPI主器件输出从器件输入
    .spi_miso        (spi_miso), // SPI主器件输入从器件输出

    .rst_n           (rst_n)
);


//=======================================================================================

endmodule

 

模块接口介绍

 

input  phy_clk,

// phy interface 和其他spi 设备连接的pin
output spi_cs,   //SPI片选 
output spi_clk,  //SPI时钟 
output spi_mosi, //SPI主器件输出从器件输入
input  spi_miso, //SPI主器件输入从器件输出

// 系统时钟域
input  sys_clk,        // 上层模块(系统时钟域) 时钟 
input  [17:0] spi_din, // 上层模块(系统时钟域) 写入spi_fifo_wr (IP) 的数据
input  spi_wr,         // 上层模块(系统时钟域) 写入spi_fifo_wr 写信号

input  spi_rd,         // 上层模块(系统时钟域) 读 spi_fifo_rd 读信号
output [7:0] spi_dout, // 上层模块(系统时钟域) 读 spi_fifo_rd 中的数据(由 spi_phy.v 写入)
output spi_empty,      // spi_fifo_rd (IP) 空 信号指示

input  spi_cmd_vld,    // 上层模块(系统时钟域)通知本模块 状态机, 上层命令发送完毕, 本模块可以读取spi_fifo_wr 中的数据,并执行当前spi 操作, 多个系统时钟宽度。
output spi_cmd_ack,    // 本模块(phy时钟域)返回给上层模块, 通知当前的命令(指令)已经执行完毕, 等待上层模块发送新的命令,多个phy 时钟宽度。

input  rd_dev_state_en,    // 上层模块(系统时钟域)读 spi flash (spi 设备) 05 寄存器(stutas register)
output [7:0] rd_dev_state, // 上层模块(系统时钟域)05 寄存器 的数据,

input rst_n

在本模块接口中,上层模块的spi 命令主要是通过fifo 进行交互的, 这样可以隔离不同的时钟域。

spi_cmd_vld, spi_cmd_ack 这对信号用于通知上层和本层模块:命令是否可以执行, 命令是否执行完毕, 多是多时钟周期的信号, 然后对这些信号采集上升沿 , 就可以实现时钟域转换了。

rd_dev_state_en, rd_dev_state 这对信号用于对spi flash 的05 寄存器进行读取, 当然也可以使用spi_fifo_rd 进行读取, 因为05寄存器需要反复读取,作为上层模块的某些固定操作,可以使用这对信号直接读取,不再使用spi_fifo_rd 通道了。

 

 

spi_cmd_vld, spi_cmd_ack  信号转换

 

reg [1:0] phy_cmd_vld = 0;
always @ (posedge phy_clk or negedge rst_n)
if(!rst_n) phy_cmd_vld <= 0;
else phy_cmd_vld <= {phy_cmd_vld[0], spi_cmd_vld};

wire pos_cmd_vld = (phy_cmd_vld == 2’b01) ? 1’b1 : 1’b0;

对上层模块spi_cmd_vld 信号(sys_clk 时钟域)在phy 时钟域下采样,获得上升沿 pos_cmd_vld 供本层模块使用。这也是异步时钟域信号转换的一种方法。

%title插图%num

reg phy_cmd_ack = 0;
reg [7:0] phy_cmd_ack_r = 0;
always @ (posedge phy_clk )
if(phy_cmd_ack) phy_cmd_ack_r <= 8’hff;
else phy_cmd_ack_r <= {phy_cmd_ack_r[6:0], 1’b0};

assign spi_cmd_ack = phy_cmd_ack_r[7];

本层模块产生的单沿信号 phy_cmd_ack 进行多时钟周期延迟,产生长周期(phy_clk)  信号,供上层模块异步采样。

%title插图%num

命令写fifo 的数据结构

 

%title插图%num

%title插图%num

这里我们设计了一个spi_fifo_wr 数据结构, 这个fifo  数据的宽度为18bit, 不同的spi 操作(transaction)可能需要很多条命令才能完成。举例: spi flash 写操作: 02 + addr[23:16] + addr[15:08] + addr[07:00] + data0 + data1 + data2 + data3 + ……

命令格式:

1)所有命令都是8bit的, bit[17] 都必须 为 0, bit[16:08] 是多少,不重要, 也不关心。

2)所有命令都必须有且只有一条读操作的长度(RD LENGTH) ,作为最后一条指令 ,同时也是本次操作的结束标志 bit[17] 必须为 1; bit[16:00] 代表 读取数据(bytes)次数,如果不需要读取bit[16:00] = 0, 最多可以读出 17’h 1_1111_1111 这么多个byes 的数据。

3)  所有写操作 (不需要从spi flash 设备中读取数据) ,最后一条指令必须是 18’h2_0000 。 (结束标志, 读取长度 0 byte)。

4)如果有读spi 的操作 ,最后一条指令必须是 (18’h2_0000_0000 | RD LENGTH) 。 (结束标志, 读取长度 RD LENGTH bytes)。

5)fifo中只能有一个操作(transaction) ,每个操作可能有多个命令。例如:02 + addr[23:16] + addr[15:08] + addr[07:00] + data0 + data1 + data2 + data3 。 向spi flash 某个地址写入 4 个数据,这里就有8 个命令  +  结束命令 (18’h2_0000_0000) 共同组成一个操作(transaction)。

 

命令举例:

1) 在 0x30_0000 地址 连续写 0x01, 0x02,0x03, 0x04  4个bytes 数据

操作(transaction)1:  // 打开spi flash 写有效

18’h0_0006

18’h2_0000

操作(transaction)2:  // 在 0x30_0000 地址 连续写 0x01, 0x02,0x03, 0x04  4个bytes 数据

18’h0_0002

18’h0_0030

18’h0_0000

18’h0_0000

18’h0_0001

18’h0_0002

18’h0_0003

18’h0_0004

18’h2_0000

 

操作(transaction)3:  // 连续读取05 寄存器,直到 状态寄存器返回的数据 最低位 为 0

18’h0_0005

18’h2_0001

 

2) 从spi flash 读取DEV ID 3 bytes

18’h0_009f

18’h2_0003

3) 从 0x40_0000 读取 11个数据

18’h0_0003

18’h0_0040

18’h0_0000

18’h0_0000

18’h2_000b

 

状态机解析:

状态机主要用于读取spi_fifo_wr 中的数据,同时将相关的命令发给spi_phy.v

状态0:空闲状态, 等待 fifo中填满一条操作(transaction),上层会发送一个操作准备好的信号,经过时钟域转换得到pos_cmd_vld, 这时,准备读取spi_fifo_wr 中的数据了,跳转状态1

状态1:如果fifo 不是空, 开始读取spi_fifo_wr , 跳转状态2

状态2:由于使用的是标准的fifo , 所以需要等待一个时钟周期,才能得到fifo里的数据。 跳转状态3 

状态3:将fifo中读取到的数据发送给spi_phy.v  同时发送 flash_din_rdy 信号给spi_phy.v 模块 ; 读取spi_fifo_wr 中的下一个数据, 跳转状态4

状态4:跳转状态5

状态5:如果spi_phy.v 完成数据的发送, 那就看一下最新从fifo中读取到的数据格式。

如果从fifo中读到的数据有结束标志(bit 17 == 1),

if(RD LENGTH == 0),   跳转状态 7

else  连续发送 8  个时钟, 这时的 flash_din 是多少已经不重要了, spi 控制器 只要连续发送 时钟, 数据就会从miso 线上返回。 跳转状态 6

如果从fifo中读到的数据结束标志(bit 17 == 0) , 表示还有命令需要发送, 发送当前的数据到spi_phy.v  , 从fifo中读下一个数据,跳转状态 4

状态6:准备开始接收 miso 线过来的数据 spi_rev_en = 1。 同时spi_cnt 计数器 加一。

if(RD LENGTH == spi_cnt),   跳转状态 7

else  继续发送 时钟。

状态7:所有操作(transcation 结束) ,通知上层模块 本次操作结束(phy_cmd_ack = 1) . 返回状态0

 

rd_dev_state_enrd_dev_state  信号操作

 

// 系统上层程序 发送 rd_dev_state_en, 想要得到 rd_dev_state。 从phy_clk 时钟域 转换到 sys_clk 时钟域
reg [7:0] rd_dev_state_r = 0;
always @ (posedge sys_clk)
if(rd_dev_state_en)
    rd_dev_state_r <= rx_fifo_din;
else
    rd_dev_state_r <= 0;

assign rd_dev_state = rd_dev_state_r;

// 从sys_clk 时钟域 转换到 phy_clk 时钟域 
reg rd_dev_state_en_phy = 0;
always @ (posedge phy_clk)
rd_dev_state_en_phy <= rd_dev_state_en;

//=======================================================================================
// 05 寄存器读取的内容,没有被写进fifo, 当然也是可以写入的, 再由上层程序读取。
always @ (posedge phy_clk)
if(flash_dout_vld & spi_rev_en)
begin
    rx_fifo_din <= flash_dout;
    rx_fifo_we <= (SPI_MODE == "FLASH") ? ~rd_dev_state_en_phy : 1'b1;
// rx_fifo_we <= 1'b1;
end
else rx_fifo_we <= 0;

将spi_phy.v 读取到的数据赋值给 rd_dev_state。 同时 将 rd_dev_state_en 同步到phy_clk 时钟域上。

接下来就是 spi_fifo_rd (IP ) 的写入操作了, 如果不希望 05 状态寄存器写入 fifo 使用:

rx_fifo_we <= (SPI_MODE == “FLASH”) ? ~rd_dev_state_en_phy : 1’b1;

如果认为单独提取 05 状态 不是非常必要,上层可以读取fifo ,得到 05 寄存器的状态 可以设置:

rx_fifo_we <= 1’b1;

目前工程中是将上层模块某些固定的操作中05 寄存器的读取,单独使用, 而不是写入到fifo中。

SPI 数据读取fifo

 

wire rx_fifo_full;
SPI_FIFO_RD SPI_FIFO_RD_inst
(
    .wr_clk  (phy_clk),        // input wire wr_clk
    .din     (rx_fifo_din),    // input wire [7 : 0] din
    .wr_en   (rx_fifo_we),     // input wire wr_en
    .full    (rx_fifo_full),   // output wire full

    .rd_clk  (sys_clk),        // input wire rd_clk
    .rd_en   (spi_rd),         // input wire rd_en
    .dout    (spi_dout),       // output wire [7 : 0] dout
    .valid   (),               // output wire valid
    .empty   (spi_empty),      // output wire empty

    .rst     (!rst_n)          // input wire rst
);

 

spi 接收fifo 主要用于从spi_phy.v 的miso 线上接收数据,经过 串行到并行的decode ,得到 8bit 的接收数据。

always @ (posedge phy_clk)

if(flash_dout_vld & spi_rev_en)

begin

    rx_fifo_din <= flash_dout;

    rx_fifo_we <= (SPI_MODE == “FLASH”) ? ~rd_dev_state_en_phy : 1’b1;

// rx_fifo_we <= 1’b1;

end

else rx_fifo_we <= 0;

其中 flash_dout, flash_dout_vld 是从flahs_phy.v 模块传输过来的, 同时配合本层状态机逻辑 spi_rev_en  (接收数据 允许)。 最终得到 rx_fifo_din, rx_fifo_we 这两个fifo 接口上的信号。

spi_fifo_rd  使用的的是标准fifo, 8bit 输入, 8bit输出, 没有相关的数据结构,即:所有数据都是从spi_phy.v 接收来的。 上层模块只需要读取这个fifo , 就可以得到相应的数据了。

 

仿真波形

%title插图%num

spi_fifo_wr  fifo中输入 18’h0_009f,  18’h2_0003 。  发送9f 命令给spi flash , 从flash 中得到相应的read-id 信息。

%title插图%num

 

 

总结

到此, spi_cmd.v 模块介绍完毕, 大家可以注意到,这个模块本身(配合spi_phy.v)  不但可以和spi flash (外设)通讯, 也可以连接其他种类的spi (外设)。 这个模块本身是和spi flash 无关的, 当然,spi flash 只是其中的一种应用罢了。

 

Posted in FPGA, FPGA, FPGA, IC, IP开发, RISC-V, RISC-V IPcore设计, 开发板, 教材与教案, 文章

发表评论

相关链接