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. Sizing a FIFO, End to End

Part 6 · Reset & CDC · Intermediate

Sizing a FIFO, End to End

Size an async FIFO from the worst backlog plus the crossing latency, with a worked burst checked in simulation.

On this page

    Why this matters

    The textbook answer for an 80-word burst into a half-speed reader is 30 words, so 32 entries. In simulation that FIFO loses 3 words, because the pointers take cycles to cross and the formula ignores them.

    After this lesson you can: size an async FIFO from burst length, both clock rates, idle cycles and the pointer crossing latency, round it for Gray pointers, and say when no depth is enough.

    Depth is the worst backlog plus the latency you cannot see

    A writer sends 80 words in a burst and a slower reader drains them. The textbook answer is a backlog of 30 words, so a 32-entry FIFO. In simulation that FIFO loses 3 words, and the 64-entry one peaks at 34 stored words.

    The rule: depth = the most words waiting at the worst moment, plus the words neither side can see yet, because each pointer takes cycles to cross. This lesson works the classic problem with real numbers and checks the answer in simulation.

    The problem. The writer runs at 100 MHz and sends a burst of 80 words, at most 4 words in every 5 cycles. The reader runs at 50 MHz and is ready every cycle. The FIFO starts empty. The writer cannot wait: a word offered while wfull is high is lost.

    Stored words during the 80-word burst in a 64-entry FIFO: the simulation rises to 34 at 985 ns, above the rate model's 30 and the 32-entry line, then drains by 1650 ns

    Read the orange curve as the number of words stored at each moment; each step up is a write and each step down is a read.

    • Until the first read at 70 ns the curve only climbs: the reader has not seen the first word yet.

    • From then on it climbs by about 3 words every 100 ns: 8 writes against 5 reads.

    • The peak, 34 stored words, comes with the last write at 985 ns. It is 4 words above the rate model and 2 above the 32-entry line.

    • After the burst the reader drains one word per 20 ns, and the FIFO is empty at 1650 ns.

    Step 1: the backlog from the rates

    The write rate is r_w = 100 MHz × 4/5 = 80 million words per second, so the burst lasts 80 / r_w = 1000 ns. The read rate is r_r = 50 million words per second, so the reader takes 50 words in that time. The backlog at the end of the burst is 80 − 50 = 30.

    Qrate​=B(1−rw​rr​​)=80(1−8050​)=30

    This is the steady-rate approximation from Why an async FIFO. It assumes the reader starts the moment the first word arrives and that the writer sees every read at once. Neither is true.

    Step 2: the latency the formula misses

    Follow the first word through the complete design. The write updates the Gray write pointer. Two read-clock edges carry it through the synchroniser, a third registers rempty low, and the read happens on the fourth. So the reader starts up to 4 read cycles (80 ns) late; in the run it starts 65 ns after the first write. The rate model counted reads in that time that never happened: 50 M × 80 ns = 4 words.

    The write side has the same delay in reverse. A read moves the Gray read pointer; two write-clock edges carry it across, a third updates wfull, and the writer can use the slot on the fourth. For up to 4 write cycles (40 ns) the writer counts a freed slot as full: 50 M × 40 ns = 2 more words.

    Term

    Formula

    Worked value

    Backlog from the rates

    B × (1 − r_r / r_w)

    30

    Reads lost while the reader starts

    min(r_r, r_w) × 4 T_r

    50 M × 80 ns = 4

    Reads the writer has not seen

    min(r_r, r_w) × 4 T_w

    50 M × 40 ns = 2

    Depth the full flag needs

    sum, rounded up

    36

    Depth to build (Gray pointers)

    next power of two

    64

    The factor 4 comes from this RTL: two synchroniser flops, one registered flag and the edge that acts on it. A three-flop synchroniser makes it 5; a registered read output adds one more read cycle. Count your own path the same way.

    The flags never let the FIFO overflow: wfull is early, not late, as Full, empty and the complete design shows. The cost of that safety is depth. With 32 entries the write side counted 32 while only 31 words were stored, and refused 3 words.

    Step 3: round up, then check in simulation

    Gray pointers need a power-of-two depth, so 36 becomes 64 and 28 entries are spare. If that is too much memory, change the problem: shorten the burst, speed up the reader, or let the writer stall on wfull. If the writer reacts to a warning through a pipeline, add the words it sends after the warning and set the almost-full margin from them, as in Almost full and almost empty.

    The bench below runs this exact burst on the topic's async FIFO at 10 ns and 20 ns clocks. The start-up delay depends on where the first write lands against the read clock, so it repeats the burst with the write clock shifted by 0 to 9 ns. It compares the queue of accepted words with every read.

    Depth

    Runs

    Peak stored

    Peak the write side counted

    Words lost

    64

    10 clock phases

    34

    36

    0

    32

    1

    31

    32

    3

    Diagram
    $ iverilog -g2012 -o size.vvp fifo_sizing_tb.sv && vvp -n size.vvp
    ...
    worked example, depth 64, 10 clock phases: peak 34 stored, write side saw at most 36, lost 0
    worked example, depth 32: peak 31 stored, write side saw 32, lost 3 words
    two bursts back to back, depth 64: lost 21 words
    PASS: depth 64 holds the burst at every phase; depth 32 loses words; each scenario fits its computed depth

    The write side's peak of 36 equals the computed depth. The formula is a bound, so other phases and patterns land at or below it.

    Four scenarios

    The same three terms size every case. Each row was run in the bench at its computed depth with no word lost.

    Scenario

    Writer

    Reader

    Rate backlog

    Latency terms

    Depth

    Simulated peak (stored / write side)

    Equal clocks

    100 MHz, every cycle, 80 words

    100 MHz, every cycle

    0

    4 + 4

    8

    4 / 6

    Faster reader

    100 MHz, every cycle, 80 words

    149 MHz (6.7 ns), every cycle

    0

    2.7 + 4 (capped at the write rate)

    8

    3 / 5

    Slower reader with idle cycles

    100 MHz, 4 in 5, 80 words

    62.5 MHz, 3 in 4 (46.9 M/s)

    33.1

    3.0 + 1.9

    64 (38 needed)

    36 / 37

    Back-to-back bursts

    100 MHz, 160 words in 160 cycles

    50 MHz, every cycle

    80

    4 + 2

    128 (86 needed)

    83 / 84

    The last row is the classic trap. "80 words in any 100 cycles" allows one burst to end with 80 writes and the next to start with 80 more, so the worst window holds 160 writes back to back. Sized for one burst, 64 entries lose 21 words.

    Common mistakes

    Common pitfall

    Sizing from average rates. A writer that averages 40 M words/s but bursts at 80 M looks safe beside a 50 M reader, yet the burst decides the depth. And if the burst has no upper bound and the writer is faster during it, the backlog B × (1 − r_r / r_w) grows with B: no depth is enough. Then the writer must stall on wfull, or the specification must bound the burst.

    Check yourself A 200 MHz writer sends 120 words, 3 in every 4 cycles. A 100 MHz reader is ready every cycle. The FIFO starts empty, uses this topic's design (4-cycle latency each way) and the writer cannot stall. What depth do you build?

    Write rate 200 × 3/4 = 150 M words/s, so the burst lasts 120 / 150 M = 800 ns. Rate backlog: 120 × (1 − 100/150) = 40. Reader start-up: 4 read cycles = 40 ns, times 100 M = 4 words. Reads the writer has not seen: 4 write cycles = 20 ns, times 100 M = 2 words. Total 46, so build 64. If the burst grew to 180 words, the backlog would be 60 and the total 66, so 128.

    Check yourself Depth 64 passes in one simulation. Why does the bench repeat the burst at ten clock phases?

    The reader's start-up delay is 3 to 4 read cycles depending on where the first write lands against the read clock. One phase may not hit the worst case; sweeping the offset does, and the bound must hold at every phase.

    Testbench

    The topic's async FIFO with the plain synchroniser, a burst writer that cannot wait, a reader with a duty cycle and a scoreboard queue. Run it with +trace to list each instance's occupancy after every write and read; the lines from g_phase[0].env are the data behind the plot.

    ptr_sync.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
    `timescale 1ns/1ps
    
    // The async FIFO from "Full, Empty and the Complete Design", with the plain two-flop
    // pointer synchroniser only (the simulation skew model and the almost flags are removed).
    module ptr_sync #(parameter int W = 5) (
      input  logic         clk, rst_n,
      input  logic [W-1:0] d,
      output logic [W-1:0] q
    );
      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
    endmodule
    
    module async_fifo #(parameter int DW = 8, parameter int AW = 4) (
      input  logic          wclk, wrst_n, winc,
      input  logic [DW-1:0] wdata,
      output logic          wfull,
      input  logic          rclk, rrst_n, rinc,
      output logic [DW-1:0] rdata,
      output logic          rempty
    );
      localparam int DEPTH = 1 << AW;
      localparam logic [AW:0] TOP2 = 3 << (AW - 1);
      logic [DW-1:0] mem [DEPTH];
      logic [AW:0] wbin, wbin_next, wgray, wgray_next, rptr_sync_w;
      logic [AW:0] rbin, rbin_next, rgray, rgray_next, wptr_sync_r;
    
      assign wbin_next  = wbin + (winc & ~wfull);
      assign wgray_next = (wbin_next >> 1) ^ wbin_next;
      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 <= (wgray_next == (rptr_sync_w ^ TOP2)); end
      always_ff @(posedge wclk)
        if (winc && !wfull) mem[wbin[AW-1:0]] <= wdata;
    
      assign rbin_next  = rbin + (rinc & ~rempty);
      assign rgray_next = (rbin_next >> 1) ^ rbin_next;
      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 <= (rgray_next == wptr_sync_r); end
      assign rdata = mem[rbin[AW-1:0]];
    
      ptr_sync #(AW + 1) u_r2w (.clk(wclk), .rst_n(wrst_n), .d(rgray), .q(rptr_sync_w));
      ptr_sync #(AW + 1) u_w2r (.clk(rclk), .rst_n(rrst_n), .d(wgray), .q(wptr_sync_r));
    
      // Occupancy as the write side sees it (for the measurement only).
      function automatic logic [AW:0] to_bin(logic [AW:0] g);
        for (int i = AW - 1; i >= 0; i--) g[i] = g[i+1] ^ g[i];
        return g;
      endfunction
      wire [AW:0] fill_w = wbin - to_bin(rptr_sync_w);
    endmodule
    
    // One burst: a writer that cannot wait (a refused word is lost) and a reader with a duty cycle.
    // The writer offers W_ON words in every W_OF write cycles until B words; the reader is ready
    // on R_ON of every R_OF read cycles. START delays the write clock against the read clock.
    module burst_env #(
      parameter real WPER = 10.0, RPER = 20.0, START = 0.0,
      parameter int AW = 6, B = 80, W_ON = 4, W_OF = 5, R_ON = 1, R_OF = 1
    ) (output int peak, peak_w, lost, errors, output bit done);
      logic wclk = 0, rclk = 0, wrst_n = 0, rrst_n = 0, winc = 0, rinc = 0;
      logic [7:0] wdata = 0, rdata;
      logic wfull, rempty;
      async_fifo #(.DW(8), .AW(AW)) dut (.*);
      initial begin #(START); forever #(WPER / 2.0) wclk = ~wclk; end
      always #(RPER / 2.0) rclk = ~rclk;
    
      logic [7:0] q [$];
      int offered = 0, wc = 0, rc = 0, reads = 0;
      bit go = 0;
      realtime t0;
      initial begin peak = 0; peak_w = 0; lost = 0; errors = 0; done = 0; end
    
      initial begin
        fork
          begin repeat (8) @(negedge wclk); wrst_n = 1; end
          begin repeat (8) @(negedge rclk); rrst_n = 1; end
        join
        repeat (10) @(negedge wclk);
        go = 1; t0 = $realtime;                            // the burst starts here, FIFO empty
        wait (offered == B);
        repeat (40 * B) @(posedge rclk);                   // plenty of time to drain
        if (q.size() != 0 || reads + lost != B) errors++;
        done = 1;
      end
    
      // writer: decides its offer on the falling edge, so it changes between rising edges
      always @(negedge wclk) begin
        winc = go && (offered < B) && ((wc % W_OF) < W_ON);
        wdata = offered[7:0];
        if (winc) offered++;
        if (go) wc++;
      end
      always @(posedge wclk) if (winc) begin
        // the occupancy the full flag is computed from at this edge (before the registers update)
        if (int'(dut.fill_w) + !wfull > peak_w) peak_w = int'(dut.fill_w) + !wfull;
        if (wfull) lost++;                                 // the source cannot wait: this word is gone
        else begin q.push_back(wdata); if (q.size() > peak) peak = q.size(); end
        if ($test$plusargs("trace")) $display("%m occ %0.1f %0d", $realtime - t0, q.size());
      end
    
      // reader: ready on R_ON of every R_OF cycles, checks order and data against the queue
      always @(negedge rclk) begin rinc = rrst_n && ((rc % R_OF) < R_ON); rc++; end
      always @(posedge rclk) if (rinc && !rempty) begin
        if (q.size() == 0 || rdata !== q[0]) errors++;
        else begin void'(q.pop_front()); reads++; end
        if ($test$plusargs("trace")) $display("%m occ %0.1f %0d", $realtime - t0, q.size());
      end
    endmodule
    

    Remember

    • Depth = backlog from the rates + reads lost while the reader starts + reads the writer has not yet seen, then round up to a power of two.

    • Turn crossing latency into time, then into words at the slower rate: up to 4 cycles of each receiving clock for this design.

    • With an unbounded burst and a writer faster than the reader during it, no depth is enough: stall the writer on full or bound the burst.

    Rule of thumb: size for the worst moment, not the average rate, and count every cycle a pointer spends crossing.

    How this was checked

    Tool: Icarus Verilog 13.0, run on 2026-10-17. Command: iverilog -g2012 -o size.vvp fifo_sizing_tb.sv && vvp -n size.vvp (the Testbench block on the page; +trace adds the occupancy lines).

    Result: depth 64 at 10 write-clock phase offsets (0 to 9 ns): peak 34 stored, the write side counted at most 36, 0 words lost. Depth 32: peak 31 stored, write side 32, 3 words lost. Scenarios at their computed depths lost nothing: equal clocks 4/6 in 8, faster reader 3/5 in 8, slower reader with idles 36/37 in 64, 160 back-to-back words 83/84 in 128; the same 160 words lose 21 in 64. Final line PASS.

    The occupancy plot is the +trace output of g_phase[0].env (depth 64, phase 0): every accepted write and read, first read at 70 ns, peak 34 at 985 ns, empty at 1650 ns. The rate-model line is drawn from the formula, not simulated.

    Scope and limits: one RTL (two-flop synchronisers, registered flags, first-word-fall-through read), ideal clocks with fixed periods and no jitter, a writer that cannot wait. The latency factor of 4 cycles each way belongs to this RTL; a deeper synchroniser or a registered read changes it. The depth formula is a bound checked by these runs, not a proof for every pattern.

    Keep going

    • Practice: RTL design of an asynchronous FIFO with CDC handling

    • Practice: High-throughput CDC using an asynchronous FIFO

    • Code lab: Async FIFO with Gray pointers

    • Interview: run a mock interview on this topic

    Related topics

    • Why an async FIFO

    • Almost full / almost empty

    • Capstone: buffered streams

    Previous

    171 / 350

    Next: CDC Protocols

    Up next

    Up nextCDC Protocols

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Why an Async FIFO Is the Standard
    • Pointers and Gray Conversion
    • Full, Empty and the Complete Design