Lesson 26

📦 FIFO缓冲

🏆 异步FIFO读写正确
✅ Verilator仿真验证通过

📖 实验描述

实现异步FIFO缓冲器,支持双时钟域读写,使用Gray码安全传递指针。

FIFO(First-In First-Out)是数字设计中最常用的缓冲结构,尤其在跨时钟域通信中不可或缺。 异步FIFO的挑战: - 写端和读端运行在不同时钟频率下 - 指针跨时钟域传递存在亚稳态风险 - 满/空判断必须精确,否则导致数据丢失或读取无效数据 解决方案: 1. Gray码:将二进制指针转换为Gray码,每次只有1位变化,消除跨时钟域的亚稳态 2. 两级同步器:将Gray码指针通过2级FF同步到对侧时钟域 3. 满/空判断:写侧用同步后的读指针判满,读侧用同步后的写指针判空 这是Cliff Cummings经典论文的工业标准实现。

🧠 核心概念

📐 电路结构

电路与状态图
异步FIFO架构:

  写时钟域                    读时钟域
  ┌──────────┐               ┌──────────┐
  │ wr_ptr   │               │ rd_ptr   │
  │ wr_en    │               │ rd_en    │
  │ wr_data──┼──→ MEM ──→───┼──rd_data │
  │ full     │    [DEPTH-1:0]│ empty    │
  └────┬─────┘               └────┬─────┘
       │ Gray码+2级同步           │ Gray码+2级同步
       │    wr_ptr_gray ──→ sync  │
       │                        rd_ptr_gray ──→ sync
       ↓                          ↓
  判满: wr_gray == ~rd_gray[MSB:MSB-1] + rd_gray[rest]
  判空: rd_gray == wr_gray_sync

📝 设计步骤

  1. 1定义FIFO深度和数据位宽参数
  2. 2实现写侧逻辑:写指针递增+数据写入RAM
  3. 3实现读侧逻辑:读指针递增+数据读出
  4. 4二进制→Gray码转换函数
  5. 5Gray码→二进制转换函数
  6. 6两级同步器传递Gray码指针
  7. 7满/空标志判断逻辑

💻 Verilog实现

fifo.svSystemVerilog · Verilator 5.020
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
// Asynchronous FIFO Buffer — 异步FIFO缓冲
// Dual-clock domain, pointer-based design with gray code crossing
module fifo_async #(
    parameter DATA_W = 8,
    parameter DEPTH  = 16
)(
    input  wire             wr_clk,
    input  wire             wr_rst,
    input  wire             wr_en,
    input  wire [DATA_W-1:0] wr_data,
    output wire             full,

    input  wire             rd_clk,
    input  wire             rd_rst,
    input  wire             rd_en,
    output wire [DATA_W-1:0] rd_data,
    output wire             empty
);

localparam ADDR_W = $clog2(DEPTH);

// Memory array
reg [DATA_W-1:0] mem [0:DEPTH-1];

// Write side pointers
reg [ADDR_W:0] wr_ptr;
wire [ADDR_W-1:0] wr_addr = wr_ptr[ADDR_W-1:0];

// Read side pointers
reg [ADDR_W:0] rd_ptr;
wire [ADDR_W-1:0] rd_addr = rd_ptr[ADDR_W-1:0];

// Gray code conversion
wire [ADDR_W:0] wr_ptr_gray = wr_ptr ^ (wr_ptr >> 1);
wire [ADDR_W:0] rd_ptr_gray = rd_ptr ^ (rd_ptr >> 1);

// Synchronized gray pointers (2-stage synchronizer)
reg [ADDR_W:0] rd_ptr_gray_sync1, rd_ptr_gray_sync2;
reg [ADDR_W:0] wr_ptr_gray_sync1, wr_ptr_gray_sync2;

// Gray to binary conversion
function automatic [ADDR_W:0] gray2bin;
    input [ADDR_W:0] gray;
    integer i;
begin
    gray2bin[ADDR_W] = gray[ADDR_W];
    for (i = ADDR_W-1; i >= 0; i = i - 1)
        gray2bin[i] = gray2bin[i+1] ^ gray[i];
