From 622a3d090db13a018d3b7bdfc91d957e8b62d4b5 Mon Sep 17 00:00:00 2001 From: Serge Rabyking Date: Wed, 22 Apr 2026 12:51:05 +0200 Subject: [PATCH 1/4] feat(rtl): support Verilog module parameters in RTLWrapper Expose Verilog `parameter`/`localparam` overrides through the RTL wrapper: - New top-level `parameters` table in the wrapper TOML supplies defaults. - `load_wrapper_from_toml(..., parameters=...)` and `RTLWrapper(..., parameters=...)` accept a caller override that merges on top of the TOML defaults. - The merged set is emitted as `p_=` kwargs on `Instance()` at elaboration, and is also fed into generator template substitution (SpinalHDL, sv2v, yosys-slang) so generated Verilog sees the final values. - `{name}` substitution in `Port.params` now resolves against the merged module parameters first, falling back to `generate.parameters` for back-compat. --- chipflow/rtl/wrapper.py | 57 +++++++++++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 11 deletions(-) diff --git a/chipflow/rtl/wrapper.py b/chipflow/rtl/wrapper.py index 57d1f19f..71500d7c 100644 --- a/chipflow/rtl/wrapper.py +++ b/chipflow/rtl/wrapper.py @@ -371,6 +371,7 @@ class ExternalWrapConfig(BaseModel): ports: Dict[str, Port] = {} pins: Dict[str, Port] = {} driver: Optional[DriverConfig] = None + parameters: Dict[str, JsonValue] = {} def _resolve_interface_type(interface_str: str) -> type | tuple: @@ -768,17 +769,30 @@ class RTLWrapper(wiring.Component): then matching patterns to identify which signals correspond to interface members. """ - def __init__(self, config: ExternalWrapConfig, verilog_files: List[Path] | None = None): + def __init__( + self, + config: ExternalWrapConfig, + verilog_files: List[Path] | None = None, + parameters: Dict[str, JsonValue] | None = None, + ): """Initialize the RTL wrapper. Args: config: Parsed TOML configuration verilog_files: List of Verilog file paths to include + parameters: Verilog module parameter overrides. Merged on top of + ``config.parameters`` (TOML defaults), then emitted as + ``p_=`` kwargs on the ``Instance()`` at elaboration. """ self._config = config self._verilog_files = verilog_files or [] self._port_mappings: Dict[str, Dict[str, str]] = {} + # Effective Verilog module parameters: TOML defaults + caller overrides. + self._parameters: Dict[str, JsonValue] = dict(config.parameters) + if parameters: + self._parameters.update(parameters) + # Parse Verilog to get port information for auto-mapping verilog_ports = self._parse_verilog_ports() @@ -994,13 +1008,17 @@ def _create_signature_member( # Complex interface class - instantiate with params params = port_config.params or {} - # Resolve parameter references from generate.parameters + # Resolve "{name}" references against the merged module parameters, + # falling back to legacy ``generate.parameters`` for back-compat. resolved_params = {} for k, v in params.items(): if isinstance(v, str) and v.startswith("{") and v.endswith("}"): param_name = v[1:-1] - if config.generate and config.generate.parameters: - resolved_params[k] = config.generate.parameters.get(param_name, v) + if param_name in self._parameters: + resolved_params[k] = self._parameters[param_name] + elif config.generate and config.generate.parameters \ + and param_name in config.generate.parameters: + resolved_params[k] = config.generate.parameters[param_name] else: resolved_params[k] = v else: @@ -1073,8 +1091,11 @@ def elaborate(self, platform): # Use it directly for the Instance parameter instance_ports[verilog_signal] = amaranth_signal + # Verilog module parameter overrides (`parameter`/`localparam`). + instance_params = {f"p_{name}": value for name, value in self._parameters.items()} + # Create the Verilog instance - m.submodules.wrapped = Instance(self._config.name, **instance_ports) + m.submodules.wrapped = Instance(self._config.name, **instance_params, **instance_ports) # Add Verilog files to the platform if platform is not None: @@ -1176,13 +1197,19 @@ def build_simulator( def load_wrapper_from_toml( - toml_path: Path | str, generate_dest: Path | None = None + toml_path: Path | str, + generate_dest: Path | None = None, + parameters: Dict[str, JsonValue] | None = None, ) -> RTLWrapper: """Load an RTLWrapper from a TOML configuration file. Args: toml_path: Path to the TOML configuration file generate_dest: Destination directory for generated Verilog (if using SpinalHDL) + parameters: Verilog module parameter overrides. Merged on top of the TOML's + ``parameters`` table and applied both to code generation (so generators + see the final values when templating their command-line) and to the + ``Instance()`` of the wrapped module at elaboration. Returns: Configured RTLWrapper component @@ -1206,6 +1233,11 @@ def load_wrapper_from_toml( error_str = "\n".join(error_messages) raise ChipFlowError(f"Validation error in {toml_path}:\n{error_str}") + # Final Verilog module parameters: TOML defaults + caller overrides. + effective_parameters: Dict[str, JsonValue] = dict(config.parameters) + if parameters: + effective_parameters.update(parameters) + verilog_files = [] # Get source path, resolving relative paths against the TOML file's directory @@ -1219,7 +1251,10 @@ def load_wrapper_from_toml( generate_dest = Path("./build/verilog") generate_dest.mkdir(parents=True, exist_ok=True) - parameters = config.generate.parameters or {} + # Generator template params: existing generate.parameters plus module params. + # Module parameter names win on collision so overrides propagate. + gen_parameters: Dict[str, JsonValue] = dict(config.generate.parameters or {}) + gen_parameters.update(effective_parameters) if config.generate.generator == Generators.SPINALHDL: if config.generate.spinalhdl is None: @@ -1228,7 +1263,7 @@ def load_wrapper_from_toml( ) generated = config.generate.spinalhdl.generate( - source_path, generate_dest, config.name, parameters + source_path, generate_dest, config.name, gen_parameters ) verilog_files.extend(generate_dest / f for f in generated) @@ -1236,7 +1271,7 @@ def load_wrapper_from_toml( # Convert SystemVerilog to Verilog using sv2v sv2v_config = config.generate.sv2v or GenerateSV2V() generated = sv2v_config.generate( - source_path, generate_dest, config.name, parameters + source_path, generate_dest, config.name, gen_parameters ) verilog_files.extend(generated) @@ -1244,7 +1279,7 @@ def load_wrapper_from_toml( # Convert SystemVerilog to Verilog using yosys-slang yosys_slang_config = config.generate.yosys_slang or GenerateYosysSlang() generated = yosys_slang_config.generate( - source_path, generate_dest, config.name, parameters + source_path, generate_dest, config.name, gen_parameters ) verilog_files.extend(generated) @@ -1277,7 +1312,7 @@ def load_wrapper_from_toml( resolved_c_files.append(str(c_path)) config.driver.c_files = resolved_c_files - return RTLWrapper(config, verilog_files) + return RTLWrapper(config, verilog_files, parameters=parameters) # CLI entry point for testing From b24300bad9eabe95b48f182ff113e731340c67fb Mon Sep 17 00:00:00 2001 From: Serge Rabyking Date: Wed, 22 Apr 2026 12:57:28 +0200 Subject: [PATCH 2/4] test(rtl): cover RTLWrapper parameter merge and emission Add tests/test_rtl_wrapper.py covering: - Parameter merge precedence: TOML defaults only, kwarg only, kwarg overrides TOML, kwarg adds to TOML. - elaborate() emits merged parameters as p_ kwargs on the wrapped Instance(), and emits no p_* kwargs when no parameters are set. - load_wrapper_from_toml() threads the caller override into the final _parameters set for both TOML-populated and empty [parameters] tables. --- tests/test_rtl_wrapper.py | 129 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 tests/test_rtl_wrapper.py diff --git a/tests/test_rtl_wrapper.py b/tests/test_rtl_wrapper.py new file mode 100644 index 00000000..408b3926 --- /dev/null +++ b/tests/test_rtl_wrapper.py @@ -0,0 +1,129 @@ +# SPDX-License-Identifier: BSD-2-Clause +"""Tests for RTLWrapper parameter handling.""" + +import shutil +import tempfile +import unittest +import unittest.mock +from pathlib import Path + +from chipflow.rtl import wrapper as wrapper_mod +from chipflow.rtl.wrapper import ( + ExternalWrapConfig, + Files, + RTLWrapper, + load_wrapper_from_toml, +) + + +def _make_config(parameters=None): + """Build a minimal ExternalWrapConfig usable for RTLWrapper unit tests.""" + return ExternalWrapConfig( + name="dummy_mod", + files=Files(path=Path("/tmp/unused-for-unit-tests")), + parameters=parameters or {}, + ) + + +class ParameterMergeTestCase(unittest.TestCase): + """Merge precedence between TOML ``[parameters]`` and Python kwarg.""" + + def test_no_parameters(self): + w = RTLWrapper(_make_config()) + self.assertEqual(w._parameters, {}) + + def test_toml_defaults_only(self): + w = RTLWrapper(_make_config(parameters={"WIDTH": 32, "DEPTH": 8})) + self.assertEqual(w._parameters, {"WIDTH": 32, "DEPTH": 8}) + + def test_kwarg_only(self): + w = RTLWrapper(_make_config(), parameters={"WIDTH": 64}) + self.assertEqual(w._parameters, {"WIDTH": 64}) + + def test_kwarg_overrides_toml(self): + w = RTLWrapper( + _make_config(parameters={"WIDTH": 32, "DEPTH": 8}), + parameters={"WIDTH": 64}, + ) + self.assertEqual(w._parameters, {"WIDTH": 64, "DEPTH": 8}) + + +class ElaborateEmitsParametersTestCase(unittest.TestCase): + """elaborate() must emit merged parameters as ``p_`` on ``Instance``.""" + + def test_p_prefixed_kwargs(self): + w = RTLWrapper( + _make_config(parameters={"WIDTH": 32}), + parameters={"DEPTH": 8}, + ) + + captured = {} + real_instance = wrapper_mod.Instance + + def fake_instance(name, **kwargs): + captured["name"] = name + captured["kwargs"] = kwargs + return real_instance(name, **kwargs) + + with unittest.mock.patch.object(wrapper_mod, "Instance", fake_instance): + w.elaborate(platform=None) + + self.assertEqual(captured["name"], "dummy_mod") + self.assertEqual(captured["kwargs"].get("p_WIDTH"), 32) + self.assertEqual(captured["kwargs"].get("p_DEPTH"), 8) + + def test_no_parameters_emits_no_p_kwargs(self): + w = RTLWrapper(_make_config()) + + captured = {} + real_instance = wrapper_mod.Instance + + def fake_instance(name, **kwargs): + captured["kwargs"] = kwargs + return real_instance(name, **kwargs) + + with unittest.mock.patch.object(wrapper_mod, "Instance", fake_instance): + w.elaborate(platform=None) + + p_kwargs = {k: v for k, v in captured["kwargs"].items() if k.startswith("p_")} + self.assertEqual(p_kwargs, {}) + + +class LoadFromTomlTestCase(unittest.TestCase): + """``load_wrapper_from_toml`` must thread parameters through the merge.""" + + def setUp(self): + self.tmpdir = Path(tempfile.mkdtemp()) + # A minimal Verilog file so `source_path.glob("**/*.v")` finds something. + (self.tmpdir / "dummy_mod.v").write_text("module dummy_mod(); endmodule\n") + self.toml_path = self.tmpdir / "wrapper.toml" + + def tearDown(self): + shutil.rmtree(self.tmpdir, ignore_errors=True) + + def _write_toml(self, extra=""): + self.toml_path.write_text( + f'name = "dummy_mod"\n' + f'[files]\n' + f'path = "{self.tmpdir}"\n' + + extra + ) + + def test_toml_parameters_only(self): + self._write_toml("[parameters]\nWIDTH = 32\nDEPTH = 8\n") + w = load_wrapper_from_toml(self.toml_path) + self.assertEqual(w._parameters, {"WIDTH": 32, "DEPTH": 8}) + + def test_kwarg_overrides_toml(self): + self._write_toml("[parameters]\nWIDTH = 32\nDEPTH = 8\n") + w = load_wrapper_from_toml(self.toml_path, parameters={"WIDTH": 64}) + self.assertEqual(w._parameters, {"WIDTH": 64, "DEPTH": 8}) + + def test_kwarg_without_toml_table(self): + self._write_toml() + w = load_wrapper_from_toml(self.toml_path, parameters={"WIDTH": 64}) + self.assertEqual(w._parameters, {"WIDTH": 64}) + + +if __name__ == "__main__": + unittest.main() From a7229ff2f164b03a9eaeee83812327b7577c2219 Mon Sep 17 00:00:00 2001 From: Serge Rabyking Date: Wed, 22 Apr 2026 13:08:06 +0200 Subject: [PATCH 3/4] docs: add RTL wrapper reference page linking to training docs Add a short docs/rtl-wrapper.rst that introduces the RTL wrapper, points to the in-depth usage guide and worked examples in chipflow-training, and demonstrates the new [parameters] table plus Python override. Autoapi continues to generate the full API reference for RTLWrapper, load_wrapper_from_toml, and ExternalWrapConfig. --- docs/index.rst | 1 + docs/rtl-wrapper.rst | 63 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 docs/rtl-wrapper.rst diff --git a/docs/index.rst b/docs/index.rst index b17bc011..5466e5a7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -15,6 +15,7 @@ It is developed at https://github.com/chipFlow/chipflow-lib/ and licensed `BSD 2 chipflow-toml-guide chipflow-commands using-pin-signatures + rtl-wrapper platform-api .. toctree:: diff --git a/docs/rtl-wrapper.rst b/docs/rtl-wrapper.rst new file mode 100644 index 00000000..683c1a67 --- /dev/null +++ b/docs/rtl-wrapper.rst @@ -0,0 +1,63 @@ +Wrapping External RTL +===================== + +``chipflow.rtl.wrapper`` turns a TOML description of an external Verilog or +SystemVerilog module into an Amaranth :py:class:`~amaranth.lib.wiring.Component` +ready to drop into a design. The TOML specifies the source files, clocks and +resets, bus/pin interfaces, optional preprocessing (sv2v, yosys-slang, +SpinalHDL), and — as of this release — Verilog module ``parameter`` overrides. + +This page is an API-level pointer. For the full usage guide, worked examples, +and the TOML reference, see the ChipFlow training material: + +- `RTLWrapper: Wrapping External RTL via TOML `__ — TOML reference, Wishbone-timer example, preprocessing. +- `Wrapping External RTL `__ — manual ``Instance(...)`` path for simple modules. +- `Wrapping CV32E40P `__ — a worked example with sv2v preprocessing. + +Quick reference +--------------- + +Load a wrapper from a TOML file and instantiate it inside a design: + +.. code-block:: python + + from chipflow.rtl.wrapper import load_wrapper_from_toml + + class MyDesign(wiring.Component): + def elaborate(self, platform): + m = Module() + m.submodules.timer = load_wrapper_from_toml("wb_timer.toml") + return m + +Supply Verilog ``parameter`` overrides from TOML, from Python, or both (the +Python kwarg wins on collisions; unmentioned parameters fall back to the TOML +table): + +.. code-block:: toml + + # wb_timer.toml + name = "wb_timer" + + [parameters] + DATA_WIDTH = 32 + ADDR_WIDTH = 4 + +.. code-block:: python + + # caller overrides DATA_WIDTH; ADDR_WIDTH=4 still applies + w = load_wrapper_from_toml("wb_timer.toml", parameters={"DATA_WIDTH": 64}) + +The merged parameters are emitted as ``p_=`` kwargs on the +``Instance()`` at elaboration, and are also fed into generator template +substitution (so SpinalHDL / sv2v / yosys-slang see the final values when +producing Verilog). + +API +--- + +- :py:class:`chipflow.rtl.wrapper.RTLWrapper` — the generated component. +- :py:func:`chipflow.rtl.wrapper.load_wrapper_from_toml` — loader that parses + the TOML, runs any configured preprocessing, and returns an + :py:class:`~chipflow.rtl.wrapper.RTLWrapper`. +- :py:class:`chipflow.rtl.wrapper.ExternalWrapConfig` — Pydantic schema for the + TOML configuration. From d740a694f896a7f44ce5d33460fc6c5dde934535 Mon Sep 17 00:00:00 2001 From: Serge Rabyking Date: Wed, 22 Apr 2026 13:26:43 +0200 Subject: [PATCH 4/4] ci: trigger CI workflow after re-enable