Skip to content

AXI4-Lite: bus-error makes BVALID/RVALID single-cycle pulses and can deadlock the slave #86

Description

@dsiquantum

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions