Skip to content

RTL Design Patterns · Chapter 9 · Handshake & Flow Control

The valid/ready Handshake

The valid/ready handshake is a two-signal contract that lets two blocks running at independent rates move data safely, one word at a time, with no custom glue. Valid is the producer saying it has data this cycle; ready is the consumer saying it can accept this cycle; and a transfer happens on every cycle both are high, and only then. This lesson adds the backward ready signal to the producer-side valid bit, then pins down the three rules that make the contract composable so any well-behaved source connects to any well-behaved sink. It also shows the classic mistake that breaks it: letting valid depend combinationally on ready, so two blocks each wait for the other and deadlock forever. The source, the sink, and the deadlock beside its fix are all coded in SystemVerilog, Verilog, and VHDL.

Intermediate16 min readRTL Design Patternsvalid/readyHandshakeBackpressureFlow ControlComposability

Chapter 9 · Section 9.1 · Handshake & Flow Control

1. The Engineering Problem

In 8.3 you gave a producer's output a valid bit: a one-bit qualifier that said this cycle carries a real result, pipelined so it stayed aligned with the data, so a consumer could ignore fill cycles and bubbles. That solved half the problem — the consumer now knows when the data is real. But it left the other half wide open: the producer has no idea whether the consumer can take the data this cycle. The valid bit is a one-way announcement. If the consumer is busy — its input buffer full, an earlier operation still in flight — the producer keeps asserting valid and driving a real word, the consumer cannot accept it, and the word is lost. Validity alone gives you a stream you can read correctly only if you can read it every cycle it is valid, which a real consumer running at its own rate cannot promise.

Here is the concrete need. You have a producer — say a compute block that finishes a result every few cycles, at data-dependent times — feeding a consumer that writes each result into a downstream FIFO. The FIFO is sometimes full. The producer sometimes has nothing. Neither side controls the other's schedule: the producer emits when its work is done, the consumer accepts when it has room, and those two events do not line up. If the producer just drives valid and its data whenever it has a result, a word offered on a cycle the FIFO is full falls on the floor — the consumer never latched it, and the producer, having no feedback, has already moved on. Symmetrically, if the consumer just reads whenever it has room, it will read on cycles the producer has nothing and store a stale or undefined word.

So the two blocks face a coordination question the data lines cannot answer alone: on which exact cycle does a word actually move from producer to consumer? It cannot be "every cycle the producer has data" (the consumer may be full) and it cannot be "every cycle the consumer has room" (the producer may be empty). It must be the cycle both conditions hold at once — and both sides must agree on that cycle, so exactly one word moves and neither side double-counts or drops it. That agreement is a two-signal contract: valid forward from the producer, ready backward from the consumer, with a transfer defined as the cycle they overlap. That contract is the valid/ready handshake.

the-need.v — valid alone loses data; you need agreement on the transfer cycle
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 8.3 gave the producer a VALID bit: a one-way "this cycle is real" announcement.
   wire        valid;      // producer: I have a real word this cycle
   wire [31:0] data;       // ...the word
 
   // But the consumer runs at its OWN rate -- sometimes it has no room:
   wire        space;      // consumer: I could accept a word this cycle
 
   // WRONG -- consumer reads only when IT has room, ignoring whether data is real:
   //   always @(posedge clk) if (space) buf <= data;   // stores stale/undefined words
 
   // WRONG -- producer drives valid+data every cycle it has work, ignoring the consumer:
   //   a word offered while the consumer is full is NEVER latched -> silently lost
 
   // The need: a BACKWARD signal, ready, plus the rule that a word moves on EXACTLY the
   // cycle valid AND ready are both high -- and both sides agree. That is the handshake.

2. Mental Model

3. Pattern Anatomy

The interface is three things on the wire — a data bus, a forward valid, a backward ready — plus one derived signal, the transfer. Everything hard is in the rules that govern how each side may drive its signal, because those rules are what make two independently-designed blocks compose with no glue.

The transfer condition. A single expression defines when a word moves: transfer = valid && ready. It is combinational and both sides see the same two wires, so producer and consumer compute the identical transfer every cycle. The producer uses it to know its word was taken (so it may present the next one); the consumer uses it to know a real word arrived (so it may capture it). There is exactly one transfer per cycle the two signals overlap, and a word that is offered but not accepted simply waits — the transfer for it happens on a later cycle, whenever ready finally rises while valid is still high.

The four cycle types. Every cycle of a handshake link is exactly one of four kinds, and naming them is half the pattern:

  • Idlevalid low, ready low (or either low with the other also low): nothing is offered and nothing is wanted. The link rests.
  • Stall / waitvalid high, ready low: the producer is offering a word but the consumer cannot take it. The word is held; no transfer. This is backpressure — the consumer pushing back on the producer.
  • Transfervalid high, ready high: the word moves, exactly once, this cycle.
  • Bubblevalid low, ready high: the consumer is waiting with room, but the producer has nothing to send. No transfer; the consumer's readiness is simply unused this cycle.

The four cycle types — a transfer happens only where valid and ready overlap

data flow
The four cycle types — a transfer happens only where valid and ready overlaphave dataready risesno more dataadvanceidle : valid=0ready=0nothing offered, nothing wantedstall : valid=1ready=0offered but held — backpressuretransfer :valid=1 ready=1the word moves, exactly oncebubble : valid=0ready=1room, but nothing to sendnext wordproducer may advance only after xfer
Read the link one cycle at a time. When valid is high and ready is low the producer is offering a word the consumer cannot yet take — a stall, which is backpressure holding the word in place. The word does not move until ready rises while valid is still high: that is the transfer, and it happens on exactly that one cycle. A cycle with ready high but valid low is a bubble — the consumer has room but the producer has nothing. Both sides compute transfer = valid && ready from the same two wires, so they always agree which cycle the word moved and the producer advances to its next word only after a transfer. This is identical in SystemVerilog, Verilog, and VHDL and is the exact contract under AXI, APB, and AHB.

