Skip to content

RTL Design Patterns · Chapter 13 · Case Studies

Streaming Block with FIFOs & Backpressure

This capstone case study builds a complete streaming datapath by composing patterns you already know rather than teaching a new one. An input FIFO absorbs a bursty source, a stall-able processing pipeline transforms the data, and an output FIFO holds finished words while a slow sink catches up. The key idea is the wiring: when the sink stops accepting words, backpressure propagates backward. The output FIFO fills and stalls the whole pipeline, the frozen pipeline stops draining the input FIFO, and the input FIFO then stalls the source. Data flows forward, ready flows backward, and every word arrives exactly once in order. You build all three pieces, wire the backpressure end to end, and prove it with a scoreboard under aggressive backpressure, in SystemVerilog, Verilog, and VHDL.

Advanced20 min readRTL Design PatternsStreamingFIFOBackpressureValid/ReadyPipeline StallFlow ControlBurst Absorption

Chapter 13 · Section 13.3 · Case Studies

1. The Engineering Problem

You are building a block in the middle of a datapath — say a unit that transforms each word of a stream (increments it, packs it, computes a running function) — and it sits between two neighbours you do not control. Upstream is a source that produces words in bursts: idle for a while, then a run of back-to-back words faster than you can finish processing them. Downstream is a sink that consumes words but occasionally stalls: it goes busy, stops taking words for a stretch, then resumes. Your block has one clock and must, every cycle, do the right thing regardless of what those two neighbours are doing this cycle. It cannot ask the source to slow down mid-burst on a whim, and it cannot make the sink take a word it has no room for. What it can do is buffer, and signal.

This is not a contrived puzzle — it is the shape of nearly every streaming block on a chip. A packet filter, a CRC engine, a color-space converter, a decompressor, a DMA descriptor processor: each is fed by an upstream that bursts and feeds a downstream that stalls, and each must keep the stream conserved — every word in comes out exactly once, in order — across arbitrary rate mismatches. Get it wrong and the failure is silent and catastrophic: a word dropped under load that never reproduces on the bench, or a word duplicated, or the stream reordered. These are the worst bugs in a chip because they only appear when the source bursts and the sink stalls at the same time — the exact condition a casual testbench never creates.

You cannot solve this by making the block faster, and you cannot solve it by hoping the rates match — they never do. You need three things wired correctly: a buffer at the ingress that absorbs a burst so the source is not stalled the instant it speeds up; a processing stage that can freeze in place when it cannot move a word forward; and a buffer at the egress that holds finished words while the sink is busy — all bound by a flow-control contract that lets a downstream stall ripple backward to the source so no word is ever pushed into a place with no room. That is the streaming block, and the reason it is a capstone is that it is the exam for the FIFO (Chapter 7), the stall-able pipeline (Chapter 8), and the valid/ready handshake (Chapter 9) at once.

the-interface.v — a stream in, a stream out, ready flowing back on each side
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // ---- ingress: a valid/ready face to the (bursty) source ----
   input  wire             in_valid;   // source is offering a word this cycle
   input  wire [WIDTH-1:0] in_data;    // the word being offered
   output wire             in_ready;   // block has room: in_ready = the INPUT FIFO is not full
   //   a word is accepted on the ONE cycle in_valid && in_ready are both high
 
   // ---- egress: a valid/ready face to the (stalling) sink ----
   output wire             out_valid;  // block is offering a processed word
   output wire [WIDTH-1:0] out_data;   // the processed word
   input  wire             out_ready;  // sink can take a word this cycle
   //   a word leaves on the ONE cycle out_valid && out_ready are both high
 
   // The contract, both sides: DATA flows forward, READY flows backward. A word never
   // moves unless the receiver has room. When out_ready drops, that stall propagates
   // BACKWARD through the block until, if it lasts, in_ready drops to the source.

2. Mental Model

3. Pattern Anatomy

The streaming block is three blocks bound by two handshakes and one stall chain. Take the forward path first (what carries the data), then the backward path (how a stall ripples upstream), then the two pieces of engineering judgment that decide correctness: sizing the input FIFO for burst absorption, and the flow-conservation invariant.

The forward path — source, input FIFO, pipeline, output FIFO, sink. A word enters on the ingress handshake, is written into the input FIFO when there is room, is read out and fed into the pipeline, is transformed stage by stage, is written into the output FIFO, and leaves on the egress handshake when the sink is ready. Every boundary between two blocks is a valid/ready pair (9.1): a word moves across a boundary on exactly the cycle its valid and the receiver's ready are both high, so the data path is a chain of buffers each of which can independently be full or empty. This is what lets the source and sink run at different, jittery rates: the buffers take up the slack.

The streaming path — source to sink, valid forward and ready backward at every boundary

data flow
The streaming path — source to sink, valid forward and ready backward at every boundaryin_valid && in_readyread on not-emptypush on stagevalidout_valid &&out_readysource (bursty)offers in_valid/in_data; runs free until in_ready fallsinput FIFO (7.1)absorbs the burst; in_ready = not full; read into pipelinepipeline(8.3/8.4)per-stage valid + transform; one enable en freezes alloutput FIFO (7.1)holds finished words; out_valid = not empty; full stalls pipesink (stalling)takes a word on out_valid && out_ready; may de-assert ready
The forward path, left to right in space, at whatever rates the two ends run. The source offers words on a valid/ready ingress; the input FIFO absorbs a burst so the source runs free until the buffer fills, at which point in_ready falls (the source then holds its word — the valid/ready contract, so nothing is lost). The pipeline reads from the input FIFO, runs a small transform with a valid bit riding alongside each word, and pushes finished words into the output FIFO. The output FIFO holds those words for the sink and offers them on out_valid; the sink takes one on out_valid && out_ready and may de-assert ready whenever it is busy. Every boundary is a valid/ready pair, so each buffer can independently be full or empty and the two ends run at different, jittery rates while the stream stays conserved. Identical in structure across SystemVerilog, Verilog, and VHDL; only the RTL spelling differs.

The backward path — how a downstream stall ripples upstream, one buffer at a time. The genuinely new work of the capstone is not any one block but the chain of ready signals that runs opposite the data. When the sink de-asserts out_ready, the output FIFO can no longer be read, so it fills; a full output FIFO drives the pipeline's stall, so the pipeline's en goes low and every stage freezes in place (bubbles, no word pushed toward the full FIFO); a frozen pipeline stops reading the input FIFO, so it fills; and a full input FIFO drives in_ready low to the source. Each block cannot accept a word until the next block has room, and that condition is what walks the stall backward. Assert the whole chain correctly and no word is ever pushed into a full buffer; break one link (a stage that pushes without checking the next has room) and that is precisely where a word is dropped.

Backpressure propagation — ready flowing backward, a sink stall rippling upstream stage by stage

