Skip to content

RTL Design Patterns · Chapter 7 · FIFO Design

Synchronous FIFO Architecture

A synchronous FIFO is a first-in first-out buffer on a single clock that lets a producer and a consumer run at their own rates without losing data or stalling each other. This lesson builds one by composing patterns you already know: a dual-port RAM for storage, a write pointer and a read pointer built as wrapping counters, and a little control that gates writes on full and reads on empty so it never overwrites unread data or returns a word that was never written. You will use a simple occupancy count to derive the full and empty flags, trace how data comes back out in the exact order it went in, and study the signature ungated read or write bug. Everything is coded and self-check-verified in SystemVerilog, Verilog, and VHDL.

Intermediate16 min readRTL Design PatternsFIFOCircular BufferDual-Port RAMPointersFlow Control

Chapter 7 · Section 7.1 · FIFO Design

1. The Engineering Problem

You have a packet parser feeding a checksum unit. The parser, when it sees a valid frame, streams out one word per cycle for eight cycles straight — a burst — and then sits idle for a while until the next frame arrives. The checksum unit consumes one word per cycle most of the time, but every few cycles it stalls for a cycle to fold in a carry. Both blocks live on the same clock. On paper their average rates match: the parser produces roughly as many words per microsecond as the checksum unit eats.

But averages do not survive contact with a burst. If you wire the parser's output straight into the checksum unit, then during the eight-cycle burst the checksum unit — which stalls one cycle in four — simply cannot keep up, and the parser has nowhere to put the words it is producing right now. Your only options with a direct connection are both bad: make the parser wait (so it can no longer stream a burst — you have thrown away its burst capability), or drop the words it produces while the consumer is busy (data loss). The producer and the consumer are welded together, and the instantaneous mismatch — not the average — is what breaks.

What you need is a buffer between them: somewhere the parser can dump its burst at full rate, that the checksum unit can drain at its own pace, and that hands the words back in exactly the order they arrived (a checksum is order-sensitive — reorder the words and the answer is wrong). That buffer is a FIFO — First-In, First-Out. It decouples the two blocks: the producer writes whenever it has data and the FIFO is not full; the consumer reads whenever it wants data and the FIFO is not empty; and in between, the FIFO absorbs the burst. This page builds the synchronous FIFO — both sides on one clock — because that is the common case and it isolates the core structure before the clock-crossing complications of an async FIFO (11.6).

the-need.v — a burst producer, a stalling consumer, one clock
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // Producer: streams an 8-word burst, then idles. Consumer: mostly 1/cycle,
   // stalls occasionally. Same clk. Average rates match; INSTANTANEOUS rates do not.
 
   // WRONG — a direct wire: the producer must stall whenever the consumer is busy,
   //         so the burst can't happen; or words produced during a stall are LOST.
   //   assign cksum_data = parser_data;   // no storage, no decoupling
 
   // RIGHT — a FIFO between them: parser WRITES into it at burst rate when !full,
   //         checksum READS out at its own pace when !empty, order preserved.
   //   (the pattern this page builds — circular buffer: RAM + wptr/rptr + gating)

2. Mental Model

3. Pattern Anatomy

A synchronous FIFO is three prior patterns bolted together, plus an interface. Take them in turn.

The storage — a simple-dual-port RAM (6.5). The buffer is a memory mem of DEPTH words, each WIDTH bits. It needs to accept a write and serve a read in the same cycle — the producer pushes while the consumer pops — so it is a simple-dual-port RAM: one write port (address wptr, data wdata) and one independent read port (address rptr, data rdata). One clock, so no clock crossing (that is the async FIFO, 11.6). Address width is AW = log2(DEPTH).

The two pointers — wrapping counters (3.1). wptr is where the next write goes; rptr is where the next read comes from. Each is an AW-bit counter that advances by one on its operation and wraps at DEPTH (for a power-of-two DEPTH the wrap is automatic — the counter simply rolls over). wptr advances only when a write actually happens (write requested and not full); rptr advances only when a read actually happens (read requested and not empty). The pointers chase each other around the ring; the distance between them is the occupancy.

The control — full/empty derivation and gating (4.1). Something must know whether the FIFO is full or empty and gate the operations. The subtle, pointer-based way — comparing wptr and rptr with an extra wrap bit to tell "full" from "empty" when the pointers are equal — is the flagship §7.2. Here we use the obviously correct version: keep an occupancy counter count (0 to DEPTH). It increments on a write-only cycle, decrements on a read-only cycle, and stays put on a simultaneous read+write or an idle cycle. Then empty is count == 0 and full is count == DEPTH. The gate is simple and total: perform the write only when wr_en && !full; perform the read only when rd_en && !empty.

The interface. Two independent sides on one clock:

  • Write side: wr_en (push request), wdata (the word), and full (back-pressure — the producer must check it and not push when high).
  • Read side: rd_en (pop request), rdata (the word out), and empty (the consumer must check it and not pop when high).

One timing subtlety you inherit from the RAM — read latency (6.2). A synchronous RAM registers its read: the word for address rptr appears on rdata one cycle after you present rptr, not combinationally. So this FIFO is a standard (registered-output) FIFO — after rd_en, the data is valid on the next clock edge. A FIFO that presents the head word combinationally the same cycle (zero read latency) is a first-word-fall-through (FWFT) FIFO — a variant covered in 7.5. Getting the read latency wrong (treating a registered read as combinational) is a classic bug; §6 lists it.

Synchronous FIFO — RAM + two wrapping pointers + occupancy gating

data flow
Synchronous FIFO — RAM + two wrapping pointers + occupancy gatingwrite sidewr_en, wdata, full (back-pressure)wptr (counter)next write slot; ++ when wr_en & !fullSDP RAM (mem)DEPTH x WIDTH; write @wptr, read @rptrrptr (counter)next read slot; ++ when rd_en & !emptyread siderd_en, rdata (1-cyc latency), emptyoccupancy countempty = count==0, full = count==DEPTH
The write side drives the write pointer, which addresses the RAM write port; the read pointer addresses the read port and drives the read side. Both pointers are wrapping counters modulo DEPTH. The occupancy count trails the difference of the two pointers and produces full and empty, which gate the two sides so the write hand never laps the read hand and the read hand never catches the write hand. The data lives in the RAM and never moves — only the pointers advance. This structure is identical in SystemVerilog, Verilog, and VHDL; only the syntax of the RAM and the counters differs.

