Skip to content

RTL Design Patterns · Chapter 9 · Handshake & Flow Control

Backpressure & Stall Propagation

A valid/ready link carries two flows in opposite directions: data moves forward from producer to consumer, while the permission to move, the ready signal, travels backward. A real datapath is not one link but a chain of blocks, each a consumer of the block before it and a producer to the block after it. When the final sink drops ready, the block feeding it must hold its result, and because it is holding it can no longer accept a fresh word, so it drops its own input ready. That refusal is backpressure, and it ripples upstream stage by stage until it reaches the source. This lesson builds a correctly back-pressuring register stage and chain, proves with a scoreboard testbench that no word is lost, duplicated, or reordered, and dissects the classic bug of an input ready not gated by downstream stall. Built in SystemVerilog, Verilog, and VHDL.

Intermediate14 min readRTL Design PatternsBackpressureStall Propagationvalid/readyFlow ControlHandshake

Chapter 9 · Section 9.2 · Handshake & Flow Control

1. The Engineering Problem

You have a streaming datapath: a source produces one data word per cycle, a processing block transforms it, and a sink consumes the result. So far this looks like the pipeline you built in Chapter 8 — words march forward, one stage per cycle. Then the sink misbehaves in the most ordinary way possible: some cycles it simply cannot take a word. Its downstream FIFO is full, its bus port lost arbitration this cycle, its own consumer stalled. On those cycles the sink de-asserts ready and says, in effect, not now — hold.

In 9.1 you learned what that means for a single link: the block feeding the sink must keep its valid high and its data lines frozen until the sink's ready returns, because a transfer only happens on a cycle where valid && ready are both high. Fine — one block holds. But holding is not free. The moment the processing block is holding an output it could not hand off, it has a problem of its own: the source above it is still trying to push a fresh word into it every cycle. If the block accepts that fresh word while it is still holding the previous result, the held result is overwritten and lost forever — a word that entered the chain never comes out. To avoid that, the block must refuse the new word: it must de-assert its own input ready. And now the source, seeing its ready low, must hold its word and data too — exactly as 9.1 told the producer to do.

That is the whole shape of the problem. A single point of backpressure at the sink does not stay at the sink. It forces the block above it to stall, which forces that block to refuse input, which forces the block above that to stall, and so on — a wave of de-asserted ready that ripples backward through the chain until it reaches the source, which finally holds. Data flows one way; the permission to move data flows the other way. Getting this ripple right — every block refusing new input exactly when it cannot pass its current output forward — is what makes a streaming datapath lose no words under arbitrary backpressure. Getting it wrong is the single most common way a valid/ready datapath silently drops data.

the-need.v — one sink stalls; the refusal must travel all the way back
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // A three-block chain: source -> block -> sink. Data moves L->R.
   wire        out_valid, out_ready;   // block -> sink   (data forward, ready back)
   wire        in_valid,  in_ready;    // source -> block (data forward, ready back)
   wire [31:0] out_data,  in_data;
 
   // The sink de-asserts ready this cycle (its FIFO is full):
   //   out_ready == 0  =>  the block cannot hand off out_data.
 
   // WRONG — the block keeps accepting input regardless:
   //   assign in_ready = 1'b1;         // "always ready" => it swallows the held word
 
   // RIGHT — backpressure must PROPAGATE: the block accepts a new word only when
   // it can move its current one. in_ready must fall when the sink stalls.
   //   assign in_ready = out_ready | slot_empty;   // gated accept (this page)

2. Mental Model

3. Pattern Anatomy

The pattern is one block — a valid/ready register stage that back-pressures correctly — and the chain you get by wiring several of them in series. Understand the single stage's backward path and the whole chain follows.

The two directions of one link. Every link has a forward pair (valid, data) driven by the producer and a backward signal (ready) driven by the consumer. A transfer happens on exactly the cycles where valid && ready. That is 9.1. Backpressure is what happens on the cycles where valid && !ready — the producer wants to move a word but the consumer won't take it, so the producer must hold: keep valid high and keep data bit-for-bit stable until it sees ready.

The stall condition, per block. A block holding an output looks downstream and computes one bit: stall = out_valid && !out_readyI have a valid word and my consumer won't take it. When stall is true the block must do two things at once, and both matter:

  • Freeze its output. Keep out_valid high and out_data unchanged (the 9.1 hold). This is a clock-enable disable on the output register — the same clock-enable freeze that stalled a pipeline stage in 8.4.
  • Refuse its input. De-assert in_ready, so the block above it cannot push a fresh word in and overwrite the word it is holding. This is the new, load-bearing piece: the stall does not just hold the output, it closes the door upstream.

The gated-accept rule. Refusing input is not "always refuse when stalled" bluntly bolted on — it is a precise condition. A single-entry register stage should accept a new input word exactly when its output slot will be free next cycle, i.e. either the slot is empty now, or the word currently in it is being taken this cycle (out_valid && out_ready). In one line: in_ready = (!out_valid) || out_readyI can take a new word if I'm not holding one, or if the one I'm holding is leaving right now. That single expression is the entire correctness of the pattern; §4 builds it, and §4d shows what breaks when you replace it with a constant 1.

The ripple, and its cost. In a chain, each block's in_ready is the previous block's out_ready. So the sink's ready low feeds block N's out_ready, which drops block N's in_ready, which is block N-1's out_ready, which drops block N-1's in_ready — a single combinational wave that reaches the source in the same cycle. That is the point of the pattern and its problem: because ready propagates combinationally back through every block, the ready path is a long combinational chain whose delay grows with chain length. A ten-block chain has a ready signal that must ripple through ten blocks' accept logic in one cycle — a timing-critical path that gets worse the longer the chain. (Registering ready to break that path is exactly what the skid buffer of 9.3 does; this page keeps ready combinational so you can see the problem it solves.)

Data forward, ready backward — a stall at the sink ripples to the source

data flow
Data forward, ready backward — a stall at the sink ripples to the sourcevalid/datavalid/datavalid/datareadyreadyreadySourceholds valid + data when in_ready lowBlock 0in_ready = !valid0 || out_ready0Block 1in_ready = !valid1 || out_ready1Sinkde-asserts ready => backpressure
The bus edges (valid/data) run left to right; the signal edges (ready) run right to left. When the sink de-asserts ready, block 1 cannot hand off its output, so it holds (valid stays high, data stable) AND de-asserts its own in_ready. That drops block 0's out_ready, so block 0 holds and de-asserts ITS in_ready, and the source finally holds valid + data. One cycle, one combinational ripple, all the way back — data forward, readiness backward. The longer the chain, the longer that backward ready path, which is the timing problem the skid buffer (9.3) solves by registering ready. Identical in SystemVerilog, Verilog, and VHDL.