data flow
Backpressure propagation — ready flowing backward, a sink stall rippling upstream stage by stagenoreadfull -> stallno drainfull ->!readysink de-assertsout_readydownstream goes busy and stops taking wordsoutput FIFO fills-> fullcannot be read -> occupancy rises to fullpipeline stalls(en = 0)full out-FIFO drives stall; ALL stages freeze as oneinput FIFO fills-> fullfrozen pipe stops draining it -> occupancy risesin_ready falls tosourcefull in-FIFO de-asserts in_ready; source holds word
The backward path, the capstone's real content. When the sink de-asserts out_ready the output FIFO can no longer be read, so its occupancy climbs to full; a full output FIFO asserts the pipeline's stall, so en goes low and every pipeline stage freezes together (injecting bubbles, pushing nothing toward the full FIFO); the frozen pipeline stops draining the input FIFO, so its occupancy climbs to full; and a full input FIFO drives in_ready low, so the source stops and HOLDS its current word. The stall does not appear everywhere at once — it ripples backward one buffer at a time, and each buffer's depth is how many cycles of grace the upstream gets before the stall reaches it. Because each block accepts only when the next has room, no word is ever pushed into a full buffer, and when the sink resumes the chain releases in the same order it filled. This backward READY chain, not any single FIFO, is what makes the block correct — and breaking one link is exactly the dropped-word bug of §7. Identical across SystemVerilog, Verilog, and VHDL.

Sizing the input FIFO — burst absorption is a 7.4 depth calculation. The input FIFO exists to cover the case where the source runs faster than the block can drain it for a bounded burst. If the source can deliver BURST words back-to-back (one per cycle) while the block, under steady processing, retires one word every RD_EVERY cycles, then during the burst window the pile-up is BURST - floor(BURST / RD_EVERY) words — and that is exactly the minimum input-FIFO depth that keeps the source from ever seeing in_ready fall during the burst (round up to a power of two for the 7.2 pointer scheme). Make it deeper and you absorb longer bursts before backpressure reaches the source; make it shallower and the source stalls sooner (which is safe — no word is lost — but costs throughput). The output FIFO is sized symmetrically for how long the sink can stall before the pipeline must freeze. This is the one architectural knob of the block, and it is a Chapter 7.4 calculation, not a guess.

The flow-conservation invariant — every word in appears once at the output, in order. The whole correctness of a streaming block reduces to one sentence: the sequence of words that transfer in (on in_valid && in_ready) equals, after the transform, the sequence that transfers out (on out_valid && out_ready) — same words, same order, none dropped, none duplicated — for every interleaving of source bursts and sink stalls. The FIFOs preserve order (they are first-in-first-out); the pipeline preserves order (a stage's word always leaves before the next stage's word behind it); the handshakes ensure a word moves exactly once (only on the transfer cycle). Conservation is not something you add — it falls out of every boundary being a correct valid/ready transfer and every buffer being FIFO-ordered. The signature bug of §7 is exactly a boundary that is not a correct transfer: a stage that pushes a word into a full output FIFO, dropping it, so conservation fails only under sustained backpressure.

4. Real RTL Implementation

This is the core of the page. We build the streaming block incrementally — (a) the input FIFO with its valid/ready ingress, (b) the stall-able processing pipeline, (c) the output FIFO with its valid/ready egress, then (d) the integrated, parameterized stream_block that wires backpressure end to end — and show the integrated block plus its scoreboard self-checking testbench (bursty source, random-ready sink) and the dropped-word bug-vs-fix in SystemVerilog, Verilog, and VHDL. The idea is identical across the three; only the spelling differs.

Three conventions run through every block, each an earlier lesson made concrete:

  • Every boundary is a valid/ready transfer (9.1). A word moves across any boundary on exactly the cycle its valid and the receiver's ready are both high. in_ready is the input FIFO's not-full; out_valid is the output FIFO's not-empty. Nothing moves into a place with no room.
  • One shared clock-enable freezes the whole pipeline (8.4). A single stall condition drives en = ~stall; every pipeline register updates only when en is high. Freeze one stage and you freeze them all — a partial stall overwrites held data (§6).
  • Synchronous, active-high reset to empty. After reset both FIFOs are empty, all pipeline valid bits are low, and no spurious word is in flight. Registers use non-blocking assignment; combinational logic (the flags, the transform) uses continuous or blocking form — the Verilog v1 discipline.

4a. Building block (i) — the input FIFO with a valid/ready ingress (from 7.1)

The input FIFO is the synchronous FIFO of 7.1 with the occupancy-based full/empty of 7.2, given a 9.1 face: in_ready is !full, a write happens on in_valid && in_ready. This is the shock absorber — while it has room the source runs free; when it fills, in_ready falls and the source holds its word. We reuse a single parameterized FIFO for both ends of the block.

sfifo.sv — synchronous FIFO reused at both ends (occupancy full/empty, valid/ready ready)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module sfifo #(
       parameter int WIDTH = 8,
       parameter int DEPTH = 8,                        // power of two -> pointers wrap for free
       localparam int AW    = $clog2(DEPTH)
   )(
       input  logic             clk,
       input  logic             rst,                   // synchronous, active-high
       // write side (a valid/ready SINK)
       input  logic             wr_valid,              // producer offers a word
       input  logic [WIDTH-1:0] wr_data,
       output logic             wr_ready,              // = !full : room for a word
       // read side (a valid/ready SOURCE)
       output logic             rd_valid,              // = !empty : a word is available
       output logic [WIDTH-1:0] rd_data,               // combinational head (FWFT-style)
       input  logic             rd_ready               // consumer can take a word
   );
       logic [WIDTH-1:0] mem [DEPTH];
       logic [AW-1:0]    wptr, rptr;
       logic [AW:0]      count;                        // occupancy 0..DEPTH needs AW+1 bits
 
       assign wr_ready = (count != DEPTH[AW:0]);       // not full
       assign rd_valid = (count != '0);                // not empty
       assign rd_data  = mem[rptr];                    // head word, combinationally visible
 
       // A transfer happens ONLY on valid && ready on each side (9.1) — the gate that
       // makes 'never write when full, never read when empty' true by construction.
       wire do_write = wr_valid & wr_ready;
       wire do_read  = rd_valid & rd_ready;
 
       always_ff @(posedge clk) begin
           if (rst) begin
               wptr <= '0; rptr <= '0; count <= '0;
           end else begin
               if (do_write) begin mem[wptr] <= wr_data; wptr <= wptr + 1'b1; end
               if (do_read)  begin                       rptr <= rptr + 1'b1; end
               case ({do_write, do_read})               // occupancy: +1 / -1 / hold
                   2'b10:   count <= count + 1'b1;
                   2'b01:   count <= count - 1'b1;
                   default: count <= count;             // both or neither -> unchanged
               endcase
           end
       end
   endmodule

At the ingress this FIFO is a sink: wire the source's in_valid/in_data to wr_valid/wr_data and expose wr_ready as in_ready. The block reads its own head off rd_valid/rd_data and asserts rd_ready only when the pipeline can accept — which is the whole backpressure story. The same module, instantiated a second time, is the output FIFO in 4c.

4b. Building block (ii) — the stall-able processing pipeline (from 8.3/8.4)

The pipeline is the per-stage-valid pipeline of 8.3 made stall-able per 8.4. A small transform (+1 here — any per-word function drops in) runs in each of two stages; a valid bit rides alongside the data; and a single shared enable en advances all stages together or freezes them all. It reads from the input FIFO when it can accept (en and the head is valid) and pushes into the output FIFO when its last stage is valid. The stall comes from downstream: en is low when the block cannot place a finished word (the output FIFO is full).

stream_pipe.sv — 2-stage transform, per-stage valid, ONE shared enable (8.4 stall)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module stream_pipe #(
       parameter int WIDTH = 8
   )(
       input  logic             clk,
       input  logic             rst,
       input  logic             en,                    // ONE shared clock-enable: advance ALL or freeze ALL
       // upstream (from input FIFO head)
       input  logic             s_valid,               // a word is available at the input
       input  logic [WIDTH-1:0] s_data,
       // downstream (to output FIFO)
       output logic             m_valid,               // last stage holds a finished word
       output logic [WIDTH-1:0] m_data
   );
       // Two stages, each carrying a valid bit alongside the data (8.3). The transform is
       // combinational per stage; here a +1 stands in for any per-word function.
       logic             v0, v1;
       logic [WIDTH-1:0] d0, d1;
 
       always_ff @(posedge clk) begin
           if (rst) begin
               v0 <= 1'b0; v1 <= 1'b0; d0 <= '0; d1 <= '0;
           end else if (en) begin                       // 8.4: register updates ONLY when enabled
               // stage 0: accept the input word, apply the transform, mark it valid
               d0 <= s_data + 1'b1;                      // <-- the per-word transform
               v0 <= s_valid;                           // valid rides with the data; bubble if s_valid=0
               // stage 1: shift stage 0 forward (identity transform in this stage)
               d1 <= d0;
               v1 <= v0;
           end
           // else STALL: no assignment -> BOTH stages hold as a unit (never a partial freeze)
       end
 
       assign m_valid = v1;
       assign m_data  = d1;
   endmodule