The three rules that make it composable. The signals are trivial; the rules are the pattern. They are what let any well-behaved source connect to any well-behaved sink with no adapter logic — the composability primitive:

  • Rule 1 — valid must not depend combinationally on ready. The producer asserts valid from its own state (it has a word), never from the consumer's ready. If valid were have_data && ready and the consumer's ready in turn looked at valid, the two would form a combinational loop and the link could deadlock — each side waiting for the other to go first, so neither ever does. Local decision for valid is what breaks that cycle. (This is the §7 deadlock.)
  • Rule 2 — once valid is high, it stays high with data stable until the transfer. The producer may not retract an offer or change the payload mid-offer. Assert valid, and both valid and data are frozen until the cycle ready is finally high and the word transfers. This lets the consumer stall for any number of cycles and still latch the exact word that was offered.
  • Rule 3 — ready may depend combinationally on valid. The asymmetry is deliberate. A consumer is allowed to look at valid before asserting readya sink can look before it leaps — for instance asserting ready only when it both has room and sees a valid offer. Because valid never looks back at ready, this is safe: the dependency goes one way only, so no loop forms. Rules 1 and 3 together are the exact reason the contract composes.

A well-behaved source and a well-behaved sink. A well-behaved source registers its payload, asserts valid from local state (a have_data flag), holds valid and data stable while valid && !ready, and advances to its next word only on a transfer. A well-behaved sink asserts ready from its own free space (space), and captures data on the cycle valid && ready. Neither knows anything about the other's internals — they agree only on the three wires and the transfer condition — which is exactly why they compose. The FIFO of Chapter 7 is a sink on its write side and a source on its read side; wiring two handshake blocks together is just connecting valid/data forward and ready backward with no glue.

The combinational ready chain — previewing 9.2 and 9.3. There is one cost hidden in Rule 3. Because a sink's ready may look at its consumer's ready, a chain of blocks that each pass ready straight through combinationally builds a long combinational path: the readiness of the last block ripples back through every stage in a single cycle, and that backpressure path can become the critical timing path of the whole pipeline. The producer half is registered and clean; the ready half, if built purely combinationally, is where long paths hide. Breaking that path — registering ready and absorbing the one in-flight word it would otherwise drop — is exactly what the skid buffer (9.3) does, and it is why backpressure and stalls (9.2) get their own treatment. For now the rule is: the transfer condition is combinational and cheap, but a fully-combinational ready chain is where you pay, and the fix is a registered stage, not a change to the contract.

The transfer condition and the three rules — why any source composes with any sink

data flow
The transfer condition and the three rules — why any source composes with any sinkvalidreadystable datacomposessource : valid,datavalid from LOCAL have_data (Rule 1)hold untilacceptedvalid + data stable while !ready (Rule 2)transfer = valid&& readythe one condition both sides computesink : ready,spaceready MAY look at valid (Rule 3)any source ↔ anysinkno glue — the composability primitive
The source drives valid from its own have_data state and never looks at ready (Rule 1), and holds valid and data stable until the word is accepted (Rule 2). The sink may drive ready as a function of valid (Rule 3) — the asymmetry is one-way, so no combinational loop can form. Both sides compute the single condition transfer = valid && ready from the same wires, so they agree on every transfer with no shared state. Because the rules constrain only the wires and not the internals, any well-behaved source connects to any well-behaved sink with zero adapter logic — that no-glue composability is the whole reason this exact contract sits under AXI, APB, and AHB, and it is identical across SystemVerilog, Verilog, and VHDL.

4. Real RTL Implementation

This is the core of the page. We build two things — (a) a well-behaved valid/ready source and sink (source: registered payload, valid asserted from a local have_data flag, valid and data held until the transfer; sink: ready from local space; transfer on valid && ready; WIDTH-generic) with a scoreboard testbench driving randomized valid/ready gaps and asserting in-order, exactly-once delivery with data stable under stall; and (b) the valid-gated-by-ready deadlock beside its correct-independence fix — and each is shown in SystemVerilog, Verilog, and VHDL with an RTL block and a self-checking clocked testbench. The reasoning is identical in all three; only the syntax differs.

One discipline runs through every RTL block, inherited from 8.3 and the register pattern: registers update on the clock with non-blocking assignments, reset is explicit (synchronous, active-high rst here), the source drives valid only from its own registered state (never from ready), and both sides read the same valid && ready to detect the transfer.

4a. A well-behaved source and sink — three ways

The source holds a small stream of words. It registers the current word into data, drives valid high whenever it still have_data, and — obeying Rule 2 — leaves valid and data untouched until the cycle valid && ready, at which point it advances to the next word (or drops valid when the stream is exhausted). The sink drives ready from its own space and captures data on the transfer.