Flow conservation — the invariant that proves it. Because every block either passes its word forward (on valid && ready) or holds it unchanged (on valid && !ready), and never accepts a new word while holding, no word is ever created or destroyed. Count the words that enter the source and the words that leave the sink: they must be equal, in the same order, for any pattern of downstream ready. That conservation law is the property §5 asserts and the thing the §4d bug violates — under the bug, the entering-count exceeds the leaving-count, because a swallowed word entered but never left.

4. Real RTL Implementation

This is the core of the page. We build (a) a WIDTH-generic valid/ready register stage that back-pressures correctly (the gated-accept rule, output held under backpressure), (b) a small chain of two such stages proving the ripple works end to end, and (d) the swallowed-word bug — input ready ungated vs the gated-ready fix, each in SystemVerilog, Verilog, and VHDL with an RTL block and a self-checking clocked testbench. The testbenches apply random downstream backpressure and scoreboard the stream: every word that goes in must come out exactly once, in order. The idea — accept a new word only when the held one can move, so ready flows backward exactly as data flows forward — is identical across all three languages.

One discipline runs through every RTL block: a transfer advances only when valid && ready, and in_ready is gated by downstream backpressure (in_ready = !out_valid || out_ready). Miss the gate and you get pattern (d)'s swallowed word.

4a. The valid/ready register stage — back-pressures correctly, three ways

The atom of the whole chapter is one register stage holding at most one word, with a valid/ready port on each side. The output register is clock-enabled by the accept condition; in_ready is gated so a new word is taken only when the held word will be gone next cycle. Because the payload register only ever updates on an accepted transfer, out_data is automatically held stable under backpressure — the 9.1 hold falls out of the gating, it is not a separate mechanism.

skid_reg.sv — one valid/ready stage that propagates backpressure (WIDTH-generic)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module vr_stage #(
       parameter int WIDTH = 32                 // payload width, any value
   )(
       input  logic             clk,
       input  logic             rst,            // synchronous, active-high
       // upstream (this block is the CONSUMER): data forward, ready back
       input  logic             in_valid,
       output logic             in_ready,
       input  logic [WIDTH-1:0] in_data,
       // downstream (this block is the PRODUCER): data forward, ready back
       output logic             out_valid,
       input  logic             out_ready,
       output logic [WIDTH-1:0] out_data
   );
       // A transfer on a link happens iff BOTH valid and ready are high that cycle.
       wire in_fire  = in_valid  && in_ready;    // we accepted a new word
       wire out_fire = out_valid && out_ready;   // we handed our word downstream
 
       // GATED ACCEPT — the whole pattern in one line:
       // accept new input only when the output slot is empty OR is emptying now.
       assign in_ready = (!out_valid) || out_ready;
 
       always_ff @(posedge clk) begin
           if (rst) begin
               out_valid <= 1'b0;
               out_data  <= '0;
           end else begin
               // The word leaves: mark slot empty (unless a new one arrives same cycle).
               if (out_fire) out_valid <= 1'b0;
               // A new word is accepted: load it, mark slot full. (in_fire can only be
               // true when in_ready is high, i.e. the slot is free or freeing.)
               if (in_fire) begin
                   out_valid <= 1'b1;             // slot now holds a valid word
                   out_data  <= in_data;          // payload updates ONLY on accept =>
               end                                //   held stable under backpressure
           end
       end
   endmodule

The out_data register updates on in_fire only, so while the block is stalled (out_valid && !out_ready, no in_fire because in_ready is low) the payload cannot change — the 9.1 stability requirement is a consequence of the gate, not extra logic. The SystemVerilog testbench is clocked: it streams an incrementing sequence in, randomly de-asserts the downstream ready, and scoreboards every accepted word against every emitted word.

skid_reg_tb.sv — random downstream backpressure; scoreboard in-order, exactly-once
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module vr_stage_tb;
       localparam int WIDTH = 32;
       localparam int NWORDS = 200;
       logic clk = 0, rst = 1;
       logic in_valid, in_ready, out_valid, out_ready;
       logic [WIDTH-1:0] in_data, out_data;
       int   errors = 0;
 
       vr_stage #(.WIDTH(WIDTH)) dut (.*);
       always #5 clk = ~clk;
 
       // Scoreboard: expected words are 0,1,2,... in order; each must come out once.
       int sent = 0, recv = 0;
 
       initial begin
           in_valid = 0; in_data = 0; out_ready = 0;
           repeat (2) @(posedge clk); rst = 0;
 
           // Producer: offer word 'sent' whenever the stage is ready to accept.
           fork
               begin : producer
                   while (sent < NWORDS) begin
                       in_valid = 1;
                       in_data  = sent;
                       @(posedge clk);
                       if (in_valid && in_ready) sent++;   // accepted -> next word
                   end
                   in_valid = 0;
               end
               begin : consumer                            // RANDOM backpressure
                   while (recv < NWORDS) begin
                       out_ready = $urandom_range(0, 1);    // ready jitters every cycle
                       @(posedge clk);
                       if (out_valid && out_ready) begin    // a word transferred out
                           // Exactly-once, in-order: the recv-th word out must == recv.
                           assert (out_data === recv)
                               else begin $error("out of order: got %0d exp %0d", out_data, recv); errors++; end
                           recv++;
                       end
                   end
                   out_ready = 0;
               end
           join
 
           // Flow conservation: every word sent came out, none extra, none lost.
           assert (sent == NWORDS && recv == NWORDS)
               else begin $error("count mismatch sent=%0d recv=%0d", sent, recv); errors++; end
           if (errors == 0) $display("PASS: %0d words, in order, none lost/dup under backpressure", recv);
           else             $display("FAIL: %0d errors", errors);
           $finish;
       end
   endmodule

The Verilog form is the same stage with reg/wire typing and always @(posedge clk); the gated-accept expression and the accept-only-when-freeing register update are identical. The testbench uses if (...) $display("FAIL") for its self-check and $random for the backpressure jitter.