Two facts make this correct. First, the enable is shared: on a stall neither stage updates, so the pipeline freezes as one and no upstream stage overwrites a downstream one — the §6 collision cannot happen. Second, the valid bit is what makes a bubble harmless: when the input head is not valid (or the pipeline is fed a gap), v0 goes low and that invalid slot flows down the pipe carrying no word, so the output FIFO is only ever pushed a real word (gate the push on m_valid). The block above knows nothing about why it is stalled; whoever drives en owns the backpressure.

4c. Building block (iii) — the output FIFO with a valid/ready egress (from 7.1)

The output FIFO is the same sfifo instantiated facing the downstream. Now it is a source on its read side: out_valid is rd_valid (not empty), a word leaves on out_valid && out_ready. On its write side it is a sink for the pipeline: it is written when the pipeline's last stage is valid and the FIFO has room. The load-bearing signal is the FIFO's wr_ready on the pipeline-facing side — that not-full is what tells the pipeline it may advance. When the sink stalls and this FIFO fills, wr_ready (its write-side not-full) goes low, and that is the pipeline's stall. No new module — the anatomy is entirely in how 4d wires the three pieces.

4d. The integrated streaming block — parameterized, in SystemVerilog

One module: sfifo (input) + stream_pipe + sfifo (output), with the backpressure chain wired explicitly. Read the ingress off in_ready and the egress off out_valid/out_data. The critical wire is en: the pipeline advances only when the output FIFO can accept whatever the pipeline would push this cycle.

stream_block.sv — integrated streaming block: in-FIFO + stall-able pipeline + out-FIFO, backpressure end to end
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module stream_block #(
       parameter int WIDTH     = 8,
       parameter int IN_DEPTH  = 8,                     // absorbs the source burst (7.4)
       parameter int OUT_DEPTH = 4                      // absorbs the sink stall
   )(
       input  logic             clk,
       input  logic             rst,                    // synchronous, active-high
       // ingress (from bursty source)
       input  logic             in_valid,
       input  logic [WIDTH-1:0] in_data,
       output logic             in_ready,
       // egress (to stalling sink)
       output logic             out_valid,
       output logic [WIDTH-1:0] out_data,
       input  logic             out_ready
   );
       // ---- input FIFO: ingress sink; in_ready = its write-side not-full ----
       logic             if_rd_valid, if_rd_ready;
       logic [WIDTH-1:0] if_rd_data;
       sfifo #(.WIDTH(WIDTH), .DEPTH(IN_DEPTH)) in_fifo (
           .clk(clk), .rst(rst),
           .wr_valid(in_valid), .wr_data(in_data), .wr_ready(in_ready),   // <- in_ready to source
           .rd_valid(if_rd_valid), .rd_data(if_rd_data), .rd_ready(if_rd_ready));
 
       // ---- output FIFO: egress source; out_valid = its read-side not-empty ----
       logic             of_wr_valid, of_wr_ready;
       logic [WIDTH-1:0] of_wr_data;
       sfifo #(.WIDTH(WIDTH), .DEPTH(OUT_DEPTH)) out_fifo (
           .clk(clk), .rst(rst),
           .wr_valid(of_wr_valid), .wr_data(of_wr_data), .wr_ready(of_wr_ready),
           .rd_valid(out_valid), .rd_data(out_data), .rd_ready(out_ready));   // <- out to sink
 
       // ---- pipeline between the two FIFOs ----
       logic             p_m_valid;
       logic [WIDTH-1:0] p_m_data;
 
       // BACKPRESSURE, the whole point: the pipeline may advance only when the output FIFO
       // can accept what it would push this cycle. en low freezes the WHOLE pipeline (8.4),
       // which stops draining the input FIFO, which fills, which drops in_ready. One line
       // carries the stall backward:
       wire en = of_wr_ready | ~p_m_valid;              // room downstream, OR nothing to push
       // The pipeline consumes an input-FIFO word only when it actually advances a real slot.
       assign if_rd_ready = en & if_rd_valid;           // read the head only when we can take it
       // Push into the output FIFO only a REAL word (gate on the pipeline's valid), and only
       // when the FIFO has room — this is the gate the §7 bug removes.
       assign of_wr_valid = p_m_valid;
       assign of_wr_data  = p_m_data;
 
       stream_pipe #(.WIDTH(WIDTH)) pipe (
           .clk(clk), .rst(rst), .en(en),
           .s_valid(if_rd_ready),                       // a word is consumed from the FIFO on this cycle
           .s_data(if_rd_data),
           .m_valid(p_m_valid), .m_data(p_m_data));
   endmodule

One line carries the block's correctness: en = of_wr_ready | ~p_m_valid. The pipeline advances when the output FIFO has room (of_wr_ready) or when the last stage holds nothing to push (~p_m_valid, a bubble). It freezes precisely when it holds a real word and the output FIFO is full — the exact moment a word would otherwise be dropped. Because en low also stops if_rd_ready from consuming the input head, the input FIFO stops draining and fills, so in_ready (its write-side not-full) falls to the source. Data forward, ready backward, no word lost. The scoreboard testbench proves it under a bursty source and a random-ready sink.

stream_block_tb.sv — scoreboard: bursty source, random-ready sink; assert in-order exactly-once, no loss
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module stream_block_tb;
       localparam int WIDTH = 8, IN_DEPTH = 8, OUT_DEPTH = 4;
       logic clk = 1'b0, rst;
       logic in_valid, in_ready, out_valid, out_ready;
       logic [WIDTH-1:0] in_data, out_data;
       int errors = 0, sent = 0, got = 0;
 
       stream_block #(.WIDTH(WIDTH), .IN_DEPTH(IN_DEPTH), .OUT_DEPTH(OUT_DEPTH)) dut (
           .clk(clk), .rst(rst),
           .in_valid(in_valid), .in_data(in_data), .in_ready(in_ready),
           .out_valid(out_valid), .out_data(out_data), .out_ready(out_ready));
 
       always #5 clk = ~clk;
 
       // REFERENCE: the block adds 1 to each word, in order. Model it as a queue of the
       // transformed words in the exact order they were accepted at the ingress.
       logic [WIDTH-1:0] expect_q[$];
 
       // Ingress driver: BURSTY source — random runs of back-to-back offers with gaps.
       initial begin
           in_valid = 1'b0; in_data = '0; rst = 1'b1;
           repeat (4) @(posedge clk); rst = 1'b0;
           for (int w = 0; w < 40; w++) begin
               in_valid = 1'b1;
               in_data  = w[WIDTH-1:0];
               @(posedge clk);
               while (!(in_valid && in_ready)) @(posedge clk);   // wait for the transfer
               expect_q.push_back(w[WIDTH-1:0] + 1'b1);           // reference = transformed word
               sent++;
               if (($urandom % 3) == 0) begin                    // random gap -> bursty pattern
                   in_valid = 1'b0; repeat ($urandom % 4) @(posedge clk);
               end
           end
           in_valid = 1'b0;
       end
 
       // Egress checker + random-ready SINK: assert out_ready randomly (aggressive stalls).
       initial out_ready = 1'b0;
       always @(posedge clk) out_ready <= ($urandom % 2);         // ~50% ready: gappy backpressure
 
       always @(posedge clk) if (!rst && out_valid && out_ready) begin
           logic [WIDTH-1:0] exp;
           if (expect_q.size() == 0) begin $error("word out with empty reference (duplicate?)"); errors++; end
           else begin
               exp = expect_q.pop_front();
               if (out_data !== exp) begin $error("out=%0d exp=%0d (order/loss)", out_data, exp); errors++; end
               got++;
           end
       end
 
       initial begin
           wait (sent == 40);
           repeat (400) @(posedge clk);                          // let the pipe + FIFOs drain
           if (got !== sent)  begin $error("delivered %0d of %0d (word lost)", got, sent); errors++; end
           if (errors == 0) $display("PASS: %0d words delivered in order, exactly once, under backpressure", got);
           else             $display("FAIL: %0d errors", errors);
           $finish;
       end
   endmodule

The scoreboard is the whole verification: sent counts words accepted at the ingress, got counts words delivered at the egress, the queue enforces order (the head must match), an empty queue on a delivered word catches a duplicate, and got == sent at the end catches a dropped word. Run with an always-ready sink and it passes trivially; run with the random-ready sink above — the aggressive backpressure — and it is the test that separates a correct block from one with the §7 bug.

4e. The signature bug — the pipeline pushes into a FULL output FIFO (buggy vs fixed)

Every streaming failure that survives a bench test lives in the backpressure not reaching the pipeline. The sharpest is the last stage pushing a word into the output FIFO every cycle its own data is valid, without checking the FIFO has room: when the sink stalls and the output FIFO fills, the next pushed word is dropped (the FIFO refuses the write) while the pipeline blithely advances. It 'works' whenever the sink is always ready — the FIFO never fills — and silently loses data the moment the sink stalls for long enough.

stream_en.sv — BUGGY (pipeline free-runs into a full FIFO) vs FIXED (gate en on out-FIFO room)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: the pipeline is ALWAYS enabled and always pushes when it has a valid word,
   //   with NO check that the output FIFO has room. When the sink stalls and the FIFO
   //   fills, of_wr_valid is high but of_wr_ready is low, so the write is REFUSED and the
   //   word the pipeline just advanced past is DROPPED. Passes when the sink is always
   //   ready (FIFO never fills); loses words only under sustained backpressure.
   module stream_ctrl_bad (
       input  logic of_wr_ready,     // output FIFO not-full (IGNORED by the bug)
       input  logic p_m_valid,       // pipeline last stage holds a real word
       input  logic if_rd_valid,     // input FIFO head valid
       output logic en,              // pipeline clock-enable
       output logic if_rd_ready,     // consume the input head
       output logic of_wr_valid      // push into output FIFO
   );
       assign en          = 1'b1;                    // BUG: pipeline NEVER stalls...
       assign if_rd_ready = if_rd_valid;             // ...so it drains the input FIFO always...
       assign of_wr_valid = p_m_valid;               // ...and pushes even into a FULL out-FIFO -> DROP
   endmodule
 
   // FIXED: en advances the pipeline only when the output FIFO can accept what would be
   //   pushed (of_wr_ready) OR there is nothing to push (~p_m_valid, a bubble). A stall
   //   freezes the WHOLE pipeline, stops draining the input FIFO, and ripples to in_ready.
   module stream_ctrl_good (
       input  logic of_wr_ready,
       input  logic p_m_valid,
       input  logic if_rd_valid,
       output logic en,
       output logic if_rd_ready,
       output logic of_wr_valid
   );
       assign en          = of_wr_ready | ~p_m_valid;   // FIX: freeze when a real word can't be placed
       assign if_rd_ready = en & if_rd_valid;           // consume the head only when we truly advance
       assign of_wr_valid = p_m_valid;                  // push a real word (into a FIFO that has room)
   endmodule
stream_en_tb.sv — random-ready sink: the free-running control drops words, the gated one does not
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module stream_en_tb;
       // Bind the DUT to stream_ctrl_good to PASS; to _bad to watch words vanish under
       // sustained backpressure. The stimulus is the same aggressive random-ready sink.
       localparam int WIDTH = 8, IN_DEPTH = 8, OUT_DEPTH = 4;
       logic clk = 1'b0, rst;
       logic in_valid, in_ready, out_valid, out_ready;
       logic [WIDTH-1:0] in_data, out_data;
       int errors = 0, sent = 0, got = 0;
       logic [WIDTH-1:0] expect_q[$];
 
       // (stream_block instantiated with the good/bad control selected internally)
       stream_block #(.WIDTH(WIDTH), .IN_DEPTH(IN_DEPTH), .OUT_DEPTH(OUT_DEPTH)) dut (
           .clk(clk), .rst(rst),
           .in_valid(in_valid), .in_data(in_data), .in_ready(in_ready),
           .out_valid(out_valid), .out_data(out_data), .out_ready(out_ready));
 
       always #5 clk = ~clk;
       initial out_ready = 1'b0;
       // Hold the sink OFF for a long stretch (force the output FIFO full), then dribble.
       initial begin
           repeat (60) @(posedge clk); out_ready = 1'b0;         // long stall: FIFO fills
           forever begin @(posedge clk); out_ready <= ($urandom % 4 == 0); end
       end
       initial begin
           in_valid = 0; in_data = 0; rst = 1;
           repeat (4) @(posedge clk); rst = 0;
           for (int w = 0; w < 20; w++) begin
               in_valid = 1; in_data = w[WIDTH-1:0]; @(posedge clk);
               while (!(in_valid && in_ready)) @(posedge clk);
               expect_q.push_back(w[WIDTH-1:0] + 1'b1); sent++;
           end
           in_valid = 0;
       end
       always @(posedge clk) if (!rst && out_valid && out_ready) begin
           logic [WIDTH-1:0] exp = expect_q.size() ? expect_q.pop_front() : 'x;
           if (out_data !== exp) errors++; else got++;
       end
       initial begin
           wait (sent == 20); repeat (600) @(posedge clk);
           // FIXED: got == 20, no drops. BUGGY: got < 20 — words pushed into the full FIFO vanished.
           if (got == sent && errors == 0) $display("PASS: all %0d words survived sustained backpressure", got);
           else $display("FAIL: delivered %0d of %0d (dropped under backpressure)", got, sent);
           $finish;
       end
   endmodule

4f. The integrated streaming block in Verilog

The same three blocks with reg/wire typing. The FIFO's occupancy math, the pipeline's shared enable, and the one backpressure line are unchanged; only the spelling differs.

sfifo.v + stream_block.v — streaming block in Verilog (occupancy FIFO, shared-enable pipeline)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module sfifo #(
       parameter WIDTH = 8, DEPTH = 8, AW = 3        // AW = clog2(DEPTH), set by instantiator
   )(
       input  wire             clk, rst,
       input  wire             wr_valid,
       input  wire [WIDTH-1:0] wr_data,
       output wire             wr_ready,
       output wire             rd_valid,
       output wire [WIDTH-1:0] rd_data,
       input  wire             rd_ready
   );
       reg  [WIDTH-1:0] mem [0:DEPTH-1];
       reg  [AW-1:0]    wptr, rptr;
       reg  [AW:0]      count;                        // AW+1 bits: occupancy 0..DEPTH
       assign wr_ready = (count != DEPTH);            // not full
       assign rd_valid = (count != 0);                // not empty
       assign rd_data  = mem[rptr];
       wire do_write = wr_valid & wr_ready;
       wire do_read  = rd_valid & rd_ready;
       always @(posedge clk) begin
           if (rst) begin wptr<=0; rptr<=0; count<=0; end
           else begin
               if (do_write) begin mem[wptr]<=wr_data; wptr<=wptr+1'b1; end
               if (do_read)                            rptr<=rptr+1'b1;
               case ({do_write, do_read})
                   2'b10:   count <= count + 1'b1;
                   2'b01:   count <= count - 1'b1;
                   default: count <= count;
               endcase
           end
       end
   endmodule
 
   module stream_block #(
       parameter WIDTH = 8, IN_DEPTH = 8, OUT_DEPTH = 4
   )(
       input  wire             clk, rst,
       input  wire             in_valid,
       input  wire [WIDTH-1:0] in_data,
       output wire             in_ready,
       output wire             out_valid,
       output wire [WIDTH-1:0] out_data,
       input  wire             out_ready
   );
       wire             if_rd_valid, if_rd_ready;  wire [WIDTH-1:0] if_rd_data;
       wire             of_wr_valid, of_wr_ready;  wire [WIDTH-1:0] of_wr_data;
       // 2-stage transform pipeline, per-stage valid, ONE shared enable (8.4)
       reg  v0, v1;  reg [WIDTH-1:0] d0, d1;
       wire en = of_wr_ready | ~v1;                  // freeze when a real word can't be placed
       always @(posedge clk) begin
           if (rst) begin v0<=1'b0; v1<=1'b0; d0<=0; d1<=0; end
           else if (en) begin
               d0 <= if_rd_data + 1'b1; v0 <= if_rd_ready;   // stage0: transform + accept
               d1 <= d0;                v1 <= v0;            // stage1: shift forward
           end
       end
       assign if_rd_ready = en & if_rd_valid;        // consume input head only when advancing
       assign of_wr_valid = v1;                      // push a REAL word only
       assign of_wr_data  = d1;
 
       sfifo #(.WIDTH(WIDTH), .DEPTH(IN_DEPTH), .AW(3)) in_fifo (
           .clk(clk), .rst(rst),
           .wr_valid(in_valid), .wr_data(in_data), .wr_ready(in_ready),
           .rd_valid(if_rd_valid), .rd_data(if_rd_data), .rd_ready(if_rd_ready));
       sfifo #(.WIDTH(WIDTH), .DEPTH(OUT_DEPTH), .AW(2)) out_fifo (
           .clk(clk), .rst(rst),
           .wr_valid(of_wr_valid), .wr_data(of_wr_data), .wr_ready(of_wr_ready),
           .rd_valid(out_valid), .rd_data(out_data), .rd_ready(out_ready));
   endmodule