vr_source.sv — a well-behaved valid/ready source (valid from local state, held until accepted)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module vr_source #(
       parameter int WIDTH = 32,                      // datapath width
       parameter int NWORDS = 8                        // words this source will emit
   )(
       input  logic             clk,
       input  logic             rst,                  // synchronous, active-high
       input  logic             ready,                // BACKWARD signal from the sink
       output logic             valid,                // 1 = I have a real word this cycle
       output logic [WIDTH-1:0] data                  // the word (held stable while !ready)
   );
       logic [$clog2(NWORDS+1)-1:0] idx;               // which word we are offering
       logic have_data;                                 // local state: is a word pending?
 
       // TRANSFER: the ONE condition -- computed from valid and the incoming ready.
       // Note valid below is a function of have_data ONLY, never of ready (Rule 1).
       wire xfer = valid & ready;
 
       always_ff @(posedge clk) begin
           if (rst) begin
               idx       <= '0;
               have_data <= 1'b1;                      // start with a word to send
               data      <= '0;
           end else if (xfer) begin
               // The offered word was ACCEPTED this cycle -> advance to the next.
               if (idx == NWORDS-1) have_data <= 1'b0; // stream exhausted -> go idle
               else                 idx       <= idx + 1'b1;
           end
           // else (no transfer): HOLD. We do not touch data or have_data, so the offer
           // stays put with the SAME payload until ready finally rises (Rule 2).
       end
 
       // valid comes from LOCAL state only (Rule 1): high iff we still have a word.
       assign valid = have_data;
       // data is a pure function of the (registered) index -> stable while idx is held.
       assign data  = {{(WIDTH-8){1'b0}}, 8'hA0} + idx;   // word k = 0xA0 + k, deterministic
   endmodule

The sink is the mirror image: it advertises ready from its own free space and latches the word on the transfer. Here the sink models a small buffer that is occasionally full (space toggles), so it exercises the stall path — valid high while ready low.

vr_sink.sv — a well-behaved sink (ready from local space; capture on valid && ready)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module vr_sink #(
       parameter int WIDTH = 32
   )(
       input  logic             clk,
       input  logic             rst,
       input  logic             valid,                // forward from the source
       input  logic [WIDTH-1:0] data,
       input  logic             space,                // local: I have room this cycle
       output logic             ready,                // 1 = I can accept this cycle
       output logic             got,                  // pulses on each accepted word
       output logic [WIDTH-1:0] captured              // the last word we accepted
   );
       // ready is driven from LOCAL space. It MAY also look at valid (Rule 3), but the
       // simplest well-behaved sink just advertises room; the transfer is valid && ready.
       assign ready = space;
       wire   xfer  = valid & ready;                   // both sides see the same condition
 
       always_ff @(posedge clk) begin
           if (rst) begin
               got      <= 1'b0;
               captured <= '0;
           end else begin
               got <= xfer;                            // one-cycle pulse per accepted word
               if (xfer) captured <= data;             // capture ONLY on the transfer
           end
       end
   endmodule

The scoreboard testbench is where the flagship stands or falls: it connects the source to the sink, drives space with a randomized on/off pattern (so ready gaps at random cycles, forcing stalls), and checks the invariant that matters — every word the source emits is received exactly once, in order, and the data is stable across every stall. It models the expected stream as a simple counter and compares each accepted word against it.

vr_source_sink_tb.sv — scoreboard: randomized ready gaps; in-order, exactly-once, data-stable
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module vr_source_sink_tb;
       localparam int WIDTH = 32, NWORDS = 8;
       logic clk = 1'b0, rst, valid, ready, space, got;
       logic [WIDTH-1:0] data, captured;
       int   errors = 0, received = 0;
 
       vr_source #(.WIDTH(WIDTH), .NWORDS(NWORDS)) src (
           .clk(clk), .rst(rst), .ready(ready), .valid(valid), .data(data));
       vr_sink #(.WIDTH(WIDTH)) snk (
           .clk(clk), .rst(rst), .valid(valid), .data(data), .space(space),
           .ready(ready), .got(got), .captured(captured));
 
       always #5 clk = ~clk;
 
       // Scoreboard: the next word we EXPECT to receive, in order.
       logic [WIDTH-1:0] expect_word;
 
       // Stability check: when valid is held (valid && !ready), data must not change.
       logic [WIDTH-1:0] prev_data;
       logic             prev_valid, prev_ready;
 
       initial begin
           rst = 1'b1; space = 1'b0; expect_word = 32'hA0; prev_valid = 1'b0;
           @(posedge clk); #1; rst = 1'b0;
 
           // Run long enough to drain all NWORDS through a gappy ready.
           for (int t = 0; t < 4*NWORDS + 8; t++) begin
               @(negedge clk);
               // Randomized backpressure: ready gaps at pseudo-random cycles.
               space = ($urandom_range(0, 2) != 0);   // ~2/3 of cycles ready
               prev_data  = data; prev_valid = valid; prev_ready = ready;
 
               @(posedge clk); #1;
               // Rule 2 check: if the source held a valid offer that was NOT accepted
               // last cycle (valid && !ready), the data must be unchanged this cycle.
               if (prev_valid && !prev_ready)
                   assert (data === prev_data)
                       else begin $error("t=%0d data changed under stall %h -> %h", t, prev_data, data); errors++; end
               // On every accepted word, it must be the next expected word, in order.
               if (got) begin
                   assert (captured === expect_word)
                       else begin $error("t=%0d out-of-order/duplicate: got %h exp %h", t, captured, expect_word); errors++; end
                   expect_word = expect_word + 1;       // advance the scoreboard
                   received++;
               end
           end
 
           // Exactly-once: every emitted word arrived, none dropped, none duplicated.
           assert (received === NWORDS)
               else begin $error("received %0d words, expected %0d", received, NWORDS); errors++; end
           if (errors == 0) $display("PASS: %0d words in order, exactly once, data stable under stall", received);
           else             $display("FAIL: %0d errors", errors);
           $finish;
       end
   endmodule

The Verilog form is the same structure with reg/wire typing; valid is still a pure function of the registered have_data, and the transfer is still valid & ready seen identically by both sides.

vr_source.v — well-behaved source in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module vr_source #(
       parameter WIDTH = 32,
       parameter NWORDS = 8
   )(
       input  wire             clk,
       input  wire             rst,
       input  wire             ready,                // backward from the sink
       output wire             valid,
       output wire [WIDTH-1:0] data
   );
       reg [31:0] idx;                                // wide enough for NWORDS
       reg        have_data;
 
       wire xfer = valid & ready;                     // the transfer condition
 
       always @(posedge clk) begin
           if (rst) begin
               idx <= 0; have_data <= 1'b1;
           end else if (xfer) begin
               if (idx == NWORDS-1) have_data <= 1'b0;   // exhausted -> idle
               else                 idx       <= idx + 1'b1;
           end
           // else HOLD: no assignment, so idx and have_data are frozen (Rule 2).
       end
 
       assign valid = have_data;                      // valid from LOCAL state only (Rule 1)
       assign data  = 32'hA0 + idx;                   // word k = 0xA0 + k, stable while idx held
   endmodule
vr_sink.v — well-behaved sink in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module vr_sink #(
       parameter WIDTH = 32
   )(
       input  wire             clk,
       input  wire             rst,
       input  wire             valid,
       input  wire [WIDTH-1:0] data,
       input  wire             space,
       output wire             ready,
       output reg              got,
       output reg  [WIDTH-1:0] captured
   );
       assign ready = space;                          // ready from local space (Rule 3 allows valid too)
       wire   xfer  = valid & ready;
 
       always @(posedge clk) begin
           if (rst) begin
               got <= 1'b0; captured <= 0;
           end else begin
               got <= xfer;                           // pulse per accepted word
               if (xfer) captured <= data;            // capture only on the transfer
           end
       end
   endmodule
