Menu Close

自建同步时钟,数据等宽FIFO IP 方法

我们在实际的工作中, 可能会使用一些IP 核, 例如 PLL, MMCM, 双端口ram, fifo 等等。 这时,我们会使用xilinx ISE, vivado , Altera Quartus 等IDE 集成软件, 通过创建厂家给出的一些IP 核 来完成我们工作中一些需求。如果xilinx, Altera 等厂家没有提供我们相应的IP 核 , 或者是他们提供的IP核 不能满足用户的特定需求,这时,就需要用户自主创建一些IP 核,来满足 客户工程的需要了。

本文介绍一种fifo IP 核的创建方法,标准的fifo IP 核 都是会被fpga 厂家提供的, 不论是xilinx, altera ,还是其他公司。 但有时我们编写的verilog 代码 不知道会在什么样的fpga 使用, 有可能是xilinx, 也有可能是altera ,或者是其他公司的fpga。因为 verilog 语言是通用的, 所以通过verilog 语言自建的IP 的适用范围会更加宽广,即使是准备生产芯片(asic 等,不再使用fpga 芯片),verilog 也是非常方便被导入到生产环节的(不必一定需要工厂提供相对应的IP了)

这种自建的fifo IP 会在很多verilog 工程中被应用,这样用户 发布的代码 往往不需要修改, 就可以在不同公司的平台上使用。

本文中自建的IP 是一个同步时钟的FIFO, 同时 fifo 写入 数据的宽度 和数据 读出的宽度 相等。

 

参考代码:

 