skid_reg.v — the valid/ready stage in Verilog (gated accept, held payload)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module vr_stage #(
       parameter WIDTH = 32
   )(
       input  wire             clk,
       input  wire             rst,             // synchronous, active-high
       input  wire             in_valid,
       output wire             in_ready,
       input  wire [WIDTH-1:0] in_data,
       output reg              out_valid,
       input  wire             out_ready,
       output reg  [WIDTH-1:0] out_data
   );
       wire in_fire  = in_valid  & in_ready;
       wire out_fire = out_valid & out_ready;
 
       // Gated accept: take input only when the slot is empty or emptying this cycle.
       assign in_ready = (~out_valid) | out_ready;
 
       always @(posedge clk) begin
           if (rst) begin
               out_valid <= 1'b0;
               out_data  <= {WIDTH{1'b0}};
           end else begin
               if (out_fire) out_valid <= 1'b0;     // word left -> slot empties
               if (in_fire) begin
                   out_valid <= 1'b1;               // new word loaded -> slot full
                   out_data  <= in_data;            // updates only on accept => held stable
               end
           end
       end
   endmodule
skid_reg_tb.v — self-checking; random out_ready; scoreboard order & count
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module vr_stage_tb;
       parameter WIDTH  = 32;
       parameter NWORDS = 200;
       reg               clk, rst;
       reg               in_valid, out_ready;
       wire              in_ready, out_valid;
       reg  [WIDTH-1:0]  in_data;
       wire [WIDTH-1:0]  out_data;
       integer sent, recv, errors;
 
       vr_stage #(.WIDTH(WIDTH)) dut (
           .clk(clk), .rst(rst),
           .in_valid(in_valid), .in_ready(in_ready), .in_data(in_data),
           .out_valid(out_valid), .out_ready(out_ready), .out_data(out_data));
 
       initial clk = 0; always #5 clk = ~clk;
 
       initial begin
           rst = 1; in_valid = 0; in_data = 0; out_ready = 0;
           sent = 0; recv = 0; errors = 0;
           repeat (2) @(posedge clk); rst = 0;
 
           // Single always-driven producer + consumer, sampled each clock.
           forever begin
               @(posedge clk);
               // Consumer: random backpressure, check any word that leaves.
               if (out_valid && out_ready) begin
                   if (out_data !== recv) begin
                       $display("FAIL: out of order got %0d exp %0d", out_data, recv);
                       errors = errors + 1;
                   end
                   recv = recv + 1;
               end
               // Producer: advance the offered word when it was accepted.
               if (in_valid && in_ready) sent = sent + 1;
 
               // Drive next cycle's stimulus.
               in_valid  = (sent < NWORDS);
               in_data   = sent;
               out_ready = $random & 1'b1;               // 0/1 backpressure jitter
 
               if (recv == NWORDS) begin
                   if (sent !== NWORDS || errors != 0)
                       $display("FAIL: sent=%0d recv=%0d errors=%0d", sent, recv, errors);
                   else
                       $display("PASS: %0d words in order, none lost/dup under backpressure", recv);
                   $finish;
               end
           end
       end
   endmodule

In VHDL the same stage uses numeric_std, a synchronous active-high reset, and the identical gated-accept expression as a concurrent assignment. The payload register updates only on an accepted transfer, so it is held under backpressure exactly as in the other two HDLs.

skid_reg.vhd — the valid/ready stage in VHDL (numeric_std, gated accept)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity vr_stage is
       generic ( WIDTH : positive := 32 );
       port (
           clk       : in  std_logic;
           rst       : in  std_logic;                          -- synchronous, active-high
           in_valid  : in  std_logic;
           in_ready  : out std_logic;
           in_data   : in  std_logic_vector(WIDTH-1 downto 0);
           out_valid : out std_logic;
           out_ready : in  std_logic;
           out_data  : out std_logic_vector(WIDTH-1 downto 0)
       );
   end entity;
 
   architecture rtl of vr_stage is
       signal ovalid : std_logic := '0';                       -- readable copy of out_valid
   begin
       out_valid <= ovalid;
       -- Gated accept: take input only when the slot is empty or emptying this cycle.
       in_ready  <= (not ovalid) or out_ready;
 
       process (clk)
           variable in_fire  : boolean;
           variable out_fire : boolean;
       begin
           if rising_edge(clk) then
               in_fire  := (in_valid = '1') and ((ovalid = '0') or (out_ready = '1'));
               out_fire := (ovalid = '1') and (out_ready = '1');
               if rst = '1' then
                   ovalid   <= '0';
                   out_data <= (others => '0');
               else
                   if out_fire then ovalid <= '0'; end if;      -- word left -> slot empties
                   if in_fire then
                       ovalid   <= '1';                         -- new word -> slot full
                       out_data <= in_data;                     -- updates only on accept
                   end if;
               end if;
           end if;
       end process;
   end architecture;

The VHDL testbench clocks the stage, jitters out_ready from a simple LFSR, and uses assert … report … severity error to check in-order, exactly-once delivery and the final flow-conservation count.

skid_reg_tb.vhd — self-checking; random backpressure; assert order & count
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity vr_stage_tb is
   end entity;
 
   architecture sim of vr_stage_tb is
       constant WIDTH  : positive := 32;
       constant NWORDS : natural  := 200;
       signal clk       : std_logic := '0';
       signal rst       : std_logic := '1';
       signal in_valid  : std_logic := '0';
       signal in_ready  : std_logic;
       signal in_data   : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
       signal out_valid : std_logic;
       signal out_ready : std_logic := '0';
       signal out_data  : std_logic_vector(WIDTH-1 downto 0);
   begin
       dut : entity work.vr_stage
           generic map (WIDTH => WIDTH)
           port map (clk => clk, rst => rst,
                     in_valid => in_valid, in_ready => in_ready, in_data => in_data,
                     out_valid => out_valid, out_ready => out_ready, out_data => out_data);
 
       clk <= not clk after 5 ns;
 
       stim : process
           variable sent, recv : natural := 0;
           variable lfsr       : unsigned(7 downto 0) := x"2B";
           variable errors     : natural := 0;
       begin
           wait until rising_edge(clk); wait until rising_edge(clk); rst <= '0';
           while recv < NWORDS loop
               -- consumer: check any word that transfers out this cycle
               if (out_valid = '1') and (out_ready = '1') then
                   assert out_data = std_logic_vector(to_unsigned(recv, WIDTH))
                       report "out of order / lost word under backpressure" severity error;
                   if out_data /= std_logic_vector(to_unsigned(recv, WIDTH)) then
                       errors := errors + 1;
                   end if;
                   recv := recv + 1;
               end if;
               -- producer: advance offered word when accepted
               if (in_valid = '1') and (in_ready = '1') then sent := sent + 1; end if;
 
               -- drive next cycle
               if sent < NWORDS then in_valid <= '1'; else in_valid <= '0'; end if;
               in_data   <= std_logic_vector(to_unsigned(sent, WIDTH));
               lfsr      := (lfsr(6 downto 0) & (lfsr(7) xor lfsr(5)));  -- cheap RNG
               out_ready <= lfsr(0);                                     -- backpressure jitter
               wait until rising_edge(clk);
           end loop;
           assert (sent = NWORDS) and (errors = 0)
               report "count mismatch or ordering error" severity error;
           report "vr_stage self-check complete" severity note;
           wait;
       end process;
   end architecture;

4b. A two-stage chain — the ripple, end to end

Wire two stages in series and the backward ripple appears on its own: stage 1's in_ready is stage 0's out_ready, so when the sink de-asserts ready, stage 1 stalls and drops its in_ready, which drops stage 0's out_ready, so stage 0 stalls and drops its in_ready — the refusal reaches the source. No extra logic; the chain is just the atom composed, and flow conservation still holds across the whole chain.

vr_chain.sv — two vr_stages in series; backpressure ripples source-ward
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module vr_chain #(
       parameter int WIDTH = 32
   )(
       input  logic             clk, rst,
       input  logic             in_valid,
       output logic             in_ready,
       input  logic [WIDTH-1:0] in_data,
       output logic             out_valid,
       input  logic             out_ready,
       output logic [WIDTH-1:0] out_data
   );
       // Internal link between the two stages: mid_valid/mid_ready/mid_data.
       logic             mid_valid, mid_ready;
       logic [WIDTH-1:0] mid_data;
 
       vr_stage #(.WIDTH(WIDTH)) s0 (
           .clk, .rst,
           .in_valid (in_valid),  .in_ready (in_ready),  .in_data (in_data),
           .out_valid(mid_valid), .out_ready(mid_ready), .out_data(mid_data));
 
       vr_stage #(.WIDTH(WIDTH)) s1 (
           .clk, .rst,
           .in_valid (mid_valid), .in_ready (mid_ready), .in_data (mid_data),
           .out_valid(out_valid), .out_ready(out_ready), .out_data(out_data));
       // s1.in_ready == s0.out_ready == mid_ready: the sink's stall reaches s0 in ONE
       // cycle, combinationally, and then the source. Data forward, ready backward.
   endmodule
