Read first: Layering & Transaction Model: what is crossing this boundary?
Assume one synchronous source and destination, sampled on rising edges of the same clock, outside reset. The source presents VALID plus a payload; the destination presents READY. Exactly one item is accepted at an edge where VALID && READY is true. A high VALID or READY alone is not a transfer, and signal changes between sampled edges do not create an extra transfer.
For the AXI-style interface used here, Rule 1: outside reset, an asserted VALID and its payload must remain stable until an accepting rising edge with VALID && READY. Rule 2: a source must not wait for READY before presenting an available item. Rule 3: a destination may wait until it has observed VALID before asserting READY. Waiting permission does not permit an AXI combinational input-to-output path: implement the dependency through suitable state. Arm IHI 0022H A3.1–A3.2 specifies both the timing restriction and handshake rules.
Retained offer: one clock, outside reset
Before E1 the source offers item A with payload D0, and the destination cannot accept.
Edge | VALID / READY | Payload | Accepted | Source after edge |
|---|---|---|---|---|
E1 | 1 / 0 | D0 | None | Retain A/D0 |
E2 | 1 / 0 | D0 | None | Retain A/D0 |
E3 | 1 / 1 | D0 | A | A may retire |
E4 | 1 / 1 | D1 | B | B may retire |
What to notice
A/D0 is unchanged at E1, E2 and the accepting edge E3.
B/D1 appears only after A was accepted; E3 and E4 are two transfers.
No. READY is 0 at E1 and E2, so A was not accepted and must remain offered through E3.
The classic bug is gating a pending offer with READY: valid = ready & internal_valid. If the destination starts with READY=0 and waits to observe VALID, the producer never presents its item and the destination never sees a reason to become ready. This is a waiting deadlock even when the destination's decision is registered. A literal combinational loop is a separate circuit problem and is not necessary for this deadlock.
Do not rely on synthesis to repair an invalid dependency. Drive VALID from retained pending-item state, preserve the payload during a stall, and choose buffering that obeys the selected interface timing rules. Registering one signal without preserving the item and controlling acceptance is not a complete repair. A directed test with READY initially low exposes the waiting bug in simulation.
Registered waiting deadlock
Before E0, A is pending and both outputs are 0. The buggy source registers VALID_next = pending && READY_seen; the destination registers READY_next = VALID_seen.
Edge | Pending | VALID / READY | Accepted | After edge |
|---|---|---|---|---|
E0 | A | 0 / 0 | None | Both keep waiting |
E1 | A | 0 / 0 | None | Both keep waiting |
E2 | A | 0 / 0 | None | No progress |
What to notice: Follow good_valid before good_ready rises, then the good_accepted count. Compare the faulty pair: both outputs remain low despite pending work. These are pre-edge samples; registered counters become visible on a later sample.
The repaired pair's VALID and READY overlap and good_accepted reaches one. The faulty pair keeps bad_valid, bad_ready and bad_accepted at zero. A pending item alone is not a transfer.
What to notice
Both decisions are registered; this deadlock needs no combinational feedback path.
A remains pending but is never presented, so the destination never responds.
No. The source must present retained pending state independently of READY. Another register leaves the circular waiting policy intact.
Both destinations register READY after observing VALID. Both sources start with one pending item carrying A0. The faulty source waits for READY; its pair stays in the zero-output state. The repaired source presents the item independently, retains it until acceptance and completes once. The bench checks six active edges; the closed zero-state dependency explains why the faulty pair cannot escape by waiting longer.
// Same-clock teaching model: registered waiting can deadlock without a combinational loop.
module pending_source #(parameter WAIT_READY = 0) (
input wire clk,
input wire rst,
input wire ready,
output reg valid,
output wire [7:0] data
);
reg pending;
assign data = 8'hA0;
always @(posedge clk) begin
if (rst) begin
pending <= 1'b1;
valid <= 1'b0;
end else if (valid && ready) begin
pending <= 1'b0;
valid <= 1'b0;
end else if (pending) begin
// WAIT_READY=1 is the deliberately faulty policy.
if (WAIT_READY == 0 || ready) valid <= 1'b1;
end
end
endmodule
module waiting_destination (
input wire clk,
input wire rst,
input wire valid,
output reg ready
);
always @(posedge clk) begin
if (rst) ready <= 1'b0;
else ready <= valid;
end
endmodule
module tb;
reg clk = 1'b0;
always #5 clk = ~clk;
reg rst = 1'b1;
wire good_valid, good_ready, bad_valid, bad_ready;
wire [7:0] good_data, bad_data;
integer good_accepted = 0;
integer bad_accepted = 0;
pending_source #(.WAIT_READY(0)) good_source (
.clk(clk), .rst(rst), .ready(good_ready), .valid(good_valid), .data(good_data)
);
pending_source #(.WAIT_READY(1)) bad_source (
.clk(clk), .rst(rst), .ready(bad_ready), .valid(bad_valid), .data(bad_data)
);
waiting_destination good_destination (
.clk(clk), .rst(rst), .valid(good_valid), .ready(good_ready)
);
waiting_destination bad_destination (
.clk(clk), .rst(rst), .valid(bad_valid), .ready(bad_ready)
);
always @(posedge clk) begin
if (rst) begin
good_accepted <= 0;
bad_accepted <= 0;
end else begin
if (good_valid && good_ready) begin
if (good_data !== 8'hA0) begin
$display("FAIL: repaired source changed the pending payload");
$finish;
end
good_accepted <= good_accepted + 1;
end
if (bad_valid && bad_ready) bad_accepted <= bad_accepted + 1;
if (bad_valid || bad_ready || !bad_source.pending) begin
$display("FAIL: faulty pair did not preserve its closed zero-output waiting state");
$finish;
end
end
end
initial begin
$dumpfile("wave.vcd");
$dumpvars(0, tb);
repeat (2) @(posedge clk);
@(negedge clk); rst = 0;
repeat (6) @(posedge clk);
#1;
if (good_accepted != 1 || good_source.pending || good_valid) begin
$display("FAIL: repaired source did not complete its one pending item");
$finish;
end
if (bad_accepted != 0 || !bad_source.pending || bad_valid || bad_ready) begin
$display("FAIL: waiting source did not remain blocked with work pending");
$finish;
end
$display("PASS: repaired source completed once; registered mutual waiting retained pending work without progress");
$finish;
end
endmoduleNever: valid = ready (direct or through combinational logic).
Retain pending VALID and payload until an accepting edge.
Review both dependency rules and actual buffering capacity.
A skid buffer retains an extra accepted item when registered backpressure takes time to reach the producer. Consider a design with an output register and one extra skid slot: two items total. If item A is stalled at the output while upstream still sees READY, accepted item B must be stored in the skid slot. Output VALID and A remain unchanged; the buffer does not withdraw the stalled offer.
Once both slots are occupied, upstream READY must stop further acceptance before item C can overflow storage. After A transfers, B can advance without being lost, duplicated or reordered. Exact latency and bypass behaviour depend on the design; buffering cannot make the downstream accept while its READY is low. Size storage for the number of additional items that can actually be accepted before backpressure takes effect.
Two-slot skid buffer: signals before each edge
Storage is an output register plus one skid slot. Before E1, output A is occupied, the skid slot is empty, upstream offers B and downstream stalls. Item 'None' means VALID is 0.
Edge | IN item | IN_READY | OUT item | OUT_READY |
|---|---|---|---|---|
E1 | B | 1 | A | 0 |
E2 | C | 0 | A | 0 |
E3 | C | 0 | A | 1 |
E4 | C | 1 | B | 1 |
E5 | None | 1 | C | 1 |
The same edges: accepted items and resulting storage
Q_after counts both storage slots immediately after the edge.
Edge | IN accepted | OUT accepted | Output / skid after edge | Q_after |
|---|---|---|---|---|
E1 | B | None | A / B | 2 |
E2 | None | None | A / B | 2 |
E3 | None | A | B / empty | 1 |
E4 | C | B | C / empty | 1 |
E5 | None | C | Empty / empty | 0 |
What to notice: Read input and output acceptance separately. During a stalled output, its offered data must stay unchanged even if another input is accepted into the spare slot. The bad_diverged flag belongs to the faulty comparison, not the correct buffer.
No. Only an edge with both high delivers an item. The accepted and delivered counters track different boundaries; their difference accounts for stored work.
What to notice
C is offered at E2/E3 but not accepted; the source retains it through E4.
E4 accepts C and completes B, so occupancy stays one.
Registered backpressure costs the E3 input opportunity in this policy.
No. IN_READY is 0 at E3; C is first accepted at E4. Keep C and its payload stable through E4.
The good buffer must preserve output order, accepted-minus-delivered occupancy and a stalled output offer. Each instance receives A/B/C once through its own handshake. The faulty copy drops the extra accepted item; the bench must detect its conservation failure. Payloads A0, B1 and C2 identify items A, B and C. The bench first loads A, then exercises the five edges in the tables above.
// Two-slot valid/ready evidence for the Bus Protocols teaching pilot.
// The correct instance retains one extra accepted beat; BUG=1 accepts that beat but drops it.
module skid2 #(parameter BUG = 0) (
input wire clk,
input wire rst,
input wire in_valid,
output wire in_ready,
input wire [7:0] in_data,
output wire out_valid,
input wire out_ready,
output wire [7:0] out_data
);
reg out_full, skid_full;
reg [7:0] out_word, skid_word;
assign in_ready = !skid_full;
assign out_valid = out_full;
assign out_data = out_word;
wire in_accept = in_valid && in_ready;
wire out_accept = out_valid && out_ready;
wire stalled = out_valid && !out_ready;
always @(posedge clk) begin
if (rst) begin
out_full <= 1'b0;
skid_full <= 1'b0;
end else if (stalled) begin
if (in_accept && BUG == 0) begin
skid_full <= 1'b1;
skid_word <= in_data;
end
end else if (skid_full) begin
out_full <= 1'b1;
out_word <= skid_word;
skid_full <= 1'b0;
end else begin
out_full <= in_valid;
out_word <= in_data;
end
end
endmodule
module tb;
reg clk = 1'b0;
always #5 clk = ~clk;
reg rst = 1'b1;
reg in_valid = 1'b0;
reg [7:0] in_data = 8'h00;
reg out_ready = 1'b0;
wire in_ready, out_valid;
wire [7:0] out_data;
wire bug_in_ready, bug_out_valid;
wire [7:0] bug_out_data;
reg traffic_started = 1'b0;
reg [1:0] bad_source_index = 0;
wire bug_in_valid = traffic_started && bad_source_index < 3;
wire [7:0] bug_in_data = bad_source_index == 0 ? 8'hA0 :
bad_source_index == 1 ? 8'hB1 : 8'hC2;
skid2 #(.BUG(0)) good (
.clk(clk), .rst(rst), .in_valid(in_valid), .in_ready(in_ready),
.in_data(in_data), .out_valid(out_valid), .out_ready(out_ready),
.out_data(out_data)
);
skid2 #(.BUG(1)) bad (
.clk(clk), .rst(rst), .in_valid(bug_in_valid), .in_ready(bug_in_ready),
.in_data(bug_in_data), .out_valid(bug_out_valid), .out_ready(out_ready),
.out_data(bug_out_data)
);
integer good_accepted = 0;
integer good_delivered = 0;
integer bad_accepted = 0;
integer bad_delivered = 0;
reg [7:0] expected [0:2];
integer expected_head = 0;
reg bad_diverged = 1'b0;
reg good_was_stalled = 1'b0;
reg [7:0] held_good_word;
always @(posedge clk) begin
if (rst) begin
good_accepted <= 0;
good_delivered <= 0;
bad_accepted <= 0;
bad_delivered <= 0;
expected_head <= 0;
bad_diverged <= 1'b0;
good_was_stalled <= 1'b0;
bad_source_index <= 0;
end else begin
if ((good_accepted - good_delivered) != ({1'b0, good.out_full} + {1'b0, good.skid_full})) begin
$display("FAIL: good buffer violated accepted-minus-delivered conservation");
$finish;
end
if (good_was_stalled && (!out_valid || out_data !== held_good_word)) begin
$display("FAIL: good output changed its stalled offer");
$finish;
end
good_was_stalled <= out_valid && !out_ready;
held_good_word <= out_data;
if (in_valid && in_ready) good_accepted <= good_accepted + 1;
if (out_valid && out_ready) begin
if (expected_head >= 3) begin
$display("FAIL: good buffer delivered an unexpected extra item");
$finish;
end
if (out_data !== expected[expected_head]) begin
$display("FAIL: good stream expected %02x, got %02x", expected[expected_head], out_data);
$finish;
end
expected_head <= expected_head + 1;
good_delivered <= good_delivered + 1;
end
// Each instance gets A/B/C once, advancing on its own input handshake.
// The mutant's incorrect READY must not make the shared source repeat C.
if (bug_in_valid && bug_in_ready) begin
bad_accepted <= bad_accepted + 1;
bad_source_index <= bad_source_index + 1'b1;
end
if (bug_out_valid && out_ready) bad_delivered <= bad_delivered + 1;
if ((bad_accepted - bad_delivered) != ({1'b0, bad.out_full} + {1'b0, bad.skid_full}))
bad_diverged <= 1'b1;
end
end
task drive(input v, input [7:0] d, input rdy);
begin
@(negedge clk);
in_valid = v;
in_data = d;
out_ready = rdy;
if (v) traffic_started = 1'b1;
end
endtask
initial begin
expected[0] = 8'hA0;
expected[1] = 8'hB1;
expected[2] = 8'hC2;
$dumpfile("wave.vcd");
$dumpvars(0, tb);
repeat (2) @(posedge clk);
@(negedge clk);
rst = 1'b0;
drive(1'b1, 8'hA0, 1'b1); // A enters the output register.
drive(1'b1, 8'hB1, 1'b0); // A stalls; B enters the skid slot.
drive(1'b1, 8'hC2, 1'b0); // C is retained upstream; no input acceptance.
drive(1'b1, 8'hC2, 1'b1); // A leaves; C is still not accepted this edge.
drive(1'b1, 8'hC2, 1'b1); // B leaves while C enters.
drive(1'b0, 8'h00, 1'b1); // C leaves.
drive(1'b0, 8'h00, 1'b1);
if (good_accepted != 3 || good_delivered != 3 || expected_head != 3) begin
$display("FAIL: good counts accepted=%0d delivered=%0d checked=%0d", good_accepted, good_delivered, expected_head);
$finish;
end
if (!bad_diverged) begin
$display("FAIL: dropped-beat mutant was not detected");
$finish;
end
if (bad_accepted != 3 || bad_delivered >= bad_accepted) begin
$display("FAIL: mutant did not accept all three inputs and lose a retained item");
$finish;
end
$display("PASS: correct skid retained A/B/C; conservation exposed the dropped-beat mutant");
$finish;
end
endmoduleWhen a downstream destination stops accepting, finite queues can absorb work temporarily; once their admission thresholds are reached, backpressure can propagate upstream through FIFOs, arbiters and bridge controls. The source you see stalled may be separated from the limiting service point, but the stall itself does not identify that point.
Plot actual FIFO occupancy alongside its local full/empty flags and accepted input/output events. High occupancy can explain frequent input stalls. An apparently empty FIFO with a stalled upstream offer needs further investigation: reset, arbitration, registered flags or a downstream dependency can prevent acceptance. Occupancy alone does not identify the cause.
Measure stall cycles per channel: % cycles where valid=1 && ready=0.
Credit exhaustion: sender has data but credit count = 0.
Arbiter service: inspect eligibility, grants and completion assumptions; a QoS field alone does not establish fairness.
Average handshake rate hides tail latency. A master stalled 40% of cycles on AR may still show 'active' waveforms. Measure stall percentage and p99 latency, not visual activity.
Size from the largest possible backlog of accepted writes minus completed reads, including any additional acceptances before backpressure takes effect. A burst's beat count already measures items; multiplying it by a clock ratio is not a general FIFO-depth formula. Average rate does not bound burst backlog or a paused consumer.
Both regulate admission from finite capacity, but their contracts differ. A ready/valid destination makes a per-edge acceptance decision; a credit protocol grants units that the sender accounts for until return/update. Debug compares advertised capacity, consumption, return and the traffic class—do not substitute one protocol's timing rules for the other.
Acceptance happens at a sampled VALID && READY edge; retain an unaccepted offer through its accepting edge.
Count storage as accepted inputs minus completed outputs, with initial occupancy included.
Present pending work without waiting for READY, and assess fairness separately from local backpressure.