`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company: Fraser Innovation Inc
// Engineer: WilliamG
// 
// Create Date: 2021/04/22 09:12:51
// Design Name: 
// Module Name: syn_fifo.v
// Project Name: 
// Target Devices: 
// Tool Versions: 
// Description: 
// 
// Dependencies: 
// 
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
// 
//////////////////////////////////////////////////////////////////////////////////

// first word for through: rd ,next clock get a new data
module syn_fifo # 
(
    parameter ADDR_WIDTH = 3,
    parameter DATA_WIDTH = 8
)
(
    input  clk,

    input  wr_en,
    input  [DATA_WIDTH - 1:0] din,
    output full,

    input  rd_en,
    output [DATA_WIDTH - 1:0] dout,
    output empty,

    output [ADDR_WIDTH:0] data_count,
    output valid,

    input  srst
);

localparam FIFO_DEPTH = 2**ADDR_WIDTH;
reg [DATA_WIDTH - 1:0] ram [0: FIFO_DEPTH - 1];

reg [ADDR_WIDTH - 1:0] rd_addr = 0;
assign dout = ram[rd_addr];

reg [ADDR_WIDTH - 1:0] wr_addr = 0;
wire ptr_match = (wr_addr == rd_addr) ? 1'b1 : 1'b0;

reg maybe_full = 0;
assign empty = ptr_match & (!maybe_full);
assign full = ptr_match & maybe_full;

wire wr_vld = (!full) & wr_en;
wire rd_vld = (!empty) & rd_en;

wire [ADDR_WIDTH:0] wr_sub_rd = wr_addr - rd_addr;
wire [ADDR_WIDTH - 1:0] ptr_diff = wr_sub_rd[ADDR_WIDTH - 1:0];
wire carry_bit = maybe_full & ptr_match;

assign data_count = {carry_bit,ptr_diff};


always @(posedge clk or posedge srst)
if (srst) wr_addr <= 0;
else if (wr_vld) wr_addr <= wr_addr + 1;


wire [DATA_WIDTH - 1:0] din_w = din;
always @(posedge clk or posedge srst)
if(srst) ram[0] <= 0;
else if(wr_vld) ram[wr_addr] <= din_w;


always @(posedge clk or posedge srst)
if (srst) rd_addr <= 0;
else if (rd_vld) rd_addr <= rd_addr + 1;


always @(posedge clk or posedge srst)
if (srst) 
    maybe_full <= 0;
else if (wr_vld != rd_vld)
    maybe_full <= wr_vld;


assign valid = rd_en & (!empty);
endmodule

接口介绍:

module syn_fifo # 
(
    parameter ADDR_WIDTH = 3,      // fifo 地址线 宽度 
    parameter DATA_WIDTH = 8       // fifo 数据线 宽度
)
(
// 同步时钟fifo 的输入时钟,由于是同步fifo ,所以写入和读出使用相同的时钟
input  clk,                        
input  wr_en,                      // fifo 写 信号
input  [DATA_WIDTH - 1:0] din,     // fifo 写 数据
output full,                       // fifo 满 标志

input  rd_en,                      // fifo 读 信号
output [DATA_WIDTH - 1:0] dout,    // fifo 读 数据
output empty,                      // fifo 空 标志

output [ADDR_WIDTH:0] data_count,  // fifo 里当前还有多少数据
output valid,                      // 读出数据有效信号,和dout 是 同步输出的

input  srst                        // fifo 复位信号
);

 

由于使用了参数化设计, 所以这个fifo IP 存储深度是可以被用户配置的。 同时写入和读出的数据宽度, 用户也是可以配置的。

 

这里的代码讲解都是以 ADDR_WIDTH = 3, DATA_WIDTH = 8 进行讲解的。其他情况,对代码的理解也是相似的。

 

FIFO 数据结构定义

// FIFO_DEPTH 表示整体fifo 的深度,例如 ADDR_WIDTH = 3 , 整体的FIFO 深度为 8 ,有8 个存储单元
localparam FIFO_DEPTH = 2**ADDR_WIDTH;  

// 定义fifo 的数据结构, 数据宽度为 8 (缺省值),用户可以更改; FIFO 的深度为 8 (0 - 7 )
reg [DATA_WIDTH - 1:0] ram [0: FIFO_DEPTH - 1]; 

reg [ADDR_WIDTH - 1:0] rd_addr = 0;  // FIFO 读数据指针
assign dout = ram[rd_addr];          // 将fifo 里的数据 输出到端口上
reg [ADDR_WIDTH - 1:0] wr_addr = 0;  // FIFO 写数据指针

 

FIFO 相关的参数定义

// 写地址指针是否可读地址指针相同,当两个地址相同时,可能是当前fifo 为空,也可能是当前fifo 满
wire ptr_match = (wr_addr == rd_addr) ? 1'b1 : 1'b0; 

/* 
表示 最后一次操作: 1 => 写操作, 0 => 读 操作。 如果是没有操作,
或者是读写同时发生, maybe_full 这个值保持之前的值不变。
*/
reg maybe_full = 0;      

// 读写指针相同, 最后一次操作是 操作 ,整个fifo 为
assign empty = ptr_match & (!maybe_full);  

// 读写指针相同, 最后一次操作是 操作 ,整个fifo 为
assign full = ptr_match & maybe_full;

 

FIFO 读写 相关的操作

wire wr_vld = (!full) & wr_en;         // 如果fifo 不是状态, 同时又是操作, 这时有效
wire rd_vld = (!empty) & rd_en;        // 如果fifo 不是状态, 同时又是操作, 这时有效

wire [ADDR_WIDTH:0] wr_sub_rd = wr_addr - rd_addr;          // 写地址指针 - 读地址指针

/* 
    写地址指针 - 读地址指针 的二进制 余数
    1)wr_addr  是 写数据地址指针, rd_addr 是读数据指针。 读写指针都是从 0 开始 一直前进 ,
    加到 7 之后 再 + 1 时 ,回到 0。 
    2)读指针只能小于 , 等于 写指针, 不可能大于 写指针。
    3)写指针 - 读指针 的结果 最大 为 8 , 不可能大于 8
    所以 (举例)
       当 读地址 = 3 , 写地址 = 2 时,代表的是 写指针 2 + 8 , 读指针为 3; ptr_diff = 3'b111
       当 读地址 = 4 , 写地址 = 7 时,代表的是 写指针 7 ,     读指针为 4; ptr_diff = 3'b011

       当 读地址 = 0 , 写地址 = 7 时,代表的是 写指针 7 ,     读指针为 0; ptr_diff = 3'b111 
       当 读地址 = 7 , 写地址 = 0 时,代表的是 写指针 0 + 8 , 读指针为 7; ptr_diff = 3'b001

       当 读地址 = 5 , 写地址 = 5 时,代表的是 写指针 5     , 读指针为 5; ptr_diff = 3'b000
*/
wire [ADDR_WIDTH - 1:0] ptr_diff = wr_sub_rd[ADDR_WIDTH - 1:0];

//读写指针相同,最后一次操作是 操作,整个fifo 为满,carry_bit = 1;读写指针不同carry_bit = 0
//读写指针相同,最后一次操作是 操作,整个fifo 为carry_bit = 0;读写指针不同carry_bit = 0
wire carry_bit = maybe_full & ptr_match;  

// 读写指针相同, fifo full: data_count = 4'b1000; fifo empty: data_count = 4'b0000;
// 读写指针不同, data_count = {1'b0, prt_diff[2:0]};
assign data_count = {carry_bit,ptr_diff};

这里,我们会发现,在计算fifo counter 时 ,不论写地址 是否 小于 读地址 ,只要 写地址 – 读 地址 , prt_diff 的结果都是正确的(1 – 7 ) , 只有 0 ,8 这两个值需要看当前fifo 是空 还是满 才能判断。

写地址计数器

always @(posedge clk or posedge srst)
if (srst) wr_addr <= 0;
else if (wr_vld) wr_addr <= wr_addr + 1;

wr_en 写fifo 信号有效, fifo 又不是为时, wr_vld 有效; 这时 写地址 + 1

 

写数据到存储空间

wire [DATA_WIDTH - 1:0] din_w = din;
always @(posedge clk or posedge srst)
if(srst) ram[0] <= 0;
else if(wr_vld) ram[wr_addr] <= din_w;

wr_en 写fifo 信号有效, fifo 又不是为时, wr_vld 有效; 这时 写数据到相应的地址空间中。

 

读地址计数器

always @(posedge clk or posedge srst)
if (srst) rd_addr <= 0;
else if (rd_vld) rd_addr <= rd_addr + 1;

rd_en 写fifo 信号有效, fifo 又不是为时, rd_vld 有效; 这时 读地址 + 1

 

满足读写不同时发生的最后一次操作

always @(posedge clk or posedge srst)
if (srst) 
    maybe_full <= 0;
else if (wr_vld != rd_vld)
    maybe_full <= wr_vld;

从FIFO 读出的数据有效标志

assign valid = rd_en & (!empty);

当rd_en 有效, 同时 fifo 不为 空, 这时输出的数据是有效的。

 

仿真代码:

`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company: Fraser Innovation Inc
// Engineer: WilliamG
// 
// Create Date: 03/02/2021 03:51:26 PM
// Design Name: 
// Module Name: tb_sim
// Project Name: 
// Target Devices: 
// Tool Versions: 
// Description: 
// 
// Dependencies: 
// 
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
// 
//////////////////////////////////////////////////////////////////////////////////


