Skip to content

RTL Design Patterns · Chapter 9 · Handshake & Flow Control

Common Handshake Bugs

The valid and ready handshake is simple, just three wires and three rules, yet the same small set of violations recurs: silicon that hangs on one traffic pattern, a stream that drops one word in a thousand, a block that passed every unit test and then deadlocked once it met its neighbour. This lesson is a field guide to those failures. Every handshake bug is either a liveness bug, where a transfer that should happen never does and the link hangs, or a safety bug, where a word is lost, duplicated, or sampled wrong. The canonical cases include valid gated by ready, valid dropped before the accept, data mutated while valid is held, sampling on valid alone, a ready glitch, and valid asserted out of reset before the data is real. Each gets its symptom, root cause, and exact fix, with the two most instructive shown buggy versus fixed against a scoreboard, in SystemVerilog, Verilog, and VHDL.

Intermediate14 min readRTL Design Patternsvalid/readyHandshakeDeadlockBackpressureDebugging

Chapter 9 · Section 9.5 · Handshake & Flow Control

1. The Engineering Problem

You have shipped a block that uses the valid/ready handshake from 9.1 correctly — or so every unit test said. It streamed words in order, exactly once, through randomized backpressure, and passed. Then it went into the system, and the bug reports started. On one program the whole pipeline hangs, dead, with valid and ready both stuck low. On another it runs but the downstream checker occasionally sees a word that is off by a few, or a word that arrives twice, or a word that is simply missing from an otherwise-perfect stream. None of it reproduced in the block's own testbench. All of it is a handshake bug.

This is the recurring shape of handshake failure, and it is why the contract needs a catalog even though the contract itself is three wires and three rules. The rules are simple, but each one has a characteristic way of being broken, and the break is usually invisible in isolation — it needs a real neighbour, a real stall, or a real reset to surface. A source that gates valid with ready looks fine against an always-ready stub and deadlocks only against a real sink. A source that mutates its payload under a held offer looks fine against an always-ready sink and corrupts a word only under backpressure. A sink that samples on valid alone looks fine when it never stalls and duplicates a word the first time it does. The bug hides in exactly the case the unit test did not exercise.

So the engineering problem here is not how to build a handshake — 9.1 and 9.2 did that. It is how to recognize a broken one: given a link that hangs, or a stream with the wrong count or a wrong value, how do you classify the failure and name the rule it violated fast enough to fix it — ideally on the whiteboard, from ten lines of RTL, before it ever reaches silicon. That reflex is what this page builds, and it starts with a taxonomy that turns a confusing symptom into a two-way decision.

the-symptoms.v — the same contract, six ways to break it
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // A handshake link that passed its own unit tests, now misbehaving in the system.
   // The symptoms cluster into exactly two families:
 
   //   LIVENESS failure -- a transfer that should happen NEVER does:
   //     valid and ready both stuck low forever -> the link is DEAD (a hang / deadlock).
 
   //   SAFETY failures -- a transfer happens, but WRONGLY:
   //     a word is LOST        (count too low),
   //     a word is DUPLICATED  (count too high),
   //     a word is MIS-SAMPLED (count right, value wrong).
 
   // Every one traces back to a violated rule from 9.1. The catalog names which.

2. Mental Model

3. Pattern Anatomy

The reference handshake is the fixed point against which every bug is measured, so the anatomy restates it — tersely, because 9.1 built it — and then lays out the failure taxonomy that hangs off it.

The correct handshake, restated. Three wires: a data bus forward, a one-bit valid forward, a one-bit ready backward. One derived signal: a transfer is exactly valid && ready, computed identically by both sides. Three rules govern how each side may drive its wire:

  • Rule 1 — valid comes from local state only, never from ready. The producer asserts valid because it has a word, full stop. If valid looked at ready and ready looked at valid, the two would form a combinational loop and could deadlock.
  • Rule 2 — once valid is high, hold it with data bit-for-bit stable until the transfer. No retracting the offer, no editing the payload mid-offer. The word is frozen from the moment it is offered until the cycle it is taken.
  • Rule 3 — ready may depend combinationally on valid. A sink can look before it leaps. The asymmetry is one-way and deliberate: because valid never looks back, no loop forms.

Get those right and the link delivers every word once, in order, with the correct value — the safety property — and always eventually moves — the liveness property. Every bug below is one of those two properties broken by one of those three rules violated.

The failure taxonomy. Six canonical bugs, split first by liveness versus safety and, within safety, by count-wrong versus value-wrong:

  • Liveness — valid gated by ready (breaks Rule 1). The source computes valid = have_data && ready; wired to a sink whose ready looks at valid, the two deadlock. Nothing ever moves. (DebugLab row 1.)
  • Safety, count-wrong (lost) — valid dropped before the accept (breaks Rule 2). The source de-asserts valid for a cycle while an offer is still standing, or advances its index on valid rather than on the transfer, so a word offered during a stall is skipped and lost. (DebugLab row 2; full tri-HDL in §4.)
  • Safety, value-wrong (mis-sample) — data mutated while valid held (breaks Rule 2). The payload changes before the accept, so a stalled sink latches a later value than the one offered. Count right, value wrong. (DebugLab row 3; full tri-HDL in §4.)
  • Safety, count-wrong (duplicate) — sampling on valid alone (ignores ready). The sink captures whenever valid is high, without checking ready, so it latches the same held word every stall cycle — a duplicate, or a word it had no room for. (DebugLab row 4.)
  • Safety — a combinational ready glitch (a racy Rule 3). ready is a multi-level combinational function that momentarily pulses high while its inputs settle, registering a phantom accept the source also sees — a word dropped or double-counted.
  • Safety — valid asserted out of reset before data is ready. valid resets high (or floats) while the payload is still X or stale, so the first word out is garbage the sink dutifully accepts.

The handshake bug taxonomy — classify the symptom, then name the broken rule

