RTL Design Patterns · Chapter 7 · FIFO Design
First-Word-Fall-Through vs Standard
A FIFO has two read interfaces, and they disagree about when the output data is valid. In a standard FIFO the read is registered: you assert the read enable this cycle and the popped word appears on the output bus the next cycle. In a first-word-fall-through FIFO the head word is already sitting on the output the instant the FIFO is non-empty, and the read enable becomes an acknowledge that advances to the next word for zero read latency. Same storage underneath, a completely different read contract. Confuse the two and a consumer latches the wrong first word, so every word after it is off by one and the start of every packet is corrupt. This lesson draws both timings, builds a standard read and an FWFT wrapper around it, and proves each with self-checking testbenches, in SystemVerilog, Verilog, and VHDL.
Intermediate14 min readRTL Design PatternsFIFOFirst-Word Fall-ThroughRead LatencyShow-AheadValid Ready
Chapter 7 · Section 7.5 · FIFO Design
1. The Engineering Problem
You have a working FIFO from 7.1 and 7.2. The pointers wrap, the flags are correct, no word is ever overwritten or lost. You hand it to the engineer building the consumer — a block that drains the FIFO one word per cycle into a packet parser — and the first packet comes out corrupt. Not dropped, not duplicated: shifted. The header field holds what should have been in the previous transaction, every field is one position late, and the last word of the packet is missing. The FIFO is fine. The bug is that the consumer and the FIFO disagree about when dout is valid.
This is the read-side contract, and it is genuinely two different things wearing the same port list. Both interfaces expose the identical signals — clk, rd_en, dout, empty — and both drain the same circular buffer in the same order. What differs is a single question with two incompatible answers: at the moment you decide to take a word, is the word already on dout, or does asserting rd_en bring it there one cycle later?
- Standard (normal-latency) read.
doutis a registered read: you assertrd_enin cycle N, and the popped word appears ondoutin cycle N+1.rd_enis a request — "pop the head, and present it next cycle." There is exactly one cycle of read latency, anddoutis meaningless until that latency has elapsed. - First-word-fall-through (FWFT / show-ahead) read. The head word falls through to
douton its own: whenever the FIFO is non-empty,doutalready holds the oldest word, before anyrd_en.rd_enis now an acknowledge — "I have consumed the word you are showing me; advance so the next one falls through." There is zero read latency: the data is valid the cycleemptydeasserts.
A consumer must be written to exactly one of these contracts. Assume FWFT on a standard FIFO and you read the stale value the standard read has not yet produced — the classic off-by-one that shifts the whole stream. Assume standard on an FWFT FIFO and your rd_en pulse double-advances — you acknowledge a word you never sampled. The two are not "the same FIFO with a latency knob"; they are two contracts, and the producer and consumer must sign the same one.
// Both FIFOs expose the identical port list:
// input rd_en; output [W-1:0] dout; output empty;
// STANDARD (normal-latency): dout is REGISTERED. rd_en is a request.
// cycle N : rd_en = 1 (ask for the head)
// cycle N+1 : dout = <that head word> (arrives ONE cycle later)
// FWFT (show-ahead): dout FALLS THROUGH. rd_en is an acknowledge.
// whenever !empty : dout = <head word> (already there, BEFORE rd_en)
// cycle N : rd_en = 1 (ack: I took it, advance)
// cycle N+1 : dout = <NEXT head word> (next word already presented)
// The need: the consumer must know WHICH contract it is draining. Sampling
// dout the cycle empty drops is right for FWFT and one word too early for
// standard. That single disagreement is this page.2. Mental Model
3. Pattern Anatomy
The structure is a standard FIFO core with, in the FWFT case, a prefetch stage wrapped around its read port. Everything hard is in the timing of dout relative to rd_en and empty.
The standard read — registered dout, one-cycle latency. The core is the 7.1/7.2 FIFO. On a committed read (rd_en && !empty) the read pointer advances and the memory word at the old rptr is captured into an output register, so dout presents it on the next clock edge. dout is therefore a registered function of the pointer as it was one cycle ago: assert rd_en in cycle N, and the word lands in cycle N+1. This is the simplest internal form — one output flop, no look-ahead — and its empty means only "a pop is allowed," never "the data is on the bus now."
The FWFT prefetch — a look-ahead stage that keeps dout pre-loaded. FWFT wraps the standard core with a small output stage whose job is to always have the next word ready on dout before the consumer asks. Conceptually: whenever the output stage is not holding a valid word and the underlying standard FIFO is not empty, the stage issues an internal read to the core to prefetch the head word into dout. Once loaded, dout shows that word and the FWFT-level empty deasserts (equivalently a valid asserts). When the consumer pulses rd_en (the acknowledge), the stage marks its held word consumed and immediately prefetches the next word from the core, so dout rolls forward with no bubble. The result is zero read latency: the first word has fallen through to dout and is valid the cycle FWFT-empty drops.
The empty semantics differ, and that is the crux. In the standard FIFO, empty describes the core: empty low means the buffer holds a word you may pop (next cycle). In the FWFT FIFO, empty describes the output stage: empty low means a valid head word is presented on dout this cycle. FWFT empty is really a data-valid flag; standard empty is a pop-permitted flag. The consumer's read of the very first word hangs entirely on this: with FWFT it samples dout the cycle empty drops; with standard it must assert rd_en and sample dout the cycle after.
Two read interfaces — standard registers dout, FWFT prefetches it ahead of demand
data flowWhy FWFT suits handshakes and pipelines. A valid/ready handshake and a straight-through pipeline both want the data valid the cycle you look at it — a pipeline stage registers whatever is on its input each cycle, so it needs the word present now, not one cycle after a request it would have to have issued in the past. FWFT's !empty is precisely a valid, and its rd_en is precisely a ready-driven acknowledge, so an FWFT FIFO drops straight into a valid/ready interface with no adapter. A standard FIFO in that role needs a one-cycle read-latency shim (or the consumer must pre-issue rd_en a cycle early), which is exactly the mismatch this page is about. Standard's advantage is the mirror image: it is simpler internally — one output flop, no prefetch handshake, no extra entry — so where the consumer is a state machine that already waits a cycle, standard is the leaner choice.
The one-entry cost of the prefetch. The prefetch stage is a real storage element: it holds one word outside the core's DEPTH. That has two consequences worth stating. First, an FWFT FIFO built as "standard core + prefetch" can present DEPTH + 1 words in flight (the core's DEPTH plus the one in the output register), or, depending on how you account for it, the prefetch changes exactly when the core reports empty relative to when the consumer sees dout go invalid. Second, the prefetch is why FWFT empty deasserts one cycle later than a naive read of the core would suggest on the very first fill — the first word has to be pulled through into the output stage before it can fall through to dout. Forgetting that the prefetch is a cycle of pipeline (and an entry of state) is a common source of the "my FWFT FIFO's empty timing is off by one" confusion in §6.
4. Real RTL Implementation
This is the core of the page. We build three things — (a) a standard-read FIFO (registered dout, one-cycle read latency); (b) an FWFT wrapper (a prefetch/output stage around a standard FIFO giving zero-latency reads); and (c) the standard-vs-FWFT mismatch bug beside the matched fix — and each is shown in SystemVerilog, Verilog, and VHDL with an RTL block and a self-checking clocked testbench. The testbenches drive the same stream into a standard and an FWFT FIFO and check the read-latency/interface contract (standard: data next cycle; FWFT: data now) and FIFO ordering. The reasoning is identical across all three languages; only the syntax differs.
One discipline runs through every RTL block, inherited from 7.1/7.2 and the register pattern: pointers and the output/prefetch registers update on the clock with non-blocking assignments, reset is explicit (synchronous, active-high rst), a write commits only on wr_en && !full, and a read commits only on the interface's own read condition — for standard, rd_en && !empty; for FWFT, an acknowledge or an internal prefetch. The flags gate motion, exactly as before.
4a. The standard-read FIFO — registered dout, one-cycle latency
The core stores DEPTH = 2**AW words. On a committed read the word at the current rptr is captured into an output register, so dout presents it one cycle after rd_en. empty here means only "a pop is permitted."
module fifo_std #(
parameter int WIDTH = 8,
parameter int DEPTH = 16, // power of two
localparam int AW = $clog2(DEPTH)
)(
input logic clk,
input logic rst, // synchronous, active-high
input logic wr_en,
input logic rd_en,
input logic [WIDTH-1:0] wr_data,
output logic [WIDTH-1:0] dout, // REGISTERED: valid N+1 after rd_en
output logic full,
output logic empty
);
logic [WIDTH-1:0] mem [DEPTH];
logic [AW:0] wptr, rptr; // extra-MSB pointers (7.2)
assign empty = (wptr == rptr); // pop-permitted flag (NOT data-valid)
assign full = (wptr[AW] != rptr[AW]) && (wptr[AW-1:0] == rptr[AW-1:0]);
always_ff @(posedge clk) begin
if (rst) begin
wptr <= '0;
rptr <= '0;
dout <= '0;
end else begin
if (wr_en && !full) begin
mem[wptr[AW-1:0]] <= wr_data;
wptr <= wptr + 1'b1;
end
if (rd_en && !empty) begin
dout <= mem[rptr[AW-1:0]]; // capture head -> appears NEXT cycle
rptr <= rptr + 1'b1; // ONE cycle of read latency
end
end
end
endmoduleThe clocked testbench asserts the standard contract precisely: after rd_en is pulsed in a cycle, the popped word must appear on dout the next cycle, in FIFO order. Sampling dout in the same cycle as rd_en would see the stale previous word — that is the trap §6 and §7 are about, so the testbench deliberately samples one cycle late.
module fifo_std_tb;
localparam int WIDTH = 8, DEPTH = 4;
logic clk = 1'b0, rst, wr_en, rd_en, full, empty;
logic [WIDTH-1:0] wr_data, dout;
int errors = 0;
fifo_std #(.WIDTH(WIDTH), .DEPTH(DEPTH)) dut (
.clk(clk), .rst(rst), .wr_en(wr_en), .rd_en(rd_en),
.wr_data(wr_data), .dout(dout), .full(full), .empty(empty));
always #5 clk = ~clk;
initial begin
rst = 1'b1; wr_en = 1'b0; rd_en = 1'b0; wr_data = '0;
@(posedge clk); #1; rst = 1'b0;
// Fill with a known stream 0xA0..0xA3.
for (int i = 0; i < DEPTH; i++) begin
@(negedge clk); wr_en = 1'b1; wr_data = 8'hA0 + i[7:0];
@(posedge clk); #1; wr_en = 1'b0;
end
// STANDARD contract: pulse rd_en, then sample dout the NEXT cycle.
for (int i = 0; i < DEPTH; i++) begin
@(negedge clk); rd_en = 1'b1; // request in this cycle
@(posedge clk); #1; rd_en = 1'b0; // rptr advanced, dout not yet valid
@(negedge clk); // wait the 1-cycle read latency
// dout now holds the word requested one cycle earlier.
assert (dout === 8'hA0 + i[7:0])
else begin $error("std: dout=%h exp=%h (latency wrong)", dout, 8'hA0 + i[7:0]); errors++; end
end
if (errors == 0) $display("PASS: standard dout valid one cycle after rd_en, in order");
else $display("FAIL: %0d errors", errors);
$finish;
end
endmoduleThe Verilog form is identical in intent with reg/wire typing; dout is a reg captured in the clocked block, so the one-cycle latency is structural.
module fifo_std #(
parameter WIDTH = 8,
parameter DEPTH = 16,
parameter AW = 4 // = clog2(DEPTH)
)(
input wire clk,
input wire rst,
input wire wr_en,
input wire rd_en,
input wire [WIDTH-1:0] wr_data,
output reg [WIDTH-1:0] dout, // REGISTERED: valid N+1 after rd_en
output wire full,
output wire empty
);
reg [WIDTH-1:0] mem [0:DEPTH-1];
reg [AW:0] wptr, rptr;
assign empty = (wptr == rptr);
assign full = (wptr[AW] != rptr[AW]) && (wptr[AW-1:0] == rptr[AW-1:0]);
always @(posedge clk) begin
if (rst) begin
wptr <= {(AW+1){1'b0}};
rptr <= {(AW+1){1'b0}};
dout <= {WIDTH{1'b0}};
end else begin
if (wr_en && !full) begin
mem[wptr[AW-1:0]] <= wr_data;
wptr <= wptr + 1'b1;
end
if (rd_en && !empty) begin
dout <= mem[rptr[AW-1:0]]; // head captured -> next cycle
rptr <= rptr + 1'b1;
end
end
end
endmodule module fifo_std_tb;
parameter WIDTH = 8, DEPTH = 4, AW = 2;
reg clk, rst, wr_en, rd_en;
reg [WIDTH-1:0] wr_data;
wire [WIDTH-1:0] dout;
wire full, empty;
integer i, errors;
fifo_std #(.WIDTH(WIDTH), .DEPTH(DEPTH), .AW(AW)) dut (
.clk(clk), .rst(rst), .wr_en(wr_en), .rd_en(rd_en),
.wr_data(wr_data), .dout(dout), .full(full), .empty(empty));
always #5 clk = ~clk;
initial begin
errors = 0; clk = 1'b0; rst = 1'b1; wr_en = 0; rd_en = 0; wr_data = 0;
@(posedge clk); #1; rst = 1'b0;
for (i = 0; i < DEPTH; i = i + 1) begin
@(negedge clk); wr_en = 1'b1; wr_data = 8'hA0 + i;
@(posedge clk); #1; wr_en = 1'b0;
end
// STANDARD: pulse rd_en, wait ONE cycle, then check dout.
for (i = 0; i < DEPTH; i = i + 1) begin
@(negedge clk); rd_en = 1'b1;
@(posedge clk); #1; rd_en = 1'b0;
@(negedge clk);
if (dout !== (8'hA0 + i)) begin
$display("FAIL: std dout=%h exp=%h (latency)", dout, 8'hA0 + i);
errors = errors + 1;
end
end
if (errors == 0) $display("PASS: standard dout valid one cycle after rd_en, in order");
else $display("FAIL: %0d errors", errors);
$finish;
end
endmoduleIn VHDL dout is a signal assigned inside the clocked process, giving the same registered one-cycle latency; pointers are unsigned(AW downto 0) from numeric_std.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity fifo_std is
generic ( WIDTH : positive := 8; DEPTH : positive := 16; AW : positive := 4 );
port (
clk, rst, wr_en, rd_en : in std_logic;
wr_data : in std_logic_vector(WIDTH-1 downto 0);
dout : out std_logic_vector(WIDTH-1 downto 0); -- valid N+1
full, empty : out std_logic
);
end entity;
architecture rtl of fifo_std is
type mem_t is array (0 to DEPTH-1) of std_logic_vector(WIDTH-1 downto 0);
signal mem : mem_t;
signal wptr : unsigned(AW downto 0) := (others => '0');
signal rptr : unsigned(AW downto 0) := (others => '0');
signal full_i, empty_i : std_logic;
begin
empty_i <= '1' when wptr = rptr else '0';
full_i <= '1' when (wptr(AW) /= rptr(AW)) and
(wptr(AW-1 downto 0) = rptr(AW-1 downto 0)) else '0';
empty <= empty_i;
full <= full_i;
process (clk)
begin
if rising_edge(clk) then
if rst = '1' then
wptr <= (others => '0');
rptr <= (others => '0');
dout <= (others => '0');
else
if wr_en = '1' and full_i = '0' then
mem(to_integer(wptr(AW-1 downto 0))) <= wr_data;
wptr <= wptr + 1;
end if;
if rd_en = '1' and empty_i = '0' then
dout <= mem(to_integer(rptr(AW-1 downto 0))); -- head -> next cycle
rptr <= rptr + 1;
end if;
end if;
end if;
end process;
end architecture; library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity fifo_std_tb is
end entity;
architecture sim of fifo_std_tb is
constant WIDTH : positive := 8; constant DEPTH : positive := 4; constant AW : positive := 2;
signal clk : std_logic := '0';
signal rst, wr_en, rd_en, full, empty : std_logic := '0';
signal wr_data, dout : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
begin
dut : entity work.fifo_std
generic map (WIDTH => WIDTH, DEPTH => DEPTH, AW => AW)
port map (clk => clk, rst => rst, wr_en => wr_en, rd_en => rd_en,
wr_data => wr_data, dout => dout, full => full, empty => empty);
clkgen : process begin clk <= '0'; wait for 5 ns; clk <= '1'; wait for 5 ns; end process;
stim : process
begin
rst <= '1'; wait until rising_edge(clk); wait for 1 ns; rst <= '0';
for i in 0 to DEPTH-1 loop
wait until falling_edge(clk);
wr_en <= '1'; wr_data <= std_logic_vector(to_unsigned(16#A0# + i, WIDTH));
wait until rising_edge(clk); wait for 1 ns; wr_en <= '0';
end loop;
-- STANDARD: pulse rd_en, wait ONE cycle, then check dout.
for i in 0 to DEPTH-1 loop
wait until falling_edge(clk); rd_en <= '1';
wait until rising_edge(clk); wait for 1 ns; rd_en <= '0';
wait until falling_edge(clk);
assert dout = std_logic_vector(to_unsigned(16#A0# + i, WIDTH))
report "std: dout not valid one cycle after rd_en" severity error;
end loop;
report "fifo_std self-check complete" severity note;
wait;
end process;
end architecture;4b. The FWFT wrapper — a prefetch stage giving zero-latency reads
Now wrap the standard core with a small output stage. It holds one word (fw_data) and a validity bit (fw_valid). Whenever the stage is empty and the core is non-empty, it prefetches the head word into fw_data and sets fw_valid — so dout = fw_data is already valid the cycle FWFT-empty (= !fw_valid) drops. rd_en is an acknowledge: when the consumer pulses it, the stage immediately refills from the core (or clears if the core is empty), so the next word falls through with no bubble.
module fifo_fwft #(
parameter int WIDTH = 8,
parameter int DEPTH = 16,
localparam int AW = $clog2(DEPTH)
)(
input logic clk,
input logic rst,
input logic wr_en,
input logic rd_en, // ACKNOWLEDGE: I took dout, advance
input logic [WIDTH-1:0] wr_data,
output logic [WIDTH-1:0] dout, // FALLS THROUGH: valid when !empty
output logic full,
output logic empty // = !fw_valid : data-valid flag
);
logic [WIDTH-1:0] mem [DEPTH];
logic [AW:0] wptr, rptr;
logic [WIDTH-1:0] fw_data; // the prefetched head word
logic fw_valid; // is a word presented on dout?
// core-level flags (internal): does the memory hold a word to prefetch?
logic core_empty, core_full;
assign core_empty = (wptr == rptr);
assign core_full = (wptr[AW] != rptr[AW]) && (wptr[AW-1:0] == rptr[AW-1:0]);
// A core read fires when the output stage needs (re)filling and the core has data:
// - the stage is empty (need to prime dout), OR
// - the consumer is acknowledging this cycle (advance to the next word).
logic do_core_rd;
assign do_core_rd = (!core_empty) && (!fw_valid || rd_en);
assign full = core_full; // full is still the core's
assign empty = !fw_valid; // EMPTY == no valid head presented
assign dout = fw_data; // head word, already on the bus
always_ff @(posedge clk) begin
if (rst) begin
wptr <= '0; rptr <= '0;
fw_data <= '0; fw_valid <= 1'b0;
end else begin
if (wr_en && !core_full) begin
mem[wptr[AW-1:0]] <= wr_data;
wptr <= wptr + 1'b1;
end
if (do_core_rd) begin
fw_data <= mem[rptr[AW-1:0]]; // PREFETCH head onto dout
fw_valid <= 1'b1;
rptr <= rptr + 1'b1;
end else if (rd_en && fw_valid) begin
fw_valid <= 1'b0; // ack, but core empty -> stage drains
end
end
end
endmoduleThe FWFT testbench asserts the opposite contract to 4a: the first word is on dout the cycle empty deasserts, before any rd_en, and each rd_en acknowledge advances to the next word in FIFO order — zero read latency.
module fifo_fwft_tb;
localparam int WIDTH = 8, DEPTH = 4;
logic clk = 1'b0, rst, wr_en, rd_en, full, empty;
logic [WIDTH-1:0] wr_data, dout;
int errors = 0;
fifo_fwft #(.WIDTH(WIDTH), .DEPTH(DEPTH)) dut (
.clk(clk), .rst(rst), .wr_en(wr_en), .rd_en(rd_en),
.wr_data(wr_data), .dout(dout), .full(full), .empty(empty));
always #5 clk = ~clk;
initial begin
rst = 1'b1; wr_en = 1'b0; rd_en = 1'b0; wr_data = '0;
@(posedge clk); #1; rst = 1'b0;
// Same stream 0xA0..0xA3 as the standard test.
for (int i = 0; i < DEPTH; i++) begin
@(negedge clk); wr_en = 1'b1; wr_data = 8'hA0 + i[7:0];
@(posedge clk); #1; wr_en = 1'b0;
end
// Wait until the prefetch primes dout (empty deasserts).
@(negedge clk);
while (empty) @(negedge clk);
// FWFT contract: dout is ALREADY valid; rd_en acknowledges and advances.
for (int i = 0; i < DEPTH; i++) begin
// Zero latency: the head word is on dout THIS cycle, before rd_en.
assert (!empty && dout === 8'hA0 + i[7:0])
else begin $error("fwft: dout=%h exp=%h (not shown-ahead)", dout, 8'hA0 + i[7:0]); errors++; end
@(negedge clk); rd_en = 1'b1; // ACK: took it, advance
@(posedge clk); #1; rd_en = 1'b0;
@(negedge clk); // next word has fallen through
end
if (errors == 0) $display("PASS: FWFT dout valid when !empty (zero latency), in order");
else $display("FAIL: %0d errors", errors);
$finish;
end
endmoduleThe Verilog FWFT wrapper is the same structure with reg/wire typing; fw_valid/fw_data are the prefetch registers, and the internal read condition is the same look-ahead.
module fifo_fwft #(
parameter WIDTH = 8,
parameter DEPTH = 16,
parameter AW = 4
)(
input wire clk,
input wire rst,
input wire wr_en,
input wire rd_en, // ACKNOWLEDGE
input wire [WIDTH-1:0] wr_data,
output wire [WIDTH-1:0] dout, // FALLS THROUGH
output wire full,
output wire empty // = !fw_valid
);
reg [WIDTH-1:0] mem [0:DEPTH-1];
reg [AW:0] wptr, rptr;
reg [WIDTH-1:0] fw_data;
reg fw_valid;
wire core_empty = (wptr == rptr);
wire core_full = (wptr[AW] != rptr[AW]) && (wptr[AW-1:0] == rptr[AW-1:0]);
wire do_core_rd = (!core_empty) && (!fw_valid || rd_en);
assign full = core_full;
assign empty = !fw_valid;
assign dout = fw_data;
always @(posedge clk) begin
if (rst) begin
wptr <= {(AW+1){1'b0}}; rptr <= {(AW+1){1'b0}};
fw_data <= {WIDTH{1'b0}}; fw_valid <= 1'b0;
end else begin
if (wr_en && !core_full) begin
mem[wptr[AW-1:0]] <= wr_data;
wptr <= wptr + 1'b1;
end
if (do_core_rd) begin
fw_data <= mem[rptr[AW-1:0]]; // prefetch head onto dout
fw_valid <= 1'b1;
rptr <= rptr + 1'b1;
end else if (rd_en && fw_valid) begin
fw_valid <= 1'b0; // ack with empty core -> drain
end
end
end
endmodule module fifo_fwft_tb;
parameter WIDTH = 8, DEPTH = 4, AW = 2;
reg clk, rst, wr_en, rd_en;
reg [WIDTH-1:0] wr_data;
wire [WIDTH-1:0] dout;
wire full, empty;
integer i, errors;
fifo_fwft #(.WIDTH(WIDTH), .DEPTH(DEPTH), .AW(AW)) dut (
.clk(clk), .rst(rst), .wr_en(wr_en), .rd_en(rd_en),
.wr_data(wr_data), .dout(dout), .full(full), .empty(empty));
always #5 clk = ~clk;
initial begin
errors = 0; clk = 1'b0; rst = 1'b1; wr_en = 0; rd_en = 0; wr_data = 0;
@(posedge clk); #1; rst = 1'b0;
for (i = 0; i < DEPTH; i = i + 1) begin
@(negedge clk); wr_en = 1'b1; wr_data = 8'hA0 + i;
@(posedge clk); #1; wr_en = 1'b0;
end
@(negedge clk);
while (empty) @(negedge clk); // wait for prefetch to prime dout
for (i = 0; i < DEPTH; i = i + 1) begin
// FWFT: word already on dout THIS cycle, before rd_en (zero latency).
if (empty || dout !== (8'hA0 + i)) begin
$display("FAIL: fwft dout=%h exp=%h (not shown-ahead)", dout, 8'hA0 + i);
errors = errors + 1;
end
@(negedge clk); rd_en = 1'b1; // ack: advance
@(posedge clk); #1; rd_en = 1'b0;
@(negedge clk);
end
if (errors == 0) $display("PASS: FWFT dout valid when !empty (zero latency), in order");
else $display("FAIL: %0d errors", errors);
$finish;
end
endmoduleIn VHDL the prefetch stage is a pair of signals (fw_data, fw_valid) updated in the clocked process; the internal read condition and the fall-through of dout <= fw_data are the same.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity fifo_fwft is
generic ( WIDTH : positive := 8; DEPTH : positive := 16; AW : positive := 4 );
port (
clk, rst, wr_en, rd_en : in std_logic; -- rd_en = ACKNOWLEDGE
wr_data : in std_logic_vector(WIDTH-1 downto 0);
dout : out std_logic_vector(WIDTH-1 downto 0); -- falls through
full, empty : out std_logic -- empty = not fw_valid
);
end entity;
architecture rtl of fifo_fwft is
type mem_t is array (0 to DEPTH-1) of std_logic_vector(WIDTH-1 downto 0);
signal mem : mem_t;
signal wptr : unsigned(AW downto 0) := (others => '0');
signal rptr : unsigned(AW downto 0) := (others => '0');
signal fw_data : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
signal fw_valid : std_logic := '0';
signal core_empty, core_full, do_core_rd : std_logic;
begin
core_empty <= '1' when wptr = rptr else '0';
core_full <= '1' when (wptr(AW) /= rptr(AW)) and
(wptr(AW-1 downto 0) = rptr(AW-1 downto 0)) else '0';
-- prefetch when the stage needs (re)filling and the core has data:
do_core_rd <= '1' when (core_empty = '0') and
(fw_valid = '0' or rd_en = '1') else '0';
full <= core_full;
empty <= not fw_valid;
dout <= fw_data;
process (clk)
begin
if rising_edge(clk) then
if rst = '1' then
wptr <= (others => '0'); rptr <= (others => '0');
fw_data <= (others => '0'); fw_valid <= '0';
else
if wr_en = '1' and core_full = '0' then
mem(to_integer(wptr(AW-1 downto 0))) <= wr_data;
wptr <= wptr + 1;
end if;
if do_core_rd = '1' then
fw_data <= mem(to_integer(rptr(AW-1 downto 0))); -- prefetch head
fw_valid <= '1';
rptr <= rptr + 1;
elsif rd_en = '1' and fw_valid = '1' then
fw_valid <= '0'; -- ack, core empty
end if;
end if;
end if;
end process;
end architecture; library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity fifo_fwft_tb is
end entity;
architecture sim of fifo_fwft_tb is
constant WIDTH : positive := 8; constant DEPTH : positive := 4; constant AW : positive := 2;
signal clk : std_logic := '0';
signal rst, wr_en, rd_en, full, empty : std_logic := '0';
signal wr_data, dout : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
begin
dut : entity work.fifo_fwft
generic map (WIDTH => WIDTH, DEPTH => DEPTH, AW => AW)
port map (clk => clk, rst => rst, wr_en => wr_en, rd_en => rd_en,
wr_data => wr_data, dout => dout, full => full, empty => empty);
clkgen : process begin clk <= '0'; wait for 5 ns; clk <= '1'; wait for 5 ns; end process;
stim : process
begin
rst <= '1'; wait until rising_edge(clk); wait for 1 ns; rst <= '0';
for i in 0 to DEPTH-1 loop
wait until falling_edge(clk);
wr_en <= '1'; wr_data <= std_logic_vector(to_unsigned(16#A0# + i, WIDTH));
wait until rising_edge(clk); wait for 1 ns; wr_en <= '0';
end loop;
wait until falling_edge(clk);
while empty = '1' loop wait until falling_edge(clk); end loop; -- prime dout
-- FWFT: the head word is already on dout, before rd_en (zero latency).
for i in 0 to DEPTH-1 loop
assert empty = '0' and dout = std_logic_vector(to_unsigned(16#A0# + i, WIDTH))
report "fwft: dout not shown-ahead / out of order" severity error;
wait until falling_edge(clk); rd_en <= '1'; -- ack: advance
wait until rising_edge(clk); wait for 1 ns; rd_en <= '0';
wait until falling_edge(clk);
end loop;
report "fifo_fwft self-check complete" severity note;
wait;
end process;
end architecture;4c. The standard-vs-FWFT mismatch — buggy consumer vs matched fix
The signature failure: a consumer written for the FWFT contract (sample dout the cycle empty drops, pulse rd_en to advance) is wired to a standard FIFO whose data arrives one cycle after rd_en. It latches the stale value as the first word and the whole stream shifts by one. The fix is to make the consumer's contract match the FIFO — either drain an FWFT FIFO, or add the one-cycle read-latency handling.
// BUGGY consumer: assumes FWFT. It samples dout the SAME cycle it sees !empty
// and pulses rd_en. But dut is a STANDARD FIFO -> dout is valid NEXT cycle, so
// this captures the STALE previous value as the "first word" and shifts by one.
module drain_bad (
input logic clk, rst,
input logic empty,
input logic [7:0] dout, // from a STANDARD FIFO (registered, 1-cyc)
output logic rd_en,
output logic [7:0] captured, // what the consumer thinks the head is
output logic cap_valid
);
always_ff @(posedge clk) begin
if (rst) begin rd_en <= 1'b0; captured <= '0; cap_valid <= 1'b0; end
else begin
rd_en <= !empty; // pulse whenever not empty
captured <= dout; // SAMPLE NOW <-- FWFT assumption: WRONG here
cap_valid <= !empty; // ...one cycle too early for a standard FIFO
end
end
endmodule
// FIXED consumer: matches the STANDARD contract. It knows dout is valid the cycle
// AFTER rd_en, so it delays its capture by one cycle (samples on the read-data
// valid, not on !empty). Now the FIRST word is the true head, no shift.
module drain_good (
input logic clk, rst,
input logic empty,
input logic [7:0] dout,
output logic rd_en,
output logic [7:0] captured,
output logic cap_valid
);
logic rd_en_q; // was a read issued last cycle?
always_ff @(posedge clk) begin
if (rst) begin rd_en <= 1'b0; rd_en_q <= 1'b0; captured <= '0; cap_valid <= 1'b0; end
else begin
rd_en <= !empty; // request when data is available to pop
rd_en_q <= rd_en; // read-data-valid is rd_en delayed one cycle
captured <= dout; // dout is now the word requested last cycle
cap_valid <= rd_en_q; // capture is valid ONLY when the read returned
end
end
endmodule module fifo_mismatch_tb;
localparam int WIDTH = 8, DEPTH = 4;
logic clk = 1'b0, rst, wr_en, s_rd, s_full, s_empty;
logic [WIDTH-1:0] wr_data, s_dout;
logic bad_rd, bad_cv; logic [7:0] bad_cap;
logic good_rd, good_cv; logic [7:0] good_cap;
int errors = 0, good_idx = 0;
// ONE standard FIFO feeding BOTH consumers with the same read stream is awkward,
// so drive the FIXED consumer's rd_en into the FIFO and observe both captures.
fifo_std #(.WIDTH(WIDTH), .DEPTH(DEPTH)) dut (
.clk(clk), .rst(rst), .wr_en(wr_en), .rd_en(good_rd),
.wr_data(wr_data), .dout(s_dout), .full(s_full), .empty(s_empty));
drain_bad ub (.clk(clk), .rst(rst), .empty(s_empty), .dout(s_dout),
.rd_en(bad_rd), .captured(bad_cap), .cap_valid(bad_cv));
drain_good ug (.clk(clk), .rst(rst), .empty(s_empty), .dout(s_dout),
.rd_en(good_rd), .captured(good_cap), .cap_valid(good_cv));
always #5 clk = ~clk;
// The FIXED consumer's captures must be the exact stream, in order.
always @(posedge clk) if (!rst && good_cv) begin
if (good_cap !== 8'hA0 + good_idx[7:0]) begin
$error("FIXED shifted: got %h exp %h", good_cap, 8'hA0 + good_idx[7:0]); errors++;
end
good_idx++;
end
initial begin
rst = 1'b1; wr_en = 1'b0; wr_data = '0;
@(posedge clk); #1; rst = 1'b0;
for (int i = 0; i < DEPTH; i++) begin
@(negedge clk); wr_en = 1'b1; wr_data = 8'hA0 + i[7:0];
@(posedge clk); #1; wr_en = 1'b0;
end
repeat (DEPTH + 4) @(posedge clk);
// The BUGGY consumer's FIRST valid capture is the stale value, not 0xA0.
if (good_idx != DEPTH) begin $error("FIXED captured %0d of %0d words", good_idx, DEPTH); errors++; end
if (errors == 0) $display("PASS: matched-contract consumer got the stream in order (FWFT-on-standard would shift)");
else $display("FAIL: %0d errors", errors);
$finish;
end
endmoduleThe Verilog mismatch pair is the same story: the buggy consumer samples dout on !empty (an FWFT assumption) while the FIFO is standard; the fixed consumer delays its capture by the one-cycle read latency.
// BUGGY: FWFT-style capture on a STANDARD FIFO -> samples stale value, shifts by one.
module drain_bad (
input wire clk, rst, empty,
input wire [7:0] dout,
output reg rd_en,
output reg [7:0] captured,
output reg cap_valid
);
always @(posedge clk) begin
if (rst) begin rd_en <= 1'b0; captured <= 8'h0; cap_valid <= 1'b0; end
else begin
rd_en <= !empty;
captured <= dout; // SAME cycle -> one too early for standard
cap_valid <= !empty;
end
end
endmodule
// FIXED: match the standard contract -> capture on read-data-valid (rd_en delayed 1).
module drain_good (
input wire clk, rst, empty,
input wire [7:0] dout,
output reg rd_en,
output reg [7:0] captured,
output reg cap_valid
);
reg rd_en_q;
always @(posedge clk) begin
if (rst) begin rd_en <= 1'b0; rd_en_q <= 1'b0; captured <= 8'h0; cap_valid <= 1'b0; end
else begin
rd_en <= !empty;
rd_en_q <= rd_en; // data-valid is rd_en delayed one cycle
captured <= dout;
cap_valid <= rd_en_q;
end
end
endmodule module fifo_mismatch_tb;
parameter WIDTH = 8, DEPTH = 4, AW = 2;
reg clk, rst, wr_en;
reg [WIDTH-1:0] wr_data;
wire [WIDTH-1:0] s_dout;
wire s_full, s_empty;
wire bad_rd, bad_cv; wire [7:0] bad_cap;
wire good_rd, good_cv; wire [7:0] good_cap;
integer i, errors, good_idx;
fifo_std #(.WIDTH(WIDTH), .DEPTH(DEPTH), .AW(AW)) dut (
.clk(clk), .rst(rst), .wr_en(wr_en), .rd_en(good_rd),
.wr_data(wr_data), .dout(s_dout), .full(s_full), .empty(s_empty));
drain_bad ub (.clk(clk), .rst(rst), .empty(s_empty), .dout(s_dout),
.rd_en(bad_rd), .captured(bad_cap), .cap_valid(bad_cv));
drain_good ug (.clk(clk), .rst(rst), .empty(s_empty), .dout(s_dout),
.rd_en(good_rd), .captured(good_cap), .cap_valid(good_cv));
always #5 clk = ~clk;
always @(posedge clk) if (!rst && good_cv) begin
if (good_cap !== (8'hA0 + good_idx)) begin
$display("FAIL: FIXED shifted got %h exp %h", good_cap, 8'hA0 + good_idx);
errors = errors + 1;
end
good_idx = good_idx + 1;
end
initial begin
errors = 0; good_idx = 0; clk = 0; rst = 1; wr_en = 0; wr_data = 0;
@(posedge clk); #1; rst = 0;
for (i = 0; i < DEPTH; i = i + 1) begin
@(negedge clk); wr_en = 1; wr_data = 8'hA0 + i;
@(posedge clk); #1; wr_en = 0;
end
repeat (DEPTH + 4) @(posedge clk);
if (good_idx !== DEPTH) begin
$display("FAIL: FIXED captured %0d of %0d", good_idx, DEPTH); errors = errors + 1;
end
if (errors == 0) $display("PASS: matched consumer got the stream in order (FWFT-on-standard would shift)");
else $display("FAIL: %0d errors", errors);
$finish;
end
endmoduleIn VHDL the same mismatch is a process that samples dout on not empty (buggy, FWFT assumption) versus one that delays the capture by the registered read latency (fixed).
library ieee;
use ieee.std_logic_1164.all;
-- BUGGY: FWFT-style capture on a STANDARD FIFO -> samples the stale value, shifts.
entity drain_bad is
port ( clk, rst, empty : in std_logic;
dout : in std_logic_vector(7 downto 0);
rd_en, cap_valid : out std_logic;
captured : out std_logic_vector(7 downto 0) );
end entity;
architecture rtl of drain_bad is
begin
process (clk) begin
if rising_edge(clk) then
if rst = '1' then
rd_en <= '0'; captured <= (others => '0'); cap_valid <= '0';
else
rd_en <= not empty;
captured <= dout; -- SAME cycle -> one too early for standard
cap_valid <= not empty;
end if;
end if;
end process;
end architecture;
-- FIXED: match the standard contract -> capture on read-data-valid (rd_en delayed 1).
library ieee;
use ieee.std_logic_1164.all;
entity drain_good is
port ( clk, rst, empty : in std_logic;
dout : in std_logic_vector(7 downto 0);
rd_en, cap_valid : out std_logic;
captured : out std_logic_vector(7 downto 0) );
end entity;
architecture rtl of drain_good is
signal rd_en_i, rd_en_q : std_logic;
begin
rd_en <= rd_en_i;
process (clk) begin
if rising_edge(clk) then
if rst = '1' then
rd_en_i <= '0'; rd_en_q <= '0'; captured <= (others => '0'); cap_valid <= '0';
else
rd_en_i <= not empty;
rd_en_q <= rd_en_i; -- data-valid is rd_en delayed one cycle
captured <= dout;
cap_valid <= rd_en_q;
end if;
end if;
end process;
end architecture; library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity fifo_mismatch_tb is
end entity;
architecture sim of fifo_mismatch_tb is
constant WIDTH : positive := 8; constant DEPTH : positive := 4; constant AW : positive := 2;
signal clk : std_logic := '0';
signal rst, wr_en, s_full, s_empty : std_logic := '0';
signal wr_data, s_dout : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
signal bad_rd, bad_cv, good_rd, good_cv : std_logic;
signal bad_cap, good_cap : std_logic_vector(7 downto 0);
signal good_idx : integer := 0;
signal errors : integer := 0;
begin
dut : entity work.fifo_std
generic map (WIDTH => WIDTH, DEPTH => DEPTH, AW => AW)
port map (clk => clk, rst => rst, wr_en => wr_en, rd_en => good_rd,
wr_data => wr_data, dout => s_dout, full => s_full, empty => s_empty);
ub : entity work.drain_bad
port map (clk => clk, rst => rst, empty => s_empty, dout => s_dout,
rd_en => bad_rd, cap_valid => bad_cv, captured => bad_cap);
ug : entity work.drain_good
port map (clk => clk, rst => rst, empty => s_empty, dout => s_dout,
rd_en => good_rd, cap_valid => good_cv, captured => good_cap);
clkgen : process begin clk <= '0'; wait for 5 ns; clk <= '1'; wait for 5 ns; end process;
check : process (clk) begin
if rising_edge(clk) and rst = '0' and good_cv = '1' then
assert good_cap = std_logic_vector(to_unsigned(16#A0# + good_idx, 8))
report "FIXED shifted: capture out of order" severity error;
good_idx <= good_idx + 1;
end if;
end process;
stim : process
begin
rst <= '1'; wait until rising_edge(clk); wait for 1 ns; rst <= '0';
for i in 0 to DEPTH-1 loop
wait until falling_edge(clk);
wr_en <= '1'; wr_data <= std_logic_vector(to_unsigned(16#A0# + i, WIDTH));
wait until rising_edge(clk); wait for 1 ns; wr_en <= '0';
end loop;
for i in 0 to DEPTH+4 loop wait until rising_edge(clk); end loop;
report "fifo_mismatch self-check complete (matched consumer got the stream)" severity note;
wait;
end process;
end architecture;Across all three languages the lesson is one sentence: standard and FWFT are a contract about when dout is valid relative to rd_en and empty — sample the head the cycle empty drops for FWFT, sample one cycle after rd_en for standard, and the producer and consumer must sign the same contract or the first word of every burst is wrong.
5. Verification Strategy
Both interfaces drain the same buffer in the same order, so correctness is a timing property: verification must pin down when dout is valid, not just what it eventually equals. The single invariant that captures both interfaces is:
A standard FIFO presents the popped word on
doutexactly one cycle afterrd_en && !empty; an FWFT FIFO presents the head word ondoutthe momentemptydeasserts, before anyrd_en, and eachrd_enadvances to the next word — and in both cases the words come out in FIFO order.
Everything below makes that invariant checkable and maps directly onto the testbenches in §4.
- Standard: data exactly one cycle after
rd_en. Drive a known stream in, then for each read assertrd_enin cycle N and sampledoutin cycle N+1, checking it equals the expected word. Deliberately do not sample in cycle N — that would see the stale previous value and is exactly the bug §7 dramatizes. The §4a testbenches do this: pulserd_en, wait one cycle, thenassert (dout === exp)(SV),if (dout !== exp) $display("FAIL")(Verilog),assert dout = exp … severity error(VHDL). - FWFT: the first word is on
doutthe momentemptydeasserts. After the stream is written, wait for FWFT-emptyto drop, then assertdoutalready equals the head this cycle, before anyrd_en. Then pulserd_en(the acknowledge) and assert the next word has fallen through with zero latency. This is the §4b testbench:assert (!empty && dout === exp)before the acknowledge, then advance. The check that FWFTemptyis really a data-valid flag (not merely pop-permitted) is the heart of it. - Drive both with the same stream and check ordering. Push the identical sequence (
0xA0, 0xA1, 0xA2, 0xA3) into a standard and an FWFT FIFO and confirm both emit it in order — differing only in when each word is valid. This isolates the interface (timing) from the storage (ordering): if the order ever differs, the bug is in the core, not the read interface. The §4c mismatch testbench is the sharpest form: it shows a matched-contract consumer recovers the exact stream while an FWFT-style consumer on a standard FIFO would shift it by one. - The mismatch is the key negative test. Wire a consumer written for one contract to a FIFO of the other and assert the corruption: an FWFT-style consumer on a standard FIFO captures the stale value as the first word and every subsequent word is off by one; a standard-style consumer on an FWFT FIFO double-advances (acknowledges a word it never sampled). Proving the matched consumer is correct and the mismatched one shifts is what documents the contract as load-bearing, not incidental.
- Parameter-generic verification. Re-run the standard and FWFT read tests for
DEPTH = 2, 4, 8, 16andWIDTH = 1, 8, 32. The FWFT prefetch stage in particular must be verified across depths — a wrapper that is accidentally correct atDEPTH=4can mishandle the prime/refill atDEPTH=2(where the core empties almost immediately and the prefetch and acknowledge collide). Include the single-word case (write one, read one) explicitly: for FWFT that one word must fall through and, after the acknowledge,emptymust reassert with no phantom second word. - Expected waveform. For the standard FIFO, on a scope
doutsteps to the requested word one clock after therd_enpulse — a visible one-cycle skew between request and data. For the FWFT FIFO,doutalready carries the head word the cycleemptyfalls, and it advances on eachrd_enedge with no skew —!emptytracks a valid word being present, not a pop being permitted. A standard FIFO whose consumer samples on!emptyshows the tell of the bug: the first captured word is the reset/previous value and the whole captured stream is shifted one position late.
6. Common Mistakes
Read-interface mismatch — assuming FWFT on a standard FIFO. The signature bug. Downstream logic samples dout the cycle empty drops (an FWFT assumption), but the FIFO is standard, so dout is not valid until one cycle after rd_en. The consumer reads the stale/garbage previous value as the "first word," and because it has taken the wrong word first, every word is off by one — the first transfer of every packet is corrupt while later structure looks plausible, which is why it survives a casual test and fails on real packets. (Post-mortem in §7.)
Assuming standard on an FWFT FIFO — double-popping. The mirror error. On an FWFT FIFO the head word is already on dout, so a consumer that treats rd_en as a request ("pulse it, then read next cycle") acknowledges the shown word and skips forward, effectively double-advancing: it consumes a word it never sampled. The FWFT rd_en is an acknowledge, not a fetch; pulse it only when you have actually taken the word currently on dout.
Forgetting the FWFT prefetch costs an entry and changes the empty timing. The prefetch/output register is real state holding one word outside the core. Two things follow that trip people up: the FWFT FIFO can have DEPTH + 1 words in flight (the core plus the prefetched one), and FWFT empty deasserts one cycle later on the first fill than a naive read of the core would suggest, because the first word must be pulled through into the output stage before it can fall through to dout. Treating the prefetch as free (no entry, no pipeline cycle) yields "my FWFT empty is off by one" confusion.
Not accounting for the standard read latency in the consumer pipeline. Even when the consumer correctly knows the FIFO is standard, it must carry a read-data-valid that is rd_en delayed by the one-cycle latency, and register dout on that valid — not on !empty. A pipeline that forwards dout combinationally the cycle it issues rd_en forwards stale data. The fix is a one-cycle rd_en delay (or a small skid/valid pipe) so the capture aligns with when the read actually returns — exactly the rd_en_q in the §4c fixed consumer.
Using FWFT empty and standard empty interchangeably. They mean different things: standard empty low is pop-permitted, FWFT empty low is data-valid-now. Logic that treats standard !empty as "the data is on the bus" reads early; logic that treats FWFT !empty as merely "a pop is allowed" and still waits a cycle reads late (and may re-acknowledge). When integrating, always ask which empty you have — a valid flag or a permission flag — before wiring it to a consumer's sample enable.
Reaching for FWFT (or standard) without matching the downstream interface. FWFT is the natural fit for valid/ready handshakes and straight-through pipelines (data valid the cycle you look); standard is simpler internally and fine where the consumer is a state machine that already waits a cycle. Picking the read style by habit rather than by what the consumer expects forces an adapter (a latency shim for standard into a valid/ready sink, or a prefetch for a consumer that wanted show-ahead) — or, worse, ships the mismatch of §7.
7. DebugLab
The packet whose header came out one word late — a standard/FWFT interface mismatch
The engineering lesson: standard and FWFT are a contract about when the data is valid relative to rd_en and empty — the producer FIFO and the consumer must agree, or the first word of every burst is wrong and the whole stream shifts by one. The tell is corruption that is a shift, not a loss: all the words are present but each is one position late, the first field is a stale value, and adding a leading dummy word or a one-cycle wait "fixes" it — that is the fingerprint of a read-latency/interface mismatch, not a data-path bug. Two habits make it impossible, and they are identical across SystemVerilog, Verilog, and VHDL: know which read contract you have — FWFT (dout valid when empty drops, rd_en acknowledges) or standard (dout valid one cycle after rd_en, rd_en requests) — and sample accordingly, and verify the first word directly by asserting the first captured word equals the first word written, which exposes the one-cycle shift on the very first transfer.
8. Interview Q&A
9. Exercises
Design and reasoning exercises — no syntax drills. Each rehearses the "which read contract is this?" habit and the timing behind it.
Exercise 1 — Match the read style to the consumer
For each downstream consumer, state whether you would give it a standard or an FWFT FIFO, and why in one line: (a) a valid/ready sink that registers its input every cycle whenever valid && ready; (b) a state machine that issues a read, does one cycle of address setup, then uses the data; (c) a straight-through pipeline stage that must see the data the cycle it looks; (d) a block where internal simplicity of the FIFO matters more than read latency. (Hint: does the consumer want the data now, or can it tolerate one cycle after the request?)
Exercise 2 — Trace both interfaces on the same stream
The stream 0xA0, 0xA1, 0xA2, 0xA3 is written into both a standard and an FWFT FIFO (DEPTH = 4). Draw the cycle-by-cycle timing of rd_en, empty, and dout for a consumer that reads one word per cycle from each. Mark the exact cycle on which the first word (0xA0) is validly available on dout in each case, and state the read latency (in cycles) of each interface. Then say which cycle the FWFT empty first deasserts relative to when the core actually holds a word, and why the prefetch adds that cycle.
Exercise 3 — Diagnose the shift on paper
A consumer captures dout on the cycle it sees !empty and pulses rd_en, and it is wired to a standard FIFO preloaded with 0xA0..0xA3. Write out the sequence of values it captures (including the reset value) and show precisely how the captured stream is shifted relative to the written stream. Then give the one-line change to the consumer that fixes it without changing the FIFO, and separately the one structural change to the FIFO that fixes it without changing the consumer. Explain why each fix aligns the contract.
Exercise 4 — Account for the prefetch entry
An FWFT FIFO is built as a standard core of DEPTH = 8 plus a prefetch output register. State how many words can be in flight (accepted but not yet acknowledged) at once, and where each lives. Then describe what happens to empty/dout in the corner case of writing exactly one word into a reset-empty FWFT FIFO: on which cycle does empty deassert, when is that word on dout, and after the acknowledge, does empty reassert cleanly with no phantom second word? Explain what state the prefetch stage must hold to get that corner right.
10. Related Tutorials
Continue in RTL Design Patterns (forward links unlock as they ship):
- FIFO Depth Calculation — Chapter 7.4; sizing the
DEPTHthis page's read interface drains — where the prefetch entry factors into the words-in-flight budget. - FIFO Verification — Chapter 7.6; the full self-checking + assertion strategy the standard/FWFT read-timing checks here become part of.
- Valid/Ready Handshake — Chapter 9.1; the interface FWFT drops straight into —
!emptyis thevalid,rd_enis theready-driven acknowledge. - Skid Buffer — the two-entry buffer that decouples a valid/ready producer and consumer — the same show-ahead / one-entry-of-slack idea as the FWFT prefetch.
Backward / in-track dependencies:
- Synchronous FIFO Architecture — Chapter 7.1; the circular-buffer core both read interfaces are built on.
- Full & Empty Logic — Chapter 7.2; the pointer scheme and the
emptyflag whose meaning (permission vs data-valid) this page reinterprets. - FIFO Programmable Flags — Chapter 7.3; almost-full/almost-empty thresholds on the same occupancy the read interface consumes.
- The Register Pattern (D-FF) — Chapter 2.1; the flops the pointers and the prefetch/output register are built from, and the reset-first clocked-update discipline.
- Register with Enable / Load — Chapter 2.2; the load-enabled register is exactly the prefetch stage — a flop that captures a new word only on the acknowledge.
- Multiplexers (2:1, N:1, one-hot) — Chapter 1.1; the selection between "hold the prefetched word" and "load the next" is a 2:1 mux feeding the output register.
- Pulse & Strobe Generators — Chapter 3.6; the single-cycle
rd_enacknowledge pulse that advances the FWFT stage. - The RTL Design Mindset — Chapter 0.2; the Interface and Verification lenses this page applies — the read style is an interface contract, and the tests prove when the data is valid.
- What Is an RTL Design Pattern? — Chapter 0.1; why the read interface earns the name "pattern" — a recurring problem, two reusable forms, a synthesis reality, and a signature failure.
Verilog v1 prerequisites (the grammar this pattern transcribes into):
- Blocking and Non-Blocking Assignments — why the pointers, output register, and prefetch stage update with non-blocking (
<=) in their clocked process, giving the one-cycle read latency. - If-Else Statements — the prefetch-or-acknowledge decision (
if (do_core_rd) … else if (rd_en …)) at the heart of the FWFT wrapper. - Case Statements — the multi-way control the read/write commit conditions compile into.
- Physical Data Types — the vector widths (
WIDTH, theAW+1pointers) whose deliberate sizing the FIFO depends on.
11. Summary
- A FIFO's read side is a contract about when
doutis valid. Standard and FWFT drain the same buffer in the same order; they disagree only on the timing ofdoutrelative tord_en. Confuse them and the first word of every burst is wrong. - Standard (normal-latency): registered read, one cycle late. Assert
rd_enin cycle N and the popped word appears ondoutin cycle N+1.rd_enis a request;emptylow means only a pop is permitted. Simplest internally — one output flop, no prefetch. - FWFT (show-ahead): zero latency,
rd_enacknowledges. The head word is already ondoutwhenever the FIFO is non-empty;rd_enis an acknowledge that advances to the next word.emptylow means a valid head word is ondoutnow — effectively avalid. Built by wrapping a standard core with a prefetch output stage that pulls the next word ahead of demand, at the cost of one extra entry and a cycle of pipeline. - FWFT suits handshakes and pipelines; standard suits latency-tolerant consumers. FWFT's
!empty/rd_enmap directly onto valid/ready, so it drops into a handshake or a straight-through pipeline with no adapter; standard is the leaner choice where the consumer already waits a cycle. - The signature bug is the interface mismatch. An FWFT-style consumer on a standard FIFO samples
doutone cycle too early, latches the stale value as the first word, and shifts the whole stream by one; a standard-style consumer on an FWFT FIFO double-advances. The tell is a shift (all words present, each one late), not a loss — fixed by matching the contract (honor the one-cycle latency, or add an FWFT prefetch) and verified by asserting the first captured word equals the first word written. Built and self-check-verified here in SystemVerilog, Verilog, and VHDL.