vr_source_sink_tb.v — self-checking scoreboard; in-order, exactly-once, data-stable
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module vr_source_sink_tb;
       parameter WIDTH = 32, NWORDS = 8;
       reg  clk, rst, space;
       wire valid, ready, got;
       wire [WIDTH-1:0] data, captured;
       integer t, errors, received;
       reg [WIDTH-1:0] expect_word, prev_data;
       reg prev_valid, prev_ready;
 
       vr_source #(.WIDTH(WIDTH), .NWORDS(NWORDS)) src (
           .clk(clk), .rst(rst), .ready(ready), .valid(valid), .data(data));
       vr_sink #(.WIDTH(WIDTH)) snk (
           .clk(clk), .rst(rst), .valid(valid), .data(data), .space(space),
           .ready(ready), .got(got), .captured(captured));
 
       always #5 clk = ~clk;
 
       initial begin
           errors = 0; received = 0; expect_word = 32'hA0; prev_valid = 1'b0;
           clk = 1'b0; rst = 1'b1; space = 1'b0;
           @(posedge clk); #1; rst = 1'b0;
 
           for (t = 0; t < 4*NWORDS + 8; t = t + 1) begin
               @(negedge clk);
               space = ($random % 3) != 0;            // ~2/3 of cycles ready -> gappy backpressure
               prev_data = data; prev_valid = valid; prev_ready = ready;
 
               @(posedge clk); #1;
               if (prev_valid && !prev_ready && (data !== prev_data)) begin
                   $display("FAIL: t=%0d data changed under stall %h -> %h", t, prev_data, data);
                   errors = errors + 1;
               end
               if (got) begin
                   if (captured !== expect_word) begin
                       $display("FAIL: t=%0d out-of-order/dup: got %h exp %h", t, captured, expect_word);
                       errors = errors + 1;
                   end
                   expect_word = expect_word + 1;
                   received = received + 1;
               end
           end
 
           if (received !== NWORDS) begin
               $display("FAIL: received %0d words, expected %0d", received, NWORDS);
               errors = errors + 1;
           end
           if (errors == 0) $display("PASS: %0d words in order, exactly once, data stable under stall", received);
           else             $display("FAIL: %0d errors", errors);
           $finish;
       end
   endmodule

In VHDL the source is a clocked process over an index with a have_data flag; valid is a concurrent function of have_data (never of ready), and the transfer is valid and ready.

vr_source.vhd — well-behaved source in VHDL (numeric_std)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity vr_source is
       generic (
           WIDTH  : positive := 32;
           NWORDS : positive := 8
       );
       port (
           clk   : in  std_logic;
           rst   : in  std_logic;                     -- synchronous, active-high
           ready : in  std_logic;                     -- backward from the sink
           valid : out std_logic;
           data  : out std_logic_vector(WIDTH-1 downto 0)
       );
   end entity;
 
   architecture rtl of vr_source is
       signal idx       : integer range 0 to NWORDS := 0;
       signal have_data : std_logic := '1';
       signal valid_i   : std_logic;
   begin
       valid_i <= have_data;                          -- valid from LOCAL state only (Rule 1)
       valid   <= valid_i;
       -- word k = 0xA0 + k, a pure function of the registered index -> stable while held.
       data    <= std_logic_vector(to_unsigned(16#A0# + idx, WIDTH));
 
       process (clk)
           variable xfer : std_logic;
       begin
           if rising_edge(clk) then
               xfer := valid_i and ready;             -- the transfer condition
               if rst = '1' then
                   idx <= 0; have_data <= '1';
               elsif xfer = '1' then
                   if idx = NWORDS-1 then have_data <= '0';   -- exhausted -> idle
                   else                   idx       <= idx + 1;
                   end if;
               end if;
               -- else HOLD: idx and have_data untouched, so the offer is frozen (Rule 2).
           end if;
       end process;
   end architecture;
vr_sink.vhd — well-behaved sink in VHDL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity vr_sink is
       generic ( WIDTH : positive := 32 );
       port (
           clk      : in  std_logic;
           rst      : in  std_logic;
           valid    : in  std_logic;
           data     : in  std_logic_vector(WIDTH-1 downto 0);
           space    : in  std_logic;                  -- local room this cycle
           ready    : out std_logic;
           got      : out std_logic;                  -- pulses per accepted word
           captured : out std_logic_vector(WIDTH-1 downto 0)
       );
   end entity;
 
   architecture rtl of vr_sink is
       signal ready_i : std_logic;
   begin
       ready_i <= space;                              -- ready from local space
       ready   <= ready_i;
 
       process (clk)
           variable xfer : std_logic;
       begin
           if rising_edge(clk) then
               xfer := valid and ready_i;             -- both sides see the same condition
               if rst = '1' then
                   got <= '0'; captured <= (others => '0');
               else
                   got <= xfer;                       -- pulse per accepted word
                   if xfer = '1' then captured <= data; end if;   -- capture on the transfer
               end if;
           end if;
       end process;
   end architecture;
vr_source_sink_tb.vhd — self-checking scoreboard; in-order, exactly-once, data-stable
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity vr_source_sink_tb is
   end entity;
 
   architecture sim of vr_source_sink_tb is
       constant WIDTH : positive := 32; constant NWORDS : positive := 8;
       signal clk : std_logic := '0';
       signal rst, valid, ready, space, got : std_logic := '0';
       signal data, captured : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
   begin
       src : entity work.vr_source
           generic map (WIDTH => WIDTH, NWORDS => NWORDS)
           port map (clk => clk, rst => rst, ready => ready, valid => valid, data => data);
       snk : entity work.vr_sink
           generic map (WIDTH => WIDTH)
           port map (clk => clk, rst => rst, valid => valid, data => data, space => space,
                     ready => ready, got => got, captured => captured);
 
       clkgen : process begin clk <= '0'; wait for 5 ns; clk <= '1'; wait for 5 ns; end process;
 
       stim : process
           variable expect_word : integer := 16#A0#;
           variable received    : integer := 0;
           variable errors      : integer := 0;
           variable prev_data   : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
           variable prev_valid, prev_ready : std_logic := '0';
           variable seed1, seed2 : positive := 7;
           variable r : real;
       begin
           rst <= '1'; space <= '0'; wait until rising_edge(clk); wait for 1 ns; rst <= '0';
 
           for t in 0 to 4*NWORDS + 8 loop
               wait until falling_edge(clk);
               uniform(seed1, seed2, r);              -- pseudo-random backpressure
               if r < 0.66 then space <= '1'; else space <= '0'; end if;
               prev_data := data; prev_valid := valid; prev_ready := ready;
 
               wait until rising_edge(clk); wait for 1 ns;
               -- Rule 2: a held valid offer must keep the same data.
               if prev_valid = '1' and prev_ready = '0' then
                   assert data = prev_data
                       report "data changed under stall" severity error;
               end if;
               -- Exactly-once, in order: each accepted word is the next expected.
               if got = '1' then
                   assert captured = std_logic_vector(to_unsigned(expect_word, WIDTH))
                       report "out-of-order or duplicate word" severity error;
                   expect_word := expect_word + 1;
                   received    := received + 1;
               end if;
           end loop;
 
           assert received = NWORDS
               report "wrong number of words received" severity error;
           report "vr source/sink self-check complete" severity note;
           wait;
       end process;
   end architecture;

4b. The valid-gated-by-ready deadlock — buggy vs fixed, in all three HDLs

Pattern (b) is the signature failure, shown here as buggy vs fixed RTL in each language and dramatized in §7. The bug violates Rule 1: the source computes valid = have_data && ready, so valid now depends combinationally on ready. Wire that source to a sink whose ready looks at valid (ready = space && valid, which Rule 3 allows), and the two form a combinational loop where each waits for the other: valid will not rise until ready is high, and ready will not rise until valid is high, so neither ever rises and no word ever transfers. The fix is Rule 1: drive valid from local state only.

vr_deadlock.sv — BUGGY (valid gated by ready) vs FIXED (valid from local state)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY source: valid is gated by ready -> valid depends combinationally on ready.
   // Paired with a sink that gates ready on valid, this is a combinational loop that
   // deadlocks: neither valid nor ready ever rises, so no transfer ever happens.
   module vr_source_bad #(parameter WIDTH = 32) (
       input  logic clk, rst, ready,
       output logic valid,
       output logic [WIDTH-1:0] data
   );
       logic have_data;
       always_ff @(posedge clk) if (rst) have_data <= 1'b1; else have_data <= have_data;
       assign valid = have_data & ready;              // WRONG: valid looks at ready (breaks Rule 1)
       assign data  = 32'hA0;
   endmodule
 
   // FIXED source: valid comes from local state ONLY; the transfer is valid && ready,
   // computed by whoever needs it -- but valid itself never looks at ready.
   module vr_source_good #(parameter WIDTH = 32) (
       input  logic clk, rst, ready,
       output logic valid,
       output logic [WIDTH-1:0] data
   );
       logic have_data;
       always_ff @(posedge clk)
           if (rst)              have_data <= 1'b1;
           else if (valid & ready) have_data <= 1'b0; // advance only on the transfer
       assign valid = have_data;                      // RIGHT: valid from local state only (Rule 1)
       assign data  = 32'hA0;
   endmodule