stream_block_tb.v — self-checking Verilog scoreboard: in-order exactly-once under random-ready sink
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module stream_block_tb;
       parameter WIDTH=8, IN_DEPTH=8, OUT_DEPTH=4, N=32;
       reg clk, rst, in_valid, out_ready;  reg [WIDTH-1:0] in_data;
       wire in_ready, out_valid;  wire [WIDTH-1:0] out_data;
       integer sent, got, errors, w, gap;
       reg [WIDTH-1:0] q [0:255];  integer qh, qt;    // simple FIFO reference queue
 
       stream_block #(.WIDTH(WIDTH),.IN_DEPTH(IN_DEPTH),.OUT_DEPTH(OUT_DEPTH)) dut (
           .clk(clk),.rst(rst),.in_valid(in_valid),.in_data(in_data),.in_ready(in_ready),
           .out_valid(out_valid),.out_data(out_data),.out_ready(out_ready));
 
       initial clk=1'b0;  always #5 clk=~clk;
 
       // random-ready sink: aggressive backpressure
       always @(posedge clk) out_ready <= ($random % 2);
 
       // egress checker: order via the queue, duplicate if queue empty, count for loss
       always @(posedge clk) if (!rst && out_valid && out_ready) begin
           if (qh == qt) begin $display("FAIL: word out, reference empty (dup)"); errors=errors+1; end
           else begin
               if (out_data !== q[qh]) begin $display("FAIL: out=%0d exp=%0d", out_data, q[qh]); errors=errors+1; end
               qh = qh + 1;  got = got + 1;
           end
       end
 
       initial begin
           errors=0; sent=0; got=0; qh=0; qt=0; in_valid=0; in_data=0; out_ready=0; rst=1;
           repeat (4) @(posedge clk); rst=0;
           for (w=0; w<N; w=w+1) begin
               in_valid=1'b1; in_data=w[WIDTH-1:0]; @(posedge clk);
               while (!(in_valid && in_ready)) @(posedge clk);
               q[qt]=w[WIDTH-1:0]+1'b1; qt=qt+1; sent=sent+1;     // reference = transformed word
               if (($random % 3)==0) begin in_valid=1'b0; for (gap=0; gap<($random%4); gap=gap+1) @(posedge clk); end
           end
           in_valid=1'b0;
           repeat (400) @(posedge clk);
           if (got!=sent) begin $display("FAIL: delivered %0d of %0d (lost)", got, sent); errors=errors+1; end
           if (errors==0) $display("PASS: %0d words in order, exactly once, under backpressure", got);
           else           $display("FAIL: %0d errors", errors);
           $finish;
       end
   endmodule

The Verilog bug-vs-fix pair is the same one-line change to the enable: a pipeline that never stalls (en = 1) pushes into a full FIFO and drops words; the fix gates en on the output FIFO having room.

stream_en.v — BUGGY (en=1, free-run into full FIFO) vs FIXED (en gated on out-FIFO room)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: pipeline never stalls, so it drains the input FIFO and pushes even when the
   //   output FIFO is full -> the refused write DROPS the word. Fine until the sink stalls.
   module stream_ctrl_bad (
       input  wire of_wr_ready, v1, if_rd_valid,
       output wire en, if_rd_ready, of_wr_valid );
       assign en          = 1'b1;                    // BUG: never freezes
       assign if_rd_ready = if_rd_valid;             // always drains the input FIFO
       assign of_wr_valid = v1;                      // pushes into a FULL out-FIFO -> drop
   endmodule
 
   // FIXED: freeze the whole pipeline when a real word cannot be placed.
   module stream_ctrl_good (
       input  wire of_wr_ready, v1, if_rd_valid,
       output wire en, if_rd_ready, of_wr_valid );
       assign en          = of_wr_ready | ~v1;       // FIX: stall on full out-FIFO
       assign if_rd_ready = en & if_rd_valid;        // consume head only when advancing
       assign of_wr_valid = v1;
   endmodule

