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..887c76f83 100644 --- a/idma.mk +++ b/idma.mk @@ -424,6 +424,15 @@ 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" +.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..b403569aa --- /dev/null +++ b/test/frontend/idma_inst64_drv_if.sv @@ -0,0 +1,258 @@ +// 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`, 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, + /// 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..d88dcfa80 --- /dev/null +++ b/test/frontend/idma_inst64_tb_pkg.sv @@ -0,0 +1,131 @@ +// 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" + +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..a20722add --- /dev/null +++ b/test/frontend/tb_idma_inst64_axi_copy.sv @@ -0,0 +1,321 @@ +// 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, 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::*; + + 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; + + // 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; + + int unsigned errors = 0; + int unsigned bytes_checked = 0; + int unsigned axi_ar_beats = 0; + int unsigned axi_aw_beats = 0; + + //-------------------------------------- + // DUT event cross-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, + 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. 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 + + // 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 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 + 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 + + // 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] 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 + + initial begin : watchdog + repeat (TimeoutCycles) @(posedge harness.clk); + $fatal(1, "simulation timeout after %0d cycles", TimeoutCycles); + end + +endmodule