The testbench pairs each source with a sink whose ready legally looks at valid (Rule 3), then simply checks whether any transfer ever occurs. The buggy source never transfers (the loop deadlocks); the fixed one transfers on the first ready cycle.

vr_deadlock_tb.sv — the deadlock DUT never transfers; the fixed DUT does
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module vr_deadlock_tb;
       localparam int WIDTH = 32;
       logic clk = 1'b0, rst, valid, ready;
       logic [WIDTH-1:0] data;
       logic space = 1'b1;                            // the sink always has room
       int   transfers = 0, errors = 0;
 
       // Bind to vr_source_good to PASS. Swap to vr_source_bad to WATCH the deadlock:
       // no transfer ever fires even though the sink is permanently ready-capable.
       vr_source_good #(.WIDTH(WIDTH)) dut (.clk(clk), .rst(rst), .ready(ready),
                                            .valid(valid), .data(data));
 
       // A sink whose ready LEGALLY looks at valid (Rule 3): ready = space && valid.
       // Safe with a good source (valid never looks back); fatal with the bad one.
       assign ready = space & valid;
 
       always #5 clk = ~clk;
 
       initial begin
           rst = 1'b1;
           @(posedge clk); #1; rst = 1'b0;
           for (int t = 0; t < 20; t++) begin
               @(posedge clk); #1;
               if (valid && ready) transfers++;       // count real transfers
           end
           // A correct link transfers at least once; the deadlock link transfers zero.
           assert (transfers > 0)
               else begin $error("DEADLOCK: no transfer occurred in 20 cycles"); errors++; end
           if (errors == 0) $display("PASS: link makes progress (%0d transfers)", transfers);
           else             $display("FAIL: link deadlocked -- valid gated by ready");
           $finish;
       end
   endmodule

The Verilog buggy/fixed pair tells the same story with reg/wire typing; the whole bug is the single & that lets valid read ready.

vr_deadlock.v — BUGGY vs FIXED in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: valid gated by ready -> combinational loop with a ready-looks-at-valid sink.
   module vr_source_bad #(parameter WIDTH = 32) (
       input wire clk, rst, ready,
       output wire valid, output wire [WIDTH-1:0] data
   );
       reg have_data;
       always @(posedge clk) if (rst) have_data <= 1'b1; else have_data <= have_data;
       assign valid = have_data & ready;              // WRONG: valid looks at ready
       assign data  = 32'hA0;
   endmodule
 
   // FIXED: valid from local state only; advance have_data only on the transfer.
   module vr_source_good #(parameter WIDTH = 32) (
       input wire clk, rst, ready,
       output wire valid, output wire [WIDTH-1:0] data
   );
       reg have_data;
       always @(posedge clk)
           if (rst)                 have_data <= 1'b1;
           else if (valid & ready)  have_data <= 1'b0;
       assign valid = have_data;                      // RIGHT: local state only (Rule 1)
       assign data  = 32'hA0;
   endmodule
