From c137f9744eb260d564dcd17f4b74d1bd0835af49 Mon Sep 17 00:00:00 2001 From: Daniel Keller Date: Wed, 8 Jul 2026 15:35:04 +0200 Subject: [PATCH 1/2] frontend: Fix reg next_id launch deadlock via pending latch The PeakRDL external-register handshake masks the CPUIF read req the cycle after an ungranted next_id read (external_pending), collapsing next_id.req to a single-cycle pulse. The launch rd_ack (req & ~req_is_wr & arb_ready) then can never fire once req is masked, external_pending never clears, and the whole reg block freezes. Hold the launch across the stall in nxt_read_pending_q: set on the seen read, clear on arb_ready, and drive arb_valid, rd_ack and stream_idx_o from the latch instead of the maskable req. This replicates the canonical reggen held-handshake (the stable .re strobe) exactly: the read stalls until the arbiter grants, completes the cycle arb_ready is high with no added latency, and returns this launch's id. status/done_id are untouched (their ungated rd_ack never latches external_pending). --- src/frontend/reg/tpl/idma_reg.sv.tpl | 29 +++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/frontend/reg/tpl/idma_reg.sv.tpl b/src/frontend/reg/tpl/idma_reg.sv.tpl index a1ea9175..d8334661 100644 --- a/src/frontend/reg/tpl/idma_reg.sv.tpl +++ b/src/frontend/reg/tpl/idma_reg.sv.tpl @@ -99,11 +99,15 @@ module idma_${identifier} #( logic [NumRegs-1:0] arb_valid; logic [NumRegs-1:0] arb_ready; + // hold the next_id launch across the read-stall (req is masked mid-stall) + logic [NumRegs-1:0][NumStreams-1:0] nxt_read_seen; + logic [NumRegs-1:0][NumStreams-1:0] nxt_read_pending_q; + always_comb begin stream_idx_o = '0; for (int r = 0; r < NumRegs; r++) begin for (int c = 0; c < NumStreams; c++) begin - if (dma_reg2hw[r].next_id[c].req && !dma_reg2hw[r].next_id[c].req_is_wr) begin + if (nxt_read_seen[r][c] || nxt_read_pending_q[r][c]) begin stream_idx_o = c; end end @@ -175,14 +179,26 @@ module idma_${identifier} #( .hwif_in ( dma_hw2reg [i] ) ); - logic read_happens; - // launch-stall: hold the reg read-ack until the arbiter accepts the request - // (protocol-agnostic — driven into hwif rd_ack below, see gen_hw2reg_connections) + // next_id launch-pending latch: set on read, cleared on grant + for (genvar c = 0; c < NumStreams; c++) begin : gen_nxt_read_pending + assign nxt_read_seen[i][c] = dma_reg2hw[i].next_id[c].req + & ~dma_reg2hw[i].next_id[c].req_is_wr; + always_ff @(posedge clk_i or negedge rst_ni) begin : proc_nxt_read_pending + if (!rst_ni) begin + nxt_read_pending_q[i][c] <= 1'b0; + end else if (nxt_read_pending_q[i][c] & arb_ready[i]) begin + nxt_read_pending_q[i][c] <= 1'b0; + end else if (nxt_read_seen[i][c] & ~arb_ready[i]) begin + nxt_read_pending_q[i][c] <= 1'b1; + end + end + end + logic read_happens; always_comb begin : proc_launch read_happens = 1'b0; for (int c = 0; c < NumStreams; c++) begin - read_happens |= dma_reg2hw[i].next_id[c].req & ~dma_reg2hw[i].next_id[c].req_is_wr; + read_happens |= nxt_read_seen[i][c] | nxt_read_pending_q[i][c]; end arb_valid[i] = read_happens; end @@ -261,8 +277,7 @@ module idma_${identifier} #( assign dma_hw2reg[i].status[c].rd_ack = dma_reg2hw[i].status[c].req & ~dma_reg2hw[i].status[c].req_is_wr; assign dma_hw2reg[i].next_id[c].rd_data.next_id = next_id_i; - assign dma_hw2reg[i].next_id[c].rd_ack = dma_reg2hw[i].next_id[c].req - & ~dma_reg2hw[i].next_id[c].req_is_wr + assign dma_hw2reg[i].next_id[c].rd_ack = (nxt_read_seen[i][c] | nxt_read_pending_q[i][c]) & arb_ready[i]; assign dma_hw2reg[i].done_id[c].rd_data.done_id = done_id_i[c]; assign dma_hw2reg[i].done_id[c].rd_ack = dma_reg2hw[i].done_id[c].req From 13ba549e06dfb1ed72d9aef0e9b2a0474ab34598 Mon Sep 17 00:00:00 2001 From: Daniel Keller Date: Wed, 8 Jul 2026 15:35:05 +0200 Subject: [PATCH 2/2] test: Add self-checking reg-frontend testbench with deadlock watchdog The register frontend had no directed testbench. tb_idma_reg_frontend drives idma_reg32_3d's apb4-flat config slave, owns the transfer-id counter via idma_transfer_id_gen, and models a controllable backend on req_ready_i so the next_id launch handshake can be stalled at will. A per-read cycle watchdog fatals if a next_id APB read fails to complete under backpressure, so the test detects the launch deadlock: it passes against the pending-latch fix and fatals (DEADLOCK) against the pre-fix template. Covers a basic launch, the backpressure/deadlock regression, multi-stream stream_idx hold under stall (NumStreams=2), and back-to-back monotonic ids. Wired into the idma_test Bender target and a new idma_sim_tb_idma_reg_frontend make target that runs both NumStreams=1 and NumStreams=2 in Questa. --- Bender.yml | 1 + idma.mk | 8 + test/tb_idma_reg_frontend.sv | 566 +++++++++++++++++++++++++++++++++++ 3 files changed, 575 insertions(+) create mode 100644 test/tb_idma_reg_frontend.sv diff --git a/Bender.yml b/Bender.yml index e5ff5e8d..91e938d6 100644 --- a/Bender.yml +++ b/Bender.yml @@ -118,6 +118,7 @@ sources: # Level 1 - test/frontend/tb_idma_desc64_top.sv - test/frontend/tb_idma_desc64_bench.sv + - test/tb_idma_reg_frontend.sv - test/future/idma_tb_per2axi.sv - test/future/TLToAXI4.v - test/midend/tb_idma_nd_midend.sv diff --git a/idma.mk b/idma.mk index 8e6bb18f..f6b4db83 100644 --- a/idma.mk +++ b/idma.mk @@ -386,6 +386,14 @@ idma_sim_tb_idma_nd_midend_b2b: $(IDMA_VSIM_DIR)/compile.tcl cd $(IDMA_VSIM_DIR); $(VSIM) -c -do "source compile.tcl; quit" cd $(IDMA_VSIM_DIR); $(VSIM) -c -t 1ps -voptargs=+acc tb_idma_nd_midend_b2b -do "run -all; quit" +# Self-checking reg-frontend regression (NumStreams 1 and 2); Test 2 gates the +# next_id launch deadlock. Run with the Questa SEPP wrapper. +.PHONY: idma_sim_tb_idma_reg_frontend +idma_sim_tb_idma_reg_frontend: $(IDMA_VSIM_DIR)/compile.tcl + cd $(IDMA_VSIM_DIR); $(VSIM) -c -do "source compile.tcl; quit" + cd $(IDMA_VSIM_DIR); $(VSIM) -c -t 1ps -voptargs=+acc -gNumStreams=1 tb_idma_reg_frontend -do "run -all; quit" + cd $(IDMA_VSIM_DIR); $(VSIM) -c -t 1ps -voptargs=+acc -gNumStreams=2 tb_idma_reg_frontend -do "run -all; quit" + .PHONY: idma_sim_tb_idma_transpose_b2b idma_sim_tb_idma_transpose_b2b: $(IDMA_VSIM_DIR)/compile.tcl cd $(IDMA_VSIM_DIR); $(VSIM) -c -do "source compile.tcl; quit" diff --git a/test/tb_idma_reg_frontend.sv b/test/tb_idma_reg_frontend.sv new file mode 100644 index 00000000..cd37bd79 --- /dev/null +++ b/test/tb_idma_reg_frontend.sv @@ -0,0 +1,566 @@ +// Copyright 2025 ETH Zurich and University of Bologna. +// Solderpad Hardware License, Version 0.51, see LICENSE for details. +// SPDX-License-Identifier: SHL-0.51 + +// Authors: +// - Daniel Keller + +// Self-checking testbench for the iDMA register frontend (idma_reg32_3d, apb4-flat). +// Drives the APB config slave against a controllable backend stub and gates the +// next_id launch deadlock with a per-read watchdog. + +`include "apb/typedef.svh" +`include "idma/typedef.svh" + +module tb_idma_reg_frontend import idma_pkg::*; #( + // number of streams the elaborated DUT exposes (checked at instantiation) + parameter int unsigned NumStreams = 32'd1 +); + + // -------------------------------------------------------------------------- + // Parameters + // -------------------------------------------------------------------------- + localparam time TCK = 10ns; + localparam int unsigned CfgAddrWidth = 32'd32; + localparam int unsigned CfgDataWidth = 32'd32; + localparam int unsigned CfgStrbWidth = CfgDataWidth / 32'd8; + localparam int unsigned IdCounterWidth = 32'd32; + localparam int unsigned NumRegs = 32'd1; + // idma data-path (reg32_3d: 32-bit data, 3 ND dims) + localparam int unsigned AddrWidth = 32'd32; + localparam int unsigned DataWidth = 32'd32; + localparam int unsigned NumDim = 32'd3; + localparam int unsigned RepWidth = 32'd32; + // watchdog bound: any next_id APB read that does not complete within this many + // config-clock cycles is a deadlock (the fix completes in a handful of cycles). + localparam int unsigned DeadlockCycles = 32'd2000; + + // register map (idma_reg32_3d_addrmap_pkg): base + per-stream stride 0x4 + localparam logic [31:0] REG_CONF = 32'h0000_0000; + localparam logic [31:0] REG_STATUS0 = 32'h0000_0004; + localparam logic [31:0] REG_NEXT_ID0 = 32'h0000_0044; + localparam logic [31:0] REG_DONE_ID0 = 32'h0000_0084; + localparam logic [31:0] REG_DST_ADDR = 32'h0000_00D0; + localparam logic [31:0] REG_SRC_ADDR = 32'h0000_00D4; + localparam logic [31:0] REG_LENGTH = 32'h0000_00D8; + + function automatic logic [31:0] reg_next_id(input int unsigned s); + return REG_NEXT_ID0 + 32'(s) * 32'h4; + endfunction + function automatic logic [31:0] reg_done_id(input int unsigned s); + return REG_DONE_ID0 + 32'(s) * 32'h4; + endfunction + + // -------------------------------------------------------------------------- + // Types + // -------------------------------------------------------------------------- + typedef logic [CfgAddrWidth-1:0] cfg_addr_t; + typedef logic [CfgDataWidth-1:0] cfg_data_t; + typedef logic [CfgStrbWidth-1:0] cfg_strb_t; + + `APB_TYPEDEF_REQ_T(cfg_apb_req_t, cfg_addr_t, cfg_data_t, cfg_strb_t) + `APB_TYPEDEF_RESP_T(cfg_apb_rsp_t, cfg_data_t) + + localparam int unsigned StrbWidth = DataWidth / 32'd8; + localparam int unsigned OffsetWidth = $clog2(StrbWidth); + typedef logic [AddrWidth-1:0] addr_t; + typedef logic [StrbWidth-1:0] strb_t; + typedef logic [OffsetWidth-1:0] offset_t; + typedef logic [RepWidth-1:0] strides_t; + typedef logic [RepWidth-1:0] reps_t; + typedef logic [AddrWidth-1:0] tf_len_t; + typedef logic idma_user_t; + + `IDMA_TYPEDEF_OPTIONS_T(options_t, logic) + `IDMA_TYPEDEF_REQ_T(idma_req_t, tf_len_t, addr_t, options_t, idma_user_t) + `IDMA_TYPEDEF_D_REQ_T(idma_d_req_t, reps_t, strides_t) + `IDMA_TYPEDEF_ND_REQ_T(idma_nd_req_t, idma_req_t, idma_d_req_t) + + typedef logic [IdCounterWidth-1:0] cnt_width_t; + + // -------------------------------------------------------------------------- + // Clock / reset + // -------------------------------------------------------------------------- + logic clk; + logic rst_n; + + initial begin + clk = 1'b0; + forever #(TCK/2) clk = ~clk; + end + + // -------------------------------------------------------------------------- + // DUT nets + // -------------------------------------------------------------------------- + cfg_apb_req_t [NumRegs-1:0] apb_req; + cfg_apb_rsp_t [NumRegs-1:0] apb_rsp; + + idma_nd_req_t dma_req; + logic req_valid; + logic req_ready; // driven by the backend stub + cnt_width_t next_id; // from the id gen + logic [(NumStreams>1?$clog2(NumStreams):1)-1:0] stream_idx; + cnt_width_t [NumStreams-1:0] done_id; + idma_busy_t [NumStreams-1:0] busy; + logic [NumStreams-1:0] midend_busy; + + // -------------------------------------------------------------------------- + // Transfer-id generator (owns the next/completed counters). Reset next=2. + // issue on an accepted launch, retire on a modeled backend completion. + // -------------------------------------------------------------------------- + logic issue; + logic retire; + + idma_transfer_id_gen #( + .IdWidth ( IdCounterWidth ) + ) i_id_gen ( + .clk_i ( clk ), + .rst_ni ( rst_n ), + .issue_i ( issue ), + .retire_i ( retire ), + .next_o ( next_id ), + .completed_o ( done_id[0] ) + ); + // multi-stream: id gen models stream 0; other streams share the same completed + // counter here (the DUT only compares per-stream done_id on read-back). + for (genvar s = 1; s < NumStreams; s++) begin : gen_done_other + assign done_id[s] = done_id[0]; + end + + assign issue = req_valid & req_ready; + + // -------------------------------------------------------------------------- + // DUT + // -------------------------------------------------------------------------- + idma_reg32_3d #( + .NumRegs ( NumRegs ), + .NumStreams ( NumStreams ), + .IdCounterWidth ( IdCounterWidth ), + .apb_req_t ( cfg_apb_req_t ), + .apb_rsp_t ( cfg_apb_rsp_t ), + .dma_req_t ( idma_nd_req_t ) + ) i_dut ( + .clk_i ( clk ), + .rst_ni ( rst_n ), + .dma_ctrl_req_i ( apb_req ), + .dma_ctrl_rsp_o ( apb_rsp ), + .dma_req_o ( dma_req ), + .req_valid_o ( req_valid ), + .req_ready_i ( req_ready ), + .next_id_i ( next_id ), + .stream_idx_o ( stream_idx ), + .done_id_i ( done_id ), + .busy_i ( busy ), + .midend_busy_i ( midend_busy ) + ); + + // -------------------------------------------------------------------------- + // Backend stub. `req_ready` is directly controllable by the tests. On each + // accepted launch (req_valid & req_ready) the emitted ND request is captured + // and enqueued with a retire-deadline; when its timer expires it is retired in + // order via the id gen so `done_id` advances. `busy`/`midend_busy` follow the + // outstanding count. A single-entry timer is enough — launches are retired FIFO + // and only re-armed once the previous one drains, which keeps ordering exact. + // -------------------------------------------------------------------------- + idma_nd_req_t captured_q[$]; // every accepted launch, for self-check + int unsigned outstanding; // in-flight (not yet retired) launches + int unsigned retire_delay; // clocks a launch stays in flight + logic backend_auto_retire; // if 0, retirement is suppressed + int retire_timer; // -1 == no launch currently timing out + + assign busy[0] = (outstanding != 0) ? '1 : '0; + assign midend_busy[0] = (outstanding != 0) ? 1'b1 : 1'b0; + for (genvar s = 1; s < NumStreams; s++) begin : gen_busy_other + assign busy[s] = busy[0]; + assign midend_busy[s] = midend_busy[0]; + end + + initial begin + outstanding = 0; + retire_timer = -1; + retire_delay = 3; + backend_auto_retire = 1'b1; + retire = 1'b0; + end + + // Unified backend model: capture launches, count outstanding, and retire FIFO. + // `accept` and `do_retire` cannot both change `outstanding` inconsistently + // because all updates happen in this one process. + always @(posedge clk) begin + automatic bit accept = rst_n && req_valid && req_ready; + automatic bit do_retire = 1'b0; + + retire <= 1'b0; + if (!rst_n) begin + outstanding <= 0; + retire_timer <= -1; + end else begin + // 1) capture an accepted launch + if (accept) + captured_q.push_back(dma_req); + + // 2) advance / fire the retire timer + if (backend_auto_retire && retire_timer == 0) begin + retire <= 1'b1; + do_retire = 1'b1; + retire_timer <= -1; // re-armed below if launches remain + end else if (retire_timer > 0) begin + retire_timer <= retire_timer - 1; + end + + // 3) update outstanding count (+accept, -retire) + outstanding <= outstanding + (accept ? 1 : 0) - (do_retire ? 1 : 0); + + // 4) arm the timer whenever a launch is waiting and none is timing out + if (backend_auto_retire) begin + automatic int unsigned next_out = + outstanding + (accept ? 1 : 0) - (do_retire ? 1 : 0); + if ((retire_timer < 0 || do_retire) && next_out > 0) + retire_timer <= retire_delay; + end + end + end + + // -------------------------------------------------------------------------- + // Watchdog: fatal if a next_id APB read stays outstanding too long. + // `nxt_read_active` is raised by apb_read on a next_id address and cleared + // when pready completes it. + // -------------------------------------------------------------------------- + logic nxt_read_active; + int unsigned nxt_read_watchdog; + initial begin + nxt_read_active = 1'b0; + nxt_read_watchdog = 0; + end + always @(posedge clk) begin + if (!rst_n) begin + nxt_read_watchdog <= 0; + end else if (nxt_read_active) begin + nxt_read_watchdog <= nxt_read_watchdog + 1; + if (nxt_read_watchdog > DeadlockCycles) begin + $fatal(1, "DEADLOCK: next_id read did not complete within %0d cycles", + DeadlockCycles); + end + end else begin + nxt_read_watchdog <= 0; + end + end + + // -------------------------------------------------------------------------- + // APB driver + // -------------------------------------------------------------------------- + task automatic apb_idle(); + apb_req[0].psel = 1'b0; + apb_req[0].penable = 1'b0; + apb_req[0].pwrite = 1'b0; + apb_req[0].paddr = '0; + apb_req[0].pwdata = '0; + apb_req[0].pstrb = '0; + apb_req[0].pprot = '0; + endtask + + // Strict APB4 master. Signals are driven just after a negedge (stable before the + // sampling posedge); pready/prdata are sampled at the posedge of the ACCESS phase. + // On the posedge that pready is high the transaction retires and the bus returns to + // IDLE at that same edge (psel/penable deasserted), then one guaranteed idle cycle + // is inserted before the next transaction. This is what keeps the reg_top APB FSM + // (which starts a transfer on psel, and clears is_active on rd/wr_ack) from being + // re-triggered into a spurious second access — one APB transaction == one CPUIF req. + task automatic apb_xact(input bit write, + input logic [31:0] addr, + input logic [31:0] wdata, + output logic [31:0] rdata, + input bit is_next_id = 1'b0); + // SETUP phase: psel high, penable low, for one cycle + @(negedge clk); + apb_req[0].psel = 1'b1; + apb_req[0].penable = 1'b0; + apb_req[0].pwrite = write; + apb_req[0].paddr = addr; + apb_req[0].pwdata = wdata; + apb_req[0].pstrb = write ? '1 : '0; + apb_req[0].pprot = '0; + if (is_next_id) nxt_read_active = 1'b1; + + // ACCESS phase: raise penable, then wait for pready at the posedge. The bus is + // held stable (psel & penable & addr/data) across every stalled cycle — exactly + // the sustained handshake a deadlocked launch would hang on. + @(negedge clk); + apb_req[0].penable = 1'b1; + forever begin + @(posedge clk); + if (apb_rsp[0].pready) begin + rdata = apb_rsp[0].prdata; // sample in the completing cycle + break; + end + end + // retire the transaction: return to IDLE immediately (no extra held posedge) + apb_idle(); + if (is_next_id) nxt_read_active = 1'b0; + // mandatory idle cycle so the FSM's is_active fully drops with psel low + @(negedge clk); + endtask + + task automatic apb_write(input logic [31:0] addr, input logic [31:0] data); + logic [31:0] dummy; + apb_xact(1'b1, addr, data, dummy); + endtask + + // APB4 read that RESPECTS pready: not complete until pready is high. If + // `is_next_id`, the watchdog net tracks the outstanding read so a frozen launch + // trips $fatal. + task automatic apb_read(input logic [31:0] addr, + output logic [31:0] data, + input bit is_next_id = 1'b0); + apb_xact(1'b0, addr, 32'h0, data, is_next_id); + endtask + + // -------------------------------------------------------------------------- + // High-level helpers + // -------------------------------------------------------------------------- + task automatic program_transfer(input logic [31:0] src, + input logic [31:0] dst, + input logic [31:0] len); + // conf: plain 1D incremental copy, ND disabled + apb_write(REG_CONF, 32'h0); + apb_write(REG_SRC_ADDR, src); + apb_write(REG_DST_ADDR, dst); + apb_write(REG_LENGTH, len); + endtask + + // launch: read next_id (this is the transfer trigger); returns the id + task automatic launch(output logic [31:0] id, input int unsigned s = 0); + apb_read(reg_next_id(s), id, .is_next_id(1'b1)); + endtask + + task automatic read_done(output logic [31:0] id, input int unsigned s = 0); + apb_read(reg_done_id(s), id); + endtask + + // poll done_id until it reaches `id` (or a bounded number of tries) + task automatic poll_done(input logic [31:0] id, input int unsigned s = 0); + logic [31:0] d; + int unsigned tries; + tries = 0; + do begin + read_done(d, s); + tries++; + if (tries > 1000) + $fatal(1, "poll_done: done_id never reached %0d (last %0d)", id, d); + end while (d != id); + endtask + + // -------------------------------------------------------------------------- + // Bookkeeping for the test program + // -------------------------------------------------------------------------- + int unsigned errors; + int unsigned checks; + + task automatic check_eq(input logic [63:0] got, input logic [63:0] exp, + input string msg); + checks++; + if (got !== exp) begin + errors++; + $error("[FAIL] %s: got 0x%0h, expected 0x%0h", msg, got, exp); + end else begin + $display("[ ok ] %s = 0x%0h", msg, got); + end + endtask + + // -------------------------------------------------------------------------- + // Test program + // -------------------------------------------------------------------------- + logic [31:0] id0, id1, id2, id3; + logic [31:0] exp_id; // id gen next_o snapshot before a launch + logic [31:0] prev_id; // last returned id, for monotonic checks + + initial begin : test + errors = 0; + checks = 0; + apb_idle(); + req_ready = 1'b1; + + // reset + rst_n = 1'b0; + repeat (5) @(negedge clk); + rst_n = 1'b1; + repeat (2) @(negedge clk); + + $display("====================================================="); + $display(" tb_idma_reg_frontend (NumStreams=%0d)", NumStreams); + $display("====================================================="); + + // ------------------------------------------------------------------ + // Test 1 — Basic launch (backend ready) + // ------------------------------------------------------------------ + $display("\n--- Test 1: basic launch ---"); + backend_auto_retire = 1'b1; + req_ready = 1'b1; + captured_q.delete(); + program_transfer(32'h1000_0000, 32'h2000_0000, 32'h0000_0040); + // id gen resets next=2, so the very first launch must return id 2 + exp_id = next_id; + check_eq(exp_id, 32'd2, "Test1 id gen resets next=2"); + launch(id0); + // the launch returns exactly the id that was pending, and issues it once + check_eq(id0, exp_id, "Test1 first launch id == next_id"); + prev_id = id0; + // exactly one launch captured, with the programmed geometry + check_eq(captured_q.size(), 32'd1, "Test1 launch count"); + if (captured_q.size() > 0) begin + check_eq(captured_q[0].burst_req.src_addr, 32'h1000_0000, "Test1 src_addr"); + check_eq(captured_q[0].burst_req.dst_addr, 32'h2000_0000, "Test1 dst_addr"); + check_eq(captured_q[0].burst_req.length, 32'h0000_0040, "Test1 length"); + end + poll_done(id0); + $display("[ ok ] Test1 done_id reached %0d", id0); + + // ------------------------------------------------------------------ + // Test 2 — DEADLOCK / backpressure regression (core gate) + // ------------------------------------------------------------------ + $display("\n--- Test 2: backpressure / deadlock regression ---"); + backend_auto_retire = 1'b0; // no auto retire while we hold the stall + captured_q.delete(); + // model a busy backend: hold req_ready LOW so the arbiter cannot grant + req_ready = 1'b0; + program_transfer(32'h3000_0000, 32'h4000_0000, 32'h0000_0080); + exp_id = next_id; // id that this (still ungranted) launch must return + fork + begin : do_launch + // this read MUST stall (pready low) while req_ready is low, then + // complete the cycle the grant lands — the watchdog guards a freeze. + launch(id1); + end + begin : release_bp + int unsigned stall_obs; + stall_obs = 0; + // observe the launch genuinely stalling for K cycles without freezing + repeat (40) begin + @(posedge clk); + if (nxt_read_active && !apb_rsp[0].pready) stall_obs++; + end + check_eq(stall_obs >= 32'd30, 1'b1, "Test2 read stalled (pready low) under BP"); + // the id must NOT have advanced while the launch was held ungranted + check_eq(next_id, exp_id, "Test2 id held (no issue) during stall"); + // release backpressure — the held launch must now complete + req_ready = 1'b1; + end + join + check_eq(id1, exp_id, "Test2 launch id after stall == pre-stall next_id"); + check_eq(id1, prev_id + 32'd1, "Test2 id monotonic after Test1"); + prev_id = id1; + check_eq(captured_q.size(), 32'd1, "Test2 launch accepted exactly once"); + if (captured_q.size() > 0) begin + check_eq(captured_q[0].burst_req.src_addr, 32'h3000_0000, "Test2 src_addr held"); + check_eq(captured_q[0].burst_req.length, 32'h0000_0080, "Test2 length held"); + end + // let it retire and confirm the reg block is still live afterwards + backend_auto_retire = 1'b1; + poll_done(id1); + $display("[ ok ] Test2 completed after backpressure, block still live"); + + // ------------------------------------------------------------------ + // Test 3 — Multi-stream (only meaningful when NumStreams > 1) + // ------------------------------------------------------------------ + if (NumStreams > 1) begin + int unsigned held1_cnt; // cycles stream_idx==1 while the latch is engaged + logic bad_idx; // stream_idx ever pointed at a wrong stream + $display("\n--- Test 3: multi-stream stream_idx held under BP ---"); + backend_auto_retire = 1'b0; + captured_q.delete(); + req_ready = 1'b0; + program_transfer(32'h5000_0000, 32'h6000_0000, 32'h0000_0100); + exp_id = next_id; + held1_cnt = 0; + bad_idx = 1'b0; + fork + begin : ms_launch + launch(id2, 1); // launch on stream 1 + end + begin : ms_check + // While the launch is genuinely stalled (the arbiter is presenting the + // request, req_valid_o high, but pready is still low), stream_idx_o MUST + // read 1 for the whole stall — exactly the latent bug the pending latch + // corrected: the maskable req dropped stream_idx to 0 mid-stall. This is + // gated on PORTS only (req_valid_o + pready), so it elaborates against the + // buggy template too and is not coupled to any internal signal name. + repeat (30) begin + @(posedge clk); + if (nxt_read_active && req_valid && !apb_rsp[0].pready) begin + if (stream_idx == 1) held1_cnt++; + else bad_idx = 1'b1; // wrong / dropped stream index + end + end + req_ready = 1'b1; // release + end + join + // held for essentially the whole 30-cycle window, never pointing elsewhere + check_eq(held1_cnt >= 32'd20, 1'b1, "Test3 stream_idx held == 1 across stall"); + check_eq(bad_idx, 1'b0, "Test3 stream_idx never pointed at wrong stream"); + check_eq(id2, exp_id, "Test3 stream1 launch id == next_id"); + check_eq(id2, prev_id + 32'd1, "Test3 id monotonic"); + prev_id = id2; + check_eq(captured_q.size(), 32'd1, "Test3 stream1 accepted once"); + backend_auto_retire = 1'b1; + poll_done(id2, 1); + $display("[ ok ] Test3 multi-stream launch completed"); + end else begin + $display("\n--- Test 3: skipped (NumStreams == 1) ---"); + end + + // ------------------------------------------------------------------ + // Test 4 — Back-to-back launches, monotonic ids, in-order done + // ------------------------------------------------------------------ + $display("\n--- Test 4: back-to-back launches ---"); + backend_auto_retire = 1'b1; + req_ready = 1'b1; + captured_q.delete(); + begin + logic [31:0] ids[4]; + for (int unsigned k = 0; k < 4; k++) begin + program_transfer(32'h1000 + k*32'h100, 32'h9000 + k*32'h100, 32'h40 + k*32'h10); + launch(ids[k]); + end + // exactly four accepted launches, each id one more than the last (distinct, + // monotonic, none dropped, none doubled) + check_eq(captured_q.size(), 32'd4, "Test4 four launches captured"); + for (int unsigned k = 0; k < 4; k++) begin + check_eq(ids[k], prev_id + 32'd1 + k, $sformatf("Test4 id[%0d] monotonic", k)); + if (k < captured_q.size()) begin + check_eq(captured_q[k].burst_req.src_addr, 32'h1000 + k*32'h100, + $sformatf("Test4 src[%0d]", k)); + check_eq(captured_q[k].burst_req.length, 32'h40 + k*32'h10, + $sformatf("Test4 len[%0d]", k)); + end + end + prev_id = ids[3]; + // done_id must advance in order to the last id + poll_done(ids[3]); + $display("[ ok ] Test4 done_id advanced in order to %0d", ids[3]); + end + + // ------------------------------------------------------------------ + // Summary + // ------------------------------------------------------------------ + repeat (5) @(negedge clk); + $display("\n====================================================="); + $display(" checks run : %0d", checks); + $display(" errors : %0d", errors); + if (errors == 0) + $display(" RESULT : PASS"); + else + $display(" RESULT : FAIL"); + $display("====================================================="); + if (errors != 0) + $fatal(1, "tb_idma_reg_frontend FAILED with %0d error(s)", errors); + $finish; + end + + // global safety net: kill a run that hangs entirely (belt-and-braces with the + // per-read watchdog, in case a hang happens outside a tracked next_id read). + initial begin + #(TCK * 200000); + $fatal(1, "GLOBAL TIMEOUT: testbench did not finish"); + end + +endmodule