vr_chain_tb.sv — same scoreboard, now across a 2-deep chain under backpressure
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module vr_chain_tb;
       localparam int WIDTH = 32, NWORDS = 300;
       logic clk = 0, rst = 1, in_valid, in_ready, out_valid, out_ready;
       logic [WIDTH-1:0] in_data, out_data;
       int sent = 0, recv = 0, errors = 0;
 
       vr_chain #(.WIDTH(WIDTH)) dut (.*);
       always #5 clk = ~clk;
 
       initial begin
           in_valid = 0; in_data = 0; out_ready = 0;
           repeat (2) @(posedge clk); rst = 0;
           forever begin
               @(posedge clk);
               if (out_valid && out_ready) begin
                   assert (out_data === recv)               // still in order across 2 stages
                       else begin $error("chain out of order: got %0d exp %0d", out_data, recv); errors++; end
                   recv++;
               end
               if (in_valid && in_ready) sent++;
               in_valid  = (sent < NWORDS);
               in_data   = sent;
               out_ready = $urandom_range(0, 1);            // random backpressure at the sink
               if (recv == NWORDS) begin
                   if (sent == NWORDS && errors == 0)
                        $display("PASS: chain delivered %0d words in order, none lost", recv);
                   else $display("FAIL: sent=%0d recv=%0d errors=%0d", sent, recv, errors);
                   $finish;
               end
           end
       end
   endmodule

The Verilog and VHDL chains are the identical structural composition — instantiate two vr_stages and wire stage 0's output link to stage 1's input link — so the RTL is a wiring exercise, not new logic. We show the Verilog wiring; the VHDL is the same two port maps sharing a mid_* link, and the §4a testbenches extend to the chain unchanged except for the deeper latency.

vr_chain.v — two vr_stages in series (Verilog wiring)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module vr_chain #(
       parameter WIDTH = 32
   )(
       input  wire             clk, rst,
       input  wire             in_valid,
       output wire             in_ready,
       input  wire [WIDTH-1:0] in_data,
       output wire             out_valid,
       input  wire             out_ready,
       output wire [WIDTH-1:0] out_data
   );
       wire             mid_valid, mid_ready;
       wire [WIDTH-1:0] mid_data;
 
       vr_stage #(.WIDTH(WIDTH)) s0 (
           .clk(clk), .rst(rst),
           .in_valid(in_valid), .in_ready(in_ready), .in_data(in_data),
           .out_valid(mid_valid), .out_ready(mid_ready), .out_data(mid_data));
 
       vr_stage #(.WIDTH(WIDTH)) s1 (
           .clk(clk), .rst(rst),
           .in_valid(mid_valid), .in_ready(mid_ready), .in_data(mid_data),
           .out_valid(out_valid), .out_ready(out_ready), .out_data(out_data));
   endmodule

4c. What holds the payload stable — the stability guarantee

Before the bug, make the stability guarantee explicit, because it is the property most engineers get accidentally right and then break during a refactor. In §4a the payload register updates on in_fire only. Under backpressure in_ready is low, so in_fire is impossible, so out_data cannot change — it is held bit-for-bit until the transfer completes. This is the 9.1 rule data must remain stable while valid is high and ready is low satisfied for free by the gating. A correct stage never needs a separate "freeze the data" clause; if you find yourself adding one, the accept condition is wrong. The §5 verification asserts this directly: while out_valid is high and out_ready is low, out_data this cycle must equal out_data last cycle.

stability_check.sv — assertion-style check that payload is held under backpressure
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // Bind or embed alongside the DUT. Conceptual SVA-style property (v1-level intent):
   // whenever we are presenting a valid word that is NOT being taken, next cycle the
   // valid stays high and the data is UNCHANGED. This is the 9.1 hold, verified.
   logic [WIDTH-1:0] data_q;
   logic             valid_q, stalled_q;
   always_ff @(posedge clk) begin
       data_q    <= out_data;
       valid_q   <= out_valid;
       stalled_q <= out_valid && !out_ready;        // we were stalled last cycle
       if (stalled_q) begin
           assert (out_valid)                        // valid must not drop during a stall
               else $error("valid dropped while backpressured");
           assert (out_data === data_q)              // data must be held bit-for-bit
               else $error("data changed while backpressured: %h -> %h", data_q, out_data);
       end
   end

