Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • RTL Foundations36
  • Combinational Design25
  • Sequential Design27
  • FSM & Control27
  • Timing & Clocking39
  • Reset & CDC34
    • Reset Architecture
    • Metastability and Synchronisation
    • Asynchronous FIFO
      • Why an Async FIFO Is the Standard
      • Pointers and Gray Conversion
      • Full, Empty and the Complete Design
      • Almost Full and Almost Empty
      • Reset and Initialisation
      • Verification and CDC Closure
      • Sizing a FIFO, End to End
    • CDC Protocols
    • Reset Domain Crossing
    • CDC Verification
  • Memory & Buses57
  • Advanced RTL28
  • Senior & Interview76
  • Glossary and Notation
Loading lesson

‹Asynchronous FIFO
  1. Topics
  2. RTL Design
  3. Reset & CDC
  4. Asynchronous FIFO
  5. Full, Empty and the Complete Design

Part 6 · Reset & CDC · Intermediate

Full, Empty and the Complete Design

The complete async FIFO with a five-experiment testbench, scoreboard and binary-pointer mutant.

On this page

    Why this matters

    Gray pointers, two-flop synchronisers and registered flags make an async FIFO safe. The same design with binary pointers still passes one of the five experiments, so a test that cannot fail the binary version proves nothing.

    After this lesson you can: read the complete async FIFO, explain why stale remote pointers make the flags safe but pessimistic, and use a binary-pointer mutant to show a test is sensitive.

    The design

    The listing below combines every earlier lesson: dual-port memory, binary and Gray pointers per side, one two-flop synchroniser per pointer, registered wfull and rempty, and the almost flags.

    AW is the address width and must be at least 1, so the depth 2**AW is at least 2; an elaboration-time check rejects AW = 0. DW can be any width.

    The ptr_sync module has two builds. The plain two-flop chain is for synthesis. SKEW = 1 is a functional old/new perturbation model with per-direction first-stage setup/hold exposure counts.

    meta_ff.sv
    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
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    `timescale 1ns/1ps
    
    // Functional old/new perturbation, not an analog resolution-time model.
    // events counts bit/window exposures (not unique transactions or physical failures).
    module meta_ff #(parameter real TCQ = 0.25, TSU = 0.3, THD = 0.15) (input logic clk, d, output logic q, output int events);
      realtime t_d = -100.0, t_clk = -100.0;
      initial begin q = 1'b0; events = 0; end
      always @(d) begin
        t_d = $realtime;
        if ($realtime - t_clk < THD) begin events++; q <= #TCQ $urandom_range(0, 1); end
      end
      always @(posedge clk) begin
        t_clk = $realtime;
        if ($realtime - t_d < TSU) begin events++; q <= #TCQ $urandom_range(0, 1); end
        else                       q <= #TCQ d;
      end
    endmodule
    
    // Pointer synchroniser: two flops in the destination domain.
    // SKEW = 1 is a SIMULATION model (one metastability-aware flop per bit); SKEW = 0 is the plain synthesisable circuit.
    module ptr_sync #(parameter int W = 5, parameter bit SKEW = 0) (
      input  logic         clk, rst_n,
      input  logic [W-1:0] d,
      output logic [W-1:0] q
    );
      int window_hits;
      if (SKEW) begin : g_meta
        logic [W-1:0] q1;
        int first_hits [W], second_hits [W];
        always_comb begin
          window_hits = 0;
          for (int i = 0; i < W; i++) window_hits += first_hits[i];
        end
        for (genvar i = 0; i < W; i++) begin : g_bit
          meta_ff f1 (.clk, .d(d[i]),  .q(q1[i]), .events(first_hits[i]));
          meta_ff f2 (.clk, .d(q1[i]), .q(q[i]), .events(second_hits[i]));
        end
      end else begin : g_plain
        assign window_hits = 0;
        logic [W-1:0] q1;
        always_ff @(posedge clk or negedge rst_n)
          if (!rst_n) begin q1 <= '0; q <= '0; end
          else begin q1 <= d; q <= q1; end
      end
    endmodule
    
    module async_fifo #(
      parameter int DW = 8,
      parameter int AW = 4,              // depth = 2**AW; legal range AW >= 1 (DEPTH >= 2)
      parameter bit GRAY = 1,            // 0 = (wrong) binary pointers across the crossing, for the experiment
      parameter bit SKEW = 0,            // simulation-only skew model in the pointer synchronisers
      parameter int MARGIN = 2           // legal range 0 <= MARGIN <= DEPTH
    ) (
      input  logic          wclk, wrst_n, winc,
      input  logic [DW-1:0] wdata,
      output logic          wfull, walmost_full,
      input  logic          rclk, rrst_n, rinc,
      output logic [DW-1:0] rdata,
      output logic          rempty, ralmost_empty
    );
      localparam int DEPTH = 1 << AW;
      if (AW < 1) begin : g_aw_check                              // a one-entry FIFO has no address bits, so the pointer slices below would be empty
        $error("async_fifo: AW must be at least 1 (DEPTH >= 2)");
      end
      if (MARGIN < 0 || MARGIN > DEPTH) begin : g_margin_check
        initial $fatal(1, "async_fifo: MARGIN must be between 0 and DEPTH inclusive");
      end
      logic [DW-1:0] mem [DEPTH];
    
      // ---------------- write domain ----------------
      logic [AW:0] wbin, wbin_next, wgray, wgray_next, wptr_out;
      logic [AW:0] rptr_sync_w;                                  // read pointer, synchronised into the write domain
      logic [AW:0] rbin_in_w;
      logic        wfull_next;
    
      assign wbin_next  = wbin + (winc & ~wfull);
      assign wgray_next = (wbin_next >> 1) ^ wbin_next;
      assign wptr_out   = GRAY ? wgray : wbin;
    
      // full: next write pointer has caught up with the (synchronised) read pointer, one lap ahead
      if (GRAY) begin : g_full_gray
        localparam logic [AW:0] TOP2 = 3 << (AW - 1);              // the two top bits set (valid down to AW = 1)
        assign wfull_next = (wgray_next == (rptr_sync_w ^ TOP2));  // Gray code of (rptr + DEPTH): the two top bits inverted
      end else begin : g_full_bin
        assign wfull_next = (wbin_next[AW] != rptr_sync_w[AW]) && (wbin_next[AW-1:0] == rptr_sync_w[AW-1:0]);
      end
    
      always_ff @(posedge wclk or negedge wrst_n)
        if (!wrst_n) begin wbin <= '0; wgray <= '0; wfull <= 1'b0; end
        else         begin wbin <= wbin_next; wgray <= wgray_next; wfull <= wfull_next; end
    
      always_ff @(posedge wclk)
        if (winc && !wfull) mem[wbin[AW-1:0]] <= wdata;
    
      // ---------------- read domain ----------------
      logic [AW:0] rbin, rbin_next, rgray, rgray_next, rptr_out;
      logic [AW:0] wptr_sync_r;                                  // write pointer, synchronised into the read domain
      logic        rempty_next;
    
      assign rbin_next  = rbin + (rinc & ~rempty);
      assign rgray_next = (rbin_next >> 1) ^ rbin_next;
      assign rptr_out   = GRAY ? rgray : rbin;
      assign rempty_next = GRAY ? (rgray_next == wptr_sync_r) : (rbin_next == wptr_sync_r);
    
      always_ff @(posedge rclk or negedge rrst_n)
        if (!rrst_n) begin rbin <= '0; rgray <= '0; rempty <= 1'b1; end
        else         begin rbin <= rbin_next; rgray <= rgray_next; rempty <= rempty_next; end
    
      assign rdata = mem[rbin[AW-1:0]];                          // first-word-fall-through read
    
      // ---------------- crossings ----------------
      ptr_sync #(AW + 1, SKEW) u_r2w (.clk(wclk), .rst_n(wrst_n), .d(rptr_out), .q(rptr_sync_w));
      ptr_sync #(AW + 1, SKEW) u_w2r (.clk(rclk), .rst_n(rrst_n), .d(wptr_out), .q(wptr_sync_r));
    
      // ---------------- almost flags (conservative: computed from synchronised, hence stale, pointers) ----------------
      function automatic logic [AW:0] to_bin(logic [AW:0] g);
        logic [AW:0] b;
        b[AW] = g[AW];
        for (int i = AW - 1; i >= 0; i--) b[i] = b[i+1] ^ g[i];
        return b;
      endfunction
      logic [AW:0] r_in_w_bin, w_in_r_bin, fill_w, fill_r;
      assign r_in_w_bin = GRAY ? to_bin(rptr_sync_w) : rptr_sync_w;
      assign w_in_r_bin = GRAY ? to_bin(wptr_sync_r) : wptr_sync_r;
      assign fill_w = wbin - r_in_w_bin;                         // occupancy as the write side sees it (>= true occupancy)
      assign fill_r = w_in_r_bin - rbin;                         // occupancy as the read side sees it (<= true occupancy)
      assign walmost_full   = (fill_w >= DEPTH - MARGIN);
      assign ralmost_empty  = (fill_r <= MARGIN);
    endmodule
    
    // One experiment: a write clock, a read clock, random traffic, and a scoreboard.
    module fifo_env #(parameter real WPER = 10.0, RPER = 7.3, parameter bit GRAY = 1, parameter int WPROB = 60, RPROB = 60,
                     parameter int MARGIN = 2, parameter bit STOP_ALMOST = 0) (
      output int errors, output int transfers, output int max_fill, output bit saw_full, output bit saw_empty, output bit done
    );
      localparam int DW = 8, AW = 4, DEPTH = 16;
      logic wclk = 0, rclk = 0, wrst_n = 0, rrst_n = 0, winc = 0, rinc = 0;
      logic [DW-1:0] wdata = 0, rdata;
      logic wfull, walmost_full, rempty, ralmost_empty;
      async_fifo #(.DW(DW), .AW(AW), .GRAY(GRAY), .SKEW(1), .MARGIN(MARGIN)) dut (.*);
    
      always #(WPER / 2.0) wclk = ~wclk;
      always #(RPER / 2.0) rclk = ~rclk;
    
      logic [DW-1:0] q [$];
      int next = 0;
      bit stopping = 0;
      int writes = 0, write_cycles = 0, warning_stalls = 0;
      int start_r2w, start_w2r;
      wire [1:0] status_known = {wfull, rempty};
      initial begin errors = 0; transfers = 0; max_fill = 0; saw_full = 0; saw_empty = 0; end
    
      // Both resets are held long enough for both clocks to run several cycles, so the pointer
      // synchronisers (which have no reset in the simulation model) are flushed of unknowns first.
      localparam real RELEASE = 6.0 * ((WPER > RPER) ? WPER : RPER);
      initial begin
        done = 0;
        fork
          begin #(RELEASE); @(negedge wclk); wrst_n = 1; end
          begin #(RELEASE); @(negedge rclk); rrst_n = 1; end
        join
        #1ps;
        start_r2w = dut.u_r2w.window_hits;
        start_w2r = dut.u_w2r.window_hits;
        #400000ns;
        stopping = 1;                 // next falling write edge removes all new offers
        @(negedge wclk); #1ps;
        begin : drain
          for (int n = 0; n < 256 && !done; n++) begin
            @(negedge rclk); #1ps;    // rising-edge accounting and NBAs have completed
            if (writes == transfers && q.size() == 0 && rempty === 1'b1) begin
              done = 1;
            end
          end
        end
        if (!done) $fatal(1, "FIFO drain watchdog: writes=%0d reads=%0d queued=%0d", writes, transfers, q.size());
        if ($isunknown(status_known)) errors++;
        if (writes != transfers || q.size() != 0) errors++;
        // Fixed 100% traffic locks to one clock phase and may never hit the window; only random traffic must sweep it.
        if ((WPROB < 100 || RPROB < 100) && (dut.u_r2w.window_hits - start_r2w < 10 || dut.u_w2r.window_hits - start_w2r < 10)) errors++;
        $display("%m MARGIN=%0d writes=%0d reads=%0d queued=%0d r2w_hits=%0d w2r_hits=%0d write_cycles=%0d warning_stalls=%0d accepted/cycle=%f",
          MARGIN, writes, transfers, q.size(), dut.u_r2w.window_hits - start_r2w,
          dut.u_w2r.window_hits - start_w2r, write_cycles, warning_stalls, real'(writes) / write_cycles);
      end
    
      // writer: pushes when allowed (never when full), data are a running counter
      always @(negedge wclk) begin
        winc = wrst_n && !stopping && (!STOP_ALMOST || !walmost_full) && ($urandom_range(0, 99) < WPROB);
        wdata = next[DW-1:0];
      end
      always @(posedge wclk) if (winc && !wfull) begin
        q.push_back(wdata);  next++; writes++;
        if (q.size() > DEPTH) errors++;                         // more items than the FIFO can hold: it overflowed
        if (q.size() > max_fill) max_fill = q.size();
        if (wfull) saw_full = 1;
      end
      always @(posedge wclk) if (wfull) saw_full = 1;
      always @(posedge wclk) if (wrst_n && !stopping) begin
        write_cycles++;
        if (STOP_ALMOST && walmost_full) warning_stalls++;
      end
    
      // almost flags must never be late: if the true occupancy is within MARGIN of full / empty, the flag must be set
      always @(negedge wclk) begin
        #1ps;
        if (wrst_n && q.size() >= DEPTH - MARGIN && !walmost_full) errors++;
      end
      always @(negedge rclk) begin
        #1ps;
        if (rrst_n && q.size() <= MARGIN && !ralmost_empty) errors++;
      end
    
      // reader: pops when not empty, checks the data and the order
      always @(negedge rclk) rinc = rrst_n && !done && (stopping || ($urandom_range(0, 99) < RPROB));
      always @(posedge rclk) begin
        if (rempty) saw_empty = 1;
        if (rinc && !rempty) begin
          if (q.size() == 0) errors++;                          // read data that was never written: underflow
          else begin
            logic [DW-1:0] exp;
            exp = q.pop_front();
            if (rdata !== exp) errors++;
            transfers++;
          end
        end
      end
    endmodule
    
    // Directed margin endpoints and midpoint, using real resettable pointer synchronisers.
    module margin_env #(parameter int MARGIN = 0) (output bit done, output int errors);
      logic clk = 0, rst_n = 0, winc = 0, rinc = 0;
      logic wfull, walmost_full, rempty, ralmost_empty;
      logic [7:0] rdata;
      always #5 clk = ~clk;
      async_fifo #(.DW(8), .AW(2), .MARGIN(MARGIN)) dut (
        .wclk(clk), .wrst_n(rst_n), .winc, .wdata(8'h5a), .wfull, .walmost_full,
        .rclk(clk), .rrst_n(rst_n), .rinc, .rdata, .rempty, .ralmost_empty
      );
    
      task automatic check_flags(input int used);
        repeat (6) @(negedge clk);                 // let both remote pointers settle before checking exact thresholds
        #1;
        if (walmost_full !== (used >= 4 - MARGIN)) errors++;
        if (ralmost_empty !== (used <= MARGIN)) errors++;
        if (wfull !== (used == 4) || rempty !== (used == 0)) errors++;
      endtask
    
      initial begin
        done = 0; errors = 0;
        repeat (6) @(negedge clk);
        rst_n = 1;
        check_flags(0);
        for (int used = 1; used <= 4; used++) begin
          @(negedge clk); winc = 1;
          @(posedge clk);
          @(negedge clk); winc = 0;
          check_flags(used);
        end
        for (int used = 3; used >= 0; used--) begin
          @(negedge clk); rinc = 1;
          @(posedge clk);
          @(negedge clk); rinc = 0;
          check_flags(used);
        end
        done = 1;
      end
    endmodule
    

    The directed margin checks are part of the regression. With DEPTH = 4 they sweep every occupancy for MARGIN = 0, 2 and 4 after pointer settling. MARGIN = 0 asserts only at full/empty; MARGIN = DEPTH keeps both almost flags asserted. Separate negative runs using margin_env #(.MARGIN(-1)) and margin_env #(.MARGIN(5)) must terminate with the parameter-contract fatal error; do not include those expected failures in the normal test top.

    The regression moves 127208 words, reaching a maximum fill of 16 of 16. Each environment stops offers, drains for at most 256 read cycles and requires accepted writes = checked reads, an empty queue and asserted empty. In the random-traffic environments each direction must record at least 10 first-stage bit/window exposures after reset. The always-ready and policy environments run fixed 100 % traffic at fixed clock ratios, lock to one phase and can record zero exposures in one direction, so the threshold does not apply to them. Keep seeds, tool versions, per-direction counts and drain summaries with any reported result.

    The same test on a binary-pointer FIFO

    Setting GRAY = 0 sends the binary pointers across the same synchronisers (and uses a binary comparison for full and empty). This binary variant fails with errors in four experiments (26, 49, 22 and 226) while the fifth (both sides always ready) stays clean.

    Warning

    That last point matters: the binary FIFO passes some experiments. A binary-pointer design that happens to pass one or two simulations is not correct; it fails when a multi-bit pointer change lands in the metastability window. Only a model that injects those events (like ptr_sync with SKEW = 1) shows it, and only a Gray-coded pointer is safe.

    Simulation: filling to full, draining to empty

    The waveform below is simulation output for a depth-4 FIFO using this design, written until full and then read until empty. How to read it: one slot is one cycle of wclk; rclk shows its own level at each slot.

    • rempty and rptr show x in the first slot because the read side is not yet reset; both settle once reset releases.

    • winc is high across slots 3 to 6 and wptr steps from 0 to 4; wfull rises with the fourth write in slot 6 and holds until slot 12, after the read pointer has crossed back.

    • rinc starts in slot 10 and rptr climbs to 4 across slots 11, 13, 15 and 17; rempty falls in slot 9, once the write pointer has crossed over, and rises again in slot 17 with the last read.

    • Each flag lags the far side: both are decided from the other side's synchronised pointer, so each changes a few slots after the pointer movement that causes it.

    Common mistakes

    Tip

    wfull and rempty are registered in this design and computed from the next pointer value. That keeps them glitch-free and means the flag is correct in the same cycle that the pointer changes.

    Common pitfall

    The read data is mem[rbin], a combinational read of the memory (first-word fall-through). If your target memory needs a registered read, add an output register and account for the extra cycle in rempty handling.

    Check yourself Why is the almost flag guaranteed never to be late, even though the pointers are synchronised and therefore stale?

    A stale read pointer (seen by the writer) is never ahead of the true read pointer, so the write side overestimates occupancy; a stale write pointer (seen by the reader) underestimates it. Both errors push the flags in the safe direction: almost-full early, almost-empty early.

    Testbench

    Five clock-ratio experiments with skewed pointer crossings, a scoreboard and occupancy checks.

    Experiment

    Write clock

    Read clock

    Traffic

    Purpose

    a

    10.0 ns

    7.3 ns

    writer 70 %, reader 40 %

    Writer faster: FIFO fills up

    b

    7.3 ns

    10.0 ns

    writer 40 %, reader 70 %

    Reader faster: FIFO runs dry

    c

    10.0 ns

    10.07 ns

    60 % / 60 %

    Nearly equal clocks, slowly drifting phase

    d

    3.1 ns

    17.9 ns

    90 % / 90 %

    6 : 1 clock ratio

    e

    10.0 ns

    7.3 ns

    100 % / 100 %

    Both sides always ready

    • A scoreboard queue holds every word written; every read is compared with the head of the queue (order and data).

    • A writer that obeys wfull and a reader that obeys rempty run with random enables.

    • The queue size is checked against the FIFO depth (overflow) and every read must have a matching write (underflow).

    • The almost flags are checked for safety: whenever the true occupancy is within 2 entries of full or empty, the flag must already be set.

    • The experiments must really reach full and empty.

    Result

    Value

    Words transferred across the five experiments

    127,208

    Overflow, underflow or data errors

    0

    Maximum occupancy reached

    16 of 16 (the FIFO was truly full)

    Remember

    • wfull and rempty are registered and computed from the next pointer value, so they are glitch-free and correct in the cycle the pointer changes.

    • A stale remote pointer may delay progress. It cannot cause an overwrite or a read of an unwritten word.

    • The read data is mem[rbin], a first-word fall-through read. A memory with a registered read needs an output register and one extra cycle.

    Rule of thumb: keep the binary-pointer mutant in the bench: the failure it produces is the proof that the test works.

    How this was checked

    Directed margin checks, run with Icarus Verilog 13.0 (2026-10-16): errors=0 for MARGIN = 0, 2 and 4.

    Regression, run with Icarus Verilog 13.0 (2026-10-16): prints PASS with 127208 words moved and max fill 16 of 16. In the random-traffic environments each direction must record at least 10 first-stage bit/window exposures after reset; these are finite model-coverage thresholds, not a reliability target. The fixed-TCQ old/new model omits analog late resolution and correlated physical effects.

    ptr_sync with SKEW = 1 is a functional old/new perturbation model. Its fixed TCQ does not model analog late resolution, characterise MTBF or prove physical safety.

    Binary-pointer variant (GRAY = 0), run with Icarus Verilog 13.0 (2026-10-16): errors in four experiments (26, 49, 22 and 226); the fifth (both sides always ready) stays clean. Exact counts depend on the simulator and seed; a mutant may also fail its drain watchdog.

    Keep going

    • Practice: Async FIFO pointer mismatch caused by a CDC bug

    • Practice: FWFT CDC FIFO with almost flags and pointer-distance occupancy

    • Code lab: Async FIFO with Gray pointers

    • Interview: run a mock interview on this topic

    Related topics

    • Almost full / almost empty

    Previous

    167 / 350

    Next: Almost Full and Almost Empty

    Up next

    Up nextAlmost Full and Almost Empty

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Why an Async FIFO Is the Standard
    • Reset and Initialisation
    • Verification and CDC Closure
    wclkrclkwincrincwfullremptywptr01234rptrx01234
    Simulation output: a depth-4 FIFO written until wfull rises and read until rempty rises again