RTL Design Patterns · Chapter 7 · FIFO Design
Almost-Full / Programmable Flags
A hard full flag is honest but arrives one cycle too late to be useful. A real producer does not stop the instant it sees the flag; it has a pipeline and a state machine, so several cycles pass before it actually halts, and during those cycles it keeps pushing into a FIFO that has no room. The fix is an earlier flag. A programmable almost-full watermark asserts when occupancy crosses a threshold you choose, giving the producer lead time to react. Mechanically it is just a comparator on the occupancy the FIFO already computes, so the whole engineering is in the value you compare against. The watermark must lead hard full by at least the reaction latency, and it needs hysteresis so it does not chatter at the line. This lesson builds almost-full and almost-empty flags with hysteresis, in SystemVerilog, Verilog, and VHDL.
Intermediate12 min readRTL Design PatternsFIFOAlmost FullProgrammable FlagsWatermarkHysteresisBackpressure
Chapter 7 · Section 7.3 · FIFO Design
1. The Engineering Problem
You have a working synchronous FIFO. The pointers, the wrap-distinguishing extra MSB, the full and empty flags — all correct, all proven in 7.2. It buffers a streaming source into a slower consumer, and in the lab it never drops a word. Then you integrate it with the real producer — a packetizer that reads a bus, formats a word, and writes it — and it starts overflowing under load. The FIFO is losing data even though full is asserting exactly when it should.
The problem is timing, not logic. Your full flag is correct: it goes high the cycle occupancy reaches DEPTH. But the producer that consumes that flag is not a single wire — it is a pipeline. From the cycle it sees full to the cycle it actually stops issuing writes, several cycles pass: the flag registers into the producer, the producer's own state machine reacts, an in-flight bus beat still completes. Call that reaction latency K. During those K cycles the producer keeps writing into a FIFO that is already full, and every one of those writes overflows. full did its job; it just arrived K cycles too late for a producer that cannot stop on a dime.
Hard full and hard empty share this flaw by construction: they mark the point of no remaining margin. They are the right flags for the FIFO's own internal correctness — you must never write when truly full or read when truly empty — but they are the wrong flags for flow control, because flow control needs warning, and a flag that fires at the last possible instant gives none. What you need is a flag that asserts early — while there is still room — so the producer's K-cycle reaction fits inside the remaining space. That flag is a programmable almost-full flag: a watermark set below DEPTH, chosen so the lead time it buys covers the producer's reaction latency. Its mirror, almost-empty, does the same for a consumer that needs warning before the FIFO runs dry. Both are comparators on the occupancy you already have — and the whole art is choosing the number they compare against.
// 7.2 gives you a correct occupancy and a correct hard-full flag:
wire [ADDRW:0] occupancy; // words currently stored (0..DEPTH)
wire full = (occupancy == DEPTH);
// But the producer does NOT stop the cycle it sees full. It has a pipeline:
// cycle 0: producer sees full=1
// cycle 1..K: in-flight writes STILL LAND <-- these overflow a full FIFO
// A flag at DEPTH gives ZERO cycles of warning to a producer that needs K.
// What flow control actually needs is an EARLY flag — a watermark below DEPTH:
// assign almost_full = (occupancy >= af_thresh); // af_thresh < DEPTH
// Choose af_thresh so (DEPTH - af_thresh) >= K, and the producer stops in time.2. Mental Model
3. Pattern Anatomy
The programmable-flag block sits on top of an existing FIFO. It reads one thing the FIFO already produces — the occupancy — and emits early warning flags. It has four parts.
Part 1 — occupancy (reused from 7.2). The flags compare against how full the FIFO is. You already have this: either an explicit up/down count register that increments on a write-without-read and decrements on a read-without-write, or the pointer difference wptr - rptr computed with the wrap-distinguishing extra MSB from 7.2. Either way, occupancy is a value in 0..DEPTH. This page does not re-derive it — it is the output of 7.2, and getting the wrong occupancy (mishandling the pointer wrap) is one of the §6 mistakes, not something this pattern fixes.
Part 2 — the watermark comparators (from 1.5). Two magnitude comparators on occupancy:
almost_full = (occupancy >= af_thresh)— asserts when the FIFO is at leastaf_threshfull.af_threshis the HIGH watermark.almost_empty = (occupancy <= ae_thresh)— asserts when the FIFO is at mostae_threshfull.ae_threshis the LOW watermark.
Each is exactly the 1.5 magnitude comparator, unsigned, on the occupancy width. The threshold can be a parameter (fixed at elaboration, cheapest, best when the reaction latency is known at design time) or a runtime input (a register the software programs, needed when the same block serves producers with different latencies). The RTL is identical; only where the number comes from changes.
Part 3 — hysteresis (assert level ≠ deassert level). A bare comparator has a single trip point, so occupancy dithering by one around it strobes the flag. Hysteresis gives the flag two trip points: almost_full asserts when occupancy >= af_set and deasserts only when occupancy <= af_clr, with af_clr < af_set. Between the two levels the flag holds its last state — which means the flag is now a tiny piece of sequential logic (a register with set/clear conditions), not pure combinational. The gap af_set - af_clr is the hysteresis band; make it wider than the expected dither and the flag stops chattering.
Part 4 — the reaction-latency margin (the whole point). This is not a signal; it is the constraint that sets af_thresh. If the producer needs K cycles to stop after seeing almost_full, and you want a guard slot of safety, then the flag must assert while K + guard slots are still free:
af_thresh <= DEPTH - K - guard
Read the other way: almost_full must lead hard full by DEPTH - af_thresh cycles of writing, and that lead must be >= K + guard. The comparator does not know K; you do, and you encode it in the threshold. The mirror holds for almost_empty and a consumer's reaction latency on the drain side.
The programmable-flag datapath — occupancy in, early warning out
data flowOne reasoning note closes the anatomy. Because almost_full and almost_empty are windows inside the hard full/empty limits, a well-chosen pair satisfies an ordering invariant: ae_thresh < af_thresh, af_thresh < DEPTH, and ae_thresh > 0 (or >= 0), so the two flags never overlap and always leave a defined middle band where neither is asserted. Violate that ordering — set af_thresh <= ae_thresh — and you get a FIFO that reports almost-full and almost-empty at the same time, which is meaningless. The thresholds are programmable, so this ordering is a constraint the programmer (parameter or software) must honour and verification must assert.
4. Real RTL Implementation
This is the core of the page. We build three things — (a) programmable almost_full / almost_empty comparators, (b) a hysteresis variant, and (c) the threshold-too-late bug (buggy vs margin-matched fix) — and each is shown in SystemVerilog, Verilog, and VHDL, with an RTL block and a self-checking clocked testbench. Every block builds on a FIFO occupancy in the style of 7.2 (an up/down count register), so the flags are just comparators layered on top. The idea is identical across all three languages; seeing them side by side is what makes the pattern language-independent.
Two disciplines run through every block. Reset is explicit and synchronous, active-high (rst), matching the v1 and 7.2 convention — the occupancy register clears to 0 on reset, so both flags take their reset-consistent values. Widths are deliberate: occupancy and the thresholds are ADDRW+1 bits wide (ADDRW = $clog2(DEPTH)), because occupancy ranges 0..DEPTH inclusive and DEPTH itself needs the extra bit — the same extra-MSB reasoning as the 7.2 pointer.
4a. Programmable almost_full / almost_empty — the comparator form
Start with the plain flags. Occupancy is maintained by an up/down count (write-without-read increments, read-without-write decrements — the 7.2 occupancy). The flags are two unsigned magnitude comparators against runtime-programmable thresholds af_thresh and ae_thresh. They are combinational on the registered occupancy, so they are latch-proof by construction — a single continuous relation with no missing path.
module fifo_pflags #(
parameter int WIDTH = 8,
parameter int DEPTH = 16,
localparam int ADDRW = $clog2(DEPTH) // occupancy is ADDRW+1 bits (0..DEPTH)
)(
input logic clk,
input logic rst, // synchronous, active-high
input logic wr_en, // write request (gated by !full elsewhere)
input logic rd_en, // read request (gated by !empty elsewhere)
input logic [ADDRW:0] af_thresh, // HIGH watermark (runtime programmable)
input logic [ADDRW:0] ae_thresh, // LOW watermark (runtime programmable)
output logic [ADDRW:0] occupancy, // words stored, 0..DEPTH
output logic almost_full, // occupancy >= af_thresh
output logic almost_empty // occupancy <= ae_thresh
);
// Occupancy register: the 7.2 up/down count. A write-without-read adds one,
// a read-without-write removes one; simultaneous read+write is a no-op.
logic [ADDRW:0] count;
always_ff @(posedge clk) begin
if (rst)
count <= '0;
else if (wr_en && !rd_en)
count <= count + 1'b1;
else if (rd_en && !wr_en)
count <= count - 1'b1;
end
assign occupancy = count;
// The flags are pure comparators on the registered occupancy (1.5 pattern).
// Unsigned magnitude compares; latch-proof because each is a total relation.
assign almost_full = (occupancy >= af_thresh); // early HIGH-water warning
assign almost_empty = (occupancy <= ae_thresh); // early LOW-water warning
endmoduleThe comparators are one line each; the value of af_thresh is what carries the engineering (§4c). The testbench is clocked — it fills the FIFO one word per cycle and checks that almost_full asserts on exactly the cycle occupancy first reaches af_thresh, then drains and checks almost_empty at ae_thresh. Because occupancy takes only DEPTH+1 values, sweeping the fill/drain is exhaustive over the threshold crossing.
module fifo_pflags_tb;
localparam int WIDTH = 8, DEPTH = 16, ADDRW = $clog2(DEPTH);
logic clk = 0, rst = 1, wr_en = 0, rd_en = 0;
logic [ADDRW:0] af_thresh, ae_thresh, occupancy;
logic almost_full, almost_empty;
int errors = 0;
fifo_pflags #(.WIDTH(WIDTH), .DEPTH(DEPTH)) dut (
.clk(clk), .rst(rst), .wr_en(wr_en), .rd_en(rd_en),
.af_thresh(af_thresh), .ae_thresh(ae_thresh),
.occupancy(occupancy), .almost_full(almost_full), .almost_empty(almost_empty));
always #5 clk = ~clk; // 100 MHz clock
initial begin
af_thresh = 12; // program the watermarks
ae_thresh = 3;
@(posedge clk); rst = 0; // release reset
// FILL one word/cycle and check almost_full trips exactly at af_thresh.
wr_en = 1; rd_en = 0;
repeat (DEPTH) begin
@(posedge clk); #1; // sample after occupancy updates
assert (almost_full === (occupancy >= af_thresh))
else begin $error("AF wrong: occ=%0d af=%0d flag=%b", occupancy, af_thresh, almost_full); errors++; end
end
wr_en = 0;
// DRAIN one word/cycle and check almost_empty trips exactly at ae_thresh.
rd_en = 1;
repeat (DEPTH) begin
@(posedge clk); #1;
assert (almost_empty === (occupancy <= ae_thresh))
else begin $error("AE wrong: occ=%0d ae=%0d flag=%b", occupancy, ae_thresh, almost_empty); errors++; end
end
rd_en = 0;
if (errors == 0) $display("PASS: flags trip at exactly af_thresh/ae_thresh over full fill/drain");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleThe Verilog form is identical in intent; only the typing (reg/wire) and the self-check idiom (if (...) $display("FAIL")) differ. The comparators are just as total, so just as latch-proof.
module fifo_pflags #(
parameter WIDTH = 8,
parameter DEPTH = 16,
parameter ADDRW = 4 // = clog2(DEPTH); set by instantiator
)(
input wire clk,
input wire rst, // synchronous, active-high
input wire wr_en,
input wire rd_en,
input wire [ADDRW:0] af_thresh, // HIGH watermark
input wire [ADDRW:0] ae_thresh, // LOW watermark
output wire [ADDRW:0] occupancy,
output wire almost_full,
output wire almost_empty
);
reg [ADDRW:0] count;
always @(posedge clk) begin
if (rst)
count <= {(ADDRW+1){1'b0}};
else if (wr_en && !rd_en)
count <= count + 1'b1; // write-without-read: +1
else if (rd_en && !wr_en)
count <= count - 1'b1; // read-without-write: -1
end
assign occupancy = count;
assign almost_full = (occupancy >= af_thresh); // unsigned magnitude compare
assign almost_empty = (occupancy <= ae_thresh);
endmodule module fifo_pflags_tb;
parameter WIDTH = 8, DEPTH = 16, ADDRW = 4;
reg clk, rst, wr_en, rd_en;
reg [ADDRW:0] af_thresh, ae_thresh;
wire [ADDRW:0] occupancy;
wire almost_full, almost_empty;
integer i, errors;
fifo_pflags #(.WIDTH(WIDTH), .DEPTH(DEPTH), .ADDRW(ADDRW)) dut (
.clk(clk), .rst(rst), .wr_en(wr_en), .rd_en(rd_en),
.af_thresh(af_thresh), .ae_thresh(ae_thresh),
.occupancy(occupancy), .almost_full(almost_full), .almost_empty(almost_empty));
initial begin clk = 0; forever #5 clk = ~clk; end
initial begin
errors = 0; rst = 1; wr_en = 0; rd_en = 0;
af_thresh = 12; ae_thresh = 3;
@(posedge clk); rst = 0;
wr_en = 1; rd_en = 0; // FILL
for (i = 0; i < DEPTH; i = i + 1) begin
@(posedge clk); #1;
if (almost_full !== (occupancy >= af_thresh)) begin
$display("FAIL: AF occ=%0d af=%0d flag=%b", occupancy, af_thresh, almost_full);
errors = errors + 1;
end
end
wr_en = 0; rd_en = 1; // DRAIN
for (i = 0; i < DEPTH; i = i + 1) begin
@(posedge clk); #1;
if (almost_empty !== (occupancy <= ae_thresh)) begin
$display("FAIL: AE occ=%0d ae=%0d flag=%b", occupancy, ae_thresh, almost_empty);
errors = errors + 1;
end
end
rd_en = 0;
if (errors == 0) $display("PASS: flags trip at exactly the thresholds");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleIn VHDL the occupancy is an unsigned register (from numeric_std) and the comparators are direct >= / <= on unsigned, converted to std_logic with a when/else. Using numeric_std makes the compare unambiguously unsigned, which is exactly what an occupancy count needs.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity fifo_pflags is
generic (
WIDTH : positive := 8;
DEPTH : positive := 16;
ADDRW : positive := 4 -- = ceil(log2(DEPTH))
);
port (
clk : in std_logic;
rst : in std_logic; -- synchronous, active-high
wr_en : in std_logic;
rd_en : in std_logic;
af_thresh : in unsigned(ADDRW downto 0); -- HIGH watermark
ae_thresh : in unsigned(ADDRW downto 0); -- LOW watermark
occupancy : out unsigned(ADDRW downto 0);
almost_full : out std_logic;
almost_empty : out std_logic
);
end entity;
architecture rtl of fifo_pflags is
signal count : unsigned(ADDRW downto 0); -- 7.2 up/down occupancy
begin
process (clk)
begin
if rising_edge(clk) then
if rst = '1' then
count <= (others => '0');
elsif wr_en = '1' and rd_en = '0' then
count <= count + 1; -- write-without-read: +1
elsif rd_en = '1' and wr_en = '0' then
count <= count - 1; -- read-without-write: -1
end if;
end if;
end process;
occupancy <= count;
-- Unsigned magnitude comparators (1.5), total relations => no latch.
almost_full <= '1' when count >= af_thresh else '0';
almost_empty <= '1' when count <= ae_thresh else '0';
end architecture;The VHDL testbench uses assert … report … severity error as its self-check, clocking a fill then a drain and checking each flag against the reference relation on the current occupancy.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity fifo_pflags_tb is
end entity;
architecture sim of fifo_pflags_tb is
constant WIDTH : positive := 8;
constant DEPTH : positive := 16;
constant ADDRW : positive := 4;
signal clk, rst, wr_en, rd_en : std_logic := '0';
signal af_thresh, ae_thresh, occ : unsigned(ADDRW downto 0);
signal almost_full, almost_empty : std_logic;
begin
dut : entity work.fifo_pflags
generic map (WIDTH => WIDTH, DEPTH => DEPTH, ADDRW => ADDRW)
port map (clk => clk, rst => rst, wr_en => wr_en, rd_en => rd_en,
af_thresh => af_thresh, ae_thresh => ae_thresh,
occupancy => occ, almost_full => almost_full, almost_empty => almost_empty);
clk <= not clk after 5 ns; -- free-running clock
stim : process
begin
af_thresh <= to_unsigned(12, ADDRW+1);
ae_thresh <= to_unsigned(3, ADDRW+1);
rst <= '1'; wait until rising_edge(clk); rst <= '0';
wr_en <= '1'; rd_en <= '0'; -- FILL
for i in 0 to DEPTH-1 loop
wait until rising_edge(clk); wait for 1 ns;
assert almost_full = ('1' when occ >= af_thresh else '0')
report "almost_full wrong at this occupancy" severity error;
end loop;
wr_en <= '0'; rd_en <= '1'; -- DRAIN
for i in 0 to DEPTH-1 loop
wait until rising_edge(clk); wait for 1 ns;
assert almost_empty = ('1' when occ <= ae_thresh else '0')
report "almost_empty wrong at this occupancy" severity error;
end loop;
rd_en <= '0';
report "fifo_pflags self-check complete" severity note;
wait;
end process;
end architecture;4b. Hysteresis — assert high, deassert low (stop the thrash)
A bare almost_full toggles every cycle when occupancy dithers by one around af_thresh — the producer hovering at the line. Hysteresis gives the flag two trip points: assert at af_set, deassert only at af_clr (af_clr < af_set), and hold between them. That makes the flag a small registered state machine, not a comparator — so it needs the clock and a reset.
module fifo_pflags_hyst #(
parameter int WIDTH = 8,
parameter int DEPTH = 16,
localparam int ADDRW = $clog2(DEPTH)
)(
input logic clk,
input logic rst,
input logic [ADDRW:0] occupancy, // from the FIFO (7.2)
input logic [ADDRW:0] af_set, // assert when occupancy >= af_set
input logic [ADDRW:0] af_clr, // deassert when occupancy <= af_clr (af_clr < af_set)
output logic almost_full // hysteretic: sticky between af_clr and af_set
);
// Registered flag with separate set/clear levels. Between af_clr and af_set
// the flag HOLDS its last value => no thrash while occupancy dithers.
always_ff @(posedge clk) begin
if (rst)
almost_full <= 1'b0;
else if (occupancy >= af_set)
almost_full <= 1'b1; // cross the HIGH level: assert
else if (occupancy <= af_clr)
almost_full <= 1'b0; // fall past the LOW level: deassert
// else: hold (the hysteresis band) — this is the anti-thrash memory
end
endmoduleThe testbench drives occupancy up across af_set, then wiggles it by one right at the boundary and asserts the flag does not toggle — the whole point of hysteresis — then drops it below af_clr and asserts it clears.
module fifo_pflags_hyst_tb;
localparam int WIDTH = 8, DEPTH = 16, ADDRW = $clog2(DEPTH);
logic clk = 0, rst = 1;
logic [ADDRW:0] occupancy, af_set, af_clr;
logic almost_full;
int errors = 0, toggles = 0;
logic prev_af;
fifo_pflags_hyst #(.WIDTH(WIDTH), .DEPTH(DEPTH)) dut (
.clk(clk), .rst(rst), .occupancy(occupancy),
.af_set(af_set), .af_clr(af_clr), .almost_full(almost_full));
always #5 clk = ~clk;
task step(input [ADDRW:0] occ); begin
occupancy = occ; @(posedge clk); #1;
if (almost_full !== prev_af) toggles++;
prev_af = almost_full;
end endtask
initial begin
af_set = 12; af_clr = 10; occupancy = 0; prev_af = 0;
@(posedge clk); rst = 0; @(posedge clk); #1; prev_af = almost_full;
step(11); // below set: still 0
assert (almost_full === 1'b0) else begin $error("should be clear at 11"); errors++; end
step(12); // hit set: assert
assert (almost_full === 1'b1) else begin $error("should assert at af_set"); errors++; end
toggles = 0; // now WIGGLE in the band [af_clr+1 .. af_set]
step(11); step(12); step(11); step(12); // dither by one at the line
assert (toggles === 0)
else begin $error("FLAG THRASHED in hysteresis band: %0d toggles", toggles); errors++; end
step(10); // fall to af_clr: deassert
assert (almost_full === 1'b0) else begin $error("should clear at af_clr"); errors++; end
if (errors == 0) $display("PASS: hysteresis holds across the band, clean set/clear");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleThe Verilog hysteresis RTL is the same registered set/clear flag with reg output and always @(posedge clk).
module fifo_pflags_hyst #(
parameter WIDTH = 8,
parameter DEPTH = 16,
parameter ADDRW = 4
)(
input wire clk,
input wire rst,
input wire [ADDRW:0] occupancy,
input wire [ADDRW:0] af_set, // assert level
input wire [ADDRW:0] af_clr, // clear level (af_clr < af_set)
output reg almost_full
);
always @(posedge clk) begin
if (rst)
almost_full <= 1'b0;
else if (occupancy >= af_set)
almost_full <= 1'b1; // cross HIGH: assert
else if (occupancy <= af_clr)
almost_full <= 1'b0; // fall past LOW: deassert
// else hold => hysteresis band, no thrash
end
endmodule module fifo_pflags_hyst_tb;
parameter WIDTH = 8, DEPTH = 16, ADDRW = 4;
reg clk, rst;
reg [ADDRW:0] occupancy, af_set, af_clr;
wire almost_full;
integer errors, toggles;
reg prev_af;
fifo_pflags_hyst #(.WIDTH(WIDTH), .DEPTH(DEPTH), .ADDRW(ADDRW)) dut (
.clk(clk), .rst(rst), .occupancy(occupancy),
.af_set(af_set), .af_clr(af_clr), .almost_full(almost_full));
initial begin clk = 0; forever #5 clk = ~clk; end
task step; input [ADDRW:0] occ; begin
occupancy = occ; @(posedge clk); #1;
if (almost_full !== prev_af) toggles = toggles + 1;
prev_af = almost_full;
end endtask
initial begin
errors = 0; toggles = 0; rst = 1;
af_set = 12; af_clr = 10; occupancy = 0; prev_af = 0;
@(posedge clk); rst = 0; @(posedge clk); #1; prev_af = almost_full;
step(11);
if (almost_full !== 1'b0) begin $display("FAIL: clear at 11"); errors = errors + 1; end
step(12);
if (almost_full !== 1'b1) begin $display("FAIL: set at af_set"); errors = errors + 1; end
toggles = 0; // WIGGLE at the boundary
step(11); step(12); step(11); step(12);
if (toggles !== 0) begin $display("FAIL: thrash, %0d toggles", toggles); errors = errors + 1; end
step(10);
if (almost_full !== 1'b0) begin $display("FAIL: clear at af_clr"); errors = errors + 1; end
if (errors == 0) $display("PASS: hysteresis holds, no thrash, clean set/clear");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleIn VHDL the hysteretic flag is a registered std_logic with the same set/clear/hold structure inside a clocked process.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity fifo_pflags_hyst is
generic ( WIDTH : positive := 8; DEPTH : positive := 16; ADDRW : positive := 4 );
port (
clk : in std_logic;
rst : in std_logic;
occupancy : in unsigned(ADDRW downto 0);
af_set : in unsigned(ADDRW downto 0); -- assert level
af_clr : in unsigned(ADDRW downto 0); -- clear level (af_clr < af_set)
almost_full : out std_logic
);
end entity;
architecture rtl of fifo_pflags_hyst is
signal af : std_logic;
begin
process (clk)
begin
if rising_edge(clk) then
if rst = '1' then
af <= '0';
elsif occupancy >= af_set then
af <= '1'; -- cross HIGH: assert
elsif occupancy <= af_clr then
af <= '0'; -- fall past LOW: deassert
end if; -- else hold => hysteresis band
end if;
end process;
almost_full <= af;
end architecture; library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity fifo_pflags_hyst_tb is
end entity;
architecture sim of fifo_pflags_hyst_tb is
constant ADDRW : positive := 4;
signal clk, rst : std_logic := '0';
signal occupancy : unsigned(ADDRW downto 0) := (others => '0');
signal af_set : unsigned(ADDRW downto 0);
signal af_clr : unsigned(ADDRW downto 0);
signal almost_full : std_logic;
signal prev_af : std_logic := '0';
signal toggles : integer := 0;
procedure step(constant occ : in integer;
signal occ_s : out unsigned(ADDRW downto 0);
signal clk_s : in std_logic) is
begin
occ_s <= to_unsigned(occ, ADDRW+1);
wait until rising_edge(clk_s); wait for 1 ns;
end procedure;
begin
dut : entity work.fifo_pflags_hyst
generic map (ADDRW => ADDRW)
port map (clk => clk, rst => rst, occupancy => occupancy,
af_set => af_set, af_clr => af_clr, almost_full => almost_full);
clk <= not clk after 5 ns;
-- count flag toggles concurrently
tog : process (clk)
begin
if rising_edge(clk) then
if almost_full /= prev_af then toggles <= toggles + 1; end if;
prev_af <= almost_full;
end if;
end process;
stim : process
begin
af_set <= to_unsigned(12, ADDRW+1);
af_clr <= to_unsigned(10, ADDRW+1);
rst <= '1'; wait until rising_edge(clk); rst <= '0';
step(11, occupancy, clk);
assert almost_full = '0' report "should be clear below af_set" severity error;
step(12, occupancy, clk);
assert almost_full = '1' report "should assert at af_set" severity error;
-- WIGGLE at the boundary; the flag must hold (no thrash)
step(11, occupancy, clk); step(12, occupancy, clk);
step(11, occupancy, clk); step(12, occupancy, clk);
assert almost_full = '1' report "flag thrashed in hysteresis band" severity error;
step(10, occupancy, clk);
assert almost_full = '0' report "should clear at af_clr" severity error;
report "hysteresis self-check complete" severity note;
wait;
end process;
end architecture;4c. The threshold-too-late bug — buggy vs margin-matched fix, in all three HDLs
Here is the signature failure, shown as buggy vs fixed threshold logic (dramatized narratively in §7). The bug is not in the comparator — the comparator is perfect. The bug is the value: the BUGGY block sets the high watermark to DEPTH - 1, assuming the producer stops the instant it sees almost_full. The FIXED block sets it to DEPTH - K - GUARD, matched to the producer's K-cycle reaction latency (and pairs it with hysteresis). Same comparator, different number, different outcome — one overflows, one does not.
// BUGGY: high watermark = DEPTH-1, as if the producer stopped INSTANTLY. A
// producer with a K-cycle pipeline pours K more words in after
// ALMOST_FULL asserts => the FIFO OVERFLOWS despite the flag.
module af_thresh_bad #(
parameter int DEPTH = 16,
localparam int ADDRW = $clog2(DEPTH)
)(
input logic [ADDRW:0] occupancy,
output logic almost_full
);
localparam int AF_THRESH = DEPTH - 1; // WRONG: zero reaction margin
assign almost_full = (occupancy >= AF_THRESH);
endmodule
// FIXED: high watermark leads hard-full by the producer's reaction latency K
// plus a guard slot, so the K in-flight writes still fit. (Add hysteresis
// on top — 4b — for a clean, debounced backpressure signal.)
module af_thresh_good #(
parameter int DEPTH = 16,
parameter int K = 4, // producer reaction latency (cycles)
parameter int GUARD = 1, // safety slot
localparam int ADDRW = $clog2(DEPTH)
)(
input logic [ADDRW:0] occupancy,
output logic almost_full
);
localparam int AF_THRESH = DEPTH - K - GUARD; // lead hard-full by K+GUARD
assign almost_full = (occupancy >= AF_THRESH);
endmoduleThe testbench models the producer's latency: it keeps writing for K cycles after almost_full first asserts (an in-flight pipeline), then checks the FIFO never exceeded DEPTH. The BUGGY threshold overflows; the FIXED threshold does not — and that is the whole lesson made mechanical.
module af_thresh_bug_tb;
localparam int DEPTH = 16, ADDRW = $clog2(DEPTH), K = 4;
logic clk = 0, rst = 1;
logic [ADDRW:0] occupancy = 0;
logic almost_full;
int errors = 0, stop_ctr;
logic stopping;
// Point the DUT at af_thresh_good to PASS; at af_thresh_bad to overflow.
af_thresh_good #(.DEPTH(DEPTH), .K(K), .GUARD(1)) dut (
.occupancy(occupancy), .almost_full(almost_full));
always #5 clk = ~clk;
// Producer model: writes every cycle until almost_full, then keeps writing
// for K MORE cycles (its pipeline drains) before it truly stops.
always @(posedge clk) begin
if (rst) begin occupancy <= 0; stopping <= 0; stop_ctr <= 0; end
else begin
if (almost_full && !stopping) begin stopping <= 1; stop_ctr <= K; end
if (!stopping || stop_ctr > 0) begin
if (occupancy < (1<<(ADDRW+1))-1) occupancy <= occupancy + 1; // a write lands
if (stopping && stop_ctr > 0) stop_ctr <= stop_ctr - 1;
end
// Invariant: a correctly-sized watermark keeps occupancy <= DEPTH.
if (occupancy > DEPTH) begin
$error("OVERFLOW: occupancy=%0d exceeded DEPTH=%0d (watermark too late)", occupancy, DEPTH);
errors++;
end
end
end
initial begin
@(posedge clk); rst = 0;
repeat (DEPTH + K + 4) @(posedge clk);
if (errors == 0) $display("PASS: watermark led hard-full by K; no overflow");
else $display("FAIL: %0d overflow cycles (threshold had no reaction margin)", errors);
$finish;
end
endmoduleThe Verilog buggy/fixed pair tells the same story with localparam thresholds and a reg-modelled producer. Binding the DUT to af_thresh_bad overflows; binding to af_thresh_good holds.
// BUGGY: watermark = DEPTH-1 => zero reaction margin => overflow with a pipelined producer.
module af_thresh_bad #(
parameter DEPTH = 16,
parameter ADDRW = 4
)(
input wire [ADDRW:0] occupancy,
output wire almost_full
);
localparam AF_THRESH = DEPTH - 1; // WRONG: no lead time
assign almost_full = (occupancy >= AF_THRESH);
endmodule
// FIXED: watermark = DEPTH - K - GUARD => leads hard-full by the producer latency.
module af_thresh_good #(
parameter DEPTH = 16,
parameter K = 4, // reaction latency
parameter GUARD = 1,
parameter ADDRW = 4
)(
input wire [ADDRW:0] occupancy,
output wire almost_full
);
localparam AF_THRESH = DEPTH - K - GUARD; // lead by K+GUARD slots
assign almost_full = (occupancy >= AF_THRESH);
endmodule module af_thresh_bug_tb;
parameter DEPTH = 16, ADDRW = 4, K = 4;
reg clk, rst;
reg [ADDRW:0] occupancy;
wire almost_full;
integer errors, stop_ctr;
reg stopping;
// Bind to af_thresh_good to PASS; to af_thresh_bad to observe the overflow.
af_thresh_good #(.DEPTH(DEPTH), .K(K), .GUARD(1), .ADDRW(ADDRW)) dut (
.occupancy(occupancy), .almost_full(almost_full));
initial begin clk = 0; forever #5 clk = ~clk; end
always @(posedge clk) begin
if (rst) begin occupancy <= 0; stopping <= 0; stop_ctr <= 0; end
else begin
if (almost_full && !stopping) begin stopping <= 1; stop_ctr <= K; end
if (!stopping || stop_ctr > 0) begin
if (occupancy < ((1<<(ADDRW+1))-1)) occupancy <= occupancy + 1; // write lands
if (stopping && stop_ctr > 0) stop_ctr <= stop_ctr - 1;
end
if (occupancy > DEPTH) begin
$display("FAIL: OVERFLOW occ=%0d > DEPTH=%0d (watermark too late)", occupancy, DEPTH);
errors = errors + 1;
end
end
end
initial begin
errors = 0; rst = 1; occupancy = 0;
@(posedge clk); rst = 0;
repeat (DEPTH + K + 4) @(posedge clk);
if (errors == 0) $display("PASS: watermark led hard-full by K; no overflow");
else $display("FAIL: %0d overflow cycles", errors);
$finish;
end
endmoduleIn VHDL the thresholds are generics/constants and the producer model is a clocked process; af_thresh_bad sets the watermark to DEPTH-1, af_thresh_good to DEPTH - K - GUARD.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
-- BUGGY: watermark = DEPTH-1 => no reaction margin => overflow with a K-cycle producer.
entity af_thresh_bad is
generic ( DEPTH : positive := 16; ADDRW : positive := 4 );
port (
occupancy : in unsigned(ADDRW downto 0);
almost_full : out std_logic
);
end entity;
architecture rtl of af_thresh_bad is
constant AF_THRESH : integer := DEPTH - 1; -- WRONG: zero lead time
begin
almost_full <= '1' when to_integer(occupancy) >= AF_THRESH else '0';
end architecture;
-- FIXED: watermark leads hard-full by the reaction latency K plus a guard slot.
entity af_thresh_good is
generic ( DEPTH : positive := 16; K : natural := 4; GUARD : natural := 1; ADDRW : positive := 4 );
port (
occupancy : in unsigned(ADDRW downto 0);
almost_full : out std_logic
);
end entity;
architecture rtl of af_thresh_good is
constant AF_THRESH : integer := DEPTH - K - GUARD; -- lead by K+GUARD slots
begin
almost_full <= '1' when to_integer(occupancy) >= AF_THRESH else '0';
end architecture; library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity af_thresh_bug_tb is
end entity;
architecture sim of af_thresh_bug_tb is
constant DEPTH : positive := 16;
constant ADDRW : positive := 4;
constant K : natural := 4;
signal clk, rst : std_logic := '0';
signal occupancy : unsigned(ADDRW downto 0) := (others => '0');
signal almost_full : std_logic;
signal stopping : boolean := false;
signal stop_ctr : natural := 0;
begin
-- Bind to af_thresh_good to PASS; to af_thresh_bad to observe the overflow.
dut : entity work.af_thresh_good
generic map (DEPTH => DEPTH, K => K, GUARD => 1, ADDRW => ADDRW)
port map (occupancy => occupancy, almost_full => almost_full);
clk <= not clk after 5 ns;
-- Producer model: after almost_full asserts, K more writes still land.
prod : process (clk)
begin
if rising_edge(clk) then
if rst = '1' then
occupancy <= (others => '0'); stopping <= false; stop_ctr <= 0;
else
if almost_full = '1' and not stopping then
stopping <= true; stop_ctr <= K;
end if;
if (not stopping) or (stop_ctr > 0) then
occupancy <= occupancy + 1; -- a write lands
if stopping and stop_ctr > 0 then
stop_ctr <= stop_ctr - 1;
end if;
end if;
-- Invariant: a margin-matched watermark keeps occupancy <= DEPTH.
assert to_integer(occupancy) <= DEPTH
report "OVERFLOW: occupancy exceeded DEPTH (watermark too late)" severity error;
end if;
end if;
end process;
stim : process
begin
rst <= '1'; wait until rising_edge(clk); rst <= '0';
for i in 0 to DEPTH + K + 4 loop
wait until rising_edge(clk);
end loop;
report "af_thresh self-check complete (no overflow)" severity note;
wait;
end process;
end architecture;Across all three languages the lesson is one sentence: the comparator is trivial and always correct; the number it compares against is a system-timing decision, and setting the watermark to DEPTH - 1 instead of DEPTH - K - guard is the difference between a flag that helps and a FIFO that overflows.
5. Verification Strategy
A programmable flag is a comparator, so part of its correctness is a combinational truth check; but because the useful behaviour (hysteresis, reaction-latency margin) is temporal, the testbench must be clocked and must fill/drain the FIFO cycle by cycle. The single invariant that captures a correct flag pair is:
almost_fullis exactlyoccupancy >= af_thresh,almost_emptyis exactlyoccupancy <= ae_thresh, the two never overlap (ae_thresh < af_thresh < DEPTH), and — the one that actually matters —almost_fullleads hardfullby at least the consumer's reaction latency, so a latency-Kproducer never overflows.
Everything below is that invariant made checkable, and it maps directly onto the §4 testbenches.
- Flags trip at the exact threshold occupancy (self-checking, exhaustive). Fill the FIFO one word per cycle and assert
almost_full === (occupancy >= af_thresh)on every cycle; drain it and assertalmost_empty === (occupancy <= ae_thresh). Because occupancy takes onlyDEPTH+1values, a single fill+drain sweep is exhaustive over the threshold crossing — it proves the flag trips at exactlyaf_threshand not one word early or late. This is the §4a testbench in all three HDLs (SVassert, Verilogif (...) $display("FAIL"), VHDLassert … severity error). - Programmable-threshold coverage. The threshold is programmable, so verify it is programmable: re-run the fill/drain sweep for several
af_thresh/ae_threshvalues (a low watermark, a mid one, one adjacent toDEPTH) and confirm the flag tracks each. A flag that is right for one threshold but hardwired to another passes a single-value test and fails in the field. - Hysteresis holds — no thrash at the boundary. Drive occupancy to
af_set, then dither it by one across the boundary (af_set,af_set-1,af_set, …) and count flag toggles: the count must be zero while occupancy stays in the band(af_clr, af_set]. Then push belowaf_clrand confirm a single clean deassert. This is the §4b test; a bare comparator (no hysteresis) fails it with one toggle per wiggle. - almost_full leads hard-full by the margin (the load-bearing check). Model the producer's latency: keep writing for
Kcycles afteralmost_fullfirst asserts, then assertoccupancy <= DEPTHthroughout. With a margin-matched watermark (af_thresh <= DEPTH - K - guard) the invariant holds; with the naiveDEPTH-1watermark it is violated — the FIFO overflows. This §4c test is what a functional-only "does the flag assert?" test never catches, because the flag does assert; it just asserts too late. - Non-overlap / ordering invariant. Assert
ae_thresh < af_threshandaf_thresh < DEPTHandae_thresh >= 0at the start of the run (and, for hysteresis,af_clr < af_set). Then no reachable occupancy makes both flags high at once; a run that setsaf_thresh <= ae_threshmust be flagged as an illegal programming, not silently simulated. - Parameter-generic verification (DEPTH, WIDTH, K). Re-run the whole suite for
DEPTH = 8, 16, 32and a producer latencyK = 1, 4— the margin formulaDEPTH - K - guardmust keep occupancy bounded at every combination. A page that only testsDEPTH=16, K=4can hide a width or wrap bug that shows up at another depth. - Expected waveform. On a fill,
occupancyramps0 → DEPTHandalmost_fullgoes high exactly asoccupancyfirst equalsaf_thresh— visibly beforefull, with the gapDEPTH - af_threshcycles wide. With hysteresis,almost_fullstays high through a boundary wiggle and drops only whenoccupancyfalls toaf_clr. The failure signature of the §7 bug is the opposite:almost_fullasserts, yetoccupancykeeps climbing pastDEPTHforKmore cycles — the overflow the flag was supposed to prevent.
6. Common Mistakes
Threshold too tight — no reaction-latency margin. The signature bug. The engineer sets af_thresh = DEPTH - 1 (or even DEPTH), reasoning "warn me one slot before full." But the producer does not stop in zero cycles; it has a K-cycle pipeline, and in those K cycles it writes K more words into a FIFO with only one free slot — overflow, despite almost_full asserting perfectly. The flag was correct; the value had no margin. The fix is af_thresh <= DEPTH - K - guard, where K is the producer's measured reaction latency. This is not a comparator bug — the comparator is flawless — it is a system-timing bug hiding in a constant. (Full post-mortem in §7.)
Flag thrashing at the boundary (no hysteresis). When occupancy hovers at the watermark and wiggles by one — a producer writing at exactly the drain rate — a bare comparator toggles the flag every cycle. Downstream, that strobing backpressure makes the producer start/stop repeatedly, wrecking throughput and, if the flag crosses a clock domain, generating a burst of pulses to (mis)synchronize. The fix is hysteresis: assert at af_set, deassert at a lower af_clr, and hold between — the §4b variant. A single trip point is a thrash waiting for a producer that sits on the line.
Comparing the wrong occupancy (pointer-wrap mishandled). The flags are only as correct as the occupancy they compare. If occupancy is computed as a raw wptr - rptr without the wrap-distinguishing extra MSB from 7.2, then after a wrap the difference is wrong and the flags trip at the wrong fill level — sometimes never asserting, sometimes asserting when nearly empty. This pattern consumes occupancy; it does not fix it. Compute occupancy correctly (7.2's up/down count, or the extra-MSB pointer difference) before you compare it.
Off-by-one on the threshold (>= vs >, inclusive vs exclusive). occupancy >= af_thresh asserts at af_thresh; occupancy > af_thresh asserts one word later. The two differ by a single slot of margin, which is exactly the margin a tight design cannot spare. Decide whether the watermark is the first asserting occupancy (>=) or the last non-asserting one, state it, and verify the flag trips on that exact word — the §4a exhaustive fill sweep pins it. The same care applies to almost_empty's <= vs <.
Making the flag combinational when it drives real backpressure across a domain. A pure combinational almost_full glitches during occupancy transitions and, if it crosses a clock boundary, those glitches can be sampled as spurious asserts. Where the flag drives cross-domain flow control, register it (the hysteretic form already is registered) so it is a clean, glitch-free, single-source-of-truth signal. A raw comparator output is fine inside one domain and a hazard across two.
7. DebugLab
The FIFO that overflowed while almost_full was high — a watermark with no reaction margin
The engineering lesson: a programmable flag is a comparator, but its value is a system-timing decision. The comparator occupancy >= af_thresh is always correct; what makes the FIFO overflow is a threshold set as if the flag's consumer reacts instantly. Nobody reacts instantly — a producer has a pipeline, an FSM, in-flight transactions — so the watermark must lead hard-full by the consumer's reaction latency K (plus a guard): af_thresh = DEPTH - K - guard. The tell is a bug that depends on rate: correct when slow, overflowing at full speed, because the in-flight write count scales with throughput. Two habits make it impossible: choose the watermark from the reaction latency, not from "one before full," and verify by modelling that latency — keep K writes in flight after the flag asserts and prove occupancy never crosses DEPTH. A flag that fires too late to help is worse than no flag, because it looks like it is working.
8. Interview Q&A
9. Exercises
Design and reasoning exercises — no syntax drills. Each rehearses the "the value is a timing decision" habit.
Exercise 1 — Size the watermark
A depth-32 FIFO is written by a producer whose reaction latency, from seeing almost_full to stopping, is measured at 6 cycles (2 flag-register stages + 4 in-flight bus beats). You want a 2-slot guard. (a) Compute af_thresh. (b) State how many free slots remain when almost_full asserts, and show the 6 in-flight writes all fit. (c) If a future revision registers the flag one more time before the producer, what is the new K and the new af_thresh?
Exercise 2 — Diagnose the rate-dependent overflow
A FIFO overflows only at full throughput and is clean when the producer is throttled. On a waveform, almost_full is clearly asserting, yet occupancy climbs past DEPTH. (i) Explain in one sentence why the failure is rate-dependent. (ii) Name the single constant most likely wrong and what it was probably set to. (iii) Give the check you would add to the testbench to reproduce it deterministically, and (iv) explain why a "does almost_full assert?" functional test passes despite the bug.
Exercise 3 — Design the hysteresis band
A producer writes at almost exactly the consumer's drain rate, so occupancy dithers by ±1 around af_thresh and the bare flag thrashes. (a) Convert the single-threshold flag to a hysteretic one: choose af_set and af_clr given the ±1 dither and a desire to tolerate up to ±3 without toggling. (b) Explain what the flag does when occupancy sits inside the band. (c) State one downstream symptom the thrash caused that the hysteresis fixes, and one new cost hysteresis introduces (hint: think about how late the flag now clears).
Exercise 4 — Choose combinational vs registered
The same almost_full flag is used in two designs: (A) it gates a producer FSM in the same clock domain; (B) it is shipped to a different clock domain to throttle a remote source through a two-flop synchronizer. For each, decide whether the flag should be combinational or registered and justify it in one line. Then explain how design B's extra register stage(s) must change the af_thresh you compute, and why forgetting that re-creates the §7 overflow.
10. Related Tutorials
Continue in RTL Design Patterns (forward links unlock as they ship):
- Synchronous FIFO Architecture — Chapter 7.1; the FIFO this page decorates — the RAM, the read/write pointers, and where the occupancy the flags compare against comes from.
- FIFO Full & Empty Logic — Chapter 7.2; the hard limits and the wrap-distinguishing occupancy this page compares a programmable threshold against — the direct prerequisite.
- FIFO Depth Calculation — Chapter 7.4; how you size
DEPTHin the first place from producer/consumer rates — the number the watermarks live inside. - valid/ready Handshake — Chapter 9.1; the handshake an almost-full flag ultimately throttles by deasserting
readyupstream. - Backpressure & Stall Propagation — Chapter 9.2; how an almost-full flag becomes a stall that ripples back through a pipeline.
- Credit-Based Flow Control — Chapter 9.4; the alternative to watermarks — the producer tracks credits so it never over-issues, trading a flag for a counter.
Backward / in-track dependencies:
- Comparators — Chapter 1.5; the magnitude comparator (
occupancy >= af_thresh) that is every programmable flag — this page is that primitive applied to occupancy. - Up/Down Counters — Chapter 3; the up/down
countthat maintains the FIFO occupancy the flags read. - The Register Pattern (D-FF) — Chapter 2.1; the registered flag the hysteresis variant becomes (set/clear/hold is a D-FF with enables).
- Multiplexers — Chapter 1.1; the sibling combinational primitive, and where the default-assignment discipline these flags rely on was established.
Backward / method:
- What Is an RTL Design Pattern? — Chapter 0.1; why an almost-full flag earns the name "pattern" — a recurring need, a reusable comparator form, and a signature failure (the too-late watermark).
- The RTL Design Mindset — Chapter 0.2; the Interface → State → Datapath → Control → Verification lenses — a programmable flag is control derived from datapath occupancy, and its correctness is a system-timing argument.
Verilog v1 prerequisites (the grammar this pattern transcribes into):
- Relational Operators — the
>=/<=magnitude compares the watermark flags are built from. - If-Else Statements — the set/clear/hold structure of the hysteretic flag.
- Blocking and Non-Blocking Assignments — why the occupancy register and the hysteretic flag use non-blocking (
<=) in their clocked blocks. - Physical Data Types — the
reg/wireand vector-width discipline behind theADDRW+1-wide occupancy and thresholds.
11. Summary
- Hard full/empty are already too late for flow control. They mark zero remaining margin, and no real producer stops in zero cycles — a producer with a
K-cycle reaction latency writesKmore words after seeingfulland overflows anyway. Flow control needs warning, and a last-instant flag gives none. - A programmable flag is a comparator on occupancy.
almost_full = (occupancy >= af_thresh),almost_empty = (occupancy <= ae_thresh)— the 1.5 magnitude comparator on the 7.2 occupancy. The threshold is a parameter (fixed, cheap) or a runtime input (software-programmed). The RTL is trivial; the engineering is entirely in the value. - The watermark must lead hard-full by the reaction latency.
af_thresh = DEPTH - K - guard, whereKis the cycles from the flag asserting to the producer actually stopping. Set it toDEPTH-1("one before full") and the flag fires too late — the FIFO overflows despite the flag, the §7 signature bug, and the failure is rate-dependent (fine when slow, overflowing at full speed). - Hysteresis stops the thrash. Assert at
af_set, deassert at a loweraf_clr, hold between — so occupancy dithering at the line does not strobe the flag. It also makes the flag registered, hence glitch-free for crossing clock domains. A single trip point thrashes the moment a producer sits on the boundary. - Verify the value, not just the flag. Sweep the fill/drain to prove the flag trips at exactly the threshold (exhaustive over
DEPTH+1occupancies), check hysteresis holds through a boundary wiggle, and — the load-bearing test — model the producer'sK-cycle latency and assert occupancy never exceedsDEPTH. A "does the flag assert?" test passes even when the watermark is fatally too late; only the latency-modelling test catches it. Self-checking clocked testbenches shown here in SystemVerilog, Verilog, and VHDL.