4d. The swallowed-word bug — ungated input ready vs the gated-ready fix

Pattern (d) is the signature failure, shown here as buggy vs fixed RTL in each language and dramatized in §7. The bug is one line: a block computes its input ready as a constant 1always accepting — instead of gating it on can I move my current output forward. When the sink de-asserts ready and the block is holding a valid output, a fresh input word arrives, the register accepts it (because in_ready was stuck high), the held word is overwritten and lost, and no one ever knows — the chain simply delivers one fewer word. It passes every test where the sink is always ready and corrupts under any backpressure.

vr_bug.sv — BUGGY (in_ready constant 1) vs FIXED (gated accept)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: in_ready is tied high. While the block holds a stalled output and the sink
   //        is not ready, a new input word still gets accepted -> it OVERWRITES the held
   //        word -> the held word is silently LOST. Backpressure does NOT propagate.
   module vr_stage_bad #(parameter int WIDTH = 32)(
       input  logic clk, rst,
       input  logic in_valid, output logic in_ready, input logic [WIDTH-1:0] in_data,
       output logic out_valid, input logic out_ready, output logic [WIDTH-1:0] out_data
   );
       assign in_ready = 1'b1;                     // <-- BUG: always accepting
       wire in_fire  = in_valid  && in_ready;
       wire out_fire = out_valid && out_ready;
       always_ff @(posedge clk) begin
           if (rst) begin out_valid <= 0; out_data <= '0; end
           else begin
               if (out_fire) out_valid <= 1'b0;
               if (in_fire) begin                  // fires even while stalled ->
                   out_valid <= 1'b1;              //   overwrites the held word
                   out_data  <= in_data;
               end
           end
       end
   endmodule
 
   // FIXED: gate in_ready on downstream backpressure. Accept a new word only when the
   //        slot is empty OR is emptying this cycle -> the held word is never clobbered,
   //        and the stall propagates upstream.
   module vr_stage_good #(parameter int WIDTH = 32)(
       input  logic clk, rst,
       input  logic in_valid, output logic in_ready, input logic [WIDTH-1:0] in_data,
       output logic out_valid, input logic out_ready, output logic [WIDTH-1:0] out_data
   );
       assign in_ready = (!out_valid) || out_ready;   // <-- FIX: gated accept
       wire in_fire  = in_valid  && in_ready;
       wire out_fire = out_valid && out_ready;
       always_ff @(posedge clk) begin
           if (rst) begin out_valid <= 0; out_data <= '0; end
           else begin
               if (out_fire) out_valid <= 1'b0;
               if (in_fire) begin out_valid <= 1'b1; out_data <= in_data; end
           end
       end
   endmodule
vr_bug_tb.sv — random backpressure; the bad DUT loses words, the good one does not
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module vr_bug_tb;
       localparam int WIDTH = 32, NWORDS = 200;
       logic clk = 0, rst = 1, in_valid, in_ready, out_valid, out_ready;
       logic [WIDTH-1:0] in_data, out_data;
       int sent = 0, recv = 0, errors = 0;
 
       // Bind to vr_stage_good to PASS; to vr_stage_bad to watch words vanish.
       vr_stage_good #(.WIDTH(WIDTH)) dut (.*);
       always #5 clk = ~clk;
 
       initial begin
           in_valid = 0; in_data = 0; out_ready = 0;
           repeat (2) @(posedge clk); rst = 0;
           repeat (4*NWORDS) begin
               @(posedge clk);
               if (out_valid && out_ready) begin
                   // exactly-once & in order: word #recv out must equal recv
                   assert (out_data === recv)
                       else begin $error("LOST/REORDERED: got %0d exp %0d", out_data, recv); errors++; end
                   recv++;
               end
               if (in_valid && in_ready) sent++;
               in_valid  = (sent < NWORDS);
               in_data   = sent;
               out_ready = $urandom_range(0, 1);       // backpressure exposes the bug
               if (recv == NWORDS) break;
           end
           // The bad DUT swallows words: sent reaches NWORDS but recv never does, or
           // out_data arrives out of order because a held word was overwritten.
           if (recv == NWORDS && errors == 0)
                $display("PASS: no word lost/dup/reordered under backpressure (gated accept)");
           else $display("FAIL: recv=%0d of %0d, errors=%0d (words swallowed under backpressure)", recv, NWORDS, errors);
           $finish;
       end
   endmodule

The Verilog buggy/fixed pair is the same one-line difference: assign in_ready = 1'b1; versus assign in_ready = (~out_valid) | out_ready;. Under random out_ready the bad stage's held word gets overwritten and the scoreboard sees a gap.

vr_bug.v — BUGGY vs FIXED in Verilog (the fix is the gate on in_ready)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: always accepting -> overwrites the held word under backpressure.
   module vr_stage_bad #(parameter WIDTH = 32)(
       input wire clk, rst,
       input wire in_valid, output wire in_ready, input wire [WIDTH-1:0] in_data,
       output reg out_valid, input wire out_ready, output reg [WIDTH-1:0] out_data
   );
       assign in_ready = 1'b1;                       // BUG
       wire in_fire  = in_valid  & in_ready;
       wire out_fire = out_valid & out_ready;
       always @(posedge clk) begin
           if (rst) begin out_valid <= 1'b0; out_data <= {WIDTH{1'b0}}; end
           else begin
               if (out_fire) out_valid <= 1'b0;
               if (in_fire) begin out_valid <= 1'b1; out_data <= in_data; end
           end
       end
   endmodule
 
   // FIXED: gate in_ready on downstream readiness -> stall propagates, no word lost.
   module vr_stage_good #(parameter WIDTH = 32)(
       input wire clk, rst,
       input wire in_valid, output wire in_ready, input wire [WIDTH-1:0] in_data,
       output reg out_valid, input wire out_ready, output reg [WIDTH-1:0] out_data
   );
       assign in_ready = (~out_valid) | out_ready;   // FIX: gated accept
       wire in_fire  = in_valid  & in_ready;
       wire out_fire = out_valid & out_ready;
       always @(posedge clk) begin
           if (rst) begin out_valid <= 1'b0; out_data <= {WIDTH{1'b0}}; end
           else begin
               if (out_fire) out_valid <= 1'b0;
               if (in_fire) begin out_valid <= 1'b1; out_data <= in_data; end
           end
       end
   endmodule