4g. The integrated streaming block in VHDL

VHDL states the same three blocks with numeric_std counters and enumerated occupancy. The FIFO's not-full/not-empty flags, the pipeline's shared enable, and the one backpressure line are identical in meaning.

sfifo.vhd — synchronous FIFO in VHDL (numeric_std occupancy, valid/ready)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity sfifo is
       generic ( WIDTH : positive := 8; DEPTH : positive := 8 );
       port (
           clk, rst : in  std_logic;
           wr_valid : in  std_logic;
           wr_data  : in  std_logic_vector(WIDTH-1 downto 0);
           wr_ready : out std_logic;
           rd_valid : out std_logic;
           rd_data  : out std_logic_vector(WIDTH-1 downto 0);
           rd_ready : in  std_logic );
   end entity;
 
   architecture rtl of sfifo is
       type mem_t is array (0 to DEPTH-1) of std_logic_vector(WIDTH-1 downto 0);
       signal mem   : mem_t := (others => (others => '0'));
       signal wptr, rptr : integer range 0 to DEPTH-1 := 0;
       signal count : integer range 0 to DEPTH := 0;          -- occupancy
       signal do_write, do_read : std_logic;
   begin
       wr_ready <= '1' when count /= DEPTH else '0';           -- not full
       rd_valid <= '1' when count /= 0     else '0';           -- not empty
       rd_data  <= mem(rptr);                                  -- head word
       do_write <= wr_valid and (('1') when count /= DEPTH else '0');
       do_read  <= rd_ready and (('1') when count /= 0     else '0');
 
       process (clk) begin
           if rising_edge(clk) then
               if rst = '1' then
                   wptr <= 0; rptr <= 0; count <= 0;
               else
                   if do_write = '1' then mem(wptr) <= wr_data;
                       if wptr = DEPTH-1 then wptr <= 0; else wptr <= wptr + 1; end if; end if;
                   if do_read = '1' then
                       if rptr = DEPTH-1 then rptr <= 0; else rptr <= rptr + 1; end if; end if;
                   if do_write = '1' and do_read = '0' then count <= count + 1;
                   elsif do_write = '0' and do_read = '1' then count <= count - 1;
                   end if;                                     -- both or neither: unchanged
               end if;
           end if;
       end process;
   end architecture;
stream_block.vhd — integrated streaming block in VHDL (shared-enable pipeline, one backpressure line)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity stream_block is
       generic ( WIDTH : positive := 8; IN_DEPTH : positive := 8; OUT_DEPTH : positive := 4 );
       port (
           clk, rst  : in  std_logic;
           in_valid  : in  std_logic;
           in_data   : in  std_logic_vector(WIDTH-1 downto 0);
           in_ready  : out std_logic;
           out_valid : out std_logic;
           out_data  : out std_logic_vector(WIDTH-1 downto 0);
           out_ready : in  std_logic );
   end entity;
 
   architecture rtl of stream_block is
       signal if_rd_valid, if_rd_ready : std_logic;
       signal if_rd_data : std_logic_vector(WIDTH-1 downto 0);
       signal of_wr_valid, of_wr_ready : std_logic;
       signal of_wr_data : std_logic_vector(WIDTH-1 downto 0);
       signal v0, v1, en : std_logic;
       signal d0, d1 : std_logic_vector(WIDTH-1 downto 0);
   begin
       -- BACKPRESSURE: advance the pipeline only when the output FIFO has room, or there
       -- is nothing to push (a bubble). en low freezes BOTH stages together (8.4).
       en          <= of_wr_ready or (not v1);
       if_rd_ready <= en and if_rd_valid;           -- consume the input head only when advancing
       of_wr_valid <= v1;                           -- push a REAL word only
       of_wr_data  <= d1;
 
       -- 2-stage transform pipeline, per-stage valid, ONE shared enable
       pipe : process (clk) begin
           if rising_edge(clk) then
               if rst = '1' then
                   v0 <= '0'; v1 <= '0'; d0 <= (others=>'0'); d1 <= (others=>'0');
               elsif en = '1' then
                   d0 <= std_logic_vector(unsigned(if_rd_data) + 1);  -- transform
                   v0 <= if_rd_ready;                                 -- valid rides with data
                   d1 <= d0; v1 <= v0;                                -- shift forward
               end if;                                                -- else: hold as a unit
           end if;
       end process;
 
       in_fifo : entity work.sfifo
           generic map (WIDTH => WIDTH, DEPTH => IN_DEPTH)
           port map (clk=>clk, rst=>rst,
                     wr_valid=>in_valid, wr_data=>in_data, wr_ready=>in_ready,
                     rd_valid=>if_rd_valid, rd_data=>if_rd_data, rd_ready=>if_rd_ready);
       out_fifo : entity work.sfifo
           generic map (WIDTH => WIDTH, DEPTH => OUT_DEPTH)
           port map (clk=>clk, rst=>rst,
                     wr_valid=>of_wr_valid, wr_data=>of_wr_data, wr_ready=>of_wr_ready,
                     rd_valid=>out_valid, rd_data=>out_data, rd_ready=>out_ready);
   end architecture;
stream_block_tb.vhd — self-checking VHDL scoreboard: bursty source, random-ready sink (severity error)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity stream_block_tb is end entity;
 
   architecture sim of stream_block_tb is
       constant WIDTH : positive := 8;
       signal clk : std_logic := '0';
       signal rst, in_valid, in_ready, out_valid, out_ready : std_logic := '0';
       signal in_data, out_data : std_logic_vector(WIDTH-1 downto 0) := (others=>'0');
       type q_t is array (0 to 255) of std_logic_vector(WIDTH-1 downto 0);
       signal q : q_t;  signal qh, qt : integer := 0;         -- reference FIFO queue
       signal sent, got, errors : integer := 0;
       signal seed1 : integer := 42;
   begin
       dut : entity work.stream_block
           generic map (WIDTH=>WIDTH, IN_DEPTH=>8, OUT_DEPTH=>4)
           port map (clk=>clk, rst=>rst, in_valid=>in_valid, in_data=>in_data, in_ready=>in_ready,
                     out_valid=>out_valid, out_data=>out_data, out_ready=>out_ready);
 
       clk <= not clk after 5 ns;
 
       -- random-ready sink: toggle out_ready pseudo-randomly (aggressive backpressure)
       sink : process (clk) begin
           if rising_edge(clk) then
               seed1 <= (seed1 * 1103515245 + 12345) mod 2147483647;
               if (seed1 mod 2) = 0 then out_ready <= '1'; else out_ready <= '0'; end if;
           end if;
       end process;
 
       -- egress checker: order via queue head, duplicate if empty, count for loss
       check : process (clk) begin
           if rising_edge(clk) and rst = '0' and out_valid = '1' and out_ready = '1' then
               assert qh /= qt report "word out with empty reference (duplicate)" severity error;
               if qh /= qt then
                   assert out_data = q(qh) report "out /= expected (order/loss)" severity error;
                   qh  <= qh + 1;  got <= got + 1;
               end if;
           end if;
       end process;
 
       stim : process
       begin
           rst <= '1'; in_valid <= '0';
           for i in 0 to 3 loop wait until rising_edge(clk); end loop; rst <= '0';
           for w in 0 to 31 loop                              -- bursty source: 32 words
               in_valid <= '1'; in_data <= std_logic_vector(to_unsigned(w, WIDTH));
               wait until rising_edge(clk);
               while not (in_valid = '1' and in_ready = '1') loop wait until rising_edge(clk); end loop;
               q(qt) <= std_logic_vector(to_unsigned(w+1, WIDTH)); qt <= qt + 1; sent <= sent + 1;
           end loop;
           in_valid <= '0';
           for i in 0 to 399 loop wait until rising_edge(clk); end loop;
           assert got = sent report "some words lost under backpressure" severity error;
           report "stream_block self-check complete (in order, exactly once, no loss)" severity note;
           wait;
       end process;
   end architecture;