vr_deadlock_tb.v — self-checking; the deadlock DUT never transfers
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module vr_deadlock_tb;
       parameter WIDTH = 32;
       reg  clk, rst;
       wire valid, ready;
       wire [WIDTH-1:0] data;
       reg  space;
       integer t, transfers, errors;
 
       // Bind to vr_source_good to PASS; vr_source_bad deadlocks.
       vr_source_good #(.WIDTH(WIDTH)) dut (.clk(clk), .rst(rst), .ready(ready),
                                            .valid(valid), .data(data));
 
       assign ready = space & valid;                  // sink ready legally looks at valid (Rule 3)
 
       always #5 clk = ~clk;
 
       initial begin
           transfers = 0; errors = 0; clk = 1'b0; rst = 1'b1; space = 1'b1;
           @(posedge clk); #1; rst = 1'b0;
           for (t = 0; t < 20; t = t + 1) begin
               @(posedge clk); #1;
               if (valid && ready) transfers = transfers + 1;
           end
           if (transfers == 0) begin
               $display("FAIL: link deadlocked -- valid gated by ready (0 transfers)");
               errors = errors + 1;
           end
           if (errors == 0) $display("PASS: link makes progress (%0d transfers)", transfers);
           $finish;
       end
   endmodule

In VHDL the same bug appears when valid <= have_data and ready; the fix is valid <= have_data with the index/flag advanced only on valid and ready.

