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.
// ---- 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 flowThe 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 flowSizing 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
validand the receiver'sreadyare both high.in_readyis the input FIFO's not-full;out_validis 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
stallcondition drivesen = ~stall; every pipeline register updates only whenenis 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
validbits 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.
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
endmoduleAt 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).
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;
endmoduleTwo 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.
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));
endmoduleOne 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.
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
endmoduleThe 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.
// 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 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
endmodule4f. 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.
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 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
endmoduleThe 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.
// 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;
endmodule4g. 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.
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; 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; 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 (onout_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 whenin_readyis high) and the output FIFO never underflows (a word is taken only whenout_validis 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 equalsout_data. Two counters close the trap: an egress transfer with an empty reference queue is a duplicate; a finalgot != sentis a dropped word; a head mismatch is a reorder. The three testbenches do exactly this (SVassert (out_data === exp)plusgot === sent; Verilog theif (out_data !== q[qh]) $display("FAIL…")form; VHDLassert 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_readyrandomly (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: holdout_readylow 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_readyis high (the source honors ready) and that a word is taken from the output FIFO only whenout_validis 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_DEPTHvalues, 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_readystay high until it is full, then drop; watch the pipeline'svalidbits march through andenstay high while the output FIFO has room. Under a sink stall, watch the output FIFO occupancy climb to full,enfall (the pipeline freezes — itsvalidbits stop moving), the input FIFO fill, andin_readyfall — 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.
10. Related Tutorials
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) andout_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):
- Blocking and Non-Blocking Assignments —
<=in the clocked FIFO/pipeline registers,=/assignfor the combinational flags and the backpressure line. - If-Else Statements — the gated reset/enable branches in the FIFO and pipeline
alwaysblocks. - Case Statements — the occupancy update
case ({do_write, do_read})in the FIFO.
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_readyis the input FIFO's not-full;out_validis the output FIFO's not-empty; a word moves across any boundary only onvalid && 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, andin_readyfalls 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_readynot 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. Holdout_readylow 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.