data flow
The handshake bug taxonomy — classify the symptom, then name the broken rulestuckmoves wrongcount offvalue offrestore rulerestore rulerestore rulesymptomthe link misbehavesliveness :nothing moveshang / deadlock -> Rule 1 (valid gated by ready)safety : wrongthing moveslost / duplicated / mis-sampled wordcount wronglost (drop valid) or dup (sample on valid alone)value wrongmis-sample -> data mutated under stall (Rule 2)fix = the rule,positivelocal valid, hold stable, capture on both
Start at the symptom and cut twice. First cut: is the link stuck (nothing moves) or is it moving but wrong? Stuck is a liveness bug and there is essentially one cause -- valid was gated by ready, so valid and ready wait on each other and the link deadlocks (Rule 1). Moving-but-wrong is a safety bug. Second cut, for safety: is the word COUNT wrong or the word VALUE wrong? Count wrong means lost (the source dropped valid before the accept) or duplicated (the sink sampled on valid alone); value wrong means mis-sampled (the payload mutated while valid was held, so a stalled sink latched a later value -- Rule 2). Every leaf maps to one violated rule, and the fix is that rule restated as a habit. This taxonomy is identical across SystemVerilog, Verilog, and VHDL, and it is the exact contract under AXI, APB, and AHB, so the same six bugs appear on every handshake you will ever debug.

Why the symptom, not the RTL, is the entry point. Handshake bugs are notorious for being invisible in isolation and obvious in the taxonomy. The deadlock needs two blocks that each look at the other; a single block against a stub never closes the loop. The lost word and the mis-sample need backpressure; against an always-ready sink the offer is accepted the cycle it is made, so valid never has to be held and the payload never has a chance to drift. The duplicate needs the sink to stall while valid is held. In every case the unit test that passed simply never entered the state where the rule mattered. That is why you debug from the symptom down the taxonomy, not from the RTL up — the RTL looks correct until you know which case to put it in.

4. Real RTL Implementation

This is a debugging clinic, so the RTL here is buggy source versus corrected source, each wired to a reference sink/scoreboard whose self-checking testbench fails on the buggy DUT and passes on the fixed one. We take the two most instructive safety bugs — (a) valid dropped before the accept, a lost word, and (b) data mutated while valid is held, a mis-sample — and show each in SystemVerilog, Verilog, and VHDL. A shared reference sink and scoreboard (built once, in SystemVerilog, and mirrored in the other two) does the catching: it drives gappy backpressure and checks in-order, exactly-once delivery with the offered word tied to the captured word. The remaining catalog bugs (the deadlock, the duplicate, the glitch, the reset-garbage) are dissected in §7 and prevented in §6.

One discipline runs through every fixed block, inherited from 9.1: registers update on the clock with non-blocking assignments, reset is explicit (synchronous, active-high rst), the source drives valid from its own registered state only, holds valid and data stable while valid && !ready, and advances only on the transfer valid && ready.

4a. The reference sink and scoreboard — the bug-catcher

Both buggy sources are wired to the same reference sink and scoreboard. The sink applies gappy backpressure (that is what surfaces the bugs) and captures on the transfer; the scoreboard ties each captured word to the word the source offered — the check that a count-only scoreboard would miss. This is the SystemVerilog reference; the Verilog and VHDL versions mirror it inline with each DUT.

ref_sink.sv — reference sink: gappy ready, capture on valid && ready, report the offer it saw
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module ref_sink #(
       parameter int WIDTH = 32
   )(
       input  logic             clk,
       input  logic             rst,
       input  logic             valid,                // forward from the source under test
       input  logic [WIDTH-1:0] data,
       input  logic             ready_gap,            // 0 on the cycles we want to STALL
       output logic             ready,                // ready from local state (never gates valid)
       output logic             got,                  // pulses on each accepted word
       output logic [WIDTH-1:0] captured              // the word we accepted this cycle
   );
       // ready is driven from LOCAL state only -- a clean, non-gating backpressure source.
       // ready_gap lets the testbench inject stalls to exercise the held-offer path.
       assign ready = ready_gap;
       wire   xfer  = valid & ready;                   // the ONE transfer condition
 
       always_ff @(posedge clk) begin
           if (rst) begin
               got <= 1'b0; captured <= '0;
           end else begin
               got <= xfer;                            // one pulse per ACCEPTED word
               if (xfer) captured <= data;             // capture ONLY on the transfer
           end
       end
   endmodule

4b. Bug (a) — valid dropped before the accept (a lost word), buggy vs fixed

The buggy source advances its index on valid alone — every cycle it has a word, whether or not that word was accepted — so any word offered during a stall is skipped and never delivered. The symptom is a lost word: the scoreboard receives fewer words than were emitted. The fix is Rule 2 plus the transfer rule: advance only on valid && ready.

src_drop.sv — BUGGY (advance on valid, drops words under stall) vs FIXED (advance on transfer)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: the index advances every cycle VALID is high, regardless of READY. Any word
   // offered on a stall cycle (valid high, ready low) is skipped -- the source moved on
   // but the sink never accepted it. Against an always-ready sink this is invisible.
   module src_drop_bad #(parameter int WIDTH = 32, parameter int NWORDS = 8) (
       input  logic clk, rst, ready,
       output logic valid,
       output logic [WIDTH-1:0] data
   );
       logic [$clog2(NWORDS+1)-1:0] idx;
       logic have_data;
       always_ff @(posedge clk) begin
           if (rst) begin idx <= '0; have_data <= 1'b1; end
           else if (valid) begin                       // WRONG: advances on VALID, not on transfer
               if (idx == NWORDS-1) have_data <= 1'b0;
               else                 idx       <= idx + 1'b1;
           end
       end
       assign valid = have_data;                       // Rule 1 OK -- valid is local
       assign data  = 32'hA0 + idx;                    // word k = 0xA0 + k
   endmodule
 
   // FIXED: advance ONLY on the transfer (valid && ready). A stalled word stays offered,
   // with the same index and same data, until the cycle it is actually accepted (Rule 2).
   module src_drop_good #(parameter int WIDTH = 32, parameter int NWORDS = 8) (
       input  logic clk, rst, ready,
       output logic valid,
       output logic [WIDTH-1:0] data
   );
       logic [$clog2(NWORDS+1)-1:0] idx;
       logic have_data;
       wire  xfer = valid & ready;                     // the transfer condition
       always_ff @(posedge clk) begin
           if (rst) begin idx <= '0; have_data <= 1'b1; end
           else if (xfer) begin                        // RIGHT: advance ONLY on the transfer
               if (idx == NWORDS-1) have_data <= 1'b0;
               else                 idx       <= idx + 1'b1;
           end
       end
       assign valid = have_data;
       assign data  = 32'hA0 + idx;                    // held stable while idx is held (Rule 2)
   endmodule

