Two blocks from two teams are connected with valid/ready. Each was verified against its own testbench, and each testbench's partner behaved politely. Connected to each other, nothing ever moves: both sit at zero forever. Each team insists its block is correct, and in isolation each one is.
Which rules must each side of a valid/ready interface follow so that any compliant producer works with any compliant consumer?
Lesson 3.3 gave the core rule: a word moves on an edge where valid and ready are both high. It is tempting to think that is all. But the rule says nothing about when each side may raise its signal. A cautious producer might reason: "I'll raise valid only once the consumer is ready, so I never offer a word that can't be taken." A cautious consumer might reason: "I'll raise ready only once a word is offered." Each policy sounds sensible. Together they wait for each other forever.
Every robust valid/ready protocol adopts the same three rules. AXI states them for all of its channels:
The producer must not wait for ready before raising valid. When it has a word, it raises valid, whatever ready is doing.
Once valid is high, the producer holds valid and the data unchanged until the word moves (an edge with valid and ready both high).
The consumer may wait for valid before raising ready, or raise ready first. Ready may depend on valid.
The asymmetry is the point. If both sides were allowed to wait for the other, two compliant blocks could deadlock. By forbidding exactly one side, the producer, from waiting, the protocol guarantees that someone always goes first: valid rises on its own, and the consumer, whatever its policy, eventually sees it.
Rule 1 also has a timing form: there must be no combinational path from ready to valid. Rule 3 permits a path from valid to ready. If both existed, valid and ready would form a combinational loop, which can oscillate or settle unpredictably, and which every synthesis and timing tool rejects.
The testbench runs two pairs side by side. Pair A follows the rules: its producer raises valid when it has data, and its consumer, legally, waits for valid. Pair B has the same consumer, but its producer breaks rule 1 by waiting for ready. Both sides of pair B register their decisions, so there is no combinational loop, only the protocol mistake.
// Two producer/consumer pairs side by side.
// Pair A follows the rules: the producer raises valid when it has data, without
// looking at ready; the consumer may wait for valid before raising ready.
// Pair B breaks rule 1: its producer waits for ready before raising valid, and its
// consumer (legally) waits for valid before raising ready. Each waits for the other.
// The checker shows pair A delivering all its words and pair B delivering none.
module tb;
logic clk = 0;
always #5 clk = ~clk;
logic rst_n = 0;
initial begin
$dumpfile("wave.vcd"); $dumpvars(0, tb);
#12 rst_n = 1;
end
// ---- Pair A: correct ----
logic vld_a = 0;
logic [7:0] data_a = 0;
logic [3:0] sent_a = 0;
wire rdy_a = vld_a; // consumer waits for valid (allowed)
wire xfer_a = vld_a && rdy_a;
always @(posedge clk) if (rst_n) begin
if (!vld_a || xfer_a) begin // free to offer the next word
vld_a <= (sent_a < 4);
if (sent_a < 4) begin data_a <= 8'(sent_a); sent_a <= sent_a + 1; end
end
end
// ---- Pair B: producer breaks rule 1 ----
// Both sides register their decision (no combinational loop): each raises its
// signal one cycle after it sees the other's. Neither ever goes first.
logic have_b = 0; // producer has data from cycle 1 on
logic vld_b = 0, rdy_b = 0;
wire xfer_b = vld_b && rdy_b;
always @(posedge clk) if (rst_n) begin
have_b <= 1;
vld_b <= have_b && rdy_b; // WRONG: valid waits for ready
rdy_b <= vld_b; // consumer waits for valid (allowed)
end
int moved_a = 0, moved_b = 0;
always @(posedge clk) if (rst_n) begin
moved_a += xfer_a;
moved_b += xfer_b;
end
initial begin
#300;
if (moved_a == 4 && moved_b == 0)
$display("PASS: the correct pair moved 4 words; the pair where valid waits for ready moved 0 (deadlock)");
else $display("FAIL: pair A moved %0d, pair B moved %0d", moved_a, moved_b);
$finish;
end
endmoduleHow to read it, slot by slot:
Pair A, slots 1 to 4: vld_a rises as soon as the producer has a word. rdy_a follows it in the same cycle (the consumer waits for valid, which rule 3 allows), so xfer_a is high and words 0 to 3 move on four consecutive edges.
Pair B, slot 1 onwards: have_b shows the producer has data. But its valid waits for ready, and its consumer's ready waits for valid. vld_b, rdy_b and xfer_b stay at 0 for the rest of the run, and would forever.
Both consumers are identical and both are compliant. The only difference is one producer that waited for ready. That is why rule 1 is written for the producer: it is the one rule that, if everyone follows it, makes deadlock between compliant blocks impossible.
Rules 1 to 3 translate directly into checks. Rule 2 is easy to check on a waveform; rule 1, being about what valid waits for, is checked by design review, by a formal tool, or by a testbench that holds ready low indefinitely and checks that valid still rises:
// snippet: the valid/ready rules as SystemVerilog assertions
// Rule 2: once valid, hold valid and data until the transfer.
assert property (@(posedge clk) disable iff (!rst_n)
valid && !ready |=> valid && $stable(data));
// Rule 1 (testbench form): with ready held low for 20 cycles, a producer that has
// data must still raise valid.
assert property (@(posedge clk) disable iff (!rst_n)
has_data && !ready [*20] |-> valid);The first assertion is the most valuable single check on any valid/ready interface: it catches producers that drop or change a stalled word, the bug behind most lost or duplicated data.
What a side may do | Producer | Consumer |
|---|---|---|
Raise its signal without seeing the other's | Must (rule 1) | May (ready before valid) |
Wait for the other's signal first | Must not | May (ready after valid) |
Combinational path from the other's signal to its own | Must not (no ready → valid) | May (valid → ready) |
Change its signal after raising it, before a transfer | Must not (rule 2) | Usually allowed while valid is low; check the specific protocol |
Protocol | Its rules |
|---|---|
AXI (all five channels) | A source must not wait for READY before asserting VALID; once VALID is asserted it stays asserted until the transfer, with the payload stable while VALID is high and READY is low; a destination may wait for VALID before asserting READY, or assert READY first. The reason given is exactly the deadlock above. |
AXI-Stream | The same rules for TVALID and TREADY. |
The opening hang (illustrative): the producing team had tested against a consumer model that held ready high all the time, so their producer's "wait for ready" policy never mattered. The consuming team had tested against a producer model that raised valid whenever it had data. Connected together, the two real blocks deadlocked on the first word. Each block was correct against its own test partner; the producer broke rule 1, which neither testbench checked.
Lesson for verification: every valid/ready testbench should include a partner that does the least the rules allow: a consumer that waits for valid before raising ready, and a producer that raises valid only when it has data. A block that works only with a generous partner is not compliant.
Yes. Its valid depends on ready, through a register. Paired with a consumer that waits for valid before raising ready, neither will ever go first.
Allowing both would create a combinational loop between the two signals. Forbidding ready → valid is enough to prevent the loop, and it is the same restriction that prevents the deadlock.
For deadlock, yes: the producer never waits for ready, so it will still raise valid when it has data. Whether a particular protocol allows ready to be withdrawn before valid is a detail to check in its specification.
The producer must not wait for ready before raising valid; the consumer may wait for valid.
Once valid is high, valid and data hold until the transfer; checking this one rule catches most lost and duplicated words.
No combinational path from ready to valid: valid → ready is allowed, the reverse would create a loop and a deadlock.