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.
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.
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.
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.
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.
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 |
$ 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 depthThe 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.
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.
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.
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.
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.
`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
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.