vr_deadlock.vhd — BUGGY (valid gated by ready) vs FIXED (valid from local state)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   -- BUGGY: valid depends on ready -> combinational loop with a ready-looks-at-valid sink.
   entity vr_source_bad is
       generic ( WIDTH : positive := 32 );
       port ( clk, rst, ready : in std_logic;
              valid : out std_logic;
              data  : out std_logic_vector(WIDTH-1 downto 0) );
   end entity;
   architecture rtl of vr_source_bad is
       signal have_data : std_logic := '1';
   begin
       process (clk) begin
           if rising_edge(clk) then
               if rst = '1' then have_data <= '1'; else have_data <= have_data; end if;
           end if;
       end process;
       valid <= have_data and ready;                  -- WRONG: valid looks at ready
       data  <= std_logic_vector(to_unsigned(16#A0#, WIDTH));
   end architecture;
 
   -- FIXED: valid from local state only; advance only on the transfer.
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
   entity vr_source_good is
       generic ( WIDTH : positive := 32 );
       port ( clk, rst, ready : in std_logic;
              valid : out std_logic;
              data  : out std_logic_vector(WIDTH-1 downto 0) );
   end entity;
   architecture rtl of vr_source_good is
       signal have_data : std_logic := '1';
       signal valid_i   : std_logic;
   begin
       valid_i <= have_data;                          -- RIGHT: local state only (Rule 1)
       valid   <= valid_i;
       process (clk) begin
           if rising_edge(clk) then
               if rst = '1' then have_data <= '1';
               elsif (valid_i and ready) = '1' then have_data <= '0';  -- advance on transfer
               end if;
           end if;
       end process;
       data <= std_logic_vector(to_unsigned(16#A0#, WIDTH));
   end architecture;
vr_deadlock_tb.vhd — self-checking; the deadlock DUT never transfers
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity vr_deadlock_tb is
   end entity;
 
   architecture sim of vr_deadlock_tb is
       constant WIDTH : positive := 32;
       signal clk : std_logic := '0';
       signal rst, valid, ready : std_logic := '0';
       signal data : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
       signal space : std_logic := '1';              -- sink always has room
   begin
       -- Bind to vr_source_good to PASS; vr_source_bad deadlocks.
       dut : entity work.vr_source_good
           generic map (WIDTH => WIDTH)
           port map (clk => clk, rst => rst, ready => ready, valid => valid, data => data);
 
       ready <= space and valid;                      -- sink ready legally looks at valid (Rule 3)
 
       clkgen : process begin clk <= '0'; wait for 5 ns; clk <= '1'; wait for 5 ns; end process;
 
       stim : process
           variable transfers : integer := 0;
       begin
           rst <= '1'; wait until rising_edge(clk); wait for 1 ns; rst <= '0';
           for t in 0 to 19 loop
               wait until rising_edge(clk); wait for 1 ns;
               if (valid and ready) = '1' then transfers := transfers + 1; end if;
           end loop;
           assert transfers > 0
               report "DEADLOCK: no transfer occurred (valid gated by ready)" severity error;
           report "vr deadlock self-check complete" severity note;
           wait;
       end process;
   end architecture;

Across all three languages the lesson is one sentence: drive valid from local state and never from ready, hold the offer with stable data until the transfer, and let a word move on exactly the cycle valid && ready — then any source composes with any sink and the link can never deadlock on itself.

5. Verification Strategy

The valid/ready handshake is a contract, so verification lives at the contract: connect a source and a sink through randomized valid/ready gaps and prove the three properties that make the link correct — every produced word is received exactly once, in order, with none lost or duplicated; the data is stable across any stall; and no transfer occurs unless both valid and ready are high.

Every word the source emits is received by the sink exactly once and in order (none dropped, none duplicated); whenever valid is high and ready is low the data bus does not change; and a word transfers on exactly the cycles valid && ready are both high — never on valid alone, never on ready alone.

Everything below makes that invariant checkable and maps onto the testbenches in §4.

  • Scoreboard: in-order, exactly-once delivery. Model the expected stream as a reference queue (or, for a deterministic source, a counter) and, on every cycle a transfer fires (valid && ready), pop the head and assert the captured word equals it. At the end assert the number of transfers equals the number of words emitted — that single count catches both a dropped word (too few) and a duplicated word (too many). This is exactly the §4 scoreboard: SV assert (captured === expect_word) plus a final received === NWORDS, Verilog the if (captured !== expect_word) $display("FAIL…") form, VHDL assert captured = … severity error. Randomize the sink's ready (gappy backpressure) and, in a fuller test, the source's valid too, so the delivery invariant is proved across many stall patterns, not one.
  • Data-stable-under-stall check. Every cycle, if the previous cycle had valid && !ready (a held offer), assert the data bus is unchanged this cycle. This is the direct detector for a Rule 2 violation — a source that mutates the payload while it is still being offered. A handshake that delivers the right count of words but corrupts one under a stall fails here and only here.
  • No-transfer-without-both check. Assert that the sink captures a word (its got pulse, or a write into its buffer) on exactly the cycles valid && ready — never when only one is high. This catches a sink that samples data on valid alone (ignoring ready, so it captures words during its own stalls) or on ready alone (capturing garbage while the producer is idle).
  • Deadlock / liveness check. Wire the source to a sink whose ready legally depends on valid (Rule 3) and assert at least one transfer occurs within a bounded number of cycles. A source that violates Rule 1 (valid gated by ready) forms a combinational loop with that sink and makes zero progress — the §4b test catches it by counting transfers and asserting the count is nonzero. Liveness (something eventually moves) is as important as safety (nothing moves wrongly), and this is the check that proves it.
  • Backpressure corner cases. Exercise the specific patterns that break weak implementations: ready low for many consecutive cycles while valid is held (a long stall — the word must survive and transfer intact when ready finally rises); ready toggling every cycle (the transfer must happen only on the high cycles); valid and ready both high back-to-back with no gaps (full-throughput streaming, one word per cycle); and a single word with ready high before valid ever rises (a bubble that must not produce a spurious transfer).
  • Expected waveform. On a correct run, valid rises when the source has a word and stays high with data frozen across every cycle ready is low, dropping only after the cycle ready is finally high; each transfer is a single overlap of the two signals; and the captured stream at the sink is the emitted stream verbatim, in order. The visual signature of a Rule 1 violation is valid and ready both stuck low forever (deadlock); of a Rule 2 violation, data changing while valid is held; of a sink sampling on valid alone, a captured word during a cycle ready was low.

6. Common Mistakes

valid combinationally gated by ready. The signature failure. Writing valid = have_data && ready makes the producer's offer depend on the consumer's acceptance — which inverts the whole contract. Pair that source with a sink whose ready looks at valid (which Rule 3 explicitly permits), and the two form a combinational loop: valid waits for ready, ready waits for valid, and neither ever rises, so the link deadlocks and no word ever transfers. Even against a permanently-ready sink it is fragile and non-composable. The rule is absolute: valid is a function of local producer state only; it must never read ready. (Post-mortem in §7.)

Data or valid changed before the word is accepted. A Rule 2 violation, in two flavours. If the producer mutates the payload while valid is still high and the transfer has not happened, the consumer — which may be stalling for several cycles — latches whichever version happens to be on the bus when ready finally rises, so the word it receives is not the word that was originally offered. If instead the producer drops valid for a cycle mid-offer (because of a glitch in its have_data logic), the consumer may miss the word entirely, or see a torn offer. The rule: once valid is asserted, hold both valid and data bit-for-bit stable until the transfer cycle. Offer, hold, transfer — never edit the offer.

A sink whose ready glitches combinationally. ready may legitimately depend on valid (Rule 3), but it must be a clean, monotone-within-a-cycle function — not a combinational signal that momentarily glitches high and low as its inputs settle. A ready that glitches high for a sliver of a cycle while valid is high can, in a fully-combinational path, register a spurious transfer the producer also sees, dropping a word or double-counting. Drive ready from stable registered state (free space, occupancy) or a clean combinational function of valid and registered state — never from a racy multi-level combinational mess.

Sampling data on valid alone, ignoring ready. A consumer that captures data whenever valid is high — without also checking ready — captures words it did not actually accept. If it is stalling (ready low) but still latches on valid, it may capture the same held word repeatedly (duplicating it downstream) or capture a word it has no room for (overrunning its buffer). A transfer is valid && ready; the consumer must capture on exactly that condition, not on valid by itself. Symmetrically, acting on ready alone captures garbage while the producer is idle.

Forgetting the producer may only advance on a transfer. A well-behaved source presents word k and must not move to word k+1 until word k has actually transferred (valid && ready). A source that advances its index every cycle valid is high — regardless of ready — skips every word it offered during a stall: those words are lost because the consumer never accepted them but the producer already moved on. Advance local state (the index, the have_data flag, the read pointer) only on the transfer, never on valid alone.

Treating the combinational ready path as free. Because ready can ripple backward through a chain of blocks combinationally (Rule 3), a naive pipeline of handshake stages can build one long combinational backpressure path from the last consumer all the way to the first producer, and that path — not the datapath — becomes the critical timing path. This is not a correctness bug but a timing one, and the fix is not to change the contract but to register the backpressure with a skid buffer (9.3). Recognize a long combinational ready chain as a real cost, and break it deliberately.

7. DebugLab

The link that never moved — a valid-waits-for-ready deadlock, and a payload that changed mid-offer

The engineering lesson: the valid/ready handshake is a contract, and the two habits that keep it are asymmetry and stability — valid is decided locally and never looks at ready (so the link cannot deadlock on itself), and the offered word is frozen until it is accepted (so a stall never corrupts it). The tells are distinctive. A deadlock shows up only at integration, when two blocks that were each fine against a trivial stub are wired together and neither will move — the fingerprint of a valid that was gated by ready. A stall-only corruption shows up only under backpressure, where the word count is right but a captured word is off by the number of stall cycles — the fingerprint of a payload that drifted while the offer was held. Three habits make both impossible, and they are identical across SystemVerilog, Verilog, and VHDL: drive valid from local state only, hold valid and data stable until the transfer, and let a word move on exactly valid && ready, advancing local state only on that transfer. Get those right and the contract composes with no glue — which is exactly why the same three wires sit under AXI, APB, and AHB, and the exact substrate the backpressure/stall pattern (9.2) and the skid buffer (9.3) refine.

8. Interview Q&A

9. Exercises

Design and reasoning exercises — no syntax drills. Each rehearses the transfer-condition / three-rules / hold-until-accepted reasoning behind the handshake.

Exercise 1 — Trace the transfer cycle by cycle

For a source offering three words (0xA0, 0xA1, 0xA2), hand-trace valid, ready, data, and transfer for this ready pattern from the sink: 0, 1, 0, 0, 1, 1 (the sink stalls, accepts, stalls twice, then runs). Assume the source has all three words ready from cycle 0. Mark the exact cycle each word transfers, confirm data is held unchanged across every stall, and state in one line why word 0xA0 is not lost even though ready was low on cycle 0.

Exercise 2 — Classify every cycle and name the backpressure

Take the same six-cycle trace and label each cycle as idle, stall, transfer, or bubble. Then answer: which cycles are backpressure (the consumer holding the producer), and which are bubbles (the producer holding up the consumer)? For the one cycle after all three words have transferred, state what valid and ready are and why no spurious transfer occurs.

Exercise 3 — Find the deadlock on paper

An engineer writes a source with valid = have_data && ready and connects it to a sink with ready = space && valid. Show, by trying to find the first cycle either signal can rise, that the link deadlocks — state the chicken-and-egg dependency explicitly. Then answer: against what specific kind of sink would this buggy source appear to work in a unit test, and why does that hide the bug until integration? Give the one-line fix and explain why it removes the loop.

Exercise 4 — Verify without shipping the bug

Write the test plan (not the RTL) that would have caught both §7 failures before tape-out. Specify: the scoreboard model and the two things it checks (in-order delivery and the exactly-once count); the data-stable-under-stall assertion and the exact cycle condition that triggers it; the deadlock/liveness check and the kind of sink you must connect for it to fire; and the backpressure corner cases (long stall, toggling ready, back-to-back full throughput, ready-before-valid bubble). State which single check directly catches a valid gated by ready, and which single check catches a payload that drifted under a stall.

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

  • The Valid-Bit Discipline — Chapter 8.3; the direct prerequisite — the producer half of this handshake, a data bus paired with a forward valid qualifier, pipelined so the pair stays aligned. This page adds the backward ready.
  • Pipeline Stalls & Flush — Chapter 8.4; the clock-enable stall that a low ready becomes here — backpressure is a stall imposed by the consumer rather than chosen locally.
  • Backpressure & Stall — Chapter 9.2; the general treatment of how a low ready propagates back to stall the producer, and how to pipeline it without losing a word.
  • Skid Buffer — Chapter 9.3; the registered stage that breaks the long combinational ready chain and catches the one in-flight word when backpressure deasserts a cycle late.
  • Credit-Based Flow Control — Chapter 9.4; the alternative to backward ready for links with latency — the producer tracks how many words the consumer can still accept instead of watching a per-cycle ready.
  • Handshake Bugs & Anti-Patterns — Chapter 9.5; the catalog of ways the contract is broken — the deadlock and the mid-offer mutation from this page's DebugLab, plus more.
  • Ready/Valid Pipelining — Chapter 9.6; composing many handshake stages into a pipeline that keeps full throughput while registering the backpressure path.
  • Case Study — Streaming FIFO — Chapter 9.7; a full FIFO exposed as a valid/ready sink on its write side and a source on its read side — the composability of this page made concrete.

Backward / in-track dependencies:

  • The Register Pattern (D-FF) — Chapter 2.1; the flop the source's payload and have_data state are built from, and the reset-first clocked-update discipline every block here follows.
  • Register with Enable / Load — Chapter 2.2; the clock-enable that becomes the hold-the-offer mechanism — a source that does not advance until the transfer is a register whose enable is valid && ready.
  • Synchronous FIFO Architecture — Chapter 7.1; the FIFO whose write side is a well-behaved sink and read side a well-behaved source — the canonical pair of handshake endpoints.
  • FIFO Full/Empty Generation — Chapter 7.2; the full and empty flags that are a FIFO's ready (not full) and valid (not empty) — the handshake signals derived from occupancy.
  • Moore vs Mealy Machines — Chapter 4.x; the Mealy-vs-Moore distinction behind Rule 3 — a ready that looks at valid is a Mealy output, and why a registered (Moore) ready breaks the combinational path.
  • The RTL Design Mindset — Chapter 0.2; the Interface and Verification lenses this page applies — the handshake is the interface contract, and the scoreboard is how you prove no word is lost.
  • What Is an RTL Design Pattern? — Chapter 0.1; why the valid/ready handshake earns the name — a recurring problem, a reusable form, a synthesis reality, and a signature failure.

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

  • Blocking and Non-Blocking Assignments — why the source's payload and state registers update with non-blocking (<=) so the offer advances atomically on the transfer edge.
  • If-Else Statements — the reset / transfer / hold priority (if (rst) … else if (valid && ready) …) that selects clear, advance, or hold.
  • Initial & Always Blocks — the clocked always block that advances the source only on a transfer, and the sink that captures on valid && ready.
  • Physical Data Types — the wire/reg typing behind driving valid/ready as pure combinational functions versus registering the payload and captured word.

11. Summary

  • Two blocks at independent rates need a contract, not a bus. A producer emits when its work is done; a consumer accepts when it has room; those events do not line up. A bare bus loses a word offered while the consumer is full and samples garbage read while the producer is empty. The valid/ready handshake is the contract that fixes it: valid means the producer has data this cycle, ready means the consumer can accept this cycle, and a transfer — one word, exactly once — happens on every cycle valid && ready are both high, and only then. Both sides compute that one condition from the same wires, so they agree on every transfer.
  • Every cycle is idle, stall, transfer, or bubble. valid low / ready low is idle; valid high / ready low is a stall (backpressure — the consumer holding the producer, word held unchanged); both high is the transfer; valid low / ready high is a bubble (the consumer waiting with room). The producer advances to its next word only after a transfer.
  • Three rules make it composable. (1) valid must not depend combinationally on ready — it comes from local producer state only, or two blocks each waiting on the other deadlock. (2) Once valid is high, hold it and data bit-for-bit stable until the transfer — no retraction, no editing the offer mid-flight. (3) ready may depend on valid — a sink can look before it leaps. The asymmetry is deliberate: the one-way dependency can never form a loop, so any well-behaved source connects to any well-behaved sink with no glue. That is the composability primitive under AXI, APB, and AHB.
  • The signature bugs are the self-deadlock and the mid-offer mutation. Gating valid with ready (valid = have_data && ready) forms a combinational loop with a sink whose ready looks at valid, and the link never moves — invisible in isolation, fatal at integration. Editing data while valid is held corrupts the word a stalled sink finally latches, off by the number of stall cycles — right count, wrong word, only under backpressure. Both are prevented by driving valid locally, holding the offer stable, and advancing only on the transfer.
  • The transfer is cheap; the ready chain is where you pay. The transfer condition is a single combinational AND, but a fully-combinational ready chain through many stages builds a long backpressure path that can become the critical timing path — fixed not by changing the contract but by registering it with a skid buffer (9.3). Built and self-check-verified here in SystemVerilog, Verilog, and VHDL — the substrate the backpressure/stall (9.2) and skid-buffer (9.3) patterns refine.