end
endfunction

wire [ADDR_W:0] rd_ptr_wr_side = gray2bin(rd_ptr_gray_sync2);
wire [ADDR_W:0] wr_ptr_rd_side = gray2bin(wr_ptr_gray_sync2);

// Full/Empty flags
assign full  = (wr_ptr_gray == {~rd_ptr_gray_sync2[ADDR_W:ADDR_W-1], rd_ptr_gray_sync2[ADDR_W-2:0]});
assign empty = (rd_ptr_gray == wr_ptr_gray_sync2);

// Write logic
always @(posedge wr_clk or posedge wr_rst) begin
    if (wr_rst) begin
        wr_ptr <= 0;
        rd_ptr_gray_sync1 <= 0;
        rd_ptr_gray_sync2 <= 0;
    end else begin
        // Synchronize read pointer into write clock domain
        rd_ptr_gray_sync1 <= rd_ptr_gray;
        rd_ptr_gray_sync2 <= rd_ptr_gray_sync1;

        if (wr_en && !full) begin
            mem[wr_addr] <= wr_data;
            wr_ptr <= wr_ptr + 1;
        end
    end
end

// Read logic
always @(posedge rd_clk or posedge rd_rst) begin
    if (rd_rst) begin
        rd_ptr <= 0;
        wr_ptr_gray_sync1 <= 0;
        wr_ptr_gray_sync2 <= 0;
    end else begin
        // Synchronize write pointer into read clock domain
        wr_ptr_gray_sync1 <= wr_ptr_gray;
        wr_ptr_gray_sync2 <= wr_ptr_gray_sync1;

        if (rd_en && !empty) begin
            rd_ptr <= rd_ptr + 1;
        end
    end
end

// Read data output (combinational)
assign rd_data = mem[rd_addr];

endmodule

🔬 仿真说明

仿真环境与策略

测试:写入8个数据(1-8),读出验证顺序正确。写满FIFO验证full标志,读空验证empty标志。使用异步读写时钟(50MHz写/33MHz读)验证跨时钟域稳定性。

✅ 验证结果

Verilator 5.020 仿真通过

本实验所有Verilog代码已通过Verilator编译验证,功能行为正确。测试用例覆盖核心功能路径,确保设计满足规格要求。

⚠️ 常见错误

🌍 真实世界

工业应用

异步FIFO是SoC中跨时钟域通信的基石。DDR控制器、PCIe、USB、以太网MAC都依赖FIFO缓冲。ARM AXI总线用异步FIFO连接不同频率的Master和Slave。Cliff Cummings的异步FIFO设计论文是数字设计必读文献。

💡 扩展挑战

🚀 自己动手

# 编译验证
verilator --lint-only fifo.sv --timing
verilator --cc fifo.sv --exe tb.cpp --build -j 0 --timing

🎯 试一试

修改参数

尝试修改代码中的关键参数,观察仿真结果变化:

  • 调整深度/位宽,观察行为变化
  • 修改时钟分频,测试不同速度
  • 改变初始值/种子,观察不同起始条件

💡 Verilator会在位宽不匹配时给出Warning,这是学习的好机会

添加功能

在现有基础上增加新功能:

  • 添加新的控制信号和状态
  • 增加状态或修改状态转移条件
  • 扩展输出,增加更多信息

🔧 增量开发:每次只改一个地方,验证通过后再改下一个

📋 Verilog速查

语法说明示例
reg [7:0]8位寄存器reg [7:0] data;
wire组合逻辑连线wire valid = cnt > 5;
always @(posedge clk)时序逻辑上升沿触发
always @(*)组合逻辑敏感列表自动推导
localparam局部常量localparam DIV = 50000000;
case多分支选择注意default分支
$clog2计算位宽$clog2(16)=4

📊 性能指标

资源估算(FPGA参考)

本设计在典型FPGA上的资源占用估算:LUT约20-150个,FF约30-120个,无BRAM/DSP依赖(特殊模块除外)。时钟频率可达50-100MHz+。Verilator仿真速度约5-10M周期/秒。

← 上一课 📚 目录 下一课 →