module tb_sim(

);

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


integer i = 0;
integer k = 0;
reg end_flag = 0;

localparam ADDR_WIDTH = 3;
localparam DATA_WIDTH = 8;


reg fifo_wr = 0;
reg [DATA_WIDTH - 1:0] fifo_din = 0;
wire full;

reg fifo_rd = 0;
wire [DATA_WIDTH - 1:0] fifo_dout;
wire empty;

wire [ADDR_WIDTH:0] io_count;

reg reset = 1;


initial
begin
    fifo_wr = 0;
    fifo_rd = 0;
    reset = 1;
    #200;
    reset = 0;
    #100;
    @(posedge clk);

    for(i = 0 ; i < 256; i = i+ 1)
    begin
        while(full) begin fifo_wr = 0; @(posedge clk); end
        fifo_wr = 1;
        fifo_din = fifo_din + 1;
        @(posedge clk);

        fifo_wr = 0;
        @(posedge clk);
    end

    fifo_wr = 0; 
    @(posedge clk); 

end

integer j = 0;
wire valid;

initial
begin
    fifo_wr = 0;
    fifo_rd = 0;
    end_flag = 0;
    #200;
    @(posedge clk);
    j = 0;
    while(empty) @(posedge clk);

    while ( k < 128 )
    begin
        while(empty) begin fifo_rd = 0; @(posedge clk); end
        fifo_rd = 1;
        @(posedge clk);

        while(empty) begin fifo_rd = 0; @(posedge clk); end
        fifo_rd = 1;
        @(posedge clk);

        while(empty) begin fifo_rd = 0; @(posedge clk); end
        fifo_rd = 1;
        @(posedge clk);

        fifo_rd = 0;
        @(posedge clk);

        while(empty) begin fifo_rd = 0; @(posedge clk); end
        fifo_rd = 1;
        @(posedge clk);
    end

    fifo_rd = 0;
    for(j = 0 ; j < 17; j = j+ 1)
    begin
        @(posedge clk);
    end

    while( k < 248 )
    begin
        while(empty) begin fifo_rd = 0; @(posedge clk); end
        fifo_rd = 1;
        @(posedge clk);

        while(empty) begin fifo_rd = 0; @(posedge clk); end
        fifo_rd = 1;
        @(posedge clk);

        while(empty) begin fifo_rd = 0; @(posedge clk); end
        fifo_rd = 1;
        @(posedge clk);

        fifo_rd = 0;
        @(posedge clk);

        while(empty) begin fifo_rd = 0; @(posedge clk); end
        fifo_rd = 1;
        @(posedge clk);
    end

    fifo_rd = 0;
    @(posedge clk);
    @(posedge clk);
    @(posedge clk);

    while( k < 256 )
    begin
        while(empty) @(posedge clk);

        fifo_rd = 1;
        @(posedge clk);
        fifo_rd = 0;
        @(posedge clk);
    end

    fifo_rd = 0;
    @(posedge clk);
    end_flag = 1;