The testbench wires the source to the reference sink, drives gappy backpressure, and asserts the scoreboard invariant: every emitted word is received exactly once, in order. The buggy source loses words under the stalls (received count too low); the fixed source delivers all NWORDS.

src_drop_tb.sv — scoreboard: the buggy source loses words under stall; the fixed one delivers all
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module src_drop_tb;
       localparam int WIDTH = 32, NWORDS = 8;
       logic clk = 1'b0, rst, valid, ready, ready_gap, got;
       logic [WIDTH-1:0] data, captured;
       int   errors = 0, received = 0;
       logic [WIDTH-1:0] expect_word = 32'hA0;
 
       // Bind to src_drop_good to PASS; swap to src_drop_bad to WATCH words vanish.
       src_drop_good #(.WIDTH(WIDTH), .NWORDS(NWORDS)) dut (
           .clk(clk), .rst(rst), .ready(ready), .valid(valid), .data(data));
       ref_sink #(.WIDTH(WIDTH)) snk (
           .clk(clk), .rst(rst), .valid(valid), .data(data), .ready_gap(ready_gap),
           .ready(ready), .got(got), .captured(captured));
 
       always #5 clk = ~clk;
 
       initial begin
           rst = 1'b1; ready_gap = 1'b0;
           @(posedge clk); #1; rst = 1'b0;
           for (int t = 0; t < 6*NWORDS; t++) begin
               @(negedge clk);
               ready_gap = ($urandom_range(0, 2) != 0);   // ~2/3 ready -> frequent stalls
               @(posedge clk); #1;
               if (got) begin                              // an accepted word must be next-in-order
                   assert (captured === expect_word)
                       else begin $error("t=%0d out-of-order/dup: got %h exp %h", t, captured, expect_word); errors++; end
                   expect_word = expect_word + 1;
                   received++;
               end
           end
           // The whole bug: the buggy source delivers FEWER than NWORDS -- words were dropped.
           assert (received === NWORDS)
               else begin $error("received %0d words, expected %0d -- WORD(S) LOST", received, NWORDS); errors++; end
           if (errors == 0) $display("PASS: all %0d words delivered in order, none lost", received);
           else             $display("FAIL: %0d errors (dropped word under stall)", errors);
           $finish;
       end
   endmodule

The Verilog form is the same story with reg/wire typing and an inline reference sink; the bug is the single if (valid) where it should be if (valid & ready).

src_drop.v — BUGGY vs FIXED in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: advance on VALID alone -> words offered during a stall are skipped.
   module src_drop_bad #(parameter WIDTH = 32, parameter NWORDS = 8) (
       input wire clk, rst, ready,
       output wire valid, output wire [WIDTH-1:0] data
   );
       reg [31:0] idx; reg have_data;
       always @(posedge clk) begin
           if (rst) begin idx <= 0; have_data <= 1'b1; end
           else if (valid) begin                       // WRONG: not gated by the transfer
               if (idx == NWORDS-1) have_data <= 1'b0;
               else                 idx       <= idx + 1'b1;
           end
       end
       assign valid = have_data;
       assign data  = 32'hA0 + idx;
   endmodule
 
   // FIXED: advance only on the transfer (valid & ready).
   module src_drop_good #(parameter WIDTH = 32, parameter NWORDS = 8) (
       input wire clk, rst, ready,
       output wire valid, output wire [WIDTH-1:0] data
   );
       reg [31:0] idx; reg have_data;
       wire xfer = valid & ready;
       always @(posedge clk) begin
           if (rst) begin idx <= 0; have_data <= 1'b1; end
           else if (xfer) begin                        // RIGHT: gated by the transfer
               if (idx == NWORDS-1) have_data <= 1'b0;
               else                 idx       <= idx + 1'b1;
           end
       end
       assign valid = have_data;
       assign data  = 32'hA0 + idx;
   endmodule
src_drop_tb.v — self-checking scoreboard with inline reference sink
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module src_drop_tb;
       parameter WIDTH = 32, NWORDS = 8;
       reg  clk, rst, ready_gap;
       wire valid;
       wire [WIDTH-1:0] data;
       reg  [WIDTH-1:0] captured, expect_word;
       reg  got;
       integer t, errors, received;
       wire ready = ready_gap;                          // reference sink: clean local ready
       wire xfer  = valid & ready;
 
       src_drop_good #(.WIDTH(WIDTH), .NWORDS(NWORDS)) dut (
           .clk(clk), .rst(rst), .ready(ready), .valid(valid), .data(data));
 
       // Inline reference sink: capture on the transfer only.
       always @(posedge clk) begin
           if (rst) begin got <= 1'b0; captured <= 0; end
           else begin got <= xfer; if (xfer) captured <= data; end
       end
 
       always #5 clk = ~clk;
 
       initial begin
           errors = 0; received = 0; expect_word = 32'hA0;
           clk = 1'b0; rst = 1'b1; ready_gap = 1'b0;
           @(posedge clk); #1; rst = 1'b0;
           for (t = 0; t < 6*NWORDS; t = t + 1) begin
               @(negedge clk);
               ready_gap = ($random % 3) != 0;          // ~2/3 ready -> frequent stalls
               @(posedge clk); #1;
               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 -- WORD(S) LOST", received, NWORDS);
               errors = errors + 1;
           end
           if (errors == 0) $display("PASS: all %0d words delivered, none lost", received);
           else             $display("FAIL: %0d errors (dropped word under stall)", errors);
           $finish;
       end
   endmodule