vr_bug_tb.v — self-checking; backpressure makes the bad DUT drop words
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module vr_bug_tb;
       parameter WIDTH = 32, NWORDS = 200;
       reg clk, rst, in_valid, out_ready;
       wire in_ready, out_valid;
       reg  [WIDTH-1:0] in_data;
       wire [WIDTH-1:0] out_data;
       integer sent, recv, errors, guard;
 
       // Bind to vr_stage_good to PASS; to vr_stage_bad to see words vanish.
       vr_stage_good #(.WIDTH(WIDTH)) dut (
           .clk(clk), .rst(rst),
           .in_valid(in_valid), .in_ready(in_ready), .in_data(in_data),
           .out_valid(out_valid), .out_ready(out_ready), .out_data(out_data));
 
       initial clk = 0; always #5 clk = ~clk;
 
       initial begin
           rst = 1; in_valid = 0; in_data = 0; out_ready = 0;
           sent = 0; recv = 0; errors = 0; guard = 0;
           repeat (2) @(posedge clk); rst = 0;
           while (recv < NWORDS && guard < 8*NWORDS) begin
               @(posedge clk);
               guard = guard + 1;
               if (out_valid && out_ready) begin
                   if (out_data !== recv) begin
                       $display("FAIL: lost/reordered got %0d exp %0d", out_data, recv);
                       errors = errors + 1;
                   end
                   recv = recv + 1;
               end
               if (in_valid && in_ready) sent = sent + 1;
               in_valid  = (sent < NWORDS);
               in_data   = sent;
               out_ready = $random & 1'b1;
           end
           if (recv == NWORDS && errors == 0)
                $display("PASS: no word lost/reordered under backpressure");
           else $display("FAIL: recv=%0d of %0d errors=%0d (swallowed under backpressure)", recv, NWORDS, errors);
           $finish;
       end
   endmodule

In VHDL the bug is the same: in_ready <= '1'; (always accepting) versus in_ready <= (not ovalid) or out_ready; (gated). The buggy architecture overwrites the held payload whenever a word arrives during a stall.

vr_bug.vhd — BUGGY (in_ready = '1') vs FIXED (gated accept) in VHDL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   -- BUGGY: always accepting -> a word arriving during a stall overwrites the held one.
   entity vr_stage_bad is
       generic ( WIDTH : positive := 32 );
       port ( clk, rst : in std_logic;
              in_valid  : in  std_logic; in_ready : out std_logic;
              in_data   : in  std_logic_vector(WIDTH-1 downto 0);
              out_valid : out std_logic; out_ready : in std_logic;
              out_data  : out std_logic_vector(WIDTH-1 downto 0) );
   end entity;
   architecture rtl of vr_stage_bad is
       signal ovalid : std_logic := '0';
   begin
       out_valid <= ovalid;
       in_ready  <= '1';                                   -- BUG: always accepting
       process (clk)
           variable inf, outf : boolean;
       begin
           if rising_edge(clk) then
               inf  := (in_valid = '1');                   -- accepts regardless of stall
               outf := (ovalid = '1') and (out_ready = '1');
               if rst = '1' then ovalid <= '0'; out_data <= (others => '0');
               else
                   if outf then ovalid <= '0'; end if;
                   if inf then ovalid <= '1'; out_data <= in_data; end if;  -- clobbers held word
               end if;
           end if;
       end process;
   end architecture;
 
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   -- FIXED: gate in_ready on downstream backpressure -> the stall propagates upstream.
   entity vr_stage_good is
       generic ( WIDTH : positive := 32 );
       port ( clk, rst : in std_logic;
              in_valid  : in  std_logic; in_ready : out std_logic;
              in_data   : in  std_logic_vector(WIDTH-1 downto 0);
              out_valid : out std_logic; out_ready : in std_logic;
              out_data  : out std_logic_vector(WIDTH-1 downto 0) );
   end entity;
   architecture rtl of vr_stage_good is
       signal ovalid : std_logic := '0';
   begin
       out_valid <= ovalid;
       in_ready  <= (not ovalid) or out_ready;             -- FIX: gated accept
       process (clk)
           variable inf, outf : boolean;
       begin
           if rising_edge(clk) then
               inf  := (in_valid = '1') and ((ovalid = '0') or (out_ready = '1'));
               outf := (ovalid = '1') and (out_ready = '1');
               if rst = '1' then ovalid <= '0'; out_data <= (others => '0');
               else
                   if outf then ovalid <= '0'; end if;
                   if inf then ovalid <= '1'; out_data <= in_data; end if;
               end if;
           end if;
       end process;
   end architecture;
vr_bug_tb.vhd — self-checking; random backpressure exposes the swallowed word
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity vr_bug_tb is
   end entity;
 
   architecture sim of vr_bug_tb is
       constant WIDTH  : positive := 32;
       constant NWORDS : natural  := 200;
       signal clk       : std_logic := '0';
       signal rst       : std_logic := '1';
       signal in_valid  : std_logic := '0';
       signal in_ready  : std_logic;
       signal in_data   : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
       signal out_valid : std_logic;
       signal out_ready : std_logic := '0';
       signal out_data  : std_logic_vector(WIDTH-1 downto 0);
   begin
       -- Bind to vr_stage_good to PASS; to vr_stage_bad to observe lost words.
       dut : entity work.vr_stage_good
           generic map (WIDTH => WIDTH)
           port map (clk => clk, rst => rst,
                     in_valid => in_valid, in_ready => in_ready, in_data => in_data,
                     out_valid => out_valid, out_ready => out_ready, out_data => out_data);
 
       clk <= not clk after 5 ns;
 
       stim : process
           variable sent, recv : natural := 0;
           variable errors     : natural := 0;
           variable lfsr       : unsigned(7 downto 0) := x"5C";
           variable guard      : natural := 0;
       begin
           wait until rising_edge(clk); wait until rising_edge(clk); rst <= '0';
           while (recv < NWORDS) and (guard < 8*NWORDS) loop
               guard := guard + 1;
               if (out_valid = '1') and (out_ready = '1') then
                   assert out_data = std_logic_vector(to_unsigned(recv, WIDTH))
                       report "lost/reordered word under backpressure" severity error;
                   if out_data /= std_logic_vector(to_unsigned(recv, WIDTH)) then
                       errors := errors + 1;
                   end if;
                   recv := recv + 1;
               end if;
               if (in_valid = '1') and (in_ready = '1') then sent := sent + 1; end if;
               if sent < NWORDS then in_valid <= '1'; else in_valid <= '0'; end if;
               in_data   <= std_logic_vector(to_unsigned(sent, WIDTH));
               lfsr      := (lfsr(6 downto 0) & (lfsr(7) xor lfsr(5)));
               out_ready <= lfsr(0);
               wait until rising_edge(clk);
           end loop;
           assert (recv = NWORDS) and (errors = 0)
               report "words swallowed under backpressure (ungated in_ready)" severity error;
           report "vr_bug self-check complete" severity note;
           wait;
       end process;
   end architecture;

