diff --git a/.github/actions/verify-setup/action.yml b/.github/actions/verify-setup/action.yml new file mode 100644 index 00000000..1a9d2901 --- /dev/null +++ b/.github/actions/verify-setup/action.yml @@ -0,0 +1,35 @@ +# Copyright 2026 ETH Zurich and University of Bologna. +# Licensed under the Apache License, Version 2.0, see LICENSE for details. +# SPDX-License-Identifier: Apache-2.0 + +# Authors: +# - Daniel Keller + +name: Set up the public verification toolchain +description: Bender database cache, uv, Bender and optionally Verilator +inputs: + verilator: + description: Install Verilator from apt; ubuntu-24.04 ships 5.020 + default: 'false' +runs: + using: composite + steps: + - uses: ./.github/actions/bender-db-cache + - uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + - uses: pulp-platform/pulp-actions/bender-install@v2.5.0 + with: + version: 0.32.0 + - if: inputs.verilator == 'true' + shell: bash + run: sudo apt-get update && sudo apt-get install -y verilator + - if: inputs.verilator == 'true' + shell: bash + run: | + verilator --version + verilator --version | grep -qE '^Verilator 5\.' || { + echo "expected Verilator 5.x from apt"; exit 1; } + - if: inputs.verilator == 'true' + shell: bash + run: uv run --locked make idma_verify_toolchain diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c89cd77..c7ddf13a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,8 +37,14 @@ jobs: uses: ./.github/workflows/analyze.yml secrets: inherit + verify: + needs: lint + uses: ./.github/workflows/verify.yml + secrets: inherit + + # The licensed EDA pipeline runs only once the license-free matrix is green gitlab-ci: - needs: build + needs: [build, verify] uses: ./.github/workflows/gitlab-ci.yml secrets: inherit diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4fcbf353..05abfaa1 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -32,6 +32,23 @@ jobs: exclude_paths: | target/sim/vsim/wave/tpl/*.do.tpl + # lint-sv is -diff scoped; this one checks all of src/ + lint-sv-tree: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v5 + - + name: Install Verible + uses: chipsalliance/verible-actions-common/install-verible@main + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + verible_version: "v0.0-3318-g8d254167" + - + name: Lint all of src + run: make idma_lint_sv + lint-sv: runs-on: ubuntu-latest steps: @@ -48,29 +65,6 @@ jobs: fail_on_error: true reviewdog_reporter: github-check -# lint-cxx: -# runs-on: ubuntu-latest -# steps: -# - -# name: Checkout -# uses: actions/checkout@v3 -# - -# name: Run Clang-format -# uses: DoozyX/clang-format-lint-action@v0.14 -# with: -# extensions: 'c,h,cpp' -# clangFormatVersion: 14 -# style: > -# { -# IndentWidth: 4, -# ColumnLimit: 100, -# AlignEscapedNewlines: DontAlign, -# SortIncludes: false, -# AllowShortFunctionsOnASingleLine: None, -# AllowShortIfStatementsOnASingleLine: true, -# AllowShortLoopsOnASingleLine: true -# } - lint-python: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml new file mode 100644 index 00000000..72e3fb7d --- /dev/null +++ b/.github/workflows/verify.yml @@ -0,0 +1,157 @@ +# Copyright 2026 ETH Zurich and University of Bologna. +# Licensed under the Apache License, Version 2.0, see LICENSE for details. +# SPDX-License-Identifier: Apache-2.0 + +# Author: +# - Daniel Keller + +# License-free verification matrix; the run set is src/db/verify.yml + +name: verify + +on: + workflow_call: + workflow_dispatch: + +jobs: + + # jobs.json and the generated tree must still describe the same design + codegen-consistency: + runs-on: ubuntu-24.04 + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Set up toolchain + uses: ./.github/actions/verify-setup + - + name: Check jobs.json, the CI matrix and codegen determinism + run: uv run --locked make idma_verify_codegen + + # One backend variant: its jobs.json parameters, a width sweep, its testbench + elab-backend: + strategy: + fail-fast: false + matrix: + id: + - rw_axi + - r_obi_w_axi + - r_axi_w_obi + - rw_axi_rw_axis + - rw_obi + - r_obi_rw_init_w_axi + - r_axi_rw_init_rw_obi + - rw_axi_rw_init_rw_obi + runs-on: ubuntu-24.04 + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Set up toolchain + uses: ./.github/actions/verify-setup + with: + verilator: 'true' + - + name: Generate RTL + run: uv run --locked make idma_hw_all + - + name: Elaborate ${{ matrix.id }} + run: uv run --locked make idma_verify_backend IDMA_VERIFY_ID=${{ matrix.id }} + + # The non-backend synthesis tops, plus the inst64 testbench + elab-shared-tops: + runs-on: ubuntu-24.04 + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Set up toolchain + uses: ./.github/actions/verify-setup + with: + verilator: 'true' + - + name: Generate RTL + run: uv run --locked make idma_hw_all + - + name: Elaborate the shared synthesis tops + run: uv run --locked make idma_verify_shared + + # Testbench tops; verilator cannot parse the verification stack, so slang only + elab-tb-shared: + runs-on: ubuntu-24.04 + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Set up toolchain + uses: ./.github/actions/verify-setup + - + name: Generate RTL + run: uv run --locked make idma_hw_all + - + name: Elaborate the shared testbenches + run: uv run --locked make idma_verify_tb_shared + + # Out-of-tree multi-head build; license-free but generated only on request + elab-multihead: + runs-on: ubuntu-24.04 + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Set up toolchain + uses: ./.github/actions/verify-setup + with: + verilator: 'true' + - + name: Elaborate the multi-head variants and testbenches + run: uv run --locked make idma_verify_multihead + + # The suite list comes from the database, so a new suite needs no edit here + sim-matrix: + needs: [elab-backend, elab-shared-tops, elab-tb-shared] + runs-on: ubuntu-24.04 + outputs: + suites: ${{ steps.legs.outputs.suites }} + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Set up toolchain + uses: ./.github/actions/verify-setup + - + name: Read the suites from the verification database + id: legs + run: | + suites=$(uv run --locked python util/run_verify.py --emit-matrix suites) + echo "suites=$suites" >> "$GITHUB_OUTPUT" + + # Real simulation; run_verify.py knows which suites are negative tests + simulate: + needs: sim-matrix + if: needs.sim-matrix.outputs.suites != '' + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.sim-matrix.outputs.suites) }} + runs-on: ubuntu-24.04 + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Set up toolchain + uses: ./.github/actions/verify-setup + with: + verilator: 'true' + - + name: Generate RTL + run: uv run --locked make idma_hw_all + - + name: Simulate ${{ matrix.suite }} + run: uv run --locked make idma_verify_sim_${{ matrix.suite }} IDMA_VLT_MAKEFLAGS=-j4 diff --git a/Bender.yml b/Bender.yml index f779f9b0..b2b62ee9 100644 --- a/Bender.yml +++ b/Bender.yml @@ -136,6 +136,7 @@ sources: - target: idma_test files: - target/rtl/tb_idma_generated.sv + - test/tb_idma_otf_transpose.sv - test/tb_idma_transpose_nd.sv - test/tb_idma_transpose_b2b.sv - test/tb_idma_mxquant.sv diff --git a/CHANGELOG.md b/CHANGELOG.md index 799e6f78..587dd236 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed +- **Breaking**: the tracer is generated one header per backend id. `idma/tracer.svh` now only holds the id-independent helpers; the `IDMA_TRACER_` macro of a variant lives in `idma/tracer_.svh`, which includes `idma/tracer.svh` itself. Downstream users replace `` `include "idma/tracer.svh" `` with the per-id header of the variant they trace. + + ## 0.6.5 - 2025-07-15 ### Added diff --git a/doc/site/src/content/docs/architecture/backend.md b/doc/site/src/content/docs/architecture/backend.md index ec9688cb..2c706205 100644 --- a/doc/site/src/content/docs/architecture/backend.md +++ b/doc/site/src/content/docs/architecture/backend.md @@ -90,7 +90,9 @@ Each backend variant combines a set of read and write protocols: | `r_axi_rw_init_rw_obi` | AXI4 | INIT + OBI | | `rw_axi_rw_init_rw_obi` | AXI4 | AXI4 + INIT + OBI | -The active set is `IDMA_BACKEND_IDS` in `idma.mk`; extend it with `IDMA_ADD_IDS` for custom protocol combinations. +The tracked set is `IDMA_TREE_IDS` in `idma.mk`; extend it with `IDMA_ADD_IDS` for custom protocol combinations. Add-id variants are generated by `make idma_hw_all idma_add_all IDMA_ADD_IDS="..."` into `target/rtl/idma_generated_add.sv` and reach Bender through the `add_ids` target. + +Each variant also gets its own tracer header, `idma/tracer_.svh`, holding the `IDMA_TRACER_` macro; `idma/tracer.svh` carries only the id-independent helpers and is included by the per-id headers. ![Variant Matrix](/iDMA/fig/variant_matrix.svg) diff --git a/idma.mk b/idma.mk index 887c76f8..019ecb02 100644 --- a/idma.mk +++ b/idma.mk @@ -7,6 +7,7 @@ BENDER ?= bender CAT ?= cat +CC ?= cc GIT ?= git PRINTF ?= printf UV ?= uv @@ -133,12 +134,21 @@ $(IDMA_RTL_DIR)/tb_idma_backend_%.sv: $(IDMA_GEN) $(IDMA_GEN_SRC) $(IDMA_RTL_DIR $(IDMA_VSIM_DIR)/wave/backend_%.do: $(IDMA_GEN) $(IDMA_GEN_SRC) $(IDMA_RTL_DIR)/tb_idma_backend_%.sv $(IDMA_VSIM_DIR)/wave/tpl/backend.do.tpl $(call idma_gen,vsim_wave,$(IDMA_VSIM_DIR)/wave/tpl/backend.do.tpl,$(IDMA_DB_FILES),$*,,$@) -$(IDMA_RTL_DIR)/include/idma/tracer.svh: $(IDMA_GEN) $(IDMA_GEN_SRC) $(IDMA_ROOT)/src/include/idma/tpl/tracer.svh.tpl $(IDMA_DB_FILES) $(IDMA_ROOT)/idma.mk $(IDMA_DB_FILES) - mkdir -p $(IDMA_RTL_DIR)/include/idma - $(call idma_gen,tracer,$(IDMA_ROOT)/src/include/idma/tpl/tracer.svh.tpl,$(IDMA_DB_FILES),$(IDMA_BACKEND_IDS),$(IDMA_FE_IDS),$@) +IDMA_INC_DIR := $(IDMA_RTL_DIR)/include/idma +IDMA_INC_TPL := $(IDMA_ROOT)/src/include/idma/tpl -$(IDMA_RTL_DIR)/include/idma/compute.svh: $(IDMA_ROOT)/src/frontend/reg/tpl/compute.svh.tpl $(IDMA_ROOT)/src/frontend/reg/idma_reg.rdl - mkdir -p $(IDMA_RTL_DIR)/include/idma +# The id-independent tracer helpers; a pure function of their own template +$(IDMA_INC_DIR)/tracer.svh: $(IDMA_GEN) $(IDMA_GEN_SRC) $(IDMA_INC_TPL)/tracer.svh.tpl + mkdir -p $(@D) + $(call idma_gen,tracer_common,$(IDMA_INC_TPL)/tracer.svh.tpl,,,,$@) + +# One tracer macro per backend id, a function of the id in the target name +$(IDMA_INC_DIR)/tracer_%.svh: $(IDMA_GEN) $(IDMA_GEN_SRC) $(IDMA_INC_TPL)/tracer_id.svh.tpl $(IDMA_DB_FILES) + mkdir -p $(@D) + $(call idma_gen,tracer,$(IDMA_INC_TPL)/tracer_id.svh.tpl,$(IDMA_DB_FILES),$*,,$@) + +$(IDMA_INC_DIR)/compute.svh: $(IDMA_ROOT)/src/frontend/reg/tpl/compute.svh.tpl $(IDMA_ROOT)/src/frontend/reg/idma_reg.rdl + mkdir -p $(IDMA_INC_DIR) $(PEAKRDL) raw-header $(IDMA_ROOT)/src/frontend/reg/idma_reg.rdl \ --template $(IDMA_ROOT)/src/frontend/reg/tpl/compute.svh.tpl -o $@ @@ -146,16 +156,16 @@ idma_rtl_clean: rm -f $(IDMA_RTL_DIR)/Bender.yml rm -f $(IDMA_RTL_DIR)/*.sv rm -f $(IDMA_VSIM_DIR)/wave/*.do - rm -f $(IDMA_RTL_DIR)/include/idma/tracer.svh - rm -f $(IDMA_RTL_DIR)/include/idma/compute.svh - rm -rf $(IDMA_RTL_DIR)/include/idma + rm -rf $(IDMA_INC_DIR) # assemble the required files -IDMA_INCLUDE_ALL += $(IDMA_RTL_DIR)/include/idma/tracer.svh -IDMA_INCLUDE_ALL += $(IDMA_RTL_DIR)/include/idma/compute.svh +IDMA_INCLUDE_ALL += $(IDMA_INC_DIR)/tracer.svh +IDMA_INCLUDE_ALL += $(foreach Y,$(IDMA_BACKEND_IDS),$(IDMA_INC_DIR)/tracer_$Y.svh) +IDMA_INCLUDE_ALL += $(IDMA_INC_DIR)/compute.svh + IDMA_RTL_ALL += $(foreach X,$(IDMA_RTL_FILES),$(foreach Y,$(IDMA_BACKEND_IDS),$X_$Y.sv)) IDMA_TB_ALL += $(foreach Y,$(IDMA_BACKEND_IDS),$(IDMA_RTL_DIR)/tb_idma_backend_$Y.sv) -IDMA_WAVE_ALL += $(foreach Y,$(IDMA_BACKEND_IDS),$(IDMA_VSIM_DIR)/wave/backend_$Y.do) +IDMA_WAVE_ALL += $(foreach Y,$(IDMA_BACKEND_IDS),$(IDMA_VSIM_DIR)/wave/backend_$Y.do) # -------------- @@ -265,14 +275,13 @@ idma_reg_clean: rm -f $(IDMA_RTL_DIR)/*_reg_top.sv rm -f $(IDMA_RTL_DIR)/*_reg_pkg.sv rm -f $(IDMA_RTL_DIR)/Bender.yml - rm -f $(IDMA_REG_CUST_ALL) # assemble the required files IDMA_RTL_ALL += $(foreach Y,$(IDMA_FE_REGS),$(IDMA_RTL_DIR)/idma_$Y_reg_pkg.sv) IDMA_RTL_ALL += $(foreach Y,$(IDMA_FE_REGS),$(IDMA_RTL_DIR)/idma_$Y_reg_top.sv) IDMA_RTL_ALL += $(foreach Y,$(IDMA_FE_REGS),$(IDMA_RTL_DIR)/idma_$Y_addrmap_pkg.sv) IDMA_RTL_ALL += $(foreach Y,$(IDMA_FE_REGS),$(IDMA_RTL_DIR)/idma_$Y_top.sv) -IDMA_RTL_DOC_ALL += $(foreach Y,$(IDMA_FE_REGS),$(IDMA_HTML_DIR)/regs/idma_$Y_reg/index.html) +IDMA_RTL_DOC_ALL += $(foreach Y,$(IDMA_FE_REGS),$(IDMA_HTML_DIR)/regs/idma_$Y_reg/index.html) # C headers IDMA_SW_ALL += $(foreach Y,$(IDMA_FE_REGS),$(IDMA_SW_DIR)/idma_$Y_regs.h) @@ -285,11 +294,12 @@ IDMA_SW_ALL += $(foreach Y,$(IDMA_FE_REGS),$(IDMA_SW_DIR)/idma_$Y_raw_regs. # RTL assembly # --------------- + $(IDMA_FULL_RTL): $(IDMA_RTL_ALL) - $(CAT) $^ > $@ + $(CAT) $(IDMA_RTL_ALL) > $@ $(IDMA_FULL_TB): $(IDMA_TB_ALL) - $(CAT) $^ > $@ + $(CAT) $(IDMA_TB_ALL) > $@ # --------------- @@ -582,29 +592,59 @@ idma_vcs_clean: .PHONY: idma_verilator_clean IDMA_VLT_DIR := $(IDMA_ROOT)/target/sim/verilator -IDMA_VLT_ARGS := --cc \ - --Wall \ - --Wno-fatal \ - +1800-2017ext+ \ - --assert \ - --error-limit 1000 \ - --hierarchical \ - --no-skip-identical -IDMA_VLT_TOP ?= -IDMA_VLT_PARAMS ?= -.PRECIOUS: $(IDMA_VLT_DIR)/%_elab.log +# Measured at 0 occurrences over the synth tops, so they gate +IDMA_VLT_WERROR := -Werror-LATCH -Werror-MULTIDRIVEN -Werror-IMPLICIT +# Unroll budget matches util/run_vlt_sim.py; 5.020 reports BLKLOOPINIT without it +IDMA_VLT_LINT_ARGS := --lint-only -Wno-fatal --timing $(IDMA_VLT_WERROR) \ + --unroll-count 4096 --unroll-stmts 200000 -$(IDMA_VLT_DIR)/%_elab.log: $(IDMA_BENDER_FILES) $(IDMA_FULL_TB) $(IDMA_FULL_RTL) $(IDMA_INCLUDE_ALL) - mkdir -p $(IDMA_VLT_DIR) - # We need a dedicated pickle here to set the defines - $(BENDER) pickle $(IDMA_PICKLE_TARGETS) --top $(IDMA_VLT_TOP) -D VERILATOR --expand-macros -o $(IDMA_VLT_DIR)/$(IDMA_VLT_TOP).sv - cd $(IDMA_VLT_DIR); $(VERILATOR) $(IDMA_VLT_ARGS) $(IDMA_VLT_PARAMS) -Mdir obj_$* $(IDMA_VLT_TOP).sv --top-module $(IDMA_VLT_TOP) 2> $*_elab.log idma_verilator_clean: rm -rf $(IDMA_VLT_DIR) +# inst64 gate: the only public concrete binding of idma_inst64_top +IDMA_INST64_TB := tb_idma_inst64_axi_copy +IDMA_INST64_T := -t rtl -t synth -t idma_test -t simulation -t sim -t test \ + -t snitch_cluster + +.PHONY: idma_lint_inst64 +idma_lint_inst64: + mkdir -p $(IDMA_VLT_DIR) + $(BENDER) script verilator $(IDMA_INST64_T) --top $(IDMA_INST64_TB) \ + > $(IDMA_VLT_DIR)/idma_inst64_tb.f + $(VERILATOR) $(IDMA_VLT_LINT_ARGS) -f $(IDMA_VLT_DIR)/idma_inst64_tb.f \ + --top-module $(IDMA_INST64_TB) + +# verilator elaborates every synth top, so fork PRs catch port and param breaks +IDMA_LINT_TOPS ?= $(addprefix idma_backend_synth_,$(IDMA_BACKEND_IDS)) \ + idma_desc64_synth \ + idma_nd_midend_synth \ + idma_mp_midend_synth \ + idma_rt_midend_synth + +# lint-sv is -diff scoped; this checks all of src/ +VERIBLE ?= verible-verilog-lint + +.PHONY: idma_lint_sv +idma_lint_sv: + $(VERIBLE) --waiver_files $(IDMA_ROOT)/.github/verible.waiver \ + $$(find $(IDMA_ROOT)/src -name '*.sv' -o -name '*.svh' | sort) + +.PHONY: idma_lint_elab +idma_lint_elab: + mkdir -p $(IDMA_VLT_DIR) + $(BENDER) script verilator -t rtl -t synth > $(IDMA_VLT_DIR)/idma_elab.f + @rc=0; for top in $(IDMA_LINT_TOPS); do \ + echo "--- elaborating $$top ---"; \ + $(VERILATOR) $(IDMA_VLT_LINT_ARGS) -f $(IDMA_VLT_DIR)/idma_elab.f \ + --top-module $$top || rc=1; \ + done; exit $$rc + +.PHONY: idma_lint_all +idma_lint_all: idma_lint_elab idma_lint_inst64 + # --------------- # Trace @@ -655,6 +695,9 @@ idma_nonfree_init: git clone $(IDMA_NONFREE_REMOTE) $(IDMA_NONFREE_DIR) cd $(IDMA_NONFREE_DIR) && git checkout $(IDMA_NONFREE_COMMIT) +# The public verification gates; see verify.mk +include $(IDMA_ROOT)/verify.mk + -include $(IDMA_NONFREE_DIR)/nonfree.mk idma_nonfree_clean: @@ -667,7 +710,7 @@ idma_nonfree_clean: .PHONY: idma_clean_all idma_clean idma_misc_clean idma_sw_clean -idma_clean_all idma_clean: idma_rtl_clean idma_reg_clean idma_pickle_clean idma_sim_clean idma_vcs_clean idma_verilator_clean idma_doc_clean idma_trace_clean idma_sw_clean +idma_clean_all idma_clean: idma_rtl_clean idma_reg_clean idma_pickle_clean idma_sim_clean idma_vcs_clean idma_verilator_clean idma_verify_clean idma_doc_clean idma_trace_clean idma_sw_clean idma_misc_clean: rm -rf scripts/__pycache__ @@ -686,7 +729,8 @@ idma_sw_clean: # Phony Targets # -------------- -.PHONY: idma_all idma_doc_all idma_pickle_all idma_rtl_all idma_sim_all +.PHONY: idma_all idma_doc_all idma_pickle_all idma_sim_all +.PHONY: idma_hw_all idma_sw_all idma_nuke # Build the Starlight/Astro site (output in doc/site/dist) after staging the graphs idma_doc_all: idma_doc_site @@ -694,7 +738,8 @@ idma_doc_all: idma_doc_site idma_pickle_all: $(IDMA_PICKLE_ALL) -idma_hw_all: $(IDMA_FULL_RTL) $(IDMA_INCLUDE_ALL) $(IDMA_FULL_TB) $(IDMA_HJSON_ALL) $(IDMA_WAVE_ALL) +idma_hw_all: $(IDMA_FULL_RTL) $(IDMA_INCLUDE_ALL) $(IDMA_FULL_TB) \ + $(IDMA_WAVE_ALL) idma_sw_all: $(IDMA_SW_ALL) diff --git a/jobs/jobs.json b/jobs/jobs.json index 63ada665..5caf7d4d 100644 --- a/jobs/jobs.json +++ b/jobs/jobs.json @@ -332,5 +332,167 @@ "proc_id" : "none", "testbench" : "idma_mp_midend_synth", "synth_top" : "idma_mp_midend_synth" + }, + "mxquant" : { + "jobs" : { + }, + "params" : { + "AddrWidth" : 32, + "UserWidth" : 1, + "AxiIdWidth" : 12, + "TFLenWidth" : 32 + }, + "testbench" : "tb_idma_mxquant", + "verify" : { + "dpi" : "idma_mxquant_dpi", + "token" : "[MXQ] ALL PASS", + "legs" : [ + { "tag" : "mxquant_32", "params" : { "DataWidth" : 32 } }, + { "tag" : "mxquant_64", "params" : { "DataWidth" : 64 } }, + { "tag" : "mxquant_256", "params" : { "DataWidth" : 256 } }, + { "tag" : "mxquant_512", "params" : { "DataWidth" : 512 } }, + { "tag" : "mxquant_1024", "params" : { "DataWidth" : 1024 } } + ] + } + }, + "mxroundtrip" : { + "jobs" : { + }, + "params" : { + "AddrWidth" : 32, + "UserWidth" : 1, + "AxiIdWidth" : 12, + "TFLenWidth" : 32 + }, + "testbench" : "tb_idma_mxroundtrip", + "verify" : { + "dpi" : "idma_mxquant_dpi", + "token" : "[MXRT] ALL PASS", + "legs" : [ + { "tag" : "mxroundtrip_32", "params" : { "DataWidth" : 32 } }, + { "tag" : "mxroundtrip_64", "params" : { "DataWidth" : 64 } }, + { "tag" : "mxroundtrip_256", "params" : { "DataWidth" : 256 } } + ], + "exclude" : [ + { "values" : [512, 1024], "why" : "heap abort during model construction, see #196" } + ] + } + }, + "transpose" : { + "jobs" : { + }, + "params" : { + }, + "testbench" : "tb_idma_otf_transpose", + "verify" : { + "dpi" : "idma_transpose_dpi", + "token" : "[TB] ALL PASS", + "legs" : [ + { "tag" : "otf_transpose", "params" : { "StrbWidth" : 8, "FullDuplex" : 1 } }, + { "tag" : "otf_transpose_bp", "params" : { "StrbWidth" : 8, "FullDuplex" : 1 }, "plusargs" : ["+BP"] }, + { "tag" : "otf_transpose_8_0", "params" : { "StrbWidth" : 8, "FullDuplex" : 0 }, "plusargs" : ["+BP"] }, + { "tag" : "otf_transpose_64_1", "params" : { "StrbWidth" : 64, "FullDuplex" : 1 }, "plusargs" : ["+BP"] }, + { "tag" : "otf_transpose_64_0", "params" : { "StrbWidth" : 64, "FullDuplex" : 0 }, "plusargs" : ["+BP"] } + ] + } + }, + "transpose_midend" : { + "jobs" : { + }, + "params" : { + "AddrWidth" : 64 + }, + "testbench" : "tb_idma_transpose_midend", + "verify" : { + "dpi" : "idma_transpose_dpi", + "token" : "[MID] ALL PASS", + "legs" : [ + { "tag" : "transpose_midend_64", "params" : { "DataWidth" : 64 } }, + { "tag" : "transpose_midend_512", "params" : { "DataWidth" : 512 } } + ] + } + }, + "mxclear" : { + "jobs" : { + }, + "params" : { + "StrbWidth" : 8 + }, + "testbench" : "tb_idma_mxclear", + "verify" : { + "expect" : "fail", + "defines" : ["INC_ASSERT"], + "token" : "clear with in-flight state", + "legs" : [ + { "tag" : "mxclear_1", "params" : { "Quant" : 1 } }, + { "tag" : "mxclear_0", "params" : { "Quant" : 0 } } + ] + } + }, + "mxneg" : { + "jobs" : { + }, + "params" : { + "AddrWidth" : 32, + "UserWidth" : 1, + "AxiIdWidth" : 12, + "TFLenWidth" : 32 + }, + "testbench" : "tb_idma_mxneg", + "verify" : { + "dpi" : "idma_mxquant_dpi", + "expect" : "fail", + "defines" : ["INC_ASSERT"], + "legs" : [ + { "tag" : "mxneg_1", "params" : { "NegCase" : 1, "DataWidth" : 64, "EnDequant" : 1, "EnFp16" : 1 }, "token" : "ComputeSizeAligned" }, + { "tag" : "mxneg_2", "params" : { "NegCase" : 2, "DataWidth" : 64, "EnDequant" : 1, "EnFp16" : 1 }, "token" : "ComputeSrcAligned" }, + { "tag" : "mxneg_3", "params" : { "NegCase" : 3, "DataWidth" : 64, "EnDequant" : 1, "EnFp16" : 1 }, "token" : "ComputeDstAligned" }, + { "tag" : "mxneg_5", "params" : { "NegCase" : 5, "DataWidth" : 64, "EnDequant" : 1, "EnFp16" : 1 }, "token" : "ComputeMxdequantBeatAligned" }, + { "tag" : "mxneg_6", "params" : { "NegCase" : 6, "DataWidth" : 64, "EnDequant" : 0, "EnFp16" : 1 }, "token" : "ComputeOpUnsupported" }, + { "tag" : "mxneg_7", "params" : { "NegCase" : 7, "DataWidth" : 64, "EnDequant" : 1, "EnFp16" : 1 }, "token" : "ComputeMxSrcProtocol" }, + { "tag" : "mxneg_8", "params" : { "NegCase" : 8, "DataWidth" : 64, "EnDequant" : 1, "EnFp16" : 1 }, "token" : "ComputeMxDstProtocol" }, + { "tag" : "mxneg_10", "params" : { "NegCase" : 10, "DataWidth" : 64, "EnDequant" : 1, "EnFp16" : 1 }, "token" : "ComputeTransposeSingleBeat" }, + { "tag" : "mxneg_13", "params" : { "NegCase" : 13, "DataWidth" : 64, "EnDequant" : 1, "EnFp16" : 0 }, "token" : "ComputeOpUnsupported" } + ], + "exclude" : [ + { "case" : 4, "why" : "DataWidth 1024 SIGSEGVs on verilator 5.020; passes on 5.046" }, + { "case" : 11, "why" : "the request is never accepted, so the guard is never sampled" }, + { "case" : 12, "why" : "DataWidth 1024 SIGSEGVs on verilator 5.020; passes on 5.046" } + ] + } + }, + "_verify" : { + "elab_widths" : [32, 64, 512, 1024], + "elab_compute" : [ + { "width" : 64, "ops" : 15, "tuning" : 1 }, + { "width" : 512, "ops" : 15, "tuning" : 1 }, + { "width" : 64, "ops" : 8, "tuning" : 0 }, + { "width" : 64, "ops" : 6, "tuning" : 1 } + ], + "elab_shared_tops" : [ + "idma_desc64_synth", "idma_nd_midend_synth", + "idma_mp_midend_synth", "idma_rt_midend_synth" + ], + "multihead_ids" : ["2r_axi_w_axi", "2rw_axi"], + "reg_variants" : [ + { "variant" : 3, "module" : "idma_reg32_3d" }, + { "variant" : 2, "module" : "idma_reg64_2d" }, + { "variant" : 1, "module" : "idma_reg64_1d" } + ], + "elab_covered_elsewhere" : ["tb_idma_reg_frontend"], + "guards_untested" : [ + { "guard" : "ComputeMxFp16Width", "why" : "only reachable from the excluded 1024 cases" }, + { "guard" : "ComputeMxdequantLengthFits", "why" : "case 11 is never accepted" }, + { "guard" : "ComputeDstTilelink", "why" : "no TileLink backend variant exists" } + ], + "tops_untested" : [ + { "top" : "tb_idma_nd_midend_b2b", "why" : "hangs ([B2B] timeout); latent reset/LFSR race" }, + { "top" : "tb_idma_transpose_nd", "why" : "AW outside the destination allocation at 80 ns" }, + { "top" : "tb_idma_transpose_b2b", "why" : "stream_watchdog trip after 4000 idle cycles" }, + { "top" : "tb_idma_mxrand", "why" : "testbench timeout at 400 ms of simulated time" }, + { "top" : "tb_idma_mxperf", "why" : "hangs on the first compute transfer" }, + { "top" : "tb_idma_rt_midend", "why" : "fails under --assert on cc_rr_arb_tree lock" }, + { "top" : "the 88 directed job files", "why" : "verilator cannot drive the class-based stimulus" } + ] } } diff --git a/src/backend/tpl/idma_legalizer.sv.tpl b/src/backend/tpl/idma_legalizer.sv.tpl index 8329ba88..4806a320 100644 --- a/src/backend/tpl/idma_legalizer.sv.tpl +++ b/src/backend/tpl/idma_legalizer.sv.tpl @@ -732,7 +732,7 @@ ${database[protocol]['legalizer_write_data_path']} ($bits(req_i.length) < 64) & (((64'(req_i.length) / 64'(idma_pkg::MxBlockBytes)) * 64'(idma_pkg::compute_out_bytes(req_i.opt.compute.op))) >= - (64'd1 << $bits(req_i.length)))), clk_i, !rst_ni) + (65'd1 << $bits(req_i.length)))), clk_i, !rst_ni) // compute retires on the per-beat write pulse; TileLink writes retire per burst `ASSERT_NEVER(ComputeDstTilelink, (ready_o & valid_i & req_i.opt.compute.enable & (req_i.opt.dst_protocol == idma_pkg::TILELINK)), clk_i, !rst_ni) diff --git a/src/frontend/inst64/idma_inst64_top.sv b/src/frontend/inst64/idma_inst64_top.sv index 802961c3..0ce1b005 100644 --- a/src/frontend/inst64/idma_inst64_top.sv +++ b/src/frontend/inst64/idma_inst64_top.sv @@ -8,7 +8,7 @@ `include "common_cells/registers.svh" `include "common_cells/assertions.svh" `include "idma/typedef.svh" -`include "idma/tracer.svh" +`include "idma/tracer_rw_axi_rw_init_rw_obi.svh" /// Implements the tightly-coupled frontend. This module can directly be connected /// to an accelerator bus in the snitch system @@ -72,7 +72,8 @@ module idma_inst64_top #( localparam int unsigned NumDim = 32'd2; localparam int unsigned BufferDepth = 32'd3; localparam int unsigned NumRules = 32'd5; - localparam int unsigned AwInFlightCntWidth = (NumAxInFlight < 2) ? 32'd1 : $clog2(NumAxInFlight + 1); + localparam int unsigned AwInFlightCntWidth = + (NumAxInFlight < 2) ? 32'd1 : $clog2(NumAxInFlight + 1); // derived constants and types localparam int unsigned StrbWidth = AxiDataWidth / 32'd8; @@ -797,7 +798,8 @@ module idma_inst64_top #( $sformat(trace_file, "dma_trace_%05x_%05x.log", hart_id_i, c); end // attach the tracer - `IDMA_TRACER_RW_AXI(gen_backend[c].i_idma_backend_rw_axi_rw_init_rw_obi, trace_file); + `IDMA_TRACER_RW_AXI_RW_INIT_RW_OBI( + gen_backend[c].i_idma_backend_rw_axi_rw_init_rw_obi, trace_file); end end `endif diff --git a/src/include/idma/tpl/tracer.svh.tpl b/src/include/idma/tpl/tracer.svh.tpl index 604b20c4..2ee37135 100644 --- a/src/include/idma/tpl/tracer.svh.tpl +++ b/src/include/idma/tpl/tracer.svh.tpl @@ -5,7 +5,8 @@ // Authors: // - Thomas Benz -// Macro holding all the resources for the iDMA backend tracer +// Shared resources of the iDMA backend tracer; the per-id macros live in +// idma/tracer_.svh and include this file themselves `ifndef IDMA_TRACER_SVH_ `define IDMA_TRACER_SVH_ @@ -26,5 +27,4 @@ if(__cond) begin <%text>\ __cond = ~__cond; <%text>\ end -${body} `endif diff --git a/src/include/idma/tpl/tracer_id.svh.tpl b/src/include/idma/tpl/tracer_id.svh.tpl new file mode 100644 index 00000000..d4aa673c --- /dev/null +++ b/src/include/idma/tpl/tracer_id.svh.tpl @@ -0,0 +1,15 @@ +// Copyright 2026 ETH Zurich and University of Bologna. +// Solderpad Hardware License, Version 0.51, see LICENSE for details. +// SPDX-License-Identifier: SHL-0.51 + +// Authors: +// - Thomas Benz +// - Daniel Keller + +// Tracer macro of the ${identifier} iDMA backend +`ifndef IDMA_TRACER_${identifier_cap}_SVH_ +`define IDMA_TRACER_${identifier_cap}_SVH_ + +`include "idma/tracer.svh" +${body} +`endif diff --git a/target/.gitignore b/target/.gitignore index ac92a5f6..0f21957a 100644 --- a/target/.gitignore +++ b/target/.gitignore @@ -1,3 +1,5 @@ doc pickle sim/verilator +sim/slang +verify diff --git a/test/frontend/idma_inst64_drv_if.sv b/test/frontend/idma_inst64_drv_if.sv index b403569a..3a645cf0 100644 --- a/test/frontend/idma_inst64_drv_if.sv +++ b/test/frontend/idma_inst64_drv_if.sv @@ -69,8 +69,11 @@ interface idma_inst64_drv_if #( acc_rsp_item_t rsp_queue [$]; always_ff @(posedge clk) begin : proc_capture_rsp + // built in a variable first: verilator rejects an assignment pattern as an argument + automatic acc_rsp_item_t rsp_item; if (rst_n && acc_res_valid && acc_res_ready) begin - rsp_queue.push_back('{id: acc_res.id, data: acc_res.data, error: acc_res.error}); + rsp_item = '{id: acc_res.id, data: acc_res.data, error: acc_res.error}; + rsp_queue.push_back(rsp_item); end end diff --git a/test/frontend/tb_idma_desc64_bench.sv b/test/frontend/tb_idma_desc64_bench.sv index 2df3b601..442bc4d3 100644 --- a/test/frontend/tb_idma_desc64_bench.sv +++ b/test/frontend/tb_idma_desc64_bench.sv @@ -7,7 +7,7 @@ `include "apb/typedef.svh" `include "apb/assign.svh" -`include "idma/tracer.svh" +`include "idma/tracer_rw_axi.svh" `include "idma/typedef.svh" `include "axi/typedef.svh" `include "axi/assign.svh" diff --git a/test/frontend/tb_idma_reg_frontend.sv b/test/frontend/tb_idma_reg_frontend.sv index 7132d1b3..0da1ff32 100644 --- a/test/frontend/tb_idma_reg_frontend.sv +++ b/test/frontend/tb_idma_reg_frontend.sv @@ -5,18 +5,16 @@ // Authors: // - Daniel Keller -// Self-checking testbench for the iDMA register frontend (idma_reg32_3d, apb4-flat). -// Drives the APB config slave with the standard apb_test::apb_driver against a -// controllable backend stub and checks the non-blocking next_id launch contract: -// the config read completes promptly (even under backend backpressure) and the -// launch fires exactly once when the arbiter grants. A per-read watchdog guards -// against any read that hangs. +// Self-checking testbench for the iDMA register frontend; checks the non-blocking +// next_id launch contract. RegVariant 2 and 1 are elaboration-only. `include "apb/typedef.svh" `include "apb/assign.svh" `include "idma/typedef.svh" module tb_idma_reg_frontend import idma_pkg::*; import apb_test::apb_driver; #( + // generated frontend under test, by ND dimensions: 3 = reg32_3d, 2 = reg64_2d, 1 = reg64_1d + parameter int unsigned RegVariant = 32'd3, // number of streams the elaborated DUT exposes (checked at instantiation) parameter int unsigned NumStreams = 32'd1, // number of config-bus ports (arbitrated by the reg frontend's rr_arb_tree) @@ -33,11 +31,11 @@ module tb_idma_reg_frontend import idma_pkg::*; import apb_test::apb_driver; #( localparam int unsigned CfgDataWidth = 32'd32; localparam int unsigned CfgStrbWidth = CfgDataWidth / 32'd8; localparam int unsigned IdCounterWidth = 32'd32; - // idma data-path (reg32_3d: 32-bit data, 3 ND dims) - localparam int unsigned AddrWidth = 32'd32; - localparam int unsigned DataWidth = 32'd32; - localparam int unsigned NumDim = 32'd3; - localparam int unsigned RepWidth = 32'd32; + // idma data-path: reg32_3d is 32-bit over 3 ND dims, both reg64 variants are 64-bit + localparam int unsigned AddrWidth = (RegVariant == 32'd3) ? 32'd32 : 32'd64; + localparam int unsigned DataWidth = AddrWidth; + localparam int unsigned NumDim = (RegVariant == 32'd3) ? 32'd3 : 32'd2; + localparam int unsigned RepWidth = AddrWidth; // apb_driver framing: the blocking driver.read() spans SETUP + first-ACCESS-check + // trailing edge before returning, so a same-cycle (non-blocking) read takes this many // config clocks end-to-end; each extra ACCESS wait state adds one more clock. @@ -179,27 +177,84 @@ module tb_idma_reg_frontend import idma_pkg::*; import apb_test::apb_driver; #( // -------------------------------------------------------------------------- // DUT // -------------------------------------------------------------------------- - idma_reg32_3d #( - .NumRegs ( NumRegs ), - .NumStreams ( NumStreams ), - .IdCounterWidth ( IdCounterWidth ), - .apb_req_t ( cfg_apb_req_t ), - .apb_rsp_t ( cfg_apb_rsp_t ), - .dma_req_t ( idma_nd_req_t ) - ) i_dut ( - .clk_i ( clk ), - .rst_ni ( rst_n ), - .dma_ctrl_req_i ( apb_req ), - .dma_ctrl_rsp_o ( apb_rsp ), - .dma_req_o ( dma_req ), - .req_valid_o ( req_valid ), - .req_ready_i ( req_ready ), - .next_id_i ( next_id ), - .stream_idx_o ( stream_idx ), - .done_id_i ( done_id ), - .busy_i ( busy ), - .midend_busy_i ( midend_busy ) - ); + // All three share the parameter and port list; reg64_1d emits a flat idma_req_t + if (RegVariant == 32'd3) begin : gen_reg32_3d + idma_reg32_3d #( + .NumRegs ( NumRegs ), + .NumStreams ( NumStreams ), + .IdCounterWidth ( IdCounterWidth ), + .apb_req_t ( cfg_apb_req_t ), + .apb_rsp_t ( cfg_apb_rsp_t ), + .dma_req_t ( idma_nd_req_t ) + ) i_dut ( + .clk_i ( clk ), + .rst_ni ( rst_n ), + .dma_ctrl_req_i ( apb_req ), + .dma_ctrl_rsp_o ( apb_rsp ), + .dma_req_o ( dma_req ), + .req_valid_o ( req_valid ), + .req_ready_i ( req_ready ), + .next_id_i ( next_id ), + .stream_idx_o ( stream_idx ), + .done_id_i ( done_id ), + .busy_i ( busy ), + .midend_busy_i ( midend_busy ) + ); + end else if (RegVariant == 32'd2) begin : gen_reg64_2d + idma_reg64_2d #( + .NumRegs ( NumRegs ), + .NumStreams ( NumStreams ), + .IdCounterWidth ( IdCounterWidth ), + .apb_req_t ( cfg_apb_req_t ), + .apb_rsp_t ( cfg_apb_rsp_t ), + .dma_req_t ( idma_nd_req_t ) + ) i_dut ( + .clk_i ( clk ), + .rst_ni ( rst_n ), + .dma_ctrl_req_i ( apb_req ), + .dma_ctrl_rsp_o ( apb_rsp ), + .dma_req_o ( dma_req ), + .req_valid_o ( req_valid ), + .req_ready_i ( req_ready ), + .next_id_i ( next_id ), + .stream_idx_o ( stream_idx ), + .done_id_i ( done_id ), + .busy_i ( busy ), + .midend_busy_i ( midend_busy ) + ); + end else if (RegVariant == 32'd1) begin : gen_reg64_1d + idma_req_t dut_req_1d; + + idma_reg64_1d #( + .NumRegs ( NumRegs ), + .NumStreams ( NumStreams ), + .IdCounterWidth ( IdCounterWidth ), + .apb_req_t ( cfg_apb_req_t ), + .apb_rsp_t ( cfg_apb_rsp_t ), + .dma_req_t ( idma_req_t ) + ) i_dut ( + .clk_i ( clk ), + .rst_ni ( rst_n ), + .dma_ctrl_req_i ( apb_req ), + .dma_ctrl_rsp_o ( apb_rsp ), + .dma_req_o ( dut_req_1d ), + .req_valid_o ( req_valid ), + .req_ready_i ( req_ready ), + .next_id_i ( next_id ), + .stream_idx_o ( stream_idx ), + .done_id_i ( done_id ), + .busy_i ( busy ), + .midend_busy_i ( midend_busy ) + ); + + always_comb begin : proc_widen_1d_req + dma_req = '0; + dma_req.burst_req = dut_req_1d; + end + end else begin : gen_bad_variant + // an out-of-range RegVariant must not silently re-elaborate the last variant + $error("RegVariant %0d is not a generated register frontend", RegVariant); + end // -------------------------------------------------------------------------- // Backend stub. `req_ready` is directly controllable by the tests. On each @@ -418,6 +473,9 @@ module tb_idma_reg_frontend import idma_pkg::*; import apb_test::apb_driver; #( int unsigned rcyc; // last read's ACCESS-phase cycle count initial begin : test + // the register map and request layout below model reg32_3d only + if (RegVariant != 32'd3) + $fatal(1, "[TB] RegVariant %0d is elaboration-only, it has no stimulus", RegVariant); errors = 0; checks = 0; req_ready = 1'b1; diff --git a/test/idma_test.sv b/test/idma_test.sv index 931a580d..899b8433 100644 --- a/test/idma_test.sv +++ b/test/idma_test.sv @@ -85,7 +85,7 @@ package idma_test; string res = "0x"; int now = 1; foreach (num[i]) begin - res = {res, num[i]}; + res = {res, string'(num[i])}; if (now % 4 == 0 & now != 0 & now != num.len()) res = {res, "_"}; now++; diff --git a/test/midend/tb_idma_nd_midend.sv b/test/midend/tb_idma_nd_midend.sv index cefcc935..e0e1a240 100644 --- a/test/midend/tb_idma_nd_midend.sv +++ b/test/midend/tb_idma_nd_midend.sv @@ -8,7 +8,7 @@ `timescale 1ns/1ns `include "axi/typedef.svh" -`include "idma/tracer.svh" +`include "idma/tracer_rw_axi.svh" `include "idma/typedef.svh" // Protocol testbench defines diff --git a/test/tpl/tb_idma_backend.sv.tpl b/test/tpl/tb_idma_backend.sv.tpl index 279ed81d..296911bd 100644 --- a/test/tpl/tb_idma_backend.sv.tpl +++ b/test/tpl/tb_idma_backend.sv.tpl @@ -9,7 +9,7 @@ `timescale 1ns/1ns `include "axi/typedef.svh" `include "axi_stream/typedef.svh" -`include "idma/tracer.svh" +`include "idma/tracer_${name_uniqueifier}.svh" `include "idma/typedef.svh" `include "obi/typedef.svh" `include "tilelink/typedef.svh" diff --git a/util/check_jobs.py b/util/check_jobs.py new file mode 100644 index 00000000..b89faa37 --- /dev/null +++ b/util/check_jobs.py @@ -0,0 +1,206 @@ +# Copyright 2026 ETH Zurich and University of Bologna. +# Solderpad Hardware License, Version 0.51, see LICENSE for details. +# SPDX-License-Identifier: SHL-0.51 + +# Authors: +# - Daniel Keller + +"""Codegen hygiene checks over jobs.json and the generated RTL. + +This is not verification. It checks that the run list and the generated output +still describe the same design: + + a) every backend id has a jobs.json entry, + b) every job path referenced by jobs.json exists, + c) every testbench and synth_top named by jobs.json exists in the sources, + d) the workflow fans out over every backend id, + e) every negative-test case the testbench defines is either run or named as + skipped, and every legalizer compute guard is either proven to fire by some + case or named as untested. Without (e) a new case or a new guard is added to + the design and silently exercised by nothing. + +The simulation matrix is not checked: CI fans it out over src/db/verify.yml, the +same file that drives the local runs. The backend-id matrix is a literal list in +the workflow, so (e) still compares it against the ids. + +The run list is always taken from jobs.json, never from `ls jobs/*`: four +error_*.txt files exist on disk for variants built with ErrorHandling=0 and +must not be run. They are listed in UNWIRED_JOBS so the reverse check (a file +on disk that nothing references) stays meaningful. +""" + +import argparse +import glob +import json +import os +import re +import sys + + +# On disk but deliberately unwired: these backends elaborate with ErrorHandling=0 +UNWIRED_JOBS = { + 'backend_r_axi_w_obi/error_simple.txt', + 'backend_r_axi_w_obi/error_mixed.txt', + 'backend_r_obi_w_axi/error_simple.txt', + 'backend_r_obi_w_axi/error_mixed.txt', +} + + +def load_sources(patterns): + text = [] + for pattern in patterns: + for path in sorted(glob.glob(pattern, recursive=True)): + with open(path, 'r', errors='replace') as handle: + text.append(handle.read()) + return '\n'.join(text) + + +def main(): + par = argparse.ArgumentParser(description=__doc__) + par.add_argument('--jobs', default='jobs/jobs.json') + par.add_argument('--jobs-dir', default='jobs') + par.add_argument('--ids', required=True, help='space-separated IDMA_BACKEND_IDS') + par.add_argument('--source', action='append', default=[], metavar='GLOB') + par.add_argument('--matrix-file', default=None, + help='CI workflow that must fan out over every backend id') + par.add_argument('--verify-db', default=None, + help='jobs/jobs.json; the run set and its named exclusions') + par.add_argument('--mxneg-tb', default=None, + help='negative-test testbench; its case labels must all be accounted for') + par.add_argument('--mxneg-guard-src', default=None, + help='source declaring the compute guards, e.g. the legalizer template') + args = par.parse_args() + + patterns = args.source or ['target/rtl/*.sv', 'src/**/*.sv', 'test/**/*.sv'] + with open(args.jobs, 'r') as handle: + jobs = json.load(handle) + # _verify holds the globals; entries with a verify key are simulation suites + # rather than backend variants, so they carry no synth_top and no job files + suites = {k: v for k, v in jobs.items() if 'verify' in v} + jobs = {k: v for k, v in jobs.items() if k != '_verify' and 'verify' not in v} + sources = load_sources(patterns) + errors = [] + + # (a) every backend id is represented by a jobs.json entry + synth_tops = {body.get('synth_top') for body in jobs.values()} + for backend_id in args.ids.split(): + expected = 'idma_backend_synth_' + backend_id + if expected not in synth_tops: + errors.append('backend id {} has no {} entry (expected synth_top {})'.format( + backend_id, os.path.basename(args.jobs), expected)) + + # (b) every referenced job path exists + referenced = set() + for name, body in jobs.items(): + for job, rel in body.get('jobs', {}).items(): + referenced.add(rel) + if not os.path.isfile(os.path.join(args.jobs_dir, rel)): + errors.append('{}: job "{}" references missing file {}/{}'.format( + name, job, args.jobs_dir, rel)) + + # (b, inverse) an unreferenced job file on disk is dead or a forgotten entry + on_disk = set() + for path in glob.glob(os.path.join(args.jobs_dir, '**', '*.txt'), recursive=True): + on_disk.add(os.path.relpath(path, args.jobs_dir)) + for rel in sorted(on_disk - referenced - UNWIRED_JOBS): + errors.append('{}/{} is referenced by no {} entry'.format( + args.jobs_dir, rel, os.path.basename(args.jobs))) + for rel in sorted(UNWIRED_JOBS - on_disk): + errors.append('{} is listed as deliberately unwired but does not exist'.format(rel)) + + # (c) named testbenches and synth wrappers exist in the sources + for name, body in jobs.items(): + for field in ('testbench', 'synth_top'): + module = body.get(field) + if not module: + errors.append('{}: no {} named'.format(name, field)) + continue + if not re.search(r'\bmodule\s+' + re.escape(module) + r'\b', sources): + errors.append('{}: {} "{}" is not defined in the sources'.format( + name, field, module)) + + # a suite entry names no synth_top, but its testbench must still exist + for name, body in sorted(suites.items()): + module = body.get('testbench') + if not module: + errors.append('{}: no testbench named'.format(name)) + elif not re.search(r'\bmodule\s+' + re.escape(module) + r'\b', sources): + errors.append('{}: testbench "{}" is not defined in the sources'.format( + name, module)) + + # (d) the backend-id matrix is a literal list in the workflow, so it can drift + if args.matrix_file: + with open(args.matrix_file, 'r') as handle: + matrix = handle.read() + for backend_id in args.ids.split(): + if not re.search(r'^\s*-\s*' + re.escape(backend_id) + r'\s*$', matrix, re.M): + errors.append('{} has no matrix leg for backend id {}'.format( + args.matrix_file, backend_id)) + + # (e) compares the run set against the design, not against a copy of itself + db = {} + if args.verify_db: + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + import run_verify + db = run_verify.load(args.verify_db) + + mxneg = db.get('suites', {}).get('mxneg', {}) + run_cases = {str(r['params']['NegCase']) for r in mxneg.get('runs', [])} + skipped = {str(e['case']): e['why'] for e in mxneg.get('exclude', [])} + + if args.mxneg_tb and db: + with open(args.mxneg_tb, 'r', errors='replace') as handle: + tb_text = handle.read() + body = re.search(r'\bcase\s*\(\s*NegCase\s*\)(.*?)\bendcase', tb_text, re.S) + if not body: + errors.append('{}: no case (NegCase) block found'.format(args.mxneg_tb)) + else: + defined = set(re.findall(r'^\s*(\d+)\s*:', body.group(1), re.M)) + for case in sorted(defined - run_cases - set(skipped), key=int): + errors.append('{}: case {} is defined but neither run nor excluded in ' + '{}'.format(args.mxneg_tb, case, os.path.basename(args.verify_db))) + for case in sorted(set(skipped) - defined, key=int): + errors.append('case {} is excluded but the testbench does not define ' + 'it'.format(case)) + for case in sorted(run_cases - defined, key=int): + errors.append('case {} is run but the testbench does not define it'.format(case)) + + # An exclude entry must name a value the sweep does not run + for name, suite in sorted(db.get('suites', {}).items()): + sweep = suite.get('sweep') + for entry in suite.get('exclude', []): + for value in entry.get('values', []): + if sweep and value in sweep.get('values', []): + errors.append('suite {}: {} is both run and excluded'.format(name, value)) + if not entry.get('values') and not entry.get('case'): + errors.append('suite {}: an exclude entry names neither values nor a ' + 'case'.format(name)) + + if args.mxneg_guard_src and db: + with open(args.mxneg_guard_src, 'r', errors='replace') as handle: + guard_text = handle.read() + declared = set(re.findall(r'`ASSERT_NEVER\(\s*(Compute\w+)', guard_text)) + tested = {r['token'] for r in mxneg.get('runs', []) if r.get('token')} + waived = {e['guard'] for e in db.get('guards_untested', [])} + for guard in sorted(declared - tested - waived): + errors.append('{}: guard {} has no negative test and is not named in ' + 'guards_untested'.format(args.mxneg_guard_src, guard)) + for guard in sorted(waived - declared): + errors.append('guard {} is named in guards_untested but is not declared in ' + '{}'.format(guard, args.mxneg_guard_src)) + for guard in sorted(tested - declared): + errors.append('guard {} is claimed by a mxneg run but is not declared in ' + '{}'.format(guard, args.mxneg_guard_src)) + + for message in errors: + print('error: ' + message) + if errors: + print('check_jobs: {} problem(s)'.format(len(errors))) + return 1 + print('check_jobs: {} entries, {} job paths, all consistent'.format( + len(jobs), len(referenced))) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/util/gen_idma.py b/util/gen_idma.py index bf3d711b..18bb9ce5 100644 --- a/util/gen_idma.py +++ b/util/gen_idma.py @@ -21,10 +21,10 @@ from mario.synth import render_synth_wrapper from mario.testbench import render_testbench from mario.frontend import render_reg_hjson, render_reg_top -from mario.tracer import render_tracer +from mario.tracer import render_tracer, render_tracer_common GENABLE_ENTITIES = ['transport', 'legalizer', 'backend', 'vsim_wave', 'testbench', 'synth_wrapper', - 'reg_top', 'reg_hjson', 'tracer'] + 'reg_top', 'reg_hjson', 'tracer', 'tracer_common'] EPILOG = ''' The iDMA configuration ID is composed of a underscore-separated list of specifiers and protocols. @@ -75,6 +75,8 @@ def main(): print(render_reg_top(frontend_ids, args.tpl, args.cpuif)) elif args.entity == 'tracer': print(render_tracer(protocol_ids, protocol_db, args.tpl)) + elif args.entity == 'tracer_common': + print(render_tracer_common(args.tpl)) else: return 1 diff --git a/util/idma_params.py b/util/idma_params.py new file mode 100644 index 00000000..1bcfa77f --- /dev/null +++ b/util/idma_params.py @@ -0,0 +1,233 @@ +# Copyright 2026 ETH Zurich and University of Bologna. +# Solderpad Hardware License, Version 0.51, see LICENSE for details. +# SPDX-License-Identifier: SHL-0.51 + +# Authors: +# - Daniel Keller + +"""Emit the elaboration configurations of one top, straight from jobs.json. + +Prints one line per configuration: + + -GName=Value -GName=Value ... + +Both slang and verilator take those flags verbatim. slang silently ignores a +``-G`` whose parameter does not exist, so every name is checked against the +module header here; an unknown or misspelled parameter is a hard error rather +than a configuration that quietly does not apply. +""" + +import argparse +import glob +import json +import os +import re +import sys + +# Parameters a compute-enabled configuration sets +COMPUTE_PARAMS = ('EnableCompute', 'ComputeOps', 'ComputeTuning') + + +def packed_struct_fields(typename, sources): + """Return the field names of packed struct *typename*, MSB field first.""" + decl = re.compile(r'typedef\s+struct\s+packed\s*\{([^{}]*)\}\s*' + + re.escape(typename) + r'\s*;', re.S) + for path in sources: + try: + with open(path, 'r', errors='replace') as handle: + text = handle.read() + except OSError: + continue + match = decl.search(text) + if not match: + continue + fields = [] + for line in match.group(1).splitlines(): + line = re.sub(r'//.*', '', line).strip() + if not line.startswith('logic'): + continue + for name in line[len('logic'):].rstrip(';').split(','): + name = name.strip() + if name: + fields.append(name) + return fields + return [] + + +def module_parameters(top, sources): + """Return the parameter names declared in the header of module *top*.""" + decl = re.compile(r'\bmodule\s+' + re.escape(top) + r'\b') + for path in sources: + try: + with open(path, 'r', errors='replace') as handle: + text = handle.read() + except OSError: + continue + match = decl.search(text) + if not match: + continue + start = text.find('#(', match.end()) + if start < 0: + return set() + depth = 0 + for pos in range(start + 1, len(text)): + if text[pos] == '(': + depth += 1 + elif text[pos] == ')': + depth -= 1 + if depth == 0: + header = text[start:pos] + break + else: + raise SystemExit('error: unterminated parameter list of {}'.format(top)) + names = re.findall(r'\bparameter\b[^,;()]*?\b([A-Za-z_]\w*)\s*=', header) + return set(names) + raise SystemExit('error: module {} not found in the given sources'.format(top)) + + +def entries_for(jobs, top): + """jobs.json entries whose synth_top or testbench is *top*.""" + return [(name, body) for name, body in jobs.items() + if body.get('synth_top') == top or body.get('testbench') == top] + + +def main(): + par = argparse.ArgumentParser(description=__doc__) + par.add_argument('--verify-db', default=None, + help='jobs/jobs.json; supplies the width and compute sweeps') + par.add_argument('--top', required=True) + par.add_argument('--jobs', default='jobs/jobs.json') + par.add_argument('--source', action='append', default=[], metavar='GLOB', + help='glob of SystemVerilog sources to search for the module header') + par.add_argument('--widths', default='', + help='space-separated DataWidth sweep; empty disables the sweep') + par.add_argument('--compute', default='', metavar='WIDTH:OPS:TUNING', + help='space-separated compute-enabled configurations; empty ' + 'disables the sweep. Skipped for tops without the parameters') + args = par.parse_args() + + # The database is the source when supplied; the flags stay for one-off runs. + if args.verify_db: + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + import run_verify + db = run_verify.load(args.verify_db) + if db.get('elab_widths'): + args.widths = ' '.join(str(w) for w in db['elab_widths']) + if db.get('elab_compute'): + args.compute = ' '.join('{}:{}:{}'.format(c['width'], c['ops'], c['tuning']) + for c in db['elab_compute']) + + patterns = args.source or ['target/rtl/*.sv', 'src/**/*.sv', 'test/**/*.sv'] + sources = [] + for pattern in patterns: + sources += sorted(glob.glob(pattern, recursive=True)) + if not sources: + raise SystemExit('error: no sources matched {}'.format(patterns)) + + with open(args.jobs, 'r') as handle: + jobs = json.load(handle) + + declared = module_parameters(args.top, sources) + matches = entries_for(jobs, args.top) + if not matches: + raise SystemExit('error: no {} entry names {} as a synth_top or testbench'.format( + os.path.basename(args.jobs), args.top)) + + lines = [] + seen = set() + base = {} + for name, body in matches: + params = body.get('params', {}) + unknown = sorted(set(params) - declared) + if unknown: + raise SystemExit('error: {} names parameter(s) {} that module {} does not ' + 'declare'.format(name, ', '.join(unknown), args.top)) + if not base: + base = dict(params) + flags = ' '.join('-G{}={}'.format(k, v) for k, v in params.items()) + seen.add(flags) + lines.append('{} {}'.format(name, flags).rstrip()) + + widths = [w for w in args.widths.split() if w] + if widths: + if 'DataWidth' not in declared: + print('note: {} has no DataWidth parameter; width sweep not applicable'.format( + args.top), file=sys.stderr) + else: + for width in widths: + cfg = dict(base) + cfg['DataWidth'] = width + flags = ' '.join('-G{}={}'.format(k, v) for k, v in cfg.items()) + if flags in seen: # identical to a jobs.json configuration + continue + seen.add(flags) + lines.append('dw{} {}'.format(width, flags)) + + computes = [c for c in args.compute.split() if c] + if computes: + have = [p for p in COMPUTE_PARAMS if p in declared] + if have and len(have) != len(COMPUTE_PARAMS): + raise SystemExit('error: module {} declares {} but not {}; the compute sweep ' + 'would silently not apply'.format( + args.top, ', '.join(have), + ', '.join(p for p in COMPUTE_PARAMS if p not in declared))) + if not have: + print('note: {} has no compute parameters; compute sweep not applicable'.format( + args.top), file=sys.stderr) + else: + ops_fields = packed_struct_fields('compute_enable_t', sources) + tuning_fields = packed_struct_fields('compute_tuning_t', sources) + if not ops_fields or not tuning_fields: + raise SystemExit('error: cannot read compute_enable_t/compute_tuning_t; ' + 'the compute sweep cannot be validated') + # MSB first; mxfp16 gates FP16 paths only, so it enables no datapath alone + real_ops = [f for f in ops_fields if f != 'mxfp16'] + real_mask = 0 + for pos, name in enumerate(reversed(ops_fields)): + if name in real_ops: + real_mask |= 1 << pos + for spec in computes: + fields = spec.split(':') + if len(fields) != 3: + raise SystemExit('error: --compute entry {} is not ' + 'WIDTH:OPS:TUNING'.format(spec)) + width, ops, tuning = fields + for name, value in (('WIDTH', width), ('OPS', ops), ('TUNING', tuning)): + if not value.isdigit(): + raise SystemExit('error: --compute entry {}: {} is not a ' + 'non-negative integer'.format(spec, name)) + if int(width) <= 0: + raise SystemExit('error: --compute entry {}: WIDTH must be ' + 'positive'.format(spec)) + if int(ops) >= 1 << len(ops_fields): + raise SystemExit('error: --compute entry {}: OPS {} does not fit ' + 'compute_enable_t ({} bits: {})'.format( + spec, ops, len(ops_fields), ', '.join(ops_fields))) + if int(tuning) >= 1 << len(tuning_fields): + raise SystemExit('error: --compute entry {}: TUNING {} does not fit ' + 'compute_tuning_t ({} bits: {})'.format( + spec, tuning, len(tuning_fields), + ', '.join(tuning_fields))) + if not int(ops) & real_mask: + raise SystemExit('error: --compute entry {}: OPS {} enables no compute ' + 'op ({}), so the datapath is never instantiated and ' + 'the configuration elaborates nothing'.format( + spec, ops, ', '.join(real_ops))) + cfg = dict(base) + if 'DataWidth' in declared: + cfg['DataWidth'] = width + cfg['EnableCompute'] = 1 + cfg['ComputeOps'] = ops + cfg['ComputeTuning'] = tuning + flags = ' '.join('-G{}={}'.format(k, v) for k, v in cfg.items()) + if flags in seen: + continue + seen.add(flags) + lines.append('compute{}_ops{}_t{} {}'.format(width, ops, tuning, flags)) + + print('\n'.join(lines)) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/util/mario/tracer.py b/util/mario/tracer.py index cb161dbe..15961e80 100644 --- a/util/mario/tracer.py +++ b/util/mario/tracer.py @@ -107,32 +107,41 @@ def _flatten_dict(d, parent_key='', delimiter='_'): ''' +def render_tracer_common(tpl_file: str) -> str: + """Generate the id-independent tracer helpers""" + with open(tpl_file, 'r', encoding='utf-8') as templ_file: + return Template(templ_file.read()).render() + + def render_tracer(prot_ids: dict, db: dict, tpl_file: str) -> str: - """Generate racer""" + """Generate the tracer of one backend id""" tracer_body = '' + # one header per id: the header name carries the id, so a list is meaningless here + if len(prot_ids) != 1: + raise ValueError(f'the tracer renders exactly one id, got {len(prot_ids)}') + with open(tpl_file, 'r', encoding='utf-8') as templ_file: tracer_tpl = templ_file.read() - # render for every is for prot_id in prot_ids: # signals signals = '' - # handle read ports + # direction-qualified: INIT is on both sides and would emit the key twice for read_prot in prot_ids[prot_id]['ar']: sig_dict = _flatten_dict(db[read_prot]['trace_signals']['read']) for signal in sig_dict: signals += ' ' - signals += f'"{read_prot}_{signal}": __backend_inst``.{sig_dict[signal]}' + signals += f'"{read_prot}_read_{signal}": __backend_inst``.{sig_dict[signal]}' signals += ', \\\n' for write_prot in prot_ids[prot_id]['aw']: sig_dict = _flatten_dict(db[write_prot]['trace_signals']['write']) for signal in sig_dict: signals += ' ' - signals += f'"{write_prot}_{signal}": __backend_inst``.{sig_dict[signal]}' + signals += f'"{write_prot}_write_{signal}": __backend_inst``.{sig_dict[signal]}' signals += ', \\\n' # post-processing @@ -149,6 +158,8 @@ def render_tracer(prot_ids: dict, db: dict, tpl_file: str) -> str: # render tracer context context = { + 'identifier': prot_id, + 'identifier_cap': prot_id.upper(), 'body': tracer_body } diff --git a/util/run_verify.py b/util/run_verify.py new file mode 100644 index 00000000..2ceade31 --- /dev/null +++ b/util/run_verify.py @@ -0,0 +1,233 @@ +# Copyright 2026 ETH Zurich and University of Bologna. +# Solderpad Hardware License, Version 0.51, see LICENSE for details. +# SPDX-License-Identifier: SHL-0.51 + +# Authors: +# - Daniel Keller + +"""Run a verification suite, or emit the CI matrix, from jobs/jobs.json. + +Both come from the same entries, so a leg cannot exist in CI and not locally, or +the reverse. The old form kept the run set in make variables and the fan-out in +the workflow, and needed a drift checker to notice when they disagreed. + + run_verify.py --suite mxneg run every leg of one suite + run_verify.py --emit-matrix suites print the suite names as a CI matrix + run_verify.py --list print every leg, one per line + run_verify.py --tb-tops print the testbench tops bender knows of + run_verify.py --prereqs mxneg print the files that suite needs built + run_verify.py --emit reg_variants print a list the make recipes loop over + +Testbench tops are asked of bender rather than listed by hand: it already owns +the file set, so a testbench added to Bender.yml is elaborated without touching +this file, and one that is not in Bender.yml cannot hide behind a stale list. +""" + +import argparse +import json +import os +import re +import subprocess +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(HERE) +DB = os.path.join(ROOT, 'jobs', 'jobs.json') + + +def load(path=None): + """Return {suites, ...globals}; entries carrying a `verify` key are suites.""" + with open(path or DB, 'r') as handle: + jobs = json.load(handle) + db = dict(jobs.pop('_verify', {})) + db['suites'] = {} + for name, entry in jobs.items(): + if 'verify' not in entry: + continue + suite = dict(entry['verify']) + suite['top'] = entry['testbench'] + # entry params are common to every leg; a leg may override one + common = entry.get('params', {}) + suite['runs'] = [dict(r, params={**common, **r.get('params', {})}) + for r in suite.pop('legs')] + db['suites'][name] = suite + return db + + +def legs(suite_name, suite): + """Expand one suite into its runs: an explicit list, or a swept parameter.""" + if 'runs' in suite: + for run in suite['runs']: + yield { + 'tag': run['tag'], + 'params': dict(run.get('params', {})), + 'token': run.get('token', suite.get('token')), + 'plusargs': run.get('plusargs', []), + } + return + if 'sweep' not in suite: + raise KeyError('suite {} has neither runs nor sweep'.format(suite_name)) + sweep = suite['sweep'] + for value in sweep['values']: + yield { + 'tag': '{}_{}'.format(suite_name, value), + 'params': {sweep['param']: value}, + 'token': suite.get('token'), + 'plusargs': [], + } + + +def bender_sources(bender, targets): + """The .sv files bender selects for *targets*, in compile order.""" + cmd = [bender, 'script', 'flist'] + targets + out = subprocess.run(cmd, cwd=ROOT, capture_output=True, text=True) + if out.returncode != 0: + raise SystemExit('bender failed: ' + out.stderr.strip()) + files = [] + for line in out.stdout.splitlines(): + line = line.strip() + if line and not line.startswith(('+incdir+', '-', '+define+')): + files.append(line) + return files + + +def tb_tops(db, bender, targets): + """Every testbench module bender knows about, for the elaboration tier. + + `tops_untested` is deliberately not subtracted: those tops elaborate fine and + only their simulation is excluded, so dropping them here would quietly lose + the elaboration coverage that still works. + """ + covered = set(db.get('elab_covered_elsewhere', [])) + found = [] + decl = re.compile(r'^\s*module\s+(tb_\w+)', re.M) + for path in bender_sources(bender, targets): + # relative: an absolute match on deps/ would filter out the whole repo + rel = os.path.relpath(path, ROOT) + if rel.startswith('..') or rel.startswith(('.bender/', 'deps/')): + continue + try: + with open(path, 'r', errors='replace') as handle: + text = handle.read() + except OSError: + continue + for name in decl.findall(text): + if name.startswith('tb_idma_backend_'): + continue # covered by the matching per-backend leg + if name in covered or name in found: + continue + found.append(name) + return sorted(found) + + +def run_suite(name, db, args): + suite = db['suites'][name] + vlt_dir = args.vlt_dir + flist = os.path.join(vlt_dir, suite['top'] + '.f') + failures = [] + planned = list(legs(name, suite)) + if not planned: + print('error: suite {} has no legs'.format(name)) + return 1 + for leg in planned: + if not leg['token']: + print('error: leg {} has no token'.format(leg['tag']), file=sys.stderr) + return 1 + cmd = [sys.executable, os.path.join(HERE, 'run_vlt_sim.py'), + '--dir', vlt_dir, '--top', suite['top'], '--flist', flist, + '--tag', leg['tag'], '--token', leg['token']] + for key, value in leg['params'].items(): + cmd += ['--param', '{}={}'.format(key, value)] + for define in suite.get('defines', []): + cmd += ['--define', define] + for plusarg in leg['plusargs']: + cmd += ['--plusarg', plusarg] + if suite.get('dpi'): + cmd += ['--dpi', os.path.join(vlt_dir, suite['dpi'] + '.o')] + if suite.get('expect') == 'fail': + cmd += ['--expect', 'fail'] + if args.verilator: + cmd += ['--verilator', args.verilator] + if args.makeflags: + cmd += ['--makeflags', args.makeflags] + if subprocess.call(cmd) != 0: + failures.append(leg['tag']) + # A suite that silently ran nothing is a pass under any per-leg check alone + print('{}: ran {} leg(s), {} failed'.format(name, len(planned), len(failures))) + return 1 if failures else 0 + + +def main(): + par = argparse.ArgumentParser(description=__doc__) + par.add_argument('--db', default=DB) + par.add_argument('--suite') + par.add_argument('--emit-matrix', choices=['suites']) + par.add_argument('--list', action='store_true') + par.add_argument('--emit', metavar='KEY', + choices=['elab_shared_tops', 'multihead_ids', 'reg_variants', 'suites'], + help='print a database list for a make recipe to loop over') + par.add_argument('--prereqs', metavar='SUITE', + help='files the suite needs built, so make needs no per-suite rule') + par.add_argument('--tb-tops', action='store_true', + help='testbench tops, from bender rather than a hand-kept list') + par.add_argument('--bender', default=os.environ.get('BENDER', 'bender')) + par.add_argument('--target', action='append', default=[], + help='bender target; repeat, e.g. --target rtl --target idma_test') + par.add_argument('--vlt-dir', default=os.path.join(ROOT, 'target/sim/verilator')) + par.add_argument('--verilator', default=os.environ.get('VERILATOR')) + par.add_argument('--makeflags', default=os.environ.get('IDMA_VLT_MAKEFLAGS')) + args = par.parse_args() + db = load(args.db) + + if args.emit_matrix == 'suites': + if not db.get('suites'): + print('error: the database lists no suites', file=sys.stderr) + return 1 + print(json.dumps({'suite': sorted(db['suites'])})) + return 0 + if args.emit: + entries = sorted(db['suites']) if args.emit == 'suites' else (db.get(args.emit) or []) + if not entries: + print('error: {} is empty'.format(args.emit), file=sys.stderr) + return 1 + if args.emit == 'reg_variants': + for entry in entries: + print('{} {}'.format(entry['variant'], entry['module'])) + else: + print(' '.join(str(e) for e in entries)) + return 0 + if args.prereqs: + if args.prereqs not in db['suites']: + print('error: no suite named {}'.format(args.prereqs), file=sys.stderr) + return 1 + suite = db['suites'][args.prereqs] + needed = [os.path.join(args.vlt_dir, suite['top'] + '.f')] + if suite.get('dpi'): + needed.append(os.path.join(args.vlt_dir, suite['dpi'] + '.o')) + print(' '.join(needed)) + return 0 + if args.tb_tops: + targets = [] + for target in args.target: + targets += ['-t', target] + tops = tb_tops(db, args.bender, targets) + if not tops: + print('error: bender returned no testbench tops', file=sys.stderr) + return 1 + print(' '.join(tops)) + return 0 + if args.list: + for name in sorted(db['suites']): + for leg in legs(name, db['suites'][name]): + print('{}\t{}\t{}'.format(name, leg['tag'], leg['token'])) + return 0 + if args.suite: + if args.suite not in db['suites']: + print('error: no suite named {}'.format(args.suite)) + return 1 + return run_suite(args.suite, db, args) + par.error('pass --suite, --emit-matrix or --list') + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/util/run_vlt_sim.py b/util/run_vlt_sim.py new file mode 100644 index 00000000..59e8b4cb --- /dev/null +++ b/util/run_vlt_sim.py @@ -0,0 +1,165 @@ +# Copyright 2026 ETH Zurich and University of Bologna. +# Solderpad Hardware License, Version 0.51, see LICENSE for details. +# SPDX-License-Identifier: SHL-0.51 + +# Authors: +# - Daniel Keller + +"""Build and run one Verilator simulation, then check it three ways. + +A simulation leg only passes when all three hold: + 1. the run exited with the expected status (0 for a positive test, non-zero + for a negative test, whose guard is supposed to fire), + 2. the log exists and is non-empty, + 3. the log contains the expected positive token. + +A negated grep for "Error:" is deliberately not used: it passes when the log is +missing, when the simulator died before writing anything, and when the token +was never printed. Every condition here is stated positively. + +A hang is a failure in its own right, including for a negative test: several +testbenches in this repository hang rather than fail, so both stages run under a +timeout and a timed-out leg can never be reported as a guard that fired. +""" + +import argparse +import os +import shlex +import signal +import subprocess +import sys +import threading +import time + + +def run(cmd, log_path, cwd, timeout=None): + """Run cmd, tee to log_path, return (rc, wall_seconds, timed_out).""" + start = time.monotonic() + timed_out = [] + with open(log_path, 'wb') as log: + # own process group: verilator and the simulation binary spawn children + proc = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, start_new_session=True) + + def expire(): + timed_out.append(True) + try: + os.killpg(os.getpgid(proc.pid), signal.SIGKILL) + except OSError: + pass + + timer = threading.Timer(timeout, expire) if timeout else None + if timer: + timer.start() + try: + for chunk in iter(lambda: proc.stdout.read(4096), b''): + log.write(chunk) + sys.stdout.buffer.write(chunk) + sys.stdout.flush() + proc.stdout.close() + rc = proc.wait() + finally: + if timer: + timer.cancel() + return rc, time.monotonic() - start, bool(timed_out) + + +def parse_args(): + par = argparse.ArgumentParser(description=__doc__) + par.add_argument('--top', required=True) + par.add_argument('--flist', required=True) + par.add_argument('--dir', required=True, help='build/run scratch directory') + par.add_argument('--tag', default=None, help='unique name for this configuration') + par.add_argument('--token', required=True, help='string the log must contain') + par.add_argument('--expect', choices=['pass', 'fail'], default='pass', + help='pass: run must exit 0; fail: run must exit non-zero') + par.add_argument('--param', action='append', default=[], metavar='NAME=VALUE', + help='top-level parameter override (-G)') + par.add_argument('--plusarg', action='append', default=[], metavar='+ARG') + par.add_argument('--dpi', action='append', default=[], metavar='OBJ', + help='precompiled DPI object to link') + par.add_argument('--verilator', default=os.environ.get('VERILATOR', 'verilator')) + par.add_argument('--makeflags', default=os.environ.get('IDMA_VLT_MAKEFLAGS', '')) + par.add_argument('--define', action='append', default=[], metavar='NAME', + help='preprocessor define passed to verilator (-D)') + par.add_argument('--vlt-arg', action='append', default=[], metavar='ARG', + help='extra verilator argument; use --vlt-arg=-X for dashed values') + par.add_argument('--timeout', type=int, default=900, metavar='S', + help='wall-clock budget for the simulation run; 0 disables') + par.add_argument('--build-timeout', type=int, default=3600, metavar='S', + help='wall-clock budget for the verilator build; 0 disables') + return par.parse_args() + + +def main(): + args = parse_args() + tag = args.tag or args.top + workdir = os.path.abspath(args.dir) + os.makedirs(workdir, exist_ok=True) + objdir = os.path.join(workdir, 'obj_' + tag) + binary = os.path.join(objdir, 'simv') + build_log = os.path.join(workdir, tag + '_build.log') + run_log = os.path.join(workdir, tag + '_run.log') + + build = shlex.split(args.verilator) + [ + '--binary', '--timing', '--assert', '-Wno-fatal', '--error-limit', '1000', + '-CFLAGS', '-O2', + '--unroll-count', '4096', '--unroll-stmts', '200000', + '-Mdir', objdir, '-o', 'simv', + '-f', os.path.abspath(args.flist), '--top-module', args.top, + ] + if args.makeflags: + build += ['-MAKEFLAGS', args.makeflags] + build += ['-G' + p for p in args.param] + build += ['-D' + d for d in args.define] + if args.dpi: + build += ['-LDFLAGS', ' '.join(os.path.abspath(d) for d in args.dpi)] + build += args.vlt_arg + + print('--- building {} [{}] ---'.format(args.top, tag), flush=True) + rc, secs, expired = run(build, build_log, workdir, args.build_timeout or None) + print('--- build {} rc={} ({:.1f} s) ---'.format(tag, rc, secs), flush=True) + if expired: + print('FAIL {}: build exceeded {} s (see {})'.format( + tag, args.build_timeout, build_log)) + return 1 + if rc != 0: + print('FAIL {}: verilator build failed (see {})'.format(tag, build_log)) + return 1 + if not os.path.isfile(binary): + print('FAIL {}: no simulation binary at {}'.format(tag, binary)) + return 1 + + print('--- running {} [{}] ---'.format(args.top, tag), flush=True) + rc, secs, expired = run([binary] + args.plusarg, run_log, workdir, args.timeout or None) + print('--- run {} rc={} ({:.2f} s) ---'.format(tag, rc, secs), flush=True) + + # 0. a hang is a failure, including for a negative test: it is not a guard firing + if expired: + print('FAIL {}: run exceeded {} s and was killed (see {})'.format( + tag, args.timeout, run_log)) + return 1 + # 1. exit status + if args.expect == 'pass' and rc != 0: + print('FAIL {}: expected exit 0, got {}'.format(tag, rc)) + return 1 + if args.expect == 'fail' and rc == 0: + print('FAIL {}: negative test exited 0; the guard never fired'.format(tag)) + return 1 + # 2. log present and non-empty + if not os.path.isfile(run_log) or os.path.getsize(run_log) == 0: + print('FAIL {}: run log {} is missing or empty'.format(tag, run_log)) + return 1 + # 3. positive token + with open(run_log, 'r', errors='replace') as handle: + text = handle.read() + if args.token not in text: + print('FAIL {}: token "{}" absent from {}'.format(tag, args.token, run_log)) + return 1 + + print('PASS {}: rc={} token="{}"'.format(tag, rc, args.token)) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/util/slang_elab.py b/util/slang_elab.py new file mode 100644 index 00000000..9b89804a --- /dev/null +++ b/util/slang_elab.py @@ -0,0 +1,43 @@ +# Copyright 2026 ETH Zurich and University of Bologna. +# Solderpad Hardware License, Version 0.51, see LICENSE for details. +# SPDX-License-Identifier: SHL-0.51 + +# Authors: +# - Daniel Keller + +"""Run a slang elaboration through pyslang. + +pyslang ships the same driver as the ``slang`` binary, so every argument is +forwarded verbatim and a red CI leg reproduces locally with the same flags. +A wrapper script is deliberately avoided: some site wrappers swallow a crash +and return 0, which would turn this gate vacuous. + +Exit codes mirror the slang driver: 0 clean, 1 bad command line, 5 compilation +or elaboration error. Any unexpected exception is also a non-zero exit. +""" + +import shlex +import sys + +import pyslang + + +def main(argv): + driver = pyslang.driver.Driver() + driver.addStandardArgs() + + # The string overload parses a full command line; token 0 is the program name. + cmdline = ' '.join(shlex.quote(a) for a in ['slang'] + list(argv)) + if not driver.parseCommandLine(cmdline): + return 1 + if not driver.processOptions(): + return 1 + + # `&` not `and`: both stages must run so parse errors are not masked + ok = driver.parseAllSources() + ok = driver.runFullCompilation() & ok + return 0 if ok else 5 + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/util/trace_idma.py b/util/trace_idma.py index 50ac8c08..5fc65a85 100644 --- a/util/trace_idma.py +++ b/util/trace_idma.py @@ -60,13 +60,15 @@ def get_global_utilization(trace: list, params: dict, be_info: dict) -> list: for ele in trace: # add read contribution for read_prot in be_info['read_prots']: - if ele['bus'][f'{read_prot}_rsp_ready'] and ele['bus'][f'{read_prot}_rsp_valid']: + if (ele['bus'][f'{read_prot}_read_rsp_ready'] + and ele['bus'][f'{read_prot}_read_rsp_valid']): read_data += params['data_width'] // 8 # add write contribution for write_prot in be_info['write_prots']: - if ele['bus'][f'{write_prot}_req_ready'] and ele['bus'][f'{write_prot}_req_valid']: - write_data += strb_to_bytes(ele['bus'][f'{write_prot}_req_strobe']) + if (ele['bus'][f'{write_prot}_write_req_ready'] + and ele['bus'][f'{write_prot}_write_req_valid']): + write_data += strb_to_bytes(ele['bus'][f'{write_prot}_write_req_strobe']) # calculate maximum possible amount of data max_data = len(trace) * params['data_width'] // 8 diff --git a/verify.mk b/verify.mk new file mode 100644 index 00000000..d296829c --- /dev/null +++ b/verify.mk @@ -0,0 +1,269 @@ +# Copyright 2026 ETH Zurich and University of Bologna. +# Solderpad Hardware License, Version 0.51, see LICENSE for details. +# SPDX-License-Identifier: SHL-0.51 + +# Authors: +# - Daniel Keller + +# License-free verification (verilator + slang); no build target depends on it + +# --------------- +# Public verification +# --------------- + +# One CI leg per top-level target; each reproduces a red check locally. + +.PHONY: idma_verify_codegen idma_verify_backend idma_verify_shared idma_verify_multihead +.PHONY: idma_verify_tb_shared +.PHONY: idma_verify_all idma_lint_params idma_slang_elab idma_slang_tb idma_slang_report +.PHONY: idma_verify_clean + +# Module to elaborate; every per-top target takes it +IDMA_TOP ?= +# Backend variant a leg covers +IDMA_VERIFY_ID ?= rw_axi +IDMA_MULTIHEAD_PAT := 2r_axi_w_axi 2rw_axi +IDMA_VERIFY_DB := $(IDMA_ROOT)/$(IDMA_JOBS_JSON) +IDMA_VERIFY_DIR := $(IDMA_ROOT)/target/verify +IDMA_SLANG_DIR := $(IDMA_ROOT)/target/sim/slang +IDMA_SLANG_VERSION ?= 11.0.0 +SLANG ?= $(UV) run --locked --project $(IDMA_ROOT) \ + --with pyslang==$(IDMA_SLANG_VERSION) python \ + $(IDMA_UTIL_DIR)/slang_elab.py +IDMA_SLANG_ARGS := -Werror --error-limit 0 +# -Wno-finish-num covers common_verification; slang cannot scope it to a dependency +IDMA_SLANG_TB_ARGS := --timescale=1ns/1ps -Wno-finish-num +IDMA_SLANG_SYNTH_T := -t rtl -t synth +IDMA_SLANG_TB_T := -t rtl -t synth -t idma_test -t simulation -t sim -t test \ + -t snitch_cluster -t asic + +# A file target, not $(shell): make discards a $(shell) exit status +$(IDMA_VERIFY_DIR)/%.list: $(IDMA_VERIFY_DB) $(IDMA_UTIL_DIR)/run_verify.py + mkdir -p $(@D) + $(PYTHON) $(IDMA_UTIL_DIR)/run_verify.py --emit $* > $@.tmp + test -s $@.tmp + mv $@.tmp $@ + +$(IDMA_VERIFY_DIR)/tb_tops.txt: $(IDMA_ROOT)/Bender.yml $(IDMA_VERIFY_DB) \ + $(IDMA_UTIL_DIR)/run_verify.py + mkdir -p $(@D) + BENDER=$(BENDER) $(PYTHON) $(IDMA_UTIL_DIR)/run_verify.py --tb-tops \ + $(patsubst -t%,--target %,$(IDMA_SLANG_TB_T)) > $@.tmp + test -s $@.tmp + mv $@.tmp $@ + +IDMA_SOURCE_GLOBS := --source '$(IDMA_RTL_DIR)/*.sv' --source '$(IDMA_ROOT)/src/**/*.sv' \ + --source '$(IDMA_ROOT)/test/**/*.sv' + +# Emit the elaboration configurations of $1 (jobs.json parameter sets + width sweep) +define idma_elab_cfg + mkdir -p $(IDMA_VERIFY_DIR) + $(PYTHON) $(IDMA_UTIL_DIR)/idma_params.py --top $1 \ + --jobs $(IDMA_ROOT)/$(IDMA_JOBS_JSON) --verify-db $(IDMA_VERIFY_DB) \ + $(IDMA_SOURCE_GLOBS) > $(IDMA_VERIFY_DIR)/$1.cfg +endef + +# Filelists +$(IDMA_VLT_DIR)/idma_verify.f: $(IDMA_BENDER_FILES) $(IDMA_FULL_RTL) $(IDMA_INCLUDE_ALL) + mkdir -p $(IDMA_VLT_DIR) + $(BENDER) script verilator $(IDMA_SLANG_SYNTH_T) > $@ + +$(IDMA_SLANG_DIR)/synth.f: $(IDMA_BENDER_FILES) $(IDMA_FULL_RTL) $(IDMA_INCLUDE_ALL) + mkdir -p $(IDMA_SLANG_DIR) + $(BENDER) script flist-plus $(IDMA_SLANG_SYNTH_T) > $@ + +$(IDMA_SLANG_DIR)/tb.f: $(IDMA_BENDER_FILES) $(IDMA_FULL_RTL) $(IDMA_FULL_TB) $(IDMA_INCLUDE_ALL) + mkdir -p $(IDMA_SLANG_DIR) + $(BENDER) script flist-plus $(IDMA_SLANG_TB_T) > $@ + + +idma_lint_params: $(IDMA_VLT_DIR)/idma_verify.f + @test -n "$(IDMA_TOP)" || { echo "error: set IDMA_TOP="; exit 1; } + $(call idma_elab_cfg,$(IDMA_TOP)) + @test -s $(IDMA_VERIFY_DIR)/$(IDMA_TOP).cfg || \ + { echo "error: no configurations for $(IDMA_TOP)"; exit 1; } + @rc=0; while read -r cfg flags; do \ + echo "--- verilator $(IDMA_TOP) [$$cfg] $$flags ---"; \ + $(VERILATOR) $(IDMA_VLT_LINT_ARGS) -f $(IDMA_VLT_DIR)/idma_verify.f \ + --top-module $(IDMA_TOP) $$flags || rc=1; \ + done < $(IDMA_VERIFY_DIR)/$(IDMA_TOP).cfg; \ + test $$rc -eq 0 && echo "idma_lint_params: $(IDMA_TOP) OK" || \ + echo "idma_lint_params: $(IDMA_TOP) FAILED"; exit $$rc + +# slang elaboration of IDMA_TOP over the same configurations +idma_slang_elab: $(IDMA_SLANG_DIR)/synth.f + @test -n "$(IDMA_TOP)" || { echo "error: set IDMA_TOP="; exit 1; } + $(call idma_elab_cfg,$(IDMA_TOP)) + @test -s $(IDMA_VERIFY_DIR)/$(IDMA_TOP).cfg || \ + { echo "error: no configurations for $(IDMA_TOP)"; exit 1; } + @rc=0; while read -r cfg flags; do \ + echo "--- slang $(IDMA_TOP) [$$cfg] $$flags ---"; \ + $(SLANG) -f $(IDMA_SLANG_DIR)/synth.f --top $(IDMA_TOP) $(IDMA_SLANG_ARGS) $$flags \ + || rc=1; \ + done < $(IDMA_VERIFY_DIR)/$(IDMA_TOP).cfg; \ + test $$rc -eq 0 && echo "idma_slang_elab: $(IDMA_TOP) OK" || \ + echo "idma_slang_elab: $(IDMA_TOP) FAILED"; exit $$rc + +# slang only; verilator cannot parse the verification stack +idma_slang_tb: $(IDMA_SLANG_DIR)/tb.f + @test -n "$(IDMA_TOP)" || { echo "error: set IDMA_TOP="; exit 1; } + $(SLANG) -f $(IDMA_SLANG_DIR)/tb.f --top $(IDMA_TOP) \ + $(IDMA_SLANG_ARGS) $(IDMA_SLANG_TB_ARGS) + +# One backend variant: synthesis top under both front ends, plus its testbench +idma_verify_backend: + $(MAKE) idma_lint_params IDMA_TOP=idma_backend_synth_$(IDMA_VERIFY_ID) + $(MAKE) idma_slang_elab IDMA_TOP=idma_backend_synth_$(IDMA_VERIFY_ID) + $(MAKE) idma_slang_tb IDMA_TOP=tb_idma_backend_$(IDMA_VERIFY_ID) + +# Non-backend synthesis tops, plus every top from idma_lint_all +idma_verify_shared: idma_lint_all $(IDMA_VERIFY_DIR)/elab_shared_tops.list + @test -s $(IDMA_VERIFY_DIR)/elab_shared_tops.list || \ + { echo "error: no shared tops listed"; exit 1; } + set -e; for top in $$(cat $(IDMA_VERIFY_DIR)/elab_shared_tops.list); do \ + $(MAKE) idma_lint_params IDMA_TOP=$$top; \ + $(MAKE) idma_slang_elab IDMA_TOP=$$top; \ + done + +idma_verify_tb_shared: $(IDMA_SLANG_DIR)/tb.f $(IDMA_VERIFY_DIR)/tb_tops.txt \ + $(IDMA_VERIFY_DIR)/reg_variants.list + @set -e; for id in $(IDMA_FE_IDS); do \ + grep -q " idma_$$id$$" $(IDMA_VERIFY_DIR)/reg_variants.list || \ + { echo "error: no reg_variants entry elaborates idma_$$id"; exit 1; }; \ + done + @test -s $(IDMA_VERIFY_DIR)/tb_tops.txt || \ + { echo "error: no testbench tops; bender returned nothing"; exit 1; } + set -e; for top in $$(cat $(IDMA_VERIFY_DIR)/tb_tops.txt); do \ + echo "--- slang $$top ---"; \ + $(SLANG) -f $(IDMA_SLANG_DIR)/tb.f --top $$top \ + $(IDMA_SLANG_ARGS) $(IDMA_SLANG_TB_ARGS); \ + done + @test -s $(IDMA_VERIFY_DIR)/reg_variants.list || \ + { echo "error: no register variants listed"; exit 1; } + set -e; while read -r v mod; do \ + echo "--- slang tb_idma_reg_frontend [$$mod] ---"; \ + $(SLANG) -f $(IDMA_SLANG_DIR)/tb.f --top tb_idma_reg_frontend -GRegVariant=$$v \ + $(IDMA_SLANG_ARGS) $(IDMA_SLANG_TB_ARGS); \ + done < $(IDMA_VERIFY_DIR)/reg_variants.list + +# Out-of-tree multi-head build; the aggregate is rebuilt after +idma_verify_multihead: $(IDMA_VERIFY_DIR)/multihead_ids.list + @test -s $(IDMA_VERIFY_DIR)/multihead_ids.list || \ + { echo "error: no multi-head ids listed"; exit 1; } + $(MAKE) idma_hw_all IDMA_ADD_IDS="$$(cat $(IDMA_VERIFY_DIR)/multihead_ids.list)" + mkdir -p $(IDMA_VLT_DIR) $(IDMA_SLANG_DIR) + $(BENDER) script verilator $(IDMA_SLANG_SYNTH_T) > $(IDMA_VLT_DIR)/idma_multihead.f + $(BENDER) script flist-plus $(IDMA_SLANG_SYNTH_T) > $(IDMA_SLANG_DIR)/mh_synth.f + $(BENDER) script flist-plus $(IDMA_SLANG_TB_T) -t multihead \ + > $(IDMA_SLANG_DIR)/mh_tb.f + set -e; for id in $$(cat $(IDMA_VERIFY_DIR)/multihead_ids.list); do \ + echo "--- verilator idma_backend_synth_$$id ---"; \ + $(VERILATOR) $(IDMA_VLT_LINT_ARGS) -f $(IDMA_VLT_DIR)/idma_multihead.f \ + --top-module idma_backend_synth_$$id; \ + echo "--- slang idma_backend_synth_$$id ---"; \ + $(SLANG) -f $(IDMA_SLANG_DIR)/mh_synth.f --top idma_backend_synth_$$id \ + $(IDMA_SLANG_ARGS); \ + done + set -e; for tb in tb_idma_backend_multihead tb_idma_backend_multihead_rw; do \ + echo "--- slang $$tb ---"; \ + $(SLANG) -f $(IDMA_SLANG_DIR)/mh_tb.f --top $$tb \ + $(IDMA_SLANG_ARGS) $(IDMA_SLANG_TB_ARGS); \ + done + rm -f $(IDMA_FULL_RTL) $(IDMA_FULL_TB) + $(MAKE) idma_hw_all + @! grep -qE '$(subst $() ,|,$(IDMA_MULTIHEAD_PAT))' $(IDMA_FULL_RTL) || \ + { echo 'error: add ids leaked into $(IDMA_FULL_RTL)'; exit 1; } + + +# --------------- +# Public simulation (verilator) +# --------------- + +IDMA_VLT_SIM_T := -t rtl -t idma_test -t simulation -t synth +IDMA_VLT_MAKEFLAGS ?= + +# verilator --timing lowers to C++20 coroutines; g++ 11 miscompiles them +IDMA_VLT_CXX = $(shell for c in g++-14 g++-13 g++-13.2.0 g++-12 g++; do \ + command -v $$c >/dev/null 2>&1 && { echo $$c; break; }; done) +IDMA_VLT_CXX_MAJOR = $(shell $(IDMA_VLT_CXX) -dumpversion 2>/dev/null | cut -d. -f1) + +# Fail here; the symptom otherwise is a SIGSEGV with an empty log +.PHONY: idma_verify_toolchain +idma_verify_toolchain: + @test -n "$(IDMA_VLT_CXX)" || { echo "error: no g++ found"; exit 1; } + @test "$(IDMA_VLT_CXX_MAJOR)" -ge 12 2>/dev/null || { \ + echo "error: $(IDMA_VLT_CXX) is g++ $(IDMA_VLT_CXX_MAJOR); the coroutine"; \ + echo " lowering needs g++ 12 or newer. Set IDMA_VLT_CXX=."; \ + exit 1; } + +IDMA_VERIFY_RUN = VERILATOR="$(VERILATOR)" \ + IDMA_VLT_MAKEFLAGS="CXX=$(IDMA_VLT_CXX) LINK=$(IDMA_VLT_CXX) $(IDMA_VLT_MAKEFLAGS)" \ + $(PYTHON) $(IDMA_UTIL_DIR)/run_verify.py --vlt-dir $(IDMA_VLT_DIR) + +$(IDMA_VLT_DIR)/%.f: $(IDMA_BENDER_FILES) $(IDMA_FULL_RTL) $(IDMA_FULL_TB) $(IDMA_INCLUDE_ALL) + mkdir -p $(IDMA_VLT_DIR) + $(BENDER) script verilator $(IDMA_VLT_SIM_T) --top $* > $@ + +# idma_mxquant_dpi and idma_transpose_dpi both export gm_load/gm_get; never link both +$(IDMA_VLT_DIR)/%_dpi.o: $(IDMA_ROOT)/test/%_dpi.c + mkdir -p $(IDMA_VLT_DIR) + $(CC) -c -O2 -fPIC $< -o $@ + +# One rule for every suite; the database names the top and the DPI object +idma_verify_sim_%: idma_verify_toolchain + @p=$$($(PYTHON) $(IDMA_UTIL_DIR)/run_verify.py --prereqs $* --vlt-dir $(IDMA_VLT_DIR)) && \ + test -n "$$p" && $(MAKE) $$p + $(IDMA_VERIFY_RUN) --suite $* + + +# --------------- +# Codegen consistency and advisory report +# --------------- + +IDMA_GEN_FILES := $(IDMA_RTL_ALL) $(IDMA_TB_ALL) $(IDMA_FULL_RTL) $(IDMA_FULL_TB) \ + $(IDMA_INCLUDE_ALL) $(IDMA_WAVE_ALL) + +# Not verification; no zero-git-diff form, target/rtl is gitignored here +idma_verify_codegen: + $(MAKE) idma_hw_all + $(PYTHON) $(IDMA_UTIL_DIR)/check_jobs.py --ids "$(IDMA_BACKEND_IDS)" \ + --jobs $(IDMA_ROOT)/$(IDMA_JOBS_JSON) --jobs-dir $(IDMA_ROOT)/jobs \ + --verify-db $(IDMA_VERIFY_DB) \ + --matrix-file $(IDMA_ROOT)/.github/workflows/verify.yml \ + --mxneg-tb $(IDMA_ROOT)/test/tb_idma_mxneg.sv \ + --mxneg-guard-src $(IDMA_ROOT)/src/backend/tpl/idma_legalizer.sv.tpl \ + $(IDMA_SOURCE_GLOBS) + mkdir -p $(IDMA_VERIFY_DIR) + set -o pipefail; md5sum $(IDMA_GEN_FILES) | sort -k2 > $(IDMA_VERIFY_DIR)/gen1.md5 + $(MAKE) idma_rtl_clean idma_reg_clean + $(MAKE) idma_hw_all + set -o pipefail; md5sum $(IDMA_GEN_FILES) | sort -k2 > $(IDMA_VERIFY_DIR)/gen2.md5 + diff -u $(IDMA_VERIFY_DIR)/gen1.md5 $(IDMA_VERIFY_DIR)/gen2.md5 + +# Advisory only; never add to the required checks +idma_slang_report: $(IDMA_SLANG_DIR)/synth.f + mkdir -p $(IDMA_VERIFY_DIR) + : > $(IDMA_VERIFY_DIR)/slang_extra.log + set -e; for top in $(IDMA_LINT_TOPS); do \ + $(SLANG) -f $(IDMA_SLANG_DIR)/synth.f --top $$top -Wextra --error-limit 0 \ + >> $(IDMA_VERIFY_DIR)/slang_extra.log 2>&1; \ + done + awk '/ (warning|error): /' $(IDMA_VERIFY_DIR)/slang_extra.log | \ + awk '!/^\.bender\//' | sort -u > $(IDMA_VERIFY_DIR)/slang_extra.txt + @echo "slang -Wextra: $$(wc -l < $(IDMA_VERIFY_DIR)/slang_extra.txt) unique iDMA findings" + +# The suite list comes from the database, like the CI fan-out +idma_verify_all: idma_verify_codegen idma_verify_shared idma_verify_tb_shared \ + idma_verify_multihead $(IDMA_VERIFY_DIR)/suites.list + @test -s $(IDMA_VERIFY_DIR)/suites.list || \ + { echo "error: no suites listed"; exit 1; } + set -e; for s in $$(cat $(IDMA_VERIFY_DIR)/suites.list); do \ + $(MAKE) idma_verify_sim_$$s; \ + done + set -e; for id in $(IDMA_BACKEND_IDS); do \ + $(MAKE) idma_verify_backend IDMA_VERIFY_ID=$$id; \ + done + +idma_verify_clean: + rm -rf $(IDMA_VERIFY_DIR) + rm -rf $(IDMA_SLANG_DIR)