Two anatomy details close the picture. First, DEPTH is assumed a power of two here, because then the pointer wrap is automatic (an AW-bit counter rolls DEPTH-1 → 0 for free) and AW = log2(DEPTH) is exact. A non-power-of-two DEPTH makes the wrap awkward — the counter must be explicitly reset to zero at DEPTH-1 instead of rolling over, and the pointer-comparison full/empty trick no longer lines up on a bit boundary; that awkwardness (and how to size a FIFO's depth in the first place) is 7.4. Second, the occupancy count is one extra register that the pointer-based scheme of 7.2 avoids — 7.2 derives full/empty from the pointers alone (with one extra pointer MSB), trading a small comparator for the counter. Here we pay the counter to keep the correctness obvious; that trade-off is the bridge into 7.2.

4. Real RTL Implementation

This is the core of the page. We build the pattern in three steps — (a) the wrapping pointer (the counter atom the FIFO is built from), (b) the full synchronous FIFO (SDP RAM + wptr/rptr + occupancy-based full/empty gating, WIDTH & DEPTH generic), and (c) the write-when-full / read-when-empty overrun bug (buggy vs fixed) — and each is shown in SystemVerilog, Verilog, and VHDL, with an RTL block and a self-checking clocked testbench. The idea is identical in all three; the syntax differs, and seeing them side by side is what makes the pattern language-independent in your head.

Two disciplines run through every RTL block. Every sequential update is guarded — a pointer advances only on a real operation (write requested and not full; read requested and not empty), never unconditionally. And the reset is explicit — synchronous, active-high in these examples — clearing both pointers and the occupancy count to zero so the FIFO powers up empty. Miss either and you get §4c's overrun.

4a. The wrapping pointer — the counter atom the FIFO is built from

Start with the atom the FIFO reuses twice. A FIFO pointer is a counter (3.1) that advances by one when its operation fires and wraps at DEPTH. For a power-of-two DEPTH, wrapping is free: an AW-bit register rolls DEPTH-1 → 0 on its own. The pointer is AW = log2(DEPTH) bits wide — deliberately exactly wide enough to index the RAM.

fifo_ptr.sv — a wrapping FIFO pointer (guarded counter, power-of-two DEPTH)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module fifo_ptr #(
       parameter int DEPTH = 16,
       localparam int AW    = $clog2(DEPTH)   // pointer indexes DEPTH slots exactly
   )(
       input  logic          clk,
       input  logic          rst,            // synchronous, active-high
       input  logic          adv,            // advance ONLY when the op really fires
       output logic [AW-1:0] ptr             // current slot (next write or read)
   );
       // The pointer advances by one when its operation (a gated write, or a gated
       // read) happens. For a power-of-two DEPTH the AW-bit register wraps for free:
       // ptr goes DEPTH-1 -> 0 automatically. That automatic wrap is what makes the
       // buffer circular. A non-power-of-two DEPTH would need an explicit reset here.
       always_ff @(posedge clk) begin
           if (rst)      ptr <= '0;
           else if (adv) ptr <= ptr + 1'b1;  // wraps at DEPTH by rollover
       end
   endmodule

The full FIFO instantiates this twice — once for wptr, once for rptr — but for clarity the FIFO below writes the pointer logic inline. The point of showing the atom first is that both pointers are the same guarded counter, differing only in the guard: wptr's guard is "a write fired," rptr's is "a read fired."

4b. The full synchronous FIFO — RAM + pointers + occupancy gating

Now the whole pattern, WIDTH and DEPTH generic. The storage is an SDP RAM (mem), the two pointers are inline guarded counters, and an occupancy count derives full/empty. The two gate expressions — do_write and do_read — are the safety of the whole design: a write happens only when wr_en && !full, a read only when rd_en && !empty.

