diff --git a/chipflow/sim/__init__.py b/chipflow/sim/__init__.py new file mode 100644 index 00000000..677812ff --- /dev/null +++ b/chipflow/sim/__init__.py @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: BSD-2-Clause +"""CXXRTL-based simulation infrastructure for ChipFlow. + +This module provides Python bindings for CXXRTL simulation, enabling fast +compiled simulation of mixed Amaranth/Verilog/SystemVerilog designs. + +Example usage:: + + from chipflow.sim import CxxrtlSimulator, build_cxxrtl + + # Build CXXRTL shared library from sources + lib_path = build_cxxrtl( + sources=["design.v", "ip.sv"], + top_module="design", + output_dir=Path("build/sim") + ) + + # Create simulator and run testbench + sim = CxxrtlSimulator(lib_path, top_module="design") + sim.reset() + + # Access signals + sim.set("clk", 1) + sim.step() + value = sim.get("data_out") +""" + +from chipflow.sim.cxxrtl import CxxrtlSimulator +from chipflow.sim.build import build_cxxrtl + +__all__ = ["CxxrtlSimulator", "build_cxxrtl"] diff --git a/chipflow/sim/build.py b/chipflow/sim/build.py new file mode 100644 index 00000000..27d0fcf0 --- /dev/null +++ b/chipflow/sim/build.py @@ -0,0 +1,336 @@ +# SPDX-License-Identifier: BSD-2-Clause +"""Build CXXRTL shared libraries from HDL sources. + +This module provides functions to compile Amaranth, Verilog, and SystemVerilog +designs into CXXRTL shared libraries for fast simulation. +""" + +import logging +import platform +import shutil +import subprocess +from pathlib import Path +from typing import Optional, Sequence, Union + +logger = logging.getLogger(__name__) + + +def _find_cxx_compiler() -> str: + """Find a C++ compiler.""" + for compiler in ["c++", "g++", "clang++"]: + if shutil.which(compiler): + return compiler + raise RuntimeError("No C++ compiler found. Install g++ or clang++.") + + +def _get_shared_lib_extension() -> str: + """Get platform-specific shared library extension.""" + system = platform.system() + if system == "Darwin": + return ".dylib" + elif system == "Windows": + return ".dll" + else: + return ".so" + + +def _get_cxxrtl_include_path() -> Path: + """Find CXXRTL runtime headers. + + Checks yowasp-yosys share directory and homebrew yosys installation. + """ + # Try yowasp-yosys first + try: + import yowasp_yosys + + yowasp_dir = Path(yowasp_yosys.__file__).parent + share_dir = yowasp_dir / "share" / "include" / "backends" / "cxxrtl" / "runtime" + if share_dir.exists(): + return share_dir + except ImportError: + pass + + # Try homebrew yosys (macOS) + homebrew_path = Path("/opt/homebrew/opt/yosys/share/yosys/include/backends/cxxrtl/runtime") + if homebrew_path.exists(): + return homebrew_path + + # Try system yosys + system_path = Path("/usr/share/yosys/include/backends/cxxrtl/runtime") + if system_path.exists(): + return system_path + + raise RuntimeError( + "Could not find CXXRTL headers. Install yowasp-yosys or yosys." + ) + + +def _run_yosys(commands: str) -> None: + """Run Yosys with the given commands. + + Uses yowasp-yosys if available, otherwise native yosys. + Note: All paths in commands must be absolute since yowasp-yosys + doesn't support cwd parameter. + """ + try: + from yowasp_yosys import run_yosys + + logger.debug("Using yowasp-yosys") + result = run_yosys(["-p", commands]) + if result != 0: + raise RuntimeError(f"yowasp-yosys failed with exit code {result}") + except ImportError: + logger.debug("Using native yosys") + yosys = shutil.which("yosys") + if not yosys: + raise RuntimeError("Neither yowasp-yosys nor native yosys found") + + result = subprocess.run( + [yosys, "-p", commands], + capture_output=True, + text=True, + ) + if result.returncode != 0: + raise RuntimeError(f"yosys failed: {result.stderr}") + + +def _generate_capi_wrapper(cxxrtl_cc_path: Path, wrapper_path: Path, top_module: str) -> None: + """Generate a C API wrapper that exports the toplevel create function. + + The CXXRTL C API requires a `_create()` function to be provided by the user. + This generates a wrapper that includes the CXXRTL code and exports the function. + """ + # Convert module name to valid C++ identifier + # CXXRTL naming convention: + # - Adds "p_" prefix + # - Replaces backslash with nothing + # - Replaces space with "__" + # - Replaces single underscore with "__" (double underscore) + cpp_class_name = "p_" + top_module.replace("\\", "").replace(" ", "__").replace("_", "__") + + wrapper_code = f'''// Auto-generated CXXRTL C API wrapper +// This file provides the C API entry point for the {top_module} module + +#include "{cxxrtl_cc_path.name}" + +extern "C" {{ + +// Create function required by CXXRTL C API +cxxrtl_toplevel {top_module}_create() {{ + return new _cxxrtl_toplevel {{ std::make_unique() }}; +}} + +}} // extern "C" +''' + wrapper_path.write_text(wrapper_code) + logger.debug(f"Generated C API wrapper: {wrapper_path}") + + +def build_cxxrtl( + sources: Sequence[Union[str, Path]], + top_module: str, + output_dir: Union[str, Path], + output_name: Optional[str] = None, + include_dirs: Optional[Sequence[Union[str, Path]]] = None, + defines: Optional[dict[str, str]] = None, + optimization: str = "-O2", + debug_info: bool = True, +) -> Path: + """Build a CXXRTL shared library from HDL sources. + + Args: + sources: List of Verilog/SystemVerilog source files + top_module: Name of the top-level module + output_dir: Directory for build artifacts + output_name: Name for output library (default: top_module) + include_dirs: Additional include directories for Verilog + defines: Verilog preprocessor defines + optimization: C++ optimization level (default: -O2) + debug_info: Include CXXRTL debug info (default: True) + + Returns: + Path to the compiled shared library + """ + output_dir = Path(output_dir).absolute() + output_dir.mkdir(parents=True, exist_ok=True) + + output_name = output_name or top_module + lib_ext = _get_shared_lib_extension() + lib_path = output_dir / f"{output_name}{lib_ext}" + cc_path = output_dir / f"{output_name}_cxxrtl.cc" + + # Build Yosys commands + yosys_cmds = [] + + # Read sources - use slang for .sv files, read_verilog for .v + # Use absolute paths since yowasp-yosys doesn't support cwd + for source in sources: + source = Path(source).absolute() + if not source.exists(): + raise FileNotFoundError(f"Source file not found: {source}") + + if source.suffix == ".sv": + yosys_cmds.append(f"read_slang {source}") + else: + yosys_cmds.append(f"read_verilog {source}") + + # Set top module and elaborate + yosys_cmds.append(f"hierarchy -top {top_module}") + + # Write CXXRTL + cxxrtl_opts = [] + if debug_info: + cxxrtl_opts.append("-g3") # Maximum debug info + + yosys_cmds.append(f"write_cxxrtl {' '.join(cxxrtl_opts)} {cc_path}") + + # Run Yosys + logger.info(f"Generating CXXRTL for {top_module}") + _run_yosys("\n".join(yosys_cmds)) + + if not cc_path.exists(): + raise RuntimeError(f"CXXRTL generation failed - {cc_path} not created") + + # Generate C API wrapper that exports the create function + wrapper_path = output_dir / f"{output_name}_capi_wrapper.cc" + _generate_capi_wrapper(cc_path, wrapper_path, top_module) + + # Find CXXRTL headers + cxxrtl_include = _get_cxxrtl_include_path() + logger.debug(f"Using CXXRTL headers from {cxxrtl_include}") + + # Compile to shared library + compiler = _find_cxx_compiler() + compile_cmd = [ + compiler, + "-std=c++17", + optimization, + "-shared", + "-fPIC", + f"-I{cxxrtl_include}", + f"-I{output_dir}", # For finding the generated CXXRTL code + "-DCXXRTL_INCLUDE_CAPI_IMPL", # Include C API implementation + "-o", str(lib_path), + str(wrapper_path), # Compile wrapper which includes the generated code + ] + + # Platform-specific flags + if platform.system() == "Darwin": + compile_cmd.extend(["-undefined", "dynamic_lookup"]) + + logger.info(f"Compiling CXXRTL library: {lib_path}") + logger.debug(f"Compile command: {' '.join(compile_cmd)}") + + result = subprocess.run( + compile_cmd, + capture_output=True, + text=True, + ) + + if result.returncode != 0: + raise RuntimeError(f"C++ compilation failed: {result.stderr}") + + logger.info(f"Built CXXRTL library: {lib_path}") + return lib_path + + +def build_cxxrtl_from_amaranth( + elaboratable, + top_module: str, + output_dir: Union[str, Path], + amaranth_platform=None, + extra_sources: Optional[Sequence[Union[str, Path]]] = None, + **kwargs, +) -> Path: + """Build CXXRTL from an Amaranth Elaboratable. + + This function generates Verilog from an Amaranth design and compiles it + to a CXXRTL shared library, optionally combining with extra Verilog/SV sources. + + Args: + elaboratable: Amaranth Elaboratable to simulate + top_module: Name for the top module + output_dir: Directory for build artifacts + amaranth_platform: Amaranth platform (optional) + extra_sources: Additional Verilog/SV files to include + **kwargs: Additional arguments passed to build_cxxrtl() + + Returns: + Path to the compiled shared library + """ + from amaranth.back import rtlil # type: ignore[attr-defined] + + output_dir = Path(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + # Generate RTLIL from Amaranth + rtlil_path = output_dir / f"{top_module}.il" + logger.info(f"Generating RTLIL for {top_module}") + + rtlil_text = rtlil.convert(elaboratable, platform=amaranth_platform) + rtlil_path.write_text(rtlil_text) + + # Build Yosys commands - read RTLIL first, then extra sources + sources: list[Path] = [rtlil_path] + if extra_sources: + sources.extend(Path(s) for s in extra_sources) + + # For RTLIL, we need special handling in Yosys + yosys_cmds = [f"read_rtlil {rtlil_path}"] + + for source in extra_sources or []: + source = Path(source) + if source.suffix == ".sv": + yosys_cmds.append(f"read_slang {source}") + elif source.suffix == ".il": + yosys_cmds.append(f"read_rtlil {source}") + else: + yosys_cmds.append(f"read_verilog {source}") + + # Elaborate and write CXXRTL + output_name = kwargs.pop("output_name", top_module) + lib_ext = _get_shared_lib_extension() + lib_path = output_dir / f"{output_name}{lib_ext}" + cc_path = output_dir / f"{output_name}_cxxrtl.cc" + + debug_info = kwargs.pop("debug_info", True) + cxxrtl_opts = ["-g3"] if debug_info else [] + + yosys_cmds.append(f"hierarchy -top {top_module}") + yosys_cmds.append(f"write_cxxrtl {' '.join(cxxrtl_opts)} {cc_path}") + + # Run Yosys + logger.info(f"Generating CXXRTL for {top_module}") + _run_yosys("\n".join(yosys_cmds)) + + if not cc_path.exists(): + raise RuntimeError(f"CXXRTL generation failed - {cc_path} not created") + + # Find CXXRTL headers and compile + cxxrtl_include = _get_cxxrtl_include_path() + compiler = _find_cxx_compiler() + optimization = kwargs.pop("optimization", "-O2") + + compile_cmd = [ + compiler, + "-std=c++17", + optimization, + "-shared", + "-fPIC", + f"-I{cxxrtl_include}", + "-DCXXRTL_INCLUDE_CAPI_IMPL", + "-o", str(lib_path), + str(cc_path), + ] + + if platform.system() == "Darwin": + compile_cmd.extend(["-undefined", "dynamic_lookup"]) + + logger.info(f"Compiling CXXRTL library: {lib_path}") + result = subprocess.run(compile_cmd, capture_output=True, text=True) + + if result.returncode != 0: + raise RuntimeError(f"C++ compilation failed: {result.stderr}") + + logger.info(f"Built CXXRTL library: {lib_path}") + return lib_path diff --git a/chipflow/sim/cxxrtl.py b/chipflow/sim/cxxrtl.py new file mode 100644 index 00000000..3ad87c05 --- /dev/null +++ b/chipflow/sim/cxxrtl.py @@ -0,0 +1,312 @@ +# SPDX-License-Identifier: BSD-2-Clause +"""CXXRTL C API bindings via ctypes. + +This module provides Python bindings for the CXXRTL simulation engine, +allowing fast compiled simulation of HDL designs with Python testbenches. +""" + +import ctypes +from ctypes import ( + CFUNCTYPE, + POINTER, + Structure, + c_char_p, + c_int, + c_size_t, + c_uint32, + c_void_p, +) +from pathlib import Path +from typing import Dict, Iterator, Tuple, Union + + +# CXXRTL object types +class CxxrtlType: + VALUE = 0 + WIRE = 1 + MEMORY = 2 + ALIAS = 3 + OUTLINE = 4 + + +# CXXRTL object flags +class CxxrtlFlag: + INPUT = 1 << 0 + OUTPUT = 1 << 1 + INOUT = INPUT | OUTPUT + DRIVEN_SYNC = 1 << 2 + DRIVEN_COMB = 1 << 3 + UNDRIVEN = 1 << 4 + + +class CxxrtlObject(Structure): + """CXXRTL object descriptor - matches struct cxxrtl_object in cxxrtl_capi.h""" + + _fields_ = [ + ("type", c_uint32), + ("flags", c_uint32), + ("width", c_size_t), + ("lsb_at", c_size_t), + ("depth", c_size_t), + ("zero_at", c_size_t), + ("curr", POINTER(c_uint32)), + ("next", POINTER(c_uint32)), + ("outline", c_void_p), + ("attrs", c_void_p), + ] + + +# Callback type for cxxrtl_enum +EnumCallback = CFUNCTYPE( + None, c_void_p, c_char_p, POINTER(CxxrtlObject), c_size_t +) + + +class CxxrtlSimulator: + """Python wrapper for CXXRTL simulation. + + This class provides a Pythonic interface to CXXRTL compiled simulations, + supporting signal access, stepping, and VCD tracing. + + Example:: + + sim = CxxrtlSimulator("build/design.so", "design") + sim.reset() + + # Clock cycle + sim.set("clk", 0) + sim.step() + sim.set("clk", 1) + sim.step() + + # Read output + value = sim.get("data_out") + """ + + def __init__(self, library_path: Union[str, Path], top_module: str): + """Initialize CXXRTL simulator. + + Args: + library_path: Path to compiled CXXRTL shared library (.so/.dylib) + top_module: Name of the top-level module (used to find create function) + """ + self._lib_path = Path(library_path) + self._top_module = top_module + self._lib: ctypes.CDLL + self._handle: c_void_p + self._objects: Dict[str, CxxrtlObject] = {} + + self._load_library() + self._create_handle() + self._discover_objects() + + def _load_library(self) -> None: + """Load the CXXRTL shared library and set up function prototypes.""" + if not self._lib_path.exists(): + raise FileNotFoundError(f"CXXRTL library not found: {self._lib_path}") + + self._lib = ctypes.CDLL(str(self._lib_path)) + + # cxxrtl_toplevel _create() + create_name = f"{self._top_module}_create" + if not hasattr(self._lib, create_name): + raise RuntimeError( + f"Library does not export {create_name}. " + f"Make sure the library was compiled with top module '{self._top_module}'" + ) + self._toplevel_create = getattr(self._lib, create_name) + self._toplevel_create.restype = c_void_p + self._toplevel_create.argtypes = [] + + # cxxrtl_handle cxxrtl_create(cxxrtl_toplevel) + self._lib.cxxrtl_create.restype = c_void_p + self._lib.cxxrtl_create.argtypes = [c_void_p] + + # void cxxrtl_destroy(cxxrtl_handle) + self._lib.cxxrtl_destroy.restype = None + self._lib.cxxrtl_destroy.argtypes = [c_void_p] + + # void cxxrtl_reset(cxxrtl_handle) + self._lib.cxxrtl_reset.restype = None + self._lib.cxxrtl_reset.argtypes = [c_void_p] + + # int cxxrtl_eval(cxxrtl_handle) + self._lib.cxxrtl_eval.restype = c_int + self._lib.cxxrtl_eval.argtypes = [c_void_p] + + # int cxxrtl_commit(cxxrtl_handle) + self._lib.cxxrtl_commit.restype = c_int + self._lib.cxxrtl_commit.argtypes = [c_void_p] + + # size_t cxxrtl_step(cxxrtl_handle) + self._lib.cxxrtl_step.restype = c_size_t + self._lib.cxxrtl_step.argtypes = [c_void_p] + + # struct cxxrtl_object *cxxrtl_get_parts(cxxrtl_handle, const char*, size_t*) + # Note: cxxrtl_get is an inline function in the header, we use cxxrtl_get_parts + self._lib.cxxrtl_get_parts.restype = POINTER(CxxrtlObject) + self._lib.cxxrtl_get_parts.argtypes = [c_void_p, c_char_p, POINTER(c_size_t)] + + # void cxxrtl_enum(cxxrtl_handle, void*, callback) + self._lib.cxxrtl_enum.restype = None + self._lib.cxxrtl_enum.argtypes = [c_void_p, c_void_p, EnumCallback] + + def _create_handle(self) -> None: + """Create the CXXRTL simulation handle.""" + toplevel = self._toplevel_create() + if not toplevel: + raise RuntimeError("Failed to create CXXRTL toplevel") + + self._handle = self._lib.cxxrtl_create(toplevel) + if not self._handle: + raise RuntimeError("Failed to create CXXRTL handle") + + def _discover_objects(self) -> None: + """Enumerate all objects in the design and cache them.""" + self._objects.clear() + names: list[str] = [] + + @EnumCallback + def callback(data, name, obj, parts): + name_str = name.decode("utf-8") + names.append(name_str) + + self._lib.cxxrtl_enum(self._handle, None, callback) + + # Now fetch each object individually using cxxrtl_get_parts + for name in names: + parts = c_size_t(0) + obj_ptr = self._lib.cxxrtl_get_parts( + self._handle, name.encode("utf-8"), ctypes.byref(parts) + ) + # Only store single-part objects (like cxxrtl_get does) + if obj_ptr and parts.value == 1: + self._objects[name] = obj_ptr.contents + + def reset(self) -> None: + """Reset the simulation to initial state.""" + self._lib.cxxrtl_reset(self._handle) + + def eval(self) -> bool: + """Evaluate combinatorial logic. + + Returns: + True if the design converged immediately + """ + return bool(self._lib.cxxrtl_eval(self._handle)) + + def commit(self) -> bool: + """Commit sequential state. + + Returns: + True if any state changed + """ + return bool(self._lib.cxxrtl_commit(self._handle)) + + def step(self) -> int: + """Simulate to a fixed point (eval + commit until stable). + + Returns: + Number of delta cycles + """ + return self._lib.cxxrtl_step(self._handle) + + def get(self, name: str) -> int: + """Get the current value of a signal. + + Args: + name: Signal name (e.g., "i_clk" or "o_data") + + Returns: + Current value as an integer + """ + obj = self._get_object(name) + return self._read_value(obj) + + def set(self, name: str, value: int) -> None: + """Set the next value of a signal. + + Args: + name: Hierarchical signal name + value: Value to set + """ + obj = self._get_object(name) + self._write_value(obj, value) + + def _get_object(self, name: str) -> CxxrtlObject: + """Get object by name, with caching.""" + if name in self._objects: + return self._objects[name] + + # Try fetching directly using cxxrtl_get_parts + parts = c_size_t(0) + obj_ptr = self._lib.cxxrtl_get_parts( + self._handle, name.encode("utf-8"), ctypes.byref(parts) + ) + if not obj_ptr or parts.value != 1: + raise KeyError(f"Signal not found: {name}") + + self._objects[name] = obj_ptr.contents + return self._objects[name] + + def _read_value(self, obj: CxxrtlObject) -> int: + """Read value from object's curr buffer.""" + if not obj.curr: + return 0 + + num_chunks = (obj.width + 31) // 32 + value = 0 + for i in range(num_chunks): + value |= obj.curr[i] << (i * 32) + + # Mask to actual width + if obj.width < 64: + value &= (1 << obj.width) - 1 + + return value + + def _write_value(self, obj: CxxrtlObject, value: int) -> None: + """Write value to object's next buffer.""" + if not obj.next: + raise RuntimeError( + f"Cannot write to read-only object (type={obj.type}, flags={obj.flags})" + ) + + num_chunks = (obj.width + 31) // 32 + for i in range(num_chunks): + obj.next[i] = (value >> (i * 32)) & 0xFFFFFFFF + + def signals(self) -> Iterator[Tuple[str, CxxrtlObject]]: + """Iterate over all signals in the design. + + Yields: + Tuples of (name, object) for each signal + """ + yield from self._objects.items() + + def inputs(self) -> Iterator[Tuple[str, CxxrtlObject]]: + """Iterate over input signals.""" + for name, obj in self._objects.items(): + if obj.flags & CxxrtlFlag.INPUT: + yield name, obj + + def outputs(self) -> Iterator[Tuple[str, CxxrtlObject]]: + """Iterate over output signals.""" + for name, obj in self._objects.items(): + if obj.flags & CxxrtlFlag.OUTPUT: + yield name, obj + + def close(self) -> None: + """Release simulation resources.""" + if hasattr(self, "_handle") and hasattr(self, "_lib"): + self._lib.cxxrtl_destroy(self._handle) + del self._handle + + def __enter__(self) -> "CxxrtlSimulator": + return self + + def __exit__(self, *args) -> None: + self.close() + + def __del__(self) -> None: + self.close() diff --git a/tests/test_cxxrtl_sim.py b/tests/test_cxxrtl_sim.py new file mode 100644 index 00000000..3196e4f4 --- /dev/null +++ b/tests/test_cxxrtl_sim.py @@ -0,0 +1,228 @@ +# SPDX-License-Identifier: BSD-2-Clause +"""Tests for CXXRTL simulation infrastructure.""" + +import shutil +import unittest +from pathlib import Path + +from chipflow.sim import CxxrtlSimulator, build_cxxrtl + + +# Path to wb_timer in chipflow-digital-ip (relative to this repo) +WB_TIMER_SV = Path(__file__).parent.parent.parent / "chipflow-digital-ip" / "chipflow_digital_ip" / "io" / "sv_timer" / "wb_timer.sv" + + +def _has_yosys_slang() -> bool: + """Check if yosys with slang plugin is available.""" + import importlib.util + if importlib.util.find_spec("yowasp_yosys") is not None: + return True + if shutil.which("yosys"): + import subprocess + try: + result = subprocess.run( + ["yosys", "-m", "slang", "-p", "help read_slang"], + capture_output=True, + timeout=10 + ) + return result.returncode == 0 + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + return False + + +@unittest.skipUnless(_has_yosys_slang(), "yosys with slang plugin not available") +@unittest.skipUnless(WB_TIMER_SV.exists(), f"wb_timer.sv not found at {WB_TIMER_SV}") +class CxxrtlBuildTestCase(unittest.TestCase): + """Test building CXXRTL from SystemVerilog.""" + + def setUp(self): + self.build_dir = Path("build/test_cxxrtl_sim") + self.build_dir.mkdir(parents=True, exist_ok=True) + + def tearDown(self): + shutil.rmtree(self.build_dir, ignore_errors=True) + + def test_build_wb_timer(self): + """Test building CXXRTL library from wb_timer SystemVerilog.""" + lib_path = build_cxxrtl( + sources=[WB_TIMER_SV], + top_module="wb_timer", + output_dir=self.build_dir, + ) + + self.assertTrue(lib_path.exists()) + # Check it's a valid shared library + self.assertTrue(lib_path.stat().st_size > 0) + + +@unittest.skipUnless(_has_yosys_slang(), "yosys with slang plugin not available") +@unittest.skipUnless(WB_TIMER_SV.exists(), f"wb_timer.sv not found at {WB_TIMER_SV}") +class CxxrtlSimulatorTestCase(unittest.TestCase): + """Test CXXRTL simulator functionality.""" + + # Register addresses (word-addressed) + REG_CTRL = 0x0 + REG_COMPARE = 0x1 + REG_COUNTER = 0x2 + REG_STATUS = 0x3 + + # Control register bits + CTRL_ENABLE = 1 << 0 + CTRL_IRQ_EN = 1 << 1 + + @classmethod + def setUpClass(cls): + """Build the CXXRTL library once for all tests.""" + cls.build_dir = Path("build/test_cxxrtl_sim") + cls.build_dir.mkdir(parents=True, exist_ok=True) + + cls.lib_path = build_cxxrtl( + sources=[WB_TIMER_SV], + top_module="wb_timer", + output_dir=cls.build_dir, + ) + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.build_dir, ignore_errors=True) + + def setUp(self): + self.sim = CxxrtlSimulator(self.lib_path, "wb_timer") + + def tearDown(self): + self.sim.close() + + def _tick(self): + """Perform a clock cycle.""" + self.sim.set("i_clk", 0) + self.sim.step() + self.sim.set("i_clk", 1) + self.sim.step() + + def _reset(self): + """Reset the design.""" + self.sim.set("i_rst_n", 0) + self.sim.set("i_clk", 0) + self._tick() + self._tick() + self.sim.set("i_rst_n", 1) + self._tick() + + def _wb_write(self, addr: int, data: int): + """Wishbone write transaction.""" + self.sim.set("i_wb_cyc", 1) + self.sim.set("i_wb_stb", 1) + self.sim.set("i_wb_we", 1) + self.sim.set("i_wb_adr", addr) + self.sim.set("i_wb_dat", data) + self.sim.set("i_wb_sel", 0xF) + + # Clock until ack + for _ in range(10): + self._tick() + if self.sim.get("o_wb_ack"): + break + + self.sim.set("i_wb_cyc", 0) + self.sim.set("i_wb_stb", 0) + self.sim.set("i_wb_we", 0) + self._tick() + + def _wb_read(self, addr: int) -> int: + """Wishbone read transaction.""" + self.sim.set("i_wb_cyc", 1) + self.sim.set("i_wb_stb", 1) + self.sim.set("i_wb_we", 0) + self.sim.set("i_wb_adr", addr) + self.sim.set("i_wb_sel", 0xF) + + # Clock until ack + for _ in range(10): + self._tick() + if self.sim.get("o_wb_ack"): + break + + data = self.sim.get("o_wb_dat") + + self.sim.set("i_wb_cyc", 0) + self.sim.set("i_wb_stb", 0) + self._tick() + + return data + + def test_signal_discovery(self): + """Test that signals are discovered correctly.""" + signals = list(self.sim.signals()) + self.assertGreater(len(signals), 0) + + # Check for expected signals + signal_names = [name for name, _ in signals] + self.assertIn("i_clk", signal_names) + self.assertIn("i_rst_n", signal_names) + self.assertIn("o_irq", signal_names) + + def test_reset(self): + """Test reset clears state.""" + self._reset() + + ctrl = self._wb_read(self.REG_CTRL) + self.assertEqual(ctrl, 0, "CTRL should be 0 after reset") + + def test_register_write_read(self): + """Test register write and readback.""" + self._reset() + + # Write to COMPARE register + self._wb_write(self.REG_COMPARE, 0x12345678) + + # Read back + value = self._wb_read(self.REG_COMPARE) + self.assertEqual(value, 0x12345678, "COMPARE should retain written value") + + def test_timer_counting(self): + """Test that the timer counts when enabled.""" + self._reset() + + # Set high compare value so we don't trigger match + self._wb_write(self.REG_COMPARE, 0xFFFFFFFF) + + # Enable timer + self._wb_write(self.REG_CTRL, self.CTRL_ENABLE) + + # Run for some cycles + for _ in range(20): + self._tick() + + # Read counter - should have incremented + counter = self._wb_read(self.REG_COUNTER) + self.assertGreater(counter, 0, "Counter should have incremented") + + def test_compare_match_irq(self): + """Test that compare match generates IRQ.""" + self._reset() + + # Set compare to 5 + self._wb_write(self.REG_COMPARE, 5) + + # Enable timer with IRQ + self._wb_write(self.REG_CTRL, self.CTRL_ENABLE | self.CTRL_IRQ_EN) + + # Run until IRQ + irq_fired = False + for _ in range(50): + self._tick() + if self.sim.get("o_irq"): + irq_fired = True + break + + self.assertTrue(irq_fired, "IRQ should fire on compare match") + + # Check status register + status = self._wb_read(self.REG_STATUS) + self.assertTrue(status & 0x1, "IRQ pending flag should be set") + self.assertTrue(status & 0x2, "Match flag should be set") + + +if __name__ == "__main__": + unittest.main()