In VHDL the same bug is elsif valid = '1' where the fix is elsif (valid and ready) = '1'; the reference sink is a clocked process capturing on the transfer.

src_drop.vhd — BUGGY vs FIXED in VHDL (numeric_std)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   -- BUGGY: advance on VALID alone -> a word offered during a stall is skipped.
   entity src_drop_bad is
       generic ( WIDTH : positive := 32; NWORDS : positive := 8 );
       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 src_drop_bad 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   <= valid_i;
       data    <= std_logic_vector(to_unsigned(16#A0# + idx, WIDTH));
       process (clk) begin
           if rising_edge(clk) then
               if rst = '1' then idx <= 0; have_data <= '1';
               elsif valid_i = '1' then                 -- WRONG: not gated by the transfer
                   if idx = NWORDS-1 then have_data <= '0';
                   else                   idx       <= idx + 1;
                   end if;
               end if;
           end if;
       end process;
   end architecture;
 
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   -- FIXED: advance ONLY on the transfer (valid and ready).
   entity src_drop_good is
       generic ( WIDTH : positive := 32; NWORDS : positive := 8 );
       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 src_drop_good 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   <= valid_i;
       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                    -- RIGHT: gated by the transfer
                   if idx = NWORDS-1 then have_data <= '0';
                   else                   idx       <= idx + 1;
                   end if;
               end if;
           end if;
       end process;
   end architecture;
src_drop_tb.vhd — self-checking scoreboard with inline reference sink
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity src_drop_tb is
   end entity;
 
   architecture sim of src_drop_tb is
       constant WIDTH : positive := 32; constant NWORDS : positive := 8;
       signal clk : std_logic := '0';
       signal rst, valid, ready, ready_gap, got : std_logic := '0';
       signal data, captured : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
   begin
       -- Bind to src_drop_good to PASS; src_drop_bad loses words.
       dut : entity work.src_drop_good
           generic map (WIDTH => WIDTH, NWORDS => NWORDS)
           port map (clk => clk, rst => rst, ready => ready, valid => valid, data => data);
 
       ready <= ready_gap;                              -- reference sink: clean local ready
 
       -- Inline reference sink: capture on the transfer only.
       sink : process (clk)
           variable xfer : std_logic;
       begin
           if rising_edge(clk) then
               xfer := valid and ready;
               if rst = '1' then got <= '0'; captured <= (others => '0');
               else got <= xfer; if xfer = '1' then captured <= data; end if;
               end if;
           end if;
       end process;
 
       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 seed1, seed2 : positive := 7;
           variable r : real;
       begin
           rst <= '1'; ready_gap <= '0';
           wait until rising_edge(clk); wait for 1 ns; rst <= '0';
           for t in 0 to 6*NWORDS - 1 loop
               wait until falling_edge(clk);
               uniform(seed1, seed2, r);                -- ~2/3 ready -> frequent stalls
               if r < 0.66 then ready_gap <= '1'; else ready_gap <= '0'; end if;
               wait until rising_edge(clk); wait for 1 ns;
               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 "word(s) lost -- received fewer than emitted" severity error;
           report "src_drop self-check complete" severity note;
           wait;
       end process;
   end architecture;

4c. Bug (b) — data mutated while valid is held (a mis-sample), buggy vs fixed

The second bug keeps the count right and gets the value wrong. The buggy source drives data from a free-running counter that ticks every cycle, so while the sink stalls with valid held, the payload under the offer drifts. When ready finally rises, the sink latches a later value — the counter shifted by the number of stall cycles — not the word that was offered. The fix is Rule 2: register the payload and change it only on the transfer.

src_mutate.sv — BUGGY (data follows a free-running counter) vs FIXED (payload frozen until transfer)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: valid is local (Rule 1 OK), but DATA is wired to a counter that advances every
   // cycle, even while the offer is held. A stalled sink latches a DRIFTED value (Rule 2 broken).
   module src_mutate_bad #(parameter int WIDTH = 32, parameter int NWORDS = 8) (
       input  logic clk, rst, ready,
       output logic valid,
       output logic [WIDTH-1:0] data
   );
       logic [WIDTH-1:0] counter;
       logic [$clog2(NWORDS+1)-1:0] delivered;
       wire  xfer = valid & ready;
       always_ff @(posedge clk) begin
           if (rst) begin counter <= 32'hA0; delivered <= '0; end
           else begin
               counter <= counter + 1'b1;                 // WRONG: DATA drifts every cycle...
               if (xfer) delivered <= delivered + 1'b1;    // ...even while the offer is held
           end
       end
       assign valid = (delivered < NWORDS);               // valid from local state (Rule 1 OK)
       assign data  = counter;                            // but the payload is NOT frozen
   endmodule
 
   // FIXED: register the payload; change DATA only on the transfer, so the held offer is stable.
   module src_mutate_good #(parameter int WIDTH = 32, parameter int NWORDS = 8) (
       input  logic clk, rst, ready,
       output logic valid,
       output logic [WIDTH-1:0] data
   );
       logic [WIDTH-1:0] data_reg;
       logic [$clog2(NWORDS+1)-1:0] delivered;
       wire  xfer = valid & ready;
       always_ff @(posedge clk) begin
           if (rst) begin data_reg <= 32'hA0; delivered <= '0; end
           else if (xfer) begin                            // RIGHT: change DATA only on the transfer
               data_reg  <= data_reg + 1'b1;               //   load the next word
               delivered <= delivered + 1'b1;
           end
           // else HOLD: data_reg untouched, so the offer is frozen (Rule 2).
       end
       assign valid = (delivered < NWORDS);
       assign data  = data_reg;                            // stable across any stall
   endmodule

The scoreboard here adds the data-stable-under-stall check: whenever the previous cycle held a valid offer that was not accepted (valid && !ready), the data must be unchanged this cycle. That single assertion is what catches a drifting payload — a count-only scoreboard would pass.

