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.
// 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:
- Idle —
validlow,readylow (or either low with the other also low): nothing is offered and nothing is wanted. The link rests. - Stall / wait —
validhigh,readylow: 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. - Transfer —
validhigh,readyhigh: the word moves, exactly once, this cycle. - Bubble —
validlow,readyhigh: 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 flowThe 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 —
validmust not depend combinationally onready. The producer assertsvalidfrom its own state (it has a word), never from the consumer'sready. Ifvalidwerehave_data && readyand the consumer'sreadyin turn looked atvalid, 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 forvalidis what breaks that cycle. (This is the §7 deadlock.) - Rule 2 — once
validis high, it stays high withdatastable until the transfer. The producer may not retract an offer or change the payload mid-offer. Assertvalid, and bothvalidanddataare frozen until the cyclereadyis 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 —
readymay depend combinationally onvalid. The asymmetry is deliberate. A consumer is allowed to look atvalidbefore assertingready— a sink can look before it leaps — for instance assertingreadyonly when it both has room and sees a valid offer. Becausevalidnever looks back atready, 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 flow4. 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.
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
endmoduleThe 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.
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
endmoduleThe 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.
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
endmoduleThe 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.
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 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 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
endmoduleIn 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.
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; 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; 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.
// 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;
endmoduleThe 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.
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
endmoduleThe Verilog buggy/fixed pair tells the same story with reg/wire typing; the whole bug is the single & that lets valid read ready.
// 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 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
endmoduleIn 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.
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; 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
validis high andreadyis low thedatabus does not change; and a word transfers on exactly the cyclesvalid && readyare both high — never onvalidalone, never onreadyalone.
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: SVassert (captured === expect_word)plus a finalreceived === NWORDS, Verilog theif (captured !== expect_word) $display("FAIL…")form, VHDLassert captured = … severity error. Randomize the sink'sready(gappy backpressure) and, in a fuller test, the source'svalidtoo, 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 thedatabus 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
gotpulse, or a write into its buffer) on exactly the cyclesvalid && ready— never when only one is high. This catches a sink that samplesdataonvalidalone (ignoringready, so it captures words during its own stalls) or onreadyalone (capturing garbage while the producer is idle). - Deadlock / liveness check. Wire the source to a sink whose
readylegally depends onvalid(Rule 3) and assert at least one transfer occurs within a bounded number of cycles. A source that violates Rule 1 (validgated byready) 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:
readylow for many consecutive cycles whilevalidis held (a long stall — the word must survive and transfer intact whenreadyfinally rises);readytoggling every cycle (the transfer must happen only on the high cycles);validandreadyboth high back-to-back with no gaps (full-throughput streaming, one word per cycle); and a single word withreadyhigh beforevalidever rises (a bubble that must not produce a spurious transfer). - Expected waveform. On a correct run,
validrises when the source has a word and stays high withdatafrozen across every cyclereadyis low, dropping only after the cyclereadyis 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 isvalidandreadyboth stuck low forever (deadlock); of a Rule 2 violation,datachanging whilevalidis held; of a sink sampling onvalidalone, a captured word during a cyclereadywas 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.
10. Related Tutorials
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
validqualifier, pipelined so the pair stays aligned. This page adds the backwardready. - Pipeline Stalls & Flush — Chapter 8.4; the clock-enable stall that a low
readybecomes 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
readypropagates 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
readychain and catches the one in-flight word when backpressure deasserts a cycle late. - Credit-Based Flow Control — Chapter 9.4; the alternative to backward
readyfor links with latency — the producer tracks how many words the consumer can still accept instead of watching a per-cycleready. - 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_datastate 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
fullandemptyflags that are a FIFO'sready(not full) andvalid(not empty) — the handshake signals derived from occupancy. - Moore vs Mealy Machines — Chapter 4.x; the Mealy-vs-Moore distinction behind Rule 3 — a
readythat looks atvalidis a Mealy output, and why a registered (Moore)readybreaks 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
alwaysblock that advances the source only on a transfer, and the sink that captures onvalid && ready. - Physical Data Types — the
wire/regtyping behind drivingvalid/readyas 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:
validmeans the producer has data this cycle,readymeans the consumer can accept this cycle, and a transfer — one word, exactly once — happens on every cyclevalid && readyare 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.
validlow /readylow is idle;validhigh /readylow is a stall (backpressure — the consumer holding the producer, word held unchanged); both high is the transfer;validlow /readyhigh 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)
validmust not depend combinationally onready— it comes from local producer state only, or two blocks each waiting on the other deadlock. (2) Oncevalidis high, hold it anddatabit-for-bit stable until the transfer — no retraction, no editing the offer mid-flight. (3)readymay depend onvalid— 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
validwithready(valid = have_data && ready) forms a combinational loop with a sink whosereadylooks atvalid, and the link never moves — invisible in isolation, fatal at integration. Editingdatawhilevalidis 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 drivingvalidlocally, holding the offer stable, and advancing only on the transfer. - The transfer is cheap; the
readychain is where you pay. The transfer condition is a single combinational AND, but a fully-combinationalreadychain 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.