Skip to content

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.

the-need.v — full is correct, and still too late for a latency-K producer
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 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 least af_thresh full. af_thresh is the HIGH watermark.
  • almost_empty = (occupancy <= ae_thresh) — asserts when the FIFO is at most ae_thresh full. ae_thresh is 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 flow
The programmable-flag datapath — occupancy in, early warning outoccupancy (7.2)count or wptr - rptr, in 0..DEPTHalmost_fullcompareoccupancy >= af_thresh (1.5 comparator)almost_emptycompareoccupancy <= ae_thresh (1.5 comparator)hysteresisset at af_set, clear at af_clr < af_setreaction-latencymarginaf_thresh <= DEPTH - K - guardproducer /consumerthrottles within its K-cycle latency
Occupancy (from 7.2) feeds two magnitude comparators (from 1.5) against programmable thresholds. almost_full crosses hysteresis (assert high, clear low) to avoid thrashing, and its threshold is set by the reaction-latency margin so the flag leads hard-full by the producer's K cycles. The comparators are trivial; the load-bearing part is the margin and the hysteresis band. Same structure in SystemVerilog, Verilog, and VHDL.

One 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.

fifo_pflags.sv — programmable almost_full / almost_empty (WIDTH/DEPTH generic)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

The 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.

fifo_pflags_tb.sv — clocked fill/drain; assert flags trip at the exact threshold
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

The 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.

fifo_pflags.v — programmable flags in Verilog (up/down count + comparators)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
fifo_pflags_tb.v — self-checking clocked fill/drain (PASS/FAIL)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

In 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.

fifo_pflags.vhd — programmable flags in VHDL (numeric_std unsigned occupancy)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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.

fifo_pflags_tb.vhd — self-checking clocked fill/drain (assert report severity)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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.

fifo_pflags_hyst.sv — almost_full with hysteresis (set at af_set, clear at af_clr)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

The 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.

fifo_pflags_hyst_tb.sv — wiggle at the boundary; assert NO thrash, clean set/clear
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

The Verilog hysteresis RTL is the same registered set/clear flag with reg output and always @(posedge clk).

fifo_pflags_hyst.v — hysteretic almost_full in Verilog (registered set/clear)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
fifo_pflags_hyst_tb.v — self-checking; count toggles in the band (must be 0)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

In VHDL the hysteretic flag is a registered std_logic with the same set/clear/hold structure inside a clocked process.

fifo_pflags_hyst.vhd — hysteretic almost_full in VHDL (registered set/clear/hold)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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;
fifo_pflags_hyst_tb.vhd — self-checking; assert no thrash in the band (assert report severity)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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.

af_thresh_bug.sv — BUGGY (no margin) vs FIXED (margin = reaction latency)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 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);
   endmodule

The 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.

af_thresh_bug_tb.sv — model a K-cycle producer; assert occupancy never exceeds DEPTH
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

The 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.

af_thresh_bug.v — BUGGY vs FIXED threshold in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 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
af_thresh_bug_tb.v — self-checking; K-cycle producer, assert no overflow
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

In 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.

af_thresh_bug.vhd — BUGGY vs FIXED threshold in VHDL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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;
af_thresh_bug_tb.vhd — self-checking; K-cycle producer, assert occupancy <= DEPTH
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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_full is exactly occupancy >= af_thresh, almost_empty is exactly occupancy <= ae_thresh, the two never overlap (ae_thresh < af_thresh < DEPTH), and — the one that actually matters — almost_full leads hard full by at least the consumer's reaction latency, so a latency-K producer 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 assert almost_empty === (occupancy <= ae_thresh). Because occupancy takes only DEPTH+1 values, a single fill+drain sweep is exhaustive over the threshold crossing — it proves the flag trips at exactly af_thresh and not one word early or late. This is the §4a testbench in all three HDLs (SV assert, Verilog if (...) $display("FAIL"), VHDL assert … 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_thresh values (a low watermark, a mid one, one adjacent to DEPTH) 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 below af_clr and 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 K cycles after almost_full first asserts, then assert occupancy <= DEPTH throughout. With a margin-matched watermark (af_thresh <= DEPTH - K - guard) the invariant holds; with the naive DEPTH-1 watermark 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_thresh and af_thresh < DEPTH and ae_thresh >= 0 at 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 sets af_thresh <= ae_thresh must be flagged as an illegal programming, not silently simulated.
  • Parameter-generic verification (DEPTH, WIDTH, K). Re-run the whole suite for DEPTH = 8, 16, 32 and a producer latency K = 1, 4 — the margin formula DEPTH - K - guard must keep occupancy bounded at every combination. A page that only tests DEPTH=16, K=4 can hide a width or wrap bug that shows up at another depth.
  • Expected waveform. On a fill, occupancy ramps 0 → DEPTH and almost_full goes high exactly as occupancy first equals af_thresh — visibly before full, with the gap DEPTH - af_thresh cycles wide. With hysteresis, almost_full stays high through a boundary wiggle and drops only when occupancy falls to af_clr. The failure signature of the §7 bug is the opposite: almost_full asserts, yet occupancy keeps climbing past DEPTH for K more 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.

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 DEPTH in 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 ready upstream.
  • 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 count that 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):

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 writes K more words after seeing full and 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, where K is the cycles from the flag asserting to the producer actually stopping. Set it to DEPTH-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 lower af_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+1 occupancies), check hysteresis holds through a boundary wiggle, and — the load-bearing test — model the producer's K-cycle latency and assert occupancy never exceeds DEPTH. 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.