11 Commits

Author SHA1 Message Date
Byron Lathi
b2fd006f7f Gate assertions behind "PEAKRDL_ASSERTIONS define" 2025-11-23 17:34:13 -08:00
Byron Lathi
59724105c5 Add taxi apb interface 2025-11-23 17:34:10 -08:00
Byron Lathi
217ff15431 Downsize paddr bits 2025-11-23 17:34:07 -08:00
Byron Lathi
35d66db1b8 Change required systemrdl-compiler verison to ~1.31 to match peakrdl 2025-11-23 17:26:48 -08:00
Arnav Sacheti
f0f25a6d92 update devcontainer extensions 2025-11-12 07:20:11 +00:00
Arnav Sacheti
a9653c8497 Tests/cocotb (#19)
* wip

* reorg

* update sv int

* apb4 working

* apb3 working

* version bump + ignore runner warning

* remove redundant check

* adding log on failure

* cleaning up verilator version issue

* devcontainer

* Fix missing libpython in GitHub Actions CI environment (#21)

* Initial plan

* Install libpython in GitHub Actions for cocotb tests

Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
2025-11-10 23:00:28 -08:00
Copilot
d7481e71ba Fix max_decode_depth to control decoder hierarchy and port generation (#18)
* Initial plan

* Fix max_decode_depth to properly control decoder hierarchy and port generation

Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>

* Fix test that relied on old depth behavior

Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>

* Update documentation for max_decode_depth parameter

Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>

* fix format

* Add variable_depth RDL file and smoke tests for max_decode_depth parameter

Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>

* Add variable depth tests for APB3 and AXI4-Lite CPUIFs

Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>

* fix

* fix

* bump

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>
2025-10-28 23:38:54 -07:00
Arnav Sacheti
858a7870ad version bump 2025-10-28 22:06:49 -07:00
Copilot
3d823572cc Fix nonconstant index errors in Questa by using unpacked structs and interface array intermediates (#17)
* Initial plan

* Fix nonconstant index error by using unpacked structs and arrays

Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>

* Add comprehensive tests for Questa compatibility with instance arrays

Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>

* Add intermediate signals for interface array fanin to fix Questa compatibility

Questa simulator rejects indexing interface arrays with non-constant variables in procedural blocks (always_comb). This fix creates intermediate unpacked array signals that are assigned from interface arrays using generate loops (with genvar), then references those intermediates in the fanin logic.

Changes:
- Added fanin_intermediate_gen.py to generate intermediate signals
- Modified APB3/APB4/AXI4-Lite cpuif classes to use intermediates for interface arrays
- Updated templates to include intermediate signal section
- Intermediate signals use genvar indexing (legal) instead of variable indexing (illegal in Questa)

Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>

* Fix type checking and formatting issues in fanin intermediate generator

- Added proper null checks for node.array_dimensions to avoid type errors
- Used getattr() to safely access is_interface and _interface attributes
- Added early returns when interface is None to prevent errors
- Fixed formatting issues in fanin_intermediate_gen.py
- All type checks now pass (only pre-existing errors remain in other files)
- All 67 tests still pass

Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>

* Fix test collection when cocotb is not installed

The cocotb test files (test_runner.py) have imports that fail at module level when cocotb is not installed, causing pytest collection to fail. This prevents running tests in environments without cocotb dependencies.

Solution: Updated conftest.py to conditionally ignore cocotb tests when cocotb is not available, allowing tests to pass in both environments (with and without cocotb).

- When cocotb is available: only ignore test_register_access.py files (existing behavior)
- When cocotb is not available: ignore all files under cocotb/** directories

This allows CI to run all 67 non-simulation tests successfully without requiring cocotb/verilator dependencies for basic testing.

Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>

* add default for unpacked struct

* cocotb is test dependancy

* Refactor fanin intermediate signal logic to cpuif level

Moved the interface-specific signal assignment logic from fanin_intermediate_gen.py to individual cpuif classes (APB3Cpuif, APB4Cpuif, AXI4LiteCpuif). This follows better architecture principles where each cpuif knows which signals it needs.

Changes:
- Added fanin_intermediate_assignments() method to BaseCpuif
- Implemented fanin_intermediate_assignments() in APB3Cpuif, APB4Cpuif, and AXI4LiteCpuif
- Updated FaninIntermediateGenerator to call the cpuif method instead of checking interface type
- Removed interface type checking logic from fanin_intermediate_gen.py

This makes the code more maintainable and follows the single responsibility principle - each cpuif class knows its own signal requirements.

Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>
2025-10-28 22:03:57 -07:00
Arnav Sacheti
f829e3894f format xargs 2025-10-27 20:34:41 -07:00
Arnav Sacheti
74eb2344b1 change to pypi gh action 2025-10-26 19:53:13 -07:00
63 changed files with 3536 additions and 419 deletions

22
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1,22 @@
FROM verilator/verilator:latest
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && \
apt-get install -y --no-install-recommends \
python3 \
python3-venv \
python3-pip \
python3-dev \
build-essential \
pkg-config \
git \
curl \
ca-certificates && \
rm -rf /var/lib/apt/lists/*
ENV UV_INSTALL_DIR=/usr/local/bin
ENV UV_LINK_MODE=copy
# Install uv globally so both VS Code and terminals can use it
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
RUN uv --version

View File

@@ -0,0 +1,36 @@
{
"name": "PeakRDL BusDecoder",
"build": {
"dockerfile": "Dockerfile"
},
"runArgs": [
"--init"
],
"features": {
"ghcr.io/devcontainers/features/common-utils:2": {
"username": "vscode",
"uid": "1000",
"gid": "1000",
"installZsh": "false",
"installOhMyZsh": "false"
}
},
"remoteUser": "vscode",
"postCreateCommand": "uv sync --frozen --all-extras --group tools --group test",
"customizations": {
"vscode": {
"settings": {
"python.defaultInterpreterPath": ".venv/bin/python",
"terminal.integrated.shell.linux": "/bin/bash"
},
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"ms-vscode.cpptools",
"charliermarsh.ruff",
"astral-sh.ty",
"meta.pyrefly"
]
}
}
}

View File

@@ -40,7 +40,6 @@ jobs:
- name: Publish to PyPI - name: Publish to PyPI
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
env: uses: pypa/gh-action-pypi-publish@release/v1
TWINE_USERNAME: __token__ with:
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} skip-existing: true
run: uvx twine upload dist/*

View File

@@ -14,6 +14,8 @@ on:
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container:
image: verilator/verilator:latest
permissions: permissions:
contents: read contents: read
strategy: strategy:
@@ -27,19 +29,21 @@ jobs:
uses: astral-sh/setup-uv@v3 uses: astral-sh/setup-uv@v3
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
enable-cache: true
- name: Install Verilator - name: Check Verilator version
run: verilator --version
- name: Install Python development packages
run: | run: |
sudo apt-get update apt-get update && apt-get install -y python3-dev libpython3-dev
sudo apt-get install -y verilator
verilator --version
- name: Install dependencies - name: Install dependencies
run: | run: |
uv sync --all-extras --group test uv sync --all-extras --group test
- name: Run tests - name: Run tests
run: uv run pytest tests/ -v --cov=peakrdl_busdecoder --cov-report=xml --cov-report=term run: uv run pytest tests/ --cov=peakrdl_busdecoder --cov-report=xml --cov-report=term
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v4

View File

@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "peakrdl-busdecoder" name = "peakrdl-busdecoder"
version = "0.2.0" version = "0.6.0"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = ["jinja2>=3.1.6", "systemrdl-compiler~=1.30.1"] dependencies = ["jinja2>=3.1.6", "systemrdl-compiler~=1.31"]
authors = [{ name = "Alex Mykyta" }] authors = [{ name = "Arnav Sacheti" }]
description = "Generate a SystemVerilog bus decoder from SystemRDL for splitting CPU interfaces to multiple sub-address spaces" description = "Generate a SystemVerilog bus decoder from SystemRDL for splitting CPU interfaces to multiple sub-address spaces"
readme = "README.md" readme = "README.md"
license = { text = "LGPLv3" } license = { text = "LGPLv3" }
@@ -114,3 +114,4 @@ markers = [
"simulation: marks tests as requiring cocotb simulation (deselect with '-m \"not simulation\"')", "simulation: marks tests as requiring cocotb simulation (deselect with '-m \"not simulation\"')",
"verilator: marks tests as requiring verilator simulator (deselect with '-m \"not verilator\"')", "verilator: marks tests as requiring verilator simulator (deselect with '-m \"not verilator\"')",
] ]
filterwarnings = ["error", "ignore::UserWarning"]

View File

@@ -5,7 +5,7 @@ from peakrdl.config import schema
from peakrdl.plugins.entry_points import get_entry_points from peakrdl.plugins.entry_points import get_entry_points
from peakrdl.plugins.exporter import ExporterSubcommandPlugin from peakrdl.plugins.exporter import ExporterSubcommandPlugin
from .cpuif import BaseCpuif, apb3, apb4, axi4lite from .cpuif import BaseCpuif, apb3, apb4, axi4lite, taxi_apb
from .exporter import BusDecoderExporter from .exporter import BusDecoderExporter
from .udps import ALL_UDPS from .udps import ALL_UDPS
@@ -24,6 +24,7 @@ def get_cpuifs(config: list[tuple[str, Any]]) -> dict[str, type[BaseCpuif]]:
"apb3-flat": apb3.APB3CpuifFlat, "apb3-flat": apb3.APB3CpuifFlat,
"apb4": apb4.APB4Cpuif, "apb4": apb4.APB4Cpuif,
"apb4-flat": apb4.APB4CpuifFlat, "apb4-flat": apb4.APB4CpuifFlat,
"taxi-apb": taxi_apb.TaxiAPBCpuif,
"axi4-lite": axi4lite.AXI4LiteCpuif, "axi4-lite": axi4lite.AXI4LiteCpuif,
"axi4-lite-flat": axi4lite.AXI4LiteCpuifFlat, "axi4-lite-flat": axi4lite.AXI4LiteCpuifFlat,
} }
@@ -116,7 +117,9 @@ class Exporter(ExporterSubcommandPlugin):
type=int, type=int,
default=1, default=1,
help="""Maximum depth for address decoder to descend into nested help="""Maximum depth for address decoder to descend into nested
addressable components. Default is 1. addressable components. Value of 0 decodes all levels (infinite depth).
Value of 1 decodes only top-level children. Value of 2 decodes top-level
and one level deeper, etc. Default is 1.
""", """,
) )

View File

@@ -52,8 +52,16 @@ class APB3Cpuif(BaseCpuif):
fanin["cpuif_rd_ack"] = "'0" fanin["cpuif_rd_ack"] = "'0"
fanin["cpuif_rd_err"] = "'0" fanin["cpuif_rd_err"] = "'0"
else: else:
fanin["cpuif_rd_ack"] = self.signal("PREADY", node, "i") # Use intermediate signals for interface arrays to avoid
fanin["cpuif_rd_err"] = self.signal("PSLVERR", node, "i") # non-constant indexing of interface arrays in procedural blocks
if self.is_interface and node.is_array and node.array_dimensions:
# Generate array index string [i0][i1]... for the intermediate signal
array_idx = "".join(f"[i{i}]" for i in range(len(node.array_dimensions)))
fanin["cpuif_rd_ack"] = f"{node.inst_name}_fanin_ready{array_idx}"
fanin["cpuif_rd_err"] = f"{node.inst_name}_fanin_err{array_idx}"
else:
fanin["cpuif_rd_ack"] = self.signal("PREADY", node, "i")
fanin["cpuif_rd_err"] = self.signal("PSLVERR", node, "i")
return "\n".join(map(lambda kv: f"{kv[0]} = {kv[1]};", fanin.items())) return "\n".join(map(lambda kv: f"{kv[0]} = {kv[1]};", fanin.items()))
@@ -62,6 +70,23 @@ class APB3Cpuif(BaseCpuif):
if node is None: if node is None:
fanin["cpuif_rd_data"] = "'0" fanin["cpuif_rd_data"] = "'0"
else: else:
fanin["cpuif_rd_data"] = self.signal("PRDATA", node, "i") # Use intermediate signals for interface arrays to avoid
# non-constant indexing of interface arrays in procedural blocks
if self.is_interface and node.is_array and node.array_dimensions:
# Generate array index string [i0][i1]... for the intermediate signal
array_idx = "".join(f"[i{i}]" for i in range(len(node.array_dimensions)))
fanin["cpuif_rd_data"] = f"{node.inst_name}_fanin_data{array_idx}"
else:
fanin["cpuif_rd_data"] = self.signal("PRDATA", node, "i")
return "\n".join(map(lambda kv: f"{kv[0]} = {kv[1]};", fanin.items())) return "\n".join(map(lambda kv: f"{kv[0]} = {kv[1]};", fanin.items()))
def fanin_intermediate_assignments(
self, node: AddressableNode, inst_name: str, array_idx: str, master_prefix: str, indexed_path: str
) -> list[str]:
"""Generate intermediate signal assignments for APB3 interface arrays."""
return [
f"assign {inst_name}_fanin_ready{array_idx} = {master_prefix}{indexed_path}.PREADY;",
f"assign {inst_name}_fanin_err{array_idx} = {master_prefix}{indexed_path}.PSLVERR;",
f"assign {inst_name}_fanin_data{array_idx} = {master_prefix}{indexed_path}.PRDATA;",
]

View File

@@ -35,15 +35,15 @@ class APB3CpuifFlat(BaseCpuif):
def fanout(self, node: AddressableNode) -> str: def fanout(self, node: AddressableNode) -> str:
fanout: dict[str, str] = {} fanout: dict[str, str] = {}
fanout[self.signal("PSEL", node)] = ( fanout[self.signal("PSEL", node, "gi")] = (
f"cpuif_wr_sel.{get_indexed_path(self.exp.ds.top_node, node, 'i')}|cpuif_rd_sel.{get_indexed_path(self.exp.ds.top_node, node, 'i')}" f"cpuif_wr_sel.{get_indexed_path(self.exp.ds.top_node, node, 'gi')}|cpuif_rd_sel.{get_indexed_path(self.exp.ds.top_node, node, 'gi')}"
) )
fanout[self.signal("PENABLE", node)] = self.signal("PENABLE") fanout[self.signal("PENABLE", node, "gi")] = self.signal("PENABLE")
fanout[self.signal("PWRITE", node)] = ( fanout[self.signal("PWRITE", node, "gi")] = (
f"cpuif_wr_sel.{get_indexed_path(self.exp.ds.top_node, node, 'i')}" f"cpuif_wr_sel.{get_indexed_path(self.exp.ds.top_node, node, 'gi')}"
) )
fanout[self.signal("PADDR", node)] = self.signal("PADDR") fanout[self.signal("PADDR", node, "gi")] = self.signal("PADDR")
fanout[self.signal("PWDATA", node)] = "cpuif_wr_data" fanout[self.signal("PWDATA", node, "gi")] = "cpuif_wr_data"
return "\n".join(map(lambda kv: f"assign {kv[0]} = {kv[1]};", fanout.items())) return "\n".join(map(lambda kv: f"assign {kv[0]} = {kv[1]};", fanout.items()))
@@ -53,8 +53,8 @@ class APB3CpuifFlat(BaseCpuif):
fanin["cpuif_rd_ack"] = "'0" fanin["cpuif_rd_ack"] = "'0"
fanin["cpuif_rd_err"] = "'0" fanin["cpuif_rd_err"] = "'0"
else: else:
fanin["cpuif_rd_ack"] = self.signal("PREADY", node) fanin["cpuif_rd_ack"] = self.signal("PREADY", node, "i")
fanin["cpuif_rd_err"] = self.signal("PSLVERR", node) fanin["cpuif_rd_err"] = self.signal("PSLVERR", node, "i")
return "\n".join(map(lambda kv: f"{kv[0]} = {kv[1]};", fanin.items())) return "\n".join(map(lambda kv: f"{kv[0]} = {kv[1]};", fanin.items()))
@@ -63,6 +63,6 @@ class APB3CpuifFlat(BaseCpuif):
if node is None: if node is None:
fanin["cpuif_rd_data"] = "'0" fanin["cpuif_rd_data"] = "'0"
else: else:
fanin["cpuif_rd_data"] = self.signal("PRDATA", node) fanin["cpuif_rd_data"] = self.signal("PRDATA", node, "i")
return "\n".join(map(lambda kv: f"{kv[0]} = {kv[1]};", fanin.items())) return "\n".join(map(lambda kv: f"{kv[0]} = {kv[1]};", fanin.items()))

View File

@@ -26,6 +26,13 @@ assign {{cpuif.signal("PSLVERR")}} = cpuif_rd_err | cpuif_rd_sel.cpuif_err | cpu
// Fanout CPU Bus interface signals // Fanout CPU Bus interface signals
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
{{fanout|walk(cpuif=cpuif)}} {{fanout|walk(cpuif=cpuif)}}
{%- if cpuif.is_interface %}
//--------------------------------------------------------------------------
// Intermediate signals for interface array fanin
//--------------------------------------------------------------------------
{{fanin_intermediate|walk(cpuif=cpuif)}}
{%- endif %}
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// Fanin CPU Bus interface signals // Fanin CPU Bus interface signals

View File

@@ -55,8 +55,16 @@ class APB4Cpuif(BaseCpuif):
fanin["cpuif_rd_ack"] = "'0" fanin["cpuif_rd_ack"] = "'0"
fanin["cpuif_rd_err"] = "'0" fanin["cpuif_rd_err"] = "'0"
else: else:
fanin["cpuif_rd_ack"] = self.signal("PREADY", node, "i") # Use intermediate signals for interface arrays to avoid
fanin["cpuif_rd_err"] = self.signal("PSLVERR", node, "i") # non-constant indexing of interface arrays in procedural blocks
if self.is_interface and node.is_array and node.array_dimensions:
# Generate array index string [i0][i1]... for the intermediate signal
array_idx = "".join(f"[i{i}]" for i in range(len(node.array_dimensions)))
fanin["cpuif_rd_ack"] = f"{node.inst_name}_fanin_ready{array_idx}"
fanin["cpuif_rd_err"] = f"{node.inst_name}_fanin_err{array_idx}"
else:
fanin["cpuif_rd_ack"] = self.signal("PREADY", node, "i")
fanin["cpuif_rd_err"] = self.signal("PSLVERR", node, "i")
return "\n".join(map(lambda kv: f"{kv[0]} = {kv[1]};", fanin.items())) return "\n".join(map(lambda kv: f"{kv[0]} = {kv[1]};", fanin.items()))
@@ -65,6 +73,23 @@ class APB4Cpuif(BaseCpuif):
if node is None: if node is None:
fanin["cpuif_rd_data"] = "'0" fanin["cpuif_rd_data"] = "'0"
else: else:
fanin["cpuif_rd_data"] = self.signal("PRDATA", node, "i") # Use intermediate signals for interface arrays to avoid
# non-constant indexing of interface arrays in procedural blocks
if self.is_interface and node.is_array and node.array_dimensions:
# Generate array index string [i0][i1]... for the intermediate signal
array_idx = "".join(f"[i{i}]" for i in range(len(node.array_dimensions)))
fanin["cpuif_rd_data"] = f"{node.inst_name}_fanin_data{array_idx}"
else:
fanin["cpuif_rd_data"] = self.signal("PRDATA", node, "i")
return "\n".join(map(lambda kv: f"{kv[0]} = {kv[1]};", fanin.items())) return "\n".join(map(lambda kv: f"{kv[0]} = {kv[1]};", fanin.items()))
def fanin_intermediate_assignments(
self, node: AddressableNode, inst_name: str, array_idx: str, master_prefix: str, indexed_path: str
) -> list[str]:
"""Generate intermediate signal assignments for APB4 interface arrays."""
return [
f"assign {inst_name}_fanin_ready{array_idx} = {master_prefix}{indexed_path}.PREADY;",
f"assign {inst_name}_fanin_err{array_idx} = {master_prefix}{indexed_path}.PSLVERR;",
f"assign {inst_name}_fanin_data{array_idx} = {master_prefix}{indexed_path}.PRDATA;",
]

View File

@@ -2,7 +2,7 @@ from typing import TYPE_CHECKING
from systemrdl.node import AddressableNode from systemrdl.node import AddressableNode
from ...utils import get_indexed_path from ...utils import clog2, get_indexed_path
from ..base_cpuif import BaseCpuif from ..base_cpuif import BaseCpuif
from .apb4_interface import APB4FlatInterface from .apb4_interface import APB4FlatInterface
@@ -35,17 +35,17 @@ class APB4CpuifFlat(BaseCpuif):
def fanout(self, node: AddressableNode) -> str: def fanout(self, node: AddressableNode) -> str:
fanout: dict[str, str] = {} fanout: dict[str, str] = {}
fanout[self.signal("PSEL", node)] = ( fanout[self.signal("PSEL", node, "gi")] = (
f"cpuif_wr_sel.{get_indexed_path(self.exp.ds.top_node, node, 'i')}|cpuif_rd_sel.{get_indexed_path(self.exp.ds.top_node, node, 'i')}" f"cpuif_wr_sel.{get_indexed_path(self.exp.ds.top_node, node, 'gi')}|cpuif_rd_sel.{get_indexed_path(self.exp.ds.top_node, node, 'gi')}"
) )
fanout[self.signal("PENABLE", node)] = self.signal("PENABLE") fanout[self.signal("PENABLE", node, "gi")] = self.signal("PENABLE")
fanout[self.signal("PWRITE", node)] = ( fanout[self.signal("PWRITE", node, "gi")] = (
f"cpuif_wr_sel.{get_indexed_path(self.exp.ds.top_node, node, 'i')}" f"cpuif_wr_sel.{get_indexed_path(self.exp.ds.top_node, node, 'gi')}"
) )
fanout[self.signal("PADDR", node)] = self.signal("PADDR") fanout[self.signal("PADDR", node, "gi")] = f"{self.signal('PADDR')}[{clog2(node.size) - 1}:0]"
fanout[self.signal("PPROT", node)] = self.signal("PPROT") fanout[self.signal("PPROT", node, "gi")] = self.signal("PPROT")
fanout[self.signal("PWDATA", node)] = "cpuif_wr_data" fanout[self.signal("PWDATA", node, "gi")] = "cpuif_wr_data"
fanout[self.signal("PSTRB", node)] = "cpuif_wr_byte_en" fanout[self.signal("PSTRB", node, "gi")] = "cpuif_wr_byte_en"
return "\n".join(map(lambda kv: f"assign {kv[0]} = {kv[1]};", fanout.items())) return "\n".join(map(lambda kv: f"assign {kv[0]} = {kv[1]};", fanout.items()))
@@ -55,8 +55,8 @@ class APB4CpuifFlat(BaseCpuif):
fanin["cpuif_rd_ack"] = "'0" fanin["cpuif_rd_ack"] = "'0"
fanin["cpuif_rd_err"] = "'0" fanin["cpuif_rd_err"] = "'0"
else: else:
fanin["cpuif_rd_ack"] = self.signal("PREADY", node) fanin["cpuif_rd_ack"] = self.signal("PREADY", node, "i")
fanin["cpuif_rd_err"] = self.signal("PSLVERR", node) fanin["cpuif_rd_err"] = self.signal("PSLVERR", node, "i")
return "\n".join(map(lambda kv: f"{kv[0]} = {kv[1]};", fanin.items())) return "\n".join(map(lambda kv: f"{kv[0]} = {kv[1]};", fanin.items()))
@@ -65,6 +65,6 @@ class APB4CpuifFlat(BaseCpuif):
if node is None: if node is None:
fanin["cpuif_rd_data"] = "'0" fanin["cpuif_rd_data"] = "'0"
else: else:
fanin["cpuif_rd_data"] = self.signal("PRDATA", node) fanin["cpuif_rd_data"] = self.signal("PRDATA", node, "i")
return "\n".join(map(lambda kv: f"{kv[0]} = {kv[1]};", fanin.items())) return "\n".join(map(lambda kv: f"{kv[0]} = {kv[1]};", fanin.items()))

View File

@@ -2,6 +2,7 @@
from systemrdl.node import AddressableNode from systemrdl.node import AddressableNode
from ...utils import clog2
from ..interface import FlatInterface, SVInterface from ..interface import FlatInterface, SVInterface
@@ -50,7 +51,7 @@ class APB4FlatInterface(FlatInterface):
f"output logic {self.signal('PSEL', child)}", f"output logic {self.signal('PSEL', child)}",
f"output logic {self.signal('PENABLE', child)}", f"output logic {self.signal('PENABLE', child)}",
f"output logic {self.signal('PWRITE', child)}", f"output logic {self.signal('PWRITE', child)}",
f"output logic [{self.cpuif.addr_width - 1}:0] {self.signal('PADDR', child)}", f"output logic [{clog2(child.size) - 1}:0] {self.signal('PADDR', child)}",
f"output logic [2:0] {self.signal('PPROT', child)}", f"output logic [2:0] {self.signal('PPROT', child)}",
f"output logic [{self.cpuif.data_width - 1}:0] {self.signal('PWDATA', child)}", f"output logic [{self.cpuif.data_width - 1}:0] {self.signal('PWDATA', child)}",
f"output logic [{self.cpuif.data_width // 8 - 1}:0] {self.signal('PSTRB', child)}", f"output logic [{self.cpuif.data_width // 8 - 1}:0] {self.signal('PSTRB', child)}",

View File

@@ -6,8 +6,10 @@
assert_bad_data_width: assert($bits({{cpuif.signal("PWDATA")}}) == {{ds.package_name}}::{{ds.module_name|upper}}_DATA_WIDTH) assert_bad_data_width: assert($bits({{cpuif.signal("PWDATA")}}) == {{ds.package_name}}::{{ds.module_name|upper}}_DATA_WIDTH)
else $error("Interface data width of %0d is incorrect. Shall be %0d bits", $bits({{cpuif.signal("PWDATA")}}), {{ds.package_name}}::{{ds.module_name|upper}}_DATA_WIDTH); else $error("Interface data width of %0d is incorrect. Shall be %0d bits", $bits({{cpuif.signal("PWDATA")}}), {{ds.package_name}}::{{ds.module_name|upper}}_DATA_WIDTH);
end end
`ifdef PEAKRDL_ASSERTIONS
assert_wr_sel: assert property (@(posedge {{cpuif.signal("PCLK")}}) {{cpuif.signal("PSEL")}} && {{cpuif.signal("PWRITE")}} |-> ##1 ({{cpuif.signal("PREADY")}} || {{cpuif.signal("PSLVERR")}})) assert_wr_sel: assert property (@(posedge {{cpuif.signal("PCLK")}}) {{cpuif.signal("PSEL")}} && {{cpuif.signal("PWRITE")}} |-> ##1 ({{cpuif.signal("PREADY")}} || {{cpuif.signal("PSLVERR")}}))
else $error("APB4 Slave port SEL implies that cpuif_wr_sel must be one-hot encoded"); else $error("APB4 Slave port SEL implies that cpuif_wr_sel must be one-hot encoded");
`endif
`endif `endif
{%- endif %} {%- endif %}
@@ -29,6 +31,13 @@ assign {{cpuif.signal("PSLVERR")}} = cpuif_rd_err | cpuif_rd_sel.cpuif_err | cpu
// Fanout CPU Bus interface signals // Fanout CPU Bus interface signals
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
{{fanout|walk(cpuif=cpuif)}} {{fanout|walk(cpuif=cpuif)}}
{%- if cpuif.is_interface %}
//--------------------------------------------------------------------------
// Intermediate signals for interface array fanin
//--------------------------------------------------------------------------
{{fanin_intermediate|walk(cpuif=cpuif)}}
{%- endif %}
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// Fanin CPU Bus interface signals // Fanin CPU Bus interface signals

View File

@@ -4,14 +4,14 @@ from systemrdl.node import AddressableNode
from ...utils import get_indexed_path from ...utils import get_indexed_path
from ..base_cpuif import BaseCpuif from ..base_cpuif import BaseCpuif
from .axi4lite_interface import AXI4LiteSVInterface from .axi4_lite_interface import AXI4LiteSVInterface
if TYPE_CHECKING: if TYPE_CHECKING:
from ...exporter import BusDecoderExporter from ...exporter import BusDecoderExporter
class AXI4LiteCpuif(BaseCpuif): class AXI4LiteCpuif(BaseCpuif):
template_path = "axi4lite_tmpl.sv" template_path = "axi4_lite_tmpl.sv"
def __init__(self, exp: "BusDecoderExporter") -> None: def __init__(self, exp: "BusDecoderExporter") -> None:
super().__init__(exp) super().__init__(exp)
@@ -68,9 +68,17 @@ class AXI4LiteCpuif(BaseCpuif):
fanin["cpuif_rd_ack"] = "'0" fanin["cpuif_rd_ack"] = "'0"
fanin["cpuif_rd_err"] = "'0" fanin["cpuif_rd_err"] = "'0"
else: else:
# Read side: ack comes from RVALID; err if RRESP[1] is set (SLVERR/DECERR) # Use intermediate signals for interface arrays to avoid
fanin["cpuif_rd_ack"] = self.signal("RVALID", node, "i") # non-constant indexing of interface arrays in procedural blocks
fanin["cpuif_rd_err"] = f"{self.signal('RRESP', node, 'i')}[1]" if self.is_interface and node.is_array and node.array_dimensions:
# Generate array index string [i0][i1]... for the intermediate signal
array_idx = "".join(f"[i{i}]" for i in range(len(node.array_dimensions)))
fanin["cpuif_rd_ack"] = f"{node.inst_name}_fanin_ready{array_idx}"
fanin["cpuif_rd_err"] = f"{node.inst_name}_fanin_err{array_idx}"
else:
# Read side: ack comes from RVALID; err if RRESP[1] is set (SLVERR/DECERR)
fanin["cpuif_rd_ack"] = self.signal("RVALID", node, "i")
fanin["cpuif_rd_err"] = f"{self.signal('RRESP', node, 'i')}[1]"
return "\n".join(f"{lhs} = {rhs};" for lhs, rhs in fanin.items()) return "\n".join(f"{lhs} = {rhs};" for lhs, rhs in fanin.items())
@@ -79,6 +87,23 @@ class AXI4LiteCpuif(BaseCpuif):
if node is None: if node is None:
fanin["cpuif_rd_data"] = "'0" fanin["cpuif_rd_data"] = "'0"
else: else:
fanin["cpuif_rd_data"] = self.signal("RDATA", node, "i") # Use intermediate signals for interface arrays to avoid
# non-constant indexing of interface arrays in procedural blocks
if self.is_interface and node.is_array and node.array_dimensions:
# Generate array index string [i0][i1]... for the intermediate signal
array_idx = "".join(f"[i{i}]" for i in range(len(node.array_dimensions)))
fanin["cpuif_rd_data"] = f"{node.inst_name}_fanin_data{array_idx}"
else:
fanin["cpuif_rd_data"] = self.signal("RDATA", node, "i")
return "\n".join(f"{lhs} = {rhs};" for lhs, rhs in fanin.items()) return "\n".join(f"{lhs} = {rhs};" for lhs, rhs in fanin.items())
def fanin_intermediate_assignments(
self, node: AddressableNode, inst_name: str, array_idx: str, master_prefix: str, indexed_path: str
) -> list[str]:
"""Generate intermediate signal assignments for AXI4-Lite interface arrays."""
return [
f"assign {inst_name}_fanin_ready{array_idx} = {master_prefix}{indexed_path}.RVALID;",
f"assign {inst_name}_fanin_err{array_idx} = {master_prefix}{indexed_path}.RRESP[1];",
f"assign {inst_name}_fanin_data{array_idx} = {master_prefix}{indexed_path}.RDATA;",
]

View File

@@ -4,7 +4,7 @@ from systemrdl.node import AddressableNode
from ...utils import get_indexed_path from ...utils import get_indexed_path
from ..base_cpuif import BaseCpuif from ..base_cpuif import BaseCpuif
from .axi4lite_interface import AXI4LiteFlatInterface from .axi4_lite_interface import AXI4LiteFlatInterface
if TYPE_CHECKING: if TYPE_CHECKING:
from ...exporter import BusDecoderExporter from ...exporter import BusDecoderExporter
@@ -13,7 +13,7 @@ if TYPE_CHECKING:
class AXI4LiteCpuifFlat(BaseCpuif): class AXI4LiteCpuifFlat(BaseCpuif):
"""Verilator-friendly variant that flattens the AXI4-Lite interface ports.""" """Verilator-friendly variant that flattens the AXI4-Lite interface ports."""
template_path = "axi4lite_tmpl.sv" template_path = "axi4_lite_tmpl.sv"
def __init__(self, exp: "BusDecoderExporter") -> None: def __init__(self, exp: "BusDecoderExporter") -> None:
super().__init__(exp) super().__init__(exp)

View File

@@ -15,6 +15,7 @@
$bits({{cpuif.signal("WDATA")}}), {{ds.package_name}}::{{ds.module_name|upper}}_DATA_WIDTH); $bits({{cpuif.signal("WDATA")}}), {{ds.package_name}}::{{ds.module_name|upper}}_DATA_WIDTH);
end end
`ifdef PEAKRDL_ASSERTIONS
// Simple handshake sanity (one-cycle implication; relax/adjust as needed) // Simple handshake sanity (one-cycle implication; relax/adjust as needed)
assert_rd_resp_enc: assert property (@(posedge {{cpuif.signal("ACLK")}}) assert_rd_resp_enc: assert property (@(posedge {{cpuif.signal("ACLK")}})
{{cpuif.signal("RVALID")}} |-> (^{{cpuif.signal("RRESP")}} !== 1'bx)) {{cpuif.signal("RVALID")}} |-> (^{{cpuif.signal("RRESP")}} !== 1'bx))
@@ -23,6 +24,7 @@
assert_wr_resp_enc: assert property (@(posedge {{cpuif.signal("ACLK")}}) assert_wr_resp_enc: assert property (@(posedge {{cpuif.signal("ACLK")}})
{{cpuif.signal("BVALID")}} |-> (^{{cpuif.signal("BRESP")}} !== 1'bx)) {{cpuif.signal("BVALID")}} |-> (^{{cpuif.signal("BRESP")}} !== 1'bx))
else $error("BRESP must be a legal AXI response when BVALID is high"); else $error("BRESP must be a legal AXI response when BVALID is high");
`endif
`endif `endif
{% endif -%} {% endif -%}
@@ -53,6 +55,13 @@ assign {{cpuif.signal("BRESP")}} = (cpuif_wr_err | cpuif_wr_sel.cpuif_err | cpu
// Fanout CPU Bus interface signals // Fanout CPU Bus interface signals
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
{{fanout|walk(cpuif=cpuif)}} {{fanout|walk(cpuif=cpuif)}}
{%- if cpuif.is_interface %}
//--------------------------------------------------------------------------
// Intermediate signals for interface array fanin
//--------------------------------------------------------------------------
{{fanin_intermediate|walk(cpuif=cpuif)}}
{%- endif %}
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// Fanin CPU Bus interface signals // Fanin CPU Bus interface signals

View File

@@ -7,6 +7,7 @@ from systemrdl.node import AddressableNode
from ..utils import clog2, get_indexed_path, is_pow2, roundup_pow2 from ..utils import clog2, get_indexed_path, is_pow2, roundup_pow2
from .fanin_gen import FaninGenerator from .fanin_gen import FaninGenerator
from .fanin_intermediate_gen import FaninIntermediateGenerator
from .fanout_gen import FanoutGenerator from .fanout_gen import FanoutGenerator
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -24,11 +25,7 @@ class BaseCpuif:
@property @property
def addressable_children(self) -> list[AddressableNode]: def addressable_children(self) -> list[AddressableNode]:
return [ return self.exp.ds.get_addressable_children_at_depth(unroll=self.unroll)
child
for child in self.exp.ds.top_node.children(unroll=self.unroll)
if isinstance(child, AddressableNode)
]
@property @property
def addr_width(self) -> int: def addr_width(self) -> int:
@@ -97,6 +94,7 @@ class BaseCpuif:
"ds": self.exp.ds, "ds": self.exp.ds,
"fanout": FanoutGenerator, "fanout": FanoutGenerator,
"fanin": FaninGenerator, "fanin": FaninGenerator,
"fanin_intermediate": FaninIntermediateGenerator,
} }
template = jj_env.get_template(self.template_path) template = jj_env.get_template(self.template_path)
@@ -116,3 +114,24 @@ class BaseCpuif:
def readback(self, node: AddressableNode | None = None) -> str: def readback(self, node: AddressableNode | None = None) -> str:
raise NotImplementedError raise NotImplementedError
def fanin_intermediate_assignments(
self, node: AddressableNode, inst_name: str, array_idx: str, master_prefix: str, indexed_path: str
) -> list[str]:
"""Generate intermediate signal assignments for interface array fanin.
This method should be implemented by cpuif classes that use interfaces.
It returns a list of assignment strings that copy signals from interface
arrays to intermediate unpacked arrays using constant (genvar) indexing.
Args:
node: The addressable node
inst_name: Instance name for the intermediate signals
array_idx: Array index string (e.g., "[gi0][gi1]")
master_prefix: Master interface prefix
indexed_path: Indexed path to the interface element
Returns:
List of assignment strings
"""
return [] # Default: no intermediate assignments needed

View File

@@ -27,6 +27,17 @@ class FaninGenerator(BusDecoderListener):
def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None: def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None:
action = super().enter_AddressableComponent(node) action = super().enter_AddressableComponent(node)
should_generate = action == WalkerAction.SkipDescendants
if not should_generate and self._ds.max_decode_depth == 0:
for child in node.children():
if isinstance(child, AddressableNode):
break
else:
should_generate = True
if not should_generate:
return action
if node.array_dimensions: if node.array_dimensions:
for i, dim in enumerate(node.array_dimensions): for i, dim in enumerate(node.array_dimensions):
fb = ForLoopBody( fb = ForLoopBody(

View File

@@ -0,0 +1,142 @@
"""Generator for intermediate signals needed for interface array fanin.
When using SystemVerilog interface arrays, we cannot use variable indices
in procedural blocks (like always_comb). This generator creates intermediate
signals that copy from interface arrays using generate loops, which can then
be safely accessed with variable indices in the fanin logic.
"""
from collections import deque
from typing import TYPE_CHECKING
from systemrdl.node import AddressableNode
from systemrdl.walker import WalkerAction
from ..body import Body, ForLoopBody
from ..design_state import DesignState
from ..listener import BusDecoderListener
from ..utils import get_indexed_path
if TYPE_CHECKING:
from .base_cpuif import BaseCpuif
class FaninIntermediateGenerator(BusDecoderListener):
"""Generates intermediate signals for interface array fanin."""
def __init__(self, ds: DesignState, cpuif: "BaseCpuif") -> None:
super().__init__(ds)
self._cpuif = cpuif
self._declarations: list[str] = []
self._stack: deque[Body] = deque()
self._stack.append(Body())
def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None:
action = super().enter_AddressableComponent(node)
# Only generate intermediates for interface arrays
# Check if cpuif has is_interface attribute (some implementations don't)
is_interface = getattr(self._cpuif, "is_interface", False)
if not is_interface or not node.array_dimensions:
return action
# Generate intermediate signal declarations
self._generate_intermediate_declarations(node)
# Generate assignment logic using generate loops
if node.array_dimensions:
for i, dim in enumerate(node.array_dimensions):
fb = ForLoopBody(
"genvar",
f"gi{i}",
dim,
)
self._stack.append(fb)
# Generate assignments from interface array to intermediates
self._stack[-1] += self._generate_intermediate_assignments(node)
return action
def exit_AddressableComponent(self, node: AddressableNode) -> None:
is_interface = getattr(self._cpuif, "is_interface", False)
if is_interface and node.array_dimensions:
for _ in node.array_dimensions:
b = self._stack.pop()
if not b:
continue
self._stack[-1] += b
super().exit_AddressableComponent(node)
def _generate_intermediate_declarations(self, node: AddressableNode) -> None:
"""Generate intermediate signal declarations for a node."""
inst_name = node.inst_name
# Array dimensions should be checked before calling this function
if not node.array_dimensions:
return
# Calculate total array size
array_size = 1
for dim in node.array_dimensions:
array_size *= dim
# Create array dimension string
array_str = "".join(f"[{dim}]" for dim in node.array_dimensions)
# Generate declarations for each fanin signal
# For APB3/4: PREADY, PSLVERR, PRDATA
# These are the signals read in fanin
self._declarations.append(f"logic {inst_name}_fanin_ready{array_str};")
self._declarations.append(f"logic {inst_name}_fanin_err{array_str};")
self._declarations.append(
f"logic [{self._cpuif.data_width - 1}:0] {inst_name}_fanin_data{array_str};"
)
def _generate_intermediate_assignments(self, node: AddressableNode) -> str:
"""Generate assignments from interface array to intermediate signals."""
inst_name = node.inst_name
indexed_path = get_indexed_path(node.parent, node, "gi", skip_kw_filter=True)
# Get master prefix - use getattr to avoid type errors
interface = getattr(self._cpuif, "_interface", None)
if interface is None:
return ""
master_prefix = interface.get_master_prefix()
# Array dimensions should be checked before calling this function
if not node.array_dimensions:
return ""
# Create indexed signal names for left-hand side
array_idx = "".join(f"[gi{i}]" for i in range(len(node.array_dimensions)))
# Delegate to cpuif to get the appropriate assignments for this interface type
assignments = self._cpuif.fanin_intermediate_assignments(
node, inst_name, array_idx, master_prefix, indexed_path
)
return "\n".join(assignments)
def get_declarations(self) -> str:
"""Get all intermediate signal declarations."""
if not self._declarations:
return ""
return "\n".join(self._declarations)
def __str__(self) -> str:
"""Get all intermediate signal declarations and assignments."""
if not self._declarations:
return ""
# Output declarations first
output = "\n".join(self._declarations)
output += "\n\n"
# Then output assignments
body_str = "\n".join(map(str, self._stack))
if body_str and body_str.strip():
output += body_str
return output

View File

@@ -23,6 +23,17 @@ class FanoutGenerator(BusDecoderListener):
def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None: def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None:
action = super().enter_AddressableComponent(node) action = super().enter_AddressableComponent(node)
should_generate = action == WalkerAction.SkipDescendants
if not should_generate and self._ds.max_decode_depth == 0:
for child in node.children():
if isinstance(child, AddressableNode):
break
else:
should_generate = True
if not should_generate:
return action
if node.array_dimensions: if node.array_dimensions:
for i, dim in enumerate(node.array_dimensions): for i, dim in enumerate(node.array_dimensions):
fb = ForLoopBody( fb = ForLoopBody(

View File

@@ -1,10 +1,13 @@
"""Interface abstraction for handling flat and non-flat signal declarations.""" """Interface abstraction for handling flat and non-flat signal declarations."""
import re
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from systemrdl.node import AddressableNode from systemrdl.node import AddressableNode
from ..utils import get_indexed_path
if TYPE_CHECKING: if TYPE_CHECKING:
from .base_cpuif import BaseCpuif from .base_cpuif import BaseCpuif
@@ -61,17 +64,20 @@ class Interface(ABC):
class SVInterface(Interface): class SVInterface(Interface):
"""SystemVerilog interface-based signal handling.""" """SystemVerilog interface-based signal handling."""
slave_modport_name = "slave"
master_modport_name = "master"
@property @property
def is_interface(self) -> bool: def is_interface(self) -> bool:
return True return True
def get_port_declaration(self, slave_name: str, master_prefix: str) -> str: def get_port_declaration(self, slave_name: str, master_prefix: str) -> str:
"""Generate SystemVerilog interface port declarations.""" """Generate SystemVerilog interface port declarations."""
slave_ports: list[str] = [f"{self.get_interface_type()}.slave {slave_name}"] slave_ports: list[str] = [f"{self.get_interface_type()}.{self.slave_modport_name} {slave_name}"]
master_ports: list[str] = [] master_ports: list[str] = []
for child in self.cpuif.addressable_children: for child in self.cpuif.addressable_children:
base = f"{self.get_interface_type()}.master {master_prefix}{child.inst_name}" base = f"{self.get_interface_type()}.{self.master_modport_name} {master_prefix}{child.inst_name}"
# When unrolled, current_idx is set - append it to the name # When unrolled, current_idx is set - append it to the name
if child.current_idx is not None: if child.current_idx is not None:
@@ -93,7 +99,6 @@ class SVInterface(Interface):
indexer: str | int | None = None, indexer: str | int | None = None,
) -> str: ) -> str:
"""Generate SystemVerilog interface signal reference.""" """Generate SystemVerilog interface signal reference."""
from ..utils import get_indexed_path
# SVInterface only supports string indexers (loop variable names like "i", "gi") # SVInterface only supports string indexers (loop variable names like "i", "gi")
if indexer is not None and not isinstance(indexer, str): if indexer is not None and not isinstance(indexer, str):
@@ -166,6 +171,13 @@ class FlatInterface(Interface):
# Is an array # Is an array
if indexer is not None: if indexer is not None:
if isinstance(indexer, str):
indexed_path = get_indexed_path(node.parent, node, indexer, skip_kw_filter=True)
pattern = r"\[.*?\]"
indexes = re.findall(pattern, indexed_path)
return f"{base}_{signal}{''.join(indexes)}"
return f"{base}_{signal}[{indexer}]" return f"{base}_{signal}[{indexer}]"
return f"{base}_{signal}[N_{node.inst_name.upper()}S]" return f"{base}_{signal}[N_{node.inst_name.upper()}S]"

View File

@@ -0,0 +1,3 @@
from .taxi_apb_cpuif import TaxiAPBCpuif
__all__ = ["TaxiAPBCpuif"]

View File

@@ -0,0 +1,99 @@
from typing import TYPE_CHECKING, overload
from systemrdl.node import AddressableNode
from ...utils import get_indexed_path
from ..base_cpuif import BaseCpuif
from .taxi_apb_interface import TaxiAPBSVInterface
if TYPE_CHECKING:
from ...exporter import BusDecoderExporter
class TaxiAPBCpuif(BaseCpuif):
template_path = "taxi_apb_tmpl.sv"
def __init__(self, exp: "BusDecoderExporter") -> None:
super().__init__(exp)
self._interface = TaxiAPBSVInterface(self)
self._interface.master_modport_name = "mst"
self._interface.slave_modport_name = "slv"
@property
def is_interface(self) -> bool:
return self._interface.is_interface
@property
def port_declaration(self) -> str:
"""Returns the port declaration for the APB4 interface."""
return self._interface.get_port_declaration("s_apb", "m_apb_")
@overload
def signal(self, signal: str, node: None = None, indexer: None = None) -> str: ...
@overload
def signal(self, signal: str, node: AddressableNode, indexer: str) -> str: ...
def signal(self, signal: str, node: AddressableNode | None = None, indexer: str | None = None) -> str:
return self._interface.signal(signal, node, indexer)
def fanout(self, node: AddressableNode) -> str:
fanout: dict[str, str] = {}
fanout[self.signal("psel", node, "gi")] = (
f"cpuif_wr_sel.{get_indexed_path(self.exp.ds.top_node, node, 'gi')}|cpuif_rd_sel.{get_indexed_path(self.exp.ds.top_node, node, 'gi')}"
)
fanout[self.signal("penable", node, "gi")] = self.signal("penable")
fanout[self.signal("pwrite", node, "gi")] = (
f"cpuif_wr_sel.{get_indexed_path(self.exp.ds.top_node, node, 'gi')}"
)
fanout[self.signal("paddr", node, "gi")] = self.signal("paddr")
fanout[self.signal("pprot", node, "gi")] = self.signal("pprot")
fanout[self.signal("pwdata", node, "gi")] = "cpuif_wr_data"
fanout[self.signal("pstrb", node, "gi")] = "cpuif_wr_byte_en"
# no user?
return "\n".join(map(lambda kv: f"assign {kv[0]} = {kv[1]};", fanout.items()))
def fanin(self, node: AddressableNode | None = None) -> str:
fanin: dict[str, str] = {}
if node is None:
fanin["cpuif_rd_ack"] = "'0"
fanin["cpuif_rd_err"] = "'0"
else:
# Use intermediate signals for interface arrays to avoid
# non-constant indexing of interface arrays in procedural blocks
if self.is_interface and node.is_array and node.array_dimensions:
# Generate array index string [i0][i1]... for the intermediate signal
array_idx = "".join(f"[i{i}]" for i in range(len(node.array_dimensions)))
fanin["cpuif_rd_ack"] = f"{node.inst_name}_fanin_ready{array_idx}"
fanin["cpuif_rd_err"] = f"{node.inst_name}_fanin_err{array_idx}"
else:
fanin["cpuif_rd_ack"] = self.signal("pready", node, "i")
fanin["cpuif_rd_err"] = self.signal("pslverr", node, "i")
# no user?
return "\n".join(map(lambda kv: f"{kv[0]} = {kv[1]};", fanin.items()))
def readback(self, node: AddressableNode | None = None) -> str:
fanin: dict[str, str] = {}
if node is None:
fanin["cpuif_rd_data"] = "'0"
else:
# Use intermediate signals for interface arrays to avoid
# non-constant indexing of interface arrays in procedural blocks
if self.is_interface and node.is_array and node.array_dimensions:
# Generate array index string [i0][i1]... for the intermediate signal
array_idx = "".join(f"[i{i}]" for i in range(len(node.array_dimensions)))
fanin["cpuif_rd_data"] = f"{node.inst_name}_fanin_data{array_idx}"
else:
fanin["cpuif_rd_data"] = self.signal("prdata", node, "i")
return "\n".join(map(lambda kv: f"{kv[0]} = {kv[1]};", fanin.items()))
def fanin_intermediate_assignments(
self, node: AddressableNode, inst_name: str, array_idx: str, master_prefix: str, indexed_path: str
) -> list[str]:
"""Generate intermediate signal assignments for APB4 interface arrays."""
return [
f"assign {inst_name}_fanin_ready{array_idx} = {master_prefix}{indexed_path}.pready;",
f"assign {inst_name}_fanin_err{array_idx} = {master_prefix}{indexed_path}.pslverr;",
f"assign {inst_name}_fanin_data{array_idx} = {master_prefix}{indexed_path}.prdata;",
]

View File

@@ -0,0 +1,16 @@
"""Taxi APB-specific interface implementations."""
from ..interface import SVInterface
class TaxiAPBSVInterface(SVInterface):
"""Taxi APB SystemVerilog interface."""
def get_interface_type(self) -> str:
return "taxi_apb_if"
def get_slave_name(self) -> str:
return "s_apb"
def get_master_prefix(self) -> str:
return "m_apb_"

View File

@@ -0,0 +1,45 @@
{%- if cpuif.is_interface %}
`ifndef SYNTHESIS
initial begin
assert_bad_addr_width: assert($bits({{cpuif.signal("paddr")}}) >= {{ds.package_name}}::{{ds.module_name|upper}}_MIN_ADDR_WIDTH)
else $error("Interface address width of %0d is too small. Shall be at least %0d bits", $bits({{cpuif.signal("paddr")}}), {{ds.package_name}}::{{ds.module_name|upper}}_MIN_ADDR_WIDTH);
assert_bad_data_width: assert($bits({{cpuif.signal("pwdata")}}) == {{ds.package_name}}::{{ds.module_name|upper}}_DATA_WIDTH)
else $error("Interface data width of %0d is incorrect. Shall be %0d bits", $bits({{cpuif.signal("pwdata")}}), {{ds.package_name}}::{{ds.module_name|upper}}_DATA_WIDTH);
end
`ifdef PEAKRDL_ASSERTIONS
assert_wr_sel: assert property (@(posedge {{cpuif.signal("PCLK")}}) {{cpuif.signal("psel")}} && {{cpuif.signal("pwrite")}} |-> ##1 ({{cpuif.signal("pready")}} || {{cpuif.signal("pslverr")}}))
else $error("APB4 Slave port SEL implies that cpuif_wr_sel must be one-hot encoded");
`endif
`endif
{%- endif %}
assign cpuif_req = {{cpuif.signal("psel")}};
assign cpuif_wr_en = {{cpuif.signal("pwrite")}};
assign cpuif_rd_en = !{{cpuif.signal("pwrite")}};
assign cpuif_wr_addr = {{cpuif.signal("paddr")}};
assign cpuif_rd_addr = {{cpuif.signal("paddr")}};
assign cpuif_wr_data = {{cpuif.signal("pwdata")}};
assign cpuif_wr_byte_en = {{cpuif.signal("pstrb")}};
assign {{cpuif.signal("prdata")}} = cpuif_rd_data;
assign {{cpuif.signal("pready")}} = cpuif_rd_ack;
assign {{cpuif.signal("pslverr")}} = cpuif_rd_err | cpuif_rd_sel.cpuif_err | cpuif_wr_sel.cpuif_err;
//--------------------------------------------------------------------------
// Fanout CPU Bus interface signals
//--------------------------------------------------------------------------
{{fanout|walk(cpuif=cpuif)}}
{%- if cpuif.is_interface %}
//--------------------------------------------------------------------------
// Intermediate signals for interface array fanin
//--------------------------------------------------------------------------
{{fanin_intermediate|walk(cpuif=cpuif)}}
{%- endif %}
//--------------------------------------------------------------------------
// Fanin CPU Bus interface signals
//--------------------------------------------------------------------------
{{fanin|walk(cpuif=cpuif)}}

View File

@@ -70,7 +70,9 @@ class DecodeLogicGenerator(BusDecoderListener):
# Avoid generating a redundant >= 0 comparison, which triggers Verilator warnings. # Avoid generating a redundant >= 0 comparison, which triggers Verilator warnings.
if not (l_bound.value == 0 and len(l_bound_comp) == 1): if not (l_bound.value == 0 and len(l_bound_comp) == 1):
predicates.append(lower_expr) predicates.append(lower_expr)
predicates.append(upper_expr) # Avoid generating a redundant full-width < max comparison, which triggers Verilator warnings.
if not (u_bound.value == (1 << addr_width) and len(u_bound_comp) == 1):
predicates.append(upper_expr)
return predicates return predicates
@@ -85,6 +87,20 @@ class DecodeLogicGenerator(BusDecoderListener):
def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None: def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None:
action = super().enter_AddressableComponent(node) action = super().enter_AddressableComponent(node)
should_decode = action == WalkerAction.SkipDescendants
if not should_decode and self._ds.max_decode_depth == 0:
# When decoding all levels, treat leaf registers as decode boundary
for child in node.children():
if isinstance(child, AddressableNode):
break
else:
should_decode = True
# Only generate select logic if we're at the decode boundary
if not should_decode:
return action
conditions: list[str] = [] conditions: list[str] = []
conditions.extend(self.cpuif_addr_predicate(node)) conditions.extend(self.cpuif_addr_predicate(node))
conditions.extend(self.cpuif_prot_predicate(node)) conditions.extend(self.cpuif_prot_predicate(node))
@@ -146,6 +162,8 @@ class DecodeLogicGenerator(BusDecoderListener):
def __str__(self) -> str: def __str__(self) -> str:
body = self._decode_stack[-1] body = self._decode_stack[-1]
if isinstance(body, IfBody): if isinstance(body, IfBody):
if len(body) == 0:
return f"{self._flavor.cpuif_select}.cpuif_err = 1'b1;"
with body.cm(...) as b: with body.cm(...) as b:
b += f"{self._flavor.cpuif_select}.cpuif_err = 1'b1;" b += f"{self._flavor.cpuif_select}.cpuif_err = 1'b1;"

View File

@@ -1,6 +1,6 @@
from typing import TypedDict from typing import TypedDict
from systemrdl.node import AddrmapNode from systemrdl.node import AddressableNode, AddrmapNode
from systemrdl.rdltypes.user_enum import UserEnum from systemrdl.rdltypes.user_enum import UserEnum
from .design_scanner import DesignScanner from .design_scanner import DesignScanner
@@ -72,3 +72,56 @@ class DesignState:
if user_addr_width < self.addr_width: if user_addr_width < self.addr_width:
msg.fatal(f"User-specified address width shall be greater than or equal to {self.addr_width}.") msg.fatal(f"User-specified address width shall be greater than or equal to {self.addr_width}.")
self.addr_width = user_addr_width self.addr_width = user_addr_width
def get_addressable_children_at_depth(self, unroll: bool = False) -> list[AddressableNode]:
"""
Get addressable children at the decode boundary based on max_decode_depth.
max_decode_depth semantics:
- 0: decode all levels (return leaf registers)
- 1: decode only top level (return children at depth 1)
- 2: decode top + 1 level (return children at depth 2)
- N: decode down to depth N (return children at depth N)
Args:
unroll: Whether to unroll arrayed nodes
Returns:
List of addressable nodes at the decode boundary
"""
from systemrdl.node import RegNode
def collect_nodes(node: AddressableNode, current_depth: int) -> list[AddressableNode]:
"""Recursively collect nodes at the decode boundary."""
result: list[AddressableNode] = []
# For depth 0, collect all leaf registers
if self.max_decode_depth == 0:
# If this is a register, it's a leaf
if isinstance(node, RegNode):
result.append(node)
else:
# Recurse into children
for child in node.children(unroll=unroll):
if isinstance(child, AddressableNode):
result.extend(collect_nodes(child, current_depth + 1))
else:
# For depth N, collect children at depth N
if current_depth == self.max_decode_depth:
# We're at the decode boundary - return this node
result.append(node)
elif current_depth < self.max_decode_depth:
# We haven't reached the boundary yet - recurse
for child in node.children(unroll=unroll):
if isinstance(child, AddressableNode):
result.extend(collect_nodes(child, current_depth + 1))
return result
# Start collecting from top node's children
nodes: list[AddressableNode] = []
for child in self.top_node.children(unroll=unroll):
if isinstance(child, AddressableNode):
nodes.extend(collect_nodes(child, 1))
return nodes

View File

@@ -89,7 +89,9 @@ class BusDecoderExporter:
interface. By default, arrayed nodes are kept as arrays. interface. By default, arrayed nodes are kept as arrays.
max_decode_depth: int max_decode_depth: int
Maximum depth for address decoder to descend into nested addressable Maximum depth for address decoder to descend into nested addressable
components. By default, the decoder descends 1 level deep. components. A value of 0 decodes all levels (infinite depth). A value
of 1 decodes only top-level children. A value of 2 decodes top-level
and one level deeper, etc. By default, the decoder descends 1 level deep.
""" """
# If it is the root node, skip to top addrmap # If it is the root node, skip to top addrmap
if isinstance(node, RootNode): if isinstance(node, RootNode):

View File

@@ -15,7 +15,12 @@ class BusDecoderListener(RDLListener):
def should_skip_node(self, node: AddressableNode) -> bool: def should_skip_node(self, node: AddressableNode) -> bool:
"""Check if this node should be skipped (not decoded).""" """Check if this node should be skipped (not decoded)."""
# Check if current depth exceeds max depth # Check if current depth exceeds max depth
if self._depth > self._ds.max_decode_depth: # max_decode_depth semantics:
# - 0 means decode all levels (infinite)
# - 1 means decode only top level (depth 0)
# - 2 means decode top + 1 level (depth 0 and 1)
# - N means decode down to depth N-1
if self._ds.max_decode_depth > 0 and self._depth >= self._ds.max_decode_depth:
return True return True
# Check if this node only contains external addressable children # Check if this node only contains external addressable children

View File

@@ -51,7 +51,7 @@ module {{ds.module_name}}
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
always_comb begin always_comb begin
// Default all write select signals to 0 // Default all write select signals to 0
cpuif_wr_sel = '0; cpuif_wr_sel = '{default: '0};
if (cpuif_req && cpuif_wr_en) begin if (cpuif_req && cpuif_wr_en) begin
// A write request is pending // A write request is pending
@@ -66,7 +66,7 @@ module {{ds.module_name}}
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
always_comb begin always_comb begin
// Default all read select signals to 0 // Default all read select signals to 0
cpuif_rd_sel = '0; cpuif_rd_sel = '{default: '0};
if (cpuif_req && cpuif_rd_en) begin if (cpuif_req && cpuif_rd_en) begin
// A read request is pending // A read request is pending

View File

@@ -3,7 +3,7 @@ from collections import deque
from systemrdl.node import AddressableNode from systemrdl.node import AddressableNode
from systemrdl.walker import WalkerAction from systemrdl.walker import WalkerAction
from .body import Body, StructBody from .body import StructBody
from .design_state import DesignState from .design_state import DesignState
from .identifier_filter import kw_filter as kwf from .identifier_filter import kw_filter as kwf
from .listener import BusDecoderListener from .listener import BusDecoderListener
@@ -16,42 +16,53 @@ class StructGenerator(BusDecoderListener):
) -> None: ) -> None:
super().__init__(ds) super().__init__(ds)
self._stack: deque[Body] = deque() self._stack: list[StructBody] = [StructBody("cpuif_sel_t", True, False)]
self._stack.append(StructBody("cpuif_sel_t", True, True)) self._struct_defs: list[StructBody] = []
self._created_struct_stack: deque[bool] = deque() # Track if we created a struct for each node
def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None: def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None:
action = super().enter_AddressableComponent(node) action = super().enter_AddressableComponent(node)
self._skip = False skip = action == WalkerAction.SkipDescendants
if action == WalkerAction.SkipDescendants:
self._skip = True
if node.children(): # Only create nested struct if we're not skipping and node has addressable children
has_addressable_children = any(isinstance(child, AddressableNode) for child in node.children())
if has_addressable_children and not skip:
# Push new body onto stack # Push new body onto stack
body = StructBody(f"cpuif_sel_{node.inst_name}_t", True, True) body = StructBody(f"cpuif_sel_{node.inst_name}_t", True, False)
self._stack.append(body) self._stack.append(body)
self._created_struct_stack.append(True)
else:
self._created_struct_stack.append(False)
return action return action
def exit_AddressableComponent(self, node: AddressableNode) -> None: def exit_AddressableComponent(self, node: AddressableNode) -> None:
type = "logic" type = "logic"
if node.children(): # Pop the created_struct flag
created_struct = self._created_struct_stack.pop()
# Only pop struct body if we created one
if created_struct:
body = self._stack.pop() body = self._stack.pop()
if body and isinstance(body, StructBody) and not self._skip: if body:
self._stack.appendleft(body) self._struct_defs.append(body)
type = body.name type = body.name
name = kwf(node.inst_name) name = kwf(node.inst_name)
if node.array_dimensions: if node.array_dimensions:
for dim in node.array_dimensions: for dim in node.array_dimensions:
name = f"[{dim - 1}:0]{name}" name = f"{name}[{dim}]"
self._stack[-1] += f"{type} {name};" self._stack[-1] += f"{type} {name};"
super().exit_AddressableComponent(node) super().exit_AddressableComponent(node)
def __str__(self) -> str: def __str__(self) -> str:
self._stack[-1] += "logic cpuif_err;" if "logic cpuif_err;" not in self._stack[-1].lines:
return "\n".join(map(str, self._stack)) self._stack[-1] += "logic cpuif_err;"
bodies = [str(body) for body in self._struct_defs]
bodies.append(str(self._stack[-1]))
return "\n".join(bodies)

View File

@@ -1,3 +1,6 @@
from typing import Literal
class SVInt: class SVInt:
def __init__(self, value: int, width: int | None = None) -> None: def __init__(self, value: int, width: int | None = None) -> None:
self.value = value self.value = value
@@ -19,3 +22,27 @@ class SVInt:
return SVInt(self.value + other.value, max(self.width, other.width)) return SVInt(self.value + other.value, max(self.width, other.width))
else: else:
return SVInt(self.value + other.value, None) return SVInt(self.value + other.value, None)
def __sub__(self, other: "SVInt") -> "SVInt":
if self.width is not None and other.width is not None:
return SVInt(self.value - other.value, max(self.width, other.width))
else:
return SVInt(self.value - other.value, None)
def __len__(self) -> int:
if self.width is not None:
return self.width
else:
return self.value.bit_length()
def to_bytes(self, byteorder: Literal["little", "big"] = "little") -> bytes:
byte_length = (self.value.bit_length() + 7) // 8
return self.value.to_bytes(byte_length, byteorder)
def __eq__(self, other: object) -> bool:
if not isinstance(other, SVInt):
return NotImplemented
return self.value == other.value and self.width == other.width
def __hash__(self) -> int:
return hash((self.value, self.width))

View File

@@ -1,15 +1,19 @@
"""APB3 smoke tests for generated multi-register design.""" """APB3 smoke tests generated from SystemRDL sources."""
from __future__ import annotations
import json
import os
from typing import Any, Iterable
import cocotb import cocotb
from cocotb.triggers import Timer from cocotb.triggers import Timer
WRITE_ADDR = 0x0 from tests.cocotb_lib.handle_utils import SignalHandle, resolve_handle
READ_ADDR = 0x8
WRITE_DATA = 0xCAFEBABE
READ_DATA = 0x0BAD_F00D
class _Apb3SlaveShim: class _Apb3SlaveShim:
"""Accessor for the APB3 slave signals on the DUT."""
def __init__(self, dut): def __init__(self, dut):
prefix = "s_apb" prefix = "s_apb"
self.PSEL = getattr(dut, f"{prefix}_PSEL") self.PSEL = getattr(dut, f"{prefix}_PSEL")
@@ -22,102 +26,161 @@ class _Apb3SlaveShim:
self.PSLVERR = getattr(dut, f"{prefix}_PSLVERR") self.PSLVERR = getattr(dut, f"{prefix}_PSLVERR")
class _Apb3MasterShim: def _load_config() -> dict[str, Any]:
def __init__(self, dut, base: str): payload = os.environ.get("RDL_TEST_CONFIG")
self.PSEL = getattr(dut, f"{base}_PSEL") if payload is None:
self.PENABLE = getattr(dut, f"{base}_PENABLE") raise RuntimeError("RDL_TEST_CONFIG environment variable was not provided")
self.PWRITE = getattr(dut, f"{base}_PWRITE") return json.loads(payload)
self.PADDR = getattr(dut, f"{base}_PADDR")
self.PWDATA = getattr(dut, f"{base}_PWDATA")
self.PRDATA = getattr(dut, f"{base}_PRDATA")
self.PREADY = getattr(dut, f"{base}_PREADY")
self.PSLVERR = getattr(dut, f"{base}_PSLVERR")
def _apb3_slave(dut): def _resolve(handle, indices: Iterable[int]):
return getattr(dut, "s_apb", None) or _Apb3SlaveShim(dut) return resolve_handle(handle, indices)
def _apb3_master(dut, base: str): def _set_value(handle, indices: Iterable[int], value: int) -> None:
return getattr(dut, base, None) or _Apb3MasterShim(dut, base) _resolve(handle, indices).value = value
def _get_int(handle, indices: Iterable[int]) -> int:
return int(_resolve(handle, indices).value)
def _build_master_table(dut, masters_cfg: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
table: dict[str, dict[str, Any]] = {}
for master in masters_cfg:
prefix = master["port_prefix"]
entry = {
"indices": [tuple(idx) for idx in master["indices"]] or [tuple()],
"outputs": {
"PSEL": SignalHandle(dut, f"{prefix}_PSEL"),
"PENABLE": SignalHandle(dut, f"{prefix}_PENABLE"),
"PWRITE": SignalHandle(dut, f"{prefix}_PWRITE"),
"PADDR": SignalHandle(dut, f"{prefix}_PADDR"),
"PWDATA": SignalHandle(dut, f"{prefix}_PWDATA"),
},
"inputs": {
"PRDATA": SignalHandle(dut, f"{prefix}_PRDATA"),
"PREADY": SignalHandle(dut, f"{prefix}_PREADY"),
"PSLVERR": SignalHandle(dut, f"{prefix}_PSLVERR"),
},
}
table[master["inst_name"]] = entry
return table
def _all_index_pairs(table: dict[str, dict[str, Any]]):
for name, entry in table.items():
for idx in entry["indices"]:
yield name, idx
def _write_pattern(address: int, width: int) -> int:
mask = (1 << width) - 1
return ((address * 0x2041) ^ 0xCAFEBABE) & mask
def _read_pattern(address: int, width: int) -> int:
mask = (1 << width) - 1
return ((address ^ 0x0BAD_F00D) + width) & mask
@cocotb.test() @cocotb.test()
async def test_apb3_read_write_paths(dut): async def test_apb3_address_decoding(dut) -> None:
"""Exercise APB3 slave interface and observe master fanout.""" """Exercise the APB3 slave interface against sampled register addresses."""
s_apb = _apb3_slave(dut) config = _load_config()
masters = { slave = _Apb3SlaveShim(dut)
"reg1": _apb3_master(dut, "m_apb_reg1"), masters = _build_master_table(dut, config["masters"])
"reg2": _apb3_master(dut, "m_apb_reg2"),
"reg3": _apb3_master(dut, "m_apb_reg3"),
}
s_apb.PSEL.value = 0 slave.PSEL.value = 0
s_apb.PENABLE.value = 0 slave.PENABLE.value = 0
s_apb.PWRITE.value = 0 slave.PWRITE.value = 0
s_apb.PADDR.value = 0 slave.PADDR.value = 0
s_apb.PWDATA.value = 0 slave.PWDATA.value = 0
for master in masters.values(): for master_name, idx in _all_index_pairs(masters):
master.PRDATA.value = 0 entry = masters[master_name]
master.PREADY.value = 0 _set_value(entry["inputs"]["PRDATA"], idx, 0)
master.PSLVERR.value = 0 _set_value(entry["inputs"]["PREADY"], idx, 0)
_set_value(entry["inputs"]["PSLVERR"], idx, 0)
await Timer(1, units="ns") await Timer(1, units="ns")
# Write to reg1 addr_mask = (1 << config["address_width"]) - 1
masters["reg1"].PREADY.value = 1
s_apb.PADDR.value = WRITE_ADDR
s_apb.PWDATA.value = WRITE_DATA
s_apb.PWRITE.value = 1
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
await Timer(1, units="ns") for txn in config["transactions"]:
master_name = txn["master"]
index = tuple(txn["index"])
entry = masters[master_name]
assert int(masters["reg1"].PSEL.value) == 1, "reg1 should be selected for write" address = txn["address"] & addr_mask
assert int(masters["reg1"].PWRITE.value) == 1, "Write should propagate to master" write_data = _write_pattern(address, config["data_width"])
assert int(masters["reg1"].PADDR.value) == WRITE_ADDR, "Address should reach selected master"
assert int(masters["reg1"].PWDATA.value) == WRITE_DATA, "Write data should fan out"
for name, master in masters.items(): _set_value(entry["inputs"]["PREADY"], index, 1)
if name != "reg1": _set_value(entry["inputs"]["PSLVERR"], index, 0)
assert int(master.PSEL.value) == 0, f"{name} must idle during reg1 write"
assert int(s_apb.PREADY.value) == 1, "Ready must reflect selected master" slave.PADDR.value = address
assert int(s_apb.PSLVERR.value) == 0, "Write should not signal error" slave.PWDATA.value = write_data
slave.PWRITE.value = 1
slave.PSEL.value = 1
slave.PENABLE.value = 1
s_apb.PSEL.value = 0 await Timer(1, units="ns")
s_apb.PENABLE.value = 0
s_apb.PWRITE.value = 0
masters["reg1"].PREADY.value = 0
await Timer(1, units="ns")
# Read from reg3 assert _get_int(entry["outputs"]["PSEL"], index) == 1, f"{master_name} should assert PSEL for write"
masters["reg3"].PRDATA.value = READ_DATA assert _get_int(entry["outputs"]["PWRITE"], index) == 1, f"{master_name} should see write direction"
masters["reg3"].PREADY.value = 1 assert _get_int(entry["outputs"]["PADDR"], index) == address, f"{master_name} must receive write address"
masters["reg3"].PSLVERR.value = 0 assert _get_int(entry["outputs"]["PWDATA"], index) == write_data, f"{master_name} must receive write data"
s_apb.PADDR.value = READ_ADDR for other_name, other_idx in _all_index_pairs(masters):
s_apb.PSEL.value = 1 if other_name == master_name and other_idx == index:
s_apb.PENABLE.value = 1 continue
s_apb.PWRITE.value = 0 other_entry = masters[other_name]
assert (
_get_int(other_entry["outputs"]["PSEL"], other_idx) == 0
), f"{other_name}{other_idx} should remain idle during {txn['label']}"
await Timer(1, units="ns") assert int(slave.PREADY.value) == 1, "Slave ready should mirror selected master"
assert int(slave.PSLVERR.value) == 0, "Write should complete without error"
assert int(masters["reg3"].PSEL.value) == 1, "reg3 should be selected for read" slave.PSEL.value = 0
assert int(masters["reg3"].PWRITE.value) == 0, "Read should clear write" slave.PENABLE.value = 0
assert int(masters["reg3"].PADDR.value) == READ_ADDR, "Address should reach read target" slave.PWRITE.value = 0
_set_value(entry["inputs"]["PREADY"], index, 0)
await Timer(1, units="ns")
for name, master in masters.items(): # ------------------------------------------------------------------
if name != "reg3": # Read phase
assert int(master.PSEL.value) == 0, f"{name} must idle during reg3 read" # ------------------------------------------------------------------
read_data = _read_pattern(address, config["data_width"])
_set_value(entry["inputs"]["PRDATA"], index, read_data)
_set_value(entry["inputs"]["PREADY"], index, 1)
_set_value(entry["inputs"]["PSLVERR"], index, 0)
assert int(s_apb.PRDATA.value) == READ_DATA, "Read data should return to slave" slave.PADDR.value = address
assert int(s_apb.PREADY.value) == 1, "Read should acknowledge" slave.PWRITE.value = 0
assert int(s_apb.PSLVERR.value) == 0, "Read should not signal error" slave.PSEL.value = 1
slave.PENABLE.value = 1
s_apb.PSEL.value = 0 await Timer(1, units="ns")
s_apb.PENABLE.value = 0
masters["reg3"].PREADY.value = 0 assert _get_int(entry["outputs"]["PSEL"], index) == 1, f"{master_name} must assert PSEL for read"
await Timer(1, units="ns") assert _get_int(entry["outputs"]["PWRITE"], index) == 0, f"{master_name} should clear write during read"
assert _get_int(entry["outputs"]["PADDR"], index) == address, f"{master_name} must receive read address"
for other_name, other_idx in _all_index_pairs(masters):
if other_name == master_name and other_idx == index:
continue
other_entry = masters[other_name]
assert (
_get_int(other_entry["outputs"]["PSEL"], other_idx) == 0
), f"{other_name}{other_idx} must stay idle during read of {txn['label']}"
assert int(slave.PRDATA.value) == read_data, "Read data should propagate back to the slave"
assert int(slave.PREADY.value) == 1, "Slave ready should acknowledge the read"
assert int(slave.PSLVERR.value) == 0, "Read should not signal an error"
slave.PSEL.value = 0
slave.PENABLE.value = 0
_set_value(entry["inputs"]["PREADY"], index, 0)
_set_value(entry["inputs"]["PRDATA"], index, 0)
await Timer(1, units="ns")

View File

@@ -1,5 +1,8 @@
"""Pytest wrapper launching the APB3 cocotb smoke test.""" """Pytest wrapper launching the APB3 cocotb smoke tests."""
from __future__ import annotations
import json
from pathlib import Path from pathlib import Path
import pytest import pytest
@@ -11,20 +14,25 @@ try: # pragma: no cover - optional dependency shim
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
from cocotb_tools.runner import get_runner from cocotb_tools.runner import get_runner
from tests.cocotb_lib.utils import compile_rdl_and_export, get_verilog_sources from tests.cocotb_lib import RDL_CASES
from tests.cocotb_lib.utils import get_verilog_sources, prepare_cpuif_case
@pytest.mark.simulation @pytest.mark.simulation
@pytest.mark.verilator @pytest.mark.verilator
def test_apb3_smoke(tmp_path: Path) -> None: @pytest.mark.parametrize(("rdl_file", "top_name"), RDL_CASES, ids=[case[1] for case in RDL_CASES])
"""Compile the APB3 design and execute the cocotb smoke test.""" def test_apb3_smoke(tmp_path: Path, rdl_file: str, top_name: str) -> None:
"""Compile each APB3 design variant and execute the cocotb smoke test."""
repo_root = Path(__file__).resolve().parents[4] repo_root = Path(__file__).resolve().parents[4]
rdl_path = repo_root / "tests" / "cocotb_lib" / "rdl" / rdl_file
build_root = tmp_path / top_name
module_path, package_path = compile_rdl_and_export( module_path, package_path, config = prepare_cpuif_case(
str(repo_root / "tests" / "cocotb_lib" / "multiple_reg.rdl"), str(rdl_path),
"multi_reg", top_name,
tmp_path, build_root,
cpuif_cls=APB3CpuifFlat, cpuif_cls=APB3CpuifFlat,
control_signal="PSEL",
) )
sources = get_verilog_sources( sources = get_verilog_sources(
@@ -34,17 +42,18 @@ def test_apb3_smoke(tmp_path: Path) -> None:
) )
runner = get_runner("verilator") runner = get_runner("verilator")
build_dir = tmp_path / "sim_build" sim_build = build_root / "sim_build"
runner.build( runner.build(
sources=sources, sources=sources,
hdl_toplevel=module_path.stem, hdl_toplevel=module_path.stem,
build_dir=build_dir, build_dir=sim_build,
) )
runner.test( runner.test(
hdl_toplevel=module_path.stem, hdl_toplevel=module_path.stem,
test_module="tests.cocotb.apb3.smoke.test_register_access", test_module="tests.cocotb.apb3.smoke.test_register_access",
build_dir=build_dir, build_dir=sim_build,
log_file=str(tmp_path / "sim.log"), log_file=str(build_root / "simulation.log"),
extra_env={"RDL_TEST_CONFIG": json.dumps(config)},
) )

View File

@@ -0,0 +1,211 @@
"""APB3 smoke tests for variable depth design testing max_decode_depth parameter."""
import cocotb
from cocotb.triggers import Timer
class _Apb3SlaveShim:
def __init__(self, dut):
prefix = "s_apb"
self.PSEL = getattr(dut, f"{prefix}_PSEL")
self.PENABLE = getattr(dut, f"{prefix}_PENABLE")
self.PWRITE = getattr(dut, f"{prefix}_PWRITE")
self.PADDR = getattr(dut, f"{prefix}_PADDR")
self.PWDATA = getattr(dut, f"{prefix}_PWDATA")
self.PRDATA = getattr(dut, f"{prefix}_PRDATA")
self.PREADY = getattr(dut, f"{prefix}_PREADY")
self.PSLVERR = getattr(dut, f"{prefix}_PSLVERR")
class _Apb3MasterShim:
def __init__(self, dut, base: str):
self.PSEL = getattr(dut, f"{base}_PSEL")
self.PENABLE = getattr(dut, f"{base}_PENABLE")
self.PWRITE = getattr(dut, f"{base}_PWRITE")
self.PADDR = getattr(dut, f"{base}_PADDR")
self.PWDATA = getattr(dut, f"{base}_PWDATA")
self.PRDATA = getattr(dut, f"{base}_PRDATA")
self.PREADY = getattr(dut, f"{base}_PREADY")
self.PSLVERR = getattr(dut, f"{base}_PSLVERR")
def _apb3_slave(dut):
return getattr(dut, "s_apb", None) or _Apb3SlaveShim(dut)
def _apb3_master(dut, base: str):
return getattr(dut, base, None) or _Apb3MasterShim(dut, base)
@cocotb.test()
async def test_depth_1(dut):
"""Test max_decode_depth=1 - should have interface for inner1 only."""
s_apb = _apb3_slave(dut)
# At depth 1, we should have m_apb_inner1 but not deeper interfaces
inner1 = _apb3_master(dut, "m_apb_inner1")
# Default slave side inputs
s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0
s_apb.PWRITE.value = 0
s_apb.PADDR.value = 0
s_apb.PWDATA.value = 0
inner1.PRDATA.value = 0
inner1.PREADY.value = 0
inner1.PSLVERR.value = 0
await Timer(1, units="ns")
# Write to address 0x0 (should select inner1)
inner1.PREADY.value = 1
s_apb.PADDR.value = 0x0
s_apb.PWDATA.value = 0x12345678
s_apb.PWRITE.value = 1
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
await Timer(1, units="ns")
assert int(inner1.PSEL.value) == 1, "inner1 must be selected"
assert int(inner1.PWRITE.value) == 1, "Write should propagate"
assert int(s_apb.PREADY.value) == 1, "Ready should mirror master"
@cocotb.test()
async def test_depth_2(dut):
"""Test max_decode_depth=2 - should have interfaces for reg1 and inner2."""
s_apb = _apb3_slave(dut)
# At depth 2, we should have m_apb_reg1 and m_apb_inner2
reg1 = _apb3_master(dut, "m_apb_reg1")
inner2 = _apb3_master(dut, "m_apb_inner2")
# Default slave side inputs
s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0
s_apb.PWRITE.value = 0
s_apb.PADDR.value = 0
s_apb.PWDATA.value = 0
reg1.PRDATA.value = 0
reg1.PREADY.value = 0
reg1.PSLVERR.value = 0
inner2.PRDATA.value = 0
inner2.PREADY.value = 0
inner2.PSLVERR.value = 0
await Timer(1, units="ns")
# Write to address 0x0 (should select reg1)
reg1.PREADY.value = 1
s_apb.PADDR.value = 0x0
s_apb.PWDATA.value = 0xABCDEF01
s_apb.PWRITE.value = 1
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
await Timer(1, units="ns")
assert int(reg1.PSEL.value) == 1, "reg1 must be selected for address 0x0"
assert int(inner2.PSEL.value) == 0, "inner2 should not be selected"
# Reset
s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0
reg1.PREADY.value = 0
await Timer(1, units="ns")
# Write to address 0x10 (should select inner2)
inner2.PREADY.value = 1
s_apb.PADDR.value = 0x10
s_apb.PWDATA.value = 0x23456789
s_apb.PWRITE.value = 1
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
await Timer(1, units="ns")
assert int(inner2.PSEL.value) == 1, "inner2 must be selected for address 0x10"
assert int(reg1.PSEL.value) == 0, "reg1 should not be selected"
@cocotb.test()
async def test_depth_0(dut):
"""Test max_decode_depth=0 - should have interfaces for all leaf registers."""
s_apb = _apb3_slave(dut)
# At depth 0, we should have all leaf registers: reg1, reg2, reg2b
reg1 = _apb3_master(dut, "m_apb_reg1")
reg2 = _apb3_master(dut, "m_apb_reg2")
reg2b = _apb3_master(dut, "m_apb_reg2b")
# Default slave side inputs
s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0
s_apb.PWRITE.value = 0
s_apb.PADDR.value = 0
s_apb.PWDATA.value = 0
for master in [reg1, reg2, reg2b]:
master.PRDATA.value = 0
master.PREADY.value = 0
master.PSLVERR.value = 0
await Timer(1, units="ns")
# Write to address 0x0 (should select reg1)
reg1.PREADY.value = 1
s_apb.PADDR.value = 0x0
s_apb.PWDATA.value = 0x11111111
s_apb.PWRITE.value = 1
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
await Timer(1, units="ns")
assert int(reg1.PSEL.value) == 1, "reg1 must be selected for address 0x0"
assert int(reg2.PSEL.value) == 0, "reg2 should not be selected"
assert int(reg2b.PSEL.value) == 0, "reg2b should not be selected"
# Reset
s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0
reg1.PREADY.value = 0
await Timer(1, units="ns")
# Write to address 0x10 (should select reg2)
reg2.PREADY.value = 1
s_apb.PADDR.value = 0x10
s_apb.PWDATA.value = 0x22222222
s_apb.PWRITE.value = 1
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
await Timer(1, units="ns")
assert int(reg2.PSEL.value) == 1, "reg2 must be selected for address 0x10"
assert int(reg1.PSEL.value) == 0, "reg1 should not be selected"
assert int(reg2b.PSEL.value) == 0, "reg2b should not be selected"
# Reset
s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0
reg2.PREADY.value = 0
await Timer(1, units="ns")
# Write to address 0x14 (should select reg2b)
reg2b.PREADY.value = 1
s_apb.PADDR.value = 0x14
s_apb.PWDATA.value = 0x33333333
s_apb.PWRITE.value = 1
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
await Timer(1, units="ns")
assert int(reg2b.PSEL.value) == 1, "reg2b must be selected for address 0x14"
assert int(reg1.PSEL.value) == 0, "reg1 should not be selected"
assert int(reg2.PSEL.value) == 0, "reg2 should not be selected"

View File

@@ -0,0 +1,128 @@
"""Pytest wrapper launching the APB3 cocotb smoke test for variable depth."""
from pathlib import Path
import pytest
from peakrdl_busdecoder.cpuif.apb3.apb3_cpuif_flat import APB3CpuifFlat
try: # pragma: no cover - optional dependency shim
from cocotb.runner import get_runner
except ImportError: # pragma: no cover
from cocotb_tools.runner import get_runner
from tests.cocotb_lib.utils import compile_rdl_and_export, get_verilog_sources
@pytest.mark.simulation
@pytest.mark.verilator
def test_apb3_variable_depth_1(tmp_path: Path) -> None:
"""Test APB3 design with max_decode_depth=1."""
repo_root = Path(__file__).resolve().parents[4]
module_path, package_path = compile_rdl_and_export(
str(repo_root / "tests" / "cocotb_lib" / "variable_depth.rdl"),
"variable_depth",
tmp_path,
cpuif_cls=APB3CpuifFlat,
max_decode_depth=1,
)
sources = get_verilog_sources(
module_path,
package_path,
[repo_root / "hdl-src" / "apb3_intf.sv"],
)
runner = get_runner("verilator")
build_dir = tmp_path / "sim_build"
runner.build(
sources=sources,
hdl_toplevel=module_path.stem,
build_dir=build_dir,
)
runner.test(
hdl_toplevel=module_path.stem,
test_module="tests.cocotb.apb3.smoke.test_variable_depth",
build_dir=build_dir,
log_file=str(tmp_path / "sim_depth1.log"),
testcase="test_depth_1",
)
@pytest.mark.simulation
@pytest.mark.verilator
def test_apb3_variable_depth_2(tmp_path: Path) -> None:
"""Test APB3 design with max_decode_depth=2."""
repo_root = Path(__file__).resolve().parents[4]
module_path, package_path = compile_rdl_and_export(
str(repo_root / "tests" / "cocotb_lib" / "variable_depth.rdl"),
"variable_depth",
tmp_path,
cpuif_cls=APB3CpuifFlat,
max_decode_depth=2,
)
sources = get_verilog_sources(
module_path,
package_path,
[repo_root / "hdl-src" / "apb3_intf.sv"],
)
runner = get_runner("verilator")
build_dir = tmp_path / "sim_build"
runner.build(
sources=sources,
hdl_toplevel=module_path.stem,
build_dir=build_dir,
)
runner.test(
hdl_toplevel=module_path.stem,
test_module="tests.cocotb.apb3.smoke.test_variable_depth",
build_dir=build_dir,
log_file=str(tmp_path / "sim_depth2.log"),
testcase="test_depth_2",
)
@pytest.mark.simulation
@pytest.mark.verilator
def test_apb3_variable_depth_0(tmp_path: Path) -> None:
"""Test APB3 design with max_decode_depth=0 (all levels)."""
repo_root = Path(__file__).resolve().parents[4]
module_path, package_path = compile_rdl_and_export(
str(repo_root / "tests" / "cocotb_lib" / "variable_depth.rdl"),
"variable_depth",
tmp_path,
cpuif_cls=APB3CpuifFlat,
max_decode_depth=0,
)
sources = get_verilog_sources(
module_path,
package_path,
[repo_root / "hdl-src" / "apb3_intf.sv"],
)
runner = get_runner("verilator")
build_dir = tmp_path / "sim_build"
runner.build(
sources=sources,
hdl_toplevel=module_path.stem,
build_dir=build_dir,
)
runner.test(
hdl_toplevel=module_path.stem,
test_module="tests.cocotb.apb3.smoke.test_variable_depth",
build_dir=build_dir,
log_file=str(tmp_path / "sim_depth0.log"),
testcase="test_depth_0",
)

View File

@@ -1,15 +1,19 @@
"""APB4 smoke tests using generated multi-register design.""" """APB4 smoke tests generated from SystemRDL sources."""
from __future__ import annotations
import json
import os
from typing import Any, Iterable
import cocotb import cocotb
from cocotb.triggers import Timer from cocotb.triggers import Timer
WRITE_ADDR = 0x4 from tests.cocotb_lib.handle_utils import SignalHandle, resolve_handle
READ_ADDR = 0x8
WRITE_DATA = 0x1234_5678
READ_DATA = 0x89AB_CDEF
class _Apb4SlaveShim: class _Apb4SlaveShim:
"""Lightweight accessor for the APB4 slave side of the DUT."""
def __init__(self, dut): def __init__(self, dut):
prefix = "s_apb" prefix = "s_apb"
self.PSEL = getattr(dut, f"{prefix}_PSEL") self.PSEL = getattr(dut, f"{prefix}_PSEL")
@@ -24,115 +28,175 @@ class _Apb4SlaveShim:
self.PSLVERR = getattr(dut, f"{prefix}_PSLVERR") self.PSLVERR = getattr(dut, f"{prefix}_PSLVERR")
class _Apb4MasterShim: def _load_config() -> dict[str, Any]:
def __init__(self, dut, base: str): """Read the JSON payload describing the generated register topology."""
self.PSEL = getattr(dut, f"{base}_PSEL") payload = os.environ.get("RDL_TEST_CONFIG")
self.PENABLE = getattr(dut, f"{base}_PENABLE") if payload is None:
self.PWRITE = getattr(dut, f"{base}_PWRITE") raise RuntimeError("RDL_TEST_CONFIG environment variable was not provided")
self.PADDR = getattr(dut, f"{base}_PADDR") return json.loads(payload)
self.PPROT = getattr(dut, f"{base}_PPROT")
self.PWDATA = getattr(dut, f"{base}_PWDATA")
self.PSTRB = getattr(dut, f"{base}_PSTRB")
self.PRDATA = getattr(dut, f"{base}_PRDATA")
self.PREADY = getattr(dut, f"{base}_PREADY")
self.PSLVERR = getattr(dut, f"{base}_PSLVERR")
def _apb4_slave(dut): def _resolve(handle, indices: Iterable[int]):
return getattr(dut, "s_apb", None) or _Apb4SlaveShim(dut) """Index into hierarchical cocotb handles."""
return resolve_handle(handle, indices)
def _apb4_master(dut, base: str): def _set_value(handle, indices: Iterable[int], value: int) -> None:
return getattr(dut, base, None) or _Apb4MasterShim(dut, base) _resolve(handle, indices).value = value
def _get_int(handle, indices: Iterable[int]) -> int:
return int(_resolve(handle, indices).value)
def _build_master_table(dut, masters_cfg: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
table: dict[str, dict[str, Any]] = {}
for master in masters_cfg:
port_prefix = master["port_prefix"]
entry = {
"port_prefix": port_prefix,
"indices": [tuple(idx) for idx in master["indices"]] or [tuple()],
"outputs": {
"PSEL": SignalHandle(dut, f"{port_prefix}_PSEL"),
"PENABLE": SignalHandle(dut, f"{port_prefix}_PENABLE"),
"PWRITE": SignalHandle(dut, f"{port_prefix}_PWRITE"),
"PADDR": SignalHandle(dut, f"{port_prefix}_PADDR"),
"PPROT": SignalHandle(dut, f"{port_prefix}_PPROT"),
"PWDATA": SignalHandle(dut, f"{port_prefix}_PWDATA"),
"PSTRB": SignalHandle(dut, f"{port_prefix}_PSTRB"),
},
"inputs": {
"PRDATA": SignalHandle(dut, f"{port_prefix}_PRDATA"),
"PREADY": SignalHandle(dut, f"{port_prefix}_PREADY"),
"PSLVERR": SignalHandle(dut, f"{port_prefix}_PSLVERR"),
},
}
table[master["inst_name"]] = entry
return table
def _all_index_pairs(table: dict[str, dict[str, Any]]):
for name, entry in table.items():
for idx in entry["indices"]:
yield name, idx
def _write_pattern(address: int, width: int) -> int:
mask = (1 << width) - 1
return ((address * 0x1021) ^ 0x1357_9BDF) & mask
def _read_pattern(address: int, width: int) -> int:
mask = (1 << width) - 1
return ((address ^ 0xDEAD_BEE5) + width) & mask
@cocotb.test() @cocotb.test()
async def test_apb4_read_write_paths(dut): async def test_apb4_address_decoding(dut) -> None:
"""Drive APB4 slave signals and observe master activity.""" """Drive the APB4 slave interface and verify master fanout across all sampled registers."""
s_apb = _apb4_slave(dut) config = _load_config()
masters = { slave = _Apb4SlaveShim(dut)
"reg1": _apb4_master(dut, "m_apb_reg1"), masters = _build_master_table(dut, config["masters"])
"reg2": _apb4_master(dut, "m_apb_reg2"),
"reg3": _apb4_master(dut, "m_apb_reg3"),
}
# Default slave side inputs slave.PSEL.value = 0
s_apb.PSEL.value = 0 slave.PENABLE.value = 0
s_apb.PENABLE.value = 0 slave.PWRITE.value = 0
s_apb.PWRITE.value = 0 slave.PADDR.value = 0
s_apb.PADDR.value = 0 slave.PPROT.value = 0
s_apb.PWDATA.value = 0 slave.PWDATA.value = 0
s_apb.PPROT.value = 0 slave.PSTRB.value = 0
s_apb.PSTRB.value = 0
for master in masters.values(): for master_name, idx in _all_index_pairs(masters):
master.PRDATA.value = 0 entry = masters[master_name]
master.PREADY.value = 0 _set_value(entry["inputs"]["PRDATA"], idx, 0)
master.PSLVERR.value = 0 _set_value(entry["inputs"]["PREADY"], idx, 0)
_set_value(entry["inputs"]["PSLVERR"], idx, 0)
await Timer(1, units="ns") await Timer(1, units="ns")
# ------------------------------------------------------------------ addr_mask = (1 << config["address_width"]) - 1
# Write transfer to reg2 strobe_mask = (1 << config["byte_width"]) - 1
# ------------------------------------------------------------------
masters["reg2"].PREADY.value = 1
s_apb.PADDR.value = WRITE_ADDR
s_apb.PWDATA.value = WRITE_DATA
s_apb.PSTRB.value = 0xF
s_apb.PPROT.value = 0
s_apb.PWRITE.value = 1
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
await Timer(1, units="ns") for txn in config["transactions"]:
master_name = txn["master"]
index = tuple(txn["index"])
entry = masters[master_name]
assert int(masters["reg2"].PSEL.value) == 1, "reg2 must be selected for write" address = txn["address"] & addr_mask
assert int(masters["reg2"].PWRITE.value) == 1, "Write strobes should propagate" write_data = _write_pattern(address, config["data_width"])
assert int(masters["reg2"].PADDR.value) == WRITE_ADDR, "Address should fan out"
assert int(masters["reg2"].PWDATA.value) == WRITE_DATA, "Write data should fan out"
for name, master in masters.items(): # Prime master-side inputs for the write phase
if name != "reg2": _set_value(entry["inputs"]["PREADY"], index, 1)
assert int(master.PSEL.value) == 0, f"{name} should remain idle on write" _set_value(entry["inputs"]["PSLVERR"], index, 0)
assert int(s_apb.PREADY.value) == 1, "Ready should mirror selected master" slave.PADDR.value = address
assert int(s_apb.PSLVERR.value) == 0, "No error expected on successful write" slave.PWDATA.value = write_data
slave.PSTRB.value = strobe_mask
slave.PPROT.value = 0
slave.PWRITE.value = 1
slave.PSEL.value = 1
slave.PENABLE.value = 1
# Return to idle await Timer(1, units="ns")
s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0
s_apb.PWRITE.value = 0
masters["reg2"].PREADY.value = 0
await Timer(1, units="ns")
# ------------------------------------------------------------------ assert _get_int(entry["outputs"]["PSEL"], index) == 1, f"{master_name} should assert PSEL for write"
# Read transfer from reg3 assert _get_int(entry["outputs"]["PWRITE"], index) == 1, f"{master_name} should see write intent"
# ------------------------------------------------------------------ assert _get_int(entry["outputs"]["PADDR"], index) == address, f"{master_name} must receive write address"
masters["reg3"].PRDATA.value = READ_DATA assert _get_int(entry["outputs"]["PWDATA"], index) == write_data, f"{master_name} must receive write data"
masters["reg3"].PREADY.value = 1 assert _get_int(entry["outputs"]["PSTRB"], index) == strobe_mask, f"{master_name} must receive full strobes"
masters["reg3"].PSLVERR.value = 0
s_apb.PADDR.value = READ_ADDR for other_name, other_idx in _all_index_pairs(masters):
s_apb.PSEL.value = 1 if other_name == master_name and other_idx == index:
s_apb.PENABLE.value = 1 continue
s_apb.PWRITE.value = 0 other_entry = masters[other_name]
assert (
_get_int(other_entry["outputs"]["PSEL"], other_idx) == 0
), f"{other_name}{other_idx} should remain idle during {txn['label']}"
await Timer(1, units="ns") assert int(slave.PREADY.value) == 1, "Slave ready should reflect selected master"
assert int(slave.PSLVERR.value) == 0, "No error expected during write"
assert int(masters["reg3"].PSEL.value) == 1, "reg3 must be selected for read" # Return to idle for next transaction
assert int(masters["reg3"].PWRITE.value) == 0, "Read should deassert write" slave.PSEL.value = 0
assert int(masters["reg3"].PADDR.value) == READ_ADDR, "Read address should propagate" slave.PENABLE.value = 0
slave.PWRITE.value = 0
_set_value(entry["inputs"]["PREADY"], index, 0)
await Timer(1, units="ns")
for name, master in masters.items(): # ------------------------------------------------------------------
if name != "reg3": # Read phase
assert int(master.PSEL.value) == 0, f"{name} should remain idle on read" # ------------------------------------------------------------------
read_data = _read_pattern(address, config["data_width"])
_set_value(entry["inputs"]["PRDATA"], index, read_data)
_set_value(entry["inputs"]["PREADY"], index, 1)
_set_value(entry["inputs"]["PSLVERR"], index, 0)
assert int(s_apb.PRDATA.value) == READ_DATA, "Read data should return from master" slave.PADDR.value = address
assert int(s_apb.PREADY.value) == 1, "Ready must follow selected master" slave.PWRITE.value = 0
assert int(s_apb.PSLVERR.value) == 0, "No error expected on successful read" slave.PSEL.value = 1
slave.PENABLE.value = 1
# Back to idle await Timer(1, units="ns")
s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0 assert _get_int(entry["outputs"]["PSEL"], index) == 1, f"{master_name} must assert PSEL for read"
masters["reg3"].PREADY.value = 0 assert _get_int(entry["outputs"]["PWRITE"], index) == 0, f"{master_name} should deassert write for reads"
await Timer(1, units="ns") assert _get_int(entry["outputs"]["PADDR"], index) == address, f"{master_name} must receive read address"
for other_name, other_idx in _all_index_pairs(masters):
if other_name == master_name and other_idx == index:
continue
other_entry = masters[other_name]
assert (
_get_int(other_entry["outputs"]["PSEL"], other_idx) == 0
), f"{other_name}{other_idx} must stay idle during read of {txn['label']}"
assert int(slave.PRDATA.value) == read_data, "Slave should observe readback data from master"
assert int(slave.PREADY.value) == 1, "Slave ready should follow responding master"
assert int(slave.PSLVERR.value) == 0, "Read should complete without error"
# Reset to idle before progressing
slave.PSEL.value = 0
slave.PENABLE.value = 0
_set_value(entry["inputs"]["PREADY"], index, 0)
_set_value(entry["inputs"]["PRDATA"], index, 0)
await Timer(1, units="ns")

View File

@@ -1,7 +1,10 @@
"""Pytest wrapper launching the APB4 cocotb smoke test.""" """Pytest wrapper launching the APB4 cocotb smoke tests."""
from __future__ import annotations
import json
from pathlib import Path from pathlib import Path
import logging
import pytest import pytest
from peakrdl_busdecoder.cpuif.apb4.apb4_cpuif_flat import APB4CpuifFlat from peakrdl_busdecoder.cpuif.apb4.apb4_cpuif_flat import APB4CpuifFlat
@@ -11,20 +14,25 @@ try: # pragma: no cover - optional dependency shim
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
from cocotb_tools.runner import get_runner from cocotb_tools.runner import get_runner
from tests.cocotb_lib.utils import compile_rdl_and_export, get_verilog_sources from tests.cocotb_lib import RDL_CASES
from tests.cocotb_lib.utils import get_verilog_sources, prepare_cpuif_case
@pytest.mark.simulation @pytest.mark.simulation
@pytest.mark.verilator @pytest.mark.verilator
def test_apb4_smoke(tmp_path: Path) -> None: @pytest.mark.parametrize(("rdl_file", "top_name"), RDL_CASES, ids=[case[1] for case in RDL_CASES])
"""Compile the APB4 design and execute the cocotb smoke test.""" def test_apb4_smoke(tmp_path: Path, rdl_file: str, top_name: str) -> None:
"""Compile each APB4 design variant and execute the cocotb smoke test."""
repo_root = Path(__file__).resolve().parents[4] repo_root = Path(__file__).resolve().parents[4]
rdl_path = repo_root / "tests" / "cocotb_lib" / "rdl" / rdl_file
build_root = tmp_path / top_name
module_path, package_path = compile_rdl_and_export( module_path, package_path, config = prepare_cpuif_case(
str(repo_root / "tests" / "cocotb_lib" / "multiple_reg.rdl"), str(rdl_path),
"multi_reg", top_name,
tmp_path, build_root,
cpuif_cls=APB4CpuifFlat, cpuif_cls=APB4CpuifFlat,
control_signal="PSEL",
) )
sources = get_verilog_sources( sources = get_verilog_sources(
@@ -34,17 +42,39 @@ def test_apb4_smoke(tmp_path: Path) -> None:
) )
runner = get_runner("verilator") runner = get_runner("verilator")
build_dir = tmp_path / "sim_build" sim_build = build_root / "sim_build"
try:
runner.build(
sources=sources,
hdl_toplevel=module_path.stem,
build_dir=sim_build,
log_file=str(build_root / "build.log"),
)
except SystemExit as e:
# Print build log on failure for easier debugging
log_path = build_root / "build.log"
if log_path.exists():
logging.error("\n\n=== Build Log ===\n")
logging.error(log_path.read_text())
logging.error("\n=== End Build Log ===\n")
if e.code != 0:
raise
runner.build( try:
sources=sources, runner.test(
hdl_toplevel=module_path.stem, hdl_toplevel=module_path.stem,
build_dir=build_dir, test_module="tests.cocotb.apb4.smoke.test_register_access",
) build_dir=sim_build,
log_file=str(build_root / "simulation.log"),
runner.test( extra_env={"RDL_TEST_CONFIG": json.dumps(config)},
hdl_toplevel=module_path.stem, )
test_module="tests.cocotb.apb4.smoke.test_register_access", except SystemExit as e:
build_dir=build_dir, # Print simulation log on failure for easier debugging
log_file=str(tmp_path / "sim.log"), log_path = build_root / "simulation.log"
) if log_path.exists():
logging.error("\n\n=== Simulation Log ===\n")
logging.error(log_path.read_text())
logging.error("\n=== End Simulation Log ===\n")
if e.code != 0:
raise

View File

@@ -0,0 +1,227 @@
"""APB4 smoke tests for variable depth design testing max_decode_depth parameter."""
import cocotb
from cocotb.triggers import Timer
class _Apb4SlaveShim:
def __init__(self, dut):
prefix = "s_apb"
self.PSEL = getattr(dut, f"{prefix}_PSEL")
self.PENABLE = getattr(dut, f"{prefix}_PENABLE")
self.PWRITE = getattr(dut, f"{prefix}_PWRITE")
self.PADDR = getattr(dut, f"{prefix}_PADDR")
self.PPROT = getattr(dut, f"{prefix}_PPROT")
self.PWDATA = getattr(dut, f"{prefix}_PWDATA")
self.PSTRB = getattr(dut, f"{prefix}_PSTRB")
self.PRDATA = getattr(dut, f"{prefix}_PRDATA")
self.PREADY = getattr(dut, f"{prefix}_PREADY")
self.PSLVERR = getattr(dut, f"{prefix}_PSLVERR")
class _Apb4MasterShim:
def __init__(self, dut, base: str):
self.PSEL = getattr(dut, f"{base}_PSEL")
self.PENABLE = getattr(dut, f"{base}_PENABLE")
self.PWRITE = getattr(dut, f"{base}_PWRITE")
self.PADDR = getattr(dut, f"{base}_PADDR")
self.PPROT = getattr(dut, f"{base}_PPROT")
self.PWDATA = getattr(dut, f"{base}_PWDATA")
self.PSTRB = getattr(dut, f"{base}_PSTRB")
self.PRDATA = getattr(dut, f"{base}_PRDATA")
self.PREADY = getattr(dut, f"{base}_PREADY")
self.PSLVERR = getattr(dut, f"{base}_PSLVERR")
def _apb4_slave(dut):
return getattr(dut, "s_apb", None) or _Apb4SlaveShim(dut)
def _apb4_master(dut, base: str):
return getattr(dut, base, None) or _Apb4MasterShim(dut, base)
@cocotb.test()
async def test_depth_1(dut):
"""Test max_decode_depth=1 - should have interface for inner1 only."""
s_apb = _apb4_slave(dut)
# At depth 1, we should have m_apb_inner1 but not deeper interfaces
inner1 = _apb4_master(dut, "m_apb_inner1")
# Default slave side inputs
s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0
s_apb.PWRITE.value = 0
s_apb.PADDR.value = 0
s_apb.PWDATA.value = 0
s_apb.PPROT.value = 0
s_apb.PSTRB.value = 0
inner1.PRDATA.value = 0
inner1.PREADY.value = 0
inner1.PSLVERR.value = 0
await Timer(1, units="ns")
# Write to address 0x0 (should select inner1)
inner1.PREADY.value = 1
s_apb.PADDR.value = 0x0
s_apb.PWDATA.value = 0x12345678
s_apb.PSTRB.value = 0xF
s_apb.PWRITE.value = 1
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
await Timer(1, units="ns")
assert int(inner1.PSEL.value) == 1, "inner1 must be selected"
assert int(inner1.PWRITE.value) == 1, "Write should propagate"
assert int(s_apb.PREADY.value) == 1, "Ready should mirror master"
@cocotb.test()
async def test_depth_2(dut):
"""Test max_decode_depth=2 - should have interfaces for reg1 and inner2."""
s_apb = _apb4_slave(dut)
# At depth 2, we should have m_apb_reg1 and m_apb_inner2
reg1 = _apb4_master(dut, "m_apb_reg1")
inner2 = _apb4_master(dut, "m_apb_inner2")
# Default slave side inputs
s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0
s_apb.PWRITE.value = 0
s_apb.PADDR.value = 0
s_apb.PWDATA.value = 0
s_apb.PPROT.value = 0
s_apb.PSTRB.value = 0
reg1.PRDATA.value = 0
reg1.PREADY.value = 0
reg1.PSLVERR.value = 0
inner2.PRDATA.value = 0
inner2.PREADY.value = 0
inner2.PSLVERR.value = 0
await Timer(1, units="ns")
# Write to address 0x0 (should select reg1)
reg1.PREADY.value = 1
s_apb.PADDR.value = 0x0
s_apb.PWDATA.value = 0xABCDEF01
s_apb.PSTRB.value = 0xF
s_apb.PWRITE.value = 1
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
await Timer(1, units="ns")
assert int(reg1.PSEL.value) == 1, "reg1 must be selected for address 0x0"
assert int(inner2.PSEL.value) == 0, "inner2 should not be selected"
# Reset
s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0
reg1.PREADY.value = 0
await Timer(1, units="ns")
# Write to address 0x10 (should select inner2)
inner2.PREADY.value = 1
s_apb.PADDR.value = 0x10
s_apb.PWDATA.value = 0x23456789
s_apb.PSTRB.value = 0xF
s_apb.PWRITE.value = 1
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
await Timer(1, units="ns")
assert int(inner2.PSEL.value) == 1, "inner2 must be selected for address 0x10"
assert int(reg1.PSEL.value) == 0, "reg1 should not be selected"
@cocotb.test()
async def test_depth_0(dut):
"""Test max_decode_depth=0 - should have interfaces for all leaf registers."""
s_apb = _apb4_slave(dut)
# At depth 0, we should have all leaf registers: reg1, reg2, reg2b
reg1 = _apb4_master(dut, "m_apb_reg1")
reg2 = _apb4_master(dut, "m_apb_reg2")
reg2b = _apb4_master(dut, "m_apb_reg2b")
# Default slave side inputs
s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0
s_apb.PWRITE.value = 0
s_apb.PADDR.value = 0
s_apb.PWDATA.value = 0
s_apb.PPROT.value = 0
s_apb.PSTRB.value = 0
for master in [reg1, reg2, reg2b]:
master.PRDATA.value = 0
master.PREADY.value = 0
master.PSLVERR.value = 0
await Timer(1, units="ns")
# Write to address 0x0 (should select reg1)
reg1.PREADY.value = 1
s_apb.PADDR.value = 0x0
s_apb.PWDATA.value = 0x11111111
s_apb.PSTRB.value = 0xF
s_apb.PWRITE.value = 1
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
await Timer(1, units="ns")
assert int(reg1.PSEL.value) == 1, "reg1 must be selected for address 0x0"
assert int(reg2.PSEL.value) == 0, "reg2 should not be selected"
assert int(reg2b.PSEL.value) == 0, "reg2b should not be selected"
# Reset
s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0
reg1.PREADY.value = 0
await Timer(1, units="ns")
# Write to address 0x10 (should select reg2)
reg2.PREADY.value = 1
s_apb.PADDR.value = 0x10
s_apb.PWDATA.value = 0x22222222
s_apb.PSTRB.value = 0xF
s_apb.PWRITE.value = 1
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
await Timer(1, units="ns")
assert int(reg2.PSEL.value) == 1, "reg2 must be selected for address 0x10"
assert int(reg1.PSEL.value) == 0, "reg1 should not be selected"
assert int(reg2b.PSEL.value) == 0, "reg2b should not be selected"
# Reset
s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0
reg2.PREADY.value = 0
await Timer(1, units="ns")
# Write to address 0x14 (should select reg2b)
reg2b.PREADY.value = 1
s_apb.PADDR.value = 0x14
s_apb.PWDATA.value = 0x33333333
s_apb.PSTRB.value = 0xF
s_apb.PWRITE.value = 1
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
await Timer(1, units="ns")
assert int(reg2b.PSEL.value) == 1, "reg2b must be selected for address 0x14"
assert int(reg1.PSEL.value) == 0, "reg1 should not be selected"
assert int(reg2.PSEL.value) == 0, "reg2 should not be selected"

View File

@@ -0,0 +1,131 @@
"""Pytest wrapper launching the APB4 cocotb smoke test for variable depth."""
from pathlib import Path
import pytest
from peakrdl_busdecoder.cpuif.apb4.apb4_cpuif_flat import APB4CpuifFlat
try: # pragma: no cover - optional dependency shim
from cocotb.runner import get_runner
except ImportError: # pragma: no cover
from cocotb_tools.runner import get_runner
from tests.cocotb_lib.utils import compile_rdl_and_export, get_verilog_sources
@pytest.mark.simulation
@pytest.mark.verilator
def test_apb4_variable_depth_1(tmp_path: Path) -> None:
"""Test APB4 design with max_decode_depth=1."""
repo_root = Path(__file__).resolve().parents[4]
module_path, package_path = compile_rdl_and_export(
str(repo_root / "tests" / "cocotb_lib" / "variable_depth.rdl"),
"variable_depth",
tmp_path,
cpuif_cls=APB4CpuifFlat,
max_decode_depth=1,
)
sources = get_verilog_sources(
module_path,
package_path,
[repo_root / "hdl-src" / "apb4_intf.sv"],
)
runner = get_runner("verilator")
build_dir = tmp_path / "sim_build"
runner.build(
sources=sources,
hdl_toplevel=module_path.stem,
build_dir=build_dir,
log_file=str(tmp_path / "build_depth_1.log"),
)
runner.test(
hdl_toplevel=module_path.stem,
test_module="tests.cocotb.apb4.smoke.test_variable_depth",
build_dir=build_dir,
log_file=str(tmp_path / "sim_depth1.log"),
testcase="test_depth_1",
)
@pytest.mark.simulation
@pytest.mark.verilator
def test_apb4_variable_depth_2(tmp_path: Path) -> None:
"""Test APB4 design with max_decode_depth=2."""
repo_root = Path(__file__).resolve().parents[4]
module_path, package_path = compile_rdl_and_export(
str(repo_root / "tests" / "cocotb_lib" / "variable_depth.rdl"),
"variable_depth",
tmp_path,
cpuif_cls=APB4CpuifFlat,
max_decode_depth=2,
)
sources = get_verilog_sources(
module_path,
package_path,
[repo_root / "hdl-src" / "apb4_intf.sv"],
)
runner = get_runner("verilator")
build_dir = tmp_path / "sim_build"
runner.build(
sources=sources,
hdl_toplevel=module_path.stem,
build_dir=build_dir,
log_file=str(tmp_path / "build_depth_2.log"),
)
runner.test(
hdl_toplevel=module_path.stem,
test_module="tests.cocotb.apb4.smoke.test_variable_depth",
build_dir=build_dir,
log_file=str(tmp_path / "sim_depth_2.log"),
testcase="test_depth_2",
)
@pytest.mark.simulation
@pytest.mark.verilator
def test_apb4_variable_depth_0(tmp_path: Path) -> None:
"""Test APB4 design with max_decode_depth=0 (all levels)."""
repo_root = Path(__file__).resolve().parents[4]
module_path, package_path = compile_rdl_and_export(
str(repo_root / "tests" / "cocotb_lib" / "variable_depth.rdl"),
"variable_depth",
tmp_path,
cpuif_cls=APB4CpuifFlat,
max_decode_depth=0,
)
sources = get_verilog_sources(
module_path,
package_path,
[repo_root / "hdl-src" / "apb4_intf.sv"],
)
runner = get_runner("verilator")
build_dir = tmp_path / "sim_build"
runner.build(
sources=sources,
hdl_toplevel=module_path.stem,
build_dir=build_dir,
log_file=str(tmp_path / "build_depth_0.log"),
)
runner.test(
hdl_toplevel=module_path.stem,
test_module="tests.cocotb.apb4.smoke.test_variable_depth",
build_dir=build_dir,
log_file=str(tmp_path / "sim_depth_0.log"),
testcase="test_depth_0",
)

View File

@@ -1,15 +1,19 @@
"""AXI4-Lite smoke test ensuring address decode fanout works.""" """AXI4-Lite smoke test driven from SystemRDL-generated register maps."""
from __future__ import annotations
import json
import os
from typing import Any, Iterable
import cocotb import cocotb
from cocotb.triggers import Timer from cocotb.triggers import Timer
WRITE_ADDR = 0x4 from tests.cocotb_lib.handle_utils import SignalHandle, resolve_handle
READ_ADDR = 0x8
WRITE_DATA = 0x1357_9BDF
READ_DATA = 0x2468_ACED
class _AxilSlaveShim: class _AxilSlaveShim:
"""Accessor for AXI4-Lite slave ports on the DUT."""
def __init__(self, dut): def __init__(self, dut):
prefix = "s_axil" prefix = "s_axil"
self.AWREADY = getattr(dut, f"{prefix}_AWREADY") self.AWREADY = getattr(dut, f"{prefix}_AWREADY")
@@ -33,129 +37,177 @@ class _AxilSlaveShim:
self.RRESP = getattr(dut, f"{prefix}_RRESP") self.RRESP = getattr(dut, f"{prefix}_RRESP")
class _AxilMasterShim: def _load_config() -> dict[str, Any]:
def __init__(self, dut, base: str): payload = os.environ.get("RDL_TEST_CONFIG")
self.AWREADY = getattr(dut, f"{base}_AWREADY") if payload is None:
self.AWVALID = getattr(dut, f"{base}_AWVALID") raise RuntimeError("RDL_TEST_CONFIG environment variable was not provided")
self.AWADDR = getattr(dut, f"{base}_AWADDR") return json.loads(payload)
self.AWPROT = getattr(dut, f"{base}_AWPROT")
self.WREADY = getattr(dut, f"{base}_WREADY")
self.WVALID = getattr(dut, f"{base}_WVALID")
self.WDATA = getattr(dut, f"{base}_WDATA")
self.WSTRB = getattr(dut, f"{base}_WSTRB")
self.BREADY = getattr(dut, f"{base}_BREADY")
self.BVALID = getattr(dut, f"{base}_BVALID")
self.BRESP = getattr(dut, f"{base}_BRESP")
self.ARREADY = getattr(dut, f"{base}_ARREADY")
self.ARVALID = getattr(dut, f"{base}_ARVALID")
self.ARADDR = getattr(dut, f"{base}_ARADDR")
self.ARPROT = getattr(dut, f"{base}_ARPROT")
self.RREADY = getattr(dut, f"{base}_RREADY")
self.RVALID = getattr(dut, f"{base}_RVALID")
self.RDATA = getattr(dut, f"{base}_RDATA")
self.RRESP = getattr(dut, f"{base}_RRESP")
def _axil_slave(dut): def _resolve(handle, indices: Iterable[int]):
return getattr(dut, "s_axil", None) or _AxilSlaveShim(dut) return resolve_handle(handle, indices)
def _axil_master(dut, base: str): def _set_value(handle, indices: Iterable[int], value: int) -> None:
return getattr(dut, base, None) or _AxilMasterShim(dut, base) _resolve(handle, indices).value = value
def _get_int(handle, indices: Iterable[int]) -> int:
return int(_resolve(handle, indices).value)
def _build_master_table(dut, masters_cfg: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
table: dict[str, dict[str, Any]] = {}
for master in masters_cfg:
prefix = master["port_prefix"]
entry = {
"indices": [tuple(idx) for idx in master["indices"]] or [tuple()],
"outputs": {
"AWVALID": SignalHandle(dut, f"{prefix}_AWVALID"),
"AWADDR": SignalHandle(dut, f"{prefix}_AWADDR"),
"AWPROT": SignalHandle(dut, f"{prefix}_AWPROT"),
"WVALID": SignalHandle(dut, f"{prefix}_WVALID"),
"WDATA": SignalHandle(dut, f"{prefix}_WDATA"),
"WSTRB": SignalHandle(dut, f"{prefix}_WSTRB"),
"ARVALID": SignalHandle(dut, f"{prefix}_ARVALID"),
"ARADDR": SignalHandle(dut, f"{prefix}_ARADDR"),
"ARPROT": SignalHandle(dut, f"{prefix}_ARPROT"),
},
"inputs": {
"AWREADY": SignalHandle(dut, f"{prefix}_AWREADY"),
"WREADY": SignalHandle(dut, f"{prefix}_WREADY"),
"BVALID": SignalHandle(dut, f"{prefix}_BVALID"),
"BRESP": SignalHandle(dut, f"{prefix}_BRESP"),
"ARREADY": SignalHandle(dut, f"{prefix}_ARREADY"),
"RVALID": SignalHandle(dut, f"{prefix}_RVALID"),
"RDATA": SignalHandle(dut, f"{prefix}_RDATA"),
"RRESP": SignalHandle(dut, f"{prefix}_RRESP"),
},
}
table[master["inst_name"]] = entry
return table
def _all_index_pairs(table: dict[str, dict[str, Any]]):
for name, entry in table.items():
for idx in entry["indices"]:
yield name, idx
def _write_pattern(address: int, width: int) -> int:
mask = (1 << width) - 1
return ((address * 0x3105) ^ 0x1357_9BDF) & mask
def _read_pattern(address: int, width: int) -> int:
mask = (1 << width) - 1
return ((address ^ 0x2468_ACED) + width) & mask
@cocotb.test() @cocotb.test()
async def test_axi4lite_read_write_paths(dut): async def test_axi4lite_address_decoding(dut) -> None:
"""Drive AXI4-Lite slave channels and validate master side wiring.""" """Stimulate AXI4-Lite slave channels and verify master port selection."""
s_axil = _axil_slave(dut) config = _load_config()
masters = { slave = _AxilSlaveShim(dut)
"reg1": _axil_master(dut, "m_axil_reg1"), masters = _build_master_table(dut, config["masters"])
"reg2": _axil_master(dut, "m_axil_reg2"),
"reg3": _axil_master(dut, "m_axil_reg3"),
}
# Default slave-side inputs slave.AWVALID.value = 0
s_axil.AWVALID.value = 0 slave.AWADDR.value = 0
s_axil.AWADDR.value = 0 slave.AWPROT.value = 0
s_axil.AWPROT.value = 0 slave.WVALID.value = 0
s_axil.WVALID.value = 0 slave.WDATA.value = 0
s_axil.WDATA.value = 0 slave.WSTRB.value = 0
s_axil.WSTRB.value = 0 slave.BREADY.value = 0
s_axil.BREADY.value = 0 slave.ARVALID.value = 0
s_axil.ARVALID.value = 0 slave.ARADDR.value = 0
s_axil.ARADDR.value = 0 slave.ARPROT.value = 0
s_axil.ARPROT.value = 0 slave.RREADY.value = 0
s_axil.RREADY.value = 0
for master in masters.values(): for master_name, idx in _all_index_pairs(masters):
master.AWREADY.value = 0 entry = masters[master_name]
master.WREADY.value = 0 _set_value(entry["inputs"]["AWREADY"], idx, 0)
master.BVALID.value = 0 _set_value(entry["inputs"]["WREADY"], idx, 0)
master.BRESP.value = 0 _set_value(entry["inputs"]["BVALID"], idx, 0)
master.ARREADY.value = 0 _set_value(entry["inputs"]["BRESP"], idx, 0)
master.RVALID.value = 0 _set_value(entry["inputs"]["ARREADY"], idx, 0)
master.RDATA.value = 0 _set_value(entry["inputs"]["RVALID"], idx, 0)
master.RRESP.value = 0 _set_value(entry["inputs"]["RDATA"], idx, 0)
_set_value(entry["inputs"]["RRESP"], idx, 0)
await Timer(1, units="ns") await Timer(1, units="ns")
# -------------------------------------------------------------- addr_mask = (1 << config["address_width"]) - 1
# Write transaction targeting reg2 strobe_mask = (1 << config["byte_width"]) - 1
# --------------------------------------------------------------
s_axil.AWADDR.value = WRITE_ADDR
s_axil.AWPROT.value = 0
s_axil.AWVALID.value = 1
s_axil.WDATA.value = WRITE_DATA
s_axil.WSTRB.value = 0xF
s_axil.WVALID.value = 1
s_axil.BREADY.value = 1
await Timer(1, units="ns") for txn in config["transactions"]:
master_name = txn["master"]
index = tuple(txn["index"])
entry = masters[master_name]
assert int(masters["reg2"].AWVALID.value) == 1, "reg2 AWVALID should follow slave" address = txn["address"] & addr_mask
assert int(masters["reg2"].WVALID.value) == 1, "reg2 WVALID should follow slave" write_data = _write_pattern(address, config["data_width"])
assert int(masters["reg2"].AWADDR.value) == WRITE_ADDR, "AWADDR should fan out"
assert int(masters["reg2"].WDATA.value) == WRITE_DATA, "WDATA should fan out"
assert int(masters["reg2"].WSTRB.value) == 0xF, "WSTRB should propagate"
for name, master in masters.items(): slave.AWADDR.value = address
if name != "reg2": slave.AWPROT.value = 0
assert int(master.AWVALID.value) == 0, f"{name} AWVALID should stay low" slave.AWVALID.value = 1
assert int(master.WVALID.value) == 0, f"{name} WVALID should stay low" slave.WDATA.value = write_data
slave.WSTRB.value = strobe_mask
slave.WVALID.value = 1
slave.BREADY.value = 1
# Release write channel await Timer(1, units="ns")
s_axil.AWVALID.value = 0
s_axil.WVALID.value = 0
s_axil.BREADY.value = 0
await Timer(1, units="ns")
# -------------------------------------------------------------- assert _get_int(entry["outputs"]["AWVALID"], index) == 1, f"{master_name} should see AWVALID asserted"
# Read transaction targeting reg3 assert _get_int(entry["outputs"]["AWADDR"], index) == address, f"{master_name} must receive AWADDR"
# -------------------------------------------------------------- assert _get_int(entry["outputs"]["WVALID"], index) == 1, f"{master_name} should see WVALID asserted"
masters["reg3"].RVALID.value = 1 assert _get_int(entry["outputs"]["WDATA"], index) == write_data, f"{master_name} must receive WDATA"
masters["reg3"].RDATA.value = READ_DATA assert _get_int(entry["outputs"]["WSTRB"], index) == strobe_mask, f"{master_name} must receive WSTRB"
masters["reg3"].RRESP.value = 0
s_axil.ARADDR.value = READ_ADDR for other_name, other_idx in _all_index_pairs(masters):
s_axil.ARPROT.value = 0 if other_name == master_name and other_idx == index:
s_axil.ARVALID.value = 1 continue
s_axil.RREADY.value = 1 other_entry = masters[other_name]
assert (
_get_int(other_entry["outputs"]["AWVALID"], other_idx) == 0
), f"{other_name}{other_idx} AWVALID should remain low during {txn['label']}"
assert (
_get_int(other_entry["outputs"]["WVALID"], other_idx) == 0
), f"{other_name}{other_idx} WVALID should remain low during {txn['label']}"
await Timer(1, units="ns") slave.AWVALID.value = 0
slave.WVALID.value = 0
slave.BREADY.value = 0
await Timer(1, units="ns")
assert int(masters["reg3"].ARVALID.value) == 1, "reg3 ARVALID should follow slave" read_data = _read_pattern(address, config["data_width"])
assert int(masters["reg3"].ARADDR.value) == READ_ADDR, "ARADDR should fan out" _set_value(entry["inputs"]["RVALID"], index, 1)
_set_value(entry["inputs"]["RDATA"], index, read_data)
_set_value(entry["inputs"]["RRESP"], index, 0)
for name, master in masters.items(): slave.ARADDR.value = address
if name != "reg3": slave.ARPROT.value = 0
assert int(master.ARVALID.value) == 0, f"{name} ARVALID should stay low" slave.ARVALID.value = 1
slave.RREADY.value = 1
assert int(s_axil.RVALID.value) == 1, "Slave should raise RVALID when master responds" await Timer(1, units="ns")
assert int(s_axil.RDATA.value) == READ_DATA, "Read data should return to slave"
assert int(s_axil.RRESP.value) == 0, "No error expected for read"
# Return to idle assert _get_int(entry["outputs"]["ARVALID"], index) == 1, f"{master_name} should assert ARVALID"
s_axil.ARVALID.value = 0 assert _get_int(entry["outputs"]["ARADDR"], index) == address, f"{master_name} must receive ARADDR"
s_axil.RREADY.value = 0
masters["reg3"].RVALID.value = 0 for other_name, other_idx in _all_index_pairs(masters):
await Timer(1, units="ns") if other_name == master_name and other_idx == index:
continue
other_entry = masters[other_name]
assert (
_get_int(other_entry["outputs"]["ARVALID"], other_idx) == 0
), f"{other_name}{other_idx} ARVALID should remain low during read of {txn['label']}"
assert int(slave.RVALID.value) == 1, "Slave should observe RVALID when master responds"
assert int(slave.RDATA.value) == read_data, "Read data must fold back to slave"
assert int(slave.RRESP.value) == 0, "Read response should indicate success"
slave.ARVALID.value = 0
slave.RREADY.value = 0
_set_value(entry["inputs"]["RVALID"], index, 0)
_set_value(entry["inputs"]["RDATA"], index, 0)
await Timer(1, units="ns")

View File

@@ -1,5 +1,8 @@
"""Pytest wrapper launching the AXI4-Lite cocotb smoke test.""" """Pytest wrapper launching the AXI4-Lite cocotb smoke tests."""
from __future__ import annotations
import json
from pathlib import Path from pathlib import Path
import pytest import pytest
@@ -11,20 +14,25 @@ try: # pragma: no cover - optional dependency shim
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
from cocotb_tools.runner import get_runner from cocotb_tools.runner import get_runner
from tests.cocotb_lib.utils import compile_rdl_and_export, get_verilog_sources from tests.cocotb_lib import RDL_CASES
from tests.cocotb_lib.utils import get_verilog_sources, prepare_cpuif_case
@pytest.mark.simulation @pytest.mark.simulation
@pytest.mark.verilator @pytest.mark.verilator
def test_axi4lite_smoke(tmp_path: Path) -> None: @pytest.mark.parametrize(("rdl_file", "top_name"), RDL_CASES, ids=[case[1] for case in RDL_CASES])
"""Compile the AXI4-Lite design and execute the cocotb smoke test.""" def test_axi4lite_smoke(tmp_path: Path, rdl_file: str, top_name: str) -> None:
"""Compile each AXI4-Lite design variant and execute the cocotb smoke test."""
repo_root = Path(__file__).resolve().parents[4] repo_root = Path(__file__).resolve().parents[4]
rdl_path = repo_root / "tests" / "cocotb_lib" / "rdl" / rdl_file
build_root = tmp_path / top_name
module_path, package_path = compile_rdl_and_export( module_path, package_path, config = prepare_cpuif_case(
str(repo_root / "tests" / "cocotb_lib" / "multiple_reg.rdl"), str(rdl_path),
"multi_reg", top_name,
tmp_path, build_root,
cpuif_cls=AXI4LiteCpuifFlat, cpuif_cls=AXI4LiteCpuifFlat,
control_signal="AWVALID",
) )
sources = get_verilog_sources( sources = get_verilog_sources(
@@ -34,17 +42,18 @@ def test_axi4lite_smoke(tmp_path: Path) -> None:
) )
runner = get_runner("verilator") runner = get_runner("verilator")
build_dir = tmp_path / "sim_build" sim_build = build_root / "sim_build"
runner.build( runner.build(
sources=sources, sources=sources,
hdl_toplevel=module_path.stem, hdl_toplevel=module_path.stem,
build_dir=build_dir, build_dir=sim_build,
) )
runner.test( runner.test(
hdl_toplevel=module_path.stem, hdl_toplevel=module_path.stem,
test_module="tests.cocotb.axi4lite.smoke.test_register_access", test_module="tests.cocotb.axi4lite.smoke.test_register_access",
build_dir=build_dir, build_dir=sim_build,
log_file=str(tmp_path / "sim.log"), log_file=str(build_root / "simulation.log"),
extra_env={"RDL_TEST_CONFIG": json.dumps(config)},
) )

View File

@@ -0,0 +1,271 @@
"""AXI4-Lite smoke tests for variable depth design testing max_decode_depth parameter."""
import cocotb
from cocotb.triggers import Timer
class _AxilSlaveShim:
def __init__(self, dut):
prefix = "s_axil"
self.AWREADY = getattr(dut, f"{prefix}_AWREADY")
self.AWVALID = getattr(dut, f"{prefix}_AWVALID")
self.AWADDR = getattr(dut, f"{prefix}_AWADDR")
self.AWPROT = getattr(dut, f"{prefix}_AWPROT")
self.WREADY = getattr(dut, f"{prefix}_WREADY")
self.WVALID = getattr(dut, f"{prefix}_WVALID")
self.WDATA = getattr(dut, f"{prefix}_WDATA")
self.WSTRB = getattr(dut, f"{prefix}_WSTRB")
self.BREADY = getattr(dut, f"{prefix}_BREADY")
self.BVALID = getattr(dut, f"{prefix}_BVALID")
self.BRESP = getattr(dut, f"{prefix}_BRESP")
self.ARREADY = getattr(dut, f"{prefix}_ARREADY")
self.ARVALID = getattr(dut, f"{prefix}_ARVALID")
self.ARADDR = getattr(dut, f"{prefix}_ARADDR")
self.ARPROT = getattr(dut, f"{prefix}_ARPROT")
self.RREADY = getattr(dut, f"{prefix}_RREADY")
self.RVALID = getattr(dut, f"{prefix}_RVALID")
self.RDATA = getattr(dut, f"{prefix}_RDATA")
self.RRESP = getattr(dut, f"{prefix}_RRESP")
class _AxilMasterShim:
def __init__(self, dut, base: str):
self.AWREADY = getattr(dut, f"{base}_AWREADY")
self.AWVALID = getattr(dut, f"{base}_AWVALID")
self.AWADDR = getattr(dut, f"{base}_AWADDR")
self.AWPROT = getattr(dut, f"{base}_AWPROT")
self.WREADY = getattr(dut, f"{base}_WREADY")
self.WVALID = getattr(dut, f"{base}_WVALID")
self.WDATA = getattr(dut, f"{base}_WDATA")
self.WSTRB = getattr(dut, f"{base}_WSTRB")
self.BREADY = getattr(dut, f"{base}_BREADY")
self.BVALID = getattr(dut, f"{base}_BVALID")
self.BRESP = getattr(dut, f"{base}_BRESP")
self.ARREADY = getattr(dut, f"{base}_ARREADY")
self.ARVALID = getattr(dut, f"{base}_ARVALID")
self.ARADDR = getattr(dut, f"{base}_ARADDR")
self.ARPROT = getattr(dut, f"{base}_ARPROT")
self.RREADY = getattr(dut, f"{base}_RREADY")
self.RVALID = getattr(dut, f"{base}_RVALID")
self.RDATA = getattr(dut, f"{base}_RDATA")
self.RRESP = getattr(dut, f"{base}_RRESP")
def _axil_slave(dut):
return getattr(dut, "s_axil", None) or _AxilSlaveShim(dut)
def _axil_master(dut, base: str):
return getattr(dut, base, None) or _AxilMasterShim(dut, base)
@cocotb.test()
async def test_depth_1(dut):
"""Test max_decode_depth=1 - should have interface for inner1 only."""
s_axil = _axil_slave(dut)
# At depth 1, we should have m_axil_inner1 but not deeper interfaces
inner1 = _axil_master(dut, "m_axil_inner1")
# Default slave side inputs
s_axil.AWVALID.value = 0
s_axil.AWADDR.value = 0
s_axil.AWPROT.value = 0
s_axil.WVALID.value = 0
s_axil.WDATA.value = 0
s_axil.WSTRB.value = 0
s_axil.BREADY.value = 0
s_axil.ARVALID.value = 0
s_axil.ARADDR.value = 0
s_axil.ARPROT.value = 0
s_axil.RREADY.value = 0
inner1.AWREADY.value = 0
inner1.WREADY.value = 0
inner1.BVALID.value = 0
inner1.BRESP.value = 0
inner1.ARREADY.value = 0
inner1.RVALID.value = 0
inner1.RDATA.value = 0
inner1.RRESP.value = 0
await Timer(1, units="ns")
# Write to address 0x0 (should select inner1)
inner1.AWREADY.value = 1
inner1.WREADY.value = 1
s_axil.AWVALID.value = 1
s_axil.AWADDR.value = 0x0
s_axil.WVALID.value = 1
s_axil.WDATA.value = 0x12345678
s_axil.WSTRB.value = 0xF
await Timer(1, units="ns")
assert int(inner1.AWVALID.value) == 1, "inner1 write address valid must be set"
assert int(inner1.WVALID.value) == 1, "inner1 write data valid must be set"
@cocotb.test()
async def test_depth_2(dut):
"""Test max_decode_depth=2 - should have interfaces for reg1 and inner2."""
s_axil = _axil_slave(dut)
# At depth 2, we should have m_axil_reg1 and m_axil_inner2
reg1 = _axil_master(dut, "m_axil_reg1")
inner2 = _axil_master(dut, "m_axil_inner2")
# Default slave side inputs
s_axil.AWVALID.value = 0
s_axil.AWADDR.value = 0
s_axil.AWPROT.value = 0
s_axil.WVALID.value = 0
s_axil.WDATA.value = 0
s_axil.WSTRB.value = 0
s_axil.BREADY.value = 0
s_axil.ARVALID.value = 0
s_axil.ARADDR.value = 0
s_axil.ARPROT.value = 0
s_axil.RREADY.value = 0
for master in [reg1, inner2]:
master.AWREADY.value = 0
master.WREADY.value = 0
master.BVALID.value = 0
master.BRESP.value = 0
master.ARREADY.value = 0
master.RVALID.value = 0
master.RDATA.value = 0
master.RRESP.value = 0
await Timer(1, units="ns")
# Write to address 0x0 (should select reg1)
reg1.AWREADY.value = 1
reg1.WREADY.value = 1
s_axil.AWVALID.value = 1
s_axil.AWADDR.value = 0x0
s_axil.WVALID.value = 1
s_axil.WDATA.value = 0xABCDEF01
s_axil.WSTRB.value = 0xF
await Timer(1, units="ns")
assert int(reg1.AWVALID.value) == 1, "reg1 must be selected for address 0x0"
assert int(inner2.AWVALID.value) == 0, "inner2 should not be selected"
# Reset
s_axil.AWVALID.value = 0
s_axil.WVALID.value = 0
reg1.AWREADY.value = 0
reg1.WREADY.value = 0
await Timer(1, units="ns")
# Write to address 0x10 (should select inner2)
inner2.AWREADY.value = 1
inner2.WREADY.value = 1
s_axil.AWVALID.value = 1
s_axil.AWADDR.value = 0x10
s_axil.WVALID.value = 1
s_axil.WDATA.value = 0x23456789
s_axil.WSTRB.value = 0xF
await Timer(1, units="ns")
assert int(inner2.AWVALID.value) == 1, "inner2 must be selected for address 0x10"
assert int(reg1.AWVALID.value) == 0, "reg1 should not be selected"
@cocotb.test()
async def test_depth_0(dut):
"""Test max_decode_depth=0 - should have interfaces for all leaf registers."""
s_axil = _axil_slave(dut)
# At depth 0, we should have all leaf registers: reg1, reg2, reg2b
reg1 = _axil_master(dut, "m_axil_reg1")
reg2 = _axil_master(dut, "m_axil_reg2")
reg2b = _axil_master(dut, "m_axil_reg2b")
# Default slave side inputs
s_axil.AWVALID.value = 0
s_axil.AWADDR.value = 0
s_axil.AWPROT.value = 0
s_axil.WVALID.value = 0
s_axil.WDATA.value = 0
s_axil.WSTRB.value = 0
s_axil.BREADY.value = 0
s_axil.ARVALID.value = 0
s_axil.ARADDR.value = 0
s_axil.ARPROT.value = 0
s_axil.RREADY.value = 0
for master in [reg1, reg2, reg2b]:
master.AWREADY.value = 0
master.WREADY.value = 0
master.BVALID.value = 0
master.BRESP.value = 0
master.ARREADY.value = 0
master.RVALID.value = 0
master.RDATA.value = 0
master.RRESP.value = 0
await Timer(1, units="ns")
# Write to address 0x0 (should select reg1)
reg1.AWREADY.value = 1
reg1.WREADY.value = 1
s_axil.AWVALID.value = 1
s_axil.AWADDR.value = 0x0
s_axil.WVALID.value = 1
s_axil.WDATA.value = 0x11111111
s_axil.WSTRB.value = 0xF
await Timer(1, units="ns")
assert int(reg1.AWVALID.value) == 1, "reg1 must be selected for address 0x0"
assert int(reg2.AWVALID.value) == 0, "reg2 should not be selected"
assert int(reg2b.AWVALID.value) == 0, "reg2b should not be selected"
# Reset
s_axil.AWVALID.value = 0
s_axil.WVALID.value = 0
reg1.AWREADY.value = 0
reg1.WREADY.value = 0
await Timer(1, units="ns")
# Write to address 0x10 (should select reg2)
reg2.AWREADY.value = 1
reg2.WREADY.value = 1
s_axil.AWVALID.value = 1
s_axil.AWADDR.value = 0x10
s_axil.WVALID.value = 1
s_axil.WDATA.value = 0x22222222
s_axil.WSTRB.value = 0xF
await Timer(1, units="ns")
assert int(reg2.AWVALID.value) == 1, "reg2 must be selected for address 0x10"
assert int(reg1.AWVALID.value) == 0, "reg1 should not be selected"
assert int(reg2b.AWVALID.value) == 0, "reg2b should not be selected"
# Reset
s_axil.AWVALID.value = 0
s_axil.WVALID.value = 0
reg2.AWREADY.value = 0
reg2.WREADY.value = 0
await Timer(1, units="ns")
# Write to address 0x14 (should select reg2b)
reg2b.AWREADY.value = 1
reg2b.WREADY.value = 1
s_axil.AWVALID.value = 1
s_axil.AWADDR.value = 0x14
s_axil.WVALID.value = 1
s_axil.WDATA.value = 0x33333333
s_axil.WSTRB.value = 0xF
await Timer(1, units="ns")
assert int(reg2b.AWVALID.value) == 1, "reg2b must be selected for address 0x14"
assert int(reg1.AWVALID.value) == 0, "reg1 should not be selected"
assert int(reg2.AWVALID.value) == 0, "reg2 should not be selected"

View File

@@ -0,0 +1,128 @@
"""Pytest wrapper launching the AXI4-Lite cocotb smoke test for variable depth."""
from pathlib import Path
import pytest
from peakrdl_busdecoder.cpuif.axi4lite.axi4_lite_cpuif_flat import AXI4LiteCpuifFlat
try: # pragma: no cover - optional dependency shim
from cocotb.runner import get_runner
except ImportError: # pragma: no cover
from cocotb_tools.runner import get_runner
from tests.cocotb_lib.utils import compile_rdl_and_export, get_verilog_sources
@pytest.mark.simulation
@pytest.mark.verilator
def test_axi4lite_variable_depth_1(tmp_path: Path) -> None:
"""Test AXI4-Lite design with max_decode_depth=1."""
repo_root = Path(__file__).resolve().parents[4]
module_path, package_path = compile_rdl_and_export(
str(repo_root / "tests" / "cocotb_lib" / "variable_depth.rdl"),
"variable_depth",
tmp_path,
cpuif_cls=AXI4LiteCpuifFlat,
max_decode_depth=1,
)
sources = get_verilog_sources(
module_path,
package_path,
[repo_root / "hdl-src" / "axi4lite_intf.sv"],
)
runner = get_runner("verilator")
build_dir = tmp_path / "sim_build"
runner.build(
sources=sources,
hdl_toplevel=module_path.stem,
build_dir=build_dir,
)
runner.test(
hdl_toplevel=module_path.stem,
test_module="tests.cocotb.axi4lite.smoke.test_variable_depth",
build_dir=build_dir,
log_file=str(tmp_path / "sim_depth1.log"),
testcase="test_depth_1",
)
@pytest.mark.simulation
@pytest.mark.verilator
def test_axi4lite_variable_depth_2(tmp_path: Path) -> None:
"""Test AXI4-Lite design with max_decode_depth=2."""
repo_root = Path(__file__).resolve().parents[4]
module_path, package_path = compile_rdl_and_export(
str(repo_root / "tests" / "cocotb_lib" / "variable_depth.rdl"),
"variable_depth",
tmp_path,
cpuif_cls=AXI4LiteCpuifFlat,
max_decode_depth=2,
)
sources = get_verilog_sources(
module_path,
package_path,
[repo_root / "hdl-src" / "axi4lite_intf.sv"],
)
runner = get_runner("verilator")
build_dir = tmp_path / "sim_build"
runner.build(
sources=sources,
hdl_toplevel=module_path.stem,
build_dir=build_dir,
)
runner.test(
hdl_toplevel=module_path.stem,
test_module="tests.cocotb.axi4lite.smoke.test_variable_depth",
build_dir=build_dir,
log_file=str(tmp_path / "sim_depth2.log"),
testcase="test_depth_2",
)
@pytest.mark.simulation
@pytest.mark.verilator
def test_axi4lite_variable_depth_0(tmp_path: Path) -> None:
"""Test AXI4-Lite design with max_decode_depth=0 (all levels)."""
repo_root = Path(__file__).resolve().parents[4]
module_path, package_path = compile_rdl_and_export(
str(repo_root / "tests" / "cocotb_lib" / "variable_depth.rdl"),
"variable_depth",
tmp_path,
cpuif_cls=AXI4LiteCpuifFlat,
max_decode_depth=0,
)
sources = get_verilog_sources(
module_path,
package_path,
[repo_root / "hdl-src" / "axi4lite_intf.sv"],
)
runner = get_runner("verilator")
build_dir = tmp_path / "sim_build"
runner.build(
sources=sources,
hdl_toplevel=module_path.stem,
build_dir=build_dir,
)
runner.test(
hdl_toplevel=module_path.stem,
test_module="tests.cocotb.axi4lite.smoke.test_variable_depth",
build_dir=build_dir,
log_file=str(tmp_path / "sim_depth0.log"),
testcase="test_depth_0",
)

View File

@@ -1,3 +1,10 @@
from pathlib import Path """Manifest of SystemRDL sources used by the cocotb simulations."""
rdls = map(Path, ["simple.rdl", "multiple_reg.rdl"]) RDL_CASES: list[tuple[str, str]] = [
("simple.rdl", "simple_test"),
("multiple_reg.rdl", "multi_reg"),
("deep_hierarchy.rdl", "deep_hierarchy"),
("wide_status.rdl", "wide_status"),
("variable_layout.rdl", "variable_layout"),
("asymmetric_bus.rdl", "asymmetric_bus"),
]

View File

@@ -0,0 +1,69 @@
"""Utilities for resolving cocotb signal handles across simulators."""
from __future__ import annotations
from typing import Any, Iterable
class SignalHandle:
"""
Wrapper that resolves array elements even when the simulator does not expose
unpacked arrays through ``handle[idx]``.
"""
def __init__(self, dut, name: str) -> None:
self._dut = dut
self._name = name
self._base = getattr(dut, name, None)
self._cache: dict[tuple[int, ...], Any] = {}
def resolve(self, indices: tuple[int, ...]):
if not indices:
return self._base if self._base is not None else self._lookup(tuple())
if indices not in self._cache:
self._cache[indices] = self._direct_or_lookup(indices)
return self._cache[indices]
def _direct_or_lookup(self, indices: tuple[int, ...]):
if self._base is not None:
ref = self._base
try:
for idx in indices:
ref = ref[idx]
return ref
except (IndexError, TypeError, AttributeError):
pass
return self._lookup(indices)
def _lookup(self, indices: tuple[int, ...]):
suffix = "".join(f"[{idx}]" for idx in indices)
path = f"{self._name}{suffix}"
try:
return getattr(self._dut, path)
except AttributeError:
pass
errors: list[Exception] = []
for extended in (False, True):
try:
return self._dut._id(path, extended=extended)
except (AttributeError, ValueError) as exc:
errors.append(exc)
raise AttributeError(f"Unable to resolve handle '{path}' via dut._id") from errors[-1]
def resolve_handle(handle, indices: Iterable[int]):
"""Resolve either a regular cocotb handle or a ``SignalHandle`` wrapper."""
index_tuple = tuple(indices)
if isinstance(handle, SignalHandle):
return handle.resolve(index_tuple)
ref = handle
for idx in index_tuple:
ref = ref[idx]
return ref

View File

@@ -0,0 +1,105 @@
regfile port_rf {
reg {
field {
sw = rw;
hw = rw;
reset = 0x0;
} port_enable[0:0];
field {
sw = rw;
hw = rw;
reset = 0x0;
} port_speed[3:1];
field {
sw = rw;
hw = rw;
reset = 0x0;
} port_width[8:4];
} control @ 0x0;
reg {
field {
sw = r;
hw = w;
reset = 0x0;
} error_count[15:0];
field {
sw = r;
hw = w;
reset = 0x0;
} retry_count[31:16];
} counters @ 0x4;
reg {
field {
sw = rw;
hw = rw;
reset = 0x0;
} qos[7:0];
field {
sw = rw;
hw = rw;
reset = 0x0;
} virtual_channel[9:8];
} qos @ 0x8;
};
addrmap asymmetric_bus {
reg {
field {
sw = rw;
hw = rw;
reset = 0x0;
} control[3:0];
field {
sw = rw;
hw = rw;
reset = 0x0;
} id[15:4];
} control @ 0x0;
reg {
field {
sw = r;
hw = w;
reset = 0x0;
} status_flags[19:0];
} status @ 0x4;
reg {
regwidth = 64;
field {
sw = rw;
hw = rw;
reset = 0x00abcdef;
} timestamp_low[31:0];
field {
sw = rw;
hw = rw;
reset = 0x00123456;
} timestamp_high[55:32];
} timestamp @ 0x8;
reg {
regwidth = 128;
field {
sw = rw;
hw = rw;
reset = 0x0;
} extended_id[63:0];
field {
sw = rw;
hw = rw;
reset = 0x1;
} parity[64:64];
} extended @ 0x10;
port_rf port[6] @ 0x40 += 0x20;
};

View File

@@ -0,0 +1,115 @@
addrmap deep_hierarchy {
regfile context_rf {
reg {
field {
sw = rw;
hw = r;
reset = 0x1;
} enable[7:0];
field {
sw = r;
hw = w;
onread = rclr;
reset = 0x0;
} status[15:8];
field {
sw = rw;
hw = rw;
reset = 0x55;
} mode[23:16];
} command @ 0x0;
reg {
field {
sw = rw;
hw = rw;
reset = 0x1234;
} threshold[15:0];
} threshold @ 0x4;
reg {
field {
sw = rw;
hw = rw;
reset = 0x0;
} counter[31:0];
} counter @ 0x8;
};
regfile engine_rf {
context_rf context[3] @ 0x0;
reg {
field {
sw = rw;
hw = rw;
reset = 0x0;
} timeout[15:0];
field {
sw = rw;
hw = rw;
reset = 0x1;
} priority[19:16];
} config @ 0x30;
reg {
field {
sw = r;
hw = w;
onread = rclr;
reset = 0x0;
} error[31:0];
} error_log @ 0x34;
};
addrmap fabric_slice {
engine_rf engines[4] @ 0x0;
regfile monitor_rf {
reg {
field {
sw = r;
hw = w;
reset = 0x0;
} perf_count[31:0];
} perf @ 0x0;
reg {
field {
sw = r;
hw = w;
reset = 0x0;
} last_error[31:0];
} last_error @ 0x4;
};
monitor_rf monitor @ 0x400;
reg {
field {
sw = rw;
hw = rw;
reset = 0xdeadbeef;
} fabric_ctrl[31:0];
} fabric_ctrl @ 0x500;
};
fabric_slice slices[2] @ 0x0 += 0x800;
reg {
field {
sw = rw;
hw = rw;
reset = 0x1;
} global_enable[0:0];
field {
sw = rw;
hw = rw;
reset = 0x4;
} debug_level[3:1];
} global_control @ 0x1000;
};

View File

@@ -0,0 +1,156 @@
reg ctrl_reg_t {
desc = "Control register shared across channels.";
field {
sw = rw;
hw = rw;
reset = 0x1;
} enable[0:0];
field {
sw = rw;
hw = rw;
reset = 0x2;
} mode[3:1];
field {
sw = rw;
hw = rw;
reset = 0x0;
} prescale[11:4];
};
regfile channel_rf {
ctrl_reg_t control @ 0x0;
reg {
field {
sw = rw;
hw = rw;
reset = 0x0;
} gain[11:0];
field {
sw = rw;
hw = rw;
reset = 0x200;
} offset[23:12];
} calibrate @ 0x4;
reg {
field {
sw = rw;
hw = rw;
reset = 0x0;
} sample_count[15:0];
field {
sw = rw;
hw = rw;
reset = 0x0;
} error_count[31:16];
} counters @ 0x8;
};
regfile slice_rf {
reg {
field {
sw = rw;
hw = rw;
reset = 0x0;
} slope[15:0];
field {
sw = rw;
hw = rw;
reset = 0x0;
} intercept[31:16];
} calibration @ 0x0;
reg {
regwidth = 64;
field {
sw = r;
hw = w;
reset = 0x0;
} min_val[31:0];
field {
sw = r;
hw = w;
reset = 0x0;
} max_val[63:32];
} range @ 0x4;
};
regfile tile_rf {
channel_rf channel[3] @ 0x0;
reg {
field {
sw = rw;
hw = rw;
reset = 0x0;
} tile_mode[1:0];
field {
sw = rw;
hw = rw;
reset = 0x0;
} tile_enable[2:2];
} tile_ctrl @ 0x100;
slice_rf slice[2] @ 0x200;
};
regfile summary_rf {
reg {
field {
sw = r;
hw = w;
reset = 0x0;
} total_errors[31:0];
} errors @ 0x0;
reg {
field {
sw = r;
hw = w;
reset = 0x0;
} total_samples[31:0];
} samples @ 0x4;
reg {
field {
sw = rw;
hw = rw;
reset = 0x0;
} interrupt_enable[7:0];
} interrupt_enable @ 0x8;
};
addrmap variable_layout {
tile_rf tiles[2] @ 0x0;
reg {
field {
sw = rw;
hw = rw;
reset = 0x0;
} watchdog_enable[0:0];
field {
sw = rw;
hw = rw;
reset = 0x100;
} watchdog_timeout[16:1];
field {
sw = rw;
hw = rw;
reset = 0x0;
} watchdog_mode[18:17];
} watchdog @ 0x2000;
summary_rf summary @ 0x3000;
};

View File

@@ -0,0 +1,69 @@
reg status_reg_t {
regwidth = 64;
desc = "Status register capturing wide flags and sticky bits.";
field {
sw = r;
hw = w;
onread = rclr;
reset = 0x0;
} flags[62:0];
field {
sw = rw;
hw = r;
reset = 0x1;
} sticky_bit[63:63];
};
reg metrics_reg_t {
regwidth = 64;
desc = "Metrics register pairing counters with thresholds.";
field {
sw = r;
hw = w;
reset = 0x0;
} count[31:0];
field {
sw = rw;
hw = rw;
reset = 0x0;
} threshold[63:32];
};
addrmap wide_status {
status_reg_t status_blocks[16] @ 0x0;
metrics_reg_t metrics[4] @ 0x400;
reg {
regwidth = 128;
field {
sw = rw;
hw = rw;
reset = 0x0;
} configuration[127:0];
} configuration @ 0x800;
reg {
field {
sw = rw;
hw = rw;
reset = 0x0;
} version_major[7:0];
field {
sw = rw;
hw = rw;
reset = 0x1;
} version_minor[15:8];
field {
sw = rw;
hw = rw;
reset = 0x0100;
} build[31:16];
} version @ 0x900;
};

View File

@@ -1,9 +1,13 @@
"""Common utilities for cocotb testbenches.""" """Common utilities for cocotb testbenches."""
from __future__ import annotations
from collections import defaultdict
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from systemrdl import RDLCompiler from systemrdl import RDLCompiler
from systemrdl.node import AddressableNode, AddrmapNode, RegNode
from peakrdl_busdecoder.cpuif.base_cpuif import BaseCpuif from peakrdl_busdecoder.cpuif.base_cpuif import BaseCpuif
from peakrdl_busdecoder.exporter import BusDecoderExporter from peakrdl_busdecoder.exporter import BusDecoderExporter
@@ -65,3 +69,206 @@ def get_verilog_sources(module_path: Path, package_path: Path, intf_files: list[
# Add module file # Add module file
sources.append(str(module_path)) sources.append(str(module_path))
return sources return sources
def prepare_cpuif_case(
rdl_source: str,
top_name: str,
output_dir: Path,
cpuif_cls: type[BaseCpuif],
*,
control_signal: str,
max_samples_per_master: int = 3,
exporter_kwargs: dict[str, Any] | None = None,
) -> tuple[Path, Path, dict[str, Any]]:
"""
Compile SystemRDL, export the CPUIF, and build a configuration payload for cocotb tests.
Parameters
----------
rdl_source:
Path to the SystemRDL source file.
top_name:
Name of the top-level addrmap to elaborate.
output_dir:
Directory where generated HDL will be written.
cpuif_cls:
CPUIF implementation class to use during export.
control_signal:
Name of the control signal used to identify master ports
(``"PSEL"`` for APB, ``"AWVALID"`` for AXI4-Lite, etc.).
max_samples_per_master:
Limit for the number of register addresses sampled per master in the test matrix.
exporter_kwargs:
Optional keyword overrides passed through to :class:`BusDecoderExporter`.
Returns
-------
tuple
``(module_path, package_path, config_dict)``, where the configuration dictionary
is JSON-serializable and describes masters, indices, and sampled transactions.
"""
compiler = RDLCompiler()
compiler.compile_file(rdl_source)
root = compiler.elaborate(top_name)
top_node = root.top # type: ignore[assignment]
export_kwargs: dict[str, Any] = {"cpuif_cls": cpuif_cls}
if exporter_kwargs:
export_kwargs.update(exporter_kwargs)
exporter = BusDecoderExporter()
exporter.export(root, str(output_dir), **export_kwargs)
module_name = export_kwargs.get("module_name", top_name)
package_name = export_kwargs.get("package_name", f"{top_name}_pkg")
module_path = Path(output_dir) / f"{module_name}.sv"
package_path = Path(output_dir) / f"{package_name}.sv"
config = _build_case_config(
top_node,
exporter.cpuif,
control_signal,
max_samples_per_master=max_samples_per_master,
)
config["address_width"] = exporter.cpuif.addr_width
config["data_width"] = exporter.cpuif.data_width
config["byte_width"] = exporter.cpuif.data_width // 8
return module_path, package_path, config
def _build_case_config(
top_node: AddrmapNode,
cpuif: BaseCpuif,
control_signal: str,
*,
max_samples_per_master: int,
) -> dict[str, Any]:
master_entries: dict[str, dict[str, Any]] = {}
for child in cpuif.addressable_children:
signal = cpuif.signal(control_signal, child)
# Example: m_apb_tiles_PSEL[N_TILESS] -> m_apb_tiles
base = signal.split("[", 1)[0]
suffix = f"_{control_signal}"
if not base.endswith(suffix):
raise ValueError(f"Unable to derive port prefix from '{signal}'")
port_prefix = base[: -len(suffix)]
master_entries[child.inst_name] = {
"inst_name": child.inst_name,
"port_prefix": port_prefix,
"is_array": bool(child.is_array),
"dimensions": list(child.array_dimensions or []),
"indices": set(),
}
# Map each register to its top-level master and collect addresses
groups: dict[tuple[str, tuple[int, ...]], list[tuple[int, str]]] = defaultdict(list)
def visit(node: AddressableNode) -> None:
if isinstance(node, RegNode):
master = node # type: AddressableNode
while master.parent is not top_node:
parent = master.parent
if not isinstance(parent, AddressableNode):
raise RuntimeError("Encountered unexpected hierarchy while resolving master node")
master = parent
inst_name = master.inst_name
if inst_name not in master_entries:
# Handles cases where the register itself is the master (direct child of top)
signal = cpuif.signal(control_signal, master)
base = signal.split("[", 1)[0]
suffix = f"_{control_signal}"
if not base.endswith(suffix):
raise ValueError(f"Unable to derive port prefix from '{signal}'")
port_prefix = base[: -len(suffix)]
master_entries[inst_name] = {
"inst_name": inst_name,
"port_prefix": port_prefix,
"is_array": bool(master.is_array),
"dimensions": list(master.array_dimensions or []),
"indices": set(),
}
idx_tuple = tuple(master.current_idx or [])
master_entries[inst_name]["indices"].add(idx_tuple)
relative_addr = int(node.absolute_address) - int(top_node.absolute_address)
full_path = node.get_path()
label = full_path.split(".", 1)[1] if "." in full_path else full_path
groups[(inst_name, idx_tuple)].append((relative_addr, label))
for child in node.children(unroll=True):
if isinstance(child, AddressableNode):
visit(child)
visit(top_node)
masters_list = []
for entry in master_entries.values():
indices = entry["indices"] or {()}
entry["indices"] = [list(idx) for idx in sorted(indices)]
masters_list.append(
{
"inst_name": entry["inst_name"],
"port_prefix": entry["port_prefix"],
"is_array": entry["is_array"],
"dimensions": entry["dimensions"],
"indices": entry["indices"],
}
)
transactions = []
for (inst_name, idx_tuple), items in groups.items():
addresses = sorted({addr for addr, _ in items})
samples = _sample_addresses(addresses, max_samples_per_master)
for addr in samples:
label = next(lbl for candidate, lbl in items if candidate == addr)
transactions.append(
{
"address": addr,
"master": inst_name,
"index": list(idx_tuple),
"label": label,
}
)
transactions.sort(key=lambda item: (item["master"], item["index"], item["address"]))
masters_list.sort(key=lambda item: item["inst_name"])
return {
"masters": masters_list,
"transactions": transactions,
}
def _sample_addresses(addresses: list[int], max_samples: int) -> list[int]:
if len(addresses) <= max_samples:
return addresses
samples: list[int] = []
samples.append(addresses[0])
if len(addresses) > 1:
samples.append(addresses[-1])
if len(addresses) > 2:
mid = addresses[len(addresses) // 2]
if mid not in samples:
samples.append(mid)
idx = 1
while len(samples) < max_samples:
pos = (len(addresses) * idx) // max_samples
candidate = addresses[min(pos, len(addresses) - 1)]
if candidate not in samples:
samples.append(candidate)
idx += 1
samples.sort()
return samples

View File

@@ -0,0 +1,31 @@
// Variable depth register hierarchy for testing max_decode_depth parameter
addrmap level2 {
reg {
field {
sw=rw;
hw=r;
} data2[31:0];
} reg2 @ 0x0;
reg {
field {
sw=rw;
hw=r;
} data2b[31:0];
} reg2b @ 0x4;
};
addrmap level1 {
reg {
field {
sw=rw;
hw=r;
} data1[31:0];
} reg1 @ 0x0;
level2 inner2 @ 0x10;
};
addrmap variable_depth {
level1 inner1 @ 0x0;
};

View File

@@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
collect_ignore_glob = ["cocotb/*/smoke/test_register_access.py"] collect_ignore_glob = ["cocotb/*/smoke/test_register_access.py", "cocotb/*/smoke/test_variable_depth.py"]
import os import os
from collections.abc import Callable from collections.abc import Callable

View File

@@ -89,7 +89,8 @@ class TestBusDecoderExporter:
exporter = BusDecoderExporter() exporter = BusDecoderExporter()
output_dir = str(tmp_path) output_dir = str(tmp_path)
exporter.export(top, output_dir, cpuif_cls=APB4Cpuif) # Use depth=0 to descend all the way to registers
exporter.export(top, output_dir, cpuif_cls=APB4Cpuif, max_decode_depth=0)
# Check that output files are created # Check that output files are created
module_file = tmp_path / "outer_block.sv" module_file = tmp_path / "outer_block.sv"

View File

@@ -0,0 +1,131 @@
"""Test Questa simulator compatibility for instance arrays."""
from collections.abc import Callable
from pathlib import Path
from tempfile import TemporaryDirectory
from systemrdl.node import AddrmapNode
from peakrdl_busdecoder import BusDecoderExporter
from peakrdl_busdecoder.cpuif.apb4 import APB4Cpuif
def test_instance_array_questa_compatibility(compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test that instance arrays generate Questa-compatible code.
This test ensures that:
- Struct members for arrays use unpacked array syntax (name[dim])
- NOT packed bit-vector syntax ([dim-1:0]name)
- Struct is unpacked (not packed)
- Array indexing with loop variables works correctly
This fixes the error: "Nonconstant index into instance array"
"""
rdl_source = """
addrmap test_map {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} my_reg[4] @ 0x0 += 0x10;
};
"""
top = compile_rdl(rdl_source, top="test_map")
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif)
# Read the generated module
module_file = Path(tmpdir) / "test_map.sv"
content = module_file.read_text()
# Should use unpacked struct
assert "typedef struct {" in content
assert "typedef struct packed" not in content
# Should use unpacked array syntax for array members
assert "logic my_reg[4];" in content
# Should NOT use packed bit-vector syntax
assert "[3:0]my_reg" not in content
# Should have proper array indexing in decode logic
assert "cpuif_wr_sel.my_reg[i0] = 1'b1;" in content
assert "cpuif_rd_sel.my_reg[i0] = 1'b1;" in content
# Should have proper array indexing in fanout/fanin logic
assert "cpuif_wr_sel.my_reg[gi0]" in content or "cpuif_rd_sel.my_reg[gi0]" in content
assert "cpuif_wr_sel.my_reg[i0]" in content or "cpuif_rd_sel.my_reg[i0]" in content
def test_multidimensional_array_questa_compatibility(compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test that multidimensional instance arrays generate Questa-compatible code."""
rdl_source = """
addrmap test_map {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} my_reg[2][3] @ 0x0 += 0x10;
};
"""
top = compile_rdl(rdl_source, top="test_map")
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif)
# Read the generated module
module_file = Path(tmpdir) / "test_map.sv"
content = module_file.read_text()
# Should use unpacked struct with multidimensional array
assert "typedef struct {" in content
# Should use unpacked array syntax for multidimensional arrays
assert "logic my_reg[2][3];" in content
# Should NOT use packed bit-vector syntax
assert "[1:0][2:0]my_reg" not in content
assert "[5:0]my_reg" not in content
def test_nested_instance_array_questa_compatibility(compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test that nested instance arrays generate Questa-compatible code."""
rdl_source = """
addrmap inner_map {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} inner_reg[2] @ 0x0 += 0x10;
};
addrmap outer_map {
inner_map inner[3] @ 0x0 += 0x100;
};
"""
top = compile_rdl(rdl_source, top="outer_map")
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif)
# Read the generated module
module_file = Path(tmpdir) / "outer_map.sv"
content = module_file.read_text()
# Should use unpacked struct
assert "typedef struct {" in content
# Inner should be an array
# The exact syntax may vary, but it should be unpacked
# Look for the pattern of unpacked arrays, not packed bit-vectors
assert "inner[3]" in content or "logic inner" in content
# Should NOT use packed bit-vector syntax like [2:0]inner
assert "[2:0]inner" not in content

View File

@@ -96,3 +96,10 @@ class TestStructGenerator:
assert "[" in result and "]" in result assert "[" in result and "]" in result
# Should reference the register # Should reference the register
assert "my_regs" in result assert "my_regs" in result
# Should use unpacked array syntax (name[size]), not packed bit-vector ([size:0]name)
assert "my_regs[4]" in result
# Should NOT use packed bit-vector syntax
assert "[3:0]my_regs" not in result
# Should be unpacked struct, not packed
assert "typedef struct {" in result
assert "typedef struct packed" not in result

View File

@@ -41,9 +41,9 @@ def test_external_nested_components_generate_correct_decoder(external_nested_rdl
assert "cpuif_rd_sel.multicast.response" not in content assert "cpuif_rd_sel.multicast.response" not in content
# Verify struct is flat (no nested structs for external children) # Verify struct is flat (no nested structs for external children)
assert "typedef struct packed" in content assert "typedef struct" in content
assert "logic multicast;" in content assert "logic multicast;" in content
assert "logic [15:0]port;" in content assert "logic port[16];" in content
def test_external_nested_components_generate_correct_interfaces(external_nested_rdl: AddrmapNode) -> None: def test_external_nested_components_generate_correct_interfaces(external_nested_rdl: AddrmapNode) -> None:
@@ -101,7 +101,8 @@ def test_non_external_nested_components_are_descended(compile_rdl: Callable[...,
with TemporaryDirectory() as tmpdir: with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter() exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif) # Use depth=0 to descend all the way down to registers
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif, max_decode_depth=0)
# Read the generated module # Read the generated module
module_file = Path(tmpdir) / "outer_block.sv" module_file = Path(tmpdir) / "outer_block.sv"

View File

@@ -0,0 +1,256 @@
"""Test max_decode_depth parameter behavior."""
from collections.abc import Callable
from pathlib import Path
from tempfile import TemporaryDirectory
from systemrdl.node import AddrmapNode
from peakrdl_busdecoder import BusDecoderExporter
from peakrdl_busdecoder.cpuif.apb4 import APB4Cpuif
def test_depth_1_generates_top_level_interface_only(compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test that depth=1 generates interface only for top-level children."""
rdl_source = """
addrmap level1 {
reg {
field { sw=rw; hw=r; } data1[31:0];
} reg1 @ 0x0;
};
addrmap level0 {
level1 inner1 @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="level0")
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif, max_decode_depth=1)
module_file = Path(tmpdir) / "level0.sv"
content = module_file.read_text()
# Should have interface for inner1 only
assert "m_apb_inner1" in content
# Should NOT have interface for reg1
assert "m_apb_reg1" not in content
# Struct should have inner1 but not nested structure
assert "logic inner1;" in content
def test_depth_2_generates_second_level_interfaces(compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test that depth=2 generates interfaces for second-level children."""
rdl_source = """
addrmap level2 {
reg {
field { sw=rw; hw=r; } data2[31:0];
} reg2 @ 0x0;
};
addrmap level1 {
reg {
field { sw=rw; hw=r; } data1[31:0];
} reg1 @ 0x0;
level2 inner2 @ 0x10;
};
addrmap level0 {
level1 inner1 @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="level0")
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif, max_decode_depth=2)
module_file = Path(tmpdir) / "level0.sv"
content = module_file.read_text()
# Should have interfaces for reg1 and inner2
assert "m_apb_reg1" in content
assert "m_apb_inner2" in content
# Should NOT have interface for inner1 or reg2
assert "m_apb_inner1" not in content
assert "m_apb_reg2" not in content
# Struct should be hierarchical with inner1.reg1 and inner1.inner2
assert "cpuif_sel_inner1_t" in content
assert "logic reg1;" in content
assert "logic inner2;" in content
def test_depth_0_decodes_all_levels(compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test that depth=0 decodes all the way down to registers."""
rdl_source = """
addrmap level2 {
reg {
field { sw=rw; hw=r; } data2[31:0];
} reg2 @ 0x0;
reg {
field { sw=rw; hw=r; } data2b[31:0];
} reg2b @ 0x4;
};
addrmap level1 {
reg {
field { sw=rw; hw=r; } data1[31:0];
} reg1 @ 0x0;
level2 inner2 @ 0x10;
};
addrmap level0 {
level1 inner1 @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="level0")
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif, max_decode_depth=0)
module_file = Path(tmpdir) / "level0.sv"
content = module_file.read_text()
# Should have interfaces for all leaf registers
assert "m_apb_reg1" in content
assert "m_apb_reg2" in content
assert "m_apb_reg2b" in content
# Should NOT have interfaces for addrmaps
assert "m_apb_inner1" not in content
assert "m_apb_inner2" not in content
# Struct should be fully hierarchical
assert "cpuif_sel_inner1_t" in content
assert "cpuif_sel_inner2_t" in content
def test_depth_affects_decode_logic(compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test that decode logic changes based on max_decode_depth."""
rdl_source = """
addrmap level1 {
reg {
field { sw=rw; hw=r; } data1[31:0];
} reg1 @ 0x0;
};
addrmap level0 {
level1 inner1 @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="level0")
# Test depth=1: should set cpuif_wr_sel.inner1
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif, max_decode_depth=1)
module_file = Path(tmpdir) / "level0.sv"
content = module_file.read_text()
assert "cpuif_wr_sel.inner1 = 1'b1;" in content
assert "cpuif_wr_sel.inner1.reg1" not in content
# Test depth=2: should set cpuif_wr_sel.inner1.reg1
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif, max_decode_depth=2)
module_file = Path(tmpdir) / "level0.sv"
content = module_file.read_text()
assert "cpuif_wr_sel.inner1.reg1 = 1'b1;" in content
assert "cpuif_wr_sel.inner1 = 1'b1;" not in content
def test_depth_affects_fanout_fanin(compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test that fanout/fanin logic changes based on max_decode_depth."""
rdl_source = """
addrmap level1 {
reg {
field { sw=rw; hw=r; } data1[31:0];
} reg1 @ 0x0;
};
addrmap level0 {
level1 inner1 @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="level0")
# Test depth=1: should have fanout for inner1
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif, max_decode_depth=1)
module_file = Path(tmpdir) / "level0.sv"
content = module_file.read_text()
assert "m_apb_inner1.PSEL" in content
assert "m_apb_reg1.PSEL" not in content
# Test depth=2: should have fanout for reg1
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif, max_decode_depth=2)
module_file = Path(tmpdir) / "level0.sv"
content = module_file.read_text()
assert "m_apb_reg1.PSEL" in content
assert "m_apb_inner1.PSEL" not in content
def test_depth_3_with_deep_hierarchy(compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test depth=3 with a 4-level deep hierarchy."""
rdl_source = """
addrmap level3 {
reg {
field { sw=rw; hw=r; } data3[31:0];
} reg3 @ 0x0;
};
addrmap level2 {
reg {
field { sw=rw; hw=r; } data2[31:0];
} reg2 @ 0x0;
level3 inner3 @ 0x10;
};
addrmap level1 {
reg {
field { sw=rw; hw=r; } data1[31:0];
} reg1 @ 0x0;
level2 inner2 @ 0x10;
};
addrmap level0 {
level1 inner1 @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="level0")
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif, max_decode_depth=3)
module_file = Path(tmpdir) / "level0.sv"
content = module_file.read_text()
# Should have interfaces at depth 3: reg2, inner3
# (reg1 is at depth 2, not 3)
assert "m_apb_reg2" in content
assert "m_apb_inner3" in content
# Should NOT have interfaces at other depths
assert "m_apb_inner1" not in content
assert "m_apb_inner2" not in content
assert "m_apb_reg1" not in content
assert "m_apb_reg3" not in content

View File

@@ -9,7 +9,6 @@ invocation constructed from stdin tokens.
from __future__ import annotations from __future__ import annotations
import os
import subprocess import subprocess
import sys import sys

2
uv.lock generated
View File

@@ -608,7 +608,7 @@ wheels = [
[[package]] [[package]]
name = "peakrdl-busdecoder" name = "peakrdl-busdecoder"
version = "0.2.0" version = "0.5.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "jinja2" }, { name = "jinja2" },