From 410d546a37ac8e1101666eae9b31aa7a5b7e8b6a Mon Sep 17 00:00:00 2001 From: AnastasiyaChernikova Date: Thu, 23 Apr 2026 14:37:21 +0000 Subject: [PATCH 1/7] [dv] Add RV32ZC handling to ISA strings Use the RV32ZC configuration parameter when constructing the ISA string passed to the cosimulator. This lets DV runs distinguish between Zca, ZcaZcb, ZcaZcmp and ZcaZcbZcmp configurations. --- dv/uvm/core_ibex/scripts/ibex_cmd.py | 25 ++++++++++++++++--- dv/uvm/core_ibex/tb/core_ibex_tb_top.sv | 7 ++++++ dv/uvm/core_ibex/tests/core_ibex_base_test.sv | 22 ++++++++++++++-- 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/dv/uvm/core_ibex/scripts/ibex_cmd.py b/dv/uvm/core_ibex/scripts/ibex_cmd.py index 0d8c5346e2..c4ec6f25e7 100644 --- a/dv/uvm/core_ibex/scripts/ibex_cmd.py +++ b/dv/uvm/core_ibex/scripts/ibex_cmd.py @@ -91,13 +91,27 @@ def get_config(cfg_name: str) -> Config: return parse_config(cfg_name, yaml_path) +def _rv32zc_isa_parts(rv32zc: str) -> list[str]: + mapping = { + 'ibex_pkg::RV32Zca': ['zicsr', 'zifencei', 'zca'], + 'ibex_pkg::RV32ZcaZcb': ['zicsr', 'zifencei', 'zca', 'zcb'], + 'ibex_pkg::RV32ZcaZcmp': ['zicsr', 'zifencei', 'zca', 'zcmp'], + 'ibex_pkg::RV32ZcaZcbZcmp': ['zicsr', 'zifencei', 'zca', 'zcb', 'zcmp'], + } + part = mapping.get(rv32zc) + if part is None: + raise ValueError(f'Unknown RV32ZC value ({rv32zc}) in config YAML') + return part + + def get_isas_for_config(cfg: Config) -> Tuple[str, str]: """Get ISA and ISS_ISA keys for the given Ibex config.""" # NOTE: This logic should match the code in the get_isa_string() function # in core_ibex/tests/core_ibex_base_test.sv: keep them in sync! has_multiplier = cfg.rv32m != 'ibex_pkg::RV32MNone' - base_isa = 'rv32{}{}c'.format('e' if cfg.rv32e else 'i', - 'm' if has_multiplier else '') + + base_isa = 'rv32{}{}'.format('e' if cfg.rv32e else 'i', + 'm' if has_multiplier else '') bitmanip_mapping = { 'ibex_pkg::RV32BNone': [], @@ -113,9 +127,12 @@ def get_isas_for_config(cfg: Config) -> Tuple[str, str]: raise ValueError(f'Unknown RV32B value ({cfg.rv32b}) in config YAML') has_bitmanip = cfg.rv32b != 'ibex_pkg::RV32BNone' - toolchain_isa = base_isa + ('b' if has_bitmanip else '') + # Keep the toolchain ISA using the legacy C extension spelling, but build + # the ISS ISA from the explicit RV32ZC subset below. + toolchain_isa = base_isa + 'c' + ('b' if has_bitmanip else '') + iss_isa = '_'.join([base_isa] + _rv32zc_isa_parts(cfg.rv32zc) + bitmanip_isa) - return (toolchain_isa, '_'.join([base_isa] + bitmanip_isa)) + return (toolchain_isa, iss_isa) _TestEntry = Dict[str, object] diff --git a/dv/uvm/core_ibex/tb/core_ibex_tb_top.sv b/dv/uvm/core_ibex/tb/core_ibex_tb_top.sv index a2658d1239..0e13f4ab7e 100644 --- a/dv/uvm/core_ibex/tb/core_ibex_tb_top.sv +++ b/dv/uvm/core_ibex/tb/core_ibex_tb_top.sv @@ -52,6 +52,10 @@ module core_ibex_tb_top; `define IBEX_CFG_RegFile ibex_pkg::RegFileFF `endif + `ifndef IBEX_CFG_RV32ZC + `define IBEX_CFG_RV32ZC ibex_pkg::RV32ZcaZcbZcmp + `endif + // Ibex Parameters parameter bit PMPEnable = 1'b0; parameter int unsigned PMPGranularity = 0; @@ -62,6 +66,7 @@ module core_ibex_tb_top; parameter ibex_pkg::rv32m_e RV32M = `IBEX_CFG_RV32M; parameter ibex_pkg::rv32b_e RV32B = `IBEX_CFG_RV32B; parameter ibex_pkg::regfile_e RegFile = `IBEX_CFG_RegFile; + parameter ibex_pkg::rv32zc_e RV32ZC = `IBEX_CFG_RV32ZC; parameter bit BranchTargetALU = 1'b0; parameter bit WritebackStage = 1'b0; parameter bit ICache = 1'b0; @@ -103,6 +108,7 @@ module core_ibex_tb_top; .RV32E (RV32E ), .RV32M (RV32M ), .RV32B (RV32B ), + .RV32ZC (RV32ZC ), .RegFile (RegFile ), .BranchTargetALU (BranchTargetALU ), .WritebackStage (WritebackStage ), @@ -370,6 +376,7 @@ module core_ibex_tb_top; uvm_config_db#(bit)::set(null, "*", "RV32E", RV32E); uvm_config_db#(ibex_pkg::rv32m_e)::set(null, "*", "RV32M", RV32M); uvm_config_db#(ibex_pkg::rv32b_e)::set(null, "*", "RV32B", RV32B); + uvm_config_db#(ibex_pkg::rv32zc_e)::set(null, "*", "RV32ZC", RV32ZC); if (PMPEnable) begin uvm_config_db#(bit [31:0])::set(null, "*", "PMPNumRegions", PMPNumRegions); diff --git a/dv/uvm/core_ibex/tests/core_ibex_base_test.sv b/dv/uvm/core_ibex/tests/core_ibex_base_test.sv index 47847ad671..7782961a1a 100644 --- a/dv/uvm/core_ibex/tests/core_ibex_base_test.sv +++ b/dv/uvm/core_ibex/tests/core_ibex_base_test.sv @@ -47,12 +47,26 @@ class core_ibex_base_test extends uvm_test; irq_collected_port = new("irq_collected_port_test", this); endfunction + function automatic string rv32zc_isa_suffix(rv32zc_e rv32zc); + case (rv32zc) + RV32Zca: return "_zicsr_zifencei_zca"; + RV32ZcaZcb: return "_zicsr_zifencei_zca_zcb"; + RV32ZcaZcmp: return "_zicsr_zifencei_zca_zcmp"; + RV32ZcaZcbZcmp: return "_zicsr_zifencei_zca_zcb_zcmp"; + default: begin + `uvm_fatal(`gfn, $sformatf("Unknown RV32ZC value: %0d", rv32zc)) + return ""; + end + endcase + endfunction + // NOTE: This logic should match the code in the get_isas_for_config() function in - // core_ibex/scripts/scripts_lib.py: keep them in sync! + // core_ibex/scripts/ibex_cmd.py: keep them in sync! function string get_isa_string(); bit RV32E; rv32m_e RV32M; rv32b_e RV32B; + rv32zc_e RV32ZC; string isa; if (!uvm_config_db#(bit)::get(null, "", "RV32E", RV32E)) begin @@ -64,12 +78,16 @@ class core_ibex_base_test extends uvm_test; if (!uvm_config_db#(rv32b_e)::get(null, "", "RV32B", RV32B)) begin `uvm_fatal(`gfn, "Cannot get RV32B parameter") end + if (!uvm_config_db#(rv32zc_e)::get(null, "", "RV32ZC", RV32ZC)) begin + `uvm_fatal(`gfn, "Cannot get RV32ZC parameter") + end // Construct the right ISA string for the cosimulator by looking at top-level testbench // parameters. isa = {"rv32", RV32E ? "e" : "i"}; if (RV32M != RV32MNone) isa = {isa, "m"}; - isa = {isa, "c"}; + isa = {isa, rv32zc_isa_suffix(RV32ZC)}; + case (RV32B) RV32BNone: ; From d4e11c33504b32b5300cf6efac5f7dec483c5bd7 Mon Sep 17 00:00:00 2001 From: AnastasiyaChernikova Date: Thu, 4 Jun 2026 13:01:44 +0000 Subject: [PATCH 2/7] [dv] Allow overriding RV32ZC from core_ibex Make Add an optional RV32ZC Make variable that overrides the compressed extension subset from the selected Ibex configuration. Thread this value through regression metadata into compile and simulation option generation, and teach ibex_config.py to apply validated FIELD=VALUE overrides via --set. This keeps the default IBEX_CONFIG behaviour unchanged while allowing the DV flow to build and run the same configuration with a different RV32ZC subset when needed. --- dv/uvm/core_ibex/Makefile | 10 +++ dv/uvm/core_ibex/scripts/compile_tb.py | 2 +- dv/uvm/core_ibex/scripts/ibex_cmd.py | 90 ++++++++++++++++++++----- dv/uvm/core_ibex/scripts/metadata.py | 3 +- dv/uvm/core_ibex/scripts/run_rtl.py | 2 +- util/ibex_config.py | 92 ++++++++++++++++++++++++++ 6 files changed, 178 insertions(+), 21 deletions(-) diff --git a/dv/uvm/core_ibex/Makefile b/dv/uvm/core_ibex/Makefile index 69909cacb2..2f1b8724f4 100644 --- a/dv/uvm/core_ibex/Makefile +++ b/dv/uvm/core_ibex/Makefile @@ -52,6 +52,15 @@ IBEX_CONFIG := opentitan # Path to DUT used for coverage reports DUT_COV_RTL_PATH := "ibex_top" +# Optional override for compressed extension subset. +# Empty means: use RV32ZC from IBEX_CONFIG in ibex_configs.yaml. +# Allowed values: +# ibex_pkg::RV32Zca +# ibex_pkg::RV32ZcaZcb +# ibex_pkg::RV32ZcaZcmp +# ibex_pkg::RV32ZcaZcbZcmp +RV32ZC := + export EXTRA_COSIM_CFLAGS ?= ifeq ($(COSIM_SIGSEGV_WORKAROUND), 1) @@ -76,6 +85,7 @@ run: SEED=$(SEED) WAVES=$(WAVES) COV=$(COV) SIMULATOR=$(SIMULATOR) \ ISS=$(ISS) TEST=$(TEST) VERBOSE=$(VERBOSE) ITERATIONS=$(ITERATIONS) \ SIGNATURE_ADDR=$(SIGNATURE_ADDR) IBEX_CONFIG=$(IBEX_CONFIG) \ + RV32ZC=$(RV32ZC) \ DUT_COV_RTL_PATH=$(DUT_COV_RTL_PATH)" @$(MAKE) --file wrapper.mk --environment-overrides --no-print-directory $(GOAL) diff --git a/dv/uvm/core_ibex/scripts/compile_tb.py b/dv/uvm/core_ibex/scripts/compile_tb.py index 871d8a49c4..631f163151 100755 --- a/dv/uvm/core_ibex/scripts/compile_tb.py +++ b/dv/uvm/core_ibex/scripts/compile_tb.py @@ -115,7 +115,7 @@ def _main() -> int: 'tb_dir': md.dir_tb, 'tb_build_log': md.tb_build_log, 'cmp_opts': - get_compile_opts(md.ibex_config, md.simulator) + \ + get_compile_opts(md.ibex_config, md.simulator, md.rv32zc) + \ # Base address of the debug module. This is passed as a parameter # at compile time as the PMP module must always allow debug mode # accesses to it. diff --git a/dv/uvm/core_ibex/scripts/ibex_cmd.py b/dv/uvm/core_ibex/scripts/ibex_cmd.py index c4ec6f25e7..3934bbaf2f 100644 --- a/dv/uvm/core_ibex/scripts/ibex_cmd.py +++ b/dv/uvm/core_ibex/scripts/ibex_cmd.py @@ -30,13 +30,23 @@ class GenError(Exception): pass -def _run_ibex_config(config_name: str, output_type: str) -> str: +def _run_ibex_config(config_name: str, + output_type: str, + overrides: Dict[str, str] = None) -> str: + if overrides is None: + overrides = {} script_path = _IBEX_ROOT/'util'/'ibex_config.py' yaml_path = _IBEX_ROOT/'ibex_configs.yaml' ibex_config_cmd = [ str(script_path), '--config_filename', str(yaml_path), + ] + + for fld, val in overrides.items(): + ibex_config_cmd += ['--set', f'{fld}={val}'] + + ibex_config_cmd += [ config_name, output_type, '--ins_hier_path', 'core_ibex_tb_top', @@ -54,7 +64,10 @@ def _run_ibex_config(config_name: str, output_type: str) -> str: return proc.stdout.strip() -def _get_x_opts(config_name: str, simulator: str, stage: str) -> str: +def _get_x_opts(config_name: str, + simulator: str, + stage: str, + overrides: Dict[str, str] = None) -> str: try: needs_compile_opts, needs_sim_opts = SIM_CFGS[simulator] except KeyError: @@ -75,15 +88,29 @@ def _get_x_opts(config_name: str, simulator: str, stage: str) -> str: output_type = (f'{simulator}_{stage}_opts' if specify_which_opts else f'{simulator}_opts') - return _run_ibex_config(config_name, output_type) + return _run_ibex_config(config_name, output_type, overrides) -def get_compile_opts(config_name: str, simulator: str) -> str: - return _get_x_opts(config_name, simulator, 'compile') +def get_compile_opts(config_name: str, + simulator: str, + rv32zc: str = '') -> str: + return _get_x_opts( + config_name, + simulator, + 'compile', + get_config_overrides(rv32zc) + ) -def get_sim_opts(config_name: str, simulator: str) -> str: - return _get_x_opts(config_name, simulator, 'sim') +def get_sim_opts(config_name: str, + simulator: str, + rv32zc: str = '') -> str: + return _get_x_opts( + config_name, + simulator, + 'sim', + get_config_overrides(rv32zc) + ) def get_config(cfg_name: str) -> Config: @@ -91,17 +118,44 @@ def get_config(cfg_name: str) -> Config: return parse_config(cfg_name, yaml_path) -def _rv32zc_isa_parts(rv32zc: str) -> list[str]: - mapping = { - 'ibex_pkg::RV32Zca': ['zicsr', 'zifencei', 'zca'], - 'ibex_pkg::RV32ZcaZcb': ['zicsr', 'zifencei', 'zca', 'zcb'], - 'ibex_pkg::RV32ZcaZcmp': ['zicsr', 'zifencei', 'zca', 'zcmp'], - 'ibex_pkg::RV32ZcaZcbZcmp': ['zicsr', 'zifencei', 'zca', 'zcb', 'zcmp'], - } - part = mapping.get(rv32zc) - if part is None: - raise ValueError(f'Unknown RV32ZC value ({rv32zc}) in config YAML') - return part +RV32ZC_VALUES = { + 'ibex_pkg::RV32Zca': ['zicsr', 'zifencei', 'zca'], + 'ibex_pkg::RV32ZcaZcb': ['zicsr', 'zifencei', 'zca', 'zcb'], + 'ibex_pkg::RV32ZcaZcmp': ['zicsr', 'zifencei', 'zca', 'zcmp'], + 'ibex_pkg::RV32ZcaZcbZcmp': ['zicsr', 'zifencei', 'zca', 'zcb', 'zcmp'], +} + + +def check_rv32zc_value(rv32zc: str) -> None: + if rv32zc not in RV32ZC_VALUES: + allowed = ', '.join(RV32ZC_VALUES) + raise ValueError( + f'Unknown RV32ZC value: {rv32zc}. Expected one of: {allowed}.' + ) + + +def get_config_overrides(rv32zc: str = '') -> Dict[str, str]: + overrides = {} + + if rv32zc: + check_rv32zc_value(rv32zc) + overrides['RV32ZC'] = rv32zc + + return overrides + + +def get_effective_config(cfg_name: str, rv32zc: str = '') -> Config: + cfg = get_config(cfg_name) + + for fld, val in get_config_overrides(rv32zc).items(): + cfg.override(fld, val) + + return cfg + + +def _rv32zc_isa_parts(rv32zc: str) -> List[str]: + check_rv32zc_value(rv32zc) + return RV32ZC_VALUES[rv32zc] def get_isas_for_config(cfg: Config) -> Tuple[str, str]: diff --git a/dv/uvm/core_ibex/scripts/metadata.py b/dv/uvm/core_ibex/scripts/metadata.py index c9d28bc792..21d61fb4c2 100755 --- a/dv/uvm/core_ibex/scripts/metadata.py +++ b/dv/uvm/core_ibex/scripts/metadata.py @@ -67,6 +67,7 @@ class RegressionMetadata(scripts_lib.testdata_cls): signature_addr: str = ' ' ibex_config: str = ' ' dut_cov_rtl_path: str = '' + rv32zc: str = '' tests_and_counts: List[Tuple[str, int, TestType]] = field(default_factory=list) isa_ibex: Optional[str] = None @@ -167,7 +168,7 @@ def _get_ibex_metadata(self): if self.seed < 0: raise RuntimeError('Bad --start_seed argument: must be non-negative') - cfg = ibex_cmd.get_config(self.ibex_config) + cfg = ibex_cmd.get_effective_config(self.ibex_config, self.rv32zc) self.isa_ibex, self.isa_iss = ibex_cmd.get_isas_for_config(cfg) @classmethod diff --git a/dv/uvm/core_ibex/scripts/run_rtl.py b/dv/uvm/core_ibex/scripts/run_rtl.py index 202997219d..60c7423197 100755 --- a/dv/uvm/core_ibex/scripts/run_rtl.py +++ b/dv/uvm/core_ibex/scripts/run_rtl.py @@ -72,7 +72,7 @@ def _main() -> int: 'core_ibex': md.ibex_dv_root, 'sim_opts': (f"+signature_addr={md.signature_addr}\n" + f"+test_timeout_s={trr.timeout_s}\n" + - f"{get_sim_opts(md.ibex_config, md.simulator)}\n" + + f"{get_sim_opts(md.ibex_config, md.simulator, md.rv32zc)}\n" + trr.ddm_sim_opts + sim_opts) } diff --git a/util/ibex_config.py b/util/ibex_config.py index c568c9d085..22a7ed3569 100755 --- a/util/ibex_config.py +++ b/util/ibex_config.py @@ -41,6 +41,15 @@ class Config: ('MHPMCounterWidth', int) ] + known_values = { + 'RV32ZC': [ + 'ibex_pkg::RV32Zca', + 'ibex_pkg::RV32ZcaZcb', + 'ibex_pkg::RV32ZcaZcmp', + 'ibex_pkg::RV32ZcaZcbZcmp', + ], + } + def __init__(self, yml): if not isinstance(yml, dict): raise ValueError('Configuration object is not a dict') @@ -79,6 +88,71 @@ def __init__(self, yml): self.mhpm_counter_num = Config.read_int('MHPMCounterNum', yml) self.mhpm_counter_width = Config.read_int('MHPMCounterWidth', yml) + @staticmethod + def parse_override_value(fld, typ, val): + if typ is str: + return val + + if typ is int: + try: + return int(val, 0) + except ValueError: + raise ConfigException( + f'{fld} override value is {val!r}, but we expected an int.' + ) from None + + if typ is bool: + if val in ['0', 'false', 'False']: + return False + if val in ['1', 'true', 'True']: + return True + raise ConfigException( + f'{fld} override value is {val!r}, but we expected a bool.' + ) + + raise ConfigException(f'{fld} has unsupported type {typ!r}.') + + def override(self, fld, val): + known_types = dict(Config.known_fields) + + if fld not in known_types: + raise ConfigException(f'Unknown configuration field {fld!r}.') + + parsed_val = Config.parse_override_value(fld, known_types[fld], val) + + if fld in Config.known_values and parsed_val not in Config.known_values[fld]: + allowed = ', '.join(Config.known_values[fld]) + raise ConfigException( + f'{fld} override value is {parsed_val!r}, but expected one of: ' + f'{allowed}.' + ) + + self.params[fld] = parsed_val + + # Keep the convenience attributes in sync with params. + attr_name = { + 'RV32E': 'rv32e', + 'RV32M': 'rv32m', + 'RV32B': 'rv32b', + 'RV32ZC': 'rv32zc', + 'RegFile': 'reg_file', + 'BranchTargetALU': 'branch_target_alu', + 'WritebackStage': 'writeback_stage', + 'ICache': 'icache', + 'ICacheECC': 'icache_ecc', + 'ICacheScramble': 'icache_scramble', + 'BranchPredictor': 'branch_predictor', + 'DbgTriggerEn': 'dbg_trigger_en', + 'SecureIbex': 'secure_ibex', + 'PMPEnable': 'pmp_enable', + 'PMPGranularity': 'pmp_granularity', + 'PMPNumRegions': 'pmp_num_regions', + 'MHPMCounterNum': 'mhpm_counter_num', + 'MHPMCounterWidth': 'mhpm_counter_width', + }[fld] + + setattr(self, attr_name, parsed_val) + @staticmethod def read_bool(fld, yml): val = yml[fld] @@ -295,6 +369,13 @@ def main(): argparser.add_argument('--config_filename', help='Config file to read', default=get_config_file_location()) + argparser.add_argument('--set', + dest='overrides', + action='append', + default=[], + metavar='FIELD=VALUE', + help='Override a configuration field after reading ' + 'the named config. Can be passed multiple times.') arg_subparser = argparser.add_subparsers( help='Format to output the configuration parameters in', @@ -311,6 +392,17 @@ def main(): sys.exit(1) parsed_ibex_config = parse_config(args.config_name, args.config_filename) + + for override in args.overrides: + try: + fld, val = override.split('=', 1) + except ValueError: + raise ConfigException( + f'Invalid override {override!r}; expected FIELD=VALUE.' + ) from None + + parsed_ibex_config.override(fld, val) + print(args.output_fn(parsed_ibex_config, args)) if __name__ == "__main__": From ca45a9accdc70792ebe2fcc70b00cb698562c422 Mon Sep 17 00:00:00 2001 From: AnastasiyaChernikova Date: Thu, 4 Jun 2026 13:04:31 +0000 Subject: [PATCH 3/7] [dv] Add external ELF manifest loading support Add a small ELF-to-manifest conversion utility and a UVM test that can load the generated manifest into both the DUT memory model and the cosim memory before releasing fetch. This provides a generic path for running prebuilt ELF images without going through the existing raw binary loader. The test remains compatible with the existing +bin flow, but also accepts +elf_manifest= for externally generated programs. --- .../riscv_dv_extension/snippy_elf_parser.py | 249 +++++++++++ .../tests/core_ibex_elf_manifest_test.sv | 413 ++++++++++++++++++ dv/uvm/core_ibex/tests/core_ibex_test_pkg.sv | 1 + 3 files changed, 663 insertions(+) create mode 100644 dv/uvm/core_ibex/riscv_dv_extension/snippy_elf_parser.py create mode 100644 dv/uvm/core_ibex/tests/core_ibex_elf_manifest_test.sv diff --git a/dv/uvm/core_ibex/riscv_dv_extension/snippy_elf_parser.py b/dv/uvm/core_ibex/riscv_dv_extension/snippy_elf_parser.py new file mode 100644 index 0000000000..a4dcda2a58 --- /dev/null +++ b/dv/uvm/core_ibex/riscv_dv_extension/snippy_elf_parser.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +""" +Convert ELF PT_LOAD segments into manifests. + +The script can generate: +1. a JSON manifest for debugging; +2. a simple SystemVerilog-friendly text manifest for UVM loading. + +Requires pyelftools. + +Examples: + python3 snippy_elf_parser.py test.elf --json-out test.json + python3 snippy_elf_parser.py test.elf --sv-out test.svload + python3 snippy_elf_parser.py test.elf --json-out test.json --sv-out test.svload +""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path +from typing import Any + +from elftools.elf.elffile import ELFFile + + +def flags_to_string(p_flags: int) -> str: + chars = [ + "R" if (p_flags & 0x4) else "-", + "W" if (p_flags & 0x2) else "-", + "X" if (p_flags & 0x1) else "-", + ] + return "".join(chars) + + +def choose_load_addr(segment: Any, prefer_paddr: bool = True) -> int: + p_paddr = int(segment["p_paddr"]) + p_vaddr = int(segment["p_vaddr"]) + if prefer_paddr and p_paddr != 0: + return p_paddr + return p_vaddr + + +def section_in_segment(section: Any, segment: Any) -> bool: + sec_size = int(section["sh_size"]) + sec_addr = int(section["sh_addr"]) + + if sec_size == 0: + return False + + seg_addr = int(segment["p_vaddr"]) + seg_memsz = int(segment["p_memsz"]) + + if seg_memsz == 0: + return False + + seg_end = seg_addr + seg_memsz + sec_end = sec_addr + sec_size + + return sec_addr >= seg_addr and sec_end <= seg_end + + +def collect_section_names(elf: ELFFile, segment: Any) -> list[str]: + names: list[str] = [] + for section in elf.iter_sections(): + flags = int(section["sh_flags"]) + shf_alloc = 0x2 # SHF_ALLOC + if not (flags & shf_alloc): + continue + if section_in_segment(section, segment): + names.append(section.name) + return names + + +def parse_elf(elf_path: Path, prefer_paddr: bool = True) -> dict[str, Any]: + with elf_path.open("rb") as f: + elf = ELFFile(f) + + manifest: dict[str, Any] = { + "format_version": 1, + "elf_path": str(elf_path), + "elf_class": elf.elfclass, + "little_endian": elf.little_endian, + "machine": elf["e_machine"], + "entry": int(elf.header["e_entry"]), + "segments": [], + } + + for idx, segment in enumerate(elf.iter_segments()): + if segment["p_type"] != "PT_LOAD": + continue + + file_size = int(segment["p_filesz"]) + mem_size = int(segment["p_memsz"]) + load_addr = choose_load_addr(segment, prefer_paddr=prefer_paddr) + data = segment.data() + + if len(data) != file_size: + raise ValueError( + f"Segment {idx}: segment.data() length {len(data)} != p_filesz {file_size}" + ) + if mem_size < file_size: + raise ValueError( + f"Segment {idx}: invalid ELF, p_memsz ({mem_size}) < p_filesz ({file_size})" + ) + + seg_info = { + "index": idx, + "load_addr": load_addr, + "vaddr": int(segment["p_vaddr"]), + "paddr": int(segment["p_paddr"]), + "offset": int(segment["p_offset"]), + "file_size": file_size, + "mem_size": mem_size, + "zero_fill": mem_size - file_size, + "align": int(segment["p_align"]), + "flags": flags_to_string(int(segment["p_flags"])), + "section_names": collect_section_names(elf, segment), + "data_hex": data.hex(), + } + manifest["segments"].append(seg_info) + + return manifest + + +def print_summary(manifest: dict[str, Any]) -> None: + print(f"ELF file : {manifest['elf_path']}") + print(f"Machine : {manifest['machine']}") + print(f"ELF class : ELF{manifest['elf_class']}") + print(f"Little endian : {manifest['little_endian']}") + print(f"Entry point : 0x{manifest['entry']:08x}") + print() + + segments = manifest["segments"] + if not segments: + print("No PT_LOAD segments found.") + return + + print("Loadable segments:") + for seg in segments: + print(f" Segment #{seg['index']}") + print(f" load_addr : 0x{seg['load_addr']:08x}") + print(f" vaddr : 0x{seg['vaddr']:08x}") + print(f" paddr : 0x{seg['paddr']:08x}") + print(f" offset : 0x{seg['offset']:08x}") + print(f" file_size : {seg['file_size']}") + print(f" mem_size : {seg['mem_size']}") + print(f" zero_fill : {seg['zero_fill']}") + print(f" flags : {seg['flags']}") + print(f" align : {seg['align']}") + print(f" sections : {', '.join(seg['section_names']) if seg['section_names'] else '-'}") + + preview_len = min(16, seg["file_size"]) + preview_hex = seg["data_hex"][: preview_len * 2] + print(f" data preview : {preview_hex}") + print() + + +def write_json_manifest(manifest: dict[str, Any], out_path: Path) -> None: + out_path.write_text(json.dumps(manifest, indent=2)) + print(f"JSON manifest written to: {out_path}") + + +def write_sv_manifest(manifest: dict[str, Any], out_path: Path) -> None: + """ + Write a line-oriented manifest for the SystemVerilog loader. + + Format: + ENTRY,0x80000000 + SEGMENT,,,, + SECTION, + BYTE,0xNN + BYTE,0xNN + ... + ZERO,0x00000010 + + Semantics: + - SEGMENT starts a new loadable segment. + - SECTION records section names for debug messages. + - BYTE lines are written sequentially from the current segment load address. + - ZERO appends N zero bytes after the file-backed segment data. + """ + with out_path.open("w", encoding="utf-8") as f: + f.write(f"ENTRY,0x{manifest['entry']:08x}\n") + + for seg in manifest["segments"]: + f.write( + f"SEGMENT,0x{seg['load_addr']:08x}," + f"0x{seg['file_size']:08x}," + f"0x{seg['mem_size']:08x}," + f"{seg['flags']}\n" + ) + + for sec_name in seg["section_names"]: + f.write(f"SECTION,{sec_name}\n") + + data_hex = seg["data_hex"] + for i in range(0, len(data_hex), 2): + byte_hex = data_hex[i:i + 2] + f.write(f"BYTE,0x{byte_hex}\n") + + if seg["zero_fill"] > 0: + f.write(f"ZERO,0x{seg['zero_fill']:08x}\n") + + print(f"SV manifest written to: {out_path}") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Convert ELF PT_LOAD segments into JSON and/or SV-friendly manifest" + ) + parser.add_argument("elf", type=Path, help="Input ELF file") + parser.add_argument("--json-out", type=Path, default=None, help="Output JSON manifest path") + parser.add_argument("--sv-out", type=Path, default=None, help="Output SV text manifest path") + parser.add_argument( + "--prefer-vaddr", + action="store_true", + help="Use p_vaddr for load_addr even if p_paddr is non-zero", + ) + args = parser.parse_args() + + if not args.elf.is_file(): + print(f"Error: ELF file not found: {args.elf}", file=sys.stderr) + return 2 + + if args.json_out is None and args.sv_out is None: + print("Error: specify at least one of --json-out or --sv-out", file=sys.stderr) + return 2 + + try: + manifest = parse_elf(args.elf, prefer_paddr=not args.prefer_vaddr) + except Exception as exc: + print(f"Error while parsing ELF: {exc}", file=sys.stderr) + return 1 + + print_summary(manifest) + + if args.json_out is not None: + write_json_manifest(manifest, args.json_out) + + if args.sv_out is not None: + write_sv_manifest(manifest, args.sv_out) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/dv/uvm/core_ibex/tests/core_ibex_elf_manifest_test.sv b/dv/uvm/core_ibex/tests/core_ibex_elf_manifest_test.sv new file mode 100644 index 0000000000..cb6ae4a872 --- /dev/null +++ b/dv/uvm/core_ibex/tests/core_ibex_elf_manifest_test.sv @@ -0,0 +1,413 @@ +// Copyright lowRISC contributors. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +class core_ibex_elf_manifest_test extends core_ibex_base_test; + + string elf_manifest; + bit use_elf_manifest = 1'b0; + + `uvm_component_utils(core_ibex_elf_manifest_test) + + function new(string name="", uvm_component parent=null); + super.new(name, parent); + endfunction + + // Copy of core_ibex_base_test::build_phase() with ELF-manifest specific input handling. + virtual function void build_phase(uvm_phase phase); + string cosim_log_file; + bit [31:0] pmp_num_regions; + bit [31:0] pmp_granularity; + bit [31:0] mhpm_counter_num; + bit secure_ibex; + bit icache; + bit disable_spurious_dside_responses; + + // Check the test binary/binaries have been passed correctly + void'($value$plusargs("bin=%0s", binary)); + void'($value$plusargs("bin_main=%0s", bin_main)); + void'($value$plusargs("bin_dm=%0s", bin_dm)); + void'($value$plusargs("vmem_main=%0s", vmem_main)); + void'($value$plusargs("vmem_dm=%0s", vmem_dm)); + void'($value$plusargs("elf_manifest=%0s", elf_manifest)); + + if (elf_manifest != "") begin + use_elf_manifest = 1'b1; + `uvm_info(`gfn, $sformatf("Using ELF manifest: %0s", elf_manifest), UVM_LOW) + end + + // If the 'discrete_debug_module' plusarg is set, we will have two binaries per test, one for + // the main memory region, and one for the debug module. + void'($value$plusargs("discrete_debug_module=%b", discrete_debug_module)); + if (!discrete_debug_module) begin + // Single binary mode: + // either old raw binary flow via +bin + // or new ELF manifest flow via +elf_manifest + if ((binary == "") && !use_elf_manifest) begin + `uvm_fatal(`gfn, + "Please specify either +bin=binary_name or +elf_manifest=manifest file") + end + end else begin + // Discrete debug module mode, expecting two binaries passed via +bin_main and +bin_dm. + string binaries[4] = '{bin_main, bin_dm, vmem_main, vmem_dm}; + foreach (binaries[bin]) begin + if (binaries[bin] == "") begin + `uvm_fatal(`gfn, + "Specify all test binaries via plusargs +bin_main, +bin_dm, +vmem_main, +vmem_dm") + end + end + end + + $value$plusargs("timeout_in_cycles=%0d", timeout_in_cycles); + if (!uvm_config_db#(virtual clk_rst_if)::get(null, "", "clk_if", clk_vif)) begin + `uvm_fatal(`gfn, "Cannot get clk_if") + end + if (!uvm_config_db#(virtual core_ibex_dut_probe_if)::get(null, "", "dut_if", dut_vif)) begin + `uvm_fatal(`gfn, "Cannot get dut_if") + end + if (!uvm_config_db#(virtual core_ibex_instr_monitor_if)::get(null, "", + "instr_monitor_if", + instr_vif)) begin + `uvm_fatal(`gfn, "Cannot get instr_monitor_if") + end + if (!uvm_config_db#(virtual core_ibex_csr_if)::get(null, "", "csr_if", csr_vif)) begin + `uvm_fatal(`gfn, "Cannot get csr_if") + end + env = core_ibex_env::type_id::create("env", this); + cfg = core_ibex_env_cfg::type_id::create("cfg", this); + + cosim_cfg = core_ibex_cosim_cfg::type_id::create("cosim_cfg", this); + + cosim_cfg.isa_string = get_isa_string(); + `uvm_info(`gfn, $sformatf("COSIM ISA = %0s", cosim_cfg.isa_string), UVM_LOW) + cosim_cfg.start_pc = ((32'h`BOOT_ADDR & ~(32'h0000_00FF)) | 8'h80); + cosim_cfg.start_mtvec = ((32'h`BOOT_ADDR & ~(32'h0000_00FF)) | 8'h01); + // TODO: Turn on when not using icache + cosim_cfg.probe_imem_for_errs = 1'b0; + void'($value$plusargs("cosim_log_file=%0s", cosim_log_file)); + cosim_cfg.log_file = cosim_log_file; + + if (!uvm_config_db#(bit [31:0])::get(null, "", "PMPNumRegions", pmp_num_regions)) begin + pmp_num_regions = '0; + end + + if (!uvm_config_db#(bit [31:0])::get(null, "", "PMPGranularity", pmp_granularity)) begin + pmp_granularity = '0; + end + + if (!uvm_config_db#(bit [31:0])::get(null, "", "MHPMCounterNum", mhpm_counter_num)) begin + mhpm_counter_num = '0; + end + + if (!uvm_config_db#(bit)::get(null, "", "SecureIbex", secure_ibex)) begin + secure_ibex = '0; + end + + if (!uvm_config_db#(bit)::get(null, "", "ICache", icache)) begin + icache = '0; + end + + cosim_cfg.pmp_num_regions = pmp_num_regions; + cosim_cfg.pmp_granularity = pmp_granularity; + cosim_cfg.mhpm_counter_num = mhpm_counter_num; + cosim_cfg.relax_cosim_check = cfg.disable_cosim; + cosim_cfg.secure_ibex = secure_ibex; + cosim_cfg.icache = icache; + cosim_cfg.dm_start_addr = 32'h`DM_ADDR; + cosim_cfg.dm_end_addr = 32'h`DM_ADDR + (32'h`DM_ADDR_MASK + 1); + + uvm_config_db#(core_ibex_cosim_cfg)::set(null, "*cosim_agent*", "cosim_cfg", cosim_cfg); + + imem_cfg = ibex_mem_intf_response_agent_cfg::type_id::create("imem_cfg", this); + dmem_cfg = ibex_mem_intf_response_agent_cfg::type_id::create("dmem_cfg", this); + // Never create bad integrity bits in response to accessing uninit memory + // on the Iside, as the Ibex can fetch speculatively. + imem_cfg.enable_bad_intg_on_uninit_access = 0; + // By default, enable bad_intg on the Dside (read plusarg to overwrite this behaviour) + dmem_cfg.enable_bad_intg_on_uninit_access = 1; + void'($value$plusargs("enable_bad_intg_on_uninit_access=%0d", + dmem_cfg.enable_bad_intg_on_uninit_access)); + uvm_config_db#(ibex_mem_intf_response_agent_cfg):: + set(this, "*instr_if_response_agent*", "cfg", imem_cfg); + uvm_config_db#(ibex_mem_intf_response_agent_cfg):: + set(this, "*data_if_response_agent*", "cfg", dmem_cfg); + + uvm_config_db#(core_ibex_env_cfg)::set(this, "*", "cfg", cfg); + mem = mem_model_pkg::mem_model#()::type_id::create("mem"); + + disable_spurious_dside_responses = 0; + void'($value$plusargs("disable_spurious_dside_responses=%0d", + disable_spurious_dside_responses)); + + // Disable spurious responses for non secure configs or when disabled through plusarg + if ((secure_ibex == 0) || disable_spurious_dside_responses) begin + cfg.enable_spurious_dside_responses = 0; + end + + // Create virtual sequence and assign memory handle + vseq = core_ibex_vseq::type_id::create("vseq"); + vseq.mem = mem; + vseq.cfg = cfg; + endfunction + + // Copy of core_ibex_base_test::run_phase() with ELF-manifest loader branch. + virtual task run_phase(uvm_phase phase); + enable_irq_seq = cfg.enable_irq_single_seq || cfg.enable_irq_multiple_seq; + phase.raise_objection(this); + cur_run_phase = phase; + + dut_vif.dut_cb.fetch_enable <= ibex_pkg::IbexMuBiOff; + clk_vif.wait_clks(100); + + if (use_elf_manifest) begin + `uvm_info(`gfn, + $sformatf("Loading ELF manifest before enabling fetch: %0s", elf_manifest), + UVM_LOW) + load_elf_manifest_to_mems(); + end else begin + load_binary_to_mems(); + end + + dut_vif.dut_cb.fetch_enable <= ibex_pkg::IbexMuBiOn; + + fork + send_stimulus(); + handle_reset(); + join_none + wait_for_test_done(); + cur_run_phase = null; + phase.drop_objection(this); + endtask + + // Copy of core_ibex_base_test::handle_reset() with ELF-manifest loader branch. + virtual task handle_reset(); + forever begin + @(posedge dut_vif.reset); + `uvm_info(`gfn, "Reset now active", UVM_LOW) + // Tear-down testbench components + // Flush FIFOs + item_collected_port.flush(); + irq_collected_port.flush(); + + @(negedge dut_vif.reset); + `uvm_info(`gfn, "Reset now inactive", UVM_LOW) + // Build-up testbench components + + // Cosim must be re-initialized before loading the memory + env.reset(); + + if (use_elf_manifest) begin + load_elf_manifest_to_mems(); + end else begin + load_binary_to_mems(); + end + end + endtask : handle_reset + + function void load_elf_manifest_to_mems(); + `uvm_info(`gfn, + $sformatf("Loading ELF manifest into DUT and cosim memory: %0s", elf_manifest), + UVM_LOW) + load_elf_manifest_to_dut_mem(elf_manifest); + load_elf_manifest_to_cosim_mem(elf_manifest); + endfunction + + function void load_elf_manifest_to_dut_mem(string manifest_file); + int fd; + int r; + string line; + bit [31:0] entry_addr; + bit [31:0] seg_addr; + bit [31:0] file_size; + bit [31:0] mem_size; + string seg_flags; + bit [7:0] byte_val; + bit [31:0] zero_count; + bit [31:0] cur_addr; + int i; + + fd = $fopen(manifest_file, "r"); + if (!fd) begin + `uvm_fatal(`gfn, $sformatf("Cannot open ELF manifest file %0s", manifest_file)) + end + + cur_addr = '0; + + while (!$feof(fd)) begin + line = ""; + r = $fgets(line, fd); + if (r == 0) begin + `uvm_info(`gfn, "Skipping empty/failed manifest read", UVM_HIGH) + continue; + end + + // ENTRY,0x80000000 + if (line.len() >= 6 && line.substr(0, 5) == "ENTRY,") begin + r = $sscanf(line, "ENTRY,0x%h", entry_addr); + if (r != 1) begin + `uvm_fatal(`gfn, $sformatf("Malformed ENTRY line: %0s", line)) + end + `uvm_info(`gfn, + $sformatf("ELF manifest entry point = 0x%08h", entry_addr), + UVM_LOW) + end + + // SEGMENT,0x80000000,0x00013f48,0x00013f48,RWX + else if (line.len() >= 8 && line.substr(0, 7) == "SEGMENT,") begin + r = $sscanf(line, "SEGMENT,0x%h,0x%h,0x%h,%s", + seg_addr, file_size, mem_size, seg_flags); + if (r != 4) begin + `uvm_fatal(`gfn, $sformatf("Malformed SEGMENT line: %0s", line)) + end + cur_addr = seg_addr; + `uvm_info(`gfn, + $sformatf("Loading segment: addr=0x%08h file_size=0x%08h mem_size=0x%08h flags=%0s", + seg_addr, file_size, mem_size, seg_flags), + UVM_LOW) + end + + // SECTION,.text + else if (line.len() >= 8 && line.substr(0, 7) == "SECTION,") begin + `uvm_info(`gfn, + $sformatf("Manifest section info: %0s", line), + UVM_HIGH) + end + + // BYTE,0x6f + else if (line.len() >= 5 && line.substr(0, 4) == "BYTE,") begin + r = $sscanf(line, "BYTE,0x%h", byte_val); + if (r != 1) begin + `uvm_fatal(`gfn, $sformatf("Malformed BYTE line: %0s", line)) + end + `uvm_info(`gfn, + $sformatf("Init mem [0x%08h] = 0x%02h", cur_addr, byte_val), + UVM_FULL) + mem.write(cur_addr, byte_val); + cur_addr++; + end + + // ZERO,0x00000010 + else if (line.len() >= 5 && line.substr(0, 4) == "ZERO,") begin + r = $sscanf(line, "ZERO,0x%h", zero_count); + if (r != 1) begin + `uvm_fatal(`gfn, $sformatf("Malformed ZERO line: %0s", line)) + end + + `uvm_info(`gfn, + $sformatf("Zero-filling %0d bytes from addr 0x%08h", zero_count, cur_addr), + UVM_LOW) + + for (i = 0; i < zero_count; i++) begin + mem.write(cur_addr, 8'h00); + cur_addr++; + end + end + + else begin + `uvm_fatal(`gfn, $sformatf("Unknown manifest line: %0s", line)) + end + end + + $fclose(fd); + endfunction + + function void load_elf_manifest_to_cosim_mem(string manifest_file); + int fd; + int r; + string line; + bit [31:0] entry_addr; + bit [31:0] seg_addr; + bit [31:0] file_size; + bit [31:0] mem_size; + string seg_flags; + bit [7:0] byte_val; + bit [31:0] zero_count; + bit [31:0] cur_addr; + int i; + + fd = $fopen(manifest_file, "r"); + if (!fd) begin + `uvm_fatal(`gfn, $sformatf("Cannot open ELF manifest file %0s for cosim loading", + manifest_file)) + end + + cur_addr = '0; + + while (!$feof(fd)) begin + line = ""; + r = $fgets(line, fd); + if (r == 0) begin + continue; + end + + // ENTRY,0x80000000 + if (line.len() >= 6 && line.substr(0, 5) == "ENTRY,") begin + r = $sscanf(line, "ENTRY,0x%h", entry_addr); + if (r != 1) begin + `uvm_fatal(`gfn, $sformatf("Malformed ENTRY line in cosim loader: %0s", line)) + end + `uvm_info(`gfn, + $sformatf("Cosim manifest entry point = 0x%08h", entry_addr), + UVM_LOW) + end + + // SEGMENT,0x80000000,0x00013f48,0x00013f48,RWX + else if (line.len() >= 8 && line.substr(0, 7) == "SEGMENT,") begin + r = $sscanf(line, "SEGMENT,0x%h,0x%h,0x%h,%s", + seg_addr, file_size, mem_size, seg_flags); + if (r != 4) begin + `uvm_fatal(`gfn, $sformatf("Malformed SEGMENT line in cosim loader: %0s", line)) + end + cur_addr = seg_addr; + `uvm_info(`gfn, + $sformatf("Loading cosim segment: addr=0x%08h file_size=0x%08h mem_size=0x%08h flags=%0s", + seg_addr, file_size, mem_size, seg_flags), + UVM_LOW) + end + + // SECTION,.text + else if (line.len() >= 8 && line.substr(0, 7) == "SECTION,") begin + `uvm_info(`gfn, + $sformatf("Cosim manifest section info: %0s", line), + UVM_HIGH) + end + + // BYTE,0x6f + else if (line.len() >= 5 && line.substr(0, 4) == "BYTE,") begin + r = $sscanf(line, "BYTE,0x%h", byte_val); + if (r != 1) begin + `uvm_fatal(`gfn, $sformatf("Malformed BYTE line in cosim loader: %0s", line)) + end + `uvm_info(`gfn, + $sformatf("Init cosim mem [0x%08h] = 0x%02h", cur_addr, byte_val), + UVM_FULL) + env.cosim_agent.write_mem_byte(cur_addr, byte_val); + cur_addr++; + end + + // ZERO,0x00000010 + else if (line.len() >= 5 && line.substr(0, 4) == "ZERO,") begin + r = $sscanf(line, "ZERO,0x%h", zero_count); + if (r != 1) begin + `uvm_fatal(`gfn, $sformatf("Malformed ZERO line in cosim loader: %0s", line)) + end + + `uvm_info(`gfn, + $sformatf("Cosim zero-filling %0d bytes from addr 0x%08h", zero_count, cur_addr), + UVM_LOW) + + for (i = 0; i < zero_count; i++) begin + env.cosim_agent.write_mem_byte(cur_addr, 8'h00); + cur_addr++; + end + end + + else begin + `uvm_fatal(`gfn, $sformatf("Unknown manifest line in cosim loader: %0s", line)) + end + end + + $fclose(fd); + endfunction + +endclass diff --git a/dv/uvm/core_ibex/tests/core_ibex_test_pkg.sv b/dv/uvm/core_ibex/tests/core_ibex_test_pkg.sv index 8f1e3afc21..931748cba9 100644 --- a/dv/uvm/core_ibex/tests/core_ibex_test_pkg.sv +++ b/dv/uvm/core_ibex/tests/core_ibex_test_pkg.sv @@ -50,5 +50,6 @@ package core_ibex_test_pkg; `include "core_ibex_vseq.sv" `include "core_ibex_base_test.sv" `include "core_ibex_test_lib.sv" + `include "core_ibex_elf_manifest_test.sv" endpackage From 1ff6310218126365c3f43f9bbaa57e3bd5603c30 Mon Sep 17 00:00:00 2001 From: AnastasiyaChernikova Date: Thu, 4 Jun 2026 13:12:13 +0000 Subject: [PATCH 4/7] [dv] Add Snippy riscv-dv extension hooks Add the riscv-dv extension pieces needed to generate a small wrapper program for llvm-snippy runs. This includes a custom assembly program generator, helper instructions for fixing up tp/ra, the user extension registration, and a metadata test entry. This commit only adds the riscv-dv/SV hooks. It does not add the Snippy runtime input files, RTL runner, CMake orchestration, YAML layouts, or coverage flow yet. --- .../snippy_asm_program_gen.sv | 154 ++++++++++++++++++ .../snippy_fix_tp_and_ra.sv | 12 ++ .../riscv_dv_extension/snippy_func_instr.sv | 59 +++++++ .../riscv_dv_extension/testlist.yaml | 7 + .../riscv_dv_extension/user_extension.svh | 3 + 5 files changed, 235 insertions(+) create mode 100644 dv/uvm/core_ibex/riscv_dv_extension/snippy_asm_program_gen.sv create mode 100644 dv/uvm/core_ibex/riscv_dv_extension/snippy_fix_tp_and_ra.sv create mode 100644 dv/uvm/core_ibex/riscv_dv_extension/snippy_func_instr.sv diff --git a/dv/uvm/core_ibex/riscv_dv_extension/snippy_asm_program_gen.sv b/dv/uvm/core_ibex/riscv_dv_extension/snippy_asm_program_gen.sv new file mode 100644 index 0000000000..cc2ecde378 --- /dev/null +++ b/dv/uvm/core_ibex/riscv_dv_extension/snippy_asm_program_gen.sv @@ -0,0 +1,154 @@ +class snippy_asm_program_gen extends ibex_asm_program_gen; + `uvm_object_utils(snippy_asm_program_gen) + + function new(string name = ""); + super.new(name); + endfunction + + virtual function void gen_ebreak_handler(int hart); + string instr[$]; + + int unsigned r_mepc; + int unsigned r_hw; + int unsigned r_tmp; + string lbl_32b; + string lbl_done; + + r_mepc = cfg.gpr[0]; + r_hw = cfg.gpr[1]; + r_tmp = cfg.gpr[2]; + + lbl_32b = $sformatf("snippy_ebrk_32b_h%0d", hart); + lbl_done = $sformatf("snippy_ebrk_done_h%0d", hart); + + gen_signature_handshake(instr, CORE_STATUS, EBREAK_EXCEPTION); + gen_signature_handshake(.instr(instr), .signature_type(WRITE_CSR), .csr(MCAUSE)); + + instr = {instr, + $sformatf("csrr x%0d, 0x%0x", r_mepc, MEPC), + $sformatf("lhu x%0d, 0(x%0d)", r_hw, r_mepc), + $sformatf("andi x%0d, x%0d, 3", r_hw, r_hw), + $sformatf("li x%0d, 3", r_tmp), + $sformatf("beq x%0d, x%0d, %s", r_hw, r_tmp, lbl_32b), + $sformatf("addi x%0d, x%0d, 2", r_mepc, r_mepc), + $sformatf("j %s", lbl_done), + $sformatf("%s:", lbl_32b), + $sformatf("addi x%0d, x%0d, 4", r_mepc, r_mepc), + $sformatf("%s:", lbl_done), + $sformatf("csrw 0x%0x, x%0d", MEPC, r_mepc) + }; + + pop_gpr_from_kernel_stack(MSTATUS, MSCRATCH, cfg.mstatus_mprv, + cfg.sp, cfg.tp, instr); + instr.push_back("mret"); + + gen_section(get_label("ebreak_handler", hart), instr); + endfunction + + virtual function void gen_illegal_instr_handler(int hart); + string instr[$]; + int unsigned r_mepc, r_tval, r_len, r_tmp; + string lbl_use2, lbl_use6, lbl_use8, lbl_done; + string lbl_tval_zero, lbl_have_tval, lbl_real0; + + r_mepc = cfg.gpr[0]; + r_tval = cfg.gpr[1]; + r_len = cfg.gpr[2]; + r_tmp = cfg.gpr[3]; + + lbl_use2 = $sformatf("snippy_ill_use2_h%0d", hart); + lbl_use6 = $sformatf("snippy_ill_use6_h%0d", hart); + lbl_use8 = $sformatf("snippy_ill_use8_h%0d", hart); + lbl_done = $sformatf("snippy_ill_done_h%0d", hart); + lbl_tval_zero = $sformatf("snippy_ill_tval0_h%0d", hart); + lbl_have_tval = $sformatf("snippy_ill_havetval_h%0d", hart); + lbl_real0 = $sformatf("snippy_ill_real0_h%0d", hart); + + + gen_signature_handshake(instr, CORE_STATUS, ILLEGAL_INSTR_EXCEPTION); + gen_signature_handshake(.instr(instr), .signature_type(WRITE_CSR), .csr(MCAUSE)); + + instr = {instr, + // r_mepc = mepc + $sformatf("csrr x%0d, 0x%0x", r_mepc, MEPC), + + // r_tval = mtval, which can be zero. + $sformatf("csrr x%0d, 0x%0x", r_tval, MTVAL), + + // default len=4 + $sformatf("li x%0d, 4", r_len), + + // If mtval == 0, distinguish real 0x00000000 instruction vs CSR-forced mtval=0 + $sformatf("beqz x%0d, %s", r_tval, lbl_tval_zero), + $sformatf("j %s", lbl_have_tval), + + $sformatf("%s:", lbl_tval_zero), + // Safe fetch from possibly halfword-aligned mepc: read 2x16b and check if both are zero + $sformatf("lhu x%0d, 0(x%0d)", r_tmp, r_mepc), // low16 -> r_tmp + $sformatf("lhu x%0d, 2(x%0d)", r_len, r_mepc), // high16 -> r_len (used as scratch here) + $sformatf("or x%0d, x%0d, x%0d", r_tmp, r_tmp, r_len), + $sformatf("beqz x%0d, %s", r_tmp, lbl_real0), + + // CSR-case: rebuild full 32-bit instruction into r_tmp and copy to r_tval + $sformatf("lhu x%0d, 0(x%0d)", r_tmp, r_mepc), // low16 + $sformatf("lhu x%0d, 2(x%0d)", r_len, r_mepc), // high16 (scratch) + $sformatf("slli x%0d, x%0d, 16", r_len, r_len), + $sformatf("or x%0d, x%0d, x%0d", r_tmp, r_tmp, r_len), + $sformatf("mv x%0d, x%0d", r_tval, r_tmp), + + // Restore default len=4 and continue with the original length-detection logic + $sformatf("li x%0d, 4", r_len), + $sformatf("j %s", lbl_have_tval), + + $sformatf("%s:", lbl_real0), + // Real 0x00000000: keep len=4 and finish + $sformatf("li x%0d, 4", r_len), + $sformatf("j %s", lbl_done), + + $sformatf("%s:", lbl_have_tval), + + // if (mtval & 3) != 3 -> len=2 (compressed) + $sformatf("andi x%0d, x%0d, 3", r_tmp, r_tval), + $sformatf("li x%0d, 3", r_len), + $sformatf("bne x%0d, x%0d, %s", r_tmp, r_len, lbl_use2), + + // here (mtval & 3) == 3 => 32/48/64 + // check 64-bit prefix: 0111111 (7 bits) => (mtval & 0x7f) == 0x3f + $sformatf("andi x%0d, x%0d, 0x7f", r_tmp, r_tval), + $sformatf("li x%0d, 0x3f", r_len), + $sformatf("beq x%0d, x%0d, %s", r_tmp, r_len, lbl_use8), + + // check 48-bit prefix: 011111 (6 bits) => (mtval & 0x3f) == 0x1f + $sformatf("andi x%0d, x%0d, 0x3f", r_tmp, r_tval), + $sformatf("li x%0d, 0x1f", r_len), + $sformatf("beq x%0d, x%0d, %s", r_tmp, r_len, lbl_use6), + + // otherwise keep 32-bit: len=4 + $sformatf("li x%0d, 4", r_len), + $sformatf("j %s", lbl_done), + + $sformatf("%s:", lbl_use2), + $sformatf("li x%0d, 2", r_len), + $sformatf("j %s", lbl_done), + + $sformatf("%s:", lbl_use6), + $sformatf("li x%0d, 6", r_len), + $sformatf("j %s", lbl_done), + + $sformatf("%s:", lbl_use8), + $sformatf("li x%0d, 8", r_len), + $sformatf("j %s", lbl_done), + + $sformatf("%s:", lbl_done), + // mepc += len + $sformatf("add x%0d, x%0d, x%0d", r_mepc, r_mepc, r_len), + $sformatf("csrw 0x%0x, x%0d", MEPC, r_mepc) + }; + + pop_gpr_from_kernel_stack(MSTATUS, MSCRATCH, cfg.mstatus_mprv, + cfg.sp, cfg.tp, instr); + instr.push_back("mret"); + gen_section(get_label("illegal_instr_handler", hart), instr); + endfunction + +endclass diff --git a/dv/uvm/core_ibex/riscv_dv_extension/snippy_fix_tp_and_ra.sv b/dv/uvm/core_ibex/riscv_dv_extension/snippy_fix_tp_and_ra.sv new file mode 100644 index 0000000000..9542904a70 --- /dev/null +++ b/dv/uvm/core_ibex/riscv_dv_extension/snippy_fix_tp_and_ra.sv @@ -0,0 +1,12 @@ +class ibex_riscv_instr_gen_config extends riscv_instr_gen_config; + `uvm_object_utils(ibex_riscv_instr_gen_config) + + constraint ibex_fix_tp_and_ra_c { + tp == TP; + ra == RA; + } + + function new(string name = ""); + super.new(name); + endfunction +endclass diff --git a/dv/uvm/core_ibex/riscv_dv_extension/snippy_func_instr.sv b/dv/uvm/core_ibex/riscv_dv_extension/snippy_func_instr.sv new file mode 100644 index 0000000000..88ee70e03f --- /dev/null +++ b/dv/uvm/core_ibex/riscv_dv_extension/snippy_func_instr.sv @@ -0,0 +1,59 @@ +class snippy_func_instr extends riscv_instr_sequence; + `uvm_object_utils(snippy_func_instr) + + function new(string name = ""); + super.new(name); + endfunction + + typedef string string_q_t[$]; + + function automatic string_q_t gen_stack_ops(string op, const ref string regs[]); + string_q_t result = {}; + int unsigned n = regs.size(); + int unsigned max_of = (n==0) ? 0 : (n-1)*4; + foreach (regs[i]) begin + int unsigned off = max_of - i*4; + result.push_back($sformatf(" %s %s, %0d(sp)", op, regs[i], off)); + end + return result; + endfunction + + virtual function void generate_instr_stream(bit no_label = 1'b0); + // list of all saved registers (callee‑saved + caller‑saved) + string saved_regs[] = '{"s0","s1","s2","s3","s4","s5", + "s6","s7","s8","s9","s10","s11", + "ra","tp","gp", + "t0","t1","t2","t3","t4","t5","t6", + "a0","a1","a2","a3","a4","a5","a6","a7"}; + instr_string_list = {}; + + // global variable for saving sp + instr_string_list.push_back(" .section .data"); + instr_string_list.push_back(" .align 2"); + instr_string_list.push_back("saved_sp:"); + instr_string_list.push_back(" .word 0"); + + // code + instr_string_list.push_back(" .section .text"); + instr_string_list.push_back("main:"); + + instr_string_list.push_back(" addi sp, sp, -128"); + + instr_string_list = {instr_string_list, gen_stack_ops("sw", saved_regs)}; + + // save current stack pointer in global + instr_string_list.push_back(" la t0, saved_sp"); + instr_string_list.push_back(" sw sp, 0(t0)"); + + instr_string_list.push_back(" call SnippyFunction"); + + // restore sp + instr_string_list.push_back(" la t0, saved_sp"); + instr_string_list.push_back(" lw sp, 0(t0)"); + + instr_string_list = {instr_string_list, gen_stack_ops("lw", saved_regs)}; + + instr_string_list.push_back(" addi sp, sp, 128"); + endfunction + +endclass diff --git a/dv/uvm/core_ibex/riscv_dv_extension/testlist.yaml b/dv/uvm/core_ibex/riscv_dv_extension/testlist.yaml index 3c2a447480..207491be2c 100644 --- a/dv/uvm/core_ibex/riscv_dv_extension/testlist.yaml +++ b/dv/uvm/core_ibex/riscv_dv_extension/testlist.yaml @@ -1157,3 +1157,10 @@ rtl_test: core_ibex_base_test rtl_params: RV32B: ["ibex_pkg::RV32BFull", "ibex_pkg::RV32BOTEarlGrey", "ibex_pkg::RV32BBalanced"] + +- test: snippy_func_test + gen_test: riscv_instr_base_test + iterations: 1 + gen_opts: > + +num_of_sub_program=0 + rtl_test: core_ibex_base_test diff --git a/dv/uvm/core_ibex/riscv_dv_extension/user_extension.svh b/dv/uvm/core_ibex/riscv_dv_extension/user_extension.svh index 7b55784157..403ee04437 100644 --- a/dv/uvm/core_ibex/riscv_dv_extension/user_extension.svh +++ b/dv/uvm/core_ibex/riscv_dv_extension/user_extension.svh @@ -1,3 +1,6 @@ `include "ibex_asm_program_gen.sv" `include "ibex_directed_instr_lib.sv" `include "ibex_debug_triggers_overrides.sv" +`include "snippy_func_instr.sv" +`include "snippy_asm_program_gen.sv" +`include "snippy_fix_tp_and_ra.sv" From 624d926bf561b577706792f08161530f3b51862d Mon Sep 17 00:00:00 2001 From: AnastasiyaChernikova Date: Thu, 4 Jun 2026 13:10:16 +0000 Subject: [PATCH 5/7] [dv] Add Snippy RTL simulation support Add scripts for running pre-generated llvm-snippy test artifacts through the Ibex RTL simulation flow and for post-processing the resulting logs. The runner writes standard Ibex TestRunResult-compatible artifacts and registers them through snippy_tests.mk so later Ibex Make goals can discover the generated runs. This does not add the CMake orchestration, Snippy input files, YAML layouts, or coverage exclusions yet. --- .../snippy_ibex_log_to_trace_csv.py | 299 +++++++++++++ .../core_ibex/scripts/analyze_snippy_log.py | 335 +++++++++++++++ dv/uvm/core_ibex/scripts/ibex_sim.mk | 28 ++ dv/uvm/core_ibex/scripts/run_snippy_rtl.py | 403 ++++++++++++++++++ 4 files changed, 1065 insertions(+) create mode 100644 dv/uvm/core_ibex/riscv_dv_extension/snippy_ibex_log_to_trace_csv.py create mode 100644 dv/uvm/core_ibex/scripts/analyze_snippy_log.py create mode 100644 dv/uvm/core_ibex/scripts/run_snippy_rtl.py diff --git a/dv/uvm/core_ibex/riscv_dv_extension/snippy_ibex_log_to_trace_csv.py b/dv/uvm/core_ibex/riscv_dv_extension/snippy_ibex_log_to_trace_csv.py new file mode 100644 index 0000000000..43135e93b1 --- /dev/null +++ b/dv/uvm/core_ibex/riscv_dv_extension/snippy_ibex_log_to_trace_csv.py @@ -0,0 +1,299 @@ +# Copyright lowRISC contributors. +# Licensed under the Apache License, Version 2.0, see LICENSE for details. +# SPDX-License-Identifier: Apache-2.0 +# +# Convert ibex log to the standard trace CSV format + +import argparse +import os +import re +import sys + +_IBEX_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), + '../../../..')) +_DV_SCRIPTS = os.path.join(_IBEX_ROOT, 'vendor/google_riscv-dv/scripts') +_OLD_SYS_PATH = sys.path + +# Import riscv_trace_csv and lib from _DV_SCRIPTS before putting sys.path back +# as it started. +try: + sys.path.insert(0, _DV_SCRIPTS) + + from riscv_trace_csv import (RiscvInstructionTraceCsv, + RiscvInstructionTraceEntry, + get_imm_hex_val) + from lib import RET_FATAL, gpr_to_abi, sint_to_hex, convert_pseudo_instr + import logging + logger = logging.getLogger(__name__) + +finally: + sys.path = _OLD_SYS_PATH + +from test_run_result import Failure_Modes + +INSTR_RE = \ + re.compile(r"^\s*(?P