RTL Design Patterns · Chapter 11 · Clock Domain Crossing
The Asynchronous FIFO
An asynchronous FIFO whose write and read ports run on unrelated clocks is the standard bridge between two clock domains, and it is the canonical clock-domain-crossing capstone. The body is a dual-port RAM with a write pointer and a read pointer, but to compute full the write side needs the read pointer and to compute empty the read side needs the write pointer, so each pointer must cross clock domains. The design is correct by construction because it composes tools you already have: keep each pointer as a Gray counter so it crosses at most one step behind, two-flop-synchronize each pointer into the other domain, and read the extra-MSB empty and full logic off those Gray pointers. Both flags come out conservative, so the FIFO never overflows or underflows. Built and scoreboard-verified in SystemVerilog, Verilog, and VHDL.
Advanced20 min readRTL Design PatternsAsynchronous FIFOClock Domain CrossingGray PointerTwo-Flop SynchronizerConservative Flags
Chapter 11 · Section 11.6 · Clock Domain Crossing
1. The Engineering Problem
You have two clocks that share no relationship — a clk_wr where a producer generates words, and a clk_rd where a consumer takes them — and you must move a stream of words from one to the other without losing or duplicating a single one. The producer bursts, the consumer stalls, their rates differ and drift; you need a buffer between them that a fast writer can fill ahead of a slow reader and a slow writer can leave for a fast reader to drain. That buffer is a FIFO, and 7.1 and 7.2 already built its body: a dual-port RAM, a write pointer wptr advancing on each push, a read pointer rptr advancing on each pop, both one bit wider than the address so the extra MSB counts laps and full/empty are unambiguous. In a single-clock FIFO you were done.
Here it is not done, and the reason is the entire page. In 7.2 both pointers lived in one clock domain, so the write side could read the read pointer and the read side could read the write pointer for free — every comparison was between two registers on the same clock. Split the ports across two unrelated clocks and that free access is gone. To decide full, the write side must compare wptr against rptr — but rptr advances on clk_rd, in the other domain. To decide empty, the read side must compare rptr against wptr — but wptr advances on clk_wr, again the other domain. Each flag needs the other domain's pointer, so each pointer has to cross an asynchronous clock-domain boundary.
And 11.3 already proved that crossing a multi-bit binary counter bit-by-bit is a trap: when a step flips several bits at once (0111 -> 1000 flips four), the per-bit synchronizers resolve old-or-new independently and the destination assembles a phantom the source never held. Compare a flag against a phantom pointer and the FIFO either thinks it has room it does not — and overflows, overwriting an unread word — or thinks it has data it does not — and underflows, reading a slot that was never written. Both are silent, both are clock-ratio-dependent, both pass a light test and corrupt data in the field. So the requirement is sharp: cross each pointer safely, and derive full and empty from the crossed pointers such that they are never optimistic. This page assembles exactly that — from parts you have already built.
// clk_wr and clk_rd are UNRELATED. wptr lives in clk_wr, rptr lives in clk_rd.
// FULL (write side) needs rptr: full = f(wptr, rptr) // rptr is in clk_rd
// EMPTY (read side) needs wptr: empty = f(rptr, wptr) // wptr is in clk_wr
// WRONG — per-bit 2FF a BINARY pointer across the boundary (11.3's phantom):
// always @(posedge clk_rd) wptr_sync <= wptr; // 0111->1000 => capture 1111/0000/...
// assign empty = (rptr == wptr_sync); // compared against a NEVER-HELD value
// => reads a slot never written (UNDERFLOW), or refuses space it has (throttles).
// The need: cross each pointer as GRAY (one bit per step => at most one behind, never
// ahead), 2FF-synchronize each into the other domain, and compute conservative flags.
// => no overflow, no underflow, EVER. That composition is this page.2. Mental Model
3. Pattern Anatomy
The block is the 7.2 FIFO cut in half down the clock boundary, with a Gray conversion on each pointer and a two-flop synchronizer carrying each pointer to the far side. Everything hard is in which pointer crosses which way and which flag is computed in which domain.
The pointers — (ADDR+1)-bit, binary for addressing, Gray for crossing. For a FIFO of DEPTH = 2**ADDR entries, both pointers are ADDR+1 bits, exactly as in 7.2: the low ADDR bits are the RAM address, the extra MSB is the wrap (lap) bit. But now each pointer exists in two forms. The binary form (wbin, rbin) is what you increment and what indexes the RAM locally — mem[wbin[ADDR-1:0]], mem[rbin[ADDR-1:0]]. The Gray form (wptr, rptr, where wptr = wbin ^ (wbin >> 1)) is the only form that crosses the boundary, because Gray adjacency is what makes the crossing coherent (11.5). You increment binary, convert to Gray, cross the Gray, and — this is the load-bearing rule — you compare Gray-to-Gray on the far side; you never convert the synchronized pointer back to binary just to compare it, because the comparison the flags need works directly on Gray.
The two synchronizers, one each way. The write Gray pointer wptr is fed through a two-flop synchronizer clocked by clk_rd to produce wq2_rptr — the write pointer as seen in the read domain. The read Gray pointer rptr is fed through a two-flop synchronizer clocked by clk_wr to produce rq2_wptr — the read pointer as seen in the write domain. Each is 11.2's structure: two back-to-back flops in the destination domain, no logic between them, carrying the Gray vector bit-for-bit. Note the asymmetry that trips people up: the write pointer travels to the read side; the read pointer travels to the write side. You do not synchronize a pointer into its own domain.
EMPTY — computed in the read domain, Gray-equal. The read side asserts rempty when its own read Gray pointer equals the synchronized write Gray pointer: rempty = (rptr == wq2_rptr). This is the direct Gray analogue of 7.2's empty = (wptr == rptr) — all bits equal means same seat, same lap, reader caught writer. It is exact in Gray because Gray equality is ordinary vector equality: two Gray values are equal iff the binary values they encode are equal. At reset both are zero, so the FIFO powers up empty.
FULL — computed in the write domain, Gray-equal with the top two bits inverted. The write side asserts wfull when its write Gray pointer equals the synchronized read Gray pointer with the top two bits of the read pointer inverted: wfull = (wptr == {~rq2_wptr[ADDR:ADDR-1], rq2_wptr[ADDR-2:0]}). This is the Gray equivalent of 7.2's binary full condition ('addresses equal but wrap bits differ'), and the two-MSB inversion is why it works. In binary, full is 'low ADDR bits equal, MSB (wrap bit) different.' When you Gray-encode a pointer, gray = bin ^ (bin >> 1), so the top Gray bit equals the top binary bit (the wrap bit), and the second Gray bit equals the XOR of the top two binary bits. For the addresses to match while the wrap bit differs, you must flip both of the top two Gray bits: flipping only the top bit changes the wrap parity but also corrupts the second Gray bit's meaning, so the addresses would no longer compare equal. Inverting the top two Gray bits of the synchronized read pointer reconstructs exactly the pattern 'same address, opposite lap' in Gray space. Invert only one bit (§6) and the flag fires a half-lap early or late.
The asynchronous FIFO — two domains, one dual-port RAM, each pointer crossing the OTHER way
data flowThe Gray full condition is worth making concrete, because the whole 'two-MSB inversion' claim rests on it. Take DEPTH = 4, so ADDR = 2 and pointers are 3 bits. In binary, full is 'wrap bits differ, low 2 bits equal' — e.g. wbin = 100, rbin = 000. Their Gray codes are wgray = 110 and rgray = 000. The write side has wgray = 110; the synchronized read pointer is rq2_wptr = 000, and inverting its top two bits gives ~00 & 0 -> 110... precisely wgray. So wfull asserts exactly when the writer has lapped the reader once — not before. Walk one step earlier: wbin = 011, rbin = 000, wgray = 010, and the inverted-read comparison value is still 110 != 010, so wfull stays low on the third write. The condition trips on exactly the DEPTH-th outstanding word, in Gray space, same as 7.2 did in binary. The second visual makes the Gray full/empty comparison itself the object of study.
Gray EMPTY vs Gray FULL — same seat resolved by the lap, now in Gray space
data flowWhere the flags live, and why. wfull is a register in the write domain (a function of the local wptr and the synchronized rq2_wptr); rempty is a register in the read domain (a function of the local rptr and the synchronized wq2_rptr). Each flag is computed where it is used — the write side gates its writes with its own wfull, the read side gates its reads with its own rempty — and each reads the far pointer only through the synchronizer. That placement is what keeps every path timing-clean: no signal is ever compared against a raw pointer from the other domain, and no flag crosses a boundary. Because wfull is a registered function of pointers that changed on a previous edge, gating the write with it is safe; likewise for rempty.
4. Real RTL Implementation
This is the core of the page. We build the FIFO incrementally — (i) the two Gray pointers with the binary increment; (ii) the two two-flop pointer synchronizers, one each way; (iii) the rempty (read domain) and wfull (write domain) flag logic; (iv) the dual-port RAM — then the integrated async_fifo module, each shown in SystemVerilog, Verilog, and VHDL, with a two-async-clock scoreboard testbench. Finally the binary-pointer bug beside the Gray-pointer fix. The reasoning is identical in all three; only the syntax differs.
One discipline runs through every block, inherited from 11.2 and 11.5: pointers increment in binary with non-blocking assignments and are Gray-encoded for crossing; the Gray value is the only thing that crosses; each synchronizer is a plain two-flop shift in its destination domain with no logic between the flops; resets are per-domain and asynchronous, active-low (wrst_n, rrst_n), so each side powers up at zero. The RAM is written in clk_wr and read in clk_rd, addressed by the binary low bits — never by a synchronized pointer.
4a. The two Gray pointers + increment; the two synchronizers; the flags
We build the write-domain half and read-domain half of the integrated module together, because the pointer, its Gray form, its synchronizer, and its flag are one unit per side. Read this as the anatomy of §3 turned into RTL: increment binary, Gray-encode, cross the other pointer in, compare Gray-to-Gray (with the two-MSB inversion on the full side).
module async_fifo #(
parameter int WIDTH = 8, // datapath width
parameter int ADDR = 4, // address bits; DEPTH = 2**ADDR
localparam int DEPTH = 1 << ADDR
)(
// ---- write domain ----
input logic clk_wr,
input logic wrst_n, // async, active-low
input logic winc, // request a push
input logic [WIDTH-1:0] wdata,
output logic wfull, // computed IN the write domain
// ---- read domain ----
input logic clk_rd,
input logic rrst_n, // async, active-low
input logic rinc, // request a pop
output logic [WIDTH-1:0] rdata,
output logic rempty // computed IN the read domain
);
// Dual-port RAM: written in clk_wr, read in clk_rd, addressed by the BINARY low bits.
logic [WIDTH-1:0] mem [DEPTH];
// ---- (i) WRITE pointer: increment BINARY, convert to GRAY for crossing ----
logic [ADDR:0] wbin, wptr; // ADDR+1 bits: [ADDR-1:0]=addr, [ADDR]=wrap
logic [ADDR:0] wbin_next, wgray_next;
assign wbin_next = wbin + (winc & ~wfull); // advance only on a committed write
assign wgray_next = wbin_next ^ (wbin_next >> 1);
always_ff @(posedge clk_wr or negedge wrst_n)
if (!wrst_n) begin wbin <= '0; wptr <= '0; end
else begin wbin <= wbin_next; wptr <= wgray_next; end // wptr is the GRAY form
// ---- (i) READ pointer: increment BINARY, convert to GRAY for crossing ----
logic [ADDR:0] rbin, rptr;
logic [ADDR:0] rbin_next, rgray_next;
assign rbin_next = rbin + (rinc & ~rempty); // advance only on a committed read
assign rgray_next = rbin_next ^ (rbin_next >> 1);
always_ff @(posedge clk_rd or negedge rrst_n)
if (!rrst_n) begin rbin <= '0; rptr <= '0; end
else begin rbin <= rbin_next; rptr <= rgray_next; end
// ---- (ii) each-way two-flop synchronizers: cross the GRAY pointers ----
// WRITE gray pointer INTO the read domain -> wq2_rptr (read side uses it for EMPTY)
(* ASYNC_REG = "TRUE" *) logic [ADDR:0] wq1_rptr, wq2_rptr;
always_ff @(posedge clk_rd or negedge rrst_n)
if (!rrst_n) begin wq1_rptr <= '0; wq2_rptr <= '0; end
else begin wq1_rptr <= wptr; wq2_rptr <= wq1_rptr; end // wire only between flops
// READ gray pointer INTO the write domain -> rq2_wptr (write side uses it for FULL)
(* ASYNC_REG = "TRUE" *) logic [ADDR:0] rq1_wptr, rq2_wptr;
always_ff @(posedge clk_wr or negedge wrst_n)
if (!wrst_n) begin rq1_wptr <= '0; rq2_wptr <= '0; end
else begin rq1_wptr <= rptr; rq2_wptr <= rq1_wptr; end
// ---- (iii) FLAGS ----
// EMPTY (read domain): read gray pointer == synchronized write gray pointer (plain ==).
always_ff @(posedge clk_rd or negedge rrst_n)
if (!rrst_n) rempty <= 1'b1; // powers up empty
else rempty <= (rgray_next == wq2_rptr);
// FULL (write domain): write gray == synchronized read gray with the TOP TWO bits inverted.
always_ff @(posedge clk_wr or negedge wrst_n)
if (!wrst_n) wfull <= 1'b0;
else wfull <= (wgray_next ==
{~rq2_wptr[ADDR:ADDR-1], rq2_wptr[ADDR-2:0]});
// ---- (iv) dual-port RAM: write in clk_wr, read in clk_rd, addressed by BINARY low bits ----
always_ff @(posedge clk_wr)
if (winc & ~wfull) mem[wbin[ADDR-1:0]] <= wdata;
assign rdata = mem[rbin[ADDR-1:0]]; // FWFT-style combinational read
endmoduleThe self-checking testbench is the flagship's real test: two unrelated clocks, an independent writer that pushes whenever not full and an independent reader that pops whenever not empty, a scoreboard asserting every word read equals the word written in FIFO order (none lost, none duplicated), and standing assertions that the FIFO never overflows (a write while full) and never underflows (a read while empty). It is run with the write clock faster than the read clock and, by swapping the periods, the read clock faster — the two directions the safety argument must survive.
module async_fifo_tb;
localparam int WIDTH = 8;
localparam int ADDR = 3; // DEPTH = 8, small so bursts hit the boundary
localparam int DEPTH = 1 << ADDR;
logic clk_wr = 1'b0, clk_rd = 1'b0, wrst_n, rrst_n;
logic winc, rinc, wfull, rempty;
logic [WIDTH-1:0] wdata, rdata;
int errors = 0, npush = 0, npop = 0;
async_fifo #(.WIDTH(WIDTH), .ADDR(ADDR)) dut (
.clk_wr(clk_wr), .wrst_n(wrst_n), .winc(winc), .wdata(wdata), .wfull(wfull),
.clk_rd(clk_rd), .rrst_n(rrst_n), .rinc(rinc), .rdata(rdata), .rempty(rempty));
// FAST WRITER, SLOW READER (swap the two numbers to invert the ratio).
always #5 clk_wr = ~clk_wr; // ~100 MHz
always #13 clk_rd = ~clk_rd; // ~38 MHz, async
// Scoreboard: a golden reference queue of every word committed to the FIFO.
logic [WIDTH-1:0] sb [$];
// WRITER: drive a running pattern whenever not full; record it on a committed write.
logic [WIDTH-1:0] wcount = 8'h00;
always @(posedge clk_wr) begin
if (!wrst_n) begin winc <= 1'b0; end
else begin
// OVERFLOW guard: winc must never be honoured while full.
if (winc && wfull) begin $error("OVERFLOW: write attempted while full"); errors++; end
winc <= ~wfull; // push whenever there is room
wdata <= wcount;
if (winc && !wfull) begin sb.push_back(wdata); wcount <= wcount + 1; npush++; end
end
end
// READER: pop whenever not empty; check the popped word against the scoreboard head.
always @(posedge clk_rd) begin
if (!rrst_n) begin rinc <= 1'b0; end
else begin
if (rinc && rempty) begin $error("UNDERFLOW: read attempted while empty"); errors++; end
if (rinc && !rempty) begin
automatic logic [WIDTH-1:0] exp = sb.pop_front();
if (rdata !== exp) begin
$error("ORDER/DATA: popped %h expected %h", rdata, exp); errors++;
end
npop++;
end
rinc <= ~rempty; // pop whenever there is data
end
end
initial begin
wrst_n = 1'b0; rrst_n = 1'b0; winc = 1'b0; rinc = 1'b0; wdata = '0;
repeat (3) @(posedge clk_wr); wrst_n = 1'b1;
repeat (3) @(posedge clk_rd); rrst_n = 1'b1;
// Run long enough that the FIFO repeatedly fills and drains across the boundary.
repeat (4000) @(posedge clk_wr);
winc = 1'b0;
// Drain the tail so pushes == pops at the end.
repeat (200) @(posedge clk_rd);
if (errors == 0 && sb.size() == 0)
$display("PASS: %0d pushed, %0d popped, in-order/exactly-once, no overflow/underflow", npush, npop);
else
$display("FAIL: %0d errors, %0d words still in scoreboard", errors, sb.size());
$finish;
end
endmoduleThe Verilog form is the same integrated FIFO with reg/wire typing; ADDR is a parameter, the Gray encode is bin ^ (bin >> 1), and each synchronizer is a two-register shift on its destination clock. The scoreboard uses an array plus head/tail indices in place of SystemVerilog's queue.
module async_fifo #(
parameter WIDTH = 8,
parameter ADDR = 4, // DEPTH = 2**ADDR
parameter DEPTH = 16
)(
input wire clk_wr,
input wire wrst_n,
input wire winc,
input wire [WIDTH-1:0] wdata,
output reg wfull,
input wire clk_rd,
input wire rrst_n,
input wire rinc,
output wire [WIDTH-1:0] rdata,
output reg rempty
);
reg [WIDTH-1:0] mem [0:DEPTH-1];
// WRITE pointer: binary increment + gray for crossing
reg [ADDR:0] wbin, wptr;
wire [ADDR:0] wbin_next = wbin + (winc & ~wfull);
wire [ADDR:0] wgray_next = wbin_next ^ (wbin_next >> 1);
always @(posedge clk_wr or negedge wrst_n)
if (!wrst_n) begin wbin <= 0; wptr <= 0; end
else begin wbin <= wbin_next; wptr <= wgray_next; end
// READ pointer: binary increment + gray for crossing
reg [ADDR:0] rbin, rptr;
wire [ADDR:0] rbin_next = rbin + (rinc & ~rempty);
wire [ADDR:0] rgray_next = rbin_next ^ (rbin_next >> 1);
always @(posedge clk_rd or negedge rrst_n)
if (!rrst_n) begin rbin <= 0; rptr <= 0; end
else begin rbin <= rbin_next; rptr <= rgray_next; end
// each-way two-flop synchronizers on the GRAY pointers
(* ASYNC_REG = "TRUE" *) reg [ADDR:0] wq1_rptr, wq2_rptr; // wptr into clk_rd
always @(posedge clk_rd or negedge rrst_n)
if (!rrst_n) begin wq1_rptr <= 0; wq2_rptr <= 0; end
else begin wq1_rptr <= wptr; wq2_rptr <= wq1_rptr; end
(* ASYNC_REG = "TRUE" *) reg [ADDR:0] rq1_wptr, rq2_wptr; // rptr into clk_wr
always @(posedge clk_wr or negedge wrst_n)
if (!wrst_n) begin rq1_wptr <= 0; rq2_wptr <= 0; end
else begin rq1_wptr <= rptr; rq2_wptr <= rq1_wptr; end
// EMPTY (read domain): plain gray equality
always @(posedge clk_rd or negedge rrst_n)
if (!rrst_n) rempty <= 1'b1;
else rempty <= (rgray_next == wq2_rptr);
// FULL (write domain): gray equality with TOP TWO synchronized-read bits inverted
always @(posedge clk_wr or negedge wrst_n)
if (!wrst_n) wfull <= 1'b0;
else wfull <= (wgray_next ==
{~rq2_wptr[ADDR:ADDR-1], rq2_wptr[ADDR-2:0]});
// dual-port RAM: BINARY low bits address it
always @(posedge clk_wr)
if (winc & ~wfull) mem[wbin[ADDR-1:0]] <= wdata;
assign rdata = mem[rbin[ADDR-1:0]];
endmodule module async_fifo_tb;
parameter WIDTH = 8, ADDR = 3, DEPTH = 8;
reg clk_wr, clk_rd, wrst_n, rrst_n, winc, rinc;
reg [WIDTH-1:0] wdata;
wire [WIDTH-1:0] rdata;
wire wfull, rempty;
integer errors, npush, npop;
async_fifo #(.WIDTH(WIDTH), .ADDR(ADDR), .DEPTH(DEPTH)) dut (
.clk_wr(clk_wr), .wrst_n(wrst_n), .winc(winc), .wdata(wdata), .wfull(wfull),
.clk_rd(clk_rd), .rrst_n(rrst_n), .rinc(rinc), .rdata(rdata), .rempty(rempty));
always #5 clk_wr = ~clk_wr; // fast writer
always #13 clk_rd = ~clk_rd; // slow reader, async
// Scoreboard: golden queue as a big array with head/tail pointers.
reg [WIDTH-1:0] sb [0:65535];
integer head, tail;
reg [WIDTH-1:0] wcount, exp;
always @(posedge clk_wr) begin
if (!wrst_n) begin winc <= 1'b0; end
else begin
if (winc && wfull) begin $display("FAIL OVERFLOW: write while full"); errors = errors + 1; end
if (winc && !wfull) begin
sb[tail] = wdata; tail = tail + 1; wcount = wcount + 1; npush = npush + 1;
end
winc <= ~wfull;
wdata <= wcount;
end
end
always @(posedge clk_rd) begin
if (!rrst_n) begin rinc <= 1'b0; end
else begin
if (rinc && rempty) begin $display("FAIL UNDERFLOW: read while empty"); errors = errors + 1; end
if (rinc && !rempty) begin
exp = sb[head]; head = head + 1; npop = npop + 1;
if (rdata !== exp) begin
$display("FAIL ORDER/DATA: popped %h exp %h", rdata, exp); errors = errors + 1;
end
end
rinc <= ~rempty;
end
end
initial begin
errors = 0; npush = 0; npop = 0; head = 0; tail = 0; wcount = 0;
clk_wr = 0; clk_rd = 0; wrst_n = 0; rrst_n = 0; winc = 0; rinc = 0; wdata = 0;
repeat (3) @(posedge clk_wr); wrst_n = 1'b1;
repeat (3) @(posedge clk_rd); rrst_n = 1'b1;
repeat (4000) @(posedge clk_wr);
winc = 1'b0;
repeat (200) @(posedge clk_rd);
if (errors == 0 && head == tail)
$display("PASS: %0d pushed, %0d popped, in-order/exactly-once, no overflow/underflow", npush, npop);
else
$display("FAIL: %0d errors, %0d words unread", errors, tail - head);
$finish;
end
endmoduleIn VHDL the pointers are unsigned(ADDR downto 0) from numeric_std; Gray encode is bin xor ('0' & bin(ADDR downto 1)) (a right shift by one), each synchronizer is a pair of registers on its destination clock, and the two-MSB-inverted full comparison uses a slice-and-not.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity async_fifo is
generic ( WIDTH : positive := 8; ADDR : positive := 4 ); -- DEPTH = 2**ADDR
port (
clk_wr : in std_logic;
wrst_n : in std_logic; -- async, active-low
winc : in std_logic;
wdata : in std_logic_vector(WIDTH-1 downto 0);
wfull : out std_logic;
clk_rd : in std_logic;
rrst_n : in std_logic;
rinc : in std_logic;
rdata : out std_logic_vector(WIDTH-1 downto 0);
rempty : out std_logic
);
end entity;
architecture rtl of async_fifo is
constant DEPTH : positive := 2**ADDR;
type mem_t is array (0 to DEPTH-1) of std_logic_vector(WIDTH-1 downto 0);
signal mem : mem_t;
signal wbin, wptr : unsigned(ADDR downto 0) := (others => '0'); -- write bin + gray
signal rbin, rptr : unsigned(ADDR downto 0) := (others => '0'); -- read bin + gray
signal wbin_next, wgray_next : unsigned(ADDR downto 0);
signal rbin_next, rgray_next : unsigned(ADDR downto 0);
signal wq1_rptr, wq2_rptr : unsigned(ADDR downto 0) := (others => '0'); -- wptr in clk_rd
signal rq1_wptr, rq2_wptr : unsigned(ADDR downto 0) := (others => '0'); -- rptr in clk_wr
signal wfull_i, rempty_i : std_logic;
signal rq2_inv : unsigned(ADDR downto 0);
attribute ASYNC_REG : string;
attribute ASYNC_REG of wq1_rptr : signal is "TRUE";
attribute ASYNC_REG of wq2_rptr : signal is "TRUE";
attribute ASYNC_REG of rq1_wptr : signal is "TRUE";
attribute ASYNC_REG of rq2_wptr : signal is "TRUE";
begin
-- next-state binary + gray (gray = bin xor (bin srl 1))
wbin_next <= wbin + 1 when (winc = '1' and wfull_i = '0') else wbin;
wgray_next <= wbin_next xor ('0' & wbin_next(ADDR downto 1));
rbin_next <= rbin + 1 when (rinc = '1' and rempty_i = '0') else rbin;
rgray_next <= rbin_next xor ('0' & rbin_next(ADDR downto 1));
-- WRITE pointer registers
process (clk_wr, wrst_n) begin
if wrst_n = '0' then wbin <= (others => '0'); wptr <= (others => '0');
elsif rising_edge(clk_wr) then wbin <= wbin_next; wptr <= wgray_next; end if;
end process;
-- READ pointer registers
process (clk_rd, rrst_n) begin
if rrst_n = '0' then rbin <= (others => '0'); rptr <= (others => '0');
elsif rising_edge(clk_rd) then rbin <= rbin_next; rptr <= rgray_next; end if;
end process;
-- each-way two-flop synchronizers on the GRAY pointers
process (clk_rd, rrst_n) begin -- wptr into clk_rd
if rrst_n = '0' then wq1_rptr <= (others => '0'); wq2_rptr <= (others => '0');
elsif rising_edge(clk_rd) then wq1_rptr <= wptr; wq2_rptr <= wq1_rptr; end if;
end process;
process (clk_wr, wrst_n) begin -- rptr into clk_wr
if wrst_n = '0' then rq1_wptr <= (others => '0'); rq2_wptr <= (others => '0');
elsif rising_edge(clk_wr) then rq1_wptr <= rptr; rq2_wptr <= rq1_wptr; end if;
end process;
-- FULL comparison value: invert the TOP TWO gray bits of the synchronized read pointer
rq2_inv <= (not rq2_wptr(ADDR downto ADDR-1)) & rq2_wptr(ADDR-2 downto 0);
-- EMPTY (read domain): plain gray equality
process (clk_rd, rrst_n) begin
if rrst_n = '0' then rempty_i <= '1';
elsif rising_edge(clk_rd) then
if rgray_next = wq2_rptr then rempty_i <= '1'; else rempty_i <= '0'; end if;
end if;
end process;
-- FULL (write domain): gray equality with top-two-inverted read pointer
process (clk_wr, wrst_n) begin
if wrst_n = '0' then wfull_i <= '0';
elsif rising_edge(clk_wr) then
if wgray_next = rq2_inv then wfull_i <= '1'; else wfull_i <= '0'; end if;
end if;
end process;
wfull <= wfull_i;
rempty <= rempty_i;
-- dual-port RAM: BINARY low bits address it
process (clk_wr) begin
if rising_edge(clk_wr) then
if winc = '1' and wfull_i = '0' then
mem(to_integer(wbin(ADDR-1 downto 0))) <= wdata;
end if;
end if;
end process;
rdata <= mem(to_integer(rbin(ADDR-1 downto 0)));
end architecture; library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity async_fifo_tb is
end entity;
architecture sim of async_fifo_tb is
constant WIDTH : positive := 8;
constant ADDR : positive := 3; -- DEPTH = 8
signal clk_wr, clk_rd : std_logic := '0';
signal wrst_n, rrst_n : std_logic := '0';
signal winc, rinc, wfull, rempty : std_logic := '0';
signal wdata, rdata : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
-- scoreboard: golden queue as an array with head/tail
type sb_t is array (0 to 65535) of std_logic_vector(WIDTH-1 downto 0);
signal sb : sb_t;
signal head, tail : integer := 0;
signal wcount : unsigned(WIDTH-1 downto 0) := (others => '0');
signal errors : integer := 0;
begin
dut : entity work.async_fifo
generic map (WIDTH => WIDTH, ADDR => ADDR)
port map (clk_wr => clk_wr, wrst_n => wrst_n, winc => winc, wdata => wdata, wfull => wfull,
clk_rd => clk_rd, rrst_n => rrst_n, rinc => rinc, rdata => rdata, rempty => rempty);
clk_wr <= not clk_wr after 5 ns; -- fast writer
clk_rd <= not clk_rd after 13 ns; -- slow reader, async
-- WRITER: push whenever not full; record committed words; overflow guard.
wproc : process (clk_wr) begin
if rising_edge(clk_wr) then
if wrst_n = '0' then winc <= '0';
else
assert not (winc = '1' and wfull = '1')
report "OVERFLOW: write while full" severity error;
if winc = '1' and wfull = '0' then
sb(tail) <= wdata; tail <= tail + 1; wcount <= wcount + 1;
end if;
winc <= not wfull;
wdata <= std_logic_vector(wcount);
end if;
end if;
end process;
-- READER: pop whenever not empty; check order; underflow guard.
rproc : process (clk_rd) begin
if rising_edge(clk_rd) then
if rrst_n = '0' then rinc <= '0';
else
assert not (rinc = '1' and rempty = '1')
report "UNDERFLOW: read while empty" severity error;
if rinc = '1' and rempty = '0' then
assert rdata = sb(head)
report "ORDER/DATA: popped word out of order" severity error;
head <= head + 1;
end if;
rinc <= not rempty;
end if;
end if;
end process;
stim : process begin
wait for 40 ns; wrst_n <= '1';
wait for 60 ns; rrst_n <= '1';
wait for 60 us; -- long run: fills and drains repeatedly
report "async_fifo self-check complete" severity note;
wait;
end process;
end architecture;4b. The binary-pointer bug beside the Gray-pointer fix
The signature capstone failure is crossing the pointers as binary instead of Gray. Shown here as buggy vs fixed, then dramatized in §7. The buggy version two-flop-synchronizes the raw binary pointers, so a multi-bit step (0111 -> 1000) lets the far side capture a phantom pointer, mis-clear a flag, and overflow or underflow. The fixed version crosses Gray and compares Gray, so every crossed pointer is real and adjacent and the flags stay conservative.
// BUGGY: cross the write pointer as BINARY, per-bit 2FF, compare binary for empty.
// On a multi-bit step the read side captures a PHANTOM wptr -> false empty-clear
// (reads a never-written slot => UNDERFLOW) or false empty (drops throughput).
module rempty_bad #(parameter int ADDR = 3)(
input logic clk_rd, rrst_n,
input logic [ADDR:0] wbin, // RAW BINARY write pointer, crossing raw
input logic [ADDR:0] rbin_next,
output logic rempty
);
logic [ADDR:0] wb_meta, wb_sync; // binary crosses -> bit skew -> phantom
always_ff @(posedge clk_rd or negedge rrst_n)
if (!rrst_n) begin wb_meta <= '0; wb_sync <= '0; end
else begin wb_meta <= wbin; wb_sync <= wb_meta; end
always_ff @(posedge clk_rd or negedge rrst_n)
if (!rrst_n) rempty <= 1'b1;
else rempty <= (rbin_next == wb_sync); // compared against a PHANTOM
endmodule
// FIXED: cross the write pointer as GRAY, compare gray for empty. Every crossed value is
// real and adjacent -> empty is CONSERVATIVE -> never a false empty-clear -> no underflow.
module rempty_good #(parameter int ADDR = 3)(
input logic clk_rd, rrst_n,
input logic [ADDR:0] wgray, // GRAY write pointer crosses
input logic [ADDR:0] rgray_next,
output logic rempty
);
(* ASYNC_REG = "TRUE" *) logic [ADDR:0] wg_meta, wg_sync;
always_ff @(posedge clk_rd or negedge rrst_n)
if (!rrst_n) begin wg_meta <= '0; wg_sync <= '0; end
else begin wg_meta <= wgray; wg_sync <= wg_meta; end
always_ff @(posedge clk_rd or negedge rrst_n)
if (!rrst_n) rempty <= 1'b1;
else rempty <= (rgray_next == wg_sync); // plain GRAY equality, coherent
endmodule module async_fifo_bug_tb;
localparam int ADDR = 3;
logic clk_rd = 1'b0, rrst_n;
logic [ADDR:0] wbin, wgray, rbin_next, rgray_next;
logic empty_bad, empty_good;
int errors = 0;
rempty_bad #(.ADDR(ADDR)) ubad (.clk_rd(clk_rd), .rrst_n(rrst_n),
.wbin(wbin), .rbin_next(rbin_next), .rempty(empty_bad));
rempty_good #(.ADDR(ADDR)) ugood (.clk_rd(clk_rd), .rrst_n(rrst_n),
.wgray(wgray), .rgray_next(rgray_next), .rempty(empty_good));
always #5 clk_rd = ~clk_rd;
assign wgray = wbin ^ (wbin >> 1);
assign rgray_next = rbin_next ^ (rbin_next >> 1);
initial begin
// Reader sits at binary 0. Writer has genuinely advanced to 8 (0111 -> 1000 step).
rrst_n = 1'b0; wbin = 4'b0111; rbin_next = 4'b0000;
@(posedge clk_rd); #1; rrst_n = 1'b1;
@(posedge clk_rd); // stage 1 captures the pointers
// BIT-SKEW MODEL of 0111 -> 1000: the BINARY path's first stage blends to 0000,
// a value equal to the reader's pointer -> false empty-clear collapses to EMPTY=1
// exactly when the FIFO actually has 8 words. Real data would be read as absent.
force ubad.wb_meta = 4'b0000; // phantom == reader pointer
// The GRAY path sees gray(0111)=0100 or gray(1000)=1100; neither equals gray(0)=0000,
// so empty correctly stays CLEAR (data is present). Model the old gray value.
force ugood.wg_meta = 4'b0100;
wbin = 4'b1000; // writer completes the step
repeat (2) @(posedge clk_rd); #1;
release ubad.wb_meta; release ugood.wg_meta;
// BUGGY: binary crossing made empty ASSERT while 8 words are present -> a reader
// gated by this empty would stall or, if it trusted a prior clear, underflow.
if (empty_bad === 1'b1)
$display("BUG OBSERVED: binary crossing declared EMPTY with 8 words present (phantom)");
// FIXED: gray crossing keeps empty CLEAR -> conservative, no false empty, no underflow.
assert (empty_good === 1'b0)
else begin $error("gray empty wrongly asserted with data present"); errors++; end
if (errors == 0) $display("PASS: gray crossing keeps flags conservative; binary crossing phantoms");
else $display("FAIL: %0d errors", errors);
$finish;
end
endmoduleThe Verilog buggy/fixed pair is the same story with reg/wire; the bit-skew model uses force/release, and the self-check prints PASS/FAIL on whether the Gray-crossed empty stays conservative.
// BUGGY: cross write pointer as BINARY, compare binary -> phantom clears/sets empty wrong.
module rempty_bad #(parameter ADDR = 3)(
input wire clk_rd, rrst_n,
input wire [ADDR:0] wbin, rbin_next,
output reg rempty
);
reg [ADDR:0] wb_meta, wb_sync;
always @(posedge clk_rd or negedge rrst_n)
if (!rrst_n) begin wb_meta <= 0; wb_sync <= 0; end
else begin wb_meta <= wbin; wb_sync <= wb_meta; end
always @(posedge clk_rd or negedge rrst_n)
if (!rrst_n) rempty <= 1'b1;
else rempty <= (rbin_next == wb_sync);
endmodule
// FIXED: cross write pointer as GRAY, compare gray -> conservative empty, no underflow.
module rempty_good #(parameter ADDR = 3)(
input wire clk_rd, rrst_n,
input wire [ADDR:0] wgray, rgray_next,
output reg rempty
);
(* ASYNC_REG = "TRUE" *) reg [ADDR:0] wg_meta, wg_sync;
always @(posedge clk_rd or negedge rrst_n)
if (!rrst_n) begin wg_meta <= 0; wg_sync <= 0; end
else begin wg_meta <= wgray; wg_sync <= wg_meta; end
always @(posedge clk_rd or negedge rrst_n)
if (!rrst_n) rempty <= 1'b1;
else rempty <= (rgray_next == wg_sync);
endmodule module async_fifo_bug_tb;
parameter ADDR = 3;
reg clk_rd, rrst_n;
reg [ADDR:0] wbin, rbin_next;
wire [ADDR:0] wgray = wbin ^ (wbin >> 1);
wire [ADDR:0] rgray_next = rbin_next ^ (rbin_next >> 1);
wire empty_bad, empty_good;
integer errors;
rempty_bad #(.ADDR(ADDR)) ubad (.clk_rd(clk_rd), .rrst_n(rrst_n),
.wbin(wbin), .rbin_next(rbin_next), .rempty(empty_bad));
rempty_good #(.ADDR(ADDR)) ugood (.clk_rd(clk_rd), .rrst_n(rrst_n),
.wgray(wgray), .rgray_next(rgray_next), .rempty(empty_good));
always #5 clk_rd = ~clk_rd;
initial begin
errors = 0; clk_rd = 1'b0; rrst_n = 1'b0; wbin = 4'b0111; rbin_next = 4'b0000;
@(posedge clk_rd); #1; rrst_n = 1'b1;
@(posedge clk_rd);
// BIT-SKEW MODEL for 0111 -> 1000.
force ubad.wb_meta = 4'b0000; // phantom == reader pointer
force ugood.wg_meta = 4'b0100; // gray(0111), real & adjacent
wbin = 4'b1000;
repeat (2) @(posedge clk_rd); #1;
release ubad.wb_meta; release ugood.wg_meta;
if (empty_bad === 1'b1)
$display("BUG OBSERVED: binary crossing declared EMPTY with 8 words present");
if (empty_good !== 1'b0) begin
$display("FAIL: gray empty wrongly asserted with data present"); errors = errors + 1;
end
if (errors == 0) $display("PASS: gray crossing keeps flags conservative; binary can phantom");
else $display("FAIL: %0d errors", errors);
$finish;
end
endmoduleIn VHDL the buggy path crosses unsigned binary pointers, the fixed path crosses their Gray codes; the bit-skew model is applied by forcing the first synchronizer stage in the testbench, and the self-check uses assert ... severity error.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
-- BUGGY: cross write pointer as BINARY, compare binary -> phantom sets/clears empty wrong.
entity rempty_bad is
generic ( ADDR : positive := 3 );
port ( clk_rd, rrst_n : in std_logic;
wbin, rbin_next : in std_logic_vector(ADDR downto 0);
rempty : out std_logic );
end entity;
architecture rtl of rempty_bad is
signal wb_meta, wb_sync : std_logic_vector(ADDR downto 0) := (others => '0');
begin
process (clk_rd, rrst_n) begin
if rrst_n = '0' then wb_meta <= (others => '0'); wb_sync <= (others => '0');
elsif rising_edge(clk_rd) then wb_meta <= wbin; wb_sync <= wb_meta; end if;
end process;
process (clk_rd, rrst_n) begin
if rrst_n = '0' then rempty <= '1';
elsif rising_edge(clk_rd) then
if rbin_next = wb_sync then rempty <= '1'; else rempty <= '0'; end if;
end if;
end process;
end architecture;
-- FIXED: cross write pointer as GRAY, compare gray -> conservative empty, no underflow.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity rempty_good is
generic ( ADDR : positive := 3 );
port ( clk_rd, rrst_n : in std_logic;
wgray, rgray_next : in std_logic_vector(ADDR downto 0);
rempty : out std_logic );
end entity;
architecture rtl of rempty_good is
signal wg_meta, wg_sync : std_logic_vector(ADDR downto 0) := (others => '0');
attribute ASYNC_REG : string;
attribute ASYNC_REG of wg_meta : signal is "TRUE";
attribute ASYNC_REG of wg_sync : signal is "TRUE";
begin
process (clk_rd, rrst_n) begin
if rrst_n = '0' then wg_meta <= (others => '0'); wg_sync <= (others => '0');
elsif rising_edge(clk_rd) then wg_meta <= wgray; wg_sync <= wg_meta; end if;
end process;
process (clk_rd, rrst_n) begin
if rrst_n = '0' then rempty <= '1';
elsif rising_edge(clk_rd) then
if rgray_next = wg_sync then rempty <= '1'; else rempty <= '0'; end if;
end if;
end process;
end architecture;Across all three languages the lesson is one sentence: cross each pointer as Gray so every synchronized copy is a real value at most one step behind, then compute rempty in the read domain and wfull in the write domain off those Gray pointers — and because the crossed pointer is never ahead of the truth, both flags are conservative and the FIFO can never overflow or underflow.
5. Verification Strategy
An async FIFO is a data-integrity across two clocks property, so verification lives at the crossing: two unrelated clocks, an independent writer and reader running at full tilt, and a scoreboard that proves every word survives intact.
Every word written is read exactly once, in FIFO order, with none lost and none duplicated; a write is never honoured while
wfull; a read is never honoured whilerempty; and this holds under a fast writer / slow reader, a slow writer / fast reader, and near-equal clocks.
Everything below makes that invariant checkable and maps onto the §4 testbenches.
- Two-async-clock scoreboard, self-checking. Drive
clk_wrandclk_rdfrom co-prime half-periods so their edges drift like a real crossing. An independent writer pushes whenever!wfulland records each committed word in a golden queue; an independent reader pops whenever!remptyand checks the popped word against the head of that queue. A mismatch is a lost, duplicated, or reordered word — the §4 TBs assert exactly this (SV queue, Verilog/VHDL array with head/tail). At the end, the queue must be empty (pushes minus in-flight equal pops). - Overflow / underflow assertions, every cycle. Two standing checks: a write must never be honoured while
wfull(overflow), and a read must never be honoured whilerempty(underflow). Because the flags gate the pointer motion and are conservative, these can never fire on a correct design — which is precisely why asserting them catches an aggressive (non-conservative) flag or a binary-crossing phantom the instant it happens. - Run all three clock ratios. Fast writer / slow reader drives the FIFO to full and holds it there (this is what exercises
wfulland the read-pointer crossing into the write domain). Slow writer / fast reader drives it to empty and holds it there (exercisingremptyand the write-pointer crossing into the read domain). Near-equal clocks with drift keep it hovering mid-depth and cross the wrap boundary repeatedly. A design that is correct in one ratio but mishandles the Gray full/empty condition on a lap is caught by running all three. - Corner cases to exercise. Simultaneous push and pop while near-full and near-empty; a burst that fills exactly to
DEPTHthen one more push (must be blocked); a drain to exactly empty then one more pop (must be blocked); independent per-domain resets asserted mid-stream (each side returns to zero and the FIFO reports empty); and a long free-run that laps the pointers many times so the wrap MSB and the two-MSB Gray full condition are exercised on the second and later laps, not just the first. - Formal / CDC-lint intent. In a real flow this block goes through a CDC-lint tool that structurally checks every asynchronous crossing has a proper synchronizer (the two 2FF chains), that no combinational logic sits between the synchronizer flops, and that the crossing signal is Gray-coded (a multi-bit binary crossing is flagged). A formal proof complements the simulation: assert the data-integrity and no-overflow/underflow properties and let the tool prove them across all clock phasings, which simulation can only sample. The
ASYNC_REGattribute on the synchronizer flops is what keeps synthesis from retiming or optimizing the chain that the lint and formal proofs rely on. - Expected result. On a correct run, across thousands of source cycles in every clock ratio, the scoreboard drains to empty with zero mismatches and the overflow/underflow assertions never fire. The visual signature of the binary-crossing bug is the opposite: the overflow or underflow assertion fires intermittently under a sustained near-boundary burst, and only at certain clock ratios — the tell of a phantom pointer, not a data-path fault.
6. Common Mistakes
Crossing the pointers as BINARY instead of Gray. The signature capstone failure. If you two-flop-synchronize the raw binary pointers, a step that flips several bits at once (0111 -> 1000) lets the destination capture a phantom the source never held. The far side then compares a flag against that phantom and mis-decides: a false empty-clear reads a slot that was never written (underflow), and a false full-clear writes over an unread word (overflow). It is clock-ratio-dependent and passes a light test. Cross each pointer as Gray so any mid-transition sample is a real, adjacent value (11.5). (Post-mortem in §7, row 1.)
Getting the Gray FULL condition wrong — inverting the wrong number of bits. full in Gray is 'write Gray equals synchronized read Gray with the top two bits inverted.' Invert only the single top bit and you change the wrap parity but leave the second Gray bit inconsistent, so the address fields no longer compare equal and full fires a half-lap early or late — a false full (throttles, wastes throughput) or, worse, a full that clears when the FIFO is actually full (overflow). The two-MSB inversion is the Gray form of 7.2's 'addresses equal, wrap bits differ'; it is exact and must be respected. (Post-mortem in §7, row 2.)
Comparing a pointer in the wrong domain, or unsynchronized. Each flag must compare a local pointer against the synchronized copy of the far pointer. Compute rempty from the raw wptr (no synchronizer) and you have sampled a metastable multi-bit value directly into logic — 11.1's disease. Compute wfull in the read domain, or vice versa, and the flag crosses a boundary it should not. rempty lives in the read domain reading wq2_rptr; wfull lives in the write domain reading rq2_wptr; nothing else.
Using the synchronized (delayed) pointer to ADDRESS the RAM. The synchronized pointer is for the flag comparison only. The RAM is addressed by the local binary pointer — mem[wbin[ADDR-1:0]] on the write side, mem[rbin[ADDR-1:0]] on the read side — because the local pointer is the true, current position in its own domain. Address the RAM with wq2_rptr or a Gray value and you read or write the wrong location, and you have thrown away the whole point of keeping the binary pointer locally. Address with the LOCAL pointer; compare with the SYNCED remote pointer.
Making a flag aggressive instead of conservative. The safety of the whole design rests on both flags being pessimistic: empty may assert too long but never too short; full may assert too long but never too short. If you 'optimize' a flag to clear a cycle earlier — say, deriving it from a pointer you have not yet fully synchronized, or from the next pointer combinationally fed back — you make it aggressive, and an aggressive full clears while the FIFO is still full and overflows. Never trade the conservative guarantee for a cycle of latency; the latency is the price of correctness at a clock crossing.
Non-power-of-two DEPTH, or a common reset that is not per-domain. As in 7.2, the extra-MSB (here Gray) scheme requires DEPTH = 2**ADDR so the address rollover and the wrap-bit toggle coincide; a non-power-of-two depth breaks the Gray full/empty conditions. And each domain needs its own reset (wrst_n, rrst_n), each released cleanly in its own clock, so both pointers and both synchronizers come up at zero and the FIFO powers up empty; a single reset shared raw across two clocks is itself an unsynchronized crossing.
7. DebugLab
The FIFO that overflowed across clocks — a binary pointer crossing and a wrong Gray full condition
The engineering lesson: an asynchronous FIFO is not a new invention — it is the 7.2 FIFO body with each pointer crossed to the other domain, and it is correct only if every crossing preserves the conservative guarantee. The two tells are distinct. A bug that overflows or underflows only under a sustained, skewed-clock burst and never reproduces in a single-clock sim is a binary pointer crossing — a phantom, cured by Gray-coding the pointers. A bug where empty is perfect but full is wrong specifically at the wrap is the Gray full condition — the two-MSB inversion done wrong. Three habits make both impossible, and they are identical across SystemVerilog, Verilog, and VHDL: cross each pointer as Gray and compare Gray-to-Gray, invert the top two Gray bits (not one) for the full condition, and keep every flag conservative — empty and full may lag but never lead — then verify with a two-async-clock scoreboard that laps the pointers. This is the capstone because it is where the whole chapter's tools have to work together: metastability (11.1), the two-flop synchronizer (11.2), the multi-bit phantom (11.3), Gray pointers (11.5), and the FIFO body (7.1, 7.2) — one block, correct by construction.
8. Interview Q&A
9. Exercises
Design and reasoning exercises — no syntax drills. Each rehearses the composition (Gray pointers + each-way synchronizers + FIFO body) and the conservative-flag reasoning behind it.
Exercise 1 — Prove the Gray full condition on paper
For DEPTH = 8 (ADDR = 3, 4-bit pointers), write the empty and full conditions in terms of wptr, rptr, wq2_rptr, rq2_wptr. Then hand-trace: fill the FIFO to exactly full and give wbin, wgray, rbin, rgray, and the top-two-inverted read value at the moment wfull asserts. Confirm in one line that wfull does not assert one write earlier, and explain in one line why inverting only the single top Gray bit would trip full at the wrong point.
Exercise 2 — Place the pieces
Draw the block: mark where each Gray conversion sits, which pointer each two-flop synchronizer carries and into which domain, and in which domain each flag is computed. Then answer, in one line each: (a) why the write pointer crosses into the read domain and not into the write domain; (b) why the RAM is addressed by the binary pointer and never by the synchronized pointer; (c) why there is no occupancy counter.
Exercise 3 — Trace the two clock ratios
For a DEPTH = 4 FIFO, argue that no word is lost, duplicated, or double-read, and that the FIFO never overflows or underflows, under (a) a fast writer / slow reader that drives it to full and holds it there, and (b) a slow writer / fast reader that drives it to empty and holds it there. In each case name which synchronized pointer's conservatism provides the guarantee and what the worst case the delayed flag causes is (hint: it is latency, not corruption).
Exercise 4 — Size the depth and write the test plan
A producer on a 200 MHz clk_wr delivers bursts of up to 64 back-to-back words; the consumer on an 80 MHz clk_rd drains one word per read clock. Estimate the minimum DEPTH (a power of two) that keeps wfull from throttling the burst, and state how the synchronizer latency affects your margin. Then write the test plan (not the RTL) that would catch a binary-pointer crossing before tape-out: the two-async-clock scoreboard, the exact overflow/underflow assertions, the three clock ratios to run, and the lint/formal checks. State which single check is the direct detector for a phantom-pointer overflow.
10. Related Tutorials
Continue in RTL Design Patterns (sibling and forward links unlock as they ship):
- Clock-Domain-Crossing Design Rules — Chapter 11.7; the checklist this capstone embodies — synchronize every crossing, Gray-code multi-bit counters, no logic between sync flops, per-domain reset — turned into design and lint rules.
- Case Study — A Streaming FIFO — a full worked block that drops this async FIFO into a real datapath with backpressure, showing the depth sizing and flag latency in context.
The pieces this capstone assembles:
- Synchronous FIFO Architecture — Chapter 7.1; the circular-buffer body — dual-port RAM and wrapping write/read pointers — that this design splits across a clock boundary.
- Full & Empty Logic — Chapter 7.2; the extra-MSB pointer scheme whose empty/full conditions this page re-expresses in Gray for the cross-domain comparison.
- FIFO Depth Calculation — Chapter 7.4; how deep the async FIFO must be for a given producer/consumer rate and burst — the depth-across-clock-ratio sizing this page's flags guard.
- Gray-Code Pointer Synchronization — Chapter 11.5; the exact crossing this FIFO uses for each pointer — increment binary, cross as Gray, at most one step behind — the coherence that makes the flags conservative.
- The Two-Flop Synchronizer — Chapter 11.2; the two-flop chain that carries each Gray pointer into the other domain, one each way.
- Dual-Port RAM — Chapter 6.2; the two-port memory written on
clk_wrand read onclk_rdat the heart of the FIFO.
Backward / in-track dependencies:
- The Multi-Bit CDC Problem — Chapter 11.3; why per-bit synchronizing a raw binary pointer captures a phantom — the failure the Gray crossing exists to prevent, and the signature bug in §7.
- Metastability — Chapter 11.1; the physical event every crossing in this design tolerates rather than prevents, contained by the two-flop synchronizers.
- Gray Counters — Chapter 3.5; the one-bit-per-step counter the pointers become so they cross coherently.
- The RTL Design Mindset — Chapter 0.2; the State and Verification lenses this capstone applies — the two pointers are the FIFO's state, and the scoreboard is how you prove it safe.
- What Is an RTL Design Pattern? — Chapter 0.1; why the async FIFO is the archetype of a composed pattern — several smaller patterns assembled into one correct-by-construction block.
Verilog v1 prerequisites (the grammar this pattern transcribes into):
- Bitwise Operators — the XORs that build
gray = bin ^ (bin >> 1)and the bit slicing (rq2_wptr[ADDR:ADDR-1]) the full condition reads. - Blocking and Non-Blocking Assignments — why the pointers, synchronizer flops, and flag registers update with non-blocking (
<=) in their clocked processes. - Case Statements — the selection grammar behind the committed-write / committed-read decoding in the counter-style variants.
- Initial and Always Blocks — the clocked
alwaysblocks each domain's pointer and synchronizer live in, and theinitialblock that drives the two-async-clock testbench.
11. Summary
- The async FIFO is the 7.2 FIFO split across two unrelated clocks — and every flag needs the other domain's pointer. The body is unchanged: a dual-port RAM, a write pointer in
clk_wr, a read pointer inclk_rd, eachADDR+1bits with an extra wrap MSB. What is new is that to computefullthe write side needsrptrand to computeemptythe read side needswptr, each of which lives in the other clock domain, so each pointer must cross an asynchronous boundary. - It is correct by construction because it composes the chapter's tools. Keep each pointer as a Gray counter (11.5) so it crosses one bit at a time and any sample is a real value at most one step behind; two-flop-synchronize (11.2) the write Gray pointer into the read domain (
wq2_rptr) and the read Gray pointer into the write domain (rq2_wptr); and read the extra-MSB empty/full logic (7.2) off those Gray pointers. Increment binary (for the local RAM address), cross Gray, compare Gray-to-Gray. emptyis computed in the read domain,fullin the write domain.rempty = (rptr == wq2_rptr)— a plain Gray equality (same address and lap).wfull = (wptr == synchronized read Gray with the top two bits inverted)— the Gray form of 'addresses equal, wrap bit opposite,' where the two-MSB inversion is required becausegray = bin ^ (bin >> 1)folds the top two binary bits together; inverting only one bit tripsfulla half-lap off.- Both flags are conservative, so no overflow or underflow — ever. Each synchronized pointer is real and never ahead of the truth, so
emptycan only be pessimistic (never claims data that is not there) andfullcan only be pessimistic (never claims space that is not there). Pessimisticemptycannot underflow; pessimisticfullcannot overflow. The cost is a hair of flag latency; the guarantee holds under a fast writer / slow reader, a slow writer / fast reader, and near-equal clocks. - The signature bugs are the binary crossing and the wrong Gray full. Crossing pointers as raw binary captures a phantom and overflows/underflows under a skewed-clock burst (invisible in single-clock sim); inverting one Gray bit instead of two makes
fullwrong at the wrap. Verify with a two-async-clock scoreboard — in-order, exactly-once, no overflow/underflow across all clock ratios and many laps — plus CDC-lint and formal. Built and scoreboard-verified here in SystemVerilog, Verilog, and VHDL.