src_mutate_tb.sv — data-stable-under-stall + in-order scoreboard; buggy drifts, fixed holds
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module src_mutate_tb;
       localparam int WIDTH = 32, NWORDS = 8;
       logic clk = 1'b0, rst, valid, ready, ready_gap, got;
       logic [WIDTH-1:0] data, captured;
       int   errors = 0, received = 0;
       logic [WIDTH-1:0] expect_word = 32'hA0;
       logic [WIDTH-1:0] prev_data;  logic prev_valid, prev_ready;
 
       // Bind to src_mutate_good to PASS; swap to src_mutate_bad to watch a stalled word drift.
       src_mutate_good #(.WIDTH(WIDTH), .NWORDS(NWORDS)) dut (
           .clk(clk), .rst(rst), .ready(ready), .valid(valid), .data(data));
       ref_sink #(.WIDTH(WIDTH)) snk (
           .clk(clk), .rst(rst), .valid(valid), .data(data), .ready_gap(ready_gap),
           .ready(ready), .got(got), .captured(captured));
 
       always #5 clk = ~clk;
 
       initial begin
           rst = 1'b1; ready_gap = 1'b0; prev_valid = 1'b0;
           @(posedge clk); #1; rst = 1'b0;
           for (int t = 0; t < 6*NWORDS; t++) begin
               @(negedge clk);
               ready_gap = ($urandom_range(0, 2) != 0);
               prev_data = data; prev_valid = valid; prev_ready = ready;
               @(posedge clk); #1;
               // Rule 2: a held, unaccepted offer must keep the SAME data.
               if (prev_valid && !prev_ready)
                   assert (data === prev_data)
                       else begin $error("t=%0d data drifted under stall %h -> %h", t, prev_data, data); errors++; end
               if (got) begin
                   assert (captured === expect_word)
                       else begin $error("t=%0d mis-sampled: got %h exp %h", t, captured, expect_word); errors++; end
                   expect_word = expect_word + 1; received++;
               end
           end
           if (errors == 0) $display("PASS: %0d words, correct value, data stable under stall", received);
           else             $display("FAIL: %0d errors (payload drifted under stall)", errors);
           $finish;
       end
   endmodule

The Verilog pair mirrors it: data wired to a ticking counter is the bug, a registered payload advanced only on the transfer is the fix, and the testbench adds the data-stable check.

src_mutate.v — BUGGY vs FIXED in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: data follows a free-running counter -> a stalled sink latches a drifted value.
   module src_mutate_bad #(parameter WIDTH = 32, parameter NWORDS = 8) (
       input wire clk, rst, ready,
       output wire valid, output wire [WIDTH-1:0] data
   );
       reg [WIDTH-1:0] counter; reg [31:0] delivered;
       wire xfer = valid & ready;
       always @(posedge clk) begin
           if (rst) begin counter <= 32'hA0; delivered <= 0; end
           else begin
               counter <= counter + 1'b1;                 // WRONG: payload drifts every cycle
               if (xfer) delivered <= delivered + 1'b1;
           end
       end
       assign valid = (delivered < NWORDS);
       assign data  = counter;
   endmodule
 
   // FIXED: registered payload, changed only on the transfer.
   module src_mutate_good #(parameter WIDTH = 32, parameter NWORDS = 8) (
       input wire clk, rst, ready,
       output wire valid, output wire [WIDTH-1:0] data
   );
       reg [WIDTH-1:0] data_reg; reg [31:0] delivered;
       wire xfer = valid & ready;
       always @(posedge clk) begin
           if (rst) begin data_reg <= 32'hA0; delivered <= 0; end
           else if (xfer) begin                            // change DATA only on the transfer
               data_reg  <= data_reg + 1'b1;
               delivered <= delivered + 1'b1;
           end
       end
       assign valid = (delivered < NWORDS);
       assign data  = data_reg;
   endmodule
src_mutate_tb.v — self-checking; data-stable-under-stall catches the drift
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module src_mutate_tb;
       parameter WIDTH = 32, NWORDS = 8;
       reg  clk, rst, ready_gap;
       wire valid;
       wire [WIDTH-1:0] data;
       reg  [WIDTH-1:0] captured, expect_word, prev_data;
       reg  got, prev_valid, prev_ready;
       integer t, errors, received;
       wire ready = ready_gap;
       wire xfer  = valid & ready;
 
       src_mutate_good #(.WIDTH(WIDTH), .NWORDS(NWORDS)) dut (
           .clk(clk), .rst(rst), .ready(ready), .valid(valid), .data(data));
 
       always @(posedge clk) begin                        // inline reference sink
           if (rst) begin got <= 1'b0; captured <= 0; end
           else begin got <= xfer; if (xfer) captured <= data; end
       end
 
       always #5 clk = ~clk;
 
       initial begin
           errors = 0; received = 0; expect_word = 32'hA0; prev_valid = 1'b0;
           clk = 1'b0; rst = 1'b1; ready_gap = 1'b0;
           @(posedge clk); #1; rst = 1'b0;
           for (t = 0; t < 6*NWORDS; t = t + 1) begin
               @(negedge clk);
               ready_gap = ($random % 3) != 0;
               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 drifted under stall %h -> %h", t, prev_data, data);
                   errors = errors + 1;
               end
               if (got) begin
                   if (captured !== expect_word) begin
                       $display("FAIL: t=%0d mis-sampled: got %h exp %h", t, captured, expect_word);
                       errors = errors + 1;
                   end
                   expect_word = expect_word + 1; received = received + 1;
               end
           end
           if (errors == 0) $display("PASS: %0d words, correct value, data stable under stall", received);
           else             $display("FAIL: %0d errors (payload drifted under stall)", errors);
           $finish;
       end
   endmodule

In VHDL the bug is data <= counter with counter incremented every cycle; the fix registers the payload and advances it only on valid and ready. The testbench adds the data-stable assertion.

