Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 46 additions & 11 deletions chipflow/rtl/wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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_<NAME>=<value>`` 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()

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -1228,23 +1263,23 @@ 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)

elif config.generate.generator == Generators.SYSTEMVERILOG:
# 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)

elif config.generate.generator == Generators.YOSYS_SLANG:
# 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)

Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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::
Expand Down
63 changes: 63 additions & 0 deletions docs/rtl-wrapper.rst
Original file line number Diff line number Diff line change
@@ -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 <https://github.com/ChipFlow/chipflow-training/blob/main/rtl-wrapper.md>`__ — TOML reference, Wishbone-timer example, preprocessing.
- `Wrapping External RTL <https://github.com/ChipFlow/chipflow-training/blob/main/wrapping-external-rtl.md>`__ — manual ``Instance(...)`` path for simple modules.
- `Wrapping CV32E40P <https://github.com/ChipFlow/chipflow-training/blob/main/cv32e40p-example.md>`__ — 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_<NAME>=<value>`` 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.
129 changes: 129 additions & 0 deletions tests/test_rtl_wrapper.py
Original file line number Diff line number Diff line change
@@ -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_<NAME>`` 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()
Loading