diff --git a/.github/verible.waiver b/.github/verible.waiver new file mode 100644 index 0000000..90012ea --- /dev/null +++ b/.github/verible.waiver @@ -0,0 +1,10 @@ +# Copyright 2025 ETH Zurich and University of Bologna. +# Solderpad Hardware License, Version 0.51, see LICENSE for details. +# SPDX-License-Identifier: SHL-0.51 +# +# Disable line length check +waive --rule=line-length +# Disable parameter style check +waive --rule=parameter-name-style +# Disable default check in case statements +waive --rule=case-missing-default diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fa09b92 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,96 @@ +# Copyright 2025 ETH Zurich and University of Bologna. +# Solderpad Hardware License, Version 0.51, see LICENSE for details. +# SPDX-License-Identifier: SHL-0.51 +# +# Run functional regression checks +name: ci +on: [push, pull_request] + +jobs: + hwpe-tests: + name: hwpe-tests (NB_CONTEXT=${{ matrix.nb_context }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + # NB_CONTEXT is a Verilator elaboration-time parameter (-GNB_CONTEXT, + # see target/sim/verilator/verilator.mk), so each value needs its own + # model build: run the whole regression once per context count (same + # reasoning as RedMulE's ProbStall matrix in its own ci.yml). + matrix: + nb_context: [2, 4] + env: + N_PROC: 4 + Target: verilator + Verilator: verilator + VERILATOR_VERSION: "5.046" + CCACHE_BASEDIR: ${{ github.workspace }} + CCACHE_COMPILERCHECK: content + + steps: + - uses: actions/checkout@v4 + + - name: Set up ccache + uses: hendrikmuhs/ccache-action@v1 + with: + key: ${{ runner.os }}-verilator-${{ env.VERILATOR_VERSION }}-nbcontext${{ matrix.nb_context }} + max-size: 2G + + - name: Install Verilator (prebuilt) + uses: veryl-lang/setup-verilator@v1 + with: + version: ${{ env.VERILATOR_VERSION }} + + - name: Install uv + uses: astral-sh/setup-uv@v8.3.2 + + - name: Install PeakRDL & prettytable + run: | + uv tool install peakrdl-cli --with peakrdl-regblock --with peakrdl-html --with peakrdl-cheader + uv venv --python python3 venv + source venv/bin/activate + uv pip install prettytable pyyaml junit-xml + + - name: Install bender + run: | + make bender + + - name: Add installed tools to PATH + run: | + echo "$(pwd)/vendor/install/cargo/bin" >> $GITHUB_PATH + echo "$HOME/.local/bin" >> $GITHUB_PATH + echo "$(pwd)/venv/bin" >> $GITHUB_PATH + + - name: Build verilator model + run: | + make hw-build-all target=verilator NbContext=${{ matrix.nb_context }} VerilatorJobs=4 + + - name: Run Tests + run: | + scripts/ci-regression.sh + + # Not present in RedMulE's ci.yml: surface the per-test transcripts as a + # downloadable artifact whenever the regression fails, since the "[TB] - + # Fail!" / $error output that pinpoints the failure only lives in + # target/sim/verilator/transcript_* on the runner and is otherwise lost + # once the job ends. + - name: Upload transcripts on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: transcripts-nbcontext${{ matrix.nb_context }} + path: target/sim/verilator/transcript_* + + # Aggregate gate with a fixed name: the matrix above reports one check per + # NB_CONTEXT value, so branch rulesets cannot pin a stable context to it. + # This is the job the ruleset requires -- keep its name in sync with the + # required status check. + run-hwpe-tests: + name: run-hwpe-tests + runs-on: ubuntu-latest + needs: hwpe-tests + if: always() + steps: + - name: Check matrix result + run: | + echo "hwpe-tests result: ${{ needs.hwpe-tests.result }}" + [ "${{ needs.hwpe-tests.result }}" = "success" ] diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..905724e --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,78 @@ +# Copyright 2025 ETH Zurich and University of Bologna. +# Solderpad Hardware License, Version 0.51, see LICENSE for details. +# SPDX-License-Identifier: SHL-0.51 +# +name: lint + +on: [ push, pull_request, workflow_dispatch ] + +jobs: + + lint-license: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v3 + - + name: Check license + uses: pulp-platform/pulp-actions/lint-license@v2 + with: + license: | + Copyright (\d{4}(-\d{4})?\s)?.* + (Solderpad Hardware License, Version 0.51|Licensed under the Apache License, Version 2.0), see LICENSE for details. + SPDX-License-Identifier: (SHL-0.51|Apache-2.0) + # Exclude generated/vendored content that falls outside this + # three-line header convention: + # - rtl/rdl-example is peakrdl-generated (make regif) and gitignored, + # so it is normally absent from a fresh checkout anyway; + # - uloop-example/ ships its own Apache-2.0 LICENSE.sw.txt with a + # differently worded, pre-existing header ("See LICENSE.sw.txt + # for details", no SPDX-License-Identifier line) and is not part + # of this WP's file set to rewrite. + # Waived non-RTL build/manifest files, which carry no header today: + # - Bender.yml and src_files.yml are dependency manifests; + # - rtl/rdl.sh and sim/* are the legacy QuestaSim helper scripts + # (the current Verilator flow lives under target/sim, which is + # NOT waived and is checked normally). + # Patterns are fnmatch'd against the repo-relative path, so `sim/*` + # matches only the top-level sim/ tree, not target/sim/. + exclude_paths: | + *.md + LICENSE* + uloop-example/* + rtl/rdl-example/* + *.rdl + Bender.yml + src_files.yml + rtl/rdl.sh + sim/* + + lint-sv: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v3 + - + name: Run Verible + uses: chipsalliance/verible-linter-action@main + with: + paths: rtl target/sim/src + # rtl/rdl-example is peakrdl-generated output (gitignored, produced + # by `make regif`); it will not pass style lint and is excluded the + # same way RedMulE excludes individual files here, via a path + # prefix matched against the discovered file list. + exclude_paths: | + rtl/rdl-example + extra_args: "--waiver_files .github/verible.waiver" + github_token: ${{ secrets.GITHUB_TOKEN }} + fail_on_error: true + reviewdog_reporter: github-check + log_file: verible-verilog-lint.log + - + name: Upload Verible Artifacts + uses: actions/upload-artifact@v4 + with: + name: lint-sv-artifacts + path: verible-verilog-lint.log diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b87fe89 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Bender / vendored tooling +.bender/ +vendor/ + +# Generated register interface (rtl/rdl.sh output) +rtl/rdl-example/ + +# Verilator simulation artifacts +target/sim/verilator/obj_dir*/ +target/sim/verilator/compile.verilator.tcl +target/sim/verilator/transcript_* +target/sim/verilator/*.vcd + +# Python +__pycache__/ diff --git a/Bender.lock b/Bender.lock new file mode 100644 index 0000000..ed243c8 --- /dev/null +++ b/Bender.lock @@ -0,0 +1,22 @@ +packages: + common_cells: + revision: 1281545696eb3fcba50ec5b4275993476a3c710e + version: 1.40.0 + source: + Git: https://github.com/pulp-platform/common_cells.git + dependencies: + - common_verification + - tech_cells_generic + common_verification: + revision: fb1885f48ea46164a10568aeff51884389f67ae3 + version: 0.2.5 + source: + Git: https://github.com/pulp-platform/common_verification.git + dependencies: [] + tech_cells_generic: + revision: 3a3de73632a06826b1bd9c65a0a2e92b32016845 + version: 0.2.14 + source: + Git: https://github.com/pulp-platform/tech_cells_generic.git + dependencies: + - common_verification diff --git a/Bender.yml b/Bender.yml index f26141e..5b1da54 100644 --- a/Bender.yml +++ b/Bender.yml @@ -33,3 +33,19 @@ sources: - rtl/deprecated/hwpe_ctrl_regfile.sv # Level 4 - rtl/deprecated/hwpe_ctrl_slave.sv + + - target: hwpe_ctrl_test + include_dirs: + - rtl/rdl-example + files: + - rtl/rdl-example/hwpe_ctrl_regif_example_pkg.sv + - rtl/rdl-example/hwpe_ctrl_regif_example.sv + - target/sim/src/hwpe_ctrl_target_wrap.sv + - target/sim/src/hwpe_ctrl_target_tb.sv + - target/sim/src/hwpe_ctrl_target_tb_wrap.sv + - target/sim/src/hwpe_ctrl_partial_mult_tb.sv + - target/sim/src/hwpe_ctrl_partial_mult_tb_wrap.sv + - target/sim/src/hwpe_ctrl_seq_mult_tb.sv + - target/sim/src/hwpe_ctrl_seq_mult_tb_wrap.sv + - target/sim/src/hwpe_ctrl_uloop_tb.sv + - target/sim/src/hwpe_ctrl_uloop_tb_wrap.sv diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..76de459 --- /dev/null +++ b/Makefile @@ -0,0 +1,53 @@ +# Copyright 2026 ETH Zurich and University of Bologna. +# Solderpad Hardware License, Version 0.51, see LICENSE for details. +# SPDX-License-Identifier: SHL-0.51 +# +# Top-level Makefile + +# Paths to folders +RootDir := $(dir $(abspath $(firstword $(MAKEFILE_LIST)))) +TargetDir := $(RootDir)target +SimDir := $(TargetDir)/sim + +Bender ?= bender + +target ?= verilator +TargetPath := $(SimDir)/$(target) + +# Included makefrags +include $(TargetPath)/$(target).mk +include bender_common.mk +include bender_sim.mk + +# Useful Parameters +gui ?= 0 + +SHELL := /bin/bash + +# Regenerate the example register interface (rtl/rdl-example) from the +# PeakRDL source (rtl/hwpe_ctrl_regif_example.rdl). +.PHONY: regif regif-clean +regif: + cd rtl && ./rdl.sh + +regif-clean: + rm -rf rtl/rdl-example + +clean-all: + rm -rf $(RootDir).bender + +# Install tools +VendorDir ?= $(RootDir)vendor +InstallDir ?= $(VendorDir)/install +# Bender (installed from prebuilt release binaries, no Rust toolchain needed) +BenderVersion ?= 0.32.1 +CargoInstallDir := $(InstallDir)/cargo + +bender: $(CargoInstallDir)/bin/bender + +$(CargoInstallDir)/bin/bender: + mkdir -p $(InstallDir) + curl --proto '=https' --tlsv1.2 -sSfL https://github.com/pulp-platform/bender/releases/download/v$(BenderVersion)/bender-installer.sh > $(InstallDir)/bender-installer.sh + BENDER_INSTALL_DIR=$(CargoInstallDir) BENDER_NO_MODIFY_PATH=1 BENDER_DISABLE_UPDATE=1 \ + sh $(InstallDir)/bender-installer.sh + rm -f $(InstallDir)/bender-installer.sh diff --git a/bender_common.mk b/bender_common.mk new file mode 100644 index 0000000..4ef5092 --- /dev/null +++ b/bender_common.mk @@ -0,0 +1,13 @@ +# Copyright 2025 ETH Zurich and University of Bologna. +# Solderpad Hardware License, Version 0.51, see LICENSE for details. +# SPDX-License-Identifier: SHL-0.51 +# +# Makefragment holding common bender flags shared across simulation and +# (future) synthesis flows. hwpe-ctrl has no core-specific configuration +# (unlike e.g. RedMulE, which uses this file to select cv32e40p/cv32e40x +# flags), so common_targs/common_defs are intentionally left empty here. +# The file is kept as a parity placeholder so downstream flows can simply +# append to common_targs/common_defs without needing to guard the include. + +common_targs += +common_defs += diff --git a/bender_sim.mk b/bender_sim.mk new file mode 100644 index 0000000..5890916 --- /dev/null +++ b/bender_sim.mk @@ -0,0 +1,8 @@ +# Copyright 2025 ETH Zurich and University of Bologna. +# Solderpad Hardware License, Version 0.51, see LICENSE for details. +# SPDX-License-Identifier: SHL-0.51 +# +# Makefragment for simulation-only bender flags. + +sim_targs += -t rtl +sim_targs += -t hwpe_ctrl_test diff --git a/rtl/deprecated/hwpe_ctrl_regfile.sv b/rtl/deprecated/hwpe_ctrl_regfile.sv index c86f8aa..c075334 100644 --- a/rtl/deprecated/hwpe_ctrl_regfile.sv +++ b/rtl/deprecated/hwpe_ctrl_regfile.sv @@ -1,3 +1,7 @@ +// Copyright 2014-2018 ETH Zurich and University of Bologna. +// Solderpad Hardware License, Version 0.51, see LICENSE for details. +// SPDX-License-Identifier: SHL-0.51 + /* * hwpe_ctrl_regfile.sv * Francesco Conti diff --git a/rtl/deprecated/hwpe_ctrl_regfile_ff.sv b/rtl/deprecated/hwpe_ctrl_regfile_ff.sv index c76aaaa..126a3f8 100644 --- a/rtl/deprecated/hwpe_ctrl_regfile_ff.sv +++ b/rtl/deprecated/hwpe_ctrl_regfile_ff.sv @@ -1,3 +1,7 @@ +// Copyright 2014-2018 ETH Zurich and University of Bologna. +// Solderpad Hardware License, Version 0.51, see LICENSE for details. +// SPDX-License-Identifier: SHL-0.51 + /* * hwpe_ctrl_regfile_latch.sv * Francesco Conti diff --git a/rtl/deprecated/hwpe_ctrl_regfile_latch.sv b/rtl/deprecated/hwpe_ctrl_regfile_latch.sv index a8e77e0..956980a 100644 --- a/rtl/deprecated/hwpe_ctrl_regfile_latch.sv +++ b/rtl/deprecated/hwpe_ctrl_regfile_latch.sv @@ -1,3 +1,7 @@ +// Copyright 2014-2018 ETH Zurich and University of Bologna. +// Solderpad Hardware License, Version 0.51, see LICENSE for details. +// SPDX-License-Identifier: SHL-0.51 + /* * hwpe_ctrl_regfile_latch.sv * Francesco Conti diff --git a/rtl/deprecated/hwpe_ctrl_regfile_latch_test_wrap.sv b/rtl/deprecated/hwpe_ctrl_regfile_latch_test_wrap.sv index dc6d60f..504c616 100644 --- a/rtl/deprecated/hwpe_ctrl_regfile_latch_test_wrap.sv +++ b/rtl/deprecated/hwpe_ctrl_regfile_latch_test_wrap.sv @@ -1,3 +1,7 @@ +// Copyright 2014-2018 ETH Zurich and University of Bologna. +// Solderpad Hardware License, Version 0.51, see LICENSE for details. +// SPDX-License-Identifier: SHL-0.51 + /* * hwpe_ctrl_regfile_latch.sv * Francesco Conti diff --git a/rtl/deprecated/hwpe_ctrl_slave.sv b/rtl/deprecated/hwpe_ctrl_slave.sv index 1be387a..b478fdb 100644 --- a/rtl/deprecated/hwpe_ctrl_slave.sv +++ b/rtl/deprecated/hwpe_ctrl_slave.sv @@ -1,3 +1,7 @@ +// Copyright 2014-2018 ETH Zurich and University of Bologna. +// Solderpad Hardware License, Version 0.51, see LICENSE for details. +// SPDX-License-Identifier: SHL-0.51 + /* * hwpe_ctrl_slave.sv * Francesco Conti diff --git a/rtl/hwpe_ctrl_interfaces.sv b/rtl/hwpe_ctrl_interfaces.sv index b84f12d..f849fea 100644 --- a/rtl/hwpe_ctrl_interfaces.sv +++ b/rtl/hwpe_ctrl_interfaces.sv @@ -1,3 +1,7 @@ +// Copyright 2014-2018 ETH Zurich and University of Bologna. +// Solderpad Hardware License, Version 0.51, see LICENSE for details. +// SPDX-License-Identifier: SHL-0.51 + /* * hwpe_ctrl_interfaces.sv * Francesco Conti diff --git a/rtl/hwpe_ctrl_package.sv b/rtl/hwpe_ctrl_package.sv index c0b15d3..57a454b 100644 --- a/rtl/hwpe_ctrl_package.sv +++ b/rtl/hwpe_ctrl_package.sv @@ -1,3 +1,7 @@ +// Copyright 2014-2018 ETH Zurich and University of Bologna. +// Solderpad Hardware License, Version 0.51, see LICENSE for details. +// SPDX-License-Identifier: SHL-0.51 + /* * hwpe_ctrl_package.sv * Francesco Conti diff --git a/rtl/hwpe_ctrl_partial_mult.sv b/rtl/hwpe_ctrl_partial_mult.sv index 9e837a7..1f2c1cf 100644 --- a/rtl/hwpe_ctrl_partial_mult.sv +++ b/rtl/hwpe_ctrl_partial_mult.sv @@ -1,3 +1,7 @@ +// Copyright 2014-2026 ETH Zurich and University of Bologna. +// Solderpad Hardware License, Version 0.51, see LICENSE for details. +// SPDX-License-Identifier: SHL-0.51 + /* * hwpe_ctrl_partial_mult.sv * Francesco Conti diff --git a/rtl/hwpe_ctrl_regif_example.rdl b/rtl/hwpe_ctrl_regif_example.rdl index 7af44a7..52d59e2 100644 --- a/rtl/hwpe_ctrl_regif_example.rdl +++ b/rtl/hwpe_ctrl_regif_example.rdl @@ -23,7 +23,7 @@ addrmap hwpe_ctrl_regif_example { desc = "Control register map for the HWPE, including mandatory control/status registers and example job-independent and job-dependent configuration registers."; // Mandatory COMMIT_TRIGGER register. Not to be updated inside HWPEs. - reg commit_trigger { + reg hwpe_commit_trigger { field { name = "reserved"; desc = "Reserved."; @@ -36,20 +36,20 @@ addrmap hwpe_ctrl_regif_example { hw = r; sw = w; swacc = true; - } value[1:0] = 0; + } commit_trigger[1:0] = 0; }; // Mandatory ACQUIRE register. Not to be updated inside HWPEs. - reg acquire { + reg hwpe_acquire { field { name = "acquire"; desc = "On read starts a job offload, locks controller. Returns job ID."; hw = w; sw = r; swacc = true; - } value[31:0] = 0; + } acquire[31:0] = 0; }; // Mandatory AUTOTRIGGER register. Not to be updated inside HWPEs. - reg autotrigger { + reg hwpe_autotrigger { field { name = "reserved"; desc = "Reserved."; @@ -64,16 +64,16 @@ addrmap hwpe_ctrl_regif_example { } autotrigger_n[0:0] = 0; }; // Mandatory STATUS register. Not to be updated inside HWPEs. - reg status { + reg hwpe_status { field { name = "status"; desc = "Status of currently running job."; hw = w; sw = r; - } value[31:0] = 0; + } status0[31:0] = 0; }; // Mandatory RUNNING_JOB register. Not to be updated inside HWPEs. - reg running_job { + reg hwpe_running_job { field { name = "reserved"; desc = "Reserved."; @@ -85,10 +85,10 @@ addrmap hwpe_ctrl_regif_example { desc = "Returns ID of currently running job if any job is running; otherwise, of the last job that has been run."; hw = w; sw = r; - } value[7:0] = 0; + } running_job[7:0] = 0; }; // Mandatory SOFT_CLEAR register. Not to be updated inside HWPEs. - reg soft_clear { + reg hwpe_soft_clear { field { name = "reserved"; desc = "Reserved."; @@ -101,43 +101,125 @@ addrmap hwpe_ctrl_regif_example { hw = r; sw = w; swacc = true; - } value[1:0] = 0; + } soft_clear[1:0] = 0; }; // Mandatory RESERVED register. Not to be updated inside HWPEs. - reg reserved { + reg hwpe_reserved { field { name = "reserved"; desc = "Reserved."; hw = r; sw = r; - } value[31:0] = 0; + } reserved[31:0] = 0; }; // "mandatory" set of HWPE registers (CONTROL regs). Not to be updated inside HWPEs. - regfile ctrl_mandatory { - commit_trigger commit_trigger @ 0x00; - acquire acquire @ 0x04; - autotrigger autotrigger @ 0x08; - status status @ 0x0c; - running_job running_job @ 0x10; - soft_clear soft_clear @ 0x14; - reserved reserved1 @ 0x18; - reserved reserved2 @ 0x1c; + regfile hwpe_ctrl_mandatory { + hwpe_commit_trigger commit_trigger @ 0x00; + hwpe_acquire acquire @ 0x04; + hwpe_autotrigger autotrigger @ 0x08; + hwpe_status status @ 0x0c; + hwpe_running_job running_job @ 0x10; + hwpe_soft_clear soft_clear @ 0x14; + hwpe_reserved reserved1 @ 0x18; + hwpe_reserved reserved2 @ 0x1c; + }; + + // Example job-independent register: a simple software-writable, + // hardware-readable configuration value with a zero reset. Replace with + // the actual job-independent registers of your HWPE. + reg config_a { + field { + name = "config_a"; + desc = "Example job-independent configuration value."; + hw = r; + sw = rw; + } value[31:0] = 0; + }; + // Example job-independent register: a simple software-writable, + // hardware-readable configuration value with a nonzero reset, to exercise + // nonzero-reset coverage. Replace with the actual job-independent + // registers of your HWPE. + reg config_b { + field { + name = "config_b"; + desc = "Example job-independent configuration value with nonzero reset."; + hw = r; + sw = rw; + } value[31:0] = 32'hdead_beef; + }; + // Example job-independent register: a hardware-driven, software-readable + // status value, to exercise the hw->sw path (to be driven by the HWPE / + // TB wrapper). Replace with the actual job-independent registers of your + // HWPE. + reg hw_status { + field { + name = "hw_status"; + desc = "Example hardware-driven status value."; + hw = w; + sw = r; + } value[31:0] = 0; + }; + + // "job-independent" set of HWPE registers. Update inside HWPEs. + regfile hwpe_ctrl_job_indep { + desc = "Example job-independent register file. Replace with the actual job-independent registers of your HWPE."; + config_a config_a @ 0x00; + config_b config_b @ 0x04; + hw_status hw_status @ 0x08; }; - // "generic" set of HWPE registers. Update inside HWPEs. - regfile ctrl_job_indep { - reserved rr; + // Example job-dependent register: a simple software-writable, + // hardware-readable parameter. Replace with the actual job-dependent + // registers of your HWPE. + reg param0 { + field { + name = "param0"; + desc = "Example job-dependent parameter."; + hw = r; + sw = rw; + } value[31:0] = 0; + }; + // Example job-dependent register: a simple software-writable, + // hardware-readable parameter. Replace with the actual job-dependent + // registers of your HWPE. + reg param1 { + field { + name = "param1"; + desc = "Example job-dependent parameter."; + hw = r; + sw = rw; + } value[31:0] = 0; + }; + // Example job-dependent register featuring reserved-bit masking: only the + // lower 16 bits are meaningful. Replace with the actual job-dependent + // registers of your HWPE. + reg param2 { + field { + name = "reserved"; + desc = "Reserved."; + hw = r; + sw = r; + } r0[31:16] = 0; + field { + name = "length"; + desc = "Example length field, exercising reserved-bit masking."; + hw = r; + sw = rw; + } length[15:0] = 0; }; // "job-dependent" set of HWPE registers. Update inside HWPEs. - regfile ctrl_job_dep { - reserved rr; + regfile hwpe_ctrl_job_dep { + desc = "Example job-dependent register file. Replace with the actual job-dependent registers of your HWPE."; + param0 param0 @ 0x00; + param1 param1 @ 0x04; + param2 param2 @ 0x08; }; // HWPE control address map. Update inside HWPEs - ctrl_mandatory ctrl @ 0x00; - ctrl_job_indep generic @ 0x20; - ctrl_job_dep job_dep @ 0x40; + hwpe_ctrl_mandatory hwpe_ctrl @ 0x00; + hwpe_ctrl_job_dep hwpe_job_dep @ 0x20; + hwpe_ctrl_job_indep hwpe_job_indep @ 0x40; }; diff --git a/rtl/hwpe_ctrl_seq_mult.sv b/rtl/hwpe_ctrl_seq_mult.sv index 023faa4..6d6bab5 100644 --- a/rtl/hwpe_ctrl_seq_mult.sv +++ b/rtl/hwpe_ctrl_seq_mult.sv @@ -1,3 +1,7 @@ +// Copyright 2014-2018 ETH Zurich and University of Bologna. +// Solderpad Hardware License, Version 0.51, see LICENSE for details. +// SPDX-License-Identifier: SHL-0.51 + /* * hwpe_ctrl_seq_mult.sv * Francesco Conti diff --git a/rtl/hwpe_ctrl_target.sv b/rtl/hwpe_ctrl_target.sv index 0385b21..0485eb2 100644 --- a/rtl/hwpe_ctrl_target.sv +++ b/rtl/hwpe_ctrl_target.sv @@ -1,3 +1,7 @@ +// Copyright 2014-2025 ETH Zurich and University of Bologna. +// Solderpad Hardware License, Version 0.51, see LICENSE for details. +// SPDX-License-Identifier: SHL-0.51 + /* * hwpe_ctrl_target.sv * Francesco Conti diff --git a/rtl/hwpe_ctrl_uloop.sv b/rtl/hwpe_ctrl_uloop.sv index 099771c..6ce9c6e 100644 --- a/rtl/hwpe_ctrl_uloop.sv +++ b/rtl/hwpe_ctrl_uloop.sv @@ -1,3 +1,7 @@ +// Copyright 2014-2018 ETH Zurich and University of Bologna. +// Solderpad Hardware License, Version 0.51, see LICENSE for details. +// SPDX-License-Identifier: SHL-0.51 + /* * hwpe_ctrl_uloop.sv * Francesco Conti diff --git a/rtl/include/hwpe_ctrl_helpers.svh b/rtl/include/hwpe_ctrl_helpers.svh index 54c43a0..f2559a1 100644 --- a/rtl/include/hwpe_ctrl_helpers.svh +++ b/rtl/include/hwpe_ctrl_helpers.svh @@ -1,3 +1,7 @@ +// Copyright 2024 ETH Zurich and University of Bologna. +// Solderpad Hardware License, Version 0.51, see LICENSE for details. +// SPDX-License-Identifier: SHL-0.51 + /* * hwpe_ctrl_helpers.svh * Francesco Conti diff --git a/rtl/rdl.sh b/rtl/rdl.sh index 4709487..9909795 100755 --- a/rtl/rdl.sh +++ b/rtl/rdl.sh @@ -1,6 +1,17 @@ #!/bin/bash +# peakrdl regblock does not create its output directory on its own (unlike +# the html/c-header subcommands), so it must exist beforehand. +mkdir -p rdl-example/ peakrdl regblock hwpe_ctrl_regif_example.rdl -o rdl-example/ --cpuif passthrough --default-reset arst_n --hwif-report --addr-width 32 peakrdl html hwpe_ctrl_regif_example.rdl -o rdl-example/html/ peakrdl c-header hwpe_ctrl_regif_example.rdl -o rdl-example/hwpe_ctrl_target.h -# PeakRDL uses unpacked structs to avoid issues at compile time, which is commendable, but incompatible with FIFOing the output of the job! -sed -i 's/typedef[[:space:]]\+struct\b/typedef struct packed/g' rdl-example/hwpe_ctrl_regif_example_pkg.sv +# PeakRDL uses unpacked structs to avoid issues at compile time, which is commendable, but incompatible with FIFOing the output of the job! (use portable sed syntax that works on both Linux and macOS) +sed -E 's/typedef[[:space:]]+struct([[:space:]])/typedef struct packed\1/g' rdl-example/hwpe_ctrl_regif_example_pkg.sv > rdl-example/hwpe_ctrl_regif_example_pkg.sv.tmp && mv rdl-example/hwpe_ctrl_regif_example_pkg.sv.tmp rdl-example/hwpe_ctrl_regif_example_pkg.sv + +# PeakRDL does not emit a license header; prepend the repository's SPDX header to the generated SystemVerilog. +HEADER='// Copyright 2026 ETH Zurich and University of Bologna. +// Solderpad Hardware License, Version 0.51, see LICENSE for details. +// SPDX-License-Identifier: SHL-0.51' +for f in rdl-example/hwpe_ctrl_regif_example.sv rdl-example/hwpe_ctrl_regif_example_pkg.sv; do + printf '%s\n' "$HEADER" | cat - "$f" > "$f.tmp" && mv "$f.tmp" "$f" +done diff --git a/scripts/bwruntests.py b/scripts/bwruntests.py new file mode 100755 index 0000000..5be7653 --- /dev/null +++ b/scripts/bwruntests.py @@ -0,0 +1,389 @@ +#!/usr/bin/env python3 +# Copyright 2020 ETH Zurich and University of Bologna +# Licensed under the Apache License, Version 2.0, see LICENSE for details. +# SPDX-License-Identifier: Apache-2.0 +# + +# Run shell commands listed in a file separated by newlines in a parallel +# fashion. If requested the results (tuples consisting of command, stdout, +# stderr and returncode) will be gathered in a junit.xml file. There a few +# knobs to tune the number of spawned processes and the junit.xml formatting. + +# Author: Robert Balas (balasr@iis.ee.ethz.ch) + +# Vendored from https://github.com/pulp-platform/neureka/blob/main/regr/bwruntests.py +# for use in RedMulE's regression infrastructure, with two fixes to fork(): +# 1. commands are now kept as a single string (instead of shlex.split into a +# list), since Popen(, shell=True) only runs list[0] as the shell +# command and silently drops the rest of the words; +# 2. the shell= kwarg is now actually forwarded to Popen() -- previously it +# was accepted as a named parameter but never passed through, so +# Popen() always used its shell=False default. + +import argparse +import re +from subprocess import (Popen, TimeoutExpired, + CalledProcessError, PIPE) +from threading import Lock +import sys +import signal +import os +import multiprocessing +import errno +import pprint +import time +import random +from collections import OrderedDict +import json + +runtest = argparse.ArgumentParser( + prog='bwruntests', + formatter_class=argparse.RawDescriptionHelpFormatter, + description="""Run PULP tests in parallel""", + epilog=""" +Test_file needs to be either a .yaml file (set the --yaml switch) +which looks like this: + +mytests.yml +[...] +parallel_bare_tests: # name of the test set + parMatrixMul8: # name of the test + path: ./parallel_bare_tests/parMatrixMul8 # path to the test's folder + command: make clean all run # command to run in the test's folder +[...] + +or + +Test_file needs to be a list of commands to be executed. Each line corresponds +to a single command and a test + +commands.f +[...] +make -C ./ml_tests/mlGrad clean all run +make -C ./ml_tests/mlDct clean all run +[...] + +Example: +bwruntests.py --proc-verbose -v \\ + --report_junit -t 3600 --yaml \\ + -o simplified-runtime.xml runtime-tests.yaml + +This Runs a set of tests defined in runtime-tests.yaml and dumps the +resulting junit.xml into simplified-runtime.xml. The --proc-verbose +scripts makes sure to print the stdout of each process to the shell. To +prevent a broken process from running forever, a maximum timeout of 3600 +seconds was set. For debugging purposes we enabled -v (--verbose) which +shows the full set of commands being run.""") + +runtest.version = '0.2' + +runtest.add_argument('test_file', type=str, + help='file defining tests to be run') +runtest.add_argument('--version', action='version', + version='%(prog)s ' + runtest.version) +runtest.add_argument('-p', '--max_procs', type=int, + default=multiprocessing.cpu_count(), + help="""Number of parallel + processes used to run test. + Default is number of cpu cores.""") +runtest.add_argument('-t', '--timeout', type=float, + default=None, + help="""Timeout for all processes in seconds""") +runtest.add_argument('-v', '--verbose', action='store_true', + help="""Enable verbose output""") +runtest.add_argument('-s', '--proc_verbose', action='store_true', + help="""Write processes' stdout and stderr to shell stdout + after they terminate""") +runtest.add_argument('--report_junit', action='store_true', + help="""Generate a junit report""") +runtest.add_argument('--disable_junit_pp', action='store_true', + help="""Disable pretty print of junit report""") +runtest.add_argument('--disable_results_pp', action='store_true', + help="""Disable printing test results""") +runtest.add_argument('-y,', '--yaml', action='store_true', + help="""Read tests from yaml file instead of executing + from a list of commands""") +runtest.add_argument('-o,', '--output', type=str, + help="""Write junit.xml to file instead of stdout""") +runtest.add_argument('-P,', '--perf', type=str, default=None, + help="""Write performance results to JSON file""") +stdout_lock = Lock() + +shared_total = 0 +len_total = 0 + +class FinishedProcess(object): + """A process that has finished running. + """ + def __init__(self, name, cwd, runargs, returncode, + stdout=None, stderr=None, time=None): + self.name = name + self.cwd = cwd + self.runargs = runargs + self.returncode = returncode + self.stdout = stdout + self.stderr = stderr + self.time = time + exec_time = 0 + throughput = 0 + workload = 0 + if returncode == 0: + matches = re.findall(r"# hwpe cycles =\s+(\d+)", stdout) + if matches: + exec_time = int(matches[0]) + self.exec_time = exec_time + + + def __repr__(self): + runargs = ['name={!r}'.format(self.name)] + runargs += ['cwd={!r}'.format(self.cwd)] + runargs += ['args={!r}'.format(self.runargs), + 'returncode={!r}'.format(self.returncode)] + if self.stdout is not None: + runargs.append('stdout={!r}'.format(self.stdout)) + if self.stderr is not None: + runargs.append('stderr={!r}'.format(self.stderr)) + if self.time is not None: + runargs.append('time={!r}'.format(self.time)) + return "{}({})".format(type(self).__name__, ', '.join(runargs)) + +def fork(name, cwd, *popenargs, check=False, shell=True, + **kwargs): + """Run subprocess and return process args, error code, stdout and stderr + """ + + def proc_out(cwd, stdout, stderr): + print('cwd={}'.format(cwd)) + print('stdout=') + print(stdout.decode('utf-8')) + print('stderr=') + print(stderr.decode('utf-8')) + + kwargs['stdout'] = PIPE + kwargs['stderr'] = PIPE + + with Popen(*popenargs, preexec_fn=os.setpgrp, cwd=cwd, shell=shell, + **kwargs) as process: + try: + # Child and parent are racing for setting/using the pgid so we have + # to set it in both processes. See glib manual. + try: + os.setpgid(process.pid, process.pid) + except OSError as e: + if e.errno != errno.EACCES: + raise + # measure runtime + start = time.time() + stdout, stderr = process.communicate(input, timeout=proc_timeout) + except TimeoutExpired: + pgid = os.getpgid(process.pid) + os.killpg(pgid, signal.SIGKILL) + # process.kill() will only kill the immediate child but not its + # forks. This won't work since our commands will create a few forks + # (make -> vsim -> etc). We need to make a process group and kill + # that + stdout, stderr = process.communicate() + timeoutmsg = 'TIMEOUT after {:f}s'.format(proc_timeout) + + if proc_verbose: + stdout_lock.acquire() + print(name) + print(timeoutmsg) + proc_out(cwd, stdout, stderr) + stdout_lock.release() + + return FinishedProcess(name, cwd, process.args, 1, + stdout.decode('utf-8'), + timeoutmsg + '\n' + + stderr.decode('utf-8'), + time.time() - start) + # Including KeyboardInterrupt, communicate handled that. + except: # noqa: E722 + pgid = os.getpgid(process.pid) + os.killpg(pgid, signal.SIGKILL) + # We don't call process.wait() as .__exit__ does that for us. + raise + retcode = process.poll() + if check and retcode: + raise CalledProcessError(retcode, process.args, + output=stdout, stderr=stderr) + if proc_verbose: + stdout_lock.acquire() + print(name) + proc_out(cwd, stdout, stderr) + stdout_lock.release() + + with lock: + shared_total.value += 1 + print("[%s][%d/%d] %s" % ("\033[1;32m OK \033[0m" if retcode == 0 else "\033[1;31mFAIL\033[0m", shared_total.value, len_total.value, name)) + + return FinishedProcess(name, cwd, process.args, retcode, + stdout.decode('utf-8'), + stderr.decode('utf-8'), + time.time() - start) + +def poolInit(s, t, l, timeout, verbose): + global shared_total + global len_total + global lock + global proc_timeout + global proc_verbose + shared_total = s + len_total = t + lock = l + proc_timeout = timeout + proc_verbose = verbose + +if __name__ == '__main__': + args = runtest.parse_args() + pp = pprint.PrettyPrinter(indent=4) + + # lazy importing so that we can work without junit_xml + if args.report_junit: + try: + from junit_xml import TestSuite, TestCase + except ImportError: + print("""Error: The --report_junit option requires +the junit_xml library which is not installed.""", + file=sys.stderr) + exit(1) + + # lazy import PrettyTable for displaying results + if not(args.disable_results_pp): + try: + from prettytable import PrettyTable + except ImportError: + print("""Warning: Displaying results requires the PrettyTable +library which is not installed""") + + tests = [] # list of tuple (testname, working dir, command) + + # load tests (yaml or command list) + if args.yaml: + try: + import yaml + except ImportError: + print("""Error: The --yaml option requires +the pyyaml library which is not installed.""", + file=sys.stderr) + exit(1) + with open(args.test_file) as f: + testyaml = yaml.load(f, Loader=yaml.Loader) + for testsetname, testv in testyaml.items(): + for testname, insn in testv.items(): + cmd = insn['command'] + cwd = insn['path'] + tests.append((testsetname + ':' + testname, cwd, cmd)) + if args.verbose: + pp.pprint(tests) + else: # (command list) + with open(args.test_file) as f: + testnames = list(map(str.rstrip, f)) + shellcmds = list(testnames) + cwds = ['./' for e in testnames] + tests = list(zip(testnames, cwds, shellcmds)) + if args.verbose: + print('Tests which we are running:') + pp.pprint(tests) + pp.pprint(shellcmds) + + # Spawning process pool + # Disable signals to prevent race. Child processes inherit SIGINT handler + original_sigint_handler = signal.signal(signal.SIGINT, signal.SIG_IGN) + lock = multiprocessing.Lock() + shared_total = multiprocessing.Value('i', 0) + len_total = multiprocessing.Value('i', len(tests)) + pool = multiprocessing.Pool(processes=args.max_procs, initializer=poolInit, initargs=(shared_total, len_total, lock, args.timeout, args.proc_verbose )) + # Restore SIGINT handler + signal.signal(signal.SIGINT, original_sigint_handler) + # Shuffle tests + random.shuffle(tests) + try: + procresults = pool.starmap(fork, tests) + except KeyboardInterrupt: + print("\nTerminating bwruntest.py") + pool.terminate() + pool.join() + exit(1) + + # pp.pprint(procresults) + pool.close() + pool.join() + + # Generate junit.xml file. Junit.xml differentiates between failure and + # errors but we treat everything as errors. + if args.report_junit: + testcases = [] + for p in procresults: + # we can either expect p.name = testsetname:testname + # or p.name = testname + testcase = TestCase(p.name, + classname=((p.name).split(':'))[0], + stdout=p.stdout, + stderr=p.stderr, + elapsed_sec=p.time) + if p.returncode != 0: + testcase.add_failure_info(p.stderr) + testcases.append(testcase) + + testsuite = TestSuite('bwruntests', testcases) + if args.output: + with open(args.output, 'w') as f: + TestSuite.to_file(f, [testsuite], + prettyprint=not(args.disable_junit_pp)) + else: + print(TestSuite.to_xml_string([testsuite], + prettyprint=(args.disable_junit_pp))) + + # # print JSON for performance regression + # if args.perf is not None: + # # if file does not exist, create new dictionary: + # if not os.path.isfile(args.perf): + # d = OrderedDict([]) + # # else, load the existing dictionary + # else: + # with open(args.perf) as f: + # d = json.load(f, object_pairs_hook=OrderedDict) + # # save the new execution times + # for p in procresults: + # if p.returncode == 0: + # d[p.name] = p.exec_time + # with open(args.perf, 'w', encoding='utf-8') as f: + # json.dump(d, f, ensure_ascii=False, indent=4) + + # print JSON for performance regression + if args.perf is not None: + # if file does not exist, create new dictionary: + if not os.path.isfile(args.perf): + d = list([]) + # else, load the existing dictionary + else: + with open(args.perf) as f: + d = json.load(f) + # save the new execution times + for p in procresults: + if p.returncode == 0: + d.append({ 'name': p.name, 'value': p.exec_time, 'unit': 'cycles'}) + with open(args.perf, 'w', encoding='utf-8') as f: + json.dump(d, f, ensure_ascii=False, indent=4) + + # print summary of test results + if not(args.disable_results_pp): + testcount = sum(1 for x in tests) + testfailcount = sum(1 for p in procresults if p.returncode != 0) + testpassedcount = testcount - testfailcount + resulttable = PrettyTable(['test', 'cycles', 'time', 'passed/total']) + resulttable.align['test'] = "l" + for p in procresults: + testpassed = 1 if p.returncode == 0 else 0 + testname = p.name + resulttable.add_row([testname, + p.exec_time, + '{0:.2f}s'.format(p.time), + '{0:d}/{1:d}'.format(testpassed, 1)]) + resulttable.add_row(['total', '', '', '{0:d}/{1:d}'. + format(testpassedcount, testcount)]) + print(resulttable) + if testpassedcount != testcount: + import sys; sys.exit(1) + diff --git a/scripts/ci-regression.sh b/scripts/ci-regression.sh new file mode 100755 index 0000000..9cf3ab5 --- /dev/null +++ b/scripts/ci-regression.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# Copyright 2025 ETH Zurich and University of Bologna. +# Solderpad Hardware License, Version 0.51, see LICENSE for details. +# SPDX-License-Identifier: SHL-0.51 +# +# Runs the hwpe-ctrl regression suite (scripts/regression.yml) through +# scripts/bwruntests.py. bwruntests.py fans the individual test commands out +# over a multiprocessing.Pool and calls sys.exit(1) if any of them failed -- +# that non-zero exit code is what fails this step in CI. +# +# Adapted from RedMulE's scripts/ci-regression.sh; the --perf flag (which +# feeds RedMulE's cycle-count benchmark tracking via scripts/perf.json) is +# dropped since hwpe-ctrl has no benchmark to publish. + +Red="\e[31m" +Green="\e[32m" +EndColor="\e[0m" + +if [ -z "$Target" ]; then + echo -e "${Red}Error: no Target defined. Set the Target variable to \"vsim\" or \"verilator\" before continue.${EndColor}" + exit 1 +fi + +ScriptDir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +BASE_TIMEOUT=500 +REGR_FILE="${REGR_FILE:-$ScriptDir/regression.yml}" +# Number of tests to run concurrently. Each entry in regression.yml only +# invokes `make hw-run` (no rebuild) against the model built once per CI +# matrix leg, so the runs are isolated and safe to parallelize. Override +# N_PROC per invocation (bounded by available cores). +N_PROC="${N_PROC:-4}" + +export Target + +# be verbose in ci regression +python3 "$ScriptDir/bwruntests.py" -s --yaml -t $BASE_TIMEOUT -p "$N_PROC" "$REGR_FILE" diff --git a/scripts/regression-smoke.yml b/scripts/regression-smoke.yml new file mode 100644 index 0000000..4454c6b --- /dev/null +++ b/scripts/regression-smoke.yml @@ -0,0 +1,16 @@ +# Copyright 2025 ETH Zurich and University of Bologna. +# Solderpad Hardware License, Version 0.51, see LICENSE for details. +# SPDX-License-Identifier: SHL-0.51 +# +# Single-test smoke check for the regression runner (scripts/bwruntests.py). +# Useful for quickly validating the pipeline without paying for the full +# scripts/regression.yml sweep. Requires a verilator model already built, +# e.g.: +# +# make hw-build target=verilator NbContext=2 VerilatorJobs=4 +# Target=verilator REGR_FILE=scripts/regression-smoke.yml ./scripts/ci-regression.sh + +hwpe_ctrl_target_regression_smoke: + reg_access: + path: . + command: make hw-run target=$Target TEST=reg_access | tee target/sim/$Target/transcript_reg_access && grep -q '\[TB\] - Success!' target/sim/$Target/transcript_reg_access diff --git a/scripts/regression.yml b/scripts/regression.yml new file mode 100644 index 0000000..0e44b4f --- /dev/null +++ b/scripts/regression.yml @@ -0,0 +1,54 @@ +# Copyright 2025 ETH Zurich and University of Bologna. +# Solderpad Hardware License, Version 0.51, see LICENSE for details. +# SPDX-License-Identifier: SHL-0.51 +# +# Regression test matrix for hwpe-ctrl, run via scripts/bwruntests.py (see +# scripts/ci-regression.sh). The verilator models are built once per CI +# matrix leg (.github/workflows/ci.yml runs `make hw-build-all` before this +# file is used, which builds every top in +# target/sim/verilator/verilator.mk's MODULES list), so every entry below +# only runs `make hw-run Module= TEST=` -- no rebuild -- against +# the already-built model for that top, and checks the transcript for the +# "[TB] - Success!" marker the testbench prints on pass (it prints +# "[TB] - Fail!" and calls $error on failure, either way followed by +# $finish). +# +# `make hw-run` on its own always exits 0 (it just runs the compiled +# simulation binary to completion; a failing testbench does not make +# Verilator itself return non-zero), so pass/fail cannot be read off the +# `make hw-run` exit code directly. Piping through `tee` lets bwruntests.py +# still show the live transcript while it is captured to a file; the +# trailing `grep -q '\[TB\] - Success!' ...` is the actual pass/fail +# arbiter bwruntests.py checks the exit code of, since it is the last +# command in the `&&` chain. This mirrors RedMulE's scripts/regression.yml +# idiom -- keep it, and keep grep after the pipe, not folded into it. +# +# Transcript filenames are unique per test (not just per Module) since +# hw-run writes to $(VerilatorDir)/transcript_, and two Modules could +# in principle share a TEST name. + +hwpe_ctrl_target_regression: + reg_access: + path: . + command: make hw-run target=$Target Module=hwpe_ctrl_target_tb TEST=reg_access | tee target/sim/$Target/transcript_reg_access && grep -q '\[TB\] - Success!' target/sim/$Target/transcript_reg_access + offload: + path: . + command: make hw-run target=$Target Module=hwpe_ctrl_target_tb TEST=offload | tee target/sim/$Target/transcript_offload && grep -q '\[TB\] - Success!' target/sim/$Target/transcript_offload + fifo_backpressure: + path: . + command: make hw-run target=$Target Module=hwpe_ctrl_target_tb TEST=fifo_backpressure | tee target/sim/$Target/transcript_fifo_backpressure && grep -q '\[TB\] - Success!' target/sim/$Target/transcript_fifo_backpressure + soft_clear: + path: . + command: make hw-run target=$Target Module=hwpe_ctrl_target_tb TEST=soft_clear | tee target/sim/$Target/transcript_soft_clear && grep -q '\[TB\] - Success!' target/sim/$Target/transcript_soft_clear + uloop: + path: . + command: make hw-run target=$Target Module=hwpe_ctrl_uloop_tb TEST=uloop | tee target/sim/$Target/transcript_uloop && grep -q '\[TB\] - Success!' target/sim/$Target/transcript_uloop + partial_mult: + path: . + command: make hw-run target=$Target Module=hwpe_ctrl_partial_mult_tb TEST=partial_mult | tee target/sim/$Target/transcript_partial_mult && grep -q '\[TB\] - Success!' target/sim/$Target/transcript_partial_mult + seq_mult: + path: . + command: make hw-run target=$Target Module=hwpe_ctrl_seq_mult_tb TEST=seq_mult | tee target/sim/$Target/transcript_seq_mult && grep -q '\[TB\] - Success!' target/sim/$Target/transcript_seq_mult + seq_mult_invert: + path: . + command: make hw-run target=$Target Module=hwpe_ctrl_seq_mult_tb TEST=seq_mult_invert | tee target/sim/$Target/transcript_seq_mult_invert && grep -q '\[TB\] - Success!' target/sim/$Target/transcript_seq_mult_invert diff --git a/sim/partial_mult/build.sh b/sim/partial_mult/build.sh deleted file mode 100755 index fd7615b..0000000 --- a/sim/partial_mult/build.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -vlib hwpe_ctrl_lib -vmap hwpe_ctrl_lib hwpe_ctrl_lib -vlog -work hwpe_ctrl_lib +nowarnSVCHK -suppress 2275 -suppress 2583 -suppress 13314 ../../rtl/hwpe_ctrl_package.sv ../../rtl/hwpe_ctrl_partial_mult.sv ../../tb/tb_hwpe_ctrl_partial_mult.sv -vopt +acc=npr -o vopt_tb_hwpe_ctrl_partial_mult tb_hwpe_ctrl_partial_mult -work hwpe_ctrl_lib - diff --git a/sim/partial_mult/clean.sh b/sim/partial_mult/clean.sh deleted file mode 100755 index 20cec3c..0000000 --- a/sim/partial_mult/clean.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -rm -rf hwpe_ctrl_lib diff --git a/sim/partial_mult/sim.sh b/sim/partial_mult/sim.sh deleted file mode 100755 index 2e6b6ac..0000000 --- a/sim/partial_mult/sim.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -vsim -c -work hwpe_ctrl_lib vopt_tb_hwpe_ctrl_partial_mult - diff --git a/sim/uloop/build.sh b/sim/uloop/build.sh deleted file mode 100755 index 2c4e945..0000000 --- a/sim/uloop/build.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -vlib hwpe_ctrl_lib -vmap hwpe_ctrl_lib hwpe_ctrl_lib -vlog -work hwpe_ctrl_lib +nowarnSVCHK -suppress 2275 -suppress 2583 -suppress 13314 ../../rtl/hwpe_ctrl_package.sv ../../rtl/hwpe_ctrl_uloop.sv ../../tb/tb_hwpe_ctrl_uloop.sv -vopt +acc=mnpr -o vopt_tb_hwpe_ctrl_uloop tb_hwpe_ctrl_uloop -work hwpe_ctrl_lib - diff --git a/sim/uloop/clean.sh b/sim/uloop/clean.sh deleted file mode 100755 index 20cec3c..0000000 --- a/sim/uloop/clean.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -rm -rf hwpe_ctrl_lib diff --git a/sim/uloop/sim.sh b/sim/uloop/sim.sh deleted file mode 100755 index 9bbcee0..0000000 --- a/sim/uloop/sim.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -vsim -work hwpe_ctrl_lib vopt_tb_hwpe_ctrl_uloop - diff --git a/src_files.yml b/src_files.yml deleted file mode 100644 index dcc7571..0000000 --- a/src_files.yml +++ /dev/null @@ -1,26 +0,0 @@ -hwpe-ctrl: - vlog_opts: [ - +nowarnSVCHK, - ] - incdirs: [ - rtl, - ] - files: [ - rtl/hwpe_ctrl_package.sv, - rtl/hwpe_ctrl_interfaces.sv, - rtl/hwpe_ctrl_regfile.sv, - rtl/hwpe_ctrl_regfile_latch.sv, - rtl/hwpe_ctrl_regfile_latch_test_wrap.sv, - rtl/hwpe_ctrl_slave.sv, - rtl/hwpe_ctrl_seq_mult.sv, - rtl/hwpe_ctrl_uloop.sv, - ] - -tb_hwpe_ctrl: - targets: [ - rtl - ] - files: [ - tb/tb_hwpe_ctrl_seq_mult.sv, - ] - diff --git a/target/sim/src/hwpe_ctrl_partial_mult_tb.sv b/target/sim/src/hwpe_ctrl_partial_mult_tb.sv new file mode 100644 index 0000000..bf877ef --- /dev/null +++ b/target/sim/src/hwpe_ctrl_partial_mult_tb.sv @@ -0,0 +1,192 @@ +// Copyright 2025-2026 ETH Zurich and University of Bologna. +// Solderpad Hardware License, Version 0.51, see LICENSE for details. +// SPDX-License-Identifier: SHL-0.51 + +/* + * hwpe_ctrl_partial_mult_tb.sv + * Francesco Conti + * + * Copyright (C) 2014-2026 ETH Zurich, University of Bologna + * Copyright and related rights are licensed under the Solderpad Hardware + * License, Version 0.51 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://solderpad.org/licenses/SHL-0.51. Unless required by applicable law + * or agreed to in writing, software, hardware and materials distributed under + * this License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +/* + * Direct testbench for hwpe_ctrl_partial_mult (a partially-sequential + * unsigned multiplier, see rtl/hwpe_ctrl_partial_mult.sv). clk_i/rst_ni are + * generated by the portless top hwpe_ctrl_partial_mult_tb_wrap; the stimulus + * below synchronizes to clk_i via @(posedge clk_i) and drives the DUT's + * inputs with ATI (#TA) timing. + * + * Scenario selection is via +TEST=; the only scenario is + * "partial_mult", dispatched from the `case` in the initial block below -- + * anything else (including a missing +TEST) is a hard failure, so that a + * mistyped or absent plusarg cannot make the run pass vacuously. + * + * NOTE on the correctness check: the legacy tb/tb_hwpe_ctrl_partial_mult.sv + * used a concurrent SVA (`assert property (@(posedge clk_i) ...)`) to check + * the multiplication result. target/sim/verilator/verilator.mk does not pass + * --assert to verilator, so concurrent assertions are silently dropped -- + * the legacy check would compile and "pass" while verifying nothing under + * this flow. It is reimplemented below as a procedural check() call inside + * an always_ff @(posedge clk_i) block, which samples a/b/invert/prod/valid + * exactly like the original property did (registered DUT outputs read at + * the same posedge that updated them, before this cycle's own update is + * observed) and actually increments `errors` on a mismatch. + */ + +module hwpe_ctrl_partial_mult_tb + import hwpe_ctrl_package::*; +#( + parameter time TCP = 1.0ns, // clock period, 1 GHz clock + parameter time TA = 0.2ns, // application time + parameter time TT = 0.8ns, // test time + parameter int unsigned AW = 32, + parameter int unsigned BW = 32, + parameter int unsigned MULT_BITS = 4, + parameter int unsigned NUM_TRANSACTIONS = 10000, + parameter bit VERBOSE = 1'b0 +) +( + input logic clk_i, + input logic rst_ni +); + + // Fixed seed so runs are reproducible: the legacy tb's $random()/ + // $urandom_range() calls were unseeded, and verilator's PRNG stream + // differs from Questa's regardless, so there was never bit-for-bit + // portability to begin with -- what matters going forward is that this + // flow's own runs are deterministic from one invocation to the next. + localparam int unsigned SEED = 32'hCAFE_F00D; + + /* ------------------------------------------------------------------ * + * DUT instantiation * + * ------------------------------------------------------------------ */ + + logic [AW-1:0] a; + logic [BW-1:0] b; + logic [AW+BW-1:0] prod; + logic valid; + logic ready; + logic start; + logic invert; + + hwpe_ctrl_partial_mult #( + .AW ( AW ), + .BW ( BW ), + .MULT_BITS ( MULT_BITS ) + ) i_dut ( + .clk_i ( clk_i ), + .rst_ni ( rst_ni ), + .clear_i ( 1'b0 ), + .start_i ( start ), + .a_i ( a ), + .b_i ( b ), + .invert_i ( invert ), + .valid_o ( valid ), + .ready_o ( ready ), + .prod_o ( prod ) + ); + + /* ------------------------------------------------------------------ * + * Bookkeeping * + * ------------------------------------------------------------------ */ + + int errors; + + task automatic check(input bit cond, input string msg); + if (!cond) begin + errors++; + $display("[TB] - ERROR: %s", msg); + end + endtask + + task automatic wait_cycles(input int n); + repeat (n) @(posedge clk_i); + endtask + + /* ------------------------------------------------------------------ * + * Result checker (replaces the legacy concurrent assertion) * + * ------------------------------------------------------------------ */ + + always @(posedge clk_i) begin + if (valid & ~start & rst_ni) begin + automatic logic [AW+BW-1:0] exp_prod; + exp_prod = invert ? -((AW+BW)'(a) * b) : (AW+BW)'(a) * b; + check(prod === exp_prod, + $sformatf("wrong multiplication result: a=0x%0h b=0x%0h invert=%0b expected=0x%0h got=0x%0h (time=%0t)", + a, b, invert, exp_prod, prod, $time)); + if (VERBOSE) + $display("prod %016x = %08x * %08x", prod, a, b); + end + end + + /* ------------------------------------------------------------------ * + * Scenario: partial_mult * + * ------------------------------------------------------------------ */ + + task automatic test_partial_mult; + $display("[TB] - test_partial_mult: starting NUM_TRANSACTIONS=%0d", NUM_TRANSACTIONS); + for (int t = 0; t < NUM_TRANSACTIONS; t++) begin + a <= #TA $urandom(); + b <= #TA $urandom(); + invert <= #TA $urandom_range(0, 1); + start <= #TA 1'b1; + wait_cycles(1); + start <= #TA 1'b0; + for (int i = 0; i < (AW/MULT_BITS + (AW % MULT_BITS ? 1 : 0)); i++) + wait_cycles(1); + end + $display("[TB] - test_partial_mult: done (errors=%0d)", errors); + endtask + + /* ------------------------------------------------------------------ * + * Top-level scenario dispatch * + * ------------------------------------------------------------------ */ + + initial begin + string test_name; + + errors = 0; + a = '0; + b = '0; + invert = '0; + start = 1'b0; + + void'($urandom(SEED)); + + // Let clk_i/rst_ni (driven from hwpe_ctrl_partial_mult_tb_wrap) settle + // past reset deassertion before driving the DUT. + wait_cycles(25); + + if (!$value$plusargs("TEST=%s", test_name)) begin + $display("[TB] - ERROR: no +TEST= plusarg given"); + errors++; + test_name = ""; + end + + case (test_name) + "partial_mult": test_partial_mult(); + default: begin + $display($sformatf("[TB] - ERROR: unknown or missing +TEST '%s'", test_name)); + errors++; + end + endcase + + wait_cycles(5); + + if (errors == 0) $display("[TB] - Success!"); + else begin + $display("[TB] - Fail!"); + $error("[TB] - errors=%0d", errors); + end + $finish; + end + +endmodule // hwpe_ctrl_partial_mult_tb diff --git a/target/sim/src/hwpe_ctrl_partial_mult_tb_wrap.sv b/target/sim/src/hwpe_ctrl_partial_mult_tb_wrap.sv new file mode 100644 index 0000000..e2d88be --- /dev/null +++ b/target/sim/src/hwpe_ctrl_partial_mult_tb_wrap.sv @@ -0,0 +1,71 @@ +// Copyright 2025-2026 ETH Zurich and University of Bologna. +// Solderpad Hardware License, Version 0.51, see LICENSE for details. +// SPDX-License-Identifier: SHL-0.51 + +/* + * hwpe_ctrl_partial_mult_tb_wrap.sv + * Francesco Conti + * + * Copyright (C) 2014-2026 ETH Zurich, University of Bologna + * Copyright and related rights are licensed under the Solderpad Hardware + * License, Version 0.51 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://solderpad.org/licenses/SHL-0.51. Unless required by applicable law + * or agreed to in writing, software, hardware and materials distributed under + * this License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +/* + * Portless verilator top for the hwpe_ctrl_partial_mult testbench (see + * target/sim/verilator/verilator.mk: --top-module hwpe_ctrl_partial_mult_tb_wrap). + * Being portless is what makes `verilator --binary --top-module` work + * directly, mirroring hwpe_ctrl_target_tb_wrap.sv in this same directory. + */ + +timeunit 1ps; +timeprecision 1ps; + +module hwpe_ctrl_partial_mult_tb_wrap; + + // ATI timing parameters. + localparam time TCP = 1.0ns; // clock period, 1 GHz clock + localparam time TA = 0.2ns; // application time + localparam time TT = 0.8ns; // test time + + logic clk_i; + logic rst_ni; + + hwpe_ctrl_partial_mult_tb #( + .TCP ( TCP ), + .TA ( TA ), + .TT ( TT ) + ) i_tb ( + .clk_i ( clk_i ), + .rst_ni ( rst_ni ) + ); + + // Performs one entire clock cycle. + task automatic cycle; + clk_i <= #(TCP/2) 1'b0; + clk_i <= #TCP 1'b1; + #TCP; + endtask + + // Free-running clock/reset generation process. hwpe_ctrl_partial_mult_tb + // synchronizes to clk_i/rst_ni via @(posedge clk_i); the actual test + // scenario runs there and calls $finish itself once done -- this process + // just keeps the clock alive for as long as the simulation runs. + initial begin + clk_i <= 1'b0; + rst_ni <= 1'b0; + + for (int i = 0; i < 20; i++) cycle(); + + rst_ni <= #TA 1'b1; + + while (1) cycle(); + end + +endmodule // hwpe_ctrl_partial_mult_tb_wrap diff --git a/target/sim/src/hwpe_ctrl_seq_mult_tb.sv b/target/sim/src/hwpe_ctrl_seq_mult_tb.sv new file mode 100644 index 0000000..0774336 --- /dev/null +++ b/target/sim/src/hwpe_ctrl_seq_mult_tb.sv @@ -0,0 +1,318 @@ +// Copyright 2025-2026 ETH Zurich and University of Bologna. +// Solderpad Hardware License, Version 0.51, see LICENSE for details. +// SPDX-License-Identifier: SHL-0.51 + +/* + * hwpe_ctrl_seq_mult_tb.sv + * Francesco Conti + * + * Copyright (C) 2014-2026 ETH Zurich, University of Bologna + * Copyright and related rights are licensed under the Solderpad Hardware + * License, Version 0.51 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://solderpad.org/licenses/SHL-0.51. Unless required by applicable law + * or agreed to in writing, software, hardware and materials distributed under + * this License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +/* + * Direct testbench for hwpe_ctrl_seq_mult (a fully-sequential unsigned + * multiplier, see rtl/hwpe_ctrl_seq_mult.sv). clk_i/rst_ni are generated by + * the portless top hwpe_ctrl_seq_mult_tb_wrap; the stimulus below + * synchronizes to clk_i via @(posedge clk_i) and drives the DUT's inputs + * with ATI (#TA/#TT) timing. + * + * This TB replaces tb/tb_hwpe_ctrl_seq_mult.sv, which had been functionally + * dead for years: it wired only 7 of the DUT's 10 ports (clear_i, invert_i + * and ready_o were never connected), so under a simulator that x-initializes + * unconnected inputs, invert_i floating to x makes the DUT's terminate + * condition `(~invert_i && cnt==AW-1) || (invert_i && cnt==AW)` evaluate to + * x, valid_o never rises, and the legacy `assert property + * (@(posedge clk_i) (valid & ~start & rst_ni) |-> (prod==a*b))` was + * therefore vacuously true -- its antecedent was never satisfied. It also + * had no $finish (an infinite `while(1)` meant to be stopped by hand in a + * GUI) and was never wired into any runner or Bender.yml target. + * + * All ten DUT ports are connected below (see the PINMISSING check in the + * verilator invocation this TB was validated against). The correctness + * check is a procedural task (do_mult, called from the scenario tasks + * below) rather than a concurrent SVA: target/sim/verilator/verilator.mk + * does not pass --assert to verilator, so a concurrent assertion compiles + * and is silently dropped, reproducing exactly the "passes while checking + * nothing" failure mode this TB is meant to fix. Unlike a background + * `always_ff @(posedge clk_i) if (valid & ...)` monitor (which reads the + * DUT-facing invert_i wire to decide what to expect, and so cannot detect a + * stimulus bug that fails to actually drive invert_i), do_mult's expected + * value is computed from the *caller's stated intent* (its op_invert + * argument) independently of whatever ends up wired to invert_i -- this is + * what makes the invert scenario's negative check (drive invert_i low while + * still expecting the negated product) meaningful instead of vacuous. + * + * Protocol derived from rtl/hwpe_ctrl_seq_mult.sv (there was no runner or + * spec to port from): + * - ready_o is high while idle; start_i must only be asserted while + * ready_o is high, held for exactly one cycle. + * - a_i/b_i/invert_i must be stable while the DUT consumes them, which + * per the RTL header comment is for AW-1 cycles after start_i (i.e. AW + * cycles total including the start cycle) -- this holds regardless of + * invert_i, since the extra invert cycle only adds a fixed +1 addend + * (shifted=1 when cnt==AW) rather than re-reading a_i/b_i. + * - valid_o/ready_o rise together exactly AW cycles after start_i when + * invert_i is low, or AW+1 cycles after start_i when invert_i is high + * (one extra cycle to two's-complement-negate the accumulated product). + * Because the extra cycle is data-dependent on invert_i, this TB + * handshakes on ready_o/valid_o rather than using a fixed cycle count + * (the legacy TB's `for(int i=0;i: "seq_mult" (normal path, plus a + * mid-operation clear_i exercise) and "seq_mult_invert" (invert path). + * Anything else (including a missing +TEST) is a hard failure, so that a + * mistyped or absent plusarg cannot make the run pass vacuously. + */ + +module hwpe_ctrl_seq_mult_tb + import hwpe_ctrl_package::*; +#( + parameter time TCP = 1.0ns, // clock period, 1 GHz clock + parameter time TA = 0.2ns, // application time + parameter time TT = 0.8ns, // test time + parameter int unsigned AW = 8, + parameter int unsigned BW = 8, + parameter int unsigned NUM_TRANSACTIONS = 32, + parameter bit VERBOSE = 1'b0 +) +( + input logic clk_i, + input logic rst_ni +); + + // Fixed seed so runs are reproducible (verilator's PRNG stream has no + // bit-for-bit relationship to Questa's regardless -- what matters is that + // this flow's own runs are deterministic from one invocation to the + // next), mirroring hwpe_ctrl_partial_mult_tb.sv. + localparam int unsigned SEED = 32'hFACE_0FF5; + + // Generous timeout (in cycles) for the ready_o/valid_o handshake waits + // below, so a genuinely stuck DUT fails fast with a clear message instead + // of hanging the run up to the outer simulation timeout. + localparam int unsigned TIMEOUT_CYCLES = 8 * (AW + BW) + 64; + + /* ------------------------------------------------------------------ * + * DUT instantiation * + * ------------------------------------------------------------------ */ + + logic clear; + logic start; + logic [AW-1:0] a; + logic [BW-1:0] b; + logic invert; + logic valid; + logic ready; + logic [AW+BW-1:0] prod; + + hwpe_ctrl_seq_mult #( + .AW ( AW ), + .BW ( BW ) + ) i_dut ( + .clk_i ( clk_i ), + .rst_ni ( rst_ni ), + .clear_i ( clear ), + .start_i ( start ), + .a_i ( a ), + .b_i ( b ), + .invert_i ( invert ), + .valid_o ( valid ), + .ready_o ( ready ), + .prod_o ( prod ) + ); + + /* ------------------------------------------------------------------ * + * Bookkeeping * + * ------------------------------------------------------------------ */ + + int errors; + + task automatic check(input bit cond, input string msg); + if (!cond) begin + errors++; + $display("[TB] - ERROR: %s", msg); + end + endtask + + task automatic wait_cycles(input int n); + repeat (n) @(posedge clk_i); + endtask + + /* ------------------------------------------------------------------ * + * Handshake primitives * + * ------------------------------------------------------------------ */ + + // Blocks until ready_o is observed high (DUT idle, safe to start a new + // operation), with a bounded timeout so a stuck DUT fails instead of + // hanging. + task automatic wait_ready; + automatic int timeout = 0; + while (ready !== 1'b1 && timeout < TIMEOUT_CYCLES) begin + @(posedge clk_i); #TT; + timeout++; + end + check(ready === 1'b1, $sformatf("timeout waiting for ready_o (time=%0t)", $time)); + endtask + + // Blocks until valid_o is observed high (operation complete), with a + // bounded timeout. Deliberately does not assume a fixed cycle count: per + // the RTL, completion takes AW cycles normally or AW+1 with invert_i, and + // hard-coding either would be wrong for the other case (exactly the + // legacy TB's bug). + task automatic wait_valid; + automatic int timeout = 0; + while (valid !== 1'b1 && timeout < TIMEOUT_CYCLES) begin + @(posedge clk_i); #TT; + timeout++; + end + check(valid === 1'b1, $sformatf("timeout waiting for valid_o (time=%0t)", $time)); + endtask + + // Runs one multiply-transaction end to end: waits for the DUT to be + // ready, drives a_i/b_i/invert_i together with a one-cycle start_i pulse, + // keeps operands stable (well beyond the AW-1-cycles-after-start_i + // requirement documented in the RTL header, since they are only changed + // by the *next* call to do_mult, after this one has already completed), + // waits for valid_o via the handshake above, and checks prod_o against + // the expected product computed from this call's own arguments -- not + // from re-reading the invert wire (see the file header comment on why). + task automatic do_mult(input logic [AW-1:0] op_a, input logic [BW-1:0] op_b, input logic op_invert); + automatic logic [AW+BW-1:0] a_ext, b_ext, expected, got; + + wait_ready(); + + a <= #TA op_a; + b <= #TA op_b; + invert <= #TA op_invert; + start <= #TA 1'b1; + wait_cycles(1); + start <= #TA 1'b0; + + wait_valid(); + got = prod; + + a_ext = op_a; + b_ext = op_b; + expected = op_invert ? (~(a_ext * b_ext) + 1'b1) : (a_ext * b_ext); + check(got === expected, + $sformatf("mult mismatch: a=%0d b=%0d invert=%0b expected=%0d got=%0d (time=%0t)", + op_a, op_b, op_invert, expected, got, $time)); + + if (VERBOSE) + $display("[TB] prod=%0d = a=%0d * b=%0d (invert=%0b)", got, op_a, op_b, op_invert); + endtask + + /* ------------------------------------------------------------------ * + * Scenario: seq_mult (normal path + clear_i exercise) * + * ------------------------------------------------------------------ */ + + // Asserts clear_i partway through an in-flight operation and checks that + // the DUT synchronously returns to idle (ready_o high, valid_o low) -- + // clear_i takes the same branch as reset in the counter/product + // always_ff blocks -- then proves the multiplier is fully usable again + // with a normal transaction. + task automatic test_clear_mid_op; + wait_ready(); + + a <= #TA 8'h55; + b <= #TA 8'hAA; + invert <= #TA 1'b0; + start <= #TA 1'b1; + wait_cycles(1); + start <= #TA 1'b0; + + wait_cycles(3); // let the multiply run partway through its AW-cycle countdown + + clear <= #TA 1'b1; + wait_cycles(1); #TT; + check(ready === 1'b1, $sformatf("ready_o not restored the cycle after clear_i mid-operation (time=%0t)", $time)); + check(valid === 1'b0, $sformatf("valid_o not deasserted after clear_i mid-operation (time=%0t)", $time)); + clear <= #TA 1'b0; + + do_mult(8'h0F, 8'hF0, 1'b0); // multiplier must still work correctly afterwards + endtask + + task automatic test_seq_mult; + $display("[TB] - test_seq_mult: starting NUM_TRANSACTIONS=%0d", NUM_TRANSACTIONS); + for (int t = 0; t < NUM_TRANSACTIONS; t++) + do_mult($urandom(), $urandom(), 1'b0); + test_clear_mid_op(); + $display("[TB] - test_seq_mult: done (errors=%0d)", errors); + endtask + + /* ------------------------------------------------------------------ * + * Scenario: seq_mult_invert * + * ------------------------------------------------------------------ */ + + task automatic test_seq_mult_invert; + $display("[TB] - test_seq_mult_invert: starting NUM_TRANSACTIONS=%0d", NUM_TRANSACTIONS); + for (int t = 0; t < NUM_TRANSACTIONS; t++) + do_mult($urandom(), $urandom(), 1'b1); + $display("[TB] - test_seq_mult_invert: done (errors=%0d)", errors); + endtask + + /* ------------------------------------------------------------------ * + * Top-level scenario dispatch * + * ------------------------------------------------------------------ */ + + initial begin + string test_name; + + errors = 0; + clear = 1'b0; + start = 1'b0; + a = '0; + b = '0; + invert = 1'b0; + + void'($urandom(SEED)); + + // Let clk_i/rst_ni (driven from hwpe_ctrl_seq_mult_tb_wrap) settle past + // reset deassertion before driving the DUT. + wait_cycles(25); + + if (!$value$plusargs("TEST=%s", test_name)) begin + $display("[TB] - ERROR: no +TEST= plusarg given"); + errors++; + test_name = ""; + end + + case (test_name) + "seq_mult": test_seq_mult(); + "seq_mult_invert": test_seq_mult_invert(); + default: begin + $display($sformatf("[TB] - ERROR: unknown or missing +TEST '%s'", test_name)); + errors++; + end + endcase + + wait_cycles(5); + + if (errors == 0) $display("[TB] - Success!"); + else begin + $display("[TB] - Fail!"); + $error("[TB] - errors=%0d", errors); + end + $finish; + end + +endmodule // hwpe_ctrl_seq_mult_tb diff --git a/target/sim/src/hwpe_ctrl_seq_mult_tb_wrap.sv b/target/sim/src/hwpe_ctrl_seq_mult_tb_wrap.sv new file mode 100644 index 0000000..7430c2a --- /dev/null +++ b/target/sim/src/hwpe_ctrl_seq_mult_tb_wrap.sv @@ -0,0 +1,79 @@ +// Copyright 2025-2026 ETH Zurich and University of Bologna. +// Solderpad Hardware License, Version 0.51, see LICENSE for details. +// SPDX-License-Identifier: SHL-0.51 + +/* + * hwpe_ctrl_seq_mult_tb_wrap.sv + * Francesco Conti + * + * Copyright (C) 2014-2026 ETH Zurich, University of Bologna + * Copyright and related rights are licensed under the Solderpad Hardware + * License, Version 0.51 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://solderpad.org/licenses/SHL-0.51. Unless required by applicable law + * or agreed to in writing, software, hardware and materials distributed under + * this License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +/* + * Portless verilator top for the hwpe_ctrl_seq_mult testbench (see + * target/sim/verilator/verilator.mk: --top-module hwpe_ctrl_seq_mult_tb_wrap). + * Being portless is what makes `verilator --binary --top-module` work + * directly, mirroring hwpe_ctrl_target_tb_wrap.sv and + * hwpe_ctrl_partial_mult_tb_wrap.sv. + */ + +timeunit 1ps; +timeprecision 1ps; + +module hwpe_ctrl_seq_mult_tb_wrap +#( + parameter int unsigned AW = 8, + parameter int unsigned BW = 8 +); + + // ATI timing parameters. + localparam time TCP = 1.0ns; // clock period, 1 GHz clock + localparam time TA = 0.2ns; // application time + localparam time TT = 0.8ns; // test time + + logic clk_i; + logic rst_ni; + + hwpe_ctrl_seq_mult_tb #( + .TCP ( TCP ), + .TA ( TA ), + .TT ( TT ), + .AW ( AW ), + .BW ( BW ) + ) i_tb ( + .clk_i ( clk_i ), + .rst_ni ( rst_ni ) + ); + + // Performs one entire clock cycle. + task automatic cycle; + clk_i <= #(TCP/2) 1'b0; + clk_i <= #TCP 1'b1; + #TCP; + endtask + + // Free-running clock/reset generation process. hwpe_ctrl_seq_mult_tb (and + // the driver tasks within it) synchronize to clk_i/rst_ni via + // @(posedge clk_i); the actual test scenario runs there and calls + // $finish itself once done -- this process just keeps the clock alive + // for as long as the simulation runs. + initial begin + clk_i <= 1'b0; + rst_ni <= 1'b0; + + for (int i = 0; i < 20; i++) cycle(); + + rst_ni <= #TA 1'b1; + + while (1) cycle(); + end + +endmodule // hwpe_ctrl_seq_mult_tb_wrap diff --git a/target/sim/src/hwpe_ctrl_target_tb.sv b/target/sim/src/hwpe_ctrl_target_tb.sv new file mode 100644 index 0000000..d78693c --- /dev/null +++ b/target/sim/src/hwpe_ctrl_target_tb.sv @@ -0,0 +1,670 @@ +// Copyright 2025-2026 ETH Zurich and University of Bologna. +// Solderpad Hardware License, Version 0.51, see LICENSE for details. +// SPDX-License-Identifier: SHL-0.51 + +/* + * hwpe_ctrl_target_tb.sv + * Francesco Conti + * + * Copyright (C) 2025-2026 ETH Zurich, University of Bologna + * Copyright and related rights are licensed under the Solderpad Hardware + * License, Version 0.51 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://solderpad.org/licenses/SHL-0.51. Unless required by applicable law + * or agreed to in writing, software, hardware and materials distributed under + * this License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +/* + * Direct register-level testbench for hwpe_ctrl_target, exercised through + * hwpe_ctrl_target_wrap (DUT + PeakRDL hwpe_ctrl_regif_example regblock). + * clk_i/rst_ni are generated by the portless top hwpe_ctrl_target_tb_wrap; + * every task below synchronizes to clk_i via @(posedge clk_i) and drives + * signals with ATI (#TA/#TT) timing. + * + * Scenario selection is via +TEST= (see the `case` in the initial + * block below); scripts/regression.yml drives the four scenarios + * (reg_access, offload, fifo_backpressure, soft_clear) against a single + * verilator model build per NB_CONTEXT value. + */ + +module hwpe_ctrl_target_tb + import hwpe_ctrl_package::*; + import hwpe_ctrl_regif_example_pkg::*; +#( + parameter time TCP = 1.0ns, // clock period, 1 GHz clock + parameter time TA = 0.2ns, // application time + parameter time TT = 0.8ns, // test time + parameter int unsigned NB_CONTEXT = 2, + parameter int unsigned NB_CLEAR_CYCLES = 3 +) +( + input logic clk_i, + input logic rst_ni +); + + /* ------------------------------------------------------------------ * + * DUT instantiation * + * ------------------------------------------------------------------ */ + + hwpe_ctrl_intf_periph #(.ID_WIDTH(2)) periph (.clk(clk_i)); + + logic clear; + logic job_trigger; + logic job_done; + logic [31:0] job_status; + logic [31:0] job_indep_hw_status; + hwpe_ctrl_regif_example__hwpe_ctrl_job_indep__out_t job_indep_regs; + logic job_dep_regs_valid; + hwpe_ctrl_regif_example__hwpe_ctrl_job_dep__out_t job_dep_regs; + + hwpe_ctrl_target_wrap #( + .NB_CONTEXT ( NB_CONTEXT ), + .NB_CLEAR_CYCLES ( NB_CLEAR_CYCLES ) + ) i_wrap ( + .clk_i ( clk_i ), + .rst_ni ( rst_ni ), + .clear_o ( clear ), + .target ( periph ), + .job_trigger_o ( job_trigger ), + .job_done_i ( job_done ), + .job_status_i ( job_status ), + .job_indep_hw_status_i ( job_indep_hw_status ), + .job_indep_regs_o ( job_indep_regs ), + .job_dep_regs_valid_o ( job_dep_regs_valid ), + .job_dep_regs_o ( job_dep_regs ) + ); + + // NOTE: a hierarchical parameter reference (e.g. `i_wrap.i_target.NB_CONTEXT`) + // was tried here first, as suggested, so the same test body would read the + // DUT's own elaborated parameters without duplicating them. Verilator + // 5.048 rejects it: "%Error-HIERPARAM: Parameter values cannot use + // hierarchical values (IEEE 1800-2023 6.20.2)". Falling back to threading + // both parameters explicitly top-down (tb_wrap -GNB_CONTEXT=.. -> tb -> + // wrap -> hwpe_ctrl_target), which keeps a single source of truth via + // module parameters instead. + localparam int unsigned NbContextHier = NB_CONTEXT; + localparam int unsigned NbClearCyclesHier = NB_CLEAR_CYCLES; + + /* ------------------------------------------------------------------ * + * Bookkeeping * + * ------------------------------------------------------------------ */ + + int errors; + logic [1:0] id_cnt; // matches periph's ID_WIDTH=2 + + task automatic check(input bit cond, input string msg); + if (!cond) begin + errors++; + $display("[TB] - ERROR: %s", msg); + end + endtask + + task automatic wait_cycles(input int n); + repeat (n) @(posedge clk_i); + endtask + + /* ------------------------------------------------------------------ * + * Bus driver primitives * + * ------------------------------------------------------------------ * + * hwpe_ctrl_intf_periph contract (rtl/hwpe_ctrl_interfaces.sv): + * wen==1 is a READ, wen==0 is a WRITE. gnt is combinational and, for this + * passthrough cpuif, always 1 (hwpe_ctrl_regif_example.sv hard-codes + * cpuif_req_stall_wr/rd to '0) -- backpressure on this target is + * signalled only via ACQUIRE's *return data*, never by withholding gnt, + * so every call below asserts gnt===1 unconditionally. + * ------------------------------------------------------------------ */ + + task automatic periph_write(input logic [31:0] addr, input logic [31:0] data, input logic [3:0] be = 4'hf); + @(posedge clk_i); + periph.req <= #TA 1'b1; + periph.wen <= #TA 1'b0; // wen==0 => WRITE + periph.add <= #TA addr; + periph.data <= #TA data; + periph.be <= #TA be; + periph.id <= #TA id_cnt; + id_cnt = id_cnt + 1'b1; + #TT; + check(periph.gnt === 1'b1, $sformatf("periph_write: gnt not asserted for addr 0x%08x (time=%0t)", addr, $time)); + @(posedge clk_i); + periph.req <= #TA 1'b0; + endtask + + task automatic periph_read(input logic [31:0] addr, output logic [31:0] data); + automatic logic [1:0] issued_id; + @(posedge clk_i); + periph.req <= #TA 1'b1; + periph.wen <= #TA 1'b1; // wen==1 => READ + periph.add <= #TA addr; + periph.data <= #TA '0; + periph.be <= #TA 4'hf; + issued_id = id_cnt; + periph.id <= #TA issued_id; + id_cnt = id_cnt + 1'b1; + #TT; + check(periph.gnt === 1'b1, $sformatf("periph_read: gnt not asserted for addr 0x%08x (time=%0t)", addr, $time)); + @(posedge clk_i); + periph.req <= #TA 1'b0; + // r_valid/r_data/r_id are registered exactly one cycle after req & gnt + // (target_r_valid_q/target_r_data_q/target_r_id_q in + // rtl/hwpe_ctrl_target.sv) -- i.e. right now, the cycle after the + // request was granted. + #TT; + check(periph.r_valid === 1'b1, $sformatf("periph_read: r_valid not asserted one cycle after addr 0x%08x (time=%0t)", addr, $time)); + check(periph.r_id === issued_id, $sformatf("periph_read: r_id mismatch for addr 0x%08x: expected %0d got %0d (time=%0t)", addr, issued_id, periph.r_id, $time)); + data = periph.r_data; + endtask + + task automatic expect_reg(input logic [31:0] addr, input logic [31:0] expected, input logic [31:0] mask = '1, input string name = ""); + automatic logic [31:0] rdata; + periph_read(addr, rdata); + check((rdata & mask) === (expected & mask), + $sformatf("%s @0x%08x: expected 0x%08x got 0x%08x (mask 0x%08x, time=%0t)", name, addr, expected, rdata, mask, $time)); + endtask + + // ACQUIRE (0x04) has a read side effect (locks the offload FSM into + // ACQUIRE state and returns/consumes a job ID) -- never sweep it + // generically, only call it deliberately. + task automatic acquire(output logic [31:0] id_or_err); + periph_read(32'h04, id_or_err); + endtask + + // COMMIT_TRIGGER (0x00) is active-low and write-only (sw=w); its decode + // strobe in hwpe_ctrl_regif_example.sv is *not* gated by is_wr (unlike + // ACQUIRE/STATUS/RUNNING_JOB/hw_status), so even a read of this address + // would pulse swacc using whatever was last written -- never issue + // periph_read/expect_reg on 0x00. commit_n/trigger_n are the raw + // active-low bits as written (0 = commit, 0 = trigger). job_commit and + // job_trigger_o are registered one cycle after this write is granted + // (commit_trigger_swacc_q in rtl/hwpe_ctrl_target.sv). + task automatic commit_trigger(input logic commit_n, input logic trigger_n); + periph_write(32'h00, {30'h0, commit_n, trigger_n}, 4'hf); + endtask + + // SOFT_CLEAR (0x14) is active-low and write-only (sw=w) with the same + // is_wr-ungated decode strobe as COMMIT_TRIGGER -- never read it either. + // regfile_n/state_n are the raw active-low bits as written (0 = clear + // regfile-side bookkeeping, 0 = clear state / pulse clear_o). The clears + // (soft_clear_regfile_q/soft_clear_state_q, clear_o) are registered one + // cycle after this write is granted (soft_clear_swacc_q in + // rtl/hwpe_ctrl_target.sv) and then held for NB_CLEAR_CYCLES cycles. + task automatic soft_clear(input logic regfile_n, input logic state_n); + periph_write(32'h14, {30'h0, state_n, regfile_n}, 4'hf); + endtask + + // Pulses job_done_i for exactly one cycle, synchronized to clk_i. + // job_done_i drives the job FIFO's pop_i directly (i_job_fifo in + // rtl/hwpe_ctrl_target.sv); fifo_v3 asserts if popped while empty, so the + // caller MUST guarantee a job was already committed before calling this. + task automatic pulse_job_done; + @(posedge clk_i); + job_done <= #TA 1'b1; + @(posedge clk_i); + job_done <= #TA 1'b0; + endtask + + // Must be called with no intervening statements right after a + // commit_trigger() call whose write is expected to (or must not) fire + // job_trigger_o: commit_trigger()'s own periph_write() already spans the + // one-cycle swacc latency, so job_trigger_o is valid to sample right now. + task automatic check_trigger_pulse(input bit expected, input string ctx); + #TT; + check((job_trigger === 1'b1) === expected, {ctx, ": job_trigger_o not asserted as expected the cycle after the write"}); + if (expected) begin + @(posedge clk_i); #TT; + check(job_trigger === 1'b0, {ctx, ": job_trigger_o did not deassert one cycle later"}); + end + endtask + + /* ------------------------------------------------------------------ * + * Register map under test (byte offsets from the target base), * + * cross-referenced against rtl/hwpe_ctrl_regif_example.rdl. * + * COMMIT_TRIGGER (0x00), ACQUIRE (0x04) and SOFT_CLEAR (0x14) are * + * deliberately excluded -- they are side-effecting/write-only and are * + * exercised by the dedicated commit_trigger()/acquire()/soft_clear() * + * tasks instead, never through this generic sweep. * + * ------------------------------------------------------------------ */ + + typedef struct packed { + logic [31:0] addr; + logic writable; + logic [31:0] reset_val; + logic [31:0] mask; + } reg_desc_t; + + localparam int NB_REG_DESC = 11; + localparam reg_desc_t REG_DESC [NB_REG_DESC] = '{ + '{32'h08, 1'b1, 32'h0000_0000, 32'h0000_0001}, // AUTOTRIGGER autotrigger_n[0], r0[31:1] + '{32'h0c, 1'b0, 32'h0000_0000, 32'h0000_0000}, // STATUS status0 (hw=w, sw=r) + '{32'h10, 1'b0, 32'h0000_0000, 32'h0000_0000}, // RUNNING_JOB running_job[7:0] (hw=w, sw=r), r0[31:8] + '{32'h18, 1'b0, 32'h0000_0000, 32'h0000_0000}, // reserved1 + '{32'h1c, 1'b0, 32'h0000_0000, 32'h0000_0000}, // reserved2 + '{32'h20, 1'b1, 32'h0000_0000, 32'hffff_ffff}, // param0 value[31:0] + '{32'h24, 1'b1, 32'h0000_0000, 32'hffff_ffff}, // param1 value[31:0] + '{32'h28, 1'b1, 32'h0000_0000, 32'h0000_ffff}, // param2 length[15:0], r0[31:16] + '{32'h40, 1'b1, 32'h0000_0000, 32'hffff_ffff}, // config_a value[31:0] + '{32'h44, 1'b1, 32'hdead_beef, 32'hffff_ffff}, // config_b value[31:0], nonzero reset + '{32'h48, 1'b0, 32'h0000_0000, 32'h0000_0000} // hw_status value[31:0] (hw=w, sw=r) + }; + localparam string REG_NAME [NB_REG_DESC] = '{ + "AUTOTRIGGER", "STATUS", "RUNNING_JOB", "reserved1", "reserved2", + "param0", "param1", "param2", "config_a", "config_b", "hw_status" + }; + + /* ------------------------------------------------------------------ * + * Scenario: reg_access * + * ------------------------------------------------------------------ */ + + task automatic test_reg_access; + automatic logic [31:0] rdata, wdata, id_or_err; + automatic int i, b; + + $display("[TB] - test_reg_access: starting"); + + // 1. Reset values (incl. config_b == 0xdead_beef). + for (i = 0; i < NB_REG_DESC; i++) + expect_reg(REG_DESC[i].addr, REG_DESC[i].reset_val, '1, {"reset value ", REG_NAME[i]}); + + // 2. Walking-ones / walking-zeros on every sw-writable field (masked to + // its legal bits), plus an explicit reserved-bit-masking check + // (writing all-ones must not leak outside `mask`, e.g. param2[31:16] + // must read back 0). + for (i = 0; i < NB_REG_DESC; i++) begin + if (REG_DESC[i].writable) begin + for (b = 0; b < 32; b++) begin + if (REG_DESC[i].mask[b]) begin + wdata = (32'h1 << b) & REG_DESC[i].mask; + periph_write(REG_DESC[i].addr, wdata, 4'hf); + expect_reg(REG_DESC[i].addr, wdata, REG_DESC[i].mask, {"walking-ones ", REG_NAME[i]}); + end + end + for (b = 0; b < 32; b++) begin + if (REG_DESC[i].mask[b]) begin + wdata = REG_DESC[i].mask & ~(32'h1 << b); + periph_write(REG_DESC[i].addr, wdata, 4'hf); + expect_reg(REG_DESC[i].addr, wdata, REG_DESC[i].mask, {"walking-zeros ", REG_NAME[i]}); + end + end + periph_write(REG_DESC[i].addr, 32'hffff_ffff, 4'hf); + expect_reg(REG_DESC[i].addr, REG_DESC[i].mask, '1, {"reserved-bit masking ", REG_NAME[i]}); + periph_write(REG_DESC[i].addr, REG_DESC[i].reset_val, 4'hf); // restore + end + end + + // 3. RO immutability: write garbage, confirm unchanged. Writes to these + // addresses are no-ops in hwpe_ctrl_regif_example.sv (their decode + // strobe is gated by `!cpuif_req_is_wr`). + for (i = 0; i < NB_REG_DESC; i++) begin + if (!REG_DESC[i].writable) begin + periph_write(REG_DESC[i].addr, 32'hffff_ffff, 4'hf); + expect_reg(REG_DESC[i].addr, REG_DESC[i].reset_val, '1, {"RO immutability ", REG_NAME[i]}); + end + end + + // 4. Byte-enable partial writes on param0. + periph_write(32'h20, 32'h0000_0000, 4'hf); + periph_write(32'h20, 32'hAABB_CCDD, 4'h1); // byte0 only + expect_reg(32'h20, 32'h0000_00DD, '1, "param0 be=4'h1"); + periph_write(32'h20, 32'h1122_3344, 4'h2); // byte1 only + expect_reg(32'h20, 32'h0000_33DD, '1, "param0 be=4'h2"); + periph_write(32'h20, 32'hAABB_CCDD, 4'hc); // bytes[3:2] + expect_reg(32'h20, 32'hAABB_33DD, '1, "param0 be=4'hc"); + periph_write(32'h20, 32'h0000_0000, 4'hf); // restore + + // 5. hw -> sw mirroring: STATUS <= job_status_i, hw_status <= job_indep_hw_status_i. + job_status <= #TA 32'hCAFE_F00D; + wait_cycles(2); + expect_reg(32'h0c, 32'hCAFE_F00D, '1, "STATUS mirrors job_status_i"); + job_status <= #TA 32'h0000_0000; + + job_indep_hw_status <= #TA 32'h1234_5678; + wait_cycles(2); + expect_reg(32'h48, 32'h1234_5678, '1, "hw_status mirrors job_indep_hw_status_i"); + job_indep_hw_status <= #TA 32'h0000_0000; + wait_cycles(2); + + // 6. RUNNING_JOB follows job completions. job_done_i directly drives + // the job FIFO's pop_i (i_job_fifo in rtl/hwpe_ctrl_target.sv) -- + // popping an empty FIFO trips fifo_v3's `empty_read` assertion, so a + // job must be committed first. ACQUIRE is side-effecting -- handled + // deliberately here and cleaned up with soft_clear() afterwards. + expect_reg(32'h10, 32'h0000_0000, '1, "RUNNING_JOB before any completion"); + acquire(id_or_err); + check(id_or_err === 32'h0000_0000, $sformatf("reg_access: first ACQUIRE expected id 0, got 0x%08x", id_or_err)); + commit_trigger(1'b0, 1'b0); // overlapped commit + trigger + check_trigger_pulse(1'b1, "reg_access: RUNNING_JOB round commit+trigger"); + pulse_job_done(); + wait_cycles(2); + expect_reg(32'h10, 32'h0000_0001, '1, "RUNNING_JOB after one completion"); + + // Clean up: bring the device back to a pristine baseline. + soft_clear(1'b0, 1'b0); + wait_cycles(NbClearCyclesHier + 2); + expect_reg(32'h10, 32'h0000_0000, '1, "RUNNING_JOB after cleanup soft_clear"); + + $display("[TB] - test_reg_access: done (errors=%0d)", errors); + endtask + + /* ------------------------------------------------------------------ * + * Scenario: offload * + * ------------------------------------------------------------------ */ + + task automatic test_offload; + automatic logic [31:0] id_or_err; + + $display("[TB] - test_offload: starting"); + + // ---- Round 0: ACQUIRE locking, overlapped commit+trigger (0x0) ----- + acquire(id_or_err); + check(id_or_err === 32'h0, $sformatf("offload: ACQUIRE #0 expected id 0 got 0x%08x", id_or_err)); + acquire(id_or_err); // second ACQUIRE before COMMIT -> busy code + check(id_or_err === 32'hffff_fffe, $sformatf("offload: second ACQUIRE before COMMIT expected busy code, got 0x%08x", id_or_err)); + + periph_write(32'h20, 32'h1111_0000); // param0 + periph_write(32'h24, 32'h1111_0001); // param1 + periph_write(32'h28, 32'h0000_0010); // param2 (length=0x10) + commit_trigger(1'b0, 1'b0); // overlapped commit + trigger + check_trigger_pulse(1'b1, "offload: round0 overlapped commit+trigger"); + wait_cycles(1); #TT; + check(job_dep_regs_valid === 1'b1, "offload: round0 job_dep_regs_valid_o should be high after commit"); + check(job_dep_regs.param0.value.value === 32'h1111_0000, "offload: round0 job_dep_regs_o.param0 mismatch"); + check(job_dep_regs.param1.value.value === 32'h1111_0001, "offload: round0 job_dep_regs_o.param1 mismatch"); + check(job_dep_regs.param2.length.value === 16'h0010, "offload: round0 job_dep_regs_o.param2.length mismatch"); + + // ---- Round 1: acquire+commit while round0's job is still running, // + // separated commit (0x1) then trigger (0x2); the trigger must be // + // masked by job_running_q until round0 completes. -------------- // + acquire(id_or_err); + check(id_or_err === 32'h1, $sformatf("offload: ACQUIRE #1 expected id 1 got 0x%08x", id_or_err)); + periph_write(32'h20, 32'h2222_0000); + periph_write(32'h24, 32'h2222_0001); + periph_write(32'h28, 32'h0000_0020); + commit_trigger(1'b0, 1'b1); // commit only (0x1) + check_trigger_pulse(1'b0, "offload: round1 commit-only must not itself trigger"); + + commit_trigger(1'b1, 1'b0); // trigger only (0x2), round0 still running + check_trigger_pulse(1'b0, "offload: external trigger must be masked while a job is running"); + + // Finish round0: RUNNING_JOB advances, and since AUTOTRIGGER is enabled + // (autotrigger_n=0 by default) round1 starts automatically + // AUTOTRIGGER_WAIT_CYCLES after job_done_i, without any further write. + pulse_job_done(); + // job_done_q (and therefore the autotrigger pulse) lands + // AUTOTRIGGER_WAIT_CYCLES cycles after the job_done_i cycle; + // pulse_job_done() already returns at the edge right after the pulsed + // cycle, so two more edges remain by default (AUTOTRIGGER_WAIT_CYCLES=3). + // No bus transaction may be issued in between: periph_read/periph_write + // themselves consume two clock edges each and would blow this count. + wait_cycles(2); + #TT; + check(job_trigger === 1'b1, "offload: autotrigger did not fire for round1 after round0's completion"); + @(posedge clk_i); #TT; + check(job_trigger === 1'b0, "offload: autotrigger pulse did not deassert one cycle later"); + expect_reg(32'h10, 32'h1, '1, "offload: RUNNING_JOB after round0 completion"); + + // ---- Finish round1, disable autotrigger, and prove an explicit ----- // + // trigger is then required (round2). ---------------------------- // + pulse_job_done(); + wait_cycles(1); + expect_reg(32'h10, 32'h2, '1, "offload: RUNNING_JOB after round1 completion"); + check(job_dep_regs_valid === 1'b0, "offload: FIFO should be empty after both jobs complete"); + + periph_write(32'h08, 32'h0000_0001); // AUTOTRIGGER: autotrigger_n=1 (disabled) + expect_reg(32'h08, 32'h0000_0001, '1, "offload: AUTOTRIGGER readback after disabling"); + + acquire(id_or_err); + check(id_or_err === 32'h2, $sformatf("offload: ACQUIRE #2 expected id 2 got 0x%08x", id_or_err)); + periph_write(32'h20, 32'h3333_0000); + periph_write(32'h24, 32'h3333_0001); + periph_write(32'h28, 32'h0000_0030); + commit_trigger(1'b0, 1'b1); // commit only (0x1) + check_trigger_pulse(1'b0, "offload: round2 commit-only, autotrigger disabled, must not trigger"); + wait_cycles(3); #TT; + check(job_trigger === 1'b0, "offload: round2 must stay untriggered with autotrigger disabled and no explicit trigger"); + + commit_trigger(1'b1, 1'b0); // explicit trigger only (0x2) + check_trigger_pulse(1'b1, "offload: round2 explicit trigger with autotrigger disabled"); + + pulse_job_done(); + wait_cycles(1); + expect_reg(32'h10, 32'h3, '1, "offload: RUNNING_JOB after round2 completion"); + check(job_dep_regs_valid === 1'b0, "offload: FIFO should be empty after round2 completes"); + + periph_write(32'h08, 32'h0000_0000); // AUTOTRIGGER: restore autotrigger_n=0 (enabled) + + $display("[TB] - test_offload: done (errors=%0d)", errors); + endtask + + /* ------------------------------------------------------------------ * + * Scenario: fifo_backpressure * + * ------------------------------------------------------------------ */ + + task automatic test_fifo_backpressure; + automatic logic [31:0] id_or_err; + automatic int n; + + $display("[TB] - test_fifo_backpressure: starting NB_CONTEXT=%0d", NbContextHier); + + // Fill the FIFO to NB_CONTEXT jobs (commit only, no trigger, so nothing + // drains while we fill). gnt===1 is already asserted unconditionally by + // every periph_write/periph_read call above -- backpressure here is + // signalled only via ACQUIRE's return *data*, not the handshake. + for (n = 0; n < NbContextHier; n++) begin + acquire(id_or_err); + check(id_or_err === n, $sformatf("fifo_backpressure: ACQUIRE #%0d expected id %0d got 0x%08x", n, n, id_or_err)); + periph_write(32'h20, 32'h2000_0000 + n); + periph_write(32'h24, 32'h2100_0000 + n); + periph_write(32'h28, 32'h0000_0000 + n); + commit_trigger(1'b0, 1'b1); // commit only + wait_cycles(2); // FALL_THROUGH=0: pushed data visible one cycle after push + end + + // FIFO full: ACQUIRE must report the queue-full code, stably. + acquire(id_or_err); + check(id_or_err === 32'hffff_ffff, $sformatf("fifo_backpressure: ACQUIRE on full queue expected 0xffff_ffff got 0x%08x", id_or_err)); + acquire(id_or_err); + check(id_or_err === 32'hffff_ffff, "fifo_backpressure: ACQUIRE on full queue not stable across repeated reads"); + + // Drain one job; ACQUIRE must recover with the next sequential ID. + pulse_job_done(); + wait_cycles(1); + acquire(id_or_err); + check(id_or_err === NbContextHier, $sformatf("fifo_backpressure: ACQUIRE after drain expected id %0d got 0x%08x", NbContextHier, id_or_err)); + + // Payload FIFO ordering across the fill/drain: job #0 was popped by the + // job_done_i pulse above, so job_dep_regs_o must now show job #1's + // payload (guaranteed to exist since NB_CONTEXT>=2 in both CI legs). + check(job_dep_regs.param0.value.value === (32'h2000_0000 + 1), "fifo_backpressure: job_dep_regs_o.param0 not FIFO-ordered after drain"); + check(job_dep_regs.param1.value.value === (32'h2100_0000 + 1), "fifo_backpressure: job_dep_regs_o.param1 not FIFO-ordered after drain"); + + // Commit the just-acquired job: fill -> drain -> fill. + periph_write(32'h20, 32'h2000_0000 + NbContextHier); + periph_write(32'h24, 32'h2100_0000 + NbContextHier); + periph_write(32'h28, 32'h0000_0000 + NbContextHier); + commit_trigger(1'b0, 1'b1); + wait_cycles(2); + acquire(id_or_err); + check(id_or_err === 32'hffff_ffff, "fifo_backpressure: ACQUIRE after re-fill expected 0xffff_ffff"); + + $display("[TB] - test_fifo_backpressure: done (errors=%0d)", errors); + endtask + + /* ------------------------------------------------------------------ * + * Scenario: soft_clear * + * ------------------------------------------------------------------ */ + + task automatic test_soft_clear; + automatic logic [31:0] id_or_err; + automatic int c; + + $display("[TB] - test_soft_clear: starting"); + + // ---- Baseline: one committed, untriggered job. --------------------- + acquire(id_or_err); + check(id_or_err === 32'h0, "soft_clear: baseline ACQUIRE expected id 0"); + periph_write(32'h20, 32'hABCD_0000); + commit_trigger(1'b0, 1'b1); // commit only + wait_cycles(2); #TT; + check(job_dep_regs_valid === 1'b1, "soft_clear: baseline job_dep_regs_valid_o should be high before any clear"); + + // ---- 0x2 (regfile only): resets acquired/running IDs, returns the -- + // FSM to IDLE, flushes the FIFO; must NOT pulse clear_o. -------- + soft_clear(1'b0, 1'b1); // regfile_n=0 (clear), state_n=1 (leave) + wait_cycles(1); #TT; + check(clear === 1'b0, "soft_clear: 0x2 (regfile only) must not pulse clear_o"); + wait_cycles(NbClearCyclesHier + 2); #TT; + check(clear === 1'b0, "soft_clear: 0x2 (regfile only) clear_o must stay low throughout"); + check(job_dep_regs_valid === 1'b0, "soft_clear: 0x2 (regfile only) must flush the FIFO"); + acquire(id_or_err); + check(id_or_err === 32'h0, $sformatf("soft_clear: 0x2 (regfile only) did not reset job_acquired_id_q, got 0x%08x", id_or_err)); + // The probe ACQUIRE above locks the FSM into ACQUIRE state; absorb it + // with a commit (returns FSM to IDLE unconditionally) and re-flush with + // another regfile-only clear so the next phase starts pristine. + commit_trigger(1'b0, 1'b1); + wait_cycles(2); + soft_clear(1'b0, 1'b1); + wait_cycles(NbClearCyclesHier + 2); #TT; + check(job_dep_regs_valid === 1'b0, "soft_clear: cleanup after the 0x2 probe failed to re-flush"); + + // ---- 0x1 (state only): pulses clear_o for exactly NB_CLEAR_CYCLES -- + // cycles, but must NOT flush the FIFO or reset the acquired-ID -- + // counter. ------------------------------------------------------ + acquire(id_or_err); + check(id_or_err === 32'h0, "soft_clear: phase3 baseline ACQUIRE expected id 0"); + periph_write(32'h20, 32'hBEEF_0001); + commit_trigger(1'b0, 1'b1); + wait_cycles(2); #TT; + check(job_dep_regs_valid === 1'b1, "soft_clear: phase3 baseline job_dep_regs_valid_o should be high"); + + soft_clear(1'b1, 1'b0); // regfile_n=1 (leave), state_n=0 (clear) + c = 0; + while (clear !== 1'b1 && c < 10) begin + wait_cycles(1); #TT; + c++; + end + check(clear === 1'b1, "soft_clear: 0x1 (state only) did not pulse clear_o at all"); + c = 0; + while (clear === 1'b1 && c < NbClearCyclesHier + 5) begin + wait_cycles(1); #TT; + c++; + end + check(c === NbClearCyclesHier, $sformatf("soft_clear: 0x1 (state only) clear_o held high for %0d cycles, expected NB_CLEAR_CYCLES=%0d", c, NbClearCyclesHier)); + check(job_dep_regs_valid === 1'b1, "soft_clear: 0x1 (state only) must NOT flush the FIFO"); + acquire(id_or_err); + check(id_or_err === 32'h1, $sformatf("soft_clear: 0x1 (state only) must not reset job_acquired_id_q, expected id 1 got 0x%08x", id_or_err)); + // Absorb the probe ACQUIRE's FSM lock again, then fully clear for the + // next phase. + commit_trigger(1'b0, 1'b1); + wait_cycles(2); + soft_clear(1'b0, 1'b0); + wait_cycles(NbClearCyclesHier + 2); + + // ---- 0x0 (both): flushes the FIFO, resets IDs, AND pulses clear_o. - + acquire(id_or_err); + check(id_or_err === 32'h0, "soft_clear: phase4 baseline ACQUIRE expected id 0 (after phase3 cleanup)"); + periph_write(32'h20, 32'hF0F0_0000); + commit_trigger(1'b0, 1'b1); + wait_cycles(2); + + soft_clear(1'b0, 1'b0); + wait_cycles(1); #TT; + check(clear === 1'b1, "soft_clear: 0x0 (both) should pulse clear_o immediately"); + wait_cycles(NbClearCyclesHier + 2); #TT; + check(clear === 1'b0, "soft_clear: 0x0 (both) clear_o should have deasserted by now"); + check(job_dep_regs_valid === 1'b0, "soft_clear: 0x0 (both) must flush the FIFO"); + acquire(id_or_err); + check(id_or_err === 32'h0, "soft_clear: 0x0 (both) must reset job_acquired_id_q to 0"); + commit_trigger(1'b0, 1'b1); // absorb the probe ACQUIRE's FSM lock + wait_cycles(2); + soft_clear(1'b0, 1'b0); // discard the phantom job just committed + wait_cycles(NbClearCyclesHier + 2); #TT; + + // ---- 0x3 (no-op): changes nothing observable. ----------------------- + check(job_dep_regs_valid === 1'b0, "soft_clear: pre-0x3 baseline FIFO should be empty"); + soft_clear(1'b1, 1'b1); + wait_cycles(1); #TT; + check(clear === 1'b0, "soft_clear: 0x3 (no-op) must not pulse clear_o"); + wait_cycles(NbClearCyclesHier + 2); #TT; + check(clear === 1'b0, "soft_clear: 0x3 (no-op) clear_o must stay low"); + check(job_dep_regs_valid === 1'b0, "soft_clear: 0x3 (no-op) FIFO should remain empty (unchanged)"); + acquire(id_or_err); + check(id_or_err === 32'h0, "soft_clear: 0x3 (no-op) should not have perturbed job_acquired_id_q"); + commit_trigger(1'b0, 1'b1); // absorb the probe ACQUIRE's FSM lock + + // ---- Prove the system is fully usable again: one more successful --- + // ACQUIRE -> COMMIT -> TRIGGER -> done round, from a known-clean -- + // baseline. ------------------------------------------------------ + soft_clear(1'b0, 1'b0); + wait_cycles(NbClearCyclesHier + 2); + + acquire(id_or_err); + check(id_or_err === 32'h0, $sformatf("soft_clear: final usability round ACQUIRE expected id 0 got 0x%08x", id_or_err)); + periph_write(32'h20, 32'hF00D_0000); + commit_trigger(1'b0, 1'b0); // overlapped commit + trigger + check_trigger_pulse(1'b1, "soft_clear: final usability round commit+trigger"); + pulse_job_done(); + wait_cycles(2); + expect_reg(32'h10, 32'h1, '1, "soft_clear: final usability round RUNNING_JOB after done"); + check(job_dep_regs_valid === 1'b0, "soft_clear: final usability round FIFO should be empty after done"); + + $display("[TB] - test_soft_clear: done (errors=%0d)", errors); + endtask + + /* ------------------------------------------------------------------ * + * Top-level scenario dispatch * + * ------------------------------------------------------------------ */ + + initial begin + string test_name; + + errors = 0; + id_cnt = '0; + + periph.req = 1'b0; + periph.wen = 1'b1; + periph.add = '0; + periph.data = '0; + periph.be = '0; + periph.id = '0; + job_done = 1'b0; + job_status = 32'h0; + job_indep_hw_status = 32'h0; + + // Let clk_i/rst_ni (driven from hwpe_ctrl_target_tb_wrap) settle past + // reset deassertion before touching the bus. + wait_cycles(25); + + // Every scenario starts with a full soft_clear + settle for isolation. + soft_clear(1'b0, 1'b0); + wait_cycles(NbClearCyclesHier + 2); + + if (!$value$plusargs("TEST=%s", test_name)) begin + $display("[TB] - ERROR: no +TEST= plusarg given"); + errors++; + test_name = ""; + end + + case (test_name) + "reg_access": test_reg_access(); + "offload": test_offload(); + "fifo_backpressure": test_fifo_backpressure(); + "soft_clear": test_soft_clear(); + default: begin + $display($sformatf("[TB] - ERROR: unknown or missing +TEST '%s'", test_name)); + errors++; + end + endcase + + wait_cycles(5); + + if (errors == 0) $display("[TB] - Success!"); + else begin + $display("[TB] - Fail!"); + $error("[TB] - errors=%0d", errors); + end + $finish; + end + +endmodule // hwpe_ctrl_target_tb diff --git a/target/sim/src/hwpe_ctrl_target_tb_wrap.sv b/target/sim/src/hwpe_ctrl_target_tb_wrap.sv new file mode 100644 index 0000000..c429958 --- /dev/null +++ b/target/sim/src/hwpe_ctrl_target_tb_wrap.sv @@ -0,0 +1,81 @@ +// Copyright 2025-2026 ETH Zurich and University of Bologna. +// Solderpad Hardware License, Version 0.51, see LICENSE for details. +// SPDX-License-Identifier: SHL-0.51 + +/* + * hwpe_ctrl_target_tb_wrap.sv + * Francesco Conti + * + * Copyright (C) 2025-2026 ETH Zurich, University of Bologna + * Copyright and related rights are licensed under the Solderpad Hardware + * License, Version 0.51 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://solderpad.org/licenses/SHL-0.51. Unless required by applicable law + * or agreed to in writing, software, hardware and materials distributed under + * this License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +/* + * Portless verilator top for the hwpe_ctrl_target testbench (see + * target/sim/verilator/verilator.mk: --top-module hwpe_ctrl_target_tb_wrap). + * Being portless is what makes `verilator --binary --top-module` work + * directly, mirroring redmule_tb_wrap.sv in the RedMulE repository. + * + * NB_CONTEXT is exposed as a module parameter purely so that verilator's + * elaboration-time `-GNB_CONTEXT=` override (see verilator.mk) has a + * handle at the top level; it threads down through hwpe_ctrl_target_tb to + * hwpe_ctrl_target_wrap and finally to hwpe_ctrl_target's own NB_CONTEXT. + */ + +timeunit 1ps; +timeprecision 1ps; + +module hwpe_ctrl_target_tb_wrap +#( + parameter int unsigned NB_CONTEXT = 2 +); + + // ATI timing parameters. + localparam time TCP = 1.0ns; // clock period, 1 GHz clock + localparam time TA = 0.2ns; // application time + localparam time TT = 0.8ns; // test time + + logic clk_i; + logic rst_ni; + + hwpe_ctrl_target_tb #( + .TCP ( TCP ), + .TA ( TA ), + .TT ( TT ), + .NB_CONTEXT ( NB_CONTEXT ) + ) i_tb ( + .clk_i ( clk_i ), + .rst_ni ( rst_ni ) + ); + + // Performs one entire clock cycle. + task automatic cycle; + clk_i <= #(TCP/2) 1'b0; + clk_i <= #TCP 1'b1; + #TCP; + endtask + + // Free-running clock/reset generation process. hwpe_ctrl_target_tb (and + // the driver tasks within it) synchronize to clk_i/rst_ni via + // @(posedge clk_i); the actual test scenario runs there and calls + // $finish itself once done -- this process just keeps the clock alive + // for as long as the simulation runs. + initial begin + clk_i <= 1'b0; + rst_ni <= 1'b0; + + for (int i = 0; i < 20; i++) cycle(); + + rst_ni <= #TA 1'b1; + + while (1) cycle(); + end + +endmodule // hwpe_ctrl_target_tb_wrap diff --git a/target/sim/src/hwpe_ctrl_target_wrap.sv b/target/sim/src/hwpe_ctrl_target_wrap.sv new file mode 100644 index 0000000..f786f66 --- /dev/null +++ b/target/sim/src/hwpe_ctrl_target_wrap.sv @@ -0,0 +1,146 @@ +// Copyright 2025-2026 ETH Zurich and University of Bologna. +// Solderpad Hardware License, Version 0.51, see LICENSE for details. +// SPDX-License-Identifier: SHL-0.51 + +/* + * hwpe_ctrl_target_wrap.sv + * Francesco Conti + * + * Copyright (C) 2025-2026 ETH Zurich, University of Bologna + * Copyright and related rights are licensed under the Solderpad Hardware + * License, Version 0.51 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://solderpad.org/licenses/SHL-0.51. Unless required by applicable law + * or agreed to in writing, software, hardware and materials distributed under + * this License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +/* + * Integration wrapper binding hwpe_ctrl_target to the PeakRDL-generated + * hwpe_ctrl_regif_example register block (rtl/rdl-example/, produced by + * `make regif` from rtl/hwpe_ctrl_regif_example.rdl -- see that file for the + * authoritative register map). This is the same pattern as + * redmule_target_decoder.sv in the RedMulE repository: 1) plug the + * SystemRDL-generated types into hwpe_ctrl_target's parametric + * hwpe_ctrl_regif_*_t / hwpe_ctrl_job_*_t parameters; 2) wire the 11-signal + * cpuif "passthrough" bundle straight through between hwpe_ctrl_target and + * the regblock; 3) plug hwif_in/hwif_out through, overriding only the one + * hwif_in leaf (hw_status) that is driven from outside the generated + * register file (job_indep_hw_status_i, exercising the hw->sw status path). + */ + +module hwpe_ctrl_target_wrap + import hwpe_ctrl_package::*; + import hwpe_ctrl_regif_example_pkg::*; +#( + parameter int unsigned NB_CONTEXT = 2, + parameter int unsigned NB_CLEAR_CYCLES = 3 +) +( + input logic clk_i, + input logic rst_ni, + output logic clear_o, + + // peripheral interconnect side (register-mapped access from the TB) + hwpe_ctrl_intf_periph.slave target, + + // job triggering, completion & status + output logic job_trigger_o, + input logic job_done_i, + input logic [31:0] job_status_i, + + // TB-driven value for the hw_status job-indep leaf (hw=w, sw=r) + input logic [31:0] job_indep_hw_status_i, + + // job-independent registers (config_a, config_b, hw_status) + output hwpe_ctrl_regif_example__hwpe_ctrl_job_indep__out_t job_indep_regs_o, + + // job-dependent registers (param0, param1, param2) + output logic job_dep_regs_valid_o, + output hwpe_ctrl_regif_example__hwpe_ctrl_job_dep__out_t job_dep_regs_o +); + + // cpuif plug target <-> regif (PeakRDL "passthrough") + logic target_cpuif_req; + logic target_cpuif_req_is_wr; + logic [31:0] target_cpuif_addr; + logic [31:0] target_cpuif_wr_data; + logic [31:0] target_cpuif_wr_biten; + logic target_cpuif_req_stall_wr; + logic target_cpuif_req_stall_rd; + logic target_cpuif_rd_ack; + logic target_cpuif_rd_err; + logic [31:0] target_cpuif_rd_data; + logic target_cpuif_wr_ack; + logic target_cpuif_wr_err; + + hwpe_ctrl_regif_example__in_t hwif_in_target; + hwpe_ctrl_regif_example__in_t hwif_in; + hwpe_ctrl_regif_example__out_t hwif_out; + + hwpe_ctrl_target #( + .NB_CONTEXT ( NB_CONTEXT ), + .NB_CLEAR_CYCLES ( NB_CLEAR_CYCLES ), + .ID_WIDTH ( 2 ), + .ADDR_WIDTH ( 8 ), + .hwpe_ctrl_regif_in_t ( hwpe_ctrl_regif_example__in_t ), + .hwpe_ctrl_regif_out_t ( hwpe_ctrl_regif_example__out_t ), + .hwpe_ctrl_job_indep_t ( hwpe_ctrl_regif_example__hwpe_ctrl_job_indep__out_t ), + .hwpe_ctrl_job_dep_t ( hwpe_ctrl_regif_example__hwpe_ctrl_job_dep__out_t ) + ) i_target ( + .clk_i ( clk_i ), + .rst_ni ( rst_ni ), + .clear_o ( clear_o ), + .target ( target ), + .job_trigger_o ( job_trigger_o ), + .job_done_i ( job_done_i ), + .job_status_i ( job_status_i ), + .job_indep_regs_o ( job_indep_regs_o ), + .job_dep_regs_valid_o ( job_dep_regs_valid_o ), + .job_dep_regs_o ( job_dep_regs_o ), + .target_cpuif_req_o ( target_cpuif_req ), + .target_cpuif_req_is_wr_o ( target_cpuif_req_is_wr ), + .target_cpuif_addr_o ( target_cpuif_addr ), + .target_cpuif_wr_data_o ( target_cpuif_wr_data ), + .target_cpuif_wr_biten_o ( target_cpuif_wr_biten ), + .target_cpuif_req_stall_wr_i ( target_cpuif_req_stall_wr ), + .target_cpuif_req_stall_rd_i ( target_cpuif_req_stall_rd ), + .target_cpuif_rd_ack_i ( target_cpuif_rd_ack ), + .target_cpuif_rd_err_i ( target_cpuif_rd_err ), + .target_cpuif_rd_data_i ( target_cpuif_rd_data ), + .target_cpuif_wr_ack_i ( target_cpuif_wr_ack ), + .target_cpuif_wr_err_i ( target_cpuif_wr_err ), + .hwif_in ( hwif_in_target ), + .hwif_out ( hwif_out ) + ); + + hwpe_ctrl_regif_example i_regif ( + .clk ( clk_i ), + .arst_n ( rst_ni ), + .s_cpuif_req ( target_cpuif_req ), + .s_cpuif_req_is_wr ( target_cpuif_req_is_wr ), + .s_cpuif_addr ( target_cpuif_addr ), + .s_cpuif_wr_data ( target_cpuif_wr_data ), + .s_cpuif_wr_biten ( target_cpuif_wr_biten ), + .s_cpuif_req_stall_wr ( target_cpuif_req_stall_wr ), + .s_cpuif_req_stall_rd ( target_cpuif_req_stall_rd ), + .s_cpuif_rd_ack ( target_cpuif_rd_ack ), + .s_cpuif_rd_err ( target_cpuif_rd_err ), + .s_cpuif_rd_data ( target_cpuif_rd_data ), + .s_cpuif_wr_ack ( target_cpuif_wr_ack ), + .s_cpuif_wr_err ( target_cpuif_wr_err ), + .hwif_in ( hwif_in ), + .hwif_out ( hwif_out ) + ); + + // Copy hwif_in from hwpe_ctrl_target verbatim, overriding only the + // hw_status leaf (driven from outside the generated register file) -- + // mirrors redmule_target_decoder.sv's hwif_in override for op_id_cnt. + always_comb begin + hwif_in = hwif_in_target; + hwif_in.hwpe_job_indep.hw_status.value.next = job_indep_hw_status_i; + end + +endmodule // hwpe_ctrl_target_wrap diff --git a/tb/tb_hwpe_ctrl_uloop.sv b/target/sim/src/hwpe_ctrl_uloop_tb.sv similarity index 67% rename from tb/tb_hwpe_ctrl_uloop.sv rename to target/sim/src/hwpe_ctrl_uloop_tb.sv index 79859d0..450e4a9 100644 --- a/tb/tb_hwpe_ctrl_uloop.sv +++ b/target/sim/src/hwpe_ctrl_uloop_tb.sv @@ -1,8 +1,12 @@ +// Copyright 2019 ETH Zurich and University of Bologna. +// Solderpad Hardware License, Version 0.51, see LICENSE for details. +// SPDX-License-Identifier: SHL-0.51 + /* - * tb_hwpe_ctrl_uloop.sv + * hwpe_ctrl_uloop_tb.sv * Francesco Conti * - * Copyright (C) 2019 ETH Zurich, University of Bologna + * Copyright (C) 2019-2026 ETH Zurich, University of Bologna * Copyright and related rights are licensed under the Solderpad Hardware * License, Version 0.51 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at @@ -14,21 +18,73 @@ * */ -timeunit 1ps; -timeprecision 1ps; - +/* + * Direct testbench for hwpe_ctrl_uloop, migrated from the legacy + * tb/tb_hwpe_ctrl_uloop.sv (QuestaSim, portless, no file I/O). The 730x4 + * golden matrix (`ground_truth`) and the two microcode constants + * (`uloop_code_i.code`/`.loops`) below are hard-coded literals produced + * offline by the Python2 scripts in uloop-example/ (uloop_run.py / + * uloop_compile.py); those scripts are only a provenance trail, not a + * build dependency -- do not attempt to regenerate the literals. + * + * clk_i/rst_ni are generated by the portless top hwpe_ctrl_uloop_tb_wrap; + * the poll loop below synchronizes to clk_i via @(posedge clk_i) and + * drives ctrl_i with ATI (#TA) timing, matching hwpe_ctrl_target_tb.sv. + * + * Scenario selection is via +TEST=; only "uloop" is defined. A + * missing or unrecognised +TEST is a hard failure, never a silent pass. + */ -module tb_hwpe_ctrl_uloop; +module hwpe_ctrl_uloop_tb import hwpe_ctrl_package::*; +#( + parameter time TCP = 1.0ns, // clock period, 1 GHz clock + parameter time TA = 0.2ns, // application time + parameter time TT = 0.8ns // test time +) +( + input logic clk_i, + input logic rst_ni +); - // parameters - parameter int unsigned NB_LOOPS = hwpe_ctrl_package::ULOOP_NB_LOOPS; - parameter int unsigned LENGTH = hwpe_ctrl_package::ULOOP_LENGTH; - parameter int unsigned NB_RO_REG = 12; - parameter int unsigned NB_REG = 4; - parameter int unsigned REG_WIDTH = hwpe_ctrl_package::ULOOP_REG_WIDTH; - parameter int unsigned CNT_WIDTH = hwpe_ctrl_package::ULOOP_CNT_WIDTH; - parameter int unsigned SHADOWED = 1; // hwpe_ctrl_package::ULOOP_SHADOWED; + /* ------------------------------------------------------------------ * + * DUT instantiation * + * ------------------------------------------------------------------ */ + + // NB_LOOPS/REG_WIDTH/CNT_WIDTH/SHADOWED below come from the package's + // ULOOP_MAX_* / ULOOP_DEFAULT_SHADOWED parameters (renamed from the old + // ULOOP_NB_LOOPS/ULOOP_LENGTH/ULOOP_REG_WIDTH/ULOOP_CNT_WIDTH/ + // ULOOP_SHADOWED by commit 186d5ab, "Treat uloop parameters as maxima / + // defaults"). Cross-checked against `git show 186d5ab` and + // rtl/hwpe_ctrl_uloop.sv: + // - ULOOP_NB_LOOPS=6 -> ULOOP_MAX_NB_LOOPS=6 (unchanged) + // - ULOOP_REG_WIDTH=32-> ULOOP_MAX_REG_WIDTH=32(unchanged) + // - ULOOP_CNT_WIDTH=12-> ULOOP_MAX_CNT_WIDTH=12(unchanged) + // - ULOOP_SHADOWED=1 -> ULOOP_DEFAULT_SHADOWED=1 (unchanged; this TB + // hard-codes SHADOWED=1 directly anyway, as the legacy file did) + // - ULOOP_LENGTH=17 -> ULOOP_MAX_LENGTH=32 (VALUE CHANGED) + // The LENGTH value change is inert for this DUT configuration: LENGTH + // only sizes the module-local `curr_addr` counter via $clog2(LENGTH) + // (rtl/hwpe_ctrl_uloop.sv:172), and $clog2(17) == $clog2(32) == 5, so + // the counter width is identical either way. The `uloop_code_i.code` + // field itself is *not* sized by this module's LENGTH parameter -- it + // is a fixed `uloop_bytecode_t [ULOOP_MAX_LENGTH-1:0]` from the + // package's uloop_code_t (also true before the rename, just against + // the then-current ULOOP_LENGTH). The 192-bit `code` literal below is + // right-justified into that field regardless of its total width, so + // the low 17 words (0..16, all this program's loop bodies ever + // address) land on the exact same bits before and after the rename; + // the rename only grows the number of trailing always-zero/unused + // words. NB_REG=4/NB_RO_REG=12 below are this TB's own local overrides + // (as in the legacy file), independent of ULOOP_MAX_NB_REG/ + // ULOOP_MAX_NB_RO_REG. Net result: the golden vectors remain valid. + localparam int unsigned NB_LOOPS = hwpe_ctrl_package::ULOOP_MAX_NB_LOOPS; + localparam int unsigned LENGTH = hwpe_ctrl_package::ULOOP_MAX_LENGTH; + localparam int unsigned NB_RO_REG = 12; + localparam int unsigned NB_REG = 4; + localparam int unsigned REG_WIDTH = hwpe_ctrl_package::ULOOP_MAX_REG_WIDTH; + localparam int unsigned CNT_WIDTH = hwpe_ctrl_package::ULOOP_MAX_CNT_WIDTH; + localparam int unsigned SHADOWED = 1; // hwpe_ctrl_package::ULOOP_DEFAULT_SHADOWED // testbench parameters -- see uloop-example/uloop_run.py int oh = 3; @@ -38,9 +94,6 @@ module tb_hwpe_ctrl_uloop; int fs0 = 3; int nif_div_TP = 384/128; - // signals - logic clk_i = '0; - logic rst_ni = '1; logic test_mode_i = '0; logic clear_i = '0; ctrl_uloop_t ctrl_i; @@ -48,33 +101,6 @@ module tb_hwpe_ctrl_uloop; uloop_code_t uloop_code_i; logic [NB_RO_REG-1:0][REG_WIDTH-1:0] registers_read_i; - // ATI timing parameters. - localparam TCP = 1.0ns; // clock period, 1 GHz clock - localparam TA = 0.2ns; // application time - localparam TT = 0.8ns; // test time - - // Performs one entire clock cycle. - task cycle; - clk_i <= #(TCP/2) 0; - clk_i <= #TCP 1; - #TCP; - endtask - - // The following task schedules the clock edges for the next cycle and - // advances the simulation time to that cycles test time (localparam TT) - // according to ATI timings. - task cycle_start; - clk_i <= #(TCP/2) 0; - clk_i <= #TCP 1; - #TT; - endtask - - // The following task finishes a clock cycle previously started with - // cycle_start by advancing the simulation time to the end of the cycle. - task cycle_end; - #(TCP-TT); - endtask - hwpe_ctrl_uloop #( .NB_LOOPS ( NB_LOOPS ), .LENGTH ( LENGTH ), @@ -94,29 +120,24 @@ module tb_hwpe_ctrl_uloop; .registers_read_i ( registers_read_i ) ); - // clock/reset gen process - initial begin - #(20*TCP); - - // Reset phase. - rst_ni <= #TA 1'b0; - #(20*TCP); - rst_ni <= #TA 1'b1; - - for (int i = 0; i < 10; i++) - cycle(); - rst_ni <= #TA 1'b0; - for (int i = 0; i < 10; i++) - cycle(); - rst_ni <= #TA 1'b1; - - while(1) begin - cycle(); - end + /* ------------------------------------------------------------------ * + * Bookkeeping * + * ------------------------------------------------------------------ */ - end + int errors; + + task automatic wait_cycles(input int n); + repeat (n) @(posedge clk_i); + endtask - // see uloop-example/uloop_run.py + /* ------------------------------------------------------------------ * + * Golden reference: (offs[0], offs[1], offs[2], offs[3]) expected on * + * each of the 729 valid outputs the uloop produces for the program * + * below (3x3x3x3x3x3 = 729 total iterations across the 6 nested * + * loops with range oh=ow=nof_div_TP=fs1=fs0=nif_div_TP=3). Entry 0 is * + * an unused sentinel -- comparisons run over indices 1..729. * + * See uloop-example/uloop_run.py (provenance trail only). * + * ------------------------------------------------------------------ */ logic [0:729][0:3][31:0] ground_truth = { {0,0,0,0}, {128,128,0,0}, @@ -850,10 +871,30 @@ module tb_hwpe_ctrl_uloop; {10240,9472,3328,4608} }; - int i=1; - // test process - initial begin + /* ------------------------------------------------------------------ * + * Scenario: uloop * + * ------------------------------------------------------------------ */ + + task automatic test_uloop; + automatic int i; + automatic bit early_exit; + automatic int n_compared; + + n_compared = 0; ctrl_i = '0; + // ctrl_i.ready is a flow-control gate added to the SHADOWED datapath + // by commit 0ee905c ("Add ready control bit to uloop", 2020-08-17), + // postdating both this TB (2019) and the ULOOP_* -> ULOOP_MAX_* + // rename (186d5ab, 2020-03-25): rtl/hwpe_ctrl_uloop.sv's + // shadowed_gen block ANDs it directly into enable_int, so leaving it + // at its ctrl_i='0 reset value permanently blocks the uloop's + // internal fetch/execute pipeline -- flags_o.valid/.done never + // assert and the poll loop below would spin forever. No wrapper in + // this repo instantiates hwpe_ctrl_uloop to show the intended + // driving convention (grep turns up only this TB and the module + // itself); tying it high here is the natural choice for a + // standalone, non-flow-controlled testbench like this one. + ctrl_i.ready = 1'b1; registers_read_i = '0; registers_read_i[0] = nif_div_TP * 128; // nif registers_read_i[1] = nof_div_TP * 128; // nof @@ -868,38 +909,132 @@ module tb_hwpe_ctrl_uloop; registers_read_i[11] = 128*128; // TP2 uloop_code_i = '0; uloop_code_i.code = 192'h00238d9128070238c91280702389502215c0885102214408; // see uloop-example/uloop_compile.py - uloop_code_i.loops = 48'h6c4c33221202; // see uloop-example/uloop_compile.py + // uloop_code_i.loops: the legacy TB's literal here was 48'h6c4c33221202, + // packed for the *pre-2020-03-31* uloop_loops_t layout (5-bit addr + + // 3-bit nb_ops = 8-bit word x 6 loops = 48 bits exactly, no slack). + // Commit 1b1dfd0 ("increase number of bits used in loop for number of + // operations", 2020-03-31) widened nb_ops to 4 bits (9-bit word x 6 = + // 54 bits), so re-using the old 48-bit literal as-is silently + // zero-extends into the wider field and shifts every word boundary, + // corrupting all six (addr, nb_ops) pairs -- confirmed by decoding + // the old literal both ways: under the 8-bit packing it yields + // (addr, nb_ops) = (0,2),(2,2),(4,2),(6,3),(9,4),(13,4), which is a + // clean, non-overlapping, gap-free program spanning exactly code + // words 0..16 (17 words, matching the *contemporaneous* ULOOP_LENGTH + // default of 17 -- see git show 1b1dfd0/186d5ab) and reproduces + // ground_truth exactly; under the current 9-bit packing the same 48 + // raw bits decode to nonsensical/overlapping loop bodies (one header + // even points past the populated code words), which is what caused + // registers to accumulate without ever resetting. Re-packed below + // for the current 9-bit-word uloop_loops_t using the same logical + // (addr, nb_ops) pairs -- this is a wire-format repack of unchanged + // loop-header intent, not a change to the expected results. + uloop_code_i.loops = 54'h1a894319084402; uloop_code_i.range[5] = oh; uloop_code_i.range[4] = ow; uloop_code_i.range[3] = nof_div_TP; uloop_code_i.range[2] = fs1; uloop_code_i.range[1] = fs0; uloop_code_i.range[0] = nif_div_TP; - #(100*TCP); - while(~flags_o.done) begin - while(~flags_o.ready) begin // wait until the uloop is ready (if SHADOWED=1) - if(flags_o.done) - $finish(); - ctrl_i.enable = 1'b0; - #(TCP); + + $display("[TB] - test_uloop: starting"); + + wait_cycles(100); + + i = 1; + early_exit = 1'b0; + + while (!flags_o.done && !early_exit) begin + while (!flags_o.ready) begin // wait until the uloop is ready (if SHADOWED=1) + if (flags_o.done) begin + early_exit = 1'b1; + break; + end + ctrl_i.enable <= #TA 1'b0; + @(posedge clk_i); #TT; + end + if (early_exit) break; + + while (!flags_o.valid) begin // get indices from the uloop once ready + if (flags_o.done) begin + early_exit = 1'b1; + break; + end + ctrl_i.enable <= #TA ~flags_o.done; + @(posedge clk_i); #TT; + end + if (early_exit) break; + + if (i > 729) begin + errors++; + $display("[TB] - ERROR: golden-array index %0d exceeds ground_truth bounds [1:729] (time=%0t)", i, $time); + early_exit = 1'b1; + break; end - while(~flags_o.valid) begin // get indeces from the uloop if it is ready - if(flags_o.done) - $finish(); - ctrl_i.enable = ~flags_o.done; - #(TCP); + + if ((flags_o.offs[0] !== ground_truth[i][0]) || + (flags_o.offs[1] !== ground_truth[i][1]) || + (flags_o.offs[2] !== ground_truth[i][2]) || + (flags_o.offs[3] !== ground_truth[i][3])) begin + errors++; + $display("[TB] - ERROR: mismatch at index %0d: expected {%0d,%0d,%0d,%0d} got {%0d,%0d,%0d,%0d} (time=%0t)", + i, + ground_truth[i][0], ground_truth[i][1], ground_truth[i][2], ground_truth[i][3], + flags_o.offs[0], flags_o.offs[1], flags_o.offs[2], flags_o.offs[3], + $time); end - if ((flags_o.offs[0] != ground_truth[i][0]) || - (flags_o.offs[1] != ground_truth[i][1]) || - (flags_o.offs[2] != ground_truth[i][2]) || - (flags_o.offs[3] != ground_truth[i][3])) - $fatal(); + n_compared = i; i += 1; - ctrl_i.enable = 1'b0; - #(TCP); + ctrl_i.enable <= #TA 1'b0; + @(posedge clk_i); #TT; + end + + // Guard against a DUT that finishes early (flags_o.done asserted + // before all 729 golden rows were produced) which would otherwise + // pass trivially by simply comparing fewer rows. + if (n_compared != 729) begin + errors++; + $display("[TB] - ERROR: expected 729 golden-row comparisons, only %0d ran (time=%0t)", n_compared, $time); + end + + wait_cycles(20); + + $display("[TB] - test_uloop: done (errors=%0d, compared=%0d)", errors, n_compared); + endtask + + /* ------------------------------------------------------------------ * + * Top-level scenario dispatch * + * ------------------------------------------------------------------ */ + + initial begin + string test_name; + + errors = 0; + + // Let clk_i/rst_ni (driven from hwpe_ctrl_uloop_tb_wrap) settle past + // reset deassertion before touching the DUT. + wait_cycles(25); + + if (!$value$plusargs("TEST=%s", test_name)) begin + $display("[TB] - ERROR: no +TEST= plusarg given"); + errors++; + test_name = ""; + end + + case (test_name) + "uloop": test_uloop(); + default: begin + $display($sformatf("[TB] - ERROR: unknown or missing +TEST '%s'", test_name)); + errors++; + end + endcase + + if (errors == 0) $display("[TB] - Success!"); + else begin + $display("[TB] - Fail!"); + $error("[TB] - errors=%0d", errors); end - #(20*TCP); - $finish(); + $finish; end -endmodule // tb_hwpe_ctrl_uloop +endmodule // hwpe_ctrl_uloop_tb diff --git a/target/sim/src/hwpe_ctrl_uloop_tb_wrap.sv b/target/sim/src/hwpe_ctrl_uloop_tb_wrap.sv new file mode 100644 index 0000000..aac6b9a --- /dev/null +++ b/target/sim/src/hwpe_ctrl_uloop_tb_wrap.sv @@ -0,0 +1,85 @@ +// Copyright 2019 ETH Zurich and University of Bologna. +// Solderpad Hardware License, Version 0.51, see LICENSE for details. +// SPDX-License-Identifier: SHL-0.51 + +/* + * hwpe_ctrl_uloop_tb_wrap.sv + * Francesco Conti + * + * Copyright (C) 2019-2026 ETH Zurich, University of Bologna + * Copyright and related rights are licensed under the Solderpad Hardware + * License, Version 0.51 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://solderpad.org/licenses/SHL-0.51. Unless required by applicable law + * or agreed to in writing, software, hardware and materials distributed under + * this License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +/* + * Portless verilator top for the hwpe_ctrl_uloop testbench (see + * target/sim/verilator/verilator.mk: --top-module hwpe_ctrl_uloop_tb_wrap). + * Being portless is what makes `verilator --binary --top-module` work + * directly, mirroring hwpe_ctrl_target_tb_wrap.sv. + * + * The clock/reset sequence below reproduces the legacy + * tb/tb_hwpe_ctrl_uloop.sv clock/reset `initial` (its lines 97-117) + * verbatim: settle, assert reset, deassert, run 10 cycles, briefly + * re-assert reset for 10 more cycles, deassert again, then free-run. + * That file was portless too and generated its own clk_i/rst_ni in the + * same module as the test; here that responsibility is split out into + * this dedicated top, with hwpe_ctrl_uloop_tb driven via ports. + */ + +timeunit 1ps; +timeprecision 1ps; + +module hwpe_ctrl_uloop_tb_wrap; + + // ATI timing parameters. + localparam time TCP = 1.0ns; // clock period, 1 GHz clock + localparam time TA = 0.2ns; // application time + localparam time TT = 0.8ns; // test time + + logic clk_i = '0; + logic rst_ni = '1; + + hwpe_ctrl_uloop_tb #( + .TCP ( TCP ), + .TA ( TA ), + .TT ( TT ) + ) i_tb ( + .clk_i ( clk_i ), + .rst_ni ( rst_ni ) + ); + + // Performs one entire clock cycle. + task automatic cycle; + clk_i <= #(TCP/2) 0; + clk_i <= #TCP 1; + #TCP; + endtask + + // clock/reset gen process + initial begin + #(20*TCP); + + // Reset phase. + rst_ni <= #TA 1'b0; + #(20*TCP); + rst_ni <= #TA 1'b1; + + for (int i = 0; i < 10; i++) + cycle(); + rst_ni <= #TA 1'b0; + for (int i = 0; i < 10; i++) + cycle(); + rst_ni <= #TA 1'b1; + + while (1) begin + cycle(); + end + end + +endmodule // hwpe_ctrl_uloop_tb_wrap diff --git a/target/sim/verilator/verilator.mk b/target/sim/verilator/verilator.mk new file mode 100644 index 0000000..f1c5519 --- /dev/null +++ b/target/sim/verilator/verilator.mk @@ -0,0 +1,85 @@ +# Copyright 2025 ETH Zurich and University of Bologna. +# Solderpad Hardware License, Version 0.51, see LICENSE for details. +# SPDX-License-Identifier: SHL-0.51 +# +# Makefragment for Verilator simulation. + +# Taken from PATH (system package, environment module, ...); override with +# `make ... Verilator=/path/to/verilator`. +Verilator ?= verilator +# Overridable per-build: `make ... Module=hwpe_ctrl_uloop_tb`. +Module ?= hwpe_ctrl_target_tb +# Per-module object dir, since a single shared obj_dir would collide across +# the different tops built from MODULES below. +ObjDirName := obj_dir_$(Module) +Vmodule := V$(Module) +VerilatorDir := $(SimDir)/$(target) +VerilatorAbsObjDir := $(VerilatorDir)/$(ObjDirName) +VerilatorCompileScript := $(VerilatorDir)/compile.$(target).tcl +VerilatorWaves := $(VerilatorDir)/$(Module).vcd +# Number of contexts instantiated in the testbench. Only +# hwpe_ctrl_target_tb_wrap exposes an NB_CONTEXT parameter; see the +# conditional -G below. +NbContext ?= 2 +# Selects which test the testbench runs; see target/sim/src for the list. +TEST ?= reg_access +# Parallelism for hw-build. With --binary this covers both verilation and the +# C++ compile of the model. +VerilatorJobs ?= 4 + +# All tops built from the shared hwpe_ctrl_test bender target (see +# Bender.yml); used by hw-build-all and hw-clean to iterate over every +# per-module object dir. +MODULES := hwpe_ctrl_target_tb hwpe_ctrl_uloop_tb hwpe_ctrl_partial_mult_tb hwpe_ctrl_seq_mult_tb + +VerilatorFlags = --trace --timing --bbox-unsup \ + -Wall -Wno-fatal --Wno-lint --Wno-UNOPTFLAT --Wno-MODDUP -Wno-BLKANDNBLK -Wno-ENUMVALUE \ + -j $(VerilatorJobs) \ + --x-assign unique --x-initial unique --top-module $(Module)_wrap --Mdir $(VerilatorAbsObjDir) \ + $(if $(filter hwpe_ctrl_target_tb,$(Module)),-GNB_CONTEXT=$(NbContext),) + +hw-clean: + rm -rf $(foreach m,$(MODULES),$(VerilatorDir)/obj_dir_$(m)) $(VerilatorCompileScript) $(VerilatorDir)/transcript* $(VerilatorDir)/*.vcd + +hw-script: regif + $(Bender) checkout + $(Bender) script $(target) \ + $(common_targs) $(common_defs) \ + $(sim_targs) \ + > $(VerilatorCompileScript) + +# Actual verilator invocation, factored out of hw-build so hw-build-all can +# reuse it per-module without re-running hw-script (regif + bender +# checkout + bender script) once per module. +hw-build-one: + OBJCACHE=ccache $(Verilator) $(VerilatorFlags) --binary -sv -cc -f $(VerilatorCompileScript) + +hw-build: hw-script hw-build-one + +# Builds every top in MODULES against a single, shared hw-script compile +# file list (bender emits one flat file list regardless of Module, so +# there is no need to regenerate it per module). Fails (non-zero exit) if +# any single module build fails, without skipping the rest -- so CI sees +# every broken top in one run instead of stopping at the first. +hw-build-all: hw-script + @status=0; \ + for m in $(MODULES); do \ + echo "==> Building $$m"; \ + $(MAKE) --no-print-directory hw-build-one target=$(target) Module=$$m NbContext=$(NbContext) VerilatorJobs=$(VerilatorJobs) || status=1; \ + done; \ + exit $$status + +# Fast syntax/elaboration check only, without running the C++/binary build. +# Useful to fail fast while sibling work packages are still landing sources. +hw-lint: hw-script + $(Verilator) $(VerilatorFlags) --lint-only -sv -cc -f $(VerilatorCompileScript) + +# Intentionally does NOT depend on hw-build: CI builds the simulation binary +# once per matrix leg and then invokes hw-run multiple times with different +# TEST= values against that same binary. +hw-run: + $(VerilatorAbsObjDir)/$(Vmodule)_wrap \ + +TEST=$(TEST) \ + $(if $(filter 1,$(gui)),,+NOTRACE) + +hw-all: hw-clean hw-build hw-run diff --git a/tb/tb_hwpe_ctrl_partial_mult.sv b/tb/tb_hwpe_ctrl_partial_mult.sv deleted file mode 100644 index 68c44b0..0000000 --- a/tb/tb_hwpe_ctrl_partial_mult.sv +++ /dev/null @@ -1,98 +0,0 @@ -/* - * tb_hwpe_ctrl_partial_mult.sv - * Francesco Conti - * - * Copyright (C) 2014-2026 ETH Zurich, University of Bologna - * Copyright and related rights are licensed under the Solderpad Hardware - * License, Version 0.51 (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * http://solderpad.org/licenses/SHL-0.51. Unless required by applicable law - * or agreed to in writing, software, hardware and materials distributed under - * this License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -timeunit 1ns; -timeprecision 1ps; - -module tb_hwpe_ctrl_partial_mult; - - localparam VERBOSE = 1; - localparam AW = 32; - localparam BW = 32; - localparam MULT_BITS = 4; - localparam NUM_TRANSACTIONS = 10000; - - logic clk_i = '0; - logic rst_ni = '1; - - logic [AW-1:0] a; - logic [BW-1:0] b; - logic [AW+BW-1:0] prod; - logic valid; - logic ready; - logic start; - logic invert; - - // ATI timing parameters. - localparam TCP = 1.0ns; // clock period, 1GHz clock - localparam TA = 0.2ns; // application time - localparam TT = 0.8ns; // test time - - // Performs one entire clock cycle. - task cycle; - clk_i <= #(TCP/2) 1'b0; - clk_i <= #TCP 1'b1; - #TCP; - endtask - - initial begin - #(20*TCP); - // Reset phase. - for (int i = 0; i < 10; i++) - cycle(); - rst_ni <= #TA 1'b0; - for (int i = 0; i < 10; i++) - cycle(); - rst_ni <= #TA 1'b1; - for (int t = 0; t < NUM_TRANSACTIONS; t++) begin - a <= #TA $random(); - b <= #TA $random(); - invert <= #TA $urandom_range(0,1); - start <= #TA 1'b1; - cycle(); - start <= #TA 1'b0; - for(int i=0; i<(AW/MULT_BITS + (AW % MULT_BITS ? 1 : 0)); i++) - cycle(); - end - $finish; - end - - hwpe_ctrl_partial_mult #( - .AW ( AW ), - .BW ( BW ), - .MULT_BITS ( MULT_BITS ) - ) ctrl_seq_mult_i ( - .clk_i ( clk_i ), - .rst_ni ( rst_ni ), - .clear_i ( 1'b0 ), - .start_i ( start ), - .a_i ( a ), - .b_i ( b ), - .invert_i ( invert ), - .valid_o ( valid ), - .ready_o ( ready ), - .prod_o ( prod ) - ); - - always_ff @(posedge clk_i) begin - if(valid & ~start & rst_ni & VERBOSE) - $display("prod %016x = %08x * %08x\n", prod, a, b); - end - - assert property (@(posedge clk_i) (valid & ~start & rst_ni) |-> - (prod == (invert ? -((AW+BW)'(a) * b) : (AW+BW)'(a) * b))) - else $fatal("Wrong multiplication data produced!!!"); - -endmodule // tb_hwpe_ctrl_partial_mult diff --git a/tb/tb_hwpe_ctrl_seq_mult.sv b/tb/tb_hwpe_ctrl_seq_mult.sv deleted file mode 100644 index 2439d66..0000000 --- a/tb/tb_hwpe_ctrl_seq_mult.sv +++ /dev/null @@ -1,81 +0,0 @@ -/* - * hwpe_ctrl_seq_mult.sv - * Francesco Conti - * - * Copyright (C) 2014-2018 ETH Zurich, University of Bologna - * Copyright and related rights are licensed under the Solderpad Hardware - * License, Version 0.51 (the "License"); you may not use this file except in - * compliance with the License. You may obtain a copy of the License at - * http://solderpad.org/licenses/SHL-0.51. Unless required by applicable law - * or agreed to in writing, software, hardware and materials distributed under - * this License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - -timeunit 1ns; -timeprecision 1ps; - -module tb_hwpe_ctrl_seq_mult; - - localparam AW = 8; - localparam BW = 8; - - logic clk_i = '0; - logic rst_ni = '1; - - logic [AW-1:0] a; - logic [BW-1:0] b; - logic [AW+BW-1:0] prod; - logic valid; - logic start; - - // ATI timing parameters. - localparam TCP = 1.0ns; // clock period, 1GHz clock - localparam TA = 0.2ns; // application time - localparam TT = 0.8ns; // test time - - // Performs one entire clock cycle. - task cycle; - clk_i <= #(TCP/2) 1'b0; - clk_i <= #TCP 1'b1; - #TCP; - endtask - - initial begin - #(20*TCP); - // Reset phase. - for (int i = 0; i < 10; i++) - cycle(); - rst_ni <= #TA 1'b0; - for (int i = 0; i < 10; i++) - cycle(); - rst_ni <= #TA 1'b1; - while (1) begin; - a <= #TA $random(); - b <= #TA $random(); - start <= #TA 1'b1; - cycle(); - start <= #TA 1'b0; - for(int i=0; i (prod == a*b)) - else $fatal("Wrong multiplication data produced!!!"); - -endmodule // tb_hwpe_ctrl_seq_mult