diff --git a/coreblocks/utils/utils.py b/coreblocks/utils/utils.py index 31fc830ab..2f7b78cb6 100644 --- a/coreblocks/utils/utils.py +++ b/coreblocks/utils/utils.py @@ -15,6 +15,7 @@ "OneHotSwitch", "flatten_signals", "align_to_power_of_two", + "align_down_to_power_of_two", "bits_from_int", "ModuleConnector", "silence_mustuse", @@ -377,6 +378,26 @@ def align_to_power_of_two(num: int, power: int) -> int: return (num & ~mask) + 2**power +def align_down_to_power_of_two(num: int, power: int) -> int: + """Rounds down a number to the given power of two. + + Parameters + ---------- + num : int + The number to align. + power : int + The power of two to align to. + + Returns + ------- + int + The aligned number. + """ + mask = 2**power - 1 + + return num & ~mask + + def bits_from_int(num: int, lower: int, length: int): """Returns [`lower`:`lower`+`length`) bits from integer `num`.""" return (num >> lower) & ((1 << (length)) - 1) diff --git a/scripts/run_signature.py b/scripts/run_signature.py new file mode 100755 index 000000000..6acb4e311 --- /dev/null +++ b/scripts/run_signature.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 + +import asyncio +import argparse +import sys +import os +import subprocess +from typing import Literal + +if __name__ == "__main__": + parent = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + sys.path.insert(0, parent) + +import test.regression.signature # noqa: E402 +from test.regression.pysim import PySimulation # noqa: E402 + + +def run_with_cocotb(test_name: str, traces: bool, output: str) -> bool: + arglist = [ + "make", + "-C", + parent + "/" if parent else "" + "test/regression/cocotb", + "-f", + "signature.Makefile", + "--no-print-directory", + ] + + if os.path.isfile(output): + os.remove(output) + + arglist += [f"TESTNAME={test_name}"] + arglist += [f"OUTPUT={output}"] + + if traces: + arglist += ["TRACES=1"] + + subprocess.run(arglist) + + return os.path.isfile(output) # completed successfully if signature file was created + + +def run_with_pysim(test_name: str, traces: bool, verbose: bool, output: str) -> bool: + traces_file = None + if traces: + traces_file = os.path.basename(test_name) + try: + asyncio.run( + test.regression.signature.run_test(PySimulation(verbose, traces_file=traces_file), test_name, output) + ) + except RuntimeError as e: + print("RuntimeError:", e) + return False + return True + + +def run_test(test: str, backend: Literal["pysim", "cocotb"], traces: bool, verbose: bool, output: str) -> bool: + if backend == "cocotb": + return run_with_cocotb(test, traces, output) + elif backend == "pysim": + return run_with_pysim(test, traces, verbose, output) + return False + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("-t", "--trace", action="store_true", help="Dump waveforms") + parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output") + parser.add_argument("-b", "--backend", default="pysim", choices=["cocotb", "pysim"], help="Simulation backend") + parser.add_argument("-o", "--output", default=None, help="Selects output file to write test signature to") + parser.add_argument("path") + + args = parser.parse_args() + + output = args.output if args.output else args.path + ".signature" + + success = run_test(args.path, args.backend, args.trace, args.verbose, output) + if not success: + print(f"{args.path}: Program execution failed") + + if output is not None: # create empty file on failure for checker scripts + with open(output, "w"): + pass + + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/test/external/riscof/coreblocks/env/link.ld b/test/external/riscof/coreblocks/env/link.ld new file mode 100644 index 000000000..4509f68a5 --- /dev/null +++ b/test/external/riscof/coreblocks/env/link.ld @@ -0,0 +1,40 @@ +OUTPUT_ARCH( "riscv" ) +ENTRY(rvtest_entry_point) + +MEMORY +{ + text (rxai!w) : ORIGIN = 0x00000000, LENGTH = 2M + data (wxa!ri) : ORIGIN = 0x10000000, LENGTH = 1M + mmio (wa!rxi) : ORIGIN = 0x80000000, LENGTH = 1K + signature (wa!rxi) : ORIGIN = 0x81000000, LENGTH = 16K + +} + +PHDRS +{ + text PT_LOAD; + data_init PT_LOAD; + data PT_NULL; + mmio PT_LOAD; + signature PT_LOAD; +} + +SECTIONS +{ + .text.init : { *(.text.init) } >text AT>text :text + . = ALIGN(0x1000); + .text : { *(.text) } >text AT>text :text + + . = ALIGN(0x1000); + .data : { *(.data) } >data AT>data :data_init + .data.string : { *(.data.string)} >data AT>data :data_init + .bss : { *(.bss) } >data AT>data :data + + . = ALIGN(0x1000); + .hostmmio : { *(.hostmmio) } >mmio AT>mmio :mmio + + . = ALIGN(0x1000); + .signature : { *(.signature) } >signature AT>signature :signature + + _end = .; +} diff --git a/test/external/riscof/coreblocks/env/model_test.h b/test/external/riscof/coreblocks/env/model_test.h new file mode 100644 index 000000000..2b9b38a05 --- /dev/null +++ b/test/external/riscof/coreblocks/env/model_test.h @@ -0,0 +1,64 @@ +#ifndef _COMPLIANCE_MODEL_H +#define _COMPLIANCE_MODEL_H +#define RVMODEL_DATA_SECTION \ + .pushsection .hostmmio,"aw",@progbits; \ + .align 8; .global tohost; tohost: .dword 0; \ + .align 8; .global fromhost; fromhost: .dword 0; \ + .popsection; \ + .align 8; .global begin_regstate; begin_regstate: \ + .word 128; \ + .align 8; .global end_regstate; end_regstate: \ + .word 4; + +//RV_COMPLIANCE_HALT +#define RVMODEL_HALT \ + li x1, 1; \ + write_tohost: \ + sw x1, tohost, t5; \ + j write_tohost; + +#define RVMODEL_BOOT + +//RV_COMPLIANCE_DATA_BEGIN +#define RVMODEL_DATA_BEGIN \ + RVMODEL_DATA_SECTION \ + .pushsection .signature,"aw",@progbits; \ + .align 2; \ + .global begin_signature; begin_signature: + +//RV_COMPLIANCE_DATA_END +#define RVMODEL_DATA_END \ + .global end_signature; end_signature: \ + .popsection; + +//RVTEST_IO_INIT +#define RVMODEL_IO_INIT +//RVTEST_IO_WRITE_STR +#define RVMODEL_IO_WRITE_STR(_R, _STR) +//RVTEST_IO_CHECK +#define RVMODEL_IO_CHECK() +//RVTEST_IO_ASSERT_GPR_EQ +#define RVMODEL_IO_ASSERT_GPR_EQ(_S, _R, _I) +//RVTEST_IO_ASSERT_SFPR_EQ +#define RVMODEL_IO_ASSERT_SFPR_EQ(_F, _R, _I) +//RVTEST_IO_ASSERT_DFPR_EQ +#define RVMODEL_IO_ASSERT_DFPR_EQ(_D, _R, _I) + +// empty macros to supress warnings +#define RVMODEL_SET_MSW_INT +#define RVMODEL_CLEAR_MSW_INT +#define RVMODEL_CLEAR_MTIMER_INT +#define RVMODEL_CLEAR_MEXT_INT +#define RVMODEL_CLR_MSW_INT +#define RVMODEL_CLR_MTIMER_INT +#define RVMODEL_CLR_MEXT_INT +#define RVMODEL_SET_SSW_INT +#define RVMODEL_CLR_SSW_INT +#define RVMODEL_CLR_STIMER_INT +#define RVMODEL_CLR_SEXT_INT +#define RVMODEL_SET_VSW_INT +#define RVMODEL_CLR_VSW_INT +#define RVMODEL_CLR_VTIMER_INT +#define RVMODEL_CLR_VEXT_INT + +#endif // _COMPLIANCE_MODEL_H diff --git a/test/regression/cocotb/signature.Makefile b/test/regression/cocotb/signature.Makefile new file mode 100644 index 000000000..e7da43e25 --- /dev/null +++ b/test/regression/cocotb/signature.Makefile @@ -0,0 +1,28 @@ +# Makefile + +# defaults +SIM ?= verilator +TOPLEVEL_LANG ?= verilog + +VERILOG_SOURCES += $(PWD)/../../../core.v +# use VHDL_SOURCES for VHDL files + +# TOPLEVEL is the name of the toplevel module in your Verilog or VHDL file +TOPLEVEL = top + +# MODULE is the basename of the Python test file +MODULE = signature_entrypoint + +SIM_BUILD = build/signature + +# Yosys/Amaranth borkedness workaround +ifeq ($(SIM),verilator) + EXTRA_ARGS += -Wno-CASEINCOMPLETE -Wno-CASEOVERLAP -Wno-WIDTHEXPAND -Wno-WIDTHTRUNC +endif + +ifeq ($(TRACES),1) + EXTRA_ARGS += --trace-fst --trace-structs +endif + +# include cocotb's make rules to take care of the simulator setup +include $(shell cocotb-config --makefiles)/Makefile.sim diff --git a/test/regression/cocotb/signature_entrypoint.py b/test/regression/cocotb/signature_entrypoint.py new file mode 100644 index 000000000..1508502fe --- /dev/null +++ b/test/regression/cocotb/signature_entrypoint.py @@ -0,0 +1,25 @@ +import os +import sys +import cocotb +from pathlib import Path + +top_dir = Path(__file__).parent.parent.parent.parent +sys.path.insert(0, str(top_dir)) + +from test.regression.cocotb import CocotbSimulation # noqa: E402 +from test.regression.signature import run_test # noqa: E402 + + +@cocotb.test() +async def do_test(dut): + cocotb.logging.getLogger().setLevel(cocotb.logging.INFO) + + test_name = os.environ["TESTNAME"] + if test_name is None: + raise RuntimeError("No ELF file provided") + + output = os.environ["OUTPUT"] + if output is None: + output = test_name + ".signature" + + await run_test(CocotbSimulation(dut), test_name, output) diff --git a/test/regression/memory.py b/test/regression/memory.py index 68bda0616..40599d044 100644 --- a/test/regression/memory.py +++ b/test/regression/memory.py @@ -4,7 +4,9 @@ from typing import Optional, TypeVar from dataclasses import dataclass, replace from elftools.elf.constants import P_FLAGS -from elftools.elf.elffile import ELFFile +from elftools.elf.elffile import ELFFile, Segment +from coreblocks.params.configurations import CoreConfiguration +from coreblocks.utils.utils import align_to_power_of_two, align_down_to_power_of_two all = [ "ReplyStatus", @@ -137,6 +139,39 @@ def write(self, req: WriteRequest) -> WriteReply: return WriteReply(status=ReplyStatus.ERROR) +def load_segment(segment: Segment, *, disable_write_protection: bool = False) -> RandomAccessMemory: + paddr = segment.header["p_paddr"] + memsz = segment.header["p_memsz"] + flags_raw = segment.header["p_flags"] + + seg_start = paddr + seg_end = paddr + memsz + + data = segment.data() + + flags = SegmentFlags(0) + if flags_raw & P_FLAGS.PF_R: + flags |= SegmentFlags.READ + if flags_raw & P_FLAGS.PF_W or disable_write_protection: + flags |= SegmentFlags.WRITE + if flags_raw & P_FLAGS.PF_X: + flags |= SegmentFlags.EXECUTABLE + + if flags_raw & P_FLAGS.PF_X: + # align only instruction section to full icache lines + align_bits = CoreConfiguration().icache_block_size_bits + + align_data_front = seg_start - align_down_to_power_of_two(seg_start, align_bits) + align_data_back = align_to_power_of_two(seg_end, align_bits) - seg_end + + data = b"\x00" * align_data_front + data + b"\x00" * align_data_back + + seg_start = align_down_to_power_of_two(seg_start, align_bits) + seg_end = align_to_power_of_two(seg_end, align_bits) + + return RandomAccessMemory(range(seg_start, seg_end), flags, data) + + def load_segments_from_elf(file_path: str, *, disable_write_protection: bool = False) -> list[RandomAccessMemory]: segments: list[RandomAccessMemory] = [] @@ -145,28 +180,6 @@ def load_segments_from_elf(file_path: str, *, disable_write_protection: bool = F for segment in elffile.iter_segments(): if segment.header["p_type"] != "PT_LOAD": continue - - paddr = segment.header["p_paddr"] - alignment = segment.header["p_align"] - memsz = segment.header["p_memsz"] - flags_raw = segment.header["p_flags"] - - def align_down(n: int) -> int: - return (n // alignment) * alignment - - seg_start = align_down(paddr) - seg_end = align_down(paddr + memsz + alignment - 1) - - data = b"\x00" * (paddr - seg_start) + segment.data() + b"\x00" * (seg_end - (paddr + len(segment.data()))) - - flags = SegmentFlags(0) - if flags_raw & P_FLAGS.PF_R: - flags |= SegmentFlags.READ - if flags_raw & P_FLAGS.PF_W or disable_write_protection: - flags |= SegmentFlags.WRITE - if flags_raw & P_FLAGS.PF_X: - flags |= SegmentFlags.EXECUTABLE - - segments.append(RandomAccessMemory(range(seg_start, seg_end), flags, data)) + segments.append(load_segment(segment, disable_write_protection=disable_write_protection)) return segments diff --git a/test/regression/signature.py b/test/regression/signature.py new file mode 100644 index 000000000..96b661199 --- /dev/null +++ b/test/regression/signature.py @@ -0,0 +1,63 @@ +from .memory import * +from .common import SimulationBackend + + +class ToHostMMIO(MemorySegment): + def __init__(self, addr: range, on_finish: Callable[[], None]): + super().__init__(addr, SegmentFlags.READ | SegmentFlags.WRITE) + self.on_finish = on_finish + + def read(self, _) -> ReadReply: + return ReadReply() + + def write(self, _) -> WriteReply: + self.on_finish() + return WriteReply() + + +def map_mem_segments( + elf_path: str, stop_callback: Callable[[], None] +) -> tuple[list[MemorySegment], RandomAccessMemory]: + mem_segments = [] + signature_ram = RandomAccessMemory(range(0, 0), SegmentFlags.WRITE, bytearray()) + + with open(elf_path, "rb") as f: + elffile = ELFFile(f) + + signature_section = elffile.get_section(elffile.get_section_index(".signature")) + tohost_section = elffile.get_section(elffile.get_section_index(".hostmmio")) + + for segment in elffile.iter_segments(): + # .signature and .tohost sections have direct segment mapping + addr_range = range(segment.header["p_vaddr"], segment.header["p_vaddr"] + segment.header["p_memsz"]) + if segment.section_in_segment(signature_section): + signature_ram = load_segment(segment) + elif segment.section_in_segment(tohost_section): + mem_segments.append(ToHostMMIO(addr_range, stop_callback)) + elif segment.header["p_type"] == "PT_LOAD": + mem_segments.append(load_segment(segment)) + + return (mem_segments, signature_ram) + + +async def run_test(sim_backend: SimulationBackend, test_path: str, signature_path: str): + (mem_segments, signature_ram) = map_mem_segments(test_path, sim_backend.stop) + + mem_segments.append(signature_ram) + mem_model = CoreMemoryModel(mem_segments) + + success = await sim_backend.run(mem_model, timeout_cycles=100000) + + if not success: + raise RuntimeError(f"{test_path}: Simulation timed out") + + print(f"{test_path}: Program execution finished! Signature: {signature_path}") + + # generate signature file in riscv-torture format (used also by riscof) + # 32-bit little endian memory dump from data between .begin_signature and .end_signature + # symbols, mapped to .signature section in our linker script + with open(signature_path, "w") as sig_file: + data = signature_ram.data.zfill(((len(signature_ram.data) + 3) // 4) * 4) + for idx in range(0, len(data), 4): + num = int.from_bytes(data[idx : idx + 4], "little") + sig_file.write(hex(num)[2:].zfill(8) + "\n") diff --git a/test/utils/test_utils.py b/test/utils/test_utils.py index fa2f63bc1..6a948f490 100644 --- a/test/utils/test_utils.py +++ b/test/utils/test_utils.py @@ -3,7 +3,13 @@ from amaranth import * from test.common import * -from coreblocks.utils import align_to_power_of_two, popcount, count_leading_zeros, count_trailing_zeros +from coreblocks.utils import ( + align_to_power_of_two, + align_down_to_power_of_two, + popcount, + count_leading_zeros, + count_trailing_zeros, +) from parameterized import parameterized_class @@ -29,6 +35,23 @@ def test_align_to_power_of_two(self): out = align_to_power_of_two(num, power) self.assertEqual(expected, out) + def test_align_down_to_power_of_two(self): + test_cases = [ + (3, 1, 2), + (3, 0, 3), + (3, 3, 0), + (8, 3, 8), + (8, 2, 8), + (33, 5, 32), + (29, 5, 0), + (29, 1, 28), + (29, 3, 24), + ] + + for num, power, expected in test_cases: + out = align_down_to_power_of_two(num, power) + self.assertEqual(expected, out) + class PopcountTestCircuit(Elaboratable): def __init__(self, size: int):