diff --git a/dv/uvm/core_ibex/CMakeLists.txt b/dv/uvm/core_ibex/CMakeLists.txt new file mode 100644 index 0000000000..87efab2be3 --- /dev/null +++ b/dv/uvm/core_ibex/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.20) + +project(IbexSnippyFlow LANGUAGES NONE) + +include(${CMAKE_CURRENT_SOURCE_DIR}/snippy_cmake/Helpers.cmake) +include(${CMAKE_CURRENT_SOURCE_DIR}/snippy_cmake/IbexMake.cmake) +include(${CMAKE_CURRENT_SOURCE_DIR}/snippy_cmake/Options.cmake) +include(${CMAKE_CURRENT_SOURCE_DIR}/snippy_cmake/Paths.cmake) +include(${CMAKE_CURRENT_SOURCE_DIR}/snippy_cmake/Inputs.cmake) +include(${CMAKE_CURRENT_SOURCE_DIR}/snippy_cmake/RiscvDv.cmake) +include(${CMAKE_CURRENT_SOURCE_DIR}/snippy_cmake/RtlCompile.cmake) +include(${CMAKE_CURRENT_SOURCE_DIR}/snippy_cmake/Cosimulation.cmake) +include(${CMAKE_CURRENT_SOURCE_DIR}/snippy_cmake/SnippyCommands.cmake) +include(${CMAKE_CURRENT_SOURCE_DIR}/snippy_cmake/Snippy.cmake) +include(${CMAKE_CURRENT_SOURCE_DIR}/snippy_cmake/Coverage.cmake) +include(${CMAKE_CURRENT_SOURCE_DIR}/snippy_cmake/FlowTargets.cmake) diff --git a/dv/uvm/core_ibex/Makefile b/dv/uvm/core_ibex/Makefile index 69909cacb2..6a70ca8404 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,9 +85,23 @@ 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) +# Run a wrapper.mk goal using already-created metadata. +# +# This is intended for external flows, such as the Snippy CMake flow, where +# metadata is created once by an earlier Ibex Make invocation and many simulator +# runs are launched afterwards. It preserves the top-level Makefile derived +# variables such as OUT-DIR, METADATA-DIR and PYTHONPATH, but avoids calling +# metadata.py --op create_metadata for every individual run. +.PHONY: run_existing_metadata +run_existing_metadata: + @test -f $(METADATA-DIR)/metadata.pickle || \ + (echo "Metadata not found: $(METADATA-DIR)/metadata.pickle. Run make run GOAL=rtl_tb_compile first."; false) + @$(MAKE) --file wrapper.mk --environment-overrides --no-print-directory $(GOAL) + ############################################################################### # This is the top-level output directory. Everything we generate goes in @@ -94,6 +117,94 @@ export METADATA-DIR := $(OUT-DIR)metadata # riscv-dv extension directory export EXT_DIR := riscv_dv_extension +############################################################################### +# Snippy CMake flow +# +# The Snippy flow is implemented in CMake under snippy_cmake/. This Makefile +# block is intentionally a thin bridge: +# +# Ibex Make knobs -> CMake cache variables -> Snippy CMake flow +# +# Toolchain and Spike variables are shared with the normal Ibex flow: +# +# RISCV_GCC +# RISCV_OBJCOPY +# SPIKE_PATH +# PKG_CONFIG_PATH +# +# Snippy runs write their standard Ibex-compatible runtime artifacts into +# $(OUT-DIR), so later Ibex coverage goals can consume them together with +# normal riscv-dv results. + +# Comma-separated list of Snippy YAML layouts to run, for example: +# +# SNIPPY_TEST="layout_arith, jalr, rem_div" +# +# Empty means: use the CMake flow default selection. +SNIPPY_TEST ?= + +# Number of Snippy runs for each selected YAML layout. +SNIPPY_ITERATIONS ?= 1 + +# Directory with Snippy YAML layout files. +# Relative paths are resolved by CMake from dv/uvm/core_ibex. +SNIPPY_YAML_DIR ?= snippy/yaml_tests + +# Placeholder metadata test name used by the Snippy CMake flow when creating +# Ibex metadata. Keep this separate from TEST so normal riscv-dv Make runs can +# keep using TEST=all or any user-selected riscv-dv test. +SNIPPY_METADATA_TEST ?= snippy_func_test + +# Use snippy/urg_exclude/urg_exclude_all.txt during VCS/URG coverage merge. +USE_URG_EXCLUDE ?= ON + +# CMake source and build directories for the Snippy flow. +# +# CMakeLists.txt is expected to live in this core_ibex directory and include the +# implementation files from snippy_cmake/. +SNIPPY_CMAKE_SOURCE_DIR ?= $(CURDIR) +SNIPPY_CMAKE_BUILD_DIR ?= $(OUT-DIR)snippy_cmake_build + +# Do not let the top-level Ibex VERBOSE=0 leak into CMake-generated Makefiles: +# for them, any non-empty VERBOSE value enables command echoing. Pass VERBOSE=1 +# to the native build tool only when the user explicitly requested it. +SNIPPY_CMAKE_BUILD_NATIVE_ARGS := --no-print-directory +ifeq ($(VERBOSE),1) +SNIPPY_CMAKE_BUILD_NATIVE_ARGS += VERBOSE=1 +endif + +.PHONY: configure_snippy +configure_snippy: + cmake -S $(SNIPPY_CMAKE_SOURCE_DIR) -B $(SNIPPY_CMAKE_BUILD_DIR) \ + -DOUT=$(abspath $(OUT-DIR)) \ + -DSEED=$(SEED) \ + -DWAVES=$(WAVES) \ + -DCOV=$(COV) \ + -DSIMULATOR=$(SIMULATOR) \ + -DISS=$(ISS) \ + -DTEST=$(SNIPPY_METADATA_TEST) \ + -DIBEX_CONFIG=$(IBEX_CONFIG) \ + -DRV32ZC=$(RV32ZC) \ + -DSIGNATURE_ADDR=$(SIGNATURE_ADDR) \ + -DRISCV_GCC="$(RISCV_GCC)" \ + -DRISCV_OBJCOPY="$(RISCV_OBJCOPY)" \ + -DSPIKE_PATH="$(SPIKE_PATH)" \ + -DPKG_CONFIG_PATH="$(PKG_CONFIG_PATH)" \ + -DSNIPPY_ITERATIONS=$(SNIPPY_ITERATIONS) \ + -DSNIPPY_TEST="$(SNIPPY_TEST)" \ + -DYAML_DIR="$(SNIPPY_YAML_DIR)" \ + -DUSE_URG_EXCLUDE=$(USE_URG_EXCLUDE) + +.PHONY: run_snippy +run_snippy: configure_snippy + env -u VERBOSE cmake --build $(SNIPPY_CMAKE_BUILD_DIR) --target run_all -j 60 -- $(SNIPPY_CMAKE_BUILD_NATIVE_ARGS) + +.PHONY: coverage_snippy +coverage_snippy: + @test -d $(SNIPPY_CMAKE_BUILD_DIR) || \ + (echo "Snippy CMake build directory not found: $(SNIPPY_CMAKE_BUILD_DIR). Run make run_snippy first."; false) + env -u VERBOSE cmake --build $(SNIPPY_CMAKE_BUILD_DIR) --target coverage -- $(SNIPPY_CMAKE_BUILD_NATIVE_ARGS) + ############################################################################### .PHONY: clean diff --git a/dv/uvm/core_ibex/README.md b/dv/uvm/core_ibex/README.md index d1fdc0920e..3b84669ede 100644 --- a/dv/uvm/core_ibex/README.md +++ b/dv/uvm/core_ibex/README.md @@ -27,9 +27,118 @@ export RISCV_GCC=riscv32-unknown-elf-gcc export RISCV_OBJCOPY=riscv32-unknown-elf-objcopy ``` +For the optional llvm-snippy flow, set `SC_SNIPPY_PATH` to the llvm-snippy +installation directory: + +```bash +export SC_SNIPPY_PATH=/path/to/llvm-snippy +``` + +The Snippy flow expects the executable at `$SC_SNIPPY_PATH/llvm-snippy`. +More Snippy-specific setup notes are documented in `snippy_cmake/README.md`. + ## Running tests To run tests you can make variations of the following command, where you replace `$TEST_NAME` with the test (or a series of comma-separated tests) that you would like to run as specified in `dv/uvm/core_ibex/riscv_dv_extension/testlist.yaml`: ```bash make --keep-going IBEX_CONFIG=opentitan SIMULATOR=xlm ISS=spike ITERATIONS=1 SEED=1 TEST=$TEST_NAME WAVES=0 COV=0 +``` + +## Running Snippy tests + +The custom Snippy flow can be launched through the Ibex Makefile with +`run_snippy`. + +Example: + +```bash +make run_snippy \ + OUT=out \ + COV=1 \ + SIMULATOR=vcs \ + IBEX_CONFIG=opentitan \ + RV32ZC=ibex_pkg::RV32Zca \ + SNIPPY_ITERATIONS=3 \ + SNIPPY_TEST="layout_arith, jalr, rem_div" \ + SNIPPY_YAML_DIR=snippy/yaml_tests +``` + +`SNIPPY_TEST` selects YAML files from `SNIPPY_YAML_DIR`. + +`SNIPPY_TEST` is a comma-separated list of Snippy YAML layouts. The accepted +forms include `layout_arith`, `arith`, and `layout_arith.yaml`. + +`SNIPPY_ITERATIONS` controls the number of Snippy runs for each selected layout. + +Snippy runtime results are written into the standard Ibex output tree: + +```text +out/run/tests/snippy_./ +``` + +The CMake-side Snippy intermediate files and summary are written under: + +```text +out/snippy_cmake_build/ +``` + +## Collecting combined riscv-dv and Snippy coverage + +To collect one combined coverage report from both riscv-dv and Snippy runs, use +the same `OUT` directory and enable `COV=1` for both flows. + +First, run riscv-dv only up to RTL simulation: + +```bash +make run \ + GOAL=rtl_sim_run \ + OUT=out \ + COV=1 \ + SIMULATOR=vcs \ + IBEX_CONFIG=opentitan \ + RV32ZC=ibex_pkg::RV32Zca \ + TEST=riscv_arithmetic_basic_test +``` + +Using `GOAL=rtl_sim_run` avoids collecting an intermediate coverage report +before the Snippy tests have been added. + +Then run Snippy into the same `OUT` directory: + +```bash +make run_snippy \ + OUT=out \ + COV=1 \ + SIMULATOR=vcs \ + IBEX_CONFIG=opentitan \ + RV32ZC=ibex_pkg::RV32Zca \ + SNIPPY_ITERATIONS=3 \ + SNIPPY_TEST="layout_arith, jalr, rem_div" +``` + +Finally, merge coverage once: + +```bash +make coverage_snippy OUT=out +``` + +The final report is written to: + +```text +out/run/coverage/report/ +``` + +## Cleaning generated files + +To remove generated outputs: + +```bash +make clean OUT=out +``` + +This also removes the Snippy CMake build directory when it is located under +`OUT`, for example: + +```text +out/snippy_cmake_build/ ``` \ No newline at end of file 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_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/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/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