src_mutate.vhd — BUGGY vs FIXED in VHDL (numeric_std)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   -- BUGGY: data follows a free-running counter -> a stalled sink latches a drifted value.
   entity src_mutate_bad is
       generic ( WIDTH : positive := 32; NWORDS : positive := 8 );
       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 src_mutate_bad is
       signal counter   : unsigned(WIDTH-1 downto 0) := to_unsigned(16#A0#, WIDTH);
       signal delivered : integer range 0 to NWORDS := 0;
       signal valid_i   : std_logic;
   begin
       valid_i <= '1' when delivered < NWORDS else '0';   -- valid from local state (Rule 1 OK)
       valid   <= valid_i;
       data    <= std_logic_vector(counter);              -- but the payload is not frozen
       process (clk)
           variable xfer : std_logic;
       begin
           if rising_edge(clk) then
               xfer := valid_i and ready;
               if rst = '1' then counter <= to_unsigned(16#A0#, WIDTH); delivered <= 0;
               else
                   counter <= counter + 1;                -- WRONG: drifts every cycle
                   if xfer = '1' then delivered <= delivered + 1; end if;
               end if;
           end if;
       end process;
   end architecture;
 
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   -- FIXED: registered payload, changed only on the transfer.
   entity src_mutate_good is
       generic ( WIDTH : positive := 32; NWORDS : positive := 8 );
       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 src_mutate_good is
       signal data_reg  : unsigned(WIDTH-1 downto 0) := to_unsigned(16#A0#, WIDTH);
       signal delivered : integer range 0 to NWORDS := 0;
       signal valid_i   : std_logic;
   begin
       valid_i <= '1' when delivered < NWORDS else '0';
       valid   <= valid_i;
       data    <= std_logic_vector(data_reg);             -- stable across any stall (Rule 2)
       process (clk)
           variable xfer : std_logic;
       begin
           if rising_edge(clk) then
               xfer := valid_i and ready;
               if rst = '1' then data_reg <= to_unsigned(16#A0#, WIDTH); delivered <= 0;
               elsif xfer = '1' then                      -- change DATA only on the transfer
                   data_reg  <= data_reg + 1;
                   delivered <= delivered + 1;
               end if;
           end if;
       end process;
   end architecture;
src_mutate_tb.vhd — self-checking; data-stable-under-stall catches the drift
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity src_mutate_tb is
   end entity;
 
   architecture sim of src_mutate_tb is
       constant WIDTH : positive := 32; constant NWORDS : positive := 8;
       signal clk : std_logic := '0';
       signal rst, valid, ready, ready_gap, got : std_logic := '0';
       signal data, captured : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
   begin
       -- Bind to src_mutate_good to PASS; src_mutate_bad drifts under stall.
       dut : entity work.src_mutate_good
           generic map (WIDTH => WIDTH, NWORDS => NWORDS)
           port map (clk => clk, rst => rst, ready => ready, valid => valid, data => data);
 
       ready <= ready_gap;
 
       sink : process (clk)
           variable xfer : std_logic;
       begin
           if rising_edge(clk) then
               xfer := valid and ready;
               if rst = '1' then got <= '0'; captured <= (others => '0');
               else got <= xfer; if xfer = '1' then captured <= data; end if;
               end if;
           end if;
       end process;
 
       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 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'; ready_gap <= '0';
           wait until rising_edge(clk); wait for 1 ns; rst <= '0';
           for t in 0 to 6*NWORDS - 1 loop
               wait until falling_edge(clk);
               uniform(seed1, seed2, r);
               if r < 0.66 then ready_gap <= '1'; else ready_gap <= '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, unaccepted offer must keep the same data.
               if prev_valid = '1' and prev_ready = '0' then
                   assert data = prev_data
                       report "data drifted under stall" severity error;
               end if;
               if got = '1' then
                   assert captured = std_logic_vector(to_unsigned(expect_word, WIDTH))
                       report "mis-sampled word" severity error;
                   expect_word := expect_word + 1; received := received + 1;
               end if;
           end loop;
           report "src_mutate self-check complete" severity note;
           wait;
       end process;
   end architecture;

Across all six blocks the lesson is one sentence: a word offered on a valid/ready link must be advanced only on the transfer and frozen until then — advance on valid alone and you lose it, edit data under a held valid and you corrupt it — and a scoreboard that ties each captured word to the offered word is what catches both.

5. Verification Strategy

Handshake bugs share a cruel property: they are invisible in the easy case and only appear under the exact stimulus the block designer skipped. So verification here is less about a clever checker and more about forcing the hard case — backpressure, integration, reset — and then asserting the two properties that split every bug: liveness (the link eventually moves) and safety (every word moves once, in order, with the offered value). The single governing invariant:

Every word the source emits is received by the sink exactly once and in order (none lost, none duplicated); the value each transfer delivers is the value that was offered when valid first rose (not a drifted one); and the link always makes progress — at least one transfer eventually occurs whenever the source has data and the sink has room.

Everything below makes that checkable and ties each check to the catalog bug it catches.

  • A scoreboard that ties captured to offered (catches lost, duplicate, mis-sample). Model the expected stream as a reference counter and, on every transfer (valid && ready), assert the captured word equals the next expected word, then advance the model. At the end assert the received count equals the emitted count. The count catches lost (too few) and duplicate (too many); the per-word compare catches mis-sample (right count, wrong value). This is the §4 scoreboard — SV assert (captured === expect_word), Verilog if (captured !== expect_word) $display("FAIL…"), VHDL assert captured = … severity error — and it must be driven with gappy backpressure, because an always-ready sink hides all three.
  • A data-stable-under-stall assertion (catches the mutation / mis-sample directly). Every cycle, if the previous cycle had valid && !ready (a held, unaccepted offer), assert data is unchanged this cycle. This is the sole direct detector of a Rule 2 payload violation; the §4c bug fails here and only here. Property in words: valid high and ready low implies data stable next cycle.
  • A no-transfer-without-both check (catches sampling on valid alone). Assert the sink captures a word on exactly the cycles valid && ready — never on valid alone (which would duplicate a held word during a stall) and never on ready alone (which would latch garbage while the producer is idle). A sink got pulse that fires when ready was low is the bug.
  • A liveness / deadlock check (catches valid gated by ready). Wire the source to a sink whose ready legally depends on valid (Rule 3) and assert at least one transfer occurs within a bounded window. A source that gates valid with ready forms a combinational loop and makes zero progress — the check is a nonzero transfer count. Liveness must be a first-class assertion, because a deadlocked link passes every safety check trivially (nothing wrong ever moves because nothing moves).
  • A reset-and-first-word check (catches garbage out of reset). Assert valid is low during and immediately after reset until the source's payload is known, and that the first accepted word is the intended first word, not an X or a stale value. Drive reset asserted, then deasserted, and confirm no transfer carries an unknown payload — the direct detector for valid asserted before data is ready.
  • Corner cases and the integration harness. Exercise the patterns that break weak implementations: ready low for many consecutive cycles with valid held (long stall — the word must survive); ready toggling every cycle (transfer only on the high cycles); back-to-back valid && ready with no gaps (full throughput, one word per cycle); a ready-before-valid bubble (no spurious transfer); and, crucially, an integration test that wires the real source to the real sink rather than a stub — the only stimulus that surfaces the deadlock, since it needs two blocks that each look at the other.
  • Expected waveform. On a correct run, valid rises when the source has a word and stays high with data frozen across every ready-low cycle, dropping only after the transfer; each transfer is a single overlap; the captured stream equals the emitted stream, in order. The visual signature of each bug is distinct: a deadlock is valid and ready both stuck low forever; a lost word is a valid-high stall cycle where the source's index advanced anyway; a mis-sample is data changing while valid is held; a duplicate is a sink capture on a cycle ready was low; garbage-out-of-reset is a transfer whose data is X.

6. Common Mistakes

valid combinationally gated by ready. The one liveness bug and the signature handshake failure. Writing valid = have_data && ready makes the offer depend on the acceptance; paired with a sink whose ready looks at valid (which Rule 3 permits), the two form a combinational loop and the link deadlocks with zero transfers. It is invisible against an always-ready stub and fatal at integration. The rule is absolute: valid is a function of local producer state only; it never reads ready. (DebugLab row 1.)

Advancing the source on valid instead of on the transfer. The lost-word bug. A source that increments its index, pops its read pointer, or clears have_data every cycle valid is high — regardless of ready — skips every word it offered during a stall, because it moved on but the sink never accepted. Advance local state only on valid && ready. (DebugLab row 2; §4b.)

Mutating data while valid is held. The mis-sample bug. If the payload changes before the accept — wired to a free-running counter, recomputed each cycle, or advanced on the wrong edge — a stalled sink latches a later value than the one offered, off by the number of stall cycles. Count right, value wrong. Register the payload and change it only on the transfer. (DebugLab row 3; §4c.)

Sampling data on valid alone, ignoring ready. The duplicate bug. A sink that captures whenever valid is high — without also checking ready — latches the same held word on every stall cycle (duplicating it downstream) or captures a word it has no room for (overrunning its buffer). A transfer is valid && ready; capture on exactly that condition, never on valid by itself. (DebugLab row 4.)

A combinational ready that glitches. ready may legally look at valid (Rule 3), but it must be a clean function of valid and registered state — not a deep combinational mess that momentarily pulses high while its inputs settle. A ready glitch high for a sliver of a cycle while valid is high can register a phantom accept the source also sees, dropping or double-counting a word. Drive ready from stable registered occupancy (or a shallow, clean combinational function of valid), never from a racy multi-level path.

valid asserted out of reset before the payload is ready. If valid resets high (or is left un-reset and floats), while the source's data is still X or stale, the first word out is garbage the sink accepts. Reset valid to 0 and keep it low until the first real word is loaded — the positive form is: reset the control bit to the safe (no-offer) state, and only raise valid once the payload is known.

Trusting the unit test that used a stub. Not a rule violation but the reason every rule violation ships: a source verified only against an always-ready sink never has to hold an offer, so Rules 1 and 2 are never exercised; a sink verified only against an always-valid source never stalls. Verify with gappy backpressure and an integration harness of the real source and real sink, or the catalog bugs sail through review.

7. DebugLab

The handshake bug clinic — a deadlock, a lost word, a mis-sample, and a duplicate

The engineering lesson: a handshake never fails in a mysterious new way — it fails in one of two, and the symptom tells you which before you read the RTL. A stuck link (nothing moves) is a liveness bug, and there is essentially one cause: a valid that was gated by ready, so the two wait on each other — restore Rule 1 and drive valid locally. A link that moves but delivers the wrong stream is a safety bug; cut again by count versus value. A wrong count means a lost word (the source advanced on valid instead of the transfer) or a duplicate (the sink sampled on valid alone) — the fix is to advance and capture on exactly valid && ready. A right count with a wrong value means a mis-sample (the payload drifted while valid was held) — the fix is Rule 2, freeze the offer until the transfer. Classify, then name the rule, then apply its positive form. And because every one of these hides against a trivial stub, the meta-lesson is to verify under gappy backpressure and at integration — the exact stimulus the unit test skipped. These four failures, and their fixes, are identical across SystemVerilog, Verilog, and VHDL, and they are the same bugs you will debug on AXI, APB, and every streaming link.

8. Interview Q&A

9. Exercises

Design and reasoning exercises — no syntax drills. Each rehearses the classify-then-name-the-rule reflex the page is built to install.

Exercise 1 — Classify six symptoms

For each observed symptom, name the family (liveness or safety), the sub-class where it applies (count-wrong lost / count-wrong duplicate / value-wrong mis-sample), and the single rule that was violated: (a) the pipeline hangs at integration with valid and ready both low; (b) under backpressure the downstream count is one short per stall; (c) under backpressure the count is right but one value is off by the stall length; (d) under backpressure the count is high and a held word appears twice; (e) the first word after reset is X; (f) a word is occasionally dropped when a deeply combinational ready settles. Do it in one line each, from the symptom alone.

Exercise 2 — Break it two ways, then trace the fix

Take a correct source that offers three words (0xA0, 0xA1, 0xA2) with the sink ready pattern 1, 0, 0, 1, 1 (accept, stall twice, accept, accept). (i) Modify it to advance on valid alone and hand-trace which word is lost and on which cycle. (ii) Separately, modify it to drive data from a counter and hand-trace which value the sink captures for the second word. Then, for the correct source, trace all three transfers and confirm every captured value equals the offered value.

Exercise 3 — Why the stub hid it

For each of the three source-side bugs (valid gated by ready, advance-on-valid, mutate-under-stall), state the exact stub sink that makes the bug invisible in a unit test, and explain in one line the mechanism by which that stub removes the condition the bug needs. Then state the one stimulus property (a single knob) that, if added to every unit test, would have surfaced all three.

Exercise 4 — Write the catch-all test plan

Write the test plan (not the RTL) that catches all six catalog bugs before tape-out. Specify: the scoreboard model and its two count/value checks; the data-stable-under-stall assertion and the exact cycle condition that triggers it; the no-transfer-without-both check; the liveness/deadlock check and the kind of sink you must connect for it to fire; the reset-and-first-word check; and the backpressure corner cases (long stall, toggling ready, back-to-back throughput, ready-before-valid bubble, integration of real source and real sink). For each of the six bugs, name the single check that catches it.

In-track dependencies (the contract this page catalogs the violations of):

  • The valid/ready Handshake — Chapter 9.1; the direct prerequisite — the three-wire contract and the three rules (valid never looks at ready; hold the offer with stable data; ready may look at valid). Every bug here is one of those rules broken.
  • Backpressure & Stall — Chapter 9.2; the direct prerequisite — how a low ready propagates back to stall the producer, and the stall vocabulary these safety bugs live in (every one of them surfaces only under backpressure).
  • The Valid-Bit Discipline — Chapter 8.3; the producer half of the handshake and the stale-valid/data misalignment that is the pipelined cousin of the mis-sample bug.

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

  • Skid Buffer — Chapter 9.3; the registered stage that both breaks the long combinational ready chain and, done wrong, is a rich source of its own lost/duplicated-word bugs — the fix for the timing cost these bugs sit near.
  • Credit-Based Flow Control — Chapter 9.4; the alternative to backward ready, with its own bug catalog (lost credits, credit over-count) that mirrors these liveness/safety families.
  • Ready/Valid Pipelining — Chapter 9.6; composing many handshake stages, where a single mis-registered ready reintroduces the deadlock and the dropped word at scale.
  • Case Study — Streaming FIFO — Chapter 9.7; a full FIFO exposed as a handshake sink and source, where these bugs appear as integration failures across the write and read handshakes.

Backward / in-track method and structure:

  • Synchronous FIFO Architecture — Chapter 7.1; the FIFO whose write side is a handshake sink and read side a source — the canonical place a sample-on-valid-alone or advance-on-valid bug corrupts occupancy.
  • FIFO Full/Empty Generation — Chapter 7.2; the full/empty flags that are a FIFO's ready/valid, and whose off-by-one is the FIFO-side cousin of these count-wrong bugs.
  • FIFO Verification — Chapter 7.x; the scoreboard and in-order/exactly-once methodology this page's bug-catcher is built on.
  • Reset Strategy — Chapter 2.5; the reset discipline behind the garbage-out-of-reset bug — resetting valid to the safe no-offer state so the first word is never X.
  • The RTL Design Mindset — Chapter 0.2; the Verification lens this page applies — you debug from the symptom down the taxonomy, not from the RTL up.
  • What Is an RTL Design Pattern? — Chapter 0.1; why a contract this simple still earns a catalog of signature failures.

Verilog v1 prerequisites (the grammar these bugs and fixes transcribe into):

  • Blocking and Non-Blocking Assignments — why the source's payload and state advance with non-blocking (<=) so the offer moves atomically on the transfer edge, and how a blocking mistake reintroduces the mutation bug.
  • If-Else Statements — the reset / transfer / hold priority (if (rst) … else if (valid && ready) …) whose wrong guard (if (valid)) is the lost-word bug.
  • Initial & Always Blocks — the clocked always block that must advance only on the transfer and capture only on valid && ready.
  • Physical Data Types — the wire/reg typing behind driving valid/ready combinationally versus registering the payload and the captured word.

11. Summary

  • Every handshake bug is liveness or safety — classify first, from the symptom. A stuck link (nothing moves, a hang or deadlock) is a liveness bug; a link that moves but delivers the wrong stream is a safety bug. That one cut, made from the symptom before you read a line of RTL, is more than half the debugging, and it maps every failure onto a violated rule from 9.1.
  • Liveness has essentially one cause: valid gated by ready. Writing valid = have_data && ready makes the offer depend on the acceptance; wired to a sink whose ready looks at valid (Rule 3 permits it), the two form a combinational loop and the link deadlocks with zero transfers — invisible against a stub, fatal at integration. The fix is Rule 1: drive valid from local state only.
  • Safety splits by count versus value. A wrong count is a lost word (the source advanced on valid instead of the transfer) or a duplicate (the sink sampled on valid alone) — fix both by acting on exactly valid && ready. A right count with a wrong value is a mis-sample (the payload drifted while valid was held) — fix by Rule 2, register the offer and change it only on the transfer.
  • The rest of the catalog: the ready glitch and the reset-garbage. A racy combinational ready can pulse a phantom accept — drive ready from clean registered state. valid asserted out of reset before the payload is real emits a garbage first word — reset valid to 0 and raise it only once the data is known. Both are safety bugs the same classification catches.
  • The bugs hide against stubs; verify the hard case. Each failure is invisible in isolation and appears only under backpressure or at integration — the exact stimulus a naive unit test skips. Verify with a scoreboard under gappy backpressure (count and value), a data-stable-under-stall assertion, a no-transfer-without-both check, and a liveness check against a real ready-depends-on-valid sink. The two most instructive bugs — a lost word and a mis-sample — are built buggy-vs-fixed and self-check-verified here in SystemVerilog, Verilog, and VHDL, and the whole catalog is the same on AXI, APB, and every streaming link.