4. 同步FIFO格雷码指针

任务目标

在同步FIFO设计中,读写指针采用格雷码编码,替代传统二进制指针。格雷码相邻计数值仅1比特变化,可显著降低多比特同时翻转带来的亚稳态风险(尽管同步FIFO本身无跨时钟域问题,但该设计为后续异步FIFO打基础,且便于观察资源开销差异)。本案例实现一个深度16、数据宽度8的同步FIFO,完成读写功能验证,并对比格雷码与二进制指针的资源占用。

设计要点

  • 指针宽度:深度16需4位地址,格雷码指针宽度同为4位(最高位用于区分空/满状态时需扩展1位,共5位)。
  • 空满判断:读写指针格雷码相等为空;写指针格雷码与读指针格雷码高2位取反、低2位相同为满(针对深度为2的幂次方)。
  • 指针转换:二进制指针递增后转换为格雷码存储,比较时直接使用格雷码值。

完整实现(FIFO模块 + 测试平台)

// sync_fifo_gray.v
module sync_fifo_gray #(
    parameter DATA_WIDTH = 8,
    parameter DEPTH      = 16
)(
    input  wire                  clk,
    input  wire                  rst_n,
    input  wire                  wr_en,
    input  wire [DATA_WIDTH-1:0] wr_data,
    input  wire                  rd_en,
    output wire [DATA_WIDTH-1:0] rd_data,
    output wire                  full,
    output wire                  empty
);
    // 地址宽度(深度16需4位,扩展1位用于空满判断)
    localparam ADDR_WIDTH = $clog2(DEPTH) + 1;  // 5位

    reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];
    reg [ADDR_WIDTH-1:0] wr_ptr_gray, rd_ptr_gray;
    reg [ADDR_WIDTH-2:0] wr_ptr_bin, rd_ptr_bin;  // 4位二进制指针

    wire [ADDR_WIDTH-2:0] wr_addr = wr_ptr_bin;
    wire [ADDR_WIDTH-2:0] rd_addr = rd_ptr_bin;

    // 二进制指针递增
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            wr_ptr_bin <= 0;
            rd_ptr_bin <= 0;
        end else begin
            if (wr_en && !full) wr_ptr_bin <= wr_ptr_bin + 1'b1;
            if (rd_en && !empty) rd_ptr_bin <= rd_ptr_bin + 1'b1;
        end
    end

    // 二进制转格雷码(扩展位参与转换)
    always @(*) begin
        wr_ptr_gray = {wr_ptr_bin[ADDR_WIDTH-2], wr_ptr_bin ^ (wr_ptr_bin >> 1)};
        rd_ptr_gray = {rd_ptr_bin[ADDR_WIDTH-2], rd_ptr_bin ^ (rd_ptr_bin >> 1)};
    end

    // 空满判断(格雷码比较)
    assign empty = (wr_ptr_gray == rd_ptr_gray);
    assign full  = (wr_ptr_gray[ADDR_WIDTH-1] != rd_ptr_gray[ADDR_WIDTH-1]) &&
                   (wr_ptr_gray[ADDR_WIDTH-2] != rd_ptr_gray[ADDR_WIDTH-2]) &&
                   (wr_ptr_gray[ADDR_WIDTH-3:0] == rd_ptr_gray[ADDR_WIDTH-3:0]);

    // 写数据
    always @(posedge clk) begin
        if (wr_en && !full) mem[wr_addr] <= wr_data;
    end

    // 读数据(组合逻辑输出)
    assign rd_data = mem[rd_addr];

endmodule
// tb_sync_fifo_gray.v
`timescale 1ns/1ps
module tb_sync_fifo_gray;
    reg clk = 0, rst_n = 0;
    reg wr_en = 0, rd_en = 0;
    reg [7:0] wr_data = 0;
    wire [7:0] rd_data;
    wire full, empty;

    sync_fifo_gray uut (.clk(clk), .rst_n(rst_n), .wr_en(wr_en), .wr_data(wr_data),
                        .rd_en(rd_en), .rd_data(rd_data), .full(full), .empty(empty));

    always #5 clk = ~clk;

    // 测试序列
    initial begin
        // 复位
        #20 rst_n = 1;

        // 写入16个数据(填满FIFO)
        repeat (16) begin
            @(posedge clk);
            wr_en = 1;
            wr_data = wr_data + 1;
        end
        @(posedge clk) wr_en = 0;
        $display("After write 16: full=%b empty=%b", full, empty);  // 预期 full=1

        // 读出16个数据(清空FIFO)
        repeat (16) begin
            @(posedge clk);
            rd_en = 1;
        end
        @(posedge clk) rd_en = 0;
        $display("After read 16: full=%b empty=%b", full, empty);  // 预期 empty=1

        // 边写边读测试(写入5个同时读出5个)
        repeat (5) begin
            @(posedge clk);
            wr_en = 1; rd_en = 1;
            wr_data = wr_data + 1;
        end
        @(posedge clk) wr_en = 0; rd_en = 0;
        $display("After simultaneous: full=%b empty=%b", full, empty);  // 预期 full=0 empty=0

        $finish;
    end

    // 监控读数据
    always @(posedge clk) begin
        if (rd_en && !empty) $display("Read data: %0d", rd_data);
    end
endmodule

运行与验证

# 编译与仿真(使用Icarus Verilog)
iverilog -o sim_vvp sync_fifo_gray.v tb_sync_fifo_gray.v
vvp sim_vvp

通过条件:仿真输出显示写入16个数据后 full=1,读出16个数据后 empty=1,边写边读时 full=0 empty=0,且读出的数据顺序与写入顺序一致(0~15,再0~4)。

资源对比分析

在Vivado或Quartus中对两种实现(格雷码指针 vs 二进制指针)分别综合,观察资源报告。格雷码方案额外增加约ADDR_WIDTH个异或门用于二进制转格雷码,但寄存器数量不变。对于深度16的FIFO,格雷码方案仅多消耗约5个LUT,而异步FIFO中该设计可消除多比特跨时钟域亚稳态风险,收益远大于开销。

二进制指针(4→5比特变化) 0111 → 1000(4比特翻转) 格雷码指针(仅1比特变化) 0100 → 1100(1比特翻转) 多比特同时翻转 任意时刻仅1比特变化 格雷码消除多比特翻转导致的亚稳态窗口

常见问题

  • 空满判断错误:确保格雷码指针位宽为ADDR_WIDTH(含扩展位),且满条件中高2位取反比较,低ADDR_WIDTH-2位相等。
  • 读数据时序:本设计为组合逻辑读(rd_data直接由地址译码),若需同步读需在输出加寄存器。
  • 深度非2的幂次:格雷码空满判断仅适用于深度为2的幂次方,其他深度需额外计数器辅助。