end

initial
begin
    k = 0;
    while (1)
    begin
        if(valid) k = k + 1;
        @(posedge clk);
    end
end


syn_fifo # 
(
    .ADDR_WIDTH (ADDR_WIDTH),
    .DATA_WIDTH (DATA_WIDTH)
)
syn_fifo_inst
(
    .clk        (clk),

    .wr_en      (fifo_wr),
    .din        (fifo_din),
    .full       (full),

    .rd_en      (fifo_rd),
    .dout       (fifo_dout),
    .empty      (empty),

    .data_count (io_count),
    .valid      (valid),

    .srst       (reset)
);


end_flag end_flag_inst
(
    .clk      (clk),
    .end_flag (end_flag)

);
//==================================================================
endmodule

仿真波形

%title插图%num

reset 过后,fifo counter 为0;fifo_rd 可以连续读, 只要是valid, 输出的fifo_dout 都是有效的。empty 时, valid 为 0, 输出的 fifo_dout 无效。

 

 

%title插图%num

fifo full 时, io_count = 8; 当 full = 0 时, 又可以继续写fifo 了。

 

%title插图%num

当k = 256 时, 表示 valid 出现过256 次, 这和fifo 写入的 数值个数 一直, 到此, fifo 写 256 个数据, fifo 读 256 个数据 完毕。

 

总结

到此, 我们的同步fifo 设计完成了。

自建的fifo的优点是不需要 xilinx , altera 或者其他公司的IP, verilog 代码可以快速移植, 甚至生产。甚至可以根据自己的要求,设计fifo 输入,输出格式。

目前这个fifo 的缺点是 只能使用同步时钟(写时钟和读时钟是一致的),写数据宽度 和 读数据的宽度必须一致的。 当然,用户掌握了自建IP 的方法, 也可以解决这些问题的。

 

附件下载

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

6 Comments

  1. William

    @(posedge clk) 在仿真中经常使用, 这种用法比 #10, #5 等等定位更加准确,它代表在时钟上升沿后发生的事情。定位非常准确

  2. 张洪泉

    老师,仿真文件里很多地方都用到 @(posedge clk); 这个啥作用呢?只了解 always @(posedge clk) 用法,此时posedge clk 是敏感列表,来触发always块执行

发表评论

相关链接