From cea0b62e02c732b06d844893d895c107f00b8721 Mon Sep 17 00:00:00 2001 From: Daniel Keller Date: Wed, 12 Aug 2026 23:11:00 +0200 Subject: [PATCH 1/5] test: Add an inst64 frontend testbench driven over the accelerator bus The tightly-coupled frontend had no public testbench, so nothing exercised the DMA ISA decode, the id/status path or the datapath below it. Port the existing harness and driver, dropping the vidma-only ISA: DMOPC decodes bit-identically to the public DMINIT, so carrying it over would have launched a real zero-fill transfer and desynchronised the completion wait. --- Bender.yml | 12 ++ idma.mk | 12 ++ test/frontend/idma_inst64_base.sv | 181 ++++++++++++++++ test/frontend/idma_inst64_drv_if.sv | 260 +++++++++++++++++++++++ test/frontend/idma_inst64_tb_pkg.sv | 133 ++++++++++++ test/frontend/tb_idma_inst64_axi_copy.sv | 165 ++++++++++++++ 6 files changed, 763 insertions(+) create mode 100644 test/frontend/idma_inst64_base.sv create mode 100644 test/frontend/idma_inst64_drv_if.sv create mode 100644 test/frontend/idma_inst64_tb_pkg.sv create mode 100644 test/frontend/tb_idma_inst64_axi_copy.sv diff --git a/Bender.yml b/Bender.yml index ea52cd933..f779f9b03 100644 --- a/Bender.yml +++ b/Bender.yml @@ -145,6 +145,18 @@ sources: - test/tb_idma_mxperf.sv - test/tb_idma_mxclear.sv + # Tightly-coupled inst64 frontend testbenches (needs the snitch_cluster-gated RTL) + - target: all(idma_test, snitch_cluster) + files: + # Level 0 + - test/frontend/idma_inst64_tb_pkg.sv + # Level 1 + - test/frontend/idma_inst64_drv_if.sv + # Level 2 + - test/frontend/idma_inst64_base.sv + # Level 3 + - test/frontend/tb_idma_inst64_axi_copy.sv + # Multi-head directed backend testbenches - target: multihead files: diff --git a/idma.mk b/idma.mk index 8f8abca4a..e24a486a2 100644 --- a/idma.mk +++ b/idma.mk @@ -424,6 +424,18 @@ idma_sim_tb_idma_reg_frontend: $(IDMA_VSIM_DIR)/compile.tcl cd $(IDMA_VSIM_DIR); $(VSIM) -c -t 1ps -voptargs=+acc -gNumStreams=2 tb_idma_reg_frontend -do "run -all; quit" cd $(IDMA_VSIM_DIR); $(VSIM) -c -t 1ps -voptargs=+acc -gNumStreams=2 -gNumRegs=2 tb_idma_reg_frontend -do "run -all; quit" +# Tightly-coupled inst64 frontend driven over the snitch accelerator bus. +# Run with the Questa SEPP wrapper: +# make idma_sim_tb_idma_inst64_axi_copy VSIM="questa-2023.4 vsim" +.PHONY: idma_sim_tb_idma_inst64_axi_copy +idma_sim_tb_idma_inst64_axi_copy: $(IDMA_VSIM_DIR)/compile_tb_idma_inst64_axi_copy.tcl + cd $(IDMA_VSIM_DIR); $(VSIM) -c -do "source compile_tb_idma_inst64_axi_copy.tcl; quit" + cd $(IDMA_VSIM_DIR); $(VSIM) -c -t 1ps -voptargs=+acc tb_idma_inst64_axi_copy \ + -logfile inst64_axi_copy.log -do "run -all; quit" + # Questa does not propagate $$fatal to the exit code; gate on the transcript + cd $(IDMA_VSIM_DIR); ! grep -qE "Error:|Fatal:" inst64_axi_copy.log + cd $(IDMA_VSIM_DIR); grep -q "TEST PASSED" inst64_axi_copy.log + .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/frontend/idma_inst64_base.sv b/test/frontend/idma_inst64_base.sv new file mode 100644 index 000000000..e1607874d --- /dev/null +++ b/test/frontend/idma_inst64_base.sv @@ -0,0 +1,181 @@ +// Copyright 2026 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 + +/// Reusable harness around `idma_inst64_top`: clock/reset, the accelerator-bus driver and +/// one AXI plus one OBI simulation memory per channel. +module idma_inst64_base #( + parameter int unsigned DMATracing = idma_inst64_tb_pkg::DMATracing, + /// TCDM (OBI) window; every address outside it decodes to ToSoC, i.e. AXI + parameter logic [63:0] TcdmStart = 64'h0000_0000_1000_0000, + parameter logic [63:0] TcdmEnd = 64'h0000_0000_1001_0000 +); + import idma_inst64_tb_pkg::*; + + logic clk; + logic rst_n; + + clk_rst_gen #( + .ClkPeriod ( Period ), + .RstClkCycles( ResetCycles ) + ) i_clock_reset_generator ( + .clk_o ( clk ), + .rst_no( rst_n ) + ); + + idma_inst64_drv_if drv_if ( + .clk ( clk ), + .rst_n( rst_n ) + ); + + axi_req_t [NumChannels-1:0] axi_req; + axi_resp_t [NumChannels-1:0] axi_res; + obi_req_t [NumChannels-1:0] obi_req; + obi_res_t [NumChannels-1:0] obi_res; + dma_events_t [NumChannels-1:0] events; + logic [NumChannels-1:0] busy; + + // TCDMAliasEnable defaults to 0, so addr_map_i holds exactly one rule. It must name a + // real TCDM window: an all-zero rule is NOT a miss, cc_addr_decode reads end_addr == 0 + // as "end of address space" and would route every address to TCDMDMA (OBI). + // Anything outside the window falls back to default_idx_i = ToSoC, i.e. AXI. + addr_rule_t [0:0] addr_map; + assign addr_map[0] = '{idx: idma_pkg::TCDMDMA, start_addr: TcdmStart, end_addr: TcdmEnd}; + + idma_inst64_top #( + .AxiDataWidth ( AxiDataWidth ), + .AxiAddrWidth ( AxiAddrWidth ), + .AxiUserWidth ( AxiUserWidth ), + .AxiIdWidth ( AxiIdWidth ), + .NumAxInFlight ( NumAxInFlight ), + .DMAReqFifoDepth ( DMAReqFifoDepth ), + .NumChannels ( NumChannels ), + .DMATracing ( DMATracing ), + .axi_ar_chan_t ( axi_ar_chan_t ), + .axi_aw_chan_t ( axi_aw_chan_t ), + .axi_req_t ( axi_req_t ), + .axi_res_t ( axi_resp_t ), + .init_req_chan_t ( init_req_chan_t ), + .init_rsp_chan_t ( init_rsp_chan_t ), + .init_req_t ( init_req_t ), + .init_rsp_t ( init_rsp_t ), + .obi_a_chan_t ( obi_a_chan_t ), + .obi_r_chan_t ( obi_r_chan_t ), + .obi_req_t ( obi_req_t ), + .obi_res_t ( obi_res_t ), + .acc_req_t ( acc_req_t ), + .acc_res_t ( acc_res_t ), + .dma_events_t ( dma_events_t ), + .addr_rule_t ( addr_rule_t ) + ) i_dut ( + .clk_i ( clk ), + .rst_ni ( rst_n ), + .axi_req_o ( axi_req ), + .axi_res_i ( axi_res ), + .obi_req_o ( obi_req ), + .obi_res_i ( obi_res ), + .busy_o ( busy ), + .acc_req_i ( drv_if.acc_req ), + .acc_req_valid_i ( drv_if.acc_req_valid ), + .acc_req_ready_o ( drv_if.acc_req_ready ), + .acc_res_o ( drv_if.acc_res ), + .acc_res_valid_o ( drv_if.acc_res_valid ), + .acc_res_ready_i ( drv_if.acc_res_ready ), + .hart_id_i ( 32'h0 ), + .events_o ( events ), + .addr_map_i ( addr_map ) + ); + + //-------------------------------------- + // Memory subsystem + //-------------------------------------- + for (genvar c = 0; c < NumChannels; c++) begin : gen_mem_ch + axi_sim_mem #( + .AddrWidth ( AxiAddrWidth ), + .DataWidth ( AxiDataWidth ), + .IdWidth ( AxiIdWidth ), + .UserWidth ( AxiUserWidth ), + .axi_req_t ( axi_req_t ), + .axi_rsp_t ( axi_resp_t ), + .WarnUninitialized ( 1'b0 ), + .ClearErrOnAccess ( 1'b1 ), + .ApplDelay ( ApplDelay ), + .AcqDelay ( AcqDelay ) + ) i_axi_sim_mem ( + .clk_i ( clk ), + .rst_ni ( rst_n ), + .axi_req_i ( axi_req[c] ), + .axi_rsp_o ( axi_res[c] ), + .mon_w_valid_o ( /* NC */ ), + .mon_w_addr_o ( /* NC */ ), + .mon_w_data_o ( /* NC */ ), + .mon_w_id_o ( /* NC */ ), + .mon_w_user_o ( /* NC */ ), + .mon_w_beat_count_o( /* NC */ ), + .mon_w_last_o ( /* NC */ ), + .mon_r_valid_o ( /* NC */ ), + .mon_r_addr_o ( /* NC */ ), + .mon_r_data_o ( /* NC */ ), + .mon_r_id_o ( /* NC */ ), + .mon_r_user_o ( /* NC */ ), + .mon_r_beat_count_o( /* NC */ ), + .mon_r_last_o ( /* NC */ ) + ); + + // Real OBI slave, not a '0 tie-off: a tie-off holds gnt low, so a mis-decoded + // transfer would hang instead of failing visibly. + obi_sim_mem #( + .ObiCfg ( ObiCfg ), + .obi_req_t ( obi_req_t ), + .obi_rsp_t ( obi_res_t ), + .obi_r_chan_t ( obi_r_chan_t ), + .WarnUninitialized ( 1'b0 ), + .ClearErrOnAccess ( 1'b1 ), + .ApplDelay ( ApplDelay ), + .AcqDelay ( AcqDelay ) + ) i_obi_sim_mem ( + .clk_i ( clk ), + .rst_ni ( rst_n ), + .obi_req_i ( obi_req[c] ), + .obi_rsp_o ( obi_res[c] ), + .mon_valid_o ( /* NC */ ), + .mon_we_o ( /* NC */ ), + .mon_addr_o ( /* NC */ ), + .mon_wdata_o ( /* NC */ ), + .mon_be_o ( /* NC */ ), + .mon_id_o ( /* NC */ ) + ); + end + + //-------------------------------------- + // AXI memory helpers (channel 0) + //-------------------------------------- + task automatic mem_init_pattern( + input addr_t base_addr, + input int num_bytes, + input logic [7:0] pattern + ); + for (int i = 0; i < num_bytes; i++) begin + gen_mem_ch[0].i_axi_sim_mem.mem[base_addr + i] = pattern; + end + endtask + + task automatic mem_write_byte( + input addr_t addr, + input byte data + ); + gen_mem_ch[0].i_axi_sim_mem.mem[addr] = data; + endtask + + function automatic logic [7:0] mem_read_byte(input addr_t addr); + if (gen_mem_ch[0].i_axi_sim_mem.mem.exists(addr)) begin + return gen_mem_ch[0].i_axi_sim_mem.mem[addr]; + end else begin + return 8'hXX; + end + endfunction + +endmodule diff --git a/test/frontend/idma_inst64_drv_if.sv b/test/frontend/idma_inst64_drv_if.sv new file mode 100644 index 000000000..2a0effa12 --- /dev/null +++ b/test/frontend/idma_inst64_drv_if.sv @@ -0,0 +1,260 @@ +// Copyright 2026 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 + +/// Snitch accelerator-bus driver for `idma_inst64_top`. Exposes the public inst64 ISA +/// only (DMSRC/DMDST/DMSTR/DMREP/DMCPY/DMCPYI/DMSTAT). The vidma-only DMOPC/ALU/MX +/// opcodes are deliberately absent: DMOPC's encoding aliases public DMINIT and would +/// silently launch a real zero-fill transfer. +interface idma_inst64_drv_if #( + /// Poll budget for `dma_wait`/`dma_wait_idle` before the wait is declared a deadlock + parameter int unsigned MaxPolls = 32'd10000, + /// Cycle budget for an outstanding accelerator response + parameter int unsigned RspTimeoutCycles = 32'd10000 +) ( + input logic clk, + input logic rst_n +); + import idma_inst64_tb_pkg::*; + + // Accelerator interface signals + acc_req_t acc_req; + logic acc_req_valid; + logic acc_req_ready; + + acc_res_t acc_res; + logic acc_res_valid; + logic acc_res_ready; + + // Driver state + logic [31:0] req_id_counter; + + // Last observed response (checked by the tests) + logic [31:0] last_req_id; + logic [31:0] last_rsp_id; + logic [63:0] last_rsp_data; + logic last_rsp_error; + + longint unsigned cycle_counter; + longint unsigned dma_start_cycle; + longint unsigned dma_end_cycle; + longint unsigned dma_cycles; + + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) cycle_counter <= '0; + else cycle_counter <= cycle_counter + 1; + end + + initial begin + acc_req_valid = 1'b0; + acc_res_ready = 1'b1; + acc_req = '0; + req_id_counter = '0; + last_req_id = '0; + last_rsp_id = '0; + last_rsp_data = '0; + last_rsp_error = 1'b0; + dma_start_cycle = '0; + dma_end_cycle = '0; + dma_cycles = '0; + end + + //-------------------------------------- + // Response capture + //-------------------------------------- + // acc_res_o comes out of a 2-deep cc_spill_register; with acc_res_ready_i tied high + // it pops on the posedge, so sample it in a clocked process instead of reading it + // combinationally from a task. + acc_rsp_item_t rsp_queue [$]; + + always_ff @(posedge clk) begin : proc_capture_rsp + if (rst_n && acc_res_valid && acc_res_ready) begin + rsp_queue.push_back('{id: acc_res.id, data: acc_res.data, error: acc_res.error}); + end + end + + function automatic int unsigned rsp_pending(); + return rsp_queue.size(); + endfunction + + //-------------------------------------- + // Low-level accelerator request driver + //-------------------------------------- + // Drive at ApplDelay, sample ready at AcqDelay of the SAME cycle, then release on the + // handshake edge. Sampling ready in the same delta as the drive reads the stale value + // and holds valid across two edges, which issues every instruction twice. + task automatic acc_issue( + input logic [31:0] data_op, + input logic [63:0] data_arga, + input logic [63:0] data_argb + ); + @(posedge clk); + #(ApplDelay); + last_req_id = req_id_counter; + acc_req.id = req_id_counter; + acc_req.data_op = data_op; + acc_req.data_arga = data_arga; + acc_req.data_argb = data_argb; + acc_req_valid = 1'b1; + req_id_counter = req_id_counter + 1; + + #(AcqDelay - ApplDelay); + while (!acc_req_ready) begin + @(posedge clk); + #(AcqDelay); + end + + @(posedge clk); + #(ApplDelay); + acc_req_valid = 1'b0; + endtask + + /// Pop the response the DUT produced for the last issued request; fails on timeout, + /// on an id mismatch or on a flagged error. + task automatic acc_get_rsp(output acc_rsp_item_t item); + int unsigned waited; + waited = 0; + // Resample at AcqDelay so the queue push (active region) is visible to this task + while (rsp_queue.size() == 0) begin + @(posedge clk); + #(AcqDelay); + waited++; + if (waited > RspTimeoutCycles) begin + $fatal(1, "[DRV] no accelerator response for req id %0d within %0d cycles", + last_req_id, RspTimeoutCycles); + end + end + item = rsp_queue.pop_front(); + last_rsp_id = item.id; + last_rsp_data = item.data; + last_rsp_error = item.error; + if (item.id !== last_req_id) begin + $fatal(1, "[DRV] response id mismatch: expected %0d, got %0d", last_req_id, item.id); + end + if (item.error !== 1'b0) begin + $fatal(1, "[DRV] response for req id %0d flags an error", last_req_id); + end + endtask + + //-------------------------------------- + // C-like API for DMA programming + //-------------------------------------- + + task automatic dma_set_source(input addr_t addr); + acc_issue(inst_encoding(idma_inst64_snitch_pkg::DMSRC), + {32'b0, addr[31:0]}, + {{(64-(AxiAddrWidth-32)){1'b0}}, addr[AxiAddrWidth-1:32]}); + endtask + + task automatic dma_set_dest(input addr_t addr); + acc_issue(inst_encoding(idma_inst64_snitch_pkg::DMDST), + {32'b0, addr[31:0]}, + {{(64-(AxiAddrWidth-32)){1'b0}}, addr[AxiAddrWidth-1:32]}); + endtask + + task automatic dma_set_strides( + input logic [31:0] src_stride, + input logic [31:0] dst_stride + ); + acc_issue(inst_encoding(idma_inst64_snitch_pkg::DMSTR), + {32'b0, src_stride}, {32'b0, dst_stride}); + endtask + + task automatic dma_set_reps(input logic [31:0] reps); + acc_issue(inst_encoding(idma_inst64_snitch_pkg::DMREP), {32'b0, reps}, 64'b0); + endtask + + /// Register-form copy; argb[1:0] = cfg, argb[4:2] = channel + task automatic dma_start_copy( + input addr_t length, + input logic [1:0] cfg, + input logic [2:0] channel, + output tf_id_t transfer_id + ); + acc_rsp_item_t item; + if (length == '0) $fatal(1, "[DRV] zero-length DMCPY: the backend rejects it silently"); + dma_start_cycle = cycle_counter; + acc_issue(inst_encoding(idma_inst64_snitch_pkg::DMCPY), + length, {59'b0, channel, cfg}); + acc_get_rsp(item); + transfer_id = item.data[31:0]; + endtask + + /// Immediate-form copy; data_op[21:20] = cfg, data_op[24:22] = channel + task automatic dma_start_copy_imm( + input addr_t length, + input logic [1:0] cfg, + input logic [2:0] channel, + output tf_id_t transfer_id + ); + acc_rsp_item_t item; + logic [31:0] encoding; + if (length == '0) $fatal(1, "[DRV] zero-length DMCPYI: the backend rejects it silently"); + encoding = inst_encoding(idma_inst64_snitch_pkg::DMCPYI); + encoding[21:20] = cfg; + encoding[24:22] = channel; + dma_start_cycle = cycle_counter; + acc_issue(encoding, length, 64'b0); + acc_get_rsp(item); + transfer_id = item.data[31:0]; + endtask + + /// Register-form status read; argb[1:0] = index, argb[4:2] = channel. + /// index 0 = completed_id, 1 = next_id, 2 = busy, 3 = fifo full + task automatic dma_poll_status( + input logic [1:0] status_idx, + input logic [2:0] channel, + output logic [63:0] status_value + ); + acc_rsp_item_t item; + acc_issue(inst_encoding(idma_inst64_snitch_pkg::DMSTAT), + 64'b0, {59'b0, channel, status_idx}); + acc_get_rsp(item); + status_value = item.data; + endtask + + /// Wait for `transfer_id` to retire. The id generator resets to next=2/completed=1, so + /// the first transfer gets id 2 and the compare below is not vacuous. + task automatic dma_wait( + input tf_id_t transfer_id, + input logic [2:0] channel + ); + logic [63:0] completed_id; + int unsigned polls; + polls = 0; + forever begin + dma_poll_status(2'b00, channel, completed_id); + if (completed_id >= transfer_id) begin + dma_end_cycle = cycle_counter; + dma_cycles = dma_end_cycle - dma_start_cycle; + break; + end + polls++; + if (polls > MaxPolls) begin + $fatal(1, "[DRV] dma_wait(id=%0d, chan=%0d) stuck after %0d polls (completed=%0d)", + transfer_id, channel, MaxPolls, completed_id); + end + repeat (10) @(posedge clk); + end + endtask + + task automatic dma_wait_idle(input logic [2:0] channel); + logic [63:0] busy_status; + int unsigned polls; + polls = 0; + forever begin + dma_poll_status(2'b10, channel, busy_status); + if (busy_status[0] == 1'b0) break; + polls++; + if (polls > MaxPolls) begin + $fatal(1, "[DRV] dma_wait_idle(chan=%0d) timed out after %0d polls", + channel, MaxPolls); + end + repeat (5) @(posedge clk); + end + endtask + +endinterface diff --git a/test/frontend/idma_inst64_tb_pkg.sv b/test/frontend/idma_inst64_tb_pkg.sv new file mode 100644 index 000000000..a1f37c616 --- /dev/null +++ b/test/frontend/idma_inst64_tb_pkg.sv @@ -0,0 +1,133 @@ +// Copyright 2026 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 + +`include "axi/typedef.svh" +`include "obi/typedef.svh" + +/// Concrete type binding for the `idma_inst64_top` testbenches. The types mirror +/// `test/idma_inst64_lint.sv`, the elaboration-proven binding of the frontend. +package idma_inst64_tb_pkg; + + localparam int unsigned AxiDataWidth = 32'd512; + localparam int unsigned AxiAddrWidth = 32'd64; + localparam int unsigned AxiUserWidth = 32'd1; + localparam int unsigned AxiIdWidth = 32'd3; + localparam int unsigned NumAxInFlight = 32'd3; + localparam int unsigned DMAReqFifoDepth = 32'd3; + localparam int unsigned NumChannels = 32'd1; + // Tracer off; idma_inst64_top applies the rw_axi tracer macro to a + // rw_axi_rw_init_rw_obi backend instance, which is a separate open issue. + localparam int unsigned DMATracing = 32'd0; + + localparam time Period = 10ns; + localparam time ApplDelay = Period / 4; + localparam time AcqDelay = Period * 3 / 4; + localparam integer ResetCycles = 10; + + typedef logic [AxiAddrWidth-1:0] addr_t; + typedef logic [31:0] tf_id_t; + + typedef logic [AxiAddrWidth-1:0] axi_addr_t; + typedef logic [AxiDataWidth-1:0] axi_data_t; + typedef logic [AxiDataWidth/8-1:0] axi_strb_t; + typedef logic [AxiUserWidth-1:0] axi_user_t; + typedef logic [AxiIdWidth-1:0] axi_id_t; + + `AXI_TYPEDEF_AW_CHAN_T(axi_aw_chan_t, axi_addr_t, axi_id_t, axi_user_t) + `AXI_TYPEDEF_W_CHAN_T(axi_w_chan_t, axi_data_t, axi_strb_t, axi_user_t) + `AXI_TYPEDEF_B_CHAN_T(axi_b_chan_t, axi_id_t, axi_user_t) + `AXI_TYPEDEF_AR_CHAN_T(axi_ar_chan_t, axi_addr_t, axi_id_t, axi_user_t) + `AXI_TYPEDEF_R_CHAN_T(axi_r_chan_t, axi_data_t, axi_id_t, axi_user_t) + `AXI_TYPEDEF_REQ_T(axi_req_t, axi_aw_chan_t, axi_w_chan_t, axi_ar_chan_t) + `AXI_TYPEDEF_RESP_T(axi_resp_t, axi_b_chan_t, axi_r_chan_t) + + typedef logic [AxiDataWidth/8-1:0] obi_strb_t; + `OBI_TYPEDEF_MINIMAL_A_OPTIONAL(obi_a_optional_t) + `OBI_TYPEDEF_MINIMAL_R_OPTIONAL(obi_r_optional_t) + `OBI_TYPEDEF_TYPE_A_CHAN_T(obi_a_chan_t, axi_addr_t, axi_data_t, obi_strb_t, axi_id_t, + obi_a_optional_t) + `OBI_TYPEDEF_TYPE_R_CHAN_T(obi_r_chan_t, axi_data_t, axi_id_t, obi_r_optional_t) + `OBI_TYPEDEF_REQ_T(obi_req_t, obi_a_chan_t) + `OBI_TYPEDEF_RSP_T(obi_res_t, obi_r_chan_t) + + // OBI sim-mem cfg; UseRReady=1 so the mem honors the backend's rready + function automatic obi_pkg::obi_cfg_t tb_obi_cfg(); + tb_obi_cfg = obi_pkg::obi_default_cfg(AxiAddrWidth, AxiDataWidth, AxiIdWidth, + obi_pkg::ObiMinimalOptionalConfig); + tb_obi_cfg.UseRReady = 1'b1; + endfunction + localparam obi_pkg::obi_cfg_t ObiCfg = tb_obi_cfg(); + + // INIT meta-channel types (mirror src/db/idma_init.yml) + typedef struct packed { + logic [AxiAddrWidth-1:0] cfg; + logic [AxiDataWidth-1:0] term; + logic [AxiDataWidth/8-1:0] strb; + logic [AxiIdWidth-1:0] id; + } init_req_chan_t; + typedef struct packed { + init_req_chan_t req_chan; + logic req_valid; + logic rsp_ready; + } init_req_t; + typedef struct packed { + logic [AxiDataWidth-1:0] init; + } init_rsp_chan_t; + typedef struct packed { + init_rsp_chan_t rsp_chan; + logic rsp_valid; + logic req_ready; + } init_rsp_t; + + typedef axi_pkg::xbar_rule_64_t addr_rule_t; + + // Snitch accelerator bus (the inst64 frontend decodes data_op/argb) + typedef struct packed { + logic [31:0] id; + logic [31:0] data_op; + logic [63:0] data_arga; + logic [63:0] data_argb; + } acc_req_t; + typedef struct packed { + logic [31:0] id; + logic [63:0] data; + logic error; + } acc_res_t; + + // obi_wr_req/obi_rd_req are required: idma_inst64_events drives them unconditionally + typedef struct packed { + logic aw_valid, aw_ready, aw_done, aw_stall; + axi_pkg::len_t aw_len; + axi_pkg::size_t aw_size; + logic ar_valid, ar_ready, ar_done, ar_stall; + axi_pkg::len_t ar_len; + axi_pkg::size_t ar_size; + logic r_valid, r_ready, r_done, r_bw, r_stall; + logic w_valid, w_ready, w_done, w_stall; + logic [31:0] num_bytes_written; + logic b_valid, b_ready, b_done; + logic obi_wr_req, obi_rd_req; + logic dma_busy; + } dma_events_t; + + // Captured accelerator response; the driver queues one entry per acc handshake + typedef struct packed { + logic [31:0] id; + logic [63:0] data; + logic error; + } acc_rsp_item_t; + + /// Strip the don't-care (z) bits out of an `idma_inst64_snitch_pkg` casez pattern. + /// The localparams are match patterns, not drivable values; a zeroed encoding still + /// matches its own casez item and no other (the funct7 fields are mutually exclusive). + function automatic logic [31:0] inst_encoding(input logic [31:0] pattern); + for (int unsigned i = 0; i < 32; i++) begin + inst_encoding[i] = (pattern[i] === 1'b1) ? 1'b1 : 1'b0; + end + endfunction + +endpackage diff --git a/test/frontend/tb_idma_inst64_axi_copy.sv b/test/frontend/tb_idma_inst64_axi_copy.sv new file mode 100644 index 000000000..e4a8197a4 --- /dev/null +++ b/test/frontend/tb_idma_inst64_axi_copy.sv @@ -0,0 +1,165 @@ +// Copyright 2026 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 + +/// Single-channel AXI-to-AXI copy through the tightly-coupled `inst64` frontend. +/// Proves the accelerator-bus programming sequence, that exactly one transfer is +/// launched, and that the payload lands byte-exact without overrunning the destination. +module tb_idma_inst64_axi_copy; + import idma_inst64_tb_pkg::*; + + idma_inst64_base harness (); + + localparam int unsigned TimeoutCycles = 32'd200000; + localparam int unsigned CopySize = 32'd4096; + localparam int unsigned BytesPerBeat = AxiDataWidth / 32'd8; + // Keep the beat rounding: MaskInvalidData is 0 in the inst64 backend, so the full + // beat-rounded source range must be initialized once CopySize stops being a multiple. + localparam int unsigned CopyPadBytes = + ((CopySize + BytesPerBeat - 1) / BytesPerBeat) * BytesPerBeat; + localparam int unsigned GuardBytes = 32'd64; + localparam logic [7:0] Sentinel = 8'h5A; + localparam logic [7:0] PatternStart = 8'hA0; + + localparam addr_t SrcAddr = 64'h8000_0000; + localparam addr_t DstAddr = 64'h9000_0000; + + int unsigned errors = 0; + int unsigned bytes_checked = 0; + int unsigned axi_ar_beats = 0; + int unsigned axi_aw_beats = 0; + + // Count AXI address handshakes; a green OBI-never-asserted check is only meaningful + // if the transfer actually went somewhere. + always_ff @(posedge harness.clk) begin : proc_count_axi + if (harness.rst_n) begin + if (harness.axi_req[0].ar_valid && harness.axi_res[0].ar_ready) axi_ar_beats++; + if (harness.axi_req[0].aw_valid && harness.axi_res[0].aw_ready) axi_aw_beats++; + end + end + + // Both endpoints sit outside the harness TCDM window, so the decoder must fall back to + // ToSoC/AXI. A rising OBI request means the protocol decode is wrong. + a_no_obi_traffic : assert property ( + @(posedge harness.clk) disable iff (!harness.rst_n) !harness.obi_req[0].req + ) else $fatal(1, "OBI leg requested during an AXI-to-AXI transfer: bad protocol decode"); + + task automatic seed_memories(); + for (int i = 0; i < CopyPadBytes; i++) begin + harness.mem_write_byte(SrcAddr + i, (i < CopySize) ? (PatternStart + i) : 8'h00); + end + // Sentinel over the destination and its guard bands: an unwritten byte must fail + // the compare rather than accidentally match, and an overrun must be visible. + // Offsets stay non-negative; a signed offset added to a 64-bit unsigned address is + // zero-extended, not sign-extended. + for (int unsigned i = 0; i < CopySize + 2*GuardBytes; i++) begin + harness.mem_write_byte(DstAddr - GuardBytes + i, Sentinel); + end + endtask + + task automatic check_payload(); + for (int i = 0; i < CopySize; i++) begin + logic [7:0] expected; + logic [7:0] actual; + expected = PatternStart + i; + actual = harness.mem_read_byte(DstAddr + i); + bytes_checked++; + if (actual !== expected) begin + if (errors < 10) begin + $error("payload mismatch at offset %0d: expected 0x%02x, got 0x%02x", + i, expected, actual); + end + errors++; + end + end + endtask + + task automatic check_guard_bands(); + for (int unsigned i = 1; i <= GuardBytes; i++) begin + logic [7:0] lo; + logic [7:0] hi; + lo = harness.mem_read_byte(DstAddr - i); + hi = harness.mem_read_byte(DstAddr + CopySize + i - 1); + if (lo !== Sentinel) begin + $error("destination underrun at -%0d: got 0x%02x", i, lo); + errors++; + end + if (hi !== Sentinel) begin + $error("destination overrun at +%0d: got 0x%02x", i, hi); + errors++; + end + end + endtask + + initial begin : test_sequence + tf_id_t tid; + logic [63:0] next_id_before; + logic [63:0] next_id_after; + logic [31:0] issued_req_id; + + @(posedge harness.rst_n); + repeat (10) @(posedge harness.clk); + + $display("[TB] inst64 AXI-to-AXI copy: 0x%0h -> 0x%0h, %0d B", SrcAddr, DstAddr, CopySize); + seed_memories(); + + harness.drv_if.dma_poll_status(2'b01, 3'd0, next_id_before); + + harness.drv_if.dma_set_source(SrcAddr); + harness.drv_if.dma_set_dest(DstAddr); + harness.drv_if.dma_start_copy(CopySize, 2'b00, 3'd0, tid); + issued_req_id = harness.drv_if.last_req_id; + + // The driver already fails hard on a bad response; re-check here so the TB states + // the contract it relies on. + if (harness.drv_if.last_rsp_error !== 1'b0) begin + $fatal(1, "DMCPY response flagged an error"); + end + if (harness.drv_if.last_rsp_id !== issued_req_id) begin + $fatal(1, "DMCPY response id %0d does not match request id %0d", + harness.drv_if.last_rsp_id, issued_req_id); + end + + harness.drv_if.dma_wait(tid, 3'd0); + $display("[TB] transfer %0d retired after %0d cycles", tid, harness.drv_if.dma_cycles); + + check_payload(); + check_guard_bands(); + + if (bytes_checked != CopySize) begin + $fatal(1, "compare loop ran %0d times, expected %0d", bytes_checked, CopySize); + end + if (CopySize == 0) $fatal(1, "zero-byte golden compare"); + + // Exactly one transfer must have been launched. A driver that holds acc_req_valid + // one cycle too long issues the DMCPY twice; the memory image would look identical. + harness.drv_if.dma_poll_status(2'b01, 3'd0, next_id_after); + if (next_id_after !== next_id_before + 1) begin + $fatal(1, "next_id moved %0d -> %0d, expected exactly one transfer", + next_id_before, next_id_after); + end + + if (axi_ar_beats == 0 || axi_aw_beats == 0) begin + $fatal(1, "no AXI address handshakes observed (ar=%0d, aw=%0d)", + axi_ar_beats, axi_aw_beats); + end + if (harness.drv_if.rsp_pending() != 0) begin + $fatal(1, "%0d unexpected accelerator responses left over", + harness.drv_if.rsp_pending()); + end + + if (errors != 0) $fatal(1, "TEST FAILED: %0d errors", errors); + $display("[TB] TEST PASSED: %0d B copied, ar=%0d aw=%0d beats", + bytes_checked, axi_ar_beats, axi_aw_beats); + $finish; + end + + initial begin : watchdog + repeat (TimeoutCycles) @(posedge harness.clk); + $fatal(1, "simulation timeout after %0d cycles", TimeoutCycles); + end + +endmodule From 94f4013fdd35d7aaa9a0677f1776dd72112c5110 Mon Sep 17 00:00:00 2001 From: Daniel Keller Date: Thu, 13 Aug 2026 10:11:31 +0200 Subject: [PATCH 2/5] test: Cross-check the inst64 event counters against the bus pins The event outputs feed downstream performance counters but nothing verified them; the testbench observed the same handshakes independently and ignored them. Compare all 26 observable fields against the pins every cycle and anchor the totals, keeping the independent sniff as the reference so a wrong counter cannot agree with itself. --- test/frontend/tb_idma_inst64_axi_copy.sv | 179 ++++++++++++++++++++++- 1 file changed, 175 insertions(+), 4 deletions(-) diff --git a/test/frontend/tb_idma_inst64_axi_copy.sv b/test/frontend/tb_idma_inst64_axi_copy.sv index e4a8197a4..0268709cf 100644 --- a/test/frontend/tb_idma_inst64_axi_copy.sv +++ b/test/frontend/tb_idma_inst64_axi_copy.sv @@ -7,7 +7,8 @@ /// Single-channel AXI-to-AXI copy through the tightly-coupled `inst64` frontend. /// Proves the accelerator-bus programming sequence, that exactly one transfer is -/// launched, and that the payload lands byte-exact without overrunning the destination. +/// launched, that the payload lands byte-exact without overrunning the destination, and +/// that the exported `dma_events_t` performance counters agree with the bus traffic. module tb_idma_inst64_axi_copy; import idma_inst64_tb_pkg::*; @@ -24,6 +25,12 @@ module tb_idma_inst64_axi_copy; localparam logic [7:0] Sentinel = 8'h5A; localparam logic [7:0] PatternStart = 8'hA0; + // One burst per direction: the copy is 64 B aligned on both ends and a multiple of a + // full beat, so every data beat carries a full strobe. + localparam int unsigned ExpDataBeats = CopyPadBytes / BytesPerBeat; + localparam axi_pkg::len_t ExpAxLen = axi_pkg::len_t'(ExpDataBeats - 1); + localparam axi_pkg::size_t ExpAxSize = axi_pkg::size_t'($clog2(BytesPerBeat)); + localparam addr_t SrcAddr = 64'h8000_0000; localparam addr_t DstAddr = 64'h9000_0000; @@ -32,12 +39,142 @@ module tb_idma_inst64_axi_copy; int unsigned axi_ar_beats = 0; int unsigned axi_aw_beats = 0; + //-------------------------------------- + // DUT event cross-check + //-------------------------------------- + // `events_o` is exported for downstream performance counters but nothing verified it. + // Every field is a zero-latency recode of the DUT's own top-level pins, so the TB + // rebuilds the expected value from those pins and compares it per cycle. This checks + // the recode (mapping, polarity, gating, width), not the bus traffic; the geometry + // anchors below are what keep it from passing on a run where nothing happened. The + // hand-counted AR/AW sniff stays the independent source of truth and is untouched. + // + // Deliberately not checked here: obi_wr_req/obi_rd_req (a_no_obi_traffic pins both + // sides to 0, so an equality proves nothing; needs a TCDM-window TB with an OBI + // read/write mix) and the per-channel index in gen_events (NumChannels == 1). + // + // Polarity note: aw_stall/ar_stall are valid && !ready, but idma_inst64_events + // overrides its own w_stall/r_stall at lines 95/96, so w_stall is a W-side bubble + // (w_ready && !w_valid) and r_stall is !r_ready && r_valid; the earlier assignments + // are dead code. Measured gap: none of the four stall fields ever asserts here, since + // the sim memories neither backpressure nor bubble. Their equality still catches a + // polarity flip or a spurious assert, but a stuck-at-0 stall would pass; that needs a + // ready-throttling harness, not a weaker check. + typedef enum int unsigned { + EvAwValid, EvAwReady, EvAwDone, EvAwStall, EvAwLen, EvAwSize, + EvArValid, EvArReady, EvArDone, EvArStall, EvArLen, EvArSize, + EvRValid, EvRReady, EvRDone, EvRBw, EvRStall, + EvWValid, EvWReady, EvWDone, EvWStall, EvBytes, + EvBValid, EvBReady, EvBDone, EvBusy, + EvNumFields + } ev_field_e; + + localparam int unsigned NumEvFields = EvNumFields; + + // Aliases of the very nets the sniff above reads; they only keep the table readable. + dma_events_t ev; + axi_req_t bus_req; + axi_resp_t bus_res; + assign ev = harness.events[0]; + assign bus_req = harness.axi_req[0]; + assign bus_res = harness.axi_res[0]; + + logic [NumEvFields-1:0] ev_field_ok; + + // len/size are compared in their full `handshake ? pin : '0` form every cycle, so a + // stale value leaking between bursts fails instead of hiding in the idle half. + always_comb begin : proc_ev_field_ok + automatic logic aw_hs = bus_req.aw_valid && bus_res.aw_ready; + automatic logic ar_hs = bus_req.ar_valid && bus_res.ar_ready; + automatic logic w_hs = bus_req.w_valid && bus_res.w_ready; + + ev_field_ok[EvAwValid] = ev.aw_valid === bus_req.aw_valid; + ev_field_ok[EvAwReady] = ev.aw_ready === bus_res.aw_ready; + ev_field_ok[EvAwDone ] = ev.aw_done === aw_hs; + ev_field_ok[EvAwStall] = ev.aw_stall === (bus_req.aw_valid && !bus_res.aw_ready); + ev_field_ok[EvAwLen ] = ev.aw_len === (aw_hs ? bus_req.aw.len : 8'd0); + ev_field_ok[EvAwSize ] = ev.aw_size === (aw_hs ? bus_req.aw.size : 3'd0); + + ev_field_ok[EvArValid] = ev.ar_valid === bus_req.ar_valid; + ev_field_ok[EvArReady] = ev.ar_ready === bus_res.ar_ready; + ev_field_ok[EvArDone ] = ev.ar_done === ar_hs; + ev_field_ok[EvArStall] = ev.ar_stall === (bus_req.ar_valid && !bus_res.ar_ready); + ev_field_ok[EvArLen ] = ev.ar_len === (ar_hs ? bus_req.ar.len : 8'd0); + ev_field_ok[EvArSize ] = ev.ar_size === (ar_hs ? bus_req.ar.size : 3'd0); + + ev_field_ok[EvRValid ] = ev.r_valid === bus_res.r_valid; + ev_field_ok[EvRReady ] = ev.r_ready === bus_req.r_ready; + ev_field_ok[EvRDone ] = ev.r_done === (bus_req.r_ready && bus_res.r_valid); + // r_bw duplicates r_done in the RTL; compare it to the pins, never to ev.r_done. + ev_field_ok[EvRBw ] = ev.r_bw === (bus_req.r_ready && bus_res.r_valid); + ev_field_ok[EvRStall ] = ev.r_stall === (!bus_req.r_ready && bus_res.r_valid); + + ev_field_ok[EvWValid ] = ev.w_valid === bus_req.w_valid; + ev_field_ok[EvWReady ] = ev.w_ready === bus_res.w_ready; + ev_field_ok[EvWDone ] = ev.w_done === w_hs; + ev_field_ok[EvWStall ] = ev.w_stall === (bus_res.w_ready && !bus_req.w_valid); + ev_field_ok[EvBytes ] = ev.num_bytes_written === + (w_hs ? 32'($countones(bus_req.w.strb)) : 32'd0); + + ev_field_ok[EvBValid ] = ev.b_valid === bus_res.b_valid; + ev_field_ok[EvBReady ] = ev.b_ready === bus_req.b_ready; + ev_field_ok[EvBDone ] = ev.b_done === (bus_req.b_ready && bus_res.b_valid); + + ev_field_ok[EvBusy ] = ev.dma_busy === harness.busy[0]; + end + + // One assertion per field so a failure names it. Both operands are combinational from + // the same pins and are sampled in the same preponed region, so the offset is zero. + for (genvar f = 0; f < NumEvFields; f++) begin : gen_ev_field_check + localparam ev_field_e Field = ev_field_e'(f); + a_ev_field : assert property ( + @(posedge harness.clk) disable iff (!harness.rst_n) ev_field_ok[f] + ) else $fatal(1, "events.%s disagrees with the AXI pins", Field.name()); + end + + // events_o has no reset, so an X on both sides would make `===` match vacuously. + // This is a separate failure, never a reason to skip the comparison. + a_ev_pins_known : assert property ( + @(posedge harness.clk) disable iff (!harness.rst_n) + !$isunknown({bus_req.aw_valid, bus_req.ar_valid, bus_req.w_valid, bus_req.r_ready, + bus_req.b_ready, bus_res.aw_ready, bus_res.ar_ready, bus_res.w_ready, + bus_res.r_valid, bus_res.b_valid, harness.busy[0]}) + ) else $fatal(1, "AXI handshake pin is X: the events comparison would match vacuously"); + + int unsigned ev_ar_beats = 0; + int unsigned ev_aw_beats = 0; + int unsigned ev_r_beats = 0; + int unsigned ev_r_bw_beats = 0; + int unsigned ev_w_beats = 0; + int unsigned ev_b_beats = 0; + int unsigned ev_busy_cycles = 0; + int unsigned ev_bytes_written = 0; + axi_pkg::len_t ev_ar_len_seen, ev_aw_len_seen; + axi_pkg::size_t ev_ar_size_seen, ev_aw_size_seen; + // Count AXI address handshakes; a green OBI-never-asserted check is only meaningful - // if the transfer actually went somewhere. + // if the transfer actually went somewhere. The DUT-side event totals accumulate in the + // same process so both views are read in the same region of the same tick. always_ff @(posedge harness.clk) begin : proc_count_axi if (harness.rst_n) begin if (harness.axi_req[0].ar_valid && harness.axi_res[0].ar_ready) axi_ar_beats++; if (harness.axi_req[0].aw_valid && harness.axi_res[0].aw_ready) axi_aw_beats++; + if (ev.ar_done) begin + ev_ar_beats++; + ev_ar_len_seen = ev.ar_len; + ev_ar_size_seen = ev.ar_size; + end + if (ev.aw_done) begin + ev_aw_beats++; + ev_aw_len_seen = ev.aw_len; + ev_aw_size_seen = ev.aw_size; + end + if (ev.r_done) ev_r_beats++; + if (ev.r_bw) ev_r_bw_beats++; + if (ev.w_done) ev_w_beats++; + if (ev.b_done) ev_b_beats++; + if (ev.dma_busy) ev_busy_cycles++; + ev_bytes_written += ev.num_bytes_written; end end @@ -151,9 +288,43 @@ module tb_idma_inst64_axi_copy; harness.drv_if.rsp_pending()); end + // Event totals: first against the independent sniff, then against the transfer + // geometry. The geometry anchors are what make the per-cycle equality above + // non-vacuous; without them a run that moved nothing would pass on 0 == 0. + if (ev_ar_beats != axi_ar_beats || ev_aw_beats != axi_aw_beats) begin + $fatal(1, "events ar/aw done (%0d/%0d) disagree with the bus sniff (%0d/%0d)", + ev_ar_beats, ev_aw_beats, axi_ar_beats, axi_aw_beats); + end + if (ev_b_beats != axi_aw_beats) begin + $fatal(1, "events counted %0d B responses for %0d AW bursts", + ev_b_beats, axi_aw_beats); + end + if (ev_r_beats != ExpDataBeats || ev_r_bw_beats != ExpDataBeats || + ev_w_beats != ExpDataBeats) begin + $fatal(1, "events data beats r=%0d r_bw=%0d w=%0d, expected %0d each", + ev_r_beats, ev_r_bw_beats, ev_w_beats, ExpDataBeats); + end + if (ev_bytes_written != bytes_checked) begin + $fatal(1, "events reported %0d B written, TB verified %0d B", + ev_bytes_written, bytes_checked); + end + if (ev_ar_len_seen !== ExpAxLen || ev_aw_len_seen !== ExpAxLen || + ev_ar_size_seen !== ExpAxSize || ev_aw_size_seen !== ExpAxSize) begin + $fatal(1, "events ax len/size ar=%0d/%0d aw=%0d/%0d, expected %0d/%0d", + ev_ar_len_seen, ev_ar_size_seen, ev_aw_len_seen, ev_aw_size_seen, + ExpAxLen, ExpAxSize); + end + if (ev_busy_cycles == 0) $fatal(1, "events never reported the DMA busy"); + if (errors != 0) $fatal(1, "TEST FAILED: %0d errors", errors); - $display("[TB] TEST PASSED: %0d B copied, ar=%0d aw=%0d beats", - bytes_checked, axi_ar_beats, axi_aw_beats); + $display("[TB] TEST PASSED: %0d B copied, ar=%0d aw=%0d beats", bytes_checked, + axi_ar_beats, axi_aw_beats); + $display({"[TB] events cross-checked vs bus pins: %0d fields every cycle; totals ", + "ar=%0d aw=%0d b=%0d r=%0d r_bw=%0d w=%0d beats, len=%0d size=%0d, ", + "%0d B written, busy %0d cycles"}, + NumEvFields, ev_ar_beats, ev_aw_beats, ev_b_beats, ev_r_beats, + ev_r_bw_beats, ev_w_beats, ev_aw_len_seen, ev_aw_size_seen, + ev_bytes_written, ev_busy_cycles); $finish; end From a1612d21cf54b2e9aced1ab5b102be2d76da23f0 Mon Sep 17 00:00:00 2001 From: Daniel Keller Date: Thu, 13 Aug 2026 12:09:11 +0200 Subject: [PATCH 3/5] test: Trim the inst64 testbench comments --- idma.mk | 3 --- test/frontend/idma_inst64_drv_if.sv | 6 ++--- test/frontend/idma_inst64_tb_pkg.sv | 2 -- test/frontend/tb_idma_inst64_axi_copy.sv | 29 ++++++------------------ 4 files changed, 9 insertions(+), 31 deletions(-) diff --git a/idma.mk b/idma.mk index e24a486a2..887c76f83 100644 --- a/idma.mk +++ b/idma.mk @@ -424,9 +424,6 @@ idma_sim_tb_idma_reg_frontend: $(IDMA_VSIM_DIR)/compile.tcl cd $(IDMA_VSIM_DIR); $(VSIM) -c -t 1ps -voptargs=+acc -gNumStreams=2 tb_idma_reg_frontend -do "run -all; quit" cd $(IDMA_VSIM_DIR); $(VSIM) -c -t 1ps -voptargs=+acc -gNumStreams=2 -gNumRegs=2 tb_idma_reg_frontend -do "run -all; quit" -# Tightly-coupled inst64 frontend driven over the snitch accelerator bus. -# Run with the Questa SEPP wrapper: -# make idma_sim_tb_idma_inst64_axi_copy VSIM="questa-2023.4 vsim" .PHONY: idma_sim_tb_idma_inst64_axi_copy idma_sim_tb_idma_inst64_axi_copy: $(IDMA_VSIM_DIR)/compile_tb_idma_inst64_axi_copy.tcl cd $(IDMA_VSIM_DIR); $(VSIM) -c -do "source compile_tb_idma_inst64_axi_copy.tcl; quit" diff --git a/test/frontend/idma_inst64_drv_if.sv b/test/frontend/idma_inst64_drv_if.sv index 2a0effa12..b403569aa 100644 --- a/test/frontend/idma_inst64_drv_if.sv +++ b/test/frontend/idma_inst64_drv_if.sv @@ -5,10 +5,8 @@ // Authors: // - Daniel Keller -/// Snitch accelerator-bus driver for `idma_inst64_top`. Exposes the public inst64 ISA -/// only (DMSRC/DMDST/DMSTR/DMREP/DMCPY/DMCPYI/DMSTAT). The vidma-only DMOPC/ALU/MX -/// opcodes are deliberately absent: DMOPC's encoding aliases public DMINIT and would -/// silently launch a real zero-fill transfer. +/// Snitch accelerator-bus driver for `idma_inst64_top`, covering the inst64 ISA +/// (DMSRC/DMDST/DMSTR/DMREP/DMCPY/DMCPYI/DMSTAT). interface idma_inst64_drv_if #( /// Poll budget for `dma_wait`/`dma_wait_idle` before the wait is declared a deadlock parameter int unsigned MaxPolls = 32'd10000, diff --git a/test/frontend/idma_inst64_tb_pkg.sv b/test/frontend/idma_inst64_tb_pkg.sv index a1f37c616..d88dcfa80 100644 --- a/test/frontend/idma_inst64_tb_pkg.sv +++ b/test/frontend/idma_inst64_tb_pkg.sv @@ -8,8 +8,6 @@ `include "axi/typedef.svh" `include "obi/typedef.svh" -/// Concrete type binding for the `idma_inst64_top` testbenches. The types mirror -/// `test/idma_inst64_lint.sv`, the elaboration-proven binding of the frontend. package idma_inst64_tb_pkg; localparam int unsigned AxiDataWidth = 32'd512; diff --git a/test/frontend/tb_idma_inst64_axi_copy.sv b/test/frontend/tb_idma_inst64_axi_copy.sv index 0268709cf..a20722add 100644 --- a/test/frontend/tb_idma_inst64_axi_copy.sv +++ b/test/frontend/tb_idma_inst64_axi_copy.sv @@ -42,24 +42,11 @@ module tb_idma_inst64_axi_copy; //-------------------------------------- // DUT event cross-check //-------------------------------------- - // `events_o` is exported for downstream performance counters but nothing verified it. - // Every field is a zero-latency recode of the DUT's own top-level pins, so the TB - // rebuilds the expected value from those pins and compares it per cycle. This checks - // the recode (mapping, polarity, gating, width), not the bus traffic; the geometry - // anchors below are what keep it from passing on a run where nothing happened. The - // hand-counted AR/AW sniff stays the independent source of truth and is untouched. - // - // Deliberately not checked here: obi_wr_req/obi_rd_req (a_no_obi_traffic pins both - // sides to 0, so an equality proves nothing; needs a TCDM-window TB with an OBI - // read/write mix) and the per-channel index in gen_events (NumChannels == 1). - // - // Polarity note: aw_stall/ar_stall are valid && !ready, but idma_inst64_events - // overrides its own w_stall/r_stall at lines 95/96, so w_stall is a W-side bubble - // (w_ready && !w_valid) and r_stall is !r_ready && r_valid; the earlier assignments - // are dead code. Measured gap: none of the four stall fields ever asserts here, since - // the sim memories neither backpressure nor bubble. Their equality still catches a - // polarity flip or a spurious assert, but a stuck-at-0 stall would pass; that needs a - // ready-throttling harness, not a weaker check. + // Every field recodes the DUT's own top-level pins, so the TB rebuilds the expected + // value from those pins and compares per cycle; the hand-counted AR/AW sniff stays + // the independent reference. Not covered: obi_wr_req/obi_rd_req (no OBI traffic + // here) and the stall fields, which never assert since the sim memories neither + // backpressure nor bubble. typedef enum int unsigned { EvAwValid, EvAwReady, EvAwDone, EvAwStall, EvAwLen, EvAwSize, EvArValid, EvArReady, EvArDone, EvArStall, EvArLen, EvArSize, @@ -188,10 +175,8 @@ module tb_idma_inst64_axi_copy; for (int i = 0; i < CopyPadBytes; i++) begin harness.mem_write_byte(SrcAddr + i, (i < CopySize) ? (PatternStart + i) : 8'h00); end - // Sentinel over the destination and its guard bands: an unwritten byte must fail - // the compare rather than accidentally match, and an overrun must be visible. - // Offsets stay non-negative; a signed offset added to a 64-bit unsigned address is - // zero-extended, not sign-extended. + // Sentinel the destination and its guard bands so an unwritten byte or an + // overrun fails the compare rather than accidentally matching. for (int unsigned i = 0; i < CopySize + 2*GuardBytes; i++) begin harness.mem_write_byte(DstAddr - GuardBytes + i, Sentinel); end From 683a7aa87bd4b1ae92e8e649a56f6fb58bd35285 Mon Sep 17 00:00:00 2001 From: Daniel Keller Date: Thu, 13 Aug 2026 12:29:58 +0200 Subject: [PATCH 4/5] test: Expect the documented stall polarity in the event cross-check The goldens mirrored the RTL rather than the counter spec, so both sides read zero for the whole transfer and the four stall comparisons could not fail. --- test/frontend/tb_idma_inst64_axi_copy.sv | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/frontend/tb_idma_inst64_axi_copy.sv b/test/frontend/tb_idma_inst64_axi_copy.sv index a20722add..029382305 100644 --- a/test/frontend/tb_idma_inst64_axi_copy.sv +++ b/test/frontend/tb_idma_inst64_axi_copy.sv @@ -94,12 +94,12 @@ module tb_idma_inst64_axi_copy; ev_field_ok[EvRDone ] = ev.r_done === (bus_req.r_ready && bus_res.r_valid); // r_bw duplicates r_done in the RTL; compare it to the pins, never to ev.r_done. ev_field_ok[EvRBw ] = ev.r_bw === (bus_req.r_ready && bus_res.r_valid); - ev_field_ok[EvRStall ] = ev.r_stall === (!bus_req.r_ready && bus_res.r_valid); + ev_field_ok[EvRStall ] = ev.r_stall === (bus_req.r_ready && !bus_res.r_valid); ev_field_ok[EvWValid ] = ev.w_valid === bus_req.w_valid; ev_field_ok[EvWReady ] = ev.w_ready === bus_res.w_ready; ev_field_ok[EvWDone ] = ev.w_done === w_hs; - ev_field_ok[EvWStall ] = ev.w_stall === (bus_res.w_ready && !bus_req.w_valid); + ev_field_ok[EvWStall ] = ev.w_stall === (!bus_res.w_ready && bus_req.w_valid); ev_field_ok[EvBytes ] = ev.num_bytes_written === (w_hs ? 32'($countones(bus_req.w.strb)) : 32'd0); From 506de645dd12d6655a8f663a9dcf5c31aba4ad61 Mon Sep 17 00:00:00 2001 From: Daniel Keller Date: Thu, 13 Aug 2026 13:46:18 +0200 Subject: [PATCH 5/5] Revert "test: Expect the documented stall polarity in the event cross-check" The matching RTL fix is not on devel yet; this expectation moves to the pull request that changes the polarity, so the two land together. --- test/frontend/tb_idma_inst64_axi_copy.sv | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/frontend/tb_idma_inst64_axi_copy.sv b/test/frontend/tb_idma_inst64_axi_copy.sv index 029382305..a20722add 100644 --- a/test/frontend/tb_idma_inst64_axi_copy.sv +++ b/test/frontend/tb_idma_inst64_axi_copy.sv @@ -94,12 +94,12 @@ module tb_idma_inst64_axi_copy; ev_field_ok[EvRDone ] = ev.r_done === (bus_req.r_ready && bus_res.r_valid); // r_bw duplicates r_done in the RTL; compare it to the pins, never to ev.r_done. ev_field_ok[EvRBw ] = ev.r_bw === (bus_req.r_ready && bus_res.r_valid); - ev_field_ok[EvRStall ] = ev.r_stall === (bus_req.r_ready && !bus_res.r_valid); + ev_field_ok[EvRStall ] = ev.r_stall === (!bus_req.r_ready && bus_res.r_valid); ev_field_ok[EvWValid ] = ev.w_valid === bus_req.w_valid; ev_field_ok[EvWReady ] = ev.w_ready === bus_res.w_ready; ev_field_ok[EvWDone ] = ev.w_done === w_hs; - ev_field_ok[EvWStall ] = ev.w_stall === (!bus_res.w_ready && bus_req.w_valid); + ev_field_ok[EvWStall ] = ev.w_stall === (bus_res.w_ready && !bus_req.w_valid); ev_field_ok[EvBytes ] = ev.num_bytes_written === (w_hs ? 32'($countones(bus_req.w.strb)) : 32'd0);