sync_fifo.sv — synchronous FIFO (SDP RAM + wptr/rptr + occupancy full/empty)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module sync_fifo #(
       parameter int WIDTH = 8,
       parameter int DEPTH = 16,                     // power of two here (auto wrap)
       localparam int AW    = $clog2(DEPTH)
   )(
       input  logic             clk,
       input  logic             rst,                 // synchronous, active-high
       input  logic             wr_en,               // push request
       input  logic [WIDTH-1:0] wdata,
       output logic             full,                // back-pressure to producer
       input  logic             rd_en,               // pop request
       output logic [WIDTH-1:0] rdata,               // valid one cycle after rd fires
       output logic             empty                // stall to consumer
   );
       logic [WIDTH-1:0] mem [DEPTH];                // simple-dual-port storage
       logic [AW-1:0]    wptr, rptr;                 // wrapping write/read pointers
       logic [AW:0]      count;                      // occupancy: 0..DEPTH (AW+1 bits)
 
       // Flags are pure functions of occupancy — obviously correct (pointer-based
       // derivation is the flagship 7.2). count needs AW+1 bits to represent DEPTH.
       assign full  = (count == DEPTH[AW:0]);
       assign empty = (count == '0);
 
       // A write/read HAPPENS only when requested AND allowed. This gate is the
       // whole safety story: no write when full, no read when empty.
       wire do_write = wr_en & ~full;
       wire do_read  = rd_en & ~empty;
 
       always_ff @(posedge clk) begin
           if (rst) begin
               wptr  <= '0;
               rptr  <= '0;
               count <= '0;
           end else begin
               // WRITE PORT: store at wptr, advance wptr (wraps at DEPTH).
               if (do_write) begin
                   mem[wptr] <= wdata;
                   wptr      <= wptr + 1'b1;
               end
               // READ PORT: registered read — rdata is valid NEXT cycle (6.2).
               if (do_read) begin
                   rdata <= mem[rptr];
                   rptr  <= rptr + 1'b1;
               end
               // OCCUPANCY: +1 on write-only, -1 on read-only, unchanged on both.
               case ({do_write, do_read})
                   2'b10:   count <= count + 1'b1;
                   2'b01:   count <= count - 1'b1;
                   default: count <= count;          // 2'b11 (both) and 2'b00 (idle)
               endcase
           end
       end
   endmodule

The count update is the crux: it changes only on an asymmetric cycle (write without read, or read without write), and holds on a simultaneous read+write — because pushing and popping in the same cycle leaves the occupancy unchanged. That single case is why full and empty are always right. The SystemVerilog testbench is clocked: it writes a known ascending stream, reads it back, and asserts each popped word equals what was pushed in order, while also asserting the FIFO never accepts a write when full or returns a read when empty.

sync_fifo_tb.sv — clocked self-check: write a stream, read it back IN ORDER
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module sync_fifo_tb;
       localparam int WIDTH = 8, DEPTH = 8;
       logic clk = 0, rst = 1, wr_en = 0, rd_en = 0;
       logic [WIDTH-1:0] wdata, rdata;
       logic full, empty;
       int errors = 0;
 
       sync_fifo #(.WIDTH(WIDTH), .DEPTH(DEPTH)) dut (
           .clk(clk), .rst(rst), .wr_en(wr_en), .wdata(wdata), .full(full),
           .rd_en(rd_en), .rdata(rdata), .empty(empty));
 
       always #5 clk = ~clk;                          // 100 MHz
 
       // Reference model: a scoreboard queue of the values we pushed, in order.
       logic [WIDTH-1:0] model [$];
 
       // A gated push: only if not full. Records into the model on success.
       task automatic push(input logic [WIDTH-1:0] d);
           @(negedge clk);
           if (!full) begin wr_en = 1; wdata = d; model.push_back(d); end
           @(negedge clk); wr_en = 0;
       endtask
 
       // A gated pop: only if not empty. Checks rdata (valid next cycle) vs model.
       task automatic pop();
           logic [WIDTH-1:0] exp;
           @(negedge clk);
           if (!empty) begin
               rd_en = 1; exp = model.pop_front();
               @(negedge clk); rd_en = 0;
               @(negedge clk);                        // registered read: data next cyc
               assert (rdata === exp)
                   else begin $error("ORDER: got %h exp %h", rdata, exp); errors++; end
           end else @(negedge clk);
       endtask
 
       initial begin
           @(negedge clk); rst = 0;
           // 1) FILL: push 0..DEPTH-1, then assert full and that an extra push is refused.
           for (int i = 0; i < DEPTH; i++) push(i[WIDTH-1:0]);
           assert (full) else begin $error("expected full after DEPTH writes"); errors++; end
           push(8'hFF);                               // must be refused (gated) -> no overrun
           // 2) DRAIN: pop everything, checking order; then assert empty.
           for (int i = 0; i < DEPTH; i++) pop();
           assert (empty) else begin $error("expected empty after draining"); errors++; end
           // 3) INTERLEAVE: mix writes and reads, order must still hold.
           for (int i = 0; i < 20; i++) begin push(i[WIDTH-1:0]); pop(); end
           if (errors == 0) $display("PASS: FIFO preserved order, no overrun/underrun");
           else             $display("FAIL: %0d errors", errors);
           $finish;
       end
   endmodule

The Verilog form is identical in intent; the differences are reg/wire typing, a memory array mem, and $display("FAIL") instead of assert. The gating (do_write, do_read) and the occupancy count are the same. Note the pointer widths are deliberate — AW bits for the pointers, AW+1 for count so it can hold the value DEPTH.

sync_fifo.v — synchronous FIFO in Verilog (memory array + gated pointers)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module sync_fifo #(
       parameter WIDTH = 8,
       parameter DEPTH = 16,                          // power of two
       parameter AW    = 4                            // = log2(DEPTH); set by instantiator
   )(
       input  wire             clk,
       input  wire             rst,                   // synchronous, active-high
       input  wire             wr_en,
       input  wire [WIDTH-1:0] wdata,
       output wire             full,
       input  wire             rd_en,
       output reg  [WIDTH-1:0] rdata,
       output wire             empty
   );
       reg [WIDTH-1:0] mem [0:DEPTH-1];               // SDP storage
       reg [AW-1:0]    wptr, rptr;                    // wrapping pointers
       reg [AW:0]      count;                         // occupancy 0..DEPTH
 
       assign full  = (count == DEPTH[AW:0]);
       assign empty = (count == {(AW+1){1'b0}});
 
       wire do_write = wr_en & ~full;                 // gated: no write when full
       wire do_read  = rd_en & ~empty;                // gated: no read when empty
 
       always @(posedge clk) begin
           if (rst) begin
               wptr  <= {AW{1'b0}};
               rptr  <= {AW{1'b0}};
               count <= {(AW+1){1'b0}};
           end else begin
               if (do_write) begin
                   mem[wptr] <= wdata;                // write @ wptr
                   wptr      <= wptr + 1'b1;           // wraps at DEPTH by rollover
               end
               if (do_read) begin
                   rdata <= mem[rptr];                // registered read: valid next cyc
                   rptr  <= rptr + 1'b1;
               end
               case ({do_write, do_read})
                   2'b10:   count <= count + 1'b1;    // write only  -> +1
                   2'b01:   count <= count - 1'b1;    // read only   -> -1
                   default: count <= count;           // both / idle -> unchanged
               endcase
           end
       end
   endmodule
sync_fifo_tb.v — self-checking Verilog testbench (write stream, read IN ORDER)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module sync_fifo_tb;
       parameter WIDTH = 8, DEPTH = 8, AW = 3;
       reg               clk, rst, wr_en, rd_en;
       reg  [WIDTH-1:0]  wdata;
       wire [WIDTH-1:0]  rdata;
       wire              full, empty;
       integer           i, errors;
 
       sync_fifo #(.WIDTH(WIDTH), .DEPTH(DEPTH), .AW(AW)) dut (
           .clk(clk), .rst(rst), .wr_en(wr_en), .wdata(wdata), .full(full),
           .rd_en(rd_en), .rdata(rdata), .empty(empty));
 
       always #5 clk = ~clk;
 
       // Reference model: a simple in-order queue of expected values.
       reg  [WIDTH-1:0] model [0:255];
       integer          head, tail;                  // model queue pointers
 
       initial begin
           clk = 0; rst = 1; wr_en = 0; rd_en = 0; wdata = 0;
           errors = 0; head = 0; tail = 0;
           @(negedge clk); rst = 0;
 
           // 1) FILL: push 0..DEPTH-1. Each push guarded by !full (no overrun).
           for (i = 0; i < DEPTH; i = i + 1) begin
               @(negedge clk);
               if (!full) begin wr_en = 1; wdata = i; model[tail] = i; tail = tail + 1; end
               @(negedge clk); wr_en = 0;
           end
           if (!full) begin $display("FAIL: expected full after DEPTH writes"); errors = errors + 1; end
 
           // Extra push while full MUST be refused -> occupancy unchanged, no overwrite.
           @(negedge clk); wr_en = 1; wdata = 8'hFF; @(negedge clk); wr_en = 0;
 
           // 2) DRAIN: pop all, check order (registered read: data one cycle later).
           for (i = 0; i < DEPTH; i = i + 1) begin
               @(negedge clk);
               if (!empty) begin
                   rd_en = 1;
                   @(negedge clk); rd_en = 0;
                   @(negedge clk);
                   if (rdata !== model[head]) begin
                       $display("FAIL ORDER: got %h exp %h", rdata, model[head]);
                       errors = errors + 1;
                   end
                   head = head + 1;
               end
           end
           if (!empty) begin $display("FAIL: expected empty after draining"); errors = errors + 1; end
 
           if (errors == 0) $display("PASS: FIFO preserved order, no overrun/underrun");
           else             $display("FAIL: %0d errors", errors);
           $finish;
       end
   endmodule

VHDL expresses the same FIFO with numeric_std: mem is an array of std_logic_vector, the pointers are unsigned that wrap by rollover, and count is an unsigned wide enough for DEPTH. The gating (do_write, do_read) and the occupancy update are the exact counterparts of the SV/Verilog logic. full and empty are concurrent assignments off count.

sync_fifo.vhd — synchronous FIFO in VHDL (numeric_std, gated pointers)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity sync_fifo is
       generic (
           WIDTH : positive := 8;
           DEPTH : positive := 16;                    -- power of two
           AW    : positive := 4                      -- = log2(DEPTH)
       );
       port (
           clk   : in  std_logic;
           rst   : in  std_logic;                     -- synchronous, active-high
           wr_en : in  std_logic;
           wdata : in  std_logic_vector(WIDTH-1 downto 0);
           full  : out std_logic;
           rd_en : in  std_logic;
           rdata : out std_logic_vector(WIDTH-1 downto 0);
           empty : out std_logic
       );
   end entity;
 
   architecture rtl of sync_fifo is
       type mem_t is array (0 to DEPTH-1) of std_logic_vector(WIDTH-1 downto 0);
       signal mem      : mem_t;                                   -- SDP storage
       signal wptr     : unsigned(AW-1 downto 0) := (others => '0');
       signal rptr     : unsigned(AW-1 downto 0) := (others => '0');
       signal count    : unsigned(AW downto 0)   := (others => '0');  -- 0..DEPTH
       signal full_i   : std_logic;
       signal empty_i  : std_logic;
       signal do_write : std_logic;
       signal do_read  : std_logic;
   begin
       -- Flags are pure functions of occupancy (pointer-based derivation is 7.2).
       full_i  <= '1' when count = to_unsigned(DEPTH, count'length) else '0';
       empty_i <= '1' when count = 0 else '0';
       full    <= full_i;
       empty   <= empty_i;
 
       -- The gate: a write only if not full, a read only if not empty.
       do_write <= wr_en and not full_i;
       do_read  <= rd_en and not empty_i;
 
       process (clk)
       begin
           if rising_edge(clk) then
               if rst = '1' then
                   wptr  <= (others => '0');
                   rptr  <= (others => '0');
                   count <= (others => '0');
               else
                   if do_write = '1' then
                       mem(to_integer(wptr)) <= wdata;            -- write @ wptr
                       wptr <= wptr + 1;                          -- wraps at DEPTH
                   end if;
                   if do_read = '1' then
                       rdata <= mem(to_integer(rptr));            -- registered read
                       rptr  <= rptr + 1;
                   end if;
                   -- occupancy: +1 write-only, -1 read-only, unchanged on both/idle
                   if (do_write = '1') and (do_read = '0') then
                       count <= count + 1;
                   elsif (do_write = '0') and (do_read = '1') then
                       count <= count - 1;
                   end if;
               end if;
           end if;
       end process;
   end architecture;
sync_fifo_tb.vhd — self-checking VHDL testbench (assert order + no overrun/underrun)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity sync_fifo_tb is
   end entity;
 
   architecture sim of sync_fifo_tb is
       constant WIDTH : positive := 8;
       constant DEPTH : positive := 8;
       constant AW    : positive := 3;
       signal clk   : std_logic := '0';
       signal rst   : std_logic := '1';
       signal wr_en : std_logic := '0';
       signal rd_en : std_logic := '0';
       signal wdata : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
       signal rdata : std_logic_vector(WIDTH-1 downto 0);
       signal full  : std_logic;
       signal empty : std_logic;
   begin
       dut : entity work.sync_fifo
           generic map (WIDTH => WIDTH, DEPTH => DEPTH, AW => AW)
           port map (clk => clk, rst => rst, wr_en => wr_en, wdata => wdata,
                     full => full, rd_en => rd_en, rdata => rdata, empty => empty);
 
       clk <= not clk after 5 ns;                                 -- free-running clock
 
       stim : process
           variable exp : integer;
       begin
           wait until falling_edge(clk); rst <= '0';
 
           -- 1) FILL: push 0..DEPTH-1, each guarded by not full (no overrun).
           for i in 0 to DEPTH-1 loop
               wait until falling_edge(clk);
               if full = '0' then
                   wr_en <= '1';
                   wdata <= std_logic_vector(to_unsigned(i, WIDTH));
               end if;
               wait until falling_edge(clk);
               wr_en <= '0';
           end loop;
           assert full = '1' report "expected full after DEPTH writes" severity error;
 
           -- extra push while full MUST be refused (gated) -> no overwrite
           wait until falling_edge(clk); wr_en <= '1'; wdata <= x"FF";
           wait until falling_edge(clk); wr_en <= '0';
 
           -- 2) DRAIN: pop all, check IN-ORDER (registered read: data one cycle later)
           for i in 0 to DEPTH-1 loop
               wait until falling_edge(clk);
               if empty = '0' then
                   rd_en <= '1';
                   wait until falling_edge(clk); rd_en <= '0';
                   wait until falling_edge(clk);
                   exp := i;
                   assert rdata = std_logic_vector(to_unsigned(exp, WIDTH))
                       report "ORDER mismatch: rdata /= expected pushed value" severity error;
               end if;
           end loop;
           assert empty = '1' report "expected empty after draining" severity error;
 
           report "sync_fifo self-check complete (order + no overrun/underrun)" severity note;
           wait;
       end process;
   end architecture;

4c. The write-when-full / read-when-empty overrun — buggy vs fixed, in all three HDLs

The signature FIFO failure, shown as buggy vs fixed RTL in each language, then dramatized in §7. The bug is the same everywhere: the control advances the pointer and touches the RAM unconditionally, without gating on full/empty. Under a sustained burst the write laps the read and silently overwrites unread data; a read past empty returns a stale/garbage word. The fix is one gate: advance wptr/write only when NOT full, advance rptr/read only when NOT empty.

fifo_bug.sv — BUGGY (ungated) vs FIXED (gate on full/empty)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: wptr/rptr advance on wr_en/rd_en with NO full/empty check. A burst that
   //        outruns the reader laps the read pointer -> overwrites unread data
   //        (silent loss). A read while empty returns whatever stale mem[rptr] holds.
   module fifo_ctrl_bad #(parameter AW = 4)(
       input  logic          clk, rst, wr_en, rd_en,
       output logic [AW-1:0] wptr, rptr
   );
       always_ff @(posedge clk) begin
           if (rst) begin wptr <= '0; rptr <= '0; end
           else begin
               if (wr_en) wptr <= wptr + 1'b1;   // NO !full gate -> overrun on burst
               if (rd_en) rptr <= rptr + 1'b1;   // NO !empty gate -> underrun (garbage)
           end
       end
   endmodule
 
   // FIXED: gate each pointer on the corresponding flag. wptr advances only on a
   //        real (accepted) write; rptr only on a real (served) read. Now the write
   //        hand can never lap the read hand, and the read hand never passes it.
   module fifo_ctrl_good #(parameter AW = 4)(
       input  logic          clk, rst, wr_en, rd_en, full, empty,
       output logic [AW-1:0] wptr, rptr
   );
       wire do_write = wr_en & ~full;            // the gate is the whole fix
       wire do_read  = rd_en & ~empty;
       always_ff @(posedge clk) begin
           if (rst) begin wptr <= '0; rptr <= '0; end
           else begin
               if (do_write) wptr <= wptr + 1'b1;
               if (do_read)  rptr <= rptr + 1'b1;
           end
       end
   endmodule
fifo_bug_tb.sv — burst past full: the good FIFO refuses, the bad one overruns
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module fifo_bug_tb;
       localparam int WIDTH = 8, DEPTH = 4, AW = 2;
       logic clk = 0, rst = 1, wr_en = 0, rd_en = 0;
       logic [WIDTH-1:0] wdata, rdata;
       logic full, empty;
       int errors = 0;
 
       // Full FIFO with correct gating (uses the fixed control internally).
       sync_fifo #(.WIDTH(WIDTH), .DEPTH(DEPTH)) dut (
           .clk(clk), .rst(rst), .wr_en(wr_en), .wdata(wdata), .full(full),
           .rd_en(rd_en), .rdata(rdata), .empty(empty));
 
       always #5 clk = ~clk;
 
       initial begin
           @(negedge clk); rst = 0;
           // Drive a burst LONGER than DEPTH with the reader idle. A correct FIFO
           // asserts full and REFUSES the surplus (no overrun). An ungated FIFO would
           // keep advancing wptr and overwrite entry 0,1,... losing the first words.
           for (int i = 0; i < DEPTH + 4; i++) begin
               @(negedge clk);
               wr_en = 1; wdata = i[WIDTH-1:0];
               @(negedge clk); wr_en = 0;
               // INVARIANT: occupancy can never exceed DEPTH -> full must hold once reached.
               if (i >= DEPTH - 1)
                   assert (full) else begin $error("overrun risk: not full at i=%0d", i); errors++; end
           end
           // Now pop: the FIRST word out must be 0 (oldest), proving the burst did NOT
           // overwrite the head. An overrun would have replaced 0 with a later value.
           @(negedge clk); rd_en = 1; @(negedge clk); rd_en = 0; @(negedge clk);
           assert (rdata === 8'd0)
               else begin $error("head corrupted: got %h exp 00 (overrun)", rdata); errors++; end
           if (errors == 0) $display("PASS: gated FIFO refused surplus, head intact (no overrun)");
           else             $display("FAIL: %0d errors (overrun/underrun)", errors);
           $finish;
       end
   endmodule

The Verilog buggy/fixed pair is the same story with reg pointers and always @(posedge clk). The whole difference is the presence or absence of the ~full/~empty guard on the pointer increment.

fifo_bug.v — BUGGY (ungated) vs FIXED (gated) in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: pointers advance on wr_en/rd_en with no flag check -> overrun/underrun.
   module fifo_ctrl_bad #(parameter AW = 4)(
       input  wire          clk, rst, wr_en, rd_en,
       output reg  [AW-1:0] wptr, rptr
   );
       always @(posedge clk) begin
           if (rst) begin wptr <= {AW{1'b0}}; rptr <= {AW{1'b0}}; end
           else begin
               if (wr_en) wptr <= wptr + 1'b1;   // no !full  -> overwrites unread data
               if (rd_en) rptr <= rptr + 1'b1;   // no !empty -> reads garbage
           end
       end
   endmodule
 
   // FIXED: gate each pointer on its flag so it advances only on an accepted op.
   module fifo_ctrl_good #(parameter AW = 4)(
       input  wire          clk, rst, wr_en, rd_en, full, empty,
       output reg  [AW-1:0] wptr, rptr
   );
       wire do_write = wr_en & ~full;
       wire do_read  = rd_en & ~empty;
       always @(posedge clk) begin
           if (rst) begin wptr <= {AW{1'b0}}; rptr <= {AW{1'b0}}; end
           else begin
               if (do_write) wptr <= wptr + 1'b1;
               if (do_read)  rptr <= rptr + 1'b1;
           end
       end
   endmodule
fifo_bug_tb.v — self-checking: burst past full, head must survive (no overrun)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module fifo_bug_tb;
       parameter WIDTH = 8, DEPTH = 4, AW = 2;
       reg               clk, rst, wr_en, rd_en;
       reg  [WIDTH-1:0]  wdata;
       wire [WIDTH-1:0]  rdata;
       wire              full, empty;
       integer           i, errors;
 
       // FIFO with correct gating inside.
       sync_fifo #(.WIDTH(WIDTH), .DEPTH(DEPTH), .AW(AW)) dut (
           .clk(clk), .rst(rst), .wr_en(wr_en), .wdata(wdata), .full(full),
           .rd_en(rd_en), .rdata(rdata), .empty(empty));
 
       always #5 clk = ~clk;
 
       initial begin
           clk = 0; rst = 1; wr_en = 0; rd_en = 0; wdata = 0; errors = 0;
           @(negedge clk); rst = 0;
           // Burst longer than DEPTH, reader idle. Correct FIFO asserts full and
           // refuses the surplus. Ungated would keep writing and overwrite the head.
           for (i = 0; i < DEPTH + 4; i = i + 1) begin
               @(negedge clk); wr_en = 1; wdata = i;
               @(negedge clk); wr_en = 0;
               if (i >= DEPTH - 1 && !full) begin
                   $display("FAIL: not full at i=%0d (overrun risk)", i);
                   errors = errors + 1;
               end
           end
           // First word out must be 0 (oldest) -> head not overwritten by the burst.
           @(negedge clk); rd_en = 1; @(negedge clk); rd_en = 0; @(negedge clk);
           if (rdata !== 8'd0) begin
               $display("FAIL: head corrupted got %h exp 00 (overrun)", rdata);
               errors = errors + 1;
           end
           if (errors == 0) $display("PASS: gated FIFO refused surplus, head intact");
           else             $display("FAIL: %0d errors", errors);
           $finish;
       end
   endmodule

In VHDL the same bug is an unconditional wptr <= wptr + 1 / rptr <= rptr + 1 inside the clocked process; the fix guards each with do_write/do_read. The full/empty inputs to the fixed control are the flags the surrounding FIFO computes from occupancy.

fifo_bug.vhd — BUGGY (ungated) vs FIXED (gated) in VHDL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   -- BUGGY: pointers advance on wr_en/rd_en with no full/empty gate -> overrun/underrun.
   entity fifo_ctrl_bad is
       generic ( AW : positive := 4 );
       port (
           clk, rst, wr_en, rd_en : in  std_logic;
           wptr, rptr             : out std_logic_vector(AW-1 downto 0)
       );
   end entity;
   architecture rtl of fifo_ctrl_bad is
       signal w, r : unsigned(AW-1 downto 0) := (others => '0');
   begin
       process (clk) begin
           if rising_edge(clk) then
               if rst = '1' then
                   w <= (others => '0'); r <= (others => '0');
               else
                   if wr_en = '1' then w <= w + 1; end if;   -- no not-full  -> overwrite
                   if rd_en = '1' then r <= r + 1; end if;   -- no not-empty -> garbage
               end if;
           end if;
       end process;
       wptr <= std_logic_vector(w);
       rptr <= std_logic_vector(r);
   end architecture;
 
   -- FIXED: gate each pointer on its flag so it advances only on an accepted op.
   entity fifo_ctrl_good is
       generic ( AW : positive := 4 );
       port (
           clk, rst, wr_en, rd_en, full, empty : in  std_logic;
           wptr, rptr                          : out std_logic_vector(AW-1 downto 0)
       );
   end entity;
   architecture rtl of fifo_ctrl_good is
       signal w, r : unsigned(AW-1 downto 0) := (others => '0');
       signal do_write, do_read : std_logic;
   begin
       do_write <= wr_en and not full;             -- the gate is the whole fix
       do_read  <= rd_en and not empty;
       process (clk) begin
           if rising_edge(clk) then
               if rst = '1' then
                   w <= (others => '0'); r <= (others => '0');
               else
                   if do_write = '1' then w <= w + 1; end if;
                   if do_read  = '1' then r <= r + 1; end if;
               end if;
           end if;
       end process;
       wptr <= std_logic_vector(w);
       rptr <= std_logic_vector(r);
   end architecture;
fifo_bug_tb.vhd — self-checking: burst past full, head survives (assert severity)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity fifo_bug_tb is
   end entity;
 
   architecture sim of fifo_bug_tb is
       constant WIDTH : positive := 8;
       constant DEPTH : positive := 4;
       constant AW    : positive := 2;
       signal clk   : std_logic := '0';
       signal rst   : std_logic := '1';
       signal wr_en : std_logic := '0';
       signal rd_en : std_logic := '0';
       signal wdata : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
       signal rdata : std_logic_vector(WIDTH-1 downto 0);
       signal full  : std_logic;
       signal empty : std_logic;
   begin
       -- FIFO with correct gating inside; the buggy control would let the burst overrun.
       dut : entity work.sync_fifo
           generic map (WIDTH => WIDTH, DEPTH => DEPTH, AW => AW)
           port map (clk => clk, rst => rst, wr_en => wr_en, wdata => wdata,
                     full => full, rd_en => rd_en, rdata => rdata, empty => empty);
 
       clk <= not clk after 5 ns;
 
       stim : process
       begin
           wait until falling_edge(clk); rst <= '0';
           -- Burst longer than DEPTH with reader idle: correct FIFO asserts full and
           -- refuses surplus; ungated would overwrite the head (loss).
           for i in 0 to DEPTH + 3 loop
               wait until falling_edge(clk);
               wr_en <= '1';
               wdata <= std_logic_vector(to_unsigned(i, WIDTH));
               wait until falling_edge(clk);
               wr_en <= '0';
               if i >= DEPTH - 1 then
                   assert full = '1'
                       report "overrun risk: not full when it should be" severity error;
               end if;
           end loop;
           -- First word out must be 0 (oldest) -> head not overwritten.
           wait until falling_edge(clk); rd_en <= '1';
           wait until falling_edge(clk); rd_en <= '0';
           wait until falling_edge(clk);
           assert rdata = std_logic_vector(to_unsigned(0, WIDTH))
               report "head corrupted (overrun): rdata /= 0" severity error;
           report "fifo_bug self-check complete (gated FIFO, head intact)" severity note;
           wait;
       end process;
   end architecture;

Across all three languages the lesson is one sentence: advance a FIFO pointer only on an accepted operation — a write gated by NOT full, a read gated by NOT empty — or the write hand laps the read hand and the stream is silently corrupted.

5. Verification Strategy

A FIFO is sequential, so its correctness is not a truth table but a stream property: whatever you write, you must read back in the same order, without loss or fabrication. Everything below makes that property checkable, and it maps onto the clocked testbenches in §4.

  • The ordering self-check (scoreboard). The core testbench keeps a reference queue (a model of the FIFO's contents): every accepted write pushes its value into the model; every served read pops the model's front and asserts rdata equals it. Because the model is a plain in-order queue, this proves First-In-First-Out directly — a reorder, a duplicated word, or a dropped word all show up as a mismatch. The three §4 testbenches do exactly this: SV assert (rdata === exp), Verilog if (rdata !== model[head]) $display("FAIL…"), VHDL assert rdata = expected report … severity error.
  • The invariants that must always hold. Stated in plain engineering terms: (i) occupancy is always in [0, DEPTH] — it never goes negative (underrun) and never exceeds DEPTH (overrun); (ii) full and empty are never both true (and for a non-empty FIFO, not neither-nor incorrectly); (iii) a write is accepted only when NOT full and a read served only when NOT empty; (iv) the count equals the number of accepted-but-unread words. If any of these ever breaks, the FIFO is corrupt — these are the properties a formal or assertion-based check would encode.
  • Corner cases to exercise. Drive the boundaries explicitly: fill to exactly full and confirm the next write is refused (no overrun — the §4c test); drain to exactly empty and confirm the next read is refused (no underrun); simultaneous read+write on a partially-full FIFO (occupancy must stay the same — the 2'b11 arm of the count case); write and read on the same cycle when empty (the read must be refused, the write accepted); back-to-back bursts; and reset mid-operation (both pointers and count return to zero, FIFO reads empty). Single-entry (DEPTH small) and a wrap-around (push/pop enough to make a pointer roll past DEPTH-1) must both be hit.
  • Failure modes and how the TB catches them. The signature failure is the §7 overrun: an ungated write overwrites the head, so after a burst the first word read is not the oldest word written. The scoreboard catches it because the popped value will not match the model's front. Underrun (reading past empty) shows up as an extra pop the model cannot satisfy, or a stale rdata. A wrong read-latency assumption (treating the registered read as combinational) shows up as an off-by-one-cycle sample of rdata — the TB must sample one cycle after rd_en, exactly as §4 does.
  • Parameter-generic verification. A FIFO is naturally configurable, so it must be verified generic: re-run the fill/drain/interleave suite for several WIDTH (1, 8, 32) and DEPTH (2, 4, 16 — powers of two here). A FIFO that passes at DEPTH=8 can still be wrong at DEPTH=2 if the count width or the full/empty comparison was sloppy.
  • Expected waveform. On a correct run: wr_en with full low advances wptr on the clock edge and bumps count; rd_en with empty low advances rptr and drops count, with rdata valid one cycle later (registered read). As the FIFO fills, count climbs to DEPTH and full asserts; as it drains, count falls to 0 and empty asserts. A wptr that keeps advancing after full is asserted is the visual signature of the §7 overrun.

6. Common Mistakes

Writing when full or reading when empty (ungated control). The signature FIFO bug and the whole point of the pattern. If the control advances wptr and writes the RAM on wr_en alone — no full check — a sustained burst laps the read pointer and overwrites unread data: silent loss, and the reader later gets the wrong words. If it advances rptr on rd_en alone — no empty check — a read past empty returns a stale/garbage word that was never written this pass. The fix is the gate: do_write = wr_en && !full, do_read = rd_en && !empty. (The exact full/empty boundary — the edge cases where the pointers are equal — is the flagship 7.2; here occupancy makes the gate obviously correct.)

The pointer not wrapping at DEPTH. A FIFO pointer must be modulo DEPTH. For a power-of-two DEPTH an AW-bit counter wraps for free, but if you accidentally make the pointer wider than AW (or use a non-power-of-two DEPTH without an explicit wrap), the pointer runs past the RAM's address range and either indexes out of bounds or stops being circular. Keep the pointer exactly AW = log2(DEPTH) bits, and for a non-power-of-two DEPTH reset it to zero explicitly at DEPTH-1 (previewed here, owned by 7.4).

Forgetting the RAM read latency. A synchronous RAM read is registered: rdata for rptr appears on the next clock edge, not the same cycle. Treating it as combinational — sampling rdata the same cycle you assert rd_en, or feeding it straight into logic that expects it now — reads the previous head or an undefined value. Either account for the one-cycle latency (this standard FIFO) or build a first-word-fall-through FIFO that presents the head combinationally (7.5). Getting this wrong is an off-by-one-cycle bug that a naive testbench misses.

Mixing up wptr and rptr. They are symmetric enough to swap by accident: writing at rptr, reading at wptr, or advancing the wrong one on an operation. The result is data written to where the reader is about to read (corruption) or a FIFO that never delivers what was written. Name them unmistakably and gate each with its own condition — wptr with the write gate, rptr with the read gate — and let the ordering scoreboard in verification catch a swap immediately.

Updating occupancy wrong on a simultaneous read+write. When a write and a read both happen in the same cycle, occupancy is unchanged — one word in, one word out. A count update that treats the two independently (e.g. count <= count + do_write - do_read is fine, but two separate if (do_write) count++; / if (do_read) count--; statements in the same block can race or double-count depending on style) can drift the count off by one and corrupt full/empty. Use a single, total update — the case ({do_write, do_read}) in §4 — so the both-at-once case is handled explicitly.

A DEPTH that is not a power of two. With a non-power-of-two DEPTH, the pointer wrap is no longer a free rollover — the counter must be explicitly reset to zero at DEPTH-1 — and the pointer-comparison full/empty scheme of 7.2 no longer lands on a clean bit boundary, so you must fall back to an occupancy count (as here) or handle the wrap by hand. Prefer a power-of-two DEPTH unless a hard sizing constraint forbids it; how to choose the depth for a given burst is 7.4.

7. DebugLab

The packet that came back scrambled — an ungated FIFO overran on a burst

The engineering lesson: a FIFO's entire job is to decouple a producer from a consumer safely — and "safely" means every write is gated by NOT full and every read by NOT empty, so a pointer advances only on an accepted operation, never on a bare request. The tell is corruption that depends on load and history ("long bursts scramble, short frames are fine"): a rate-mismatch failure that only appears when the buffer overflows is the fingerprint of a missing full/empty gate, not a bug in the producer or consumer. full and empty are not status LEDs — they are flow-control signals that must gate the datapath, and getting the boundary of that gate exactly right (the equal-pointer edge case) is the heart of FIFO design, which the flagship 7.2 makes exact.

8. Interview Q&A

9. Exercises

Design and reasoning exercises — no syntax drills. Each rehearses the "safe decoupling" habit.

Exercise 1 — Trace the pointers around the ring

A FIFO has DEPTH = 4 (so AW = 2), starts empty (wptr = rptr = 0, count = 0). Apply this per-cycle sequence of (wr_en, rd_en): W, W, W, R, W, W (nothing else). For each cycle, give the resulting wptr, rptr, count, full, and empty after the clock edge, remembering the write is gated on !full and the read on !empty. On which cycle (if any) is a write refused, and why? Note where a pointer wraps from 3 back to 0.

Exercise 2 — Occupancy count vs pointer comparison

This page derives full/empty from an occupancy count. The flagship 7.2 derives them by comparing wptr and rptr with an extra MSB instead. (i) State the one problem the occupancy count avoids that a naive pointer equality comparison runs into. (ii) Explain in one sentence how the extra pointer MSB resolves that problem. (iii) Name the resource trade-off between the two schemes (what the counter costs vs what the extra MSB + comparator costs). Which would you pick for a very wide, very deep FIFO, and why?

Exercise 3 — Break the FIFO on paper, then gate it

Take the ungated control of §4c (pointers advance on wr_en/rd_en with no flag check). For a DEPTH = 4 FIFO, describe a specific stimulus (a burst length and reader behaviour) that causes the write hand to lap the read hand, and state exactly which written word gets overwritten and which wrong word the reader will eventually see. Then do the symmetric exercise for the read side (reading past empty) and say what rdata returns. Finally, state the single-line fix for each side and why it makes the lapping impossible.

Exercise 4 — Standard vs first-word-fall-through

Your consumer is a control FSM that must decode the head word combinationally this cycle to choose a transition. (i) Explain why the standard registered-read FIFO of this page forces the FSM to insert a wait state, and how many cycles of latency the read costs. (ii) Describe at a block level how a first-word-fall-through FIFO would remove that wait (what it presents on rdata, and when). (iii) Name one cost FWFT adds. (Cross-reference: this is the bridge to 7.5.)

Continue in RTL Design Patterns (FIFO siblings and forward links unlock as they ship):

  • FIFO Full/Empty Logic — Chapter 7.2 (flagship); the exact pointer-based full/empty boundary — the extra-MSB wrap distinction this page deferred and replaced with an occupancy count.
  • FIFO Programmable Flags — Chapter 7.3; almost-full / almost-empty thresholds for early back-pressure, built on this occupancy count.
  • FIFO Depth Calculation — Chapter 7.4; how to size DEPTH for a given producer/consumer burst, and the non-power-of-two wrap this page previewed.
  • First-Word-Fall-Through FIFO — Chapter 7.5; the zero-read-latency variant of this standard registered-read FIFO.
  • FIFO Verification — Chapter 7.6; the scoreboard, invariants, and corner-case suite of §5, built out in full.
  • valid/ready Handshake — Chapter 9.1 (flagship); the generic back-pressure protocol that full/empty are a special case of.
  • Asynchronous FIFO — Chapter 11.6 (flagship); this same circular buffer across two clocks, with gray-coded pointers and synchronizers.

Backward / composes these patterns:

  • Dual-Port & Simple-Dual-Port RAM — Chapter 6.5; the SDP RAM that is this FIFO's storage — write one port, read the other.
  • Inferring RAM — Chapter 6.1; how the mem array maps onto a real block RAM instead of flops.
  • Memory Read Timing — Chapter 6.2; the registered-read latency that makes rdata valid one cycle after rd_en.
  • Up/Down & Enable Counters — Chapter 3.1; the guarded, wrapping counters the write and read pointers are built from.
  • Modulo-N Counters — Chapter 3.x; the modulo-DEPTH wrap that makes the pointers circular (and the non-power-of-two case).
  • FSM Fundamentals — Chapter 4.1; the control mindset behind gating writes on full and reads on empty.
  • The Register Pattern (D-FF) — Chapter 2.1; the clocked, reset, enable-gated update the pointers and count all use.
  • Comparators — Chapter 1.x; the equality compares (count == DEPTH, count == 0) that produce the flags.
  • Multiplexers (2:1, N:1, one-hot) — Chapter 1.1; the addressed RAM read is a mux over the memory, selected by the read pointer.

Verilog v1 prerequisites (the grammar this pattern transcribes into):

11. Summary

  • A FIFO decouples a producer from a consumer on one clock. They rarely run at the same instantaneous rate; the FIFO absorbs bursts and rate mismatches and hands data back in the order it arrived — turning an unsolvable rate-matching problem into a solvable capacity one.
  • A synchronous FIFO is a circular buffer — data still, pointers moving. Storage is a simple-dual-port RAM (6.5); a write pointer and a read pointer are wrapping counters (3.1) that chase each other around the ring, wrapping modulo DEPTH. The data never moves; only the pointers advance, so a deep FIFO costs a RAM and two small counters, not a conveyor of registers.
  • Full/empty gate the two sides, and getting the gate right is the whole game. Here full/empty come from an occupancy count (count == DEPTH / count == 0) that is obviously correct; the write is gated wr_en && !full, the read rd_en && !empty. The pointer-based derivation — equal pointers, the extra-MSB wrap distinction — is the flagship 7.2.
  • The signature failure is the ungated overrun. A write with no full check laps the read pointer and silently overwrites unread data; a read with no empty check returns garbage. Both are the same structural error — advancing a pointer on a request instead of an accepted operation — and both are load- and history-dependent, so they hide in light traffic (the §7 DebugLab).
  • Mind the read latency, and verify with a scoreboard. The registered RAM read makes rdata valid one cycle after rd_en (FWFT with zero latency is 7.5). Prove correctness by writing a known stream and reading it back in order against a reference queue, asserting occupancy stays in [0, DEPTH] and no write-when-full / read-when-empty ever slips through — self-checking clocked testbenches shown here in SystemVerilog, Verilog, and VHDL.