> 技术文档 > 基于vivado设计一个参数化的同步FIFO_vivado设计同步fifo流程

基于vivado设计一个参数化的同步FIFO_vivado设计同步fifo流程

-----大三生初步接触  仅供参考-----

一、题目描述:

设计一个同步FIFO模块。该FIFO应具有可配置的数据宽度(DATA_WIDTH)和深度(DEPTH)。FIFO的操作应严格遵循时钟信号,并提供“满”(full)和“空”(empty)状态指示信号。

设计要求:

  1. 参数化:
    • 使用 parameter 定义数据宽度 DATA_WIDTH (默认值 8)。
    • 使用 parameter 定义FIFO深度 DEPTH (默认值 16)。假设 DEPTH 总是2的幂次方,以便简化地址指针逻辑。
  2. 接口(Ports):

端口名

方向

位宽

描述

clk

input

1

时钟信号

rst_n

input

1

异步复位信号,低电平有效

wr_en

input

1

写使能信号

w_data

input

DATA_WIDTH

写入FIFO的数据

rd_en

input

1

读使能信号

r_data

output

DATA_WIDTH

从FIFO读出的数据

full

output

1

FIFO满状态标志 (1 = 满, 0 = 未满)

empty

output

1

FIFO空状态标志 (1 = 空, 0 = 非空)

3.编程语言:基于verilog-2001和verilog-95,有适当的注释。

4.Testbench Verilog代码 (fifo_tb.v):编写一个测试平台来验证你的FIFO设计。要求交付测试计划,测试计划中包含要测试哪些功能和收集哪些覆盖率;按照测试计划完成测试;输出测试结果文档(说明针对计划的结果)。

5.基于Xilinx的Vivado进行设计和验证,任选FPGA型号

二、设计思路:

1.确定模块端口

根据题目要求,我先确定了模块的输入输出端口:

(1)时钟(clk)和复位(rst_n)

(2)读写控制:写使能(wr_en)和写数据(w_data),读控制:读使能(rd_en)和读数据(r_data)

(3)状态指示:空(empty)和满(full)信号

2. 内部结构设计

存储部分:使用二维寄存器数组mem来存储数据,宽度为DATA_WIDTH,深度为DEPTH(8位宽16深,就是16个8位寄存器)

读写指针:读写指针需要能循环遍历整个存储空间

计数器:用一个简单的计数器来跟踪FIFO中的数据量进行空满判断(count==0为空,count==DEPTH为满)

4. 关键逻辑实现

(1)写操作
在时钟上升沿,如果写使能且不满,就把数据写入当前写指针位置
写完后指针加1,自动回绕(因为深度是2的幂次方)
(2)读操作
在时钟上升沿,如果读使能且不空,就从当前读指针位置读出数据
读完后指针加1,自动回绕
(3)空满判断
空:计数器为0
满:计数器等于DEPTH
同时读写时计数器不变(一进一出)
用组合逻辑assign实现,实时反映状态

4.测试文件构思

(1)写满测试:连续写入直到FIFO满

(2)读空测试:连续读出直到FIFO空

(3)并发读写:同时进行读写操作

(4)随机测试:随机读写组合

三、程序源码

1.程序源码

`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// 同步FIFO模块 - 参数化设计
// 文件名:fifo420.v
// 说明:支持可配置数据宽度和深度,深度需为2的幂次方
module fifo420  #(
    parameter DATA_WIDTH = 8,//参数设置
    parameter DEPTH = 16
)(
//input总是wire型
//output中wire:用于连接模块或由assign驱动的信号。reg:用于在always或initial块中被赋值的信号。
    input  wire                  clk,    // 全局时钟
    input  wire                  rst_n,  // 复位信号
    input  wire                  wr_en,  // 写使能
    input  wire [DATA_WIDTH-1:0] w_data, // 写入数据
    input  wire                  rd_en,  // 读使能
    output reg  [DATA_WIDTH-1:0] r_data, // 读出数据
    output wire                  full,   // FIFO满标志
    output wire                  empty   // FIFO空标志
);
localparam ADDR_WIDTH = $clog2(DEPTH);  // 地址位宽
reg [ADDR_WIDTH:0] wr_ptr, rd_ptr;     // 读写指针(多1位用于满标志判断)
reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];  // 存储器
//DATA_WIDTH是每个数据的位数(比如8位),DEPTH是FIFO能存储的数据个数(比如16个)
//所以这是一个宽度为DATA_WIDTH,深度为DEPTH的存储阵列

    // FIFO计数器
    reg [$clog2(DEPTH):0] count;
    
    // 空满标志逻辑
   assign empty = (count == 0);
   assign full = (count == DEPTH);
//用 assign?empty 和 full 是组合逻辑信号:它们的值直接由当前 count 的值决定,不需要时钟触发。

// 写入逻辑
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            wr_ptr <= 0;
        end else if (wr_en && !full) begin
            mem[wr_ptr[$clog2(DEPTH)-1:0]] <= w_data;
            wr_ptr <= wr_ptr + 1;
        end
    end
    
    // 读出逻辑
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            rd_ptr <= 0;
            r_data <= 0;
        end else if (rd_en && !empty) begin
            r_data <= mem[rd_ptr[$clog2(DEPTH)-1:0]];
            rd_ptr <= rd_ptr + 1;
        end else begin
            r_data <= r_data;  // 保持r_data不变(实际上默认就是这样)
        end
    end
    // 计数器逻辑
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            count <= 0;
        end else begin
            case ({wr_en, rd_en})
                2\'b01: if (!empty) count <= count - 1;  // 只读
                2\'b10: if (!full) count <= count + 1;    // 只写
                2\'b11: if(empty)count <= 1;                // 同时读写,计数不变
                default: count <= count;
            endcase
        end
    end

   
endmodule
 

2.tb文件源码

`timescale 1ns / 1ps

