With bus-error: true, BVALID and RVALID are one-cycle pulses that do not wait
for BREADY/RREADY. If the master is not ready in that cycle the response is lost
and the slave stops accepting new requests permanently — it is wedged until
reset.
Follow-up to #7, same feature as #68.
Observed on master @ 2026fce (2026-05-20) and cheby 1.7.dev0.
File: proto/cheby/hdl/axi4litebus.py.
Cause
In expand_bus_r, the correct clear is disabled when bus_error is set:
# Clear 'set' bit at the end of the transaction # 281
proc_if = HDLIfElse(HDLEq(HDLParen(HDLAnd(axi_rdone, self.root.h_bus['rready'])), bit_1))
proc_if.then_stmts.append(HDLAssign(axi_arset, bit_0))
if not opts.bus_error: # 284
proc_if.then_stmts.append(HDLAssign(axi_rdone, bit_0))
and an unconditional clear is added in its place:
if opts.bus_error: # 264
# Stop accepting handshake as soon as reset is over
proc.sync_stmts.append(HDLAssign(axi_rdone, bit_0)) # 266
expand_bus_w is the same shape: line 148 clears axi_wdone unconditionally,
line 192 removes the clear from the axi_wdone & bready branch.
Since rvalid = axi_rdone and rd_ack is itself a one-cycle pulse
(rd_ack <= rd_ack_d0, rd_ack_d0 = rd_req), RVALID is asserted for exactly one
cycle. This violates AXI4-Lite A3.2.1 — "once VALID is asserted it must remain
asserted until the handshake occurs" — which the generator quotes in the comment
just above expand_bus_r.
The deadlock follows because axi_arset is cleared only by axi_rdone & rready.
If RREADY is low in that one cycle, axi_arset stays set, arready = ~axi_arset
is low forever, no further AR is accepted, so rd_ack never fires again and
axi_rdone can never be set again. Write side identical via axi_awset /
axi_wset.
Reproducer
Two identical maps differing only in the bus-error option:
memory-map:
bus: axi4-lite-32
name: handshake_err # and handshake_noerr, with bus-error: false
x-hdl:
bus-granularity: byte
wmask: true
bus-error: true
children:
- reg:
name: scratch
width: 32
access: rw
cheby --hdl sv --gen-hdl=handshake_err.sv -i handshake_err.cheby
cheby --hdl sv --gen-hdl=handshake_noerr.sv -i handshake_noerr.cheby
The port lists of the two generated modules are identical, so the same
testbench drives both. It holds RREADY low for 10 cycles after the AR is
accepted, then raises it, and checks that the transaction completes; then it
issues a second read to check the channel is still alive. The same is done for
BREADY. There is one monitor, taken straight from A3.2.1: once VALID is high it
must stay high until VALID && READY are sampled together.
Result — Vivado xsim 2024.2
bus-error: true:
=== DUT: handshake_err (bus-error: true) ===
[95000] INFO during reset: RVALID=1 BVALID=1 RRESP=10 BRESP=10
[145000] TEST 1: read with RREADY held low for 10 cycles
[195000] FAIL A3.2.1: RVALID deasserted without an RREADY handshake
[255000] INFO: RVALID observed high while RREADY was low: 1
[2255000] FAIL: read response never completed after RREADY was raised
[2305000] TEST 2: second read, RREADY high throughout
[4320000] FAIL: ARREADY never asserted for the second read - AR channel is wedged
[4375000] TEST 3: write with BREADY held low for 10 cycles
[4425000] FAIL A3.2.1: BVALID deasserted without a BREADY handshake
[4485000] INFO: BVALID observed high while BREADY was low: 1
[6485000] FAIL: write response never completed after BREADY was raised
[6535000] TEST 4: second write, BREADY high throughout
[8550000] FAIL: AWREADY/WREADY never asserted for the second write - W channel is wedged
RESULT: FAIL (6 errors)
bus-error: false, same testbench, same stimulus:
=== DUT: handshake_noerr (bus-error: false) ===
[95000] INFO during reset: RVALID=0 BVALID=0 RRESP=0 BRESP=0
[145000] TEST 1: read with RREADY held low for 10 cycles
[255000] INFO: RVALID observed high while RREADY was low: 1
[265000] PASS: read response completed, RDATA=0x00000000 RRESP=0
[315000] TEST 2: second read, RREADY high throughout
[365000] PASS: second read completed
[415000] TEST 3: write with BREADY held low for 10 cycles
[525000] INFO: BVALID observed high while BREADY was low: 1
[535000] PASS: write response completed, BRESP=0
[585000] TEST 4: second write, BREADY high throughout
[635000] PASS: second write completed
RESULT: PASS (0 errors)
Why it is easy to miss
Any master that raises RREADY/BREADY before the response appears and holds it
never sees this, which covers a lot of BFM configurations. It needs an
interconnect that throttles readiness, or a protocol checker.
Secondary, same branch
Visible in the first line of each log above: with bus-error the reset branch
sets axi_wdone/axi_rdone to 1 with SLVERR, so BVALID and RVALID are
asserted during reset and then drop on reset release with no handshake, and
while reset is held the slave presents responses with no preceding AW/AR. The
comment says this is deliberate, so flagging rather than reporting — but it is
in the same branch and may be worth revisiting alongside the above.
Testbench
tb_axi4lite_handshake.sv
// Testbench for the AXI4-Lite VALID/READY handshake of a Cheby-generated slave.
//
// Oracle is AXI4-Lite, not the generated RTL:
// A3.2.1 - "once VALID is asserted it must remain asserted until the
// handshake occurs".
//
// The same testbench is run against a map generated with bus-error: true and
// one generated with bus-error: false. The two maps are otherwise identical,
// so a difference in result is attributable to that option alone.
//
// Compile-time selection of the device under test:
// -d DUT_HANDSHAKE_ERR -> handshake_err (bus-error: true)
// -d DUT_HANDSHAKE_NOERR -> handshake_noerr (bus-error: false)
`timescale 1ns/1ps
module tb_axi4lite_handshake;
localparam int TIMEOUT_CYCLES = 200;
localparam int STALL_CYCLES = 10;
logic aclk = 1'b0;
always #5 aclk = ~aclk;
logic areset_n = 1'b0;
logic awvalid = 1'b0;
wire awready;
logic [1:0] awaddr = 2'b00;
logic [2:0] awprot = 3'b000;
logic wvalid = 1'b0;
wire wready;
logic [31:0] wdata = 32'h0;
logic [3:0] wstrb = 4'hF;
wire bvalid;
logic bready = 1'b0;
wire [1:0] bresp;
logic arvalid = 1'b0;
wire arready;
logic [1:0] araddr = 2'b00;
logic [2:0] arprot = 3'b000;
wire rvalid;
logic rready = 1'b0;
wire [31:0] rdata;
wire [1:0] rresp;
wire [31:0] scratch_o;
`ifdef DUT_HANDSHAKE_NOERR
handshake_noerr
`else
handshake_err
`endif
dut (
.aclk (aclk),
.areset_n (areset_n),
.awvalid (awvalid),
.awready (awready),
.awaddr (awaddr),
.awprot (awprot),
.wvalid (wvalid),
.wready (wready),
.wdata (wdata),
.wstrb (wstrb),
.bvalid (bvalid),
.bready (bready),
.bresp (bresp),
.arvalid (arvalid),
.arready (arready),
.araddr (araddr),
.arprot (arprot),
.rvalid (rvalid),
.rready (rready),
.rdata (rdata),
.rresp (rresp),
.scratch_o (scratch_o)
);
int errors = 0;
bit monitor_armed = 1'b0;
// ---------------------------------------------------------------------
// A3.2.1 monitor: VALID, once asserted, must hold until the handshake.
// ---------------------------------------------------------------------
logic rvalid_q, rready_q, bvalid_q, bready_q;
always_ff @(posedge aclk) begin
if (monitor_armed) begin
if (rvalid_q && !rready_q && !rvalid) begin
errors++;
$display("[%0t] FAIL A3.2.1: RVALID deasserted without an RREADY handshake", $time);
end
if (bvalid_q && !bready_q && !bvalid) begin
errors++;
$display("[%0t] FAIL A3.2.1: BVALID deasserted without a BREADY handshake", $time);
end
end
rvalid_q <= rvalid;
rready_q <= rready;
bvalid_q <= bvalid;
bready_q <= bready;
end
// ---------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------
task automatic step(input int n = 1);
repeat (n) @(posedge aclk);
endtask
// Drive an AR and wait for the slave to accept it. Returns 0 on timeout.
task automatic ar_issue(input logic [1:0] addr, output bit ok);
int cyc;
@(negedge aclk);
araddr = addr;
arvalid = 1'b1;
ok = 1'b0;
cyc = 0;
while (cyc < TIMEOUT_CYCLES) begin
@(posedge aclk);
if (arready) begin
ok = 1'b1;
break;
end
cyc++;
end
@(negedge aclk);
arvalid = 1'b0;
endtask
// Wait for the R response to complete (RVALID && RREADY both high).
task automatic r_complete(output bit ok);
int cyc;
ok = 1'b0;
cyc = 0;
while (cyc < TIMEOUT_CYCLES) begin
@(posedge aclk);
if (rvalid && rready) begin
ok = 1'b1;
break;
end
cyc++;
end
endtask
task automatic aw_w_issue(input logic [1:0] addr, input logic [31:0] data, output bit ok);
int cyc;
@(negedge aclk);
awaddr = addr;
wdata = data;
awvalid = 1'b1;
wvalid = 1'b1;
ok = 1'b0;
cyc = 0;
while (cyc < TIMEOUT_CYCLES) begin
@(posedge aclk);
if (awready && wready) begin
ok = 1'b1;
break;
end
cyc++;
end
@(negedge aclk);
awvalid = 1'b0;
wvalid = 1'b0;
endtask
task automatic b_complete(output bit ok);
int cyc;
ok = 1'b0;
cyc = 0;
while (cyc < TIMEOUT_CYCLES) begin
@(posedge aclk);
if (bvalid && bready) begin
ok = 1'b1;
break;
end
cyc++;
end
endtask
// ---------------------------------------------------------------------
// Sequence
// ---------------------------------------------------------------------
bit ok;
bit rvalid_seen_while_stalled;
bit bvalid_seen_while_stalled;
bit rvalid_during_reset;
bit bvalid_during_reset;
initial begin
`ifdef DUT_HANDSHAKE_NOERR
$display("=== DUT: handshake_noerr (bus-error: false) ===");
`else
$display("=== DUT: handshake_err (bus-error: true) ===");
`endif
areset_n = 1'b0;
rready = 1'b0;
bready = 1'b0;
step(10);
// Recorded, not failed: the reset-time behaviour is separately documented.
rvalid_during_reset = rvalid;
bvalid_during_reset = bvalid;
$display("[%0t] INFO during reset: RVALID=%0b BVALID=%0b RRESP=%0b BRESP=%0b",
$time, rvalid, bvalid, rresp, bresp);
@(negedge aclk);
areset_n = 1'b1;
step(5);
monitor_armed = 1'b1;
// -----------------------------------------------------------------
// Test 1 - read while RREADY is held low, then accept.
// -----------------------------------------------------------------
$display("[%0t] TEST 1: read with RREADY held low for %0d cycles", $time, STALL_CYCLES);
ar_issue(2'b00, ok);
if (!ok) begin
errors++;
$display("[%0t] FAIL: ARREADY never asserted for the first read", $time);
end
rvalid_seen_while_stalled = 1'b0;
repeat (STALL_CYCLES) begin
@(posedge aclk);
if (rvalid) rvalid_seen_while_stalled = 1'b1;
end
$display("[%0t] INFO: RVALID observed high while RREADY was low: %0b",
$time, rvalid_seen_while_stalled);
@(negedge aclk);
rready = 1'b1;
r_complete(ok);
if (!ok) begin
errors++;
$display("[%0t] FAIL: read response never completed after RREADY was raised", $time);
end else begin
$display("[%0t] PASS: read response completed, RDATA=0x%08x RRESP=%0b", $time, rdata, rresp);
end
@(negedge aclk);
rready = 1'b0;
// -----------------------------------------------------------------
// Test 2 - liveness. A second read with RREADY high throughout must
// still be accepted; if AR is wedged, ARREADY never returns.
// -----------------------------------------------------------------
step(5);
$display("[%0t] TEST 2: second read, RREADY high throughout", $time);
@(negedge aclk);
rready = 1'b1;
ar_issue(2'b00, ok);
if (!ok) begin
errors++;
$display("[%0t] FAIL: ARREADY never asserted for the second read - AR channel is wedged", $time);
end else begin
r_complete(ok);
if (!ok) begin
errors++;
$display("[%0t] FAIL: second read response never completed", $time);
end else begin
$display("[%0t] PASS: second read completed", $time);
end
end
@(negedge aclk);
rready = 1'b0;
// -----------------------------------------------------------------
// Test 3 - write while BREADY is held low, then accept.
// -----------------------------------------------------------------
step(5);
$display("[%0t] TEST 3: write with BREADY held low for %0d cycles", $time, STALL_CYCLES);
aw_w_issue(2'b00, 32'hA5A5_1234, ok);
if (!ok) begin
errors++;
$display("[%0t] FAIL: AWREADY/WREADY never asserted for the first write", $time);
end
bvalid_seen_while_stalled = 1'b0;
repeat (STALL_CYCLES) begin
@(posedge aclk);
if (bvalid) bvalid_seen_while_stalled = 1'b1;
end
$display("[%0t] INFO: BVALID observed high while BREADY was low: %0b",
$time, bvalid_seen_while_stalled);
@(negedge aclk);
bready = 1'b1;
b_complete(ok);
if (!ok) begin
errors++;
$display("[%0t] FAIL: write response never completed after BREADY was raised", $time);
end else begin
$display("[%0t] PASS: write response completed, BRESP=%0b", $time, bresp);
end
@(negedge aclk);
bready = 1'b0;
// -----------------------------------------------------------------
// Test 4 - write liveness.
// -----------------------------------------------------------------
step(5);
$display("[%0t] TEST 4: second write, BREADY high throughout", $time);
@(negedge aclk);
bready = 1'b1;
aw_w_issue(2'b00, 32'h0000_BEEF, ok);
if (!ok) begin
errors++;
$display("[%0t] FAIL: AWREADY/WREADY never asserted for the second write - W channel is wedged", $time);
end else begin
b_complete(ok);
if (!ok) begin
errors++;
$display("[%0t] FAIL: second write response never completed", $time);
end else begin
$display("[%0t] PASS: second write completed", $time);
end
end
step(5);
$display("----------------------------------------------------------");
if (errors == 0)
$display("RESULT: PASS (0 errors)");
else
$display("RESULT: FAIL (%0d errors)", errors);
$display("----------------------------------------------------------");
$finish;
end
// Global watchdog so a wedged DUT cannot hang the run.
initial begin
#100000;
$display("RESULT: FAIL (global timeout - the DUT is wedged)");
$finish;
end
endmodule
Happy to contribute this as a testcase, and a fix, if that is useful.
With
bus-error: true, BVALID and RVALID are one-cycle pulses that do not waitfor BREADY/RREADY. If the master is not ready in that cycle the response is lost
and the slave stops accepting new requests permanently — it is wedged until
reset.
Follow-up to #7, same feature as #68.
Observed on
master@ 2026fce (2026-05-20) and cheby 1.7.dev0.File:
proto/cheby/hdl/axi4litebus.py.Cause
In
expand_bus_r, the correct clear is disabled whenbus_erroris set:and an unconditional clear is added in its place:
expand_bus_wis the same shape: line 148 clearsaxi_wdoneunconditionally,line 192 removes the clear from the
axi_wdone & breadybranch.Since
rvalid = axi_rdoneandrd_ackis itself a one-cycle pulse(
rd_ack <= rd_ack_d0,rd_ack_d0 = rd_req), RVALID is asserted for exactly onecycle. This violates AXI4-Lite A3.2.1 — "once VALID is asserted it must remain
asserted until the handshake occurs" — which the generator quotes in the comment
just above
expand_bus_r.The deadlock follows because
axi_arsetis cleared only byaxi_rdone & rready.If RREADY is low in that one cycle,
axi_arsetstays set,arready = ~axi_arsetis low forever, no further AR is accepted, so
rd_acknever fires again andaxi_rdonecan never be set again. Write side identical viaaxi_awset/axi_wset.Reproducer
Two identical maps differing only in the
bus-erroroption:The port lists of the two generated modules are identical, so the same
testbench drives both. It holds RREADY low for 10 cycles after the AR is
accepted, then raises it, and checks that the transaction completes; then it
issues a second read to check the channel is still alive. The same is done for
BREADY. There is one monitor, taken straight from A3.2.1: once VALID is high it
must stay high until VALID && READY are sampled together.
Result — Vivado xsim 2024.2
bus-error: true:bus-error: false, same testbench, same stimulus:Why it is easy to miss
Any master that raises RREADY/BREADY before the response appears and holds it
never sees this, which covers a lot of BFM configurations. It needs an
interconnect that throttles readiness, or a protocol checker.
Secondary, same branch
Visible in the first line of each log above: with
bus-errorthe reset branchsets
axi_wdone/axi_rdoneto1with SLVERR, so BVALID and RVALID areasserted during reset and then drop on reset release with no handshake, and
while reset is held the slave presents responses with no preceding AW/AR. The
comment says this is deliberate, so flagging rather than reporting — but it is
in the same branch and may be worth revisiting alongside the above.
Testbench
tb_axi4lite_handshake.sv
Happy to contribute this as a testcase, and a fix, if that is useful.