Across all three languages the block's whole correctness is one sentence: advance the pipeline only when the output FIFO can accept what it would push — freeze the WHOLE pipeline otherwise — so the stall ripples backward through the output FIFO, the pipeline, and the input FIFO to the source, and every word transfers across every boundary exactly once, in order, under any pattern of source burst and sink stall.

5. Verification Strategy

A streaming block's correctness is not 'did the first few words look right' — it is 'is the stream conserved — every word in, out exactly once, in order — for every interleaving of source bursts and sink stalls, including the worst ones.' The one invariant, and the whole §4 scoreboard testbench, is:

The sequence of words that transfer in (on in_valid && in_ready), after the block's transform, equals the sequence that transfer out (on out_valid && out_ready) — same words, same order, none dropped, none duplicated — for every pattern of source burst and sink stall; and the input FIFO never overflows (a write is offered only when in_ready is high) and the output FIFO never underflows (a word is taken only when out_valid is high).

Everything below is that invariant made checkable, and it maps onto the scoreboard testbenches in §4.

  • Scoreboard: in-order, exactly-once delivery (the core test). Model the expected output as a reference FIFO queue: on every ingress transfer (in_valid && in_ready), push the transformed word; on every egress transfer (out_valid && out_ready), pop the head and assert it equals out_data. Two counters close the trap: an egress transfer with an empty reference queue is a duplicate; a final got != sent is a dropped word; a head mismatch is a reorder. The three testbenches do exactly this (SV assert (out_data === exp) plus got === sent; Verilog the if (out_data !== q[qh]) $display("FAIL…") form; VHDL assert out_data = q(qh) … severity error).
  • Bursty source + random-ready sink (the stress that matters). Drive the ingress with random runs of back-to-back offers separated by gaps (a bursty source) and drive out_ready randomly (a stalling sink), so the block is exercised across many burst/stall interleavings, not one. A design that passes with an always-ready sink can still drop words under backpressure — the random-ready sink is the test that finds the §7 bug. Include the worst case: hold out_ready low for a long stretch so the output FIFO fills completely, then dribble it, so the backpressure chain is forced all the way back to the source.
  • No-overflow / no-underflow assertions. Assert that a write is offered to the input FIFO only when in_ready is high (the source honors ready) and that a word is taken from the output FIFO only when out_valid is high (the sink honors valid). These are the two boundary invariants; violating the first is the §7 row-2 overflow, and a testbench that asserts them catches an ingress that overruns the FIFO.
  • Rate-mismatch sweep. Re-run with the source and sink at several relative rates — source-much-faster (burst absorption dominates), sink-much-faster (bubbles dominate), balanced — and at a couple of IN_DEPTH/OUT_DEPTH values, because a block that passes at one depth can fail at another if the depth was assumed rather than sized. A shallower FIFO is safe (it just applies backpressure sooner); the scoreboard must still show zero loss at every depth down to depth 1.
  • Backpressure-release ordering. After a long stall, confirm the block resumes delivering in the same order it stopped (the FIFOs are first-in-first-out, so the released words come out oldest-first). A block that reordered on release would fail the queue-head check, which is exactly why the scoreboard enforces order and not just membership.
  • Expected waveform. Under a burst, watch the input FIFO occupancy climb, in_ready stay high until it is full, then drop; watch the pipeline's valid bits march through and en stay high while the output FIFO has room. Under a sink stall, watch the output FIFO occupancy climb to full, en fall (the pipeline freezes — its valid bits stop moving), the input FIFO fill, and in_ready fall — the backpressure ripple is visible as a wave of full/freeze/full propagating from the sink toward the source. On release, the same wave recedes. A word that appears at the output without a matching ingress transfer (or vice versa) is the visual signature of the §7 conservation bug.

6. Common Mistakes

The pipeline advances while the output path is stalled — a word pushed into a full FIFO. The signature failure and the reason this is a capstone. If the last pipeline stage pushes a word into the output FIFO every cycle its data is valid without checking the FIFO has room, then when the sink stalls and the FIFO fills, the write is refused and the word is dropped — while the pipeline advances past it. It 'passes' whenever the sink is always ready (the FIFO never fills) and loses data only under sustained backpressure. The fix is to stall the whole pipeline when the egress cannot accept: gate the shared enable on the output FIFO having room (en = of_wr_ready | ~m_valid), so a real word is never pushed where there is no space. Full post-mortem in §7 row 1; buggy-vs-fixed RTL in §4e.

Ignoring the input FIFO's full/ready — the source overruns the ingress. If the block asserts in_ready to the source unconditionally (or from something other than the input FIFO's not-full), the source keeps writing after the FIFO is full and the extra words are dropped or overwrite live entries — an overflow. in_ready must be exactly the input FIFO's write-side not-full, and the source must honor it (offer a word only when in_ready is high). Post-mortem in §7 row 2.

A partial stall — some pipeline stages freeze while others advance. When you stall the pipeline you must stall every stage with the one shared enable. Gating only the stalled stage lets the stage behind it advance into the frozen slot and overwrite the held word — the same word is now gone, and a stale one takes its place. This is the 8.4 collision: the enable is shared for exactly this reason. If you find yourself writing a separate enable per stage, you are about to lose a word.

Changing the data (or dropping valid) under backpressure. While a word is held (offered but not yet accepted), neither its data nor its valid may change — the valid/ready contract (9.1) requires a stable offer until the transfer. A block that recomputes or advances its held word during a stall breaks the offer: the receiver, when it finally becomes ready, captures a different word than the one that was offered. Under the shared-enable discipline this falls out for free (a frozen register cannot change), which is another reason to freeze the whole pipeline rather than hand-hold individual signals.

Sizing the input FIFO by guess instead of by burst. The input FIFO's depth is not a round number you pick — it is the 7.4 calculation from how far ahead the source can get: BURST - floor(BURST / RD_EVERY) for a burst of BURST words against a block that retires one every RD_EVERY cycles. Too shallow and the source stalls earlier than intended (safe, but a throughput cost you did not mean to pay); grossly too deep and you burn area on a buffer that never fills. Size it deliberately.

Deriving in_ready combinationally from out_ready across the whole block. It is tempting to make in_ready a direct combinational function of out_ready (a long path straight through the block). That both creates a slow combinational chain from the sink to the source and risks a handshake loop if a stage's ready also feeds its own valid. The FIFOs are what break that path: in_ready is the input FIFO's registered occupancy, not a wire from the sink, so the backpressure is a sequential ripple (one buffer per cycle of grace), not a combinational storm. If you ever need to register a long ready path, that is the skid buffer (9.3).

7. DebugLab

The streaming block that passes every fast-sink test and silently drops words the moment the sink stalls