module tb_fifo420;

    // 参数定义
    parameter DATA_WIDTH = 8;
    parameter DEPTH = 16;

    // 信号定义
    reg clk;
    reg rst_n;
    reg wr_en;
    reg rd_en;
    reg [DATA_WIDTH-1:0] w_data;
    wire [DATA_WIDTH-1:0] r_data;
    wire full;
    wire empty;

    // 实例化 DUT(Device Under Test)
    fifo420 #(
        .DATA_WIDTH(DATA_WIDTH),
        .DEPTH(DEPTH)
    ) uut (
        .clk(clk),
        .rst_n(rst_n),
        .wr_en(wr_en),
        .w_data(w_data),
        .rd_en(rd_en),
        .r_data(r_data),
        .full(full),
        .empty(empty)
    );

    // 生成时钟,10ns周期(100MHz)
    initial begin
        clk = 0;
        forever #5 clk = ~clk;
    end

    // 复位过程
    task reset_fifo;
    begin
        rst_n = 0;
        wr_en = 0;
        rd_en = 0;
        w_data = 0;
        #20;
        rst_n = 1;
        #20;
    end
    endtask

    // 写满测试
    task write_full_test;
    begin
        $display(\"=== 写满测试开始 ===\");
        wr_en = 1;
        rd_en = 0;
        for (integer i = 0; i < DEPTH + 5; i = i + 1) 
       begin

           w_data = i;
           @(posedge clk);  // 关键修复:等待时钟边沿!
            $display(\"写入数据:%h\", w_data);
           if (full) $display(\"FIFO 已满!\");
          
        end
        wr_en = 0;
       
      
    end
    endtask

    // 读空测试
    task read_empty_test;
    begin
        $display(\"=== 读空测试开始 ===\");
        rd_en = 1;
        wr_en = 0;
        for (integer i = 0; i < DEPTH + 5; i = i + 1) 
   begin
            @(posedge clk);
            $display(\"读出数据[%0d] = %h\", i, r_data);
        end
        rd_en = 0;
        @(posedge clk);
        $display(\"=== 读空测试完成 ===\");
    end
    endtask

    // 并发读写测试
    task concurrent_read_write_test;
    begin
        $display(\"=== 并发读写测试开始 ===\");
        wr_en = 1;
        rd_en = 1;
          for (integer i = 0; i < DEPTH + 5; i = i + 1) begin
            w_data = $random;
            @(posedge clk);
            $display(\"写入:%h,读出:%h\", w_data, r_data);
            
            @(posedge clk);
        end
        wr_en = 0;
        rd_en = 0;
        @(posedge clk);
        $display(\"=== 并发读写测试完成 ===\");
    end
    endtask

    // 随机读写测试
    task random_test;
    begin
        $display(\"=== 随机读写测试开始 ===\");
        for (integer i = 0; i < 50; i = i + 1) begin
            wr_en = $random % 2;
            rd_en = $random % 2;
            w_data = $random;
            @(posedge clk);
           $display(\"time=%0t wr_en=%b, rd_en=%b, w_data=%h, r_data=%h, full=%b, empty=%b\", $time, wr_en, rd_en, w_data, r_data, full, empty);

        end
        wr_en = 0;
        rd_en = 0;
        @(posedge clk);
        $display(\"=== 随机读写测试完成 ===\");
    end
    endtask

    // 主仿真流程
    initial begin
        // 初始化信号
        wr_en = 0;
        rd_en = 0;
        w_data = 0;
        
        // 测试顺序
        reset_fifo;
        write_full_test;
        read_empty_test;
        concurrent_read_write_test;
        random_test;

        $display(\"=== 所有测试完成 ===\");
        $finish;
    end

endmodule
 

四、过程bug解决

1.在运行仿真过程中,有时候修改tb文件的内容后仿真波形并没有变化。网上没也看到这种情况,发现仿真之后需要点击close simulation才能更新。否则跑出来一直是之前的波形。

2.不显示已满

for (integer i = 0; i < DEPTH + 5; i = i + 1) begin w_data = i; $display(\"写入数据:%h\", w_data); if (full) $display(\"FIFO 已满!\"); // 问题点:缺少 @(posedge clk)!end

分析:缺少 @(posedge clk)full 是同步信号,需在时钟边沿检查。当前代码在同一个时间点连续写入,无法正确触发 full修改在问题点前加上 @(posedge clk)即可

3.读空后再同时读写时,empty一直为1导致无法正常读取

 考虑特殊情况修改代码:2\'b11: if(empty)count <= 1;                // 同时读写,计数不变

4.覆盖率测试

参考网页

怎么用Vivado做覆盖率分析_vivado覆盖率统计-CSDN博客

5.修改建议:

(1)用my_clog2函数代替$clog2 系统函数

(2)增加count变量看波形

(3)再根据coverage进行修改