Across all three languages the lesson is one sentence: a block may accept a new input word only on a cycle where its held output can move — in_ready = !out_valid || out_ready — or backpressure fails to propagate and a word is silently swallowed.

5. Verification Strategy

Backpressure correctness is a conservation law, and the good news is that a scoreboard testbench checks it directly: drive an identifiable stream into the source, jerk the sink's ready around randomly, and prove the sink receives exactly that stream — every word, once, in order. The single invariant that captures a correct back-pressuring chain is:

No word is created or destroyed under backpressure. Every word that enters leaves exactly once, in the same order, for any pattern of downstream ready.

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

  • Scoreboard testbench with random backpressure (self-checking). Drive an incrementing sequence (0, 1, 2, …) into the source and de-assert the sink's ready on a random subset of cycles. The consumer side counts words out and asserts the k-th word out equals k — this proves in-order and exactly-once in one check, because a lost word makes the count fall short and a reordered/duplicated word makes out_data !== recv. The three §4a/§4d testbenches do exactly this: SV assert (out_data === recv), Verilog if (out_data !== recv) $display("FAIL…"), VHDL assert out_data = to_unsigned(recv,…) report … severity error. Run the same test against vr_stage_bad and it must fail — a verification suite that passes the buggy DUT is not testing backpressure.
  • Flow-conservation invariant. State it as the property that must hold at end of test: words_sent == words_received == NWORDS, with no gaps. Conceptually (v1-level, no SVA class needed): the number of accepted input transfers (in_valid && in_ready) always equals the number of completed output transfers (out_valid && out_ready) plus the number currently held in the chain. If that identity ever breaks, a block dropped or duplicated a word.
  • Payload-stability assertion under backpressure. While out_valid is high and out_ready is low, out_data next cycle must equal out_data this cycle, and out_valid must stay high — the §4c check. This catches a block that stalls its output but lets its data lines wobble, and a block that drops valid mid-stall (which would look to the consumer like the word vanished).
  • Source-stall corner. Assert that the source actually stalls when the chain is back-pressured: when the sink holds ready low long enough to fill the chain, the source's in_ready must go low and the source must stop advancing (its offered word does not change). Drive a burst, hold the sink off, and confirm nothing enters the chain during the stall — this proves the ripple reached all the way back.
  • Corner cases to exercise. Sink ready stuck low for many cycles (full stall, chain fills and holds); sink ready toggling every cycle (maximal jitter — the worst case for a lost word); a single-cycle ready pulse (one word slips through, the rest hold); ready low across a rst deassert (the chain must start empty and lose nothing); back-to-back full-throughput (ready always high, one word per cycle — no bubbles, the baseline).
  • Expected waveform. On a correct run, whenever out_valid && !out_ready, out_data is a flat horizontal line (held) and out_valid stays high across the stall; the moment out_ready rises, that word transfers and the next appears. Trace in_ready and you see it fall the cycle the sink stalls and the chain is full — the visible signature of the ripple reaching the source. The buggy DUT's waveform shows in_ready stuck high through a stall and out_data changing while out_valid && !out_ready — the held word being overwritten, live.

6. Common Mistakes

Producing anyway under backpressure (advancing without valid && ready). The most fundamental error: a block updates its output or considers a word "sent" on a cycle where out_ready was low. In valid/ready, a transfer happens only on valid && ready — if you advance state assuming the word left when it did not, the downstream never took it and the word is lost. Every state update that represents "this word moved on" must be gated on out_valid && out_ready, never on out_valid alone. This is the root of most backpressure data loss.

Not propagating the stall (holding output but still accepting input). A block correctly freezes its output when the sink stalls, but leaves its input ready high — so it keeps swallowing upstream words while its output slot is occupied, overwriting the held word (the §7 bug) or, in a deeper buffer, overrunning it. Holding your output is only half the job; the other half is closing the door upstream by gating in_ready on whether your current output can move. A stall that does not ripple back is not a stall, it is a leak.

Changing the payload while backpressured. The block holds out_valid high but lets out_data change cycle to cycle while out_ready is low. The consumer, which samples on the cycle it finally asserts ready, then captures a different word than the one that was presented earlier — silent corruption that looks like data getting scrambled. The 9.1 rule is absolute: while valid is high and ready is low, data must be bit-for-bit stable. In the register-stage pattern this is automatic (the payload updates only on accept); it breaks when someone drives out_data from combinational logic that keeps recomputing.

Combinational ready/valid loop. Gating in_ready on out_ready combinationally is correct — but if a block also makes its out_valid depend combinationally on its in_valid (a fully-combinational passthrough on both signals), a chain can form a combinational cycle where ready depends on valid depends on ready. It either fails to converge in simulation or creates a real combinational loop in synthesis. The register-stage pattern breaks this by registering out_valid; a purely combinational stage must break the loop on one direction (which is part of what the skid buffer of 9.3 formalizes).

Ignoring the length of the combinational ready path. Because ready ripples back through every block combinationally in one cycle, a long chain has a ready signal whose delay grows with the number of blocks — a timing-critical path that can miss its clock period. Treating a ten-stage valid/ready chain as if each link were independent ignores that the whole backward ready path is one combinational arc. The fix is architectural: register ready at intervals (skid buffers, 9.3) to segment the path, trading a cycle of latency for a shorter critical path.

Assuming full throughput because it passed with ready always high. A chain tested only with the sink always ready never exercises a single stall, so the swallowed-word bug (§7) is invisible — the block's ungated in_ready is never wrong when nothing ever back-pressures. The test that matters is random backpressure; a suite without it validates nothing about flow control. If your backpressure test passes the buggy DUT, the test, not the DUT, is broken.

7. DebugLab

The word that vanished under backpressure — a stall that never propagated upstream