The engineering lesson: a streaming block is correct only if a word is never pushed into — nor accepted into — a buffer that has no room, and both signature bugs are the same crime, a boundary where valid is asserted without honoring the receiver's ready, committed at the two ends of the block. Row 1 breaks the egress boundary (the pipeline pushes into a full output FIFO, dropping the word at the output); row 2 breaks the ingress boundary (the block accepts into a full input FIFO, dropping the word at the input). Both pass whenever the relevant buffer never fills — a fast sink, a short burst — and both surface the instant real backpressure appears. Three habits make the block trustworthy, identical across SystemVerilog, Verilog, and VHDL: make every boundary a true valid/ready transfer (a word moves only on valid && ready, never on valid alone), stall the WHOLE pipeline as one unit when the egress cannot accept (one shared enable, so backpressure ripples cleanly backward), and verify against a random-ready sink and an over-long burst, not a fast sink and a short one — the stress test is what separates a block that works on the bench from one that works under load.

8. Interview Q&A

9. Exercises

Design and reasoning exercises — no syntax drills. Each rehearses composing the pieces and getting the backpressure accounting right.

Exercise 1 — Size both FIFOs for a real rate mismatch

A source bursts up to 24 words back-to-back; the block's pipeline retires one word every 2 cycles under a sink that, on average, keeps up but stalls for runs of up to 6 cycles. Derive (i) the minimum input-FIFO depth that keeps in_ready high through the worst burst, using BURST - floor(BURST / RD_EVERY); (ii) the minimum output-FIFO depth that keeps the pipeline from freezing through the worst sink stall; and (iii) what changes about your answer if the source's bursts can arrive before the previous burst has fully drained. State each depth rounded to a power of two and say which failure a one-slot-too-shallow FIFO produces (loss vs early backpressure).

Exercise 2 — Predict the dropped-word failure

An engineer leaves the pipeline always enabled (en = 1) and pushes on m_valid without checking the output FIFO's ready. Describe precisely (i) what a scoreboard reports with an always-ready sink, (ii) what happens when the sink holds out_ready low for 20 cycles against a depth-4 output FIFO, (iii) exactly which words are lost and why the survivors are still in order. Then give the one-line fix and the single testbench change (sink behaviour) that would have exposed it.

Exercise 3 — Add a second output stream (a fork)

Extend the block so each processed word is written into two output FIFOs feeding two independent sinks (a broadcast fork). State (i) the new condition under which the pipeline may advance (both output FIFOs must have room, not just one), (ii) why gating en on only one FIFO's not-full reintroduces the §7 drop on the other stream, and (iii) what happens to throughput when one sink is persistently slower than the other. Name the pattern the combined en condition generalizes.

Exercise 4 — Replace a FIFO with a skid buffer

The output FIFO is depth-OUT_DEPTH; replace it with a single skid buffer (9.3) and reason about the consequences. State (i) how the skid buffer still sustains one word per cycle while registering the ready path, (ii) how much sink stall it can now absorb before the pipeline freezes compared to the depth-OUT_DEPTH FIFO, and (iii) when you would choose the skid buffer (cut a combinational ready path, minimal area) over a deeper FIFO (absorb a long stall). Name which earlier pattern the skid buffer draws its two-slot structure from.

The patterns this streaming block composes (the lessons you are assembling here):

  • Synchronous FIFO Architecture — Chapter 7.1; the buffer used twice here — input FIFO (burst absorption) and output FIFO (stall holding) — with wrapping pointers and gated read/write.
  • FIFO Full/Empty Logic — Chapter 7.2; the not-full / not-empty flags that become in_ready (not full) and out_valid (not empty) at the two boundaries.
  • FIFO Depth Calculation — Chapter 7.4; sizing the input FIFO for burst absorption — BURST - floor(BURST / RD_EVERY) — the one architectural knob of the block.
  • Pipeline Stalls & Flush — Chapter 8.4; the shared clock-enable that freezes the whole pipeline as one unit and injects bubbles when the block cannot advance.
  • valid/ready Handshake — Chapter 9.1; the contract at every boundary — a word moves only on valid && ready, data forward, ready backward.
  • Backpressure & Stalls — Chapter 9.2; the ripple this whole capstone wires — a downstream stall propagating backward through the buffers to the source.

Related refinements:

  • Ready/Valid Pipelining — Chapter 9.6; register slices on the valid/ready path — the alternative to a FIFO when you need to cut a timing path at zero throughput cost.
  • Skid Buffer — Chapter 9.3; the two-slot buffer that registers the ready path while sustaining one word per cycle — a drop-in for a shallow output FIFO (Exercise 4).

Backward / method:

  • The RTL Design Mindset — Chapter 0.2; the Interface → State → Datapath → Control → Verification lenses this case study walks end to end.
  • What Is an RTL Design Pattern? — Chapter 0.1; why composing patterns into a real block — not inventing a new one — is the design skill this capstone rehearses.

Sibling case studies:

  • UART Case Study — Chapter 13.1; the first integration capstone — FSM + shift register + baud timer — the model this one follows for pacing and depth.

Forward — the other case studies (unlock as they ship):

  • SPI Master Case Study (/rtl-design-patterns/case-study-spi-master) — Chapter 13.2; FSM + datapath + handshake for a source-synchronous serial master.
  • Arbitered Bus Case Study (/rtl-design-patterns/case-study-arbitered-bus) — Chapter 13.4; many masters, one shared resource, resolved by an arbiter — backpressure meets arbitration.
  • CDC Stream Case Study (/rtl-design-patterns/case-study-cdc-stream) — Chapter 13.5; moving a stream safely across two unrelated clocks with an async FIFO — this block, but dual-clock.

Verilog v1 prerequisites (the grammar these blocks transcribe into):

11. Summary

  • A streaming block is patterns composed into a real datapath, not a new pattern. It is an input FIFO (7.1/7.2, burst absorption) feeding a stall-able pipeline (per-stage valid 8.3 + shared clock-enable 8.4) feeding an output FIFO (7.1/7.2, stall holding), every boundary a valid/ready handshake (9.1). Each block is a lesson you already had; the capstone is wiring the backpressure exact.
  • Data flows forward, ready flows backward. in_ready is the input FIFO's not-full; out_valid is the output FIFO's not-empty; a word moves across any boundary only on valid && ready. Nothing is ever pushed into — or accepted into — a buffer with no room.
  • A sink stall ripples backward, one buffer at a time. The sink drops out_ready, the output FIFO fills, its full stalls the whole pipeline (en = of_wr_ready | ~m_valid, freeze all stages), the frozen pipeline stops draining the input FIFO, it fills, and in_ready falls to the source. Each buffer's depth is how many cycles of grace the upstream gets before the stall reaches it.
  • The signature bugs are the same crime at the two ends. Push a word into a full output FIFO (pipeline free-runs, no egress backpressure) and it is dropped at the egress (§7 row 1); accept a word into a full input FIFO (in_ready not from the FIFO's not-full) and it is dropped at the ingress (§7 row 2). Both pass whenever the buffer never fills — a fast sink, a short burst — and both surface under real backpressure.
  • Prove it with a bursty source and a random-ready sink, not a fast one. Wire a scoreboard: push the transformed word to a reference queue on each ingress transfer, pop and compare on each egress transfer, and assert got == sent (no loss), an empty-queue-on-output catches duplicates, a head mismatch catches reorders. Hold out_ready low long enough to fill the output FIFO so backpressure reaches the source. That stress test is what separates a block that works on the bench from one that works under load — self-checking scoreboard testbenches shown in SystemVerilog, Verilog, and VHDL.