The engineering lesson: backpressure must ripple all the way back — a block may accept a new input word only when it can move its current output. Holding your output under a downstream stall is only half the contract; the other half is de-asserting your input ready so the block above you holds too, and so on to the source. The tell is data loss that depends on when the sink stalls: a stream that is complete when the consumer is fast and short by a word or two when it back-pressures means a stall stopped at some block's output and never reached its input. Two habits make it impossible, and they are identical across SystemVerilog, Verilog, and VHDL: gate every block's in_ready on whether its current output can advance (!out_valid || out_ready), and verify with random downstream backpressure and a scoreboard, so no stall pattern goes untested. Ready flows backward exactly as data flows forward — build both directions or the contract is broken.

8. Interview Q&A

9. Exercises

Design and reasoning exercises — no syntax drills. Each rehearses the "data forward, ready backward, and the stall must ripple" habit.

Exercise 1 — Trace the ripple

A chain is source → A → B → C → sink, each block a single-entry valid/ready stage, all currently holding a valid word (the chain is full). On cycle t the sink de-asserts ready. Write down, for cycle t, the value of each link's ready (sink→C, C→B, B→A, A→source) and state which blocks hold and whether the source advances. Then the sink re-asserts ready on cycle t+1: describe what moves and in what order. (Hint: ready is combinational within a cycle; the whole backward arc resolves in cycle t.)

Exercise 2 — Predict the loss

Two engineers build the compute block from §1. Engineer A writes assign in_ready = !out_valid || out_ready;. Engineer B writes assign in_ready = 1'b1;. Both pass a testbench where the sink is always ready. Describe (i) exactly which cycle B's block first loses a word once the sink de-asserts ready, (ii) what the scoreboard sees (sent vs received count, and the first out-of-order value), and (iii) why A's block is safe by construction. Then say what one change to the testbench would have exposed B's bug on the first run.

Exercise 3 — Where the payload must be held

For the §4a stage, prove on paper that out_data cannot change while out_valid && !out_ready. Identify the exact signal that gates the payload register, show why it is low during a stall, and then describe a plausible refactor (e.g. driving out_data from combinational logic instead of a register) that would break the stability guarantee even though out_valid still holds — and what the consumer would observe as a result.

Exercise 4 — Size the timing problem

You have a valid/ready chain of 12 combinational-ready stages, and static timing shows the backward ready path is now the critical path. Without changing the number of stages, give two architectural changes that shorten the ready arc, and name the cost each introduces (latency, storage, or throughput). (Hint: think about registering ready, where a skid buffer fits, and whether every link truly needs a combinational ready.)

Continue in RTL Design Patterns (sibling and forward links unlock as they ship):

  • valid/ready Handshake — Chapter 9.1; the direct prerequisite — the two-signal contract, what one transfer is (valid && ready), and the producer-holds rule this page propagates through a chain.
  • Skid Buffer — Chapter 9.3; registers ready to break the long combinational backward path this page's chain suffers from, without dropping the in-flight word — the timing fix for backpressure.
  • Credit-Based Flow Control — Chapter 9.4; the alternative to per-cycle backpressure — the consumer grants credits ahead of time so the producer never has to stall on a combinational ready.
  • Handshake Bugs & Anti-Patterns — Chapter 9.5; the catalogue of valid/ready failures, of which the swallowed word here is one.
  • Ready/Valid Pipelining — Chapter 9.6; composing back-pressuring stages into a full-throughput pipeline.

In-track dependencies (what backpressure drives and buffers):

  • Pipeline Stalls, Bubbles & Flush — Chapter 8.4; the stall mechanism this page rides — stall = valid_out && !ready_out, clock-enable freeze upstream, bubble below — now driven by the interface ready instead of a manual stall wire.
  • Pipeline Valid-Bit Discipline — Chapter 8.3; carrying valid alongside data, the forward half of the contract whose backward half is backpressure.
  • Pipelined Datapath — Chapter 8.2; the forward-marching pipeline that backpressure teaches how to stall correctly.
  • Synchronous FIFO Architecture — Chapter 7.1; the elastic buffer that absorbs backpressure — a FIFO between producer and consumer decouples their rates so the stall doesn't ripple every cycle.
  • FIFO Full/Empty Logic — Chapter 7.2; full is a FIFO's backpressure (it de-asserts write-ready) and empty gates its read-valid — the same contract, named differently.
  • Register with Enable/Load — Chapter 2.3; the clock-enabled register that a valid/ready stage is built from — enable = accept, hold = stall.

Verilog v1 prerequisites (the grammar this pattern transcribes into):

  • Blocking and Non-Blocking Assignments — why the payload/valid registers use non-blocking (<=) in the clocked block, and the gated-accept combinational logic uses continuous assignment.
  • If-Else Statements — the conditional structure behind the accept/hold decision inside the clocked block.
  • Case Statements — the construct a multi-state flow-control block generalizes to when a stage carries more than one entry.

11. Summary

  • A valid/ready chain carries two flows in opposite directions. Data flows forward (valid, data, producer-driven); readiness — backpressure — flows backward (ready, consumer-driven). A word transfers only on a cycle where valid && ready; on valid && !ready the producer must hold valid high and data stable. Backpressure is not a bolt-on — it is the interface seen from the readiness direction.
  • A stall must propagate, not stay local. A block that cannot hand off its output must both freeze that output and de-assert its own input ready, so the block above holds too. That refusal ripples upstream, block by block, on the same cycle, until it reaches the source, which holds per 9.1. This is the 8.4 stall (stall = valid_out && !ready_out, clock-enable freeze upstream, bubble below), now driven by the interface ready.
  • The gated-accept rule is the whole pattern. A single-entry stage asserts in_ready = !out_valid || out_ready — accept a new word only when the output slot is empty or is emptying this cycle. This one line both propagates the stall and holds the payload stable for free (the data register updates only on an accepted transfer), and replacing it with a constant 1 is the signature swallowed-word bug of §7.
  • The backward ready path is combinational and grows with chain length. Because ready ripples through every block's accept gate in one cycle, a long chain has a timing-critical backward arc — the problem the skid buffer (9.3) solves by registering ready, trading a cycle of latency for a shorter path.
  • Correctness is flow conservation, proved by a scoreboard under random backpressure. No word is created or destroyed: every word in leaves exactly once, in order, for any ready pattern. Verify by driving an identifiable stream, jittering the sink's ready randomly, and asserting the k-th word out equals k — self-checking testbenches shown here in SystemVerilog, Verilog, and VHDL, each of which fails the ungated-in_ready DUT and passes the gated fix.