27 Commits

Author SHA1 Message Date
7d88b26a65 use fanin_wr and fanin_rd 2026-02-04 07:35:28 -08:00
ceed4586cc fix type check error 2026-02-03 22:33:04 -08:00
Byron Lathi
3f39cac8f4 Gate assertions behind "PEAKRDL_ASSERTIONS define" 2026-02-03 21:58:55 -08:00
Byron Lathi
fbe0f1898b Add taxi apb interface 2026-02-03 21:57:44 -08:00
Arnav Sacheti
36ec8b9715 update lock 2026-02-03 08:47:59 +00:00
Arnav Sacheti
244bd8d773 revamp docs 2026-02-03 08:47:18 +00:00
Arnav Sacheti
bad845d15e Fix/better spec enforcing (#41)
* revamp

* consolidate

* version bump
2026-02-03 00:03:04 -08:00
Copilot
1e09da6dbf Fix APB PREADY not asserted on invalid address decode errors (#40)
Fix APB PREADY signal to assert during error conditions

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>
2026-02-02 23:34:09 -08:00
Arnav Sacheti
2abf7cf7f2 fix cocotb units deprecation 2026-02-03 05:41:48 +00:00
Arnav Sacheti
caad523b06 fixing mismatched title (#38) 2026-01-26 21:26:49 -08:00
Arnav Sacheti
4a327a0290 Migrated from Pyrefly to ty (#33) 2026-01-05 23:03:24 -08:00
Arnav Sacheti
8cc4b838a3 remove currrent_date from generated files 2025-12-31 07:55:44 +00:00
Arnav Sacheti
51a71daa79 fix broken actions 2025-12-24 21:40:53 +00:00
Arnav Sacheti
549ebe6085 remove apb4 wr_sel assrt 2025-12-24 21:07:02 +00:00
Arnav Sacheti
ad364ab8d6 add utility tests 2025-12-24 20:12:05 +00:00
Arnav Sacheti
bc8b2c8807 Update Version 2025-12-15 05:56:17 +00:00
Copilot
e0e480ef9e Remove power-of-2 alignment requirement for external components (#30)
* Initial plan

* Remove alignment check on external components and add tests

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

* Improve test comment clarity

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-12-14 21:55:09 -08:00
Arnav Sacheti
82fd06b467 update coverage status badge 2025-12-05 06:29:04 +00:00
Arnav Sacheti
826765cde8 Add Coverall Support (#28)
Add Coveralls tracking for python 3.12 tests
2025-12-04 22:27:19 -08:00
Arnav Sacheti
f9f9d36db7 Update Status Badges 2025-12-05 05:45:39 +00:00
Arnav Sacheti
c7fb6a92e5 version bump 2025-12-05 05:32:59 +00:00
Arnav Sacheti
c63b2cbab2 Dev/downsize apb paddr (#27)
* Downsize paddr bits

* Updated Test suite to use offset aligned address

* fix for apb3 and axi4lite

* modified structure to pass hierarchy information

---------

Co-authored-by: Byron Lathi <bslathi19@gmail.com>
2025-12-04 21:31:44 -08:00
Arnav Sacheti
9f41487430 version bump 2025-11-26 17:51:46 +00:00
Arnav Sacheti
5152adf00c relax dependencies constraints from pinning to any path to any minor (#25) 2025-11-26 09:51:04 -08:00
Arnav Sacheti
88827c65b5 add colorized build/sim log propgate on error to all runners (#26)
* add colorized build/sim log propgate on error to all runners

* add doctoring

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-26 08:01:41 -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
90 changed files with 3524 additions and 3084 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

@@ -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,30 @@ 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 Coveralls
if: matrix.python-version == '3.12'
env:
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
uv pip install coveralls
uv run coveralls --service=github
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v4

View File

@@ -22,5 +22,5 @@ jobs:
- name: Install package - name: Install package
run: uv sync --extra cli run: uv sync --extra cli
- name: Run pyrefly type check - name: Run ty type check
run: uvx pyrefly check src/ run: uvx ty check src/

View File

@@ -1,6 +1,7 @@
[![Documentation Status](https://readthedocs.org/projects/peakrdl-busdecoder/badge/?version=latest)](http://peakrdl-busdecoder.readthedocs.io) [![Build](https://github.com/arnavsacheti/PeakRDL-BusDecoder/actions/workflows/build.yml/badge.svg)](https://github.com/arnavsacheti/PeakRDL-BusDecoder/actions/workflows/build.yml)
[![build](https://github.com/arnavsacheti/PeakRDL-BusDecoder/workflows/build/badge.svg)](https://github.com/arnavsacheti/PeakRDL-BusDecoder/actions?query=workflow%3Abuild+branch%3Amain) [![Test](https://github.com/arnavsacheti/PeakRDL-BusDecoder/actions/workflows/test.yml/badge.svg)](https://github.com/arnavsacheti/PeakRDL-BusDecoder/actions/workflows/test.yml)
[![Coverage Status](https://coveralls.io/repos/github/arnavsacheti/PeakRDL-BusDecoder/badge.svg?branch=main)](https://coveralls.io/github/arnavsacheti/PeakRDL-BusDecoder?branch=main) [![Documentation](https://github.com/arnavsacheti/PeakRDL-BusDecoder/actions/workflows/docs.yml/badge.svg)](https://github.com/arnavsacheti/PeakRDL-BusDecoder/actions/workflows/docs.yml)
[![Coverage Status](https://coveralls.io/repos/github/arnavsacheti/PeakRDL-BusDecoder/badge.svg?branch=tests/coveralls)](https://coveralls.io/github/arnavsacheti/PeakRDL-BusDecoder?branch=tests/coveralls)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/peakrdl-busdecoder.svg)](https://pypi.org/project/peakrdl-busdecoder) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/peakrdl-busdecoder.svg)](https://pypi.org/project/peakrdl-busdecoder)
# PeakRDL-BusDecoder # PeakRDL-BusDecoder

View File

@@ -15,10 +15,11 @@ implementation from SystemRDL source.
.. code-block:: python .. code-block:: python
:emphasize-lines: 2-4, 29-33 :emphasize-lines: 2-4, 29-33
import sys
from systemrdl import RDLCompiler, RDLCompileError from systemrdl import RDLCompiler, RDLCompileError
from peakrdl_busdecoder import BusDecoderExporter from peakrdl_busdecoder import BusDecoderExporter
from peakrdl_busdecoder.cpuif.axi4lite import AXI4Lite_Cpuif from peakrdl_busdecoder.cpuif.axi4lite import AXI4LiteCpuif
from peakrdl_busdecoder.udps import ALL_UDPS
input_files = [ input_files = [
"PATH/TO/my_register_block.rdl" "PATH/TO/my_register_block.rdl"
@@ -27,10 +28,6 @@ implementation from SystemRDL source.
# Create an instance of the compiler # Create an instance of the compiler
rdlc = RDLCompiler() rdlc = RDLCompiler()
# Register all UDPs that 'busdecoder' requires
for udp in ALL_UDPS:
rdlc.register_udp(udp)
try: try:
# Compile your RDL files # Compile your RDL files
for input_file in input_files: for input_file in input_files:
@@ -46,5 +43,5 @@ implementation from SystemRDL source.
exporter = BusDecoderExporter() exporter = BusDecoderExporter()
exporter.export( exporter.export(
root, "path/to/output_dir", root, "path/to/output_dir",
cpuif_cls=AXI4Lite_Cpuif cpuif_cls=AXI4LiteCpuif
) )

View File

@@ -1,64 +1,54 @@
Register Block Architecture Bus Decoder Architecture
=========================== ========================
The generated bus decoder RTL is organized into several sections. The generated RTL is a pure bus-routing layer. It accepts a single CPU interface
Each section is automatically generated based on the source register model and on the slave side and fans transactions out to a set of child interfaces on the
is rendered into the output SystemVerilog RTL module. The bus decoder serves as master side. No register storage or field logic is generated.
an address decode and routing layer that splits a single CPU interface into
multiple sub-address spaces corresponding to child addrmaps in your SystemRDL design.
.. figure:: diagrams/arch.png Although you do not need to know the inner workings to use the exporter, the
sections below explain the structure of the generated module and how it maps to
Although it is not completely necessary to know the inner workings of the SystemRDL hierarchy.
generated RTL, it can be helpful to understand the implications of various
exporter configuration options.
CPU Interface CPU Interface Adapter
------------- ---------------------
The CPU interface logic layer provides an abstraction between the Each supported CPU interface protocol (APB3, APB4, AXI4-Lite) provides a small
application-specific bus protocol and the internal register file logic. adapter that translates the external bus protocol into internal request/response
This logic layer normalizes external CPU read & write transactions into a common signals. These internal signals are then used by the address decoder and fanout
:ref:`cpuif_protocol` that is used to interact with the register file. When the logic.
design contains multiple child addrmaps, the CPU interface handles fanout of
transactions to the appropriate sub-address space. If you write a custom CPU interface, it must implement the internal signals
described in :ref:`cpuif_protocol`.
Address Decode Address Decode
-------------- --------------
A common address decode operation is generated which computes individual access The address decoder computes per-child select signals based on address ranges.
strobes for each software-accessible register or child addrmap in the design. The decode boundary is controlled by ``max_decode_depth``:
This operation is performed completely combinationally. The decoder determines
which sub-address space should handle each transaction based on the address range. * ``0``: Decode all the way down to leaf registers
* ``1`` (default): Decode only top-level children
* ``N``: Decode down to depth ``N`` from the top-level
This allows you to choose whether the bus decoder routes to large blocks (e.g.,
child addrmaps) or to smaller sub-blocks.
Field Logic Fanout to Child Interfaces
----------- --------------------------
This layer of the register block implements the storage elements and state-change For each decoded child, the bus decoder drives a master-side CPU interface.
logic for every field in the design. Field state is updated based on address All address, data, and control signals are forwarded to the selected child.
decode strobes from software read/write actions, as well as events from the
hardware interface input struct. Arrayed children can be kept as arrays or unrolled into discrete interfaces using
This section also assigns any hardware interface outputs. ``--unroll``. This only affects port structure and naming; decode semantics are
unchanged.
Readback Fanin and Error Handling
-------- ------------------------
The readback layer aggregates and reduces all readable registers into a single Read and write responses are muxed back from the selected child to the slave
read response. During a read operation, the same address decode strobes are used interface. If no child is selected for a transaction, the decoder generates an
to select the active register that is being accessed. error response on the slave interface.
This allows for a simple OR-reduction operation to be used to compute the read
data response.
For designs with a large number of software-readable registers, an optional The exact error signaling depends on the chosen CPU interface protocol (e.g.,
fanin re-timing stage can be enabled. This stage is automatically inserted at a ``PSLVERR`` for APB, ``RRESP/BRESP`` for AXI4-Lite).
balanced point in the read-data reduction so that fanin and logic-levels are
optimally reduced.
.. figure:: diagrams/readback.png
:width: 65%
:align: center
A second optional read response retiming register can be enabled in-line with the
path back to the CPU interface layer. This can be useful if the CPU interface protocol
used has a fully combinational response path, and the design's complexity requires
this path to be retimed further.

View File

@@ -32,9 +32,7 @@ author = "Arnav Sacheti"
extensions = [ extensions = [
"sphinx.ext.autodoc", "sphinx.ext.autodoc",
"sphinx.ext.napoleon", "sphinx.ext.napoleon",
"sphinxcontrib.wavedrom",
] ]
render_using_wavedrompy = True
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"] templates_path = ["_templates"]
@@ -42,7 +40,7 @@ templates_path = ["_templates"]
# List of patterns, relative to source directory, that match files and # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path. # This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "dev_notes", "dev_notes/**"]
# -- Options for HTML output ------------------------------------------------- # -- Options for HTML output -------------------------------------------------

View File

@@ -1,13 +1,13 @@
.. _peakrdl_cfg: .. _peakrdl_cfg:
Configuring PeakRDL-BusDecoder Configuring PeakRDL-BusDecoder
============================ ==============================
If using the `PeakRDL command line tool <https://peakrdl.readthedocs.io/>`_, If using the `PeakRDL command line tool <https://peakrdl.readthedocs.io/>`_,
some aspects of the ``busdecoder`` command have additional configuration options some aspects of the ``busdecoder`` command can be configured via the PeakRDL
available via the PeakRDL TOML file. TOML file.
All busdecoder-specific options are defined under the ``[busdecoder]`` TOML heading. All busdecoder-specific options are defined under the ``[busdecoder]`` heading.
.. data:: cpuifs .. data:: cpuifs
@@ -24,22 +24,15 @@ All busdecoder-specific options are defined under the ``[busdecoder]`` TOML head
cpuifs.my-cpuif-name = "my_cpuif_module:MyCPUInterfaceClass" cpuifs.my-cpuif-name = "my_cpuif_module:MyCPUInterfaceClass"
.. data:: default_reset Command-Line Options
--------------------
Choose the default style of reset signal if not explicitly The following options are available on the ``peakrdl busdecoder`` command:
specified by the SystemRDL design. If unspecified, the default reset
is active-high and synchronous.
Choice of: * ``--cpuif``: Select the CPU interface (``apb3``, ``apb3-flat``, ``apb4``,
``apb4-flat``, ``axi4-lite``, ``axi4-lite-flat``)
* ``rst`` (default) * ``--module-name``: Override the generated module name
* ``rst_n`` * ``--package-name``: Override the generated package name
* ``arst`` * ``--addr-width``: Override the slave address width
* ``arst_n`` * ``--unroll``: Unroll arrayed children into discrete interfaces
* ``--max-decode-depth``: Control how far the decoder descends into hierarchy
For example:
.. code-block:: toml
[busdecoder]
default_reset = "arst"

View File

@@ -20,7 +20,7 @@ Both APB3 and APB4 standards are supported.
APB3 APB3
---- ----
Implements the register block using an Implements the bus decoder using an
`AMBA 3 APB <https://developer.arm.com/documentation/ihi0024/b/Introduction/About-the-AMBA-3-APB>`_ `AMBA 3 APB <https://developer.arm.com/documentation/ihi0024/b/Introduction/About-the-AMBA-3-APB>`_
CPU interface. CPU interface.
@@ -29,19 +29,19 @@ The APB3 CPU interface comes in two i/o port flavors:
SystemVerilog Interface SystemVerilog Interface
* Command line: ``--cpuif apb3`` * Command line: ``--cpuif apb3``
* Interface Definition: :download:`apb3_intf.sv <../../hdl-src/apb3_intf.sv>` * Interface Definition: :download:`apb3_intf.sv <../../hdl-src/apb3_intf.sv>`
* Class: :class:`peakrdl_busdecoder.cpuif.apb3.APB3_Cpuif` * Class: :class:`peakrdl_busdecoder.cpuif.apb3.APB3Cpuif`
Flattened inputs/outputs Flattened inputs/outputs
Flattens the interface into discrete input and output ports. Flattens the interface into discrete input and output ports.
* Command line: ``--cpuif apb3-flat`` * Command line: ``--cpuif apb3-flat``
* Class: :class:`peakrdl_busdecoder.cpuif.apb3.APB3_Cpuif_flattened` * Class: :class:`peakrdl_busdecoder.cpuif.apb3.APB3CpuifFlat`
APB4 APB4
---- ----
Implements the register block using an Implements the bus decoder using an
`AMBA 4 APB <https://developer.arm.com/documentation/ihi0024/d/?lang=en>`_ `AMBA 4 APB <https://developer.arm.com/documentation/ihi0024/d/?lang=en>`_
CPU interface. CPU interface.
@@ -50,10 +50,10 @@ The APB4 CPU interface comes in two i/o port flavors:
SystemVerilog Interface SystemVerilog Interface
* Command line: ``--cpuif apb4`` * Command line: ``--cpuif apb4``
* Interface Definition: :download:`apb4_intf.sv <../../hdl-src/apb4_intf.sv>` * Interface Definition: :download:`apb4_intf.sv <../../hdl-src/apb4_intf.sv>`
* Class: :class:`peakrdl_busdecoder.cpuif.apb4.APB4_Cpuif` * Class: :class:`peakrdl_busdecoder.cpuif.apb4.APB4Cpuif`
Flattened inputs/outputs Flattened inputs/outputs
Flattens the interface into discrete input and output ports. Flattens the interface into discrete input and output ports.
* Command line: ``--cpuif apb4-flat`` * Command line: ``--cpuif apb4-flat``
* Class: :class:`peakrdl_busdecoder.cpuif.apb4.APB4_Cpuif_flattened` * Class: :class:`peakrdl_busdecoder.cpuif.apb4.APB4CpuifFlat`

View File

@@ -1,33 +0,0 @@
Intel Avalon
============
Implements the register block using an
`Intel Avalon MM <https://www.intel.com/content/www/us/en/docs/programmable/683091/22-3/memory-mapped-interfaces.html>`_
CPU interface.
The Avalon interface comes in two i/o port flavors:
SystemVerilog Interface
* Command line: ``--cpuif avalon-mm``
* Interface Definition: :download:`avalon_mm_intf.sv <../../hdl-src/avalon_mm_intf.sv>`
* Class: :class:`peakrdl_busdecoder.cpuif.avalon.Avalon_Cpuif`
Flattened inputs/outputs
Flattens the interface into discrete input and output ports.
* Command line: ``--cpuif avalon-mm-flat``
* Class: :class:`peakrdl_busdecoder.cpuif.avalon.Avalon_Cpuif_flattened`
Implementation Details
----------------------
This implementation of the Avalon protocol has the following features:
* Interface uses word addressing.
* Supports `pipelined transfers <https://www.intel.com/content/www/us/en/docs/programmable/683091/22-3/pipelined-transfers.html>`_
* Responses may have variable latency
In most cases, latency is fixed and is determined by how many retiming
stages are enabled in your design.
However if your design contains external components, access latency is
not guaranteed to be uniform.

View File

@@ -3,7 +3,7 @@
AMBA AXI4-Lite AMBA AXI4-Lite
============== ==============
Implements the register block using an Implements the bus decoder using an
`AMBA AXI4-Lite <https://developer.arm.com/documentation/ihi0022/e/AMBA-AXI4-Lite-Interface-Specification>`_ `AMBA AXI4-Lite <https://developer.arm.com/documentation/ihi0022/e/AMBA-AXI4-Lite-Interface-Specification>`_
CPU interface. CPU interface.
@@ -12,21 +12,22 @@ The AXI4-Lite CPU interface comes in two i/o port flavors:
SystemVerilog Interface SystemVerilog Interface
* Command line: ``--cpuif axi4-lite`` * Command line: ``--cpuif axi4-lite``
* Interface Definition: :download:`axi4lite_intf.sv <../../hdl-src/axi4lite_intf.sv>` * Interface Definition: :download:`axi4lite_intf.sv <../../hdl-src/axi4lite_intf.sv>`
* Class: :class:`peakrdl_busdecoder.cpuif.axi4lite.AXI4Lite_Cpuif` * Class: :class:`peakrdl_busdecoder.cpuif.axi4lite.AXI4LiteCpuif`
Flattened inputs/outputs Flattened inputs/outputs
Flattens the interface into discrete input and output ports. Flattens the interface into discrete input and output ports.
* Command line: ``--cpuif axi4-lite-flat`` * Command line: ``--cpuif axi4-lite-flat``
* Class: :class:`peakrdl_busdecoder.cpuif.axi4lite.AXI4Lite_Cpuif_flattened` * Class: :class:`peakrdl_busdecoder.cpuif.axi4lite.AXI4LiteCpuifFlat`
Pipelined Performance Protocol Notes
--------------------- --------------
This implementation of the AXI4-Lite interface supports transaction pipelining The AXI4-Lite adapter is intentionally simplified:
which can significantly improve performance of back-to-back transfers.
In order to support transaction pipelining, the CPU interface will accept multiple * AW and W channels must be asserted together for writes. The adapter does not
concurrent transactions. The number of outstanding transactions allowed is automatically support decoupled address/data for writes.
determined based on the register file pipeline depth (affected by retiming options), * Only a single outstanding transaction is supported. Masters should wait for
and influences the depth of the internal transaction response skid buffer. the corresponding response before issuing the next request.
* Burst transfers are not supported (single-beat transfers only), consistent
with AXI4-Lite.

View File

@@ -29,15 +29,15 @@ Rather than rewriting a new CPU interface definition, you can extend and adjust
.. code-block:: python .. code-block:: python
from peakrdl_busdecoder.cpuif.axi4lite import AXI4Lite_Cpuif from peakrdl_busdecoder.cpuif.axi4lite import AXI4LiteCpuif
class My_AXI4Lite(AXI4Lite_Cpuif): class My_AXI4Lite(AXI4LiteCpuif):
@property @property
def port_declaration(self) -> str: def port_declaration(self) -> str:
# Override the port declaration text to use the alternate interface name and modport style # Override the port declaration text to use the alternate interface name and modport style
return "axi4_lite_interface.Slave_mp s_axil" return "axi4_lite_interface.Slave_mp s_axil"
def signal(self, name:str) -> str: def signal(self, name: str, node=None, indexer=None) -> str:
# Override the signal names to be lowercase instead # Override the signal names to be lowercase instead
return "s_axil." + name.lower() return "s_axil." + name.lower()
@@ -72,7 +72,7 @@ you can define your own.
2. Create a Python class that defines your CPUIF 2. Create a Python class that defines your CPUIF
Extend your class from :class:`peakrdl_busdecoder.cpuif.CpuifBase`. Extend your class from :class:`peakrdl_busdecoder.cpuif.BaseCpuif`.
Define the port declaration string, and provide a reference to your template file. Define the port declaration string, and provide a reference to your template file.
3. Use your new CPUIF definition when exporting. 3. Use your new CPUIF definition when exporting.

View File

@@ -3,10 +3,10 @@
Internal CPUIF Protocol Internal CPUIF Protocol
======================= =======================
Internally, the busdecoder generator uses a common CPU interface handshake Internally, the bus decoder uses a small set of common request/response signals
protocol. This strobe-based protocol is designed to add minimal overhead to the that each CPU interface adapter must drive. This protocol is intentionally simple
busdecoder implementation, while also being flexible enough to support advanced and supports a single outstanding transaction at a time. The CPU interface logic
features of a variety of bus interface standards. is responsible for holding request signals stable until the transaction completes.
Signal Descriptions Signal Descriptions
@@ -15,62 +15,49 @@ Signal Descriptions
Request Request
^^^^^^^ ^^^^^^^
cpuif_req cpuif_req
When asserted, a read or write transfer will be initiated. When asserted, a read or write transfer is in progress. Request signals must
Denotes that the following signals are valid: ``cpuif_addr``, remain stable until the transfer completes.
``cpuif_req_is_wr``, and ``cpuif_wr_data``.
A transfer will only initiate if the relevant stall signal is not asserted. cpuif_wr_en
If stalled, the request shall be held until accepted. A request's parameters When asserted alongside ``cpuif_req``, denotes a write transfer.
(type, address, etc) shall remain static throughout the stall.
cpuif_addr cpuif_rd_en
Byte-address of the transfer. When asserted alongside ``cpuif_req``, denotes a read transfer.
cpuif_req_is_wr cpuif_wr_addr / cpuif_rd_addr
If ``1``, denotes that the current transfer is a write. Otherwise transfer is Byte address of the write or read transfer, respectively.
a read.
cpuif_wr_data cpuif_wr_data
Data to be written for the write transfer. This signal is ignored for read Data to be written for the write transfer.
transfers.
cpuif_wr_biten cpuif_wr_byte_en
Active-high bit-level write-enable strobes. Active-high byte-enable strobes for writes. Some CPU interfaces do not
Only asserted bit positions will change the register value during a write provide byte enables and may drive this as all-ones.
transfer.
cpuif_req_stall_rd
If asserted, and the next pending request is a read operation, then the
transfer will not be accepted until this signal is deasserted.
cpuif_req_stall_wr
If asserted, and the next pending request is a write operation, then the
transfer will not be accepted until this signal is deasserted.
Read Response Read Response
^^^^^^^^^^^^^ ^^^^^^^^^^^^^
cpuif_rd_ack cpuif_rd_ack
Single-cycle strobe indicating a read transfer has completed. Single-cycle strobe indicating a read transfer has completed.
Qualifies that the following signals are valid: ``cpuif_rd_err`` and Qualifies ``cpuif_rd_err`` and ``cpuif_rd_data``.
``cpuif_rd_data``
cpuif_rd_err cpuif_rd_err
If set, indicates that the read transaction failed and the CPUIF logic Indicates that the read transaction failed. The CPU interface should return
should return an error response if possible. an error response if possible.
cpuif_rd_data cpuif_rd_data
Read data. Is sampled on the same cycle that ``cpuif_rd_ack`` is asserted. Read data. Sampled on the same cycle that ``cpuif_rd_ack`` is asserted.
Write Response Write Response
^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
cpuif_wr_ack cpuif_wr_ack
Single-cycle strobe indicating a write transfer has completed. Single-cycle strobe indicating a write transfer has completed.
Qualifies that the ``cpuif_wr_err`` signal is valid. Qualifies ``cpuif_wr_err``.
cpuif_wr_err cpuif_wr_err
If set, indicates that the write transaction failed and the CPUIF logic Indicates that the write transaction failed. The CPU interface should return
should return an error response if possible. an error response if possible.
Transfers Transfers
@@ -78,155 +65,7 @@ Transfers
Transfers have the following characteristics: Transfers have the following characteristics:
* Only one transfer can be initiated per clock-cycle. This is implicit as there * Only one outstanding transaction is supported.
is only one set of request signals. * The CPU interface must hold ``cpuif_req`` and request parameters stable until
* The register block implementation shall guarantee that only one response can be the corresponding ``cpuif_*_ack`` is asserted.
asserted in a given clock cycle. Only one ``cpuif_*_ack`` signal can be * Responses shall arrive in the same order as requests.
asserted at a time.
* Responses shall arrive in the same order as their corresponding request was
dispatched.
Basic Transfer
^^^^^^^^^^^^^^
Depending on the configuration of the exported register block, transfers can be
fully combinational or they may require one or more clock cycles to complete.
Both are valid and CPU interface logic shall be designed to anticipate either.
.. wavedrom::
{
"signal": [
{"name": "clk", "wave": "p...."},
{"name": "cpuif_req", "wave": "010.."},
{"name": "cpuif_req_is_wr", "wave": "x2x.."},
{"name": "cpuif_addr", "wave": "x2x..", "data": ["A"]},
{},
{"name": "cpuif_*_ack", "wave": "010.."},
{"name": "cpuif_*_err", "wave": "x2x.."}
],
"foot": {
"text": "Zero-latency transfer"
}
}
.. wavedrom::
{
"signal": [
{"name": "clk", "wave": "p..|..."},
{"name": "cpuif_req", "wave": "010|..."},
{"name": "cpuif_req_is_wr", "wave": "x2x|..."},
{"name": "cpuif_addr", "wave": "x2x|...", "data": ["A"]},
{},
{"name": "cpuif_*_ack", "wave": "0..|10."},
{"name": "cpuif_*_err", "wave": "x..|2x."}
],
"foot": {
"text": "Transfer with non-zero latency"
}
}
Read & Write Transactions
-------------------------
Waveforms below show the timing relationship of simple read/write transactions.
For brevity, only showing non-zero latency transfers.
.. wavedrom::
{
"signal": [
{"name": "clk", "wave": "p..|..."},
{"name": "cpuif_req", "wave": "010|..."},
{"name": "cpuif_req_is_wr", "wave": "x0x|..."},
{"name": "cpuif_addr", "wave": "x3x|...", "data": ["A"]},
{},
{"name": "cpuif_rd_ack", "wave": "0..|10."},
{"name": "cpuif_rd_err", "wave": "x..|0x."},
{"name": "cpuif_rd_data", "wave": "x..|5x.", "data": ["D"]}
],
"foot": {
"text": "Read Transaction"
}
}
.. wavedrom::
{
"signal": [
{"name": "clk", "wave": "p..|..."},
{"name": "cpuif_req", "wave": "010|..."},
{"name": "cpuif_req_is_wr", "wave": "x1x|..."},
{"name": "cpuif_addr", "wave": "x3x|...", "data": ["A"]},
{"name": "cpuif_wr_data", "wave": "x5x|...", "data": ["D"]},
{},
{"name": "cpuif_wr_ack", "wave": "0..|10."},
{"name": "cpuif_wr_err", "wave": "x..|0x."}
],
"foot": {
"text": "Write Transaction"
}
}
Transaction Pipelining & Stalls
-------------------------------
If the CPU interface supports it, read and write operations can be pipelined.
.. wavedrom::
{
"signal": [
{"name": "clk", "wave": "p......"},
{"name": "cpuif_req", "wave": "01..0.."},
{"name": "cpuif_req_is_wr", "wave": "x0..x.."},
{"name": "cpuif_addr", "wave": "x333x..", "data": ["A1", "A2", "A3"]},
{},
{"name": "cpuif_rd_ack", "wave": "0.1..0."},
{"name": "cpuif_rd_err", "wave": "x.0..x."},
{"name": "cpuif_rd_data", "wave": "x.555x.", "data": ["D1", "D2", "D3"]}
]
}
It is very likely that the transfer latency of a read transaction will not
be the same as a write for a given register block configuration. Typically read
operations will be more deeply pipelined. This latency asymmetry would create a
hazard for response collisions.
In order to eliminate this hazard, additional stall signals (``cpuif_req_stall_rd``
and ``cpuif_req_stall_wr``) are provided to delay the next incoming transfer
request if necessary. When asserted, the CPU interface shall hold the next pending
request until the stall is cleared.
For non-pipelined CPU interfaces that only allow one outstanding transaction at a time,
these stall signals can be safely ignored.
In the following example, the busdecoder is configured such that:
* A read transaction takes 1 clock cycle to complete
* A write transaction takes 0 clock cycles to complete
.. wavedrom::
{
"signal": [
{"name": "clk", "wave": "p......."},
{"name": "cpuif_req", "wave": "01.....0"},
{"name": "cpuif_req_is_wr", "wave": "x1.0.1.x"},
{"name": "cpuif_addr", "wave": "x33443.x", "data": ["W1", "W2", "R1", "R2", "W3"]},
{"name": "cpuif_req_stall_wr", "wave": "0...1.0."},
{},
{"name": "cpuif_rd_ack", "wave": "0...220.", "data": ["R1", "R2"]},
{"name": "cpuif_wr_ack", "wave": "0220..20", "data": ["W1", "W2", "W3"]}
]
}
In the above waveform, observe that:
* The ``R2`` read request is not affected by the assertion of the write stall,
since the write stall only applies to write requests.
* The ``W3`` write request is stalled for one cycle, and is accepted once the stall is cleared.

View File

@@ -2,16 +2,17 @@ Introduction
============ ============
The CPU interface logic layer provides an abstraction between the The CPU interface logic layer provides an abstraction between the
application-specific bus protocol and the internal register file logic. application-specific bus protocol and the internal bus decoder logic.
When exporting a design, you can select from a variety of popular CPU interface When exporting a design, you can select from supported CPU interface protocols.
protocols. These are described in more detail in the pages that follow. These are described in more detail in the pages that follow.
Bus Width Bus Width
^^^^^^^^^ ^^^^^^^^^
The CPU interface bus width is automatically determined from the contents of the The CPU interface bus width is inferred from the contents of the design.
design being exported. The bus width is equal to the widest ``accesswidth`` It is intended to be equal to the widest ``accesswidth`` encountered in the
encountered in the design. design. If the exported addrmap contains only external components, the width
cannot be inferred and will default to 32 bits.
Addressing Addressing
@@ -32,5 +33,6 @@ For example, consider a fictional AXI4-Lite device that:
- If care is taken to align the global address offset to the size of the device, - If care is taken to align the global address offset to the size of the device,
creating a relative address is as simple as pruning down address bits. creating a relative address is as simple as pruning down address bits.
By default, the bit-width of the address bus will be the minimum size to span the contents By default, the bit-width of the address bus will be the minimum size to span the
of the register block. If needed, the address width can be overridden to a larger range. contents of the decoded address space. If needed, the address width can be
overridden to a larger range using ``--addr-width``.

View File

@@ -1,10 +0,0 @@
CPUIF Passthrough
=================
This CPUIF mode bypasses the protocol converter stage and directly exposes the
internal CPUIF handshake signals to the user.
* Command line: ``--cpuif passthrough``
* Class: :class:`peakrdl_busdecoder.cpuif.passthrough.PassthroughCpuif`
For more details on the protocol itself, see: :ref:`cpuif_protocol`.

View File

@@ -1,131 +0,0 @@
Frequently Asked Questions
==========================
Why isn't there an option for a flat non-struct hardware interface?
-------------------------------------------------------------------
SystemRDL is inherently a very hierarchical language.
For small register blocks, flattening the hardware interface may be acceptable,
but this ends up scaling very poorly as the design becomes larger and has more
complex hierarchy.
Using struct compositions for the hardware interface has the benefit of
preserving conceptual hierarchy and arrays exactly as defined in the original
SystemRDL.
How do I know I connected everything? Structs are harder to review
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Initially this can be daunting, but fortunately the tool has an option to generate a
flattened hardware interface report upon export. Try using the ``--hwif-report``
command line option when exporting. This is the easiest way to quickly
understand the structure of the hardware interface.
Why does the tool generate un-packed structs? I prefer packed structs.
----------------------------------------------------------------------
Packed structs are great when describing vectors that have bit-level structure.
In this tool, the use of un-packed structs is intentional since the hardware
interface is not something that is meant to be bit-accessible. In the case of
the hardware interface structs, using a packed struct is semantically inappropriate.
... Then how can I initialize the struct?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
We get this request most often because designers want to initialize the ``hwif_in``
struct with a simple assignment:
.. code:: systemverilog
always_comb begin
hwif_in = '0;
end
Of course since the struct actually is **unpacked**, this will result in a
compile error which usually leads to the inappropriate assumption that it ought
to be packed. (See this amusing blog post about `X/Y problems <https://xyproblem.info>`_)
If your goal is to initialize the packed struct, fortunately SystemVerilog already
has syntax to do this:
.. code:: systemverilog
always_comb begin
hwif_in = '{default: '0};
end
This is lesser-known syntax, but still very well supported by synthesis
tools, and is the recommended way to handle this.
... What if I want to assign it to a bit-vector?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Assigning the hwif struct to a bit-vector is strongly discouraged. This tool makes
no guarantees regarding the field ordering of the hwif structure, so doing so
should be considered functionally dangerous.
That said, if you still need to do this, it is still trivially possible to
without requiring packed structs. Instead, use the SystemVerilog streaming operator:
.. code:: systemverilog
my_packed_vector = {<<{hwif_out}};
... Why are unpacked structs preferred?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In the case of the hardware interface ports, unpacked structs help prevent
mistakes that are very easy to make.
Consider the following situation - a designer has a field that sets the following
properties: ``sw=rw; hw=rw; we;``, and wants to assign the hardware input value,
so they erroneously do the following assignment in Verilog:
.. code:: systemverilog
assign hwif_in.my_register.my_field = <some value>;
This is actually a bug since the ``my_field`` member is actually a struct that
has two members: ``we`` and ``next``. If this were a packed struct, this would
silently compile and you would potentially have a bug that may not be noticed
(depending on how thorough the test campaign is).
With an unpacked struct, this gets flagged immediately as a compile error since
the assignment is invalid.
The designer may have simply forgotten that the field is an aggregate of multiple
members and intended to do the following:
.. code:: systemverilog
assign hwif.my_register.my_field.next = <some value>;
assign hwif.my_register.my_field.we = <some control signal>;
The generated output does not match our organization's coding style
-------------------------------------------------------------------
SystemVerilog coding styles vary wildly, and unfortunately there is little
consensus on this topic within the digital design community.
The output generated by PeakRDL-BusDecoder strives to be as human-readable as possible,
and follow consistent indentation and styling. We do our best to use the most
widely accepted coding style, but since this is a very opinionated space, it is
impossible to satisfy everyone.
In general, we strive to follow the
`SystemVerilog style guide by lowRISC <https://github.com/lowRISC/style-guides/blob/master/VerilogCodingStyle.md>`_,
but may deviate in some areas if not practical or would impose excessive complexity on the code generator.
The lint tool I am using is flagging violations in generated code
-----------------------------------------------------------------
Code linting tools are a great way to check for user-error, flag inconsistencies,
and enforce best-practices within an organization. In many cases, linter tools
may be configured to also enforce stylistic preferences.
Unfortunately just like coding styles, lint rules can often be more
opinionated than practical.
In general, we will not address lint violations unless they flag actual
structural issues or semantically dangerous code.
Stylistic violations that pose no actual danger to the correctness of the design
will rarely be addressed, especially if the change would add unreasonable
complexity to the tool.
If you encounter a lint violation, please carefully review and consider waiving
it if it does not pose an actual danger. If you still believe it is a problem,
please let us know by `submitting an issue <https://github.com/arnavsacheti/PeakRDL-BusDecoder/issues>`_
that describes the problem.

View File

@@ -1,61 +0,0 @@
Hardware Interface
------------------
The generated register block will present the entire hardware interface to the user
using two struct ports:
* ``hwif_in``
* ``hwif_out``
All field inputs and outputs as well as signals are consolidated into these
struct ports. The presence of each depends on the specific contents of the design
being exported.
Using structs for the hardware interface has the following benefits:
* Preserves register map component grouping, arrays, and hierarchy.
* Avoids naming collisions and cumbersome signal name flattening.
* Allows for more natural mapping and distribution of register block signals to a design's hardware components.
* Use of unpacked arrays/structs prevents common assignment mistakes as they are enforced by the compiler.
Structs are organized as follows: ``hwif_out.<heir_path>.<feature>``
For example, a simple design such as:
.. code-block:: systemrdl
addrmap my_design {
reg {
field {
sw = rw;
hw = rw;
we;
} my_field;
} my_reg[2];
};
... results in the following struct members:
.. code-block:: text
hwif_out.my_reg[0].my_field.value
hwif_in.my_reg[0].my_field.next
hwif_in.my_reg[0].my_field.we
hwif_out.my_reg[1].my_field.value
hwif_in.my_reg[1].my_field.next
hwif_in.my_reg[1].my_field.we
For brevity in this documentation, hwif features will be described using shorthand
notation that omits the hierarchical path: ``hwif_out..<feature>``
.. important::
The PeakRDL tool makes no guarantees on the field order of the hwif structs.
For this reason, it is strongly recommended to always access struct members
directly, by name.
If using the SystemVerilog streaming operator to assign the hwif struct to a
packed vector, be extremely careful to avoid assumptions on the resulting bit-position of a field.

View File

@@ -1,28 +1,34 @@
Introduction Introduction
============ ============
PeakRDL-BusDecoder is a free and open-source bus decoder generator for hierarchical register address maps. PeakRDL-BusDecoder is a free and open-source bus decoder generator for hierarchical
This code generator translates your SystemRDL register description into a synthesizable SystemRDL address maps. It produces a synthesizable SystemVerilog RTL module that
SystemVerilog RTL module that decodes CPU interface transactions and routes them to accepts a single CPU interface (slave side) and fans transactions out to multiple
multiple sub-address spaces (child addrmaps). This is particularly useful for: child address spaces (master side).
This tool **does not** generate register storage or field logic. It is strictly a
bus-routing layer that decodes addresses and forwards requests to child blocks.
This is particularly useful for:
* Creating hierarchical register maps with multiple sub-components * Creating hierarchical register maps with multiple sub-components
* Splitting a single CPU interface bus to serve multiple independent register blocks * Splitting a single CPU interface bus to serve multiple independent register blocks
* Organizing large register designs into logical sub-address spaces * Organizing large address spaces into logical sub-regions
* Implementing address decode logic for multi-drop bus architectures * Implementing address decode logic for multi-drop bus architectures
The generated bus decoder provides: The generated bus decoder provides:
* Fully synthesizable SystemVerilog RTL (IEEE 1800-2012) * Fully synthesizable SystemVerilog RTL (IEEE 1800-2012)
* Support for many popular CPU interface protocols (AMBA APB, AXI4-Lite, and more) * A top-level slave CPU interface and per-child master CPU interfaces
* Address decode logic that routes transactions to child address maps * Address decode logic that routes transactions to child address maps
* Configurable pipelining options for designs with fast clock rates * Support for APB3, APB4, and AXI4-Lite (plus plugin-defined CPU interfaces)
* Broad support for SystemRDL 2.0 features * Configurable decode depth and array unrolling
Quick Start Quick Start
----------- -----------
The easiest way to use PeakRDL-BusDecoder is via the `PeakRDL command line tool <https://peakrdl.readthedocs.io/>`_: The easiest way to use PeakRDL-BusDecoder is via the
`PeakRDL command line tool <https://peakrdl.readthedocs.io/>`_:
.. code-block:: bash .. code-block:: bash
@@ -32,6 +38,20 @@ The easiest way to use PeakRDL-BusDecoder is via the `PeakRDL command line tool
# Export! # Export!
peakrdl busdecoder atxmega_spi.rdl -o busdecoder/ --cpuif axi4-lite peakrdl busdecoder atxmega_spi.rdl -o busdecoder/ --cpuif axi4-lite
The exporter writes two files:
* A SystemVerilog module (the bus decoder)
* A SystemVerilog package (constants like data width and per-child address widths)
Key command-line options:
* ``--cpuif``: Select the CPU interface (``apb3``, ``apb3-flat``, ``apb4``, ``apb4-flat``, ``axi4-lite``, ``axi4-lite-flat``)
* ``--module-name``: Override the generated module name
* ``--package-name``: Override the generated package name
* ``--addr-width``: Override the slave address width
* ``--unroll``: Unroll arrayed children into discrete interfaces
* ``--max-decode-depth``: Control how far the decoder descends into hierarchy
Looking for VHDL? Looking for VHDL?
----------------- -----------------
@@ -55,10 +75,8 @@ Links
self self
architecture architecture
hwif
configuring configuring
limitations limitations
faq
licensing licensing
api api
@@ -69,29 +87,5 @@ Links
cpuif/introduction cpuif/introduction
cpuif/apb cpuif/apb
cpuif/axi4lite cpuif/axi4lite
cpuif/avalon
cpuif/passthrough
cpuif/internal_protocol cpuif/internal_protocol
cpuif/customizing cpuif/customizing
.. toctree::
:hidden:
:caption: SystemRDL Properties
props/field
props/reg
props/addrmap
props/signal
props/rhs_props
.. toctree::
:hidden:
:caption: Other SystemRDL Features
rdl_features/external
.. toctree::
:hidden:
:caption: Extended Properties
udps/intro

View File

@@ -15,7 +15,7 @@ be contrary to this project's philosophy.
What is covered by the LGPL v3 license? What is covered by the LGPL v3 license?
-------------------------------------- ---------------------------------------
The LGPL license is intended for the code generator itself. This includes all The LGPL license is intended for the code generator itself. This includes all
Python sources, Jinja template files, as well as testcase infrastructure not Python sources, Jinja template files, as well as testcase infrastructure not
explicitly mentioned in the exemptions below. explicitly mentioned in the exemptions below.

View File

@@ -1,53 +1,47 @@
Known Limitations Known Limitations
================= =================
Not all SystemRDL features are supported by this exporter. For a listing of The busdecoder exporter intentionally focuses on address decode and routing.
supported properties, see the appropriate property listing page in the sections Some SystemRDL features are ignored, and a few are explicitly disallowed.
that follow.
Alias Registers Address Alignment
--------------- -----------------
Registers instantiated using the ``alias`` keyword are not supported yet. All address offsets and array strides must be aligned to the CPU interface data
bus width (in bytes). Misaligned offsets/strides are rejected.
Unaligned Registers Wide Registers
------------------- --------------
All address offsets & strides shall be a multiple of the cpuif bus width used. Specifically: If a register is wider than its ``accesswidth`` (a multi-word register), its
``accesswidth`` must match the CPU interface data width. Multi-word registers
* Bus width is inferred by the maximum accesswidth used in the busdecoder. with a smaller accesswidth are not supported.
* Each component's address and array stride shall be aligned to the bus width.
Uniform accesswidth Fields Spanning Sub-Words
------------------- -------------------------
All registers within a register block shall use the same accesswidth. If a field spans multiple sub-words of a wide register:
One exception is that registers with regwidth that is narrower than the cpuif * Software-writable fields must have write buffering enabled
bus width are permitted, provided that their regwidth is equal to their accesswidth. * Fields with ``onread`` side-effects must have read buffering enabled
For example: These rules are enforced to avoid ambiguous multi-word access behavior.
.. code-block:: systemrdl
// (Largest accesswidth used is 32, therefore the CPUIF bus width is 32) External Boundary References
----------------------------
Property references are not allowed to cross the internal/external boundary of
the exported addrmap. References must point to components that are internal to
the busdecoder being generated.
reg { CPU Interface Reset Location
regwidth = 32; ----------------------------
accesswidth = 32; Only ``cpuif_reset`` signals instantiated at the top-level addrmap (or above)
} reg_a @ 0x00; // OK. Regular 32-bit register are honored. Nested ``cpuif_reset`` signals are ignored.
reg {
regwidth = 64;
accesswidth = 32;
} reg_b @ 0x08; // OK. "Wide" register of 64-bits, but is accessed using 32-bit subwords
reg { Unsupported Properties
regwidth = 8; ----------------------
accesswidth = 8; The following SystemRDL properties are explicitly rejected:
} reg_c @ 0x10; // OK. Is aligned to the cpuif bus width
reg { * ``sharedextbus`` on addrmap/regfile components
regwidth = 32;
accesswidth = 8;
} bad_reg @ 0x14; // NOT OK. accesswidth conflicts with cpuif width

View File

@@ -1,28 +0,0 @@
Addrmap/Regfile Properties
==========================
.. note:: Any properties not explicitly listed here are either implicitly
supported, or are not relevant to the busdecoder exporter and are ignored.
errextbus
---------
|NO|
sharedextbus
------------
|NO|
--------------------------------------------------------------------------------
Addrmap Properties
==================
bigendian/littleendian
----------------------
|NO|
rsvdset
-------
|NO|

View File

@@ -1,491 +0,0 @@
Field Properties
================
.. note:: Any properties not explicitly listed here are either implicitly
supported, or are not relevant to the busdecoder exporter and are ignored.
Software Access Properties
--------------------------
onread/onwrite
^^^^^^^^^^^^^^
All onread/onwrite actions are supported (except for ruser/wuser)
rclr/rset
^^^^^^^^^
See ``onread``. These are effectively aliases of the onread property.
singlepulse
^^^^^^^^^^^
If set, field will get cleared back to zero after being written.
.. wavedrom::
{"signal": [
{"name": "clk", "wave": "p....."},
{"name": "<swmod>", "wave": "0.10.."},
{"name": "hwif_out..value", "wave": "0..10."}
]}
sw
^^^
All sw access modes are supported except for ``w1`` and ``rw1``.
swacc
^^^^^
If true, infers an output signal ``hwif_out..swacc`` that is asserted when
accessed by software. Specifically, on the same clock cycle that the field is
being sampled during a software read operation, or as it is being written.
.. wavedrom::
{"signal": [
{"name": "clk", "wave": "p...."},
{"name": "hwif_in..next", "wave": "x.=x.", "data": ["D"]},
{"name": "hwif_out..swacc", "wave": "0.10."}
]}
swmod
^^^^^
If true, infers an output signal ``hwif_out..swmod`` that is asserted as the
field is being modified by software. This can be due to a software write
operation, or a software read operation that has clear/set side-effects.
.. wavedrom::
{"signal": [
{"name": "clk", "wave": "p....."},
{"name": "hwif_out..value", "wave": "=..=..", "data": ["old", "new"]},
{"name": "hwif_out..swmod", "wave": "0.10.."}
]}
swwe/swwel
^^^^^^^^^^
Provides a mechanism that allows hardware to override whether the field is
writable by software.
boolean
If True, infers an input signal ``hwif_in..swwe`` or ``hwif_in..swwel``.
reference
Single-bit reference controls field's behavior.
woclr/woset
^^^^^^^^^^^
See ``onwrite``. These are effectively aliases of the onwrite property.
--------------------------------------------------------------------------------
Hardware Access Properties
--------------------------
anded/ored/xored
^^^^^^^^^^^^^^^^
If true, infers the existence of output signal: ``hwif_out..anded``,
``hwif_out..ored``, ``hwif_out..xored``
hw
^^^
Controls hardware access to the field.
If readable, enables output signal ``hwif_out..value``. If writable, enables
input ``hwif_in..next``.
Hardware-writable fields can optionally define the ``next`` property which replaces
the inferred ``hwif_in..next`` input with an alternate reference.
hwclr/hwset
^^^^^^^^^^^
If both ``hwclr`` and ``hwset`` properties are used, and both are asserted at
the same clock cycle, then ``hwset`` will take precedence.
boolean
If true, infers the existence of input signal: ``hwif_in..hwclr``, ``hwif_in..hwset``
reference
Reference to any single-bit internal object to drive this control.
hwenable/hwmask
^^^^^^^^^^^^^^^
Reference to a component that provides bit-level control of hardware writeability.
we/wel
^^^^^^
Write-enable control from hardware interface.
If true, infers the existence of input signal: ``hwif_in..we``, ``hwif_in..wel``
.. wavedrom::
{"signal": [
{"name": "clk", "wave": "p...."},
{"name": "hwif_in..next", "wave": "x.=x.", "data": ["D"]},
{"name": "hwif_in..we", "wave": "0.10."},
{"name": "hwif_in..wel", "wave": "1.01."},
{"name": "<field value>", "wave": "x..=.", "data": ["D"]}
]}
boolean
If true, infers the existence of input signal ``hwif_in..we`` or ``hwif_in..wel``
reference
Reference to any single-bit internal object to drive this control.
--------------------------------------------------------------------------------
Counter Properties
------------------
counter
^^^^^^^
If true, marks this field as a counter. The counter direction is inferred based
based on which properties are assigned. By default, an up-counter is implemented.
If any of the properties associated with an up-counter are used, then up-counting
capabilities will be implemented. The same is true for down-counters and up/down
counters.
Unless alternate control signals are specified, the existence of input signals
``hwif_in..incr`` and ``hwif_in..decr`` will be inferred depending on the type
of counter described.
incr
^^^^
Assign a reference to an alternate control signal to increment the counter.
If assigned, the inferred ``hwif_in..incr`` input will not be generated.
incrsaturate/saturate
^^^^^^^^^^^^^^^^^^^^^
If assigned, indicates that the counter will saturate instead of wrapping.
If an alternate saturation point is specified, the counter value will be
adjusted so that it does not exceed that limit, even after non-increment actions.
boolean
If true, saturation point is at the counter's maximum count value. (2^width - 1)
integer
Specify a static saturation value.
reference
Specify a dynamic saturation value.
incrthreshold/threshold
^^^^^^^^^^^^^^^^^^^^^^^
If assigned, infers a ``hwif_out..incrthreshold`` output signal. This signal is
asserted if the counter value is greater or equal to the threshold.
.. wavedrom::
{
"signal": [
{"name": "clk", "wave": "p......"},
{"name": "hwif_in..incr", "wave": "01...0."},
{"name": "<counter>", "wave": "=.=3==..", "data": [4,5,6,7,8,9]},
{"name": "hwif_out..incrthreshold", "wave": "0..1...."}
],
"foot": {
"text": "Example where incrthreshold = 6"
}
}
boolean
If true, threshold is the counter's maximum count value. (2^width - 1)
integer
Specify a static threshold value.
reference
Specify a dynamic threshold value.
incrvalue
^^^^^^^^^
Override the counter's increment step size.
integer
Specify a static increment step size.
reference
Reference a component that controls the step size.
incrwidth
^^^^^^^^^
If assigned, infers an input signal ``hwif_in..incrvalue``. The value of this
property defines the signal's width.
overflow
^^^^^^^^
If true, infers an output signal ``hwif_out..overflow`` that is asserted when
the counter is about to wrap.
.. wavedrom::
{
"signal": [
{"name": "clk", "wave": "p......."},
{"name": "hwif_in..incr", "wave": "0101010."},
{"name": "<counter>", "wave": "=.=.=.=.", "data": [14,15,0,1]},
{"name": "hwif_out..overflow", "wave": "0..10..."}
],
"foot": {
"text": "A 4-bit counter overflowing"
}
}
decr
^^^^
Assign a reference to an alternate control signal to decrement the counter.
If assigned, the inferred ``hwif_in..decr`` input will not be generated.
decrsaturate
^^^^^^^^^^^^
If assigned, indicates that the counter will saturate instead of wrapping.
If an alternate saturation point is specified, the counter value will be
adjusted so that it does not exceed that limit, even after non-decrement actions.
boolean
If true, saturation point is when the counter reaches 0.
integer
Specify a static saturation value.
reference
Specify a dynamic saturation value.
decrthreshold
^^^^^^^^^^^^^
If assigned, infers a ``hwif_out..decrthreshold`` output signal. This signal is
asserted if the counter value is less than or equal to the threshold.
.. wavedrom::
{
"signal": [
{"name": "clk", "wave": "p......"},
{"name": "hwif_in..decr", "wave": "01...0."},
{"name": "<counter>", "wave": "=.=3==.", "data": [9,8,7,6,5,4]},
{"name": "hwif_out..decrthreshold", "wave": "0..1..."}
],
"foot": {
"text": "Example where incrthreshold = 7"
}
}
boolean
If true, threshold is 0.
integer
Specify a static threshold value.
reference
Specify a dynamic threshold value.
decrvalue
^^^^^^^^^
Override the counter's decrement step size.
integer
Specify a static step size.
reference
Reference to a component that controls the step size.
decrwidth
^^^^^^^^^
If assigned, infers an input signal ``hwif_in..decrvalue``. The value of this
property defines the signal's width.
underflow
^^^^^^^^^
If true, infers an output signal ``hwif_out..underflow`` that is asserted when
the counter is about to wrap.
.. wavedrom::
{
"signal": [
{"name": "clk", "wave": "p......."},
{"name": "hwif_in..decr", "wave": "0101010."},
{"name": "<counter>", "wave": "=.=.=.=.", "data": [1,0,15,14]},
{"name": "hwif_out..underflow", "wave": "0..10..."}
],
"foot": {
"text": "A 4-bit counter underflowing"
}
}
--------------------------------------------------------------------------------
Interrupt Properties
--------------------
intr
^^^^
If set, this field becomes an interrupt field.
The enclosing register infers an output signal ``hwif_out..intr`` which denotes
that an interrupt is active. This is an or-reduction of all interrupt fields
after applying the appropriate ``enable`` or ``mask`` to the field value.
level (default)
Interrupt is level-sensitive. If a bit on the field's ``hwif_in..next`` input
is '1', it will trigger an interrupt event.
posedge
If a bit on the field's ``hwif_in..next`` input transitions from '0' to '1',
it will trigger an interrupt event. This transition shall still be synchronous
to the register block's clock.
negedge
If a bit on the field's ``hwif_in..next`` input transitions from '1' to '0',
it will trigger an interrupt event. This transition shall still be synchronous
to the register block's clock.
bothedge
If a bit on the field's ``hwif_in..next`` input transitions from '0' to '1' or '1' to '0',
it will trigger an interrupt event. This transition shall still be synchronous
to the register block's clock.
nonsticky
Interrupt event is not sticky.
enable
^^^^^^
Reference to a field or signal that, if set to 1, define which bits in the field
are used to assert an interrupt.
mask
^^^^
Reference to a field or signal that, if set to 1, define which bits in the field
are *not* used to assert an interrupt.
haltenable
^^^^^^^^^^
Reference to a field or signal that, if set to 1, define which bits in the field
are used to assert the halt output.
If this property is set, the enclosing register will infer a ``hwif_out..halt`` output.
haltmask
^^^^^^^^
Reference to a field or signal that, if set to 1, define which bits in the field
are *not* used to assert the halt output.
If this property is set, the enclosing register will infer a ``hwif_out..halt`` output.
stickybit
^^^^^^^^^
When an interrupt trigger occurs, a stickybit field will set the corresponding
bit to '1' and hold it until it is cleared by a software access.
The interrupt trigger depends on the interrupt type. By default, interrupts are
level-sensitive, but the interrupt modifiers allow for edge-sensitive triggers as
well.
The waveform below demonstrates a level-sensitive interrupt:
.. wavedrom::
{
"signal": [
{"name": "clk", "wave": "p....."},
{"name": "hwif_in..next", "wave": "010..."},
{"name": "<field value>", "wave": "0.1..."}
]
}
sticky
^^^^^^
Unlike ``stickybit`` fields, a sticky field will latch an entire value. The
value is latched as soon as ``hwif_in..next`` is nonzero, and is held until the
field contents are cleared back to 0 by a software access.
.. wavedrom::
{
"signal": [
{"name": "clk", "wave": "p....."},
{"name": "hwif_in..next", "wave": "23.22.", "data": [0,10,20,30]},
{"name": "<field value>", "wave": "2.3...", "data": [0, 10]}
]
}
--------------------------------------------------------------------------------
Misc
----
encode
^^^^^^
If assigned a user-defined enumeration, the resulting package file will include
its definition. Due to limitations from type-strictness rules in SystemVerilog,
the field will remain as a ``logic`` datatype.
next
^^^^
If assigned, replaces the inferred ``hwif_in..next`` input with an explicit reference.
paritycheck
^^^^^^^^^^^
If set, enables parity checking for this field.
Adds a ``parity_error`` output signal to the module.
.. note::
If this field does not implement storage, the ``partycheck`` property is ignored.
precedence
^^^^^^^^^^
Control whether hardware or software has precedence when field value update
contention occurs. Software has precedence by default.
reset
^^^^^
Control the reset value of the field's storage element.
If not specified, the field will not be reset.
integer
Static reset value
reference
Reference to a dynamic reset value.
resetsignal
^^^^^^^^^^^
Provide an alternate reset trigger for this field.

View File

@@ -1,14 +0,0 @@
Register Properties
===================
.. note:: Any properties not explicitly listed here are either implicitly
supported, or are not relevant to the busdecoder exporter and are ignored.
accesswidth
-----------
Control the software access width. The register block's CPUIF bus width is
determined by the maximum accesswidth encountered.
regwidth
--------
Control the bit-width of the register.

View File

@@ -1,182 +0,0 @@
RHS Property References
=======================
SystemRDL allows some properties to be referenced in the righthand-side of
property assignment expressions:
.. code-block:: systemrdl
some_property = my_reg.my_field -> some_property;
The official SystemRDL spec refers to these as "Ref targets" in Table G1, but
unfortunately does not describe their semantics in much detail.
The text below describes the interpretations used for this exporter.
--------------------------------------------------------------------------------
Field
-----
field -> swacc
^^^^^^^^^^^^^^
Single-cycle strobe that indicates the field is being accessed by software
(read or write).
field -> swmod
^^^^^^^^^^^^^^^
Single-cycle strobe that indicates the field is being modified during a software
access operation.
field -> swwe/swwel
^^^^^^^^^^^^^^^^^^^
Represents the signal that controls the field's swwe/swwel behavior.
field -> anded/ored/xored
^^^^^^^^^^^^^^^^^^^^^^^^^
Represents the current and/or/xor reduction of the field's value.
field -> hwclr/hwset
^^^^^^^^^^^^^^^^^^^^
|EX|
Represents the signal that controls the field's hwclr/hwset behavior.
field -> hwenable/hwmask
^^^^^^^^^^^^^^^^^^^^^^^^
Represents the signal that controls the field's hwenable/hwmask behavior.
field -> we/wel
^^^^^^^^^^^^^^^
Represents the signal that controls the field's we/wel behavior.
field -> next
^^^^^^^^^^^^^
|EX|
field -> reset
^^^^^^^^^^^^^^
Represents the value that was assigned to this property.
field -> resetsignal
^^^^^^^^^^^^^^^^^^^^
Represents the value that was assigned to this property.
--------------------------------------------------------------------------------
Field Counter Properties
------------------------
field -> incr
^^^^^^^^^^^^^
Represents the signal that controls the field's counter increment control.
field -> incrsaturate/saturate
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Represents the internal 1-bit event signal that indicates whether the counter is saturated
at its saturation value.
.. wavedrom::
{
"signal": [
{"name": "clk", "wave": "p......"},
{"name": "hwif_in..decr", "wave": "0101010"},
{"name": "<counter>", "wave": "=.=....", "data": [1,0]},
{"name": "<decrsaturate>", "wave": "0.1...."}
],
"foot": {
"text": "A 4-bit counter saturating"
}
}
field -> incrthreshold/threshold
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Represents the 1-bit event signal that indicates whether the counter has met or
exceeded its incrthreshold.
field -> incrvalue
^^^^^^^^^^^^^^^^^^
Represents the value that was assigned to this property.
field -> overflow
^^^^^^^^^^^^^^^^^
Represents the event signal that is asserted when the counter is about to wrap.
field -> decr
^^^^^^^^^^^^^
Represents the signal that controls the field's counter decrement control.
field -> decrsaturate
^^^^^^^^^^^^^^^^^^^^^
Represents the internal 1-bit event signal that indicates whether the counter is saturated
at its saturation value.
.. wavedrom::
{
"signal": [
{"name": "clk", "wave": "p......"},
{"name": "hwif_in..incr", "wave": "0101010"},
{"name": "<counter>", "wave": "=.=....", "data": [14,15]},
{"name": "<incrsaturate>", "wave": "0.1...."}
],
"foot": {
"text": "A 4-bit counter saturating"
}
}
field -> decrthreshold
^^^^^^^^^^^^^^^^^^^^^^
Represents the 1-bit event signal that indicates whether the counter has met or
exceeded its incrthreshold.
field -> decrvalue
^^^^^^^^^^^^^^^^^^
Represents the value that was assigned to this property.
field -> underflow
^^^^^^^^^^^^^^^^^^
Represents the event signal that is asserted when the counter is about to wrap.
--------------------------------------------------------------------------------
Field Interrupt Properties
--------------------------
field -> enable
^^^^^^^^^^^^^^^
Represents the value that was assigned to this property.
field -> mask
^^^^^^^^^^^^^
Represents the value that was assigned to this property.
field -> haltenable
^^^^^^^^^^^^^^^^^^^
Represents the value that was assigned to this property.
field -> haltmask
^^^^^^^^^^^^^^^^^
Represents the value that was assigned to this property.
--------------------------------------------------------------------------------
Register
--------
reg -> intr
^^^^^^^^^^^
References the register's ``hwif_out..intr`` signal.
reg -> halt
^^^^^^^^^^^
References the register's ``hwif_out..halt`` signal.

View File

@@ -1,28 +0,0 @@
Signal Properties
=================
.. note:: Any properties not explicitly listed here are either implicitly
supported, or are not relevant to the busdecoder exporter and are ignored.
activehigh/activelow
--------------------
Only relevant for signals used as resets. Defines the reset signal's polarity.
sync/async
----------
Only supported for signals used as resets to infer edge-sensitive reset.
Ignored in all other contexts.
cpuif_reset
-----------
Specify that this signal shall be used as alternate reset signal for the CPU
interface for this busdecoder.
field_reset
-----------
Specify that this signal is used as an alternate reset signal for all fields
instantiated in sub-hierarchies relative to this signal.

View File

@@ -1,155 +0,0 @@
External Components
===================
SystemRDL allows some component instances to be defined as "external" elements
of an address space definition. In the context of this busdecoder generator,
the implementation of an external component is left up to the designer. When
generating the RTL for a busdecoder, the implementations of external components
are omitted and instead a user-interface is presented on the
``hwif_in``/``hwif_out`` i/o structs.
External component signals on the hardware interface closely follow the semantics
of the :ref:`cpuif_protocol`.
Things you should know
----------------------
* By default external ``hwif_out`` signals are driven combinationally. An
optional output retiming stage can be enabled if needed.
* Due to the uncertain access latency of external components, the busdecoder will
only issue one outstanding transaction to an external component at a time.
This is enforced even if the CPUIF is capable of pipelined accesses such as
AXI4-Lite.
External Registers
------------------
External registers can be useful if it is necessary to implement a register that
cannot easily be expressed using SystemRDL semantics. This could be a unique
access policy, or FIFO-like push/pop registers.
External registers are annotated as such by using the ``external`` keyword:
.. code-block:: systemrdl
// An internal register
my_reg int_reg;
// An external register
external my_reg ext_reg;
Request
^^^^^^^
hwif_out..req
When asserted, a read or write transfer will be initiated.
Qualifies all other request signals.
If the register is wide (``regwidth`` > ``accesswidth``), then the
``hwif_out..req`` will consist of multiple bits, representing the access
strobe for each sub-word of the register.
If the register does not contain any readable fields, this strobe will be
suppressed for read operations.
If the register does not contain any writable readable fields, this strobe
will be suppressed for write operations.
hwif_out..req_is_wr
If ``1``, denotes that the current transfer is a write. Otherwise transfer is
a read.
hwif_out..wr_data
Data to be written for the write transfer. This signal is ignored for read
transfers.
The bit-width of this signal always matches the CPUIF's bus width,
regardless of the regwidth.
If the register does not contain any writable fields, this signal is omitted.
hwif_out..wr_biten
Active-high bit-level write-enable strobes.
Only asserted bit positions will change the register value during a write
transfer.
If the register does not contain any writable fields, this signal is omitted.
Read Response
^^^^^^^^^^^^^
hwif_in..rd_ack
Single-cycle strobe indicating a read transfer has completed.
Qualifies all other read response signals.
If the transfer is always completed in the same cycle, it is acceptable to
tie this signal to ``hwif_out..req && !hwif_out..req_is_wr``.
If the register does not contain any readable fields, this signal is omitted.
hwif_in..rd_data
Read response data.
If the register does not contain any readable fields, this signal is omitted.
Write Response
^^^^^^^^^^^^^^
hwif_in..wr_ack
Single-cycle strobe indicating a write transfer has completed.
If the transfer is always completed in the same cycle, it is acceptable to
tie this signal to ``hwif_out..req && hwif_out..req_is_wr``.
If the register does not contain any writable fields, this signal is omitted.
External Blocks
---------------
Broader external address regions can be represented by external block-like
components such as ``addrmap``, ``regfile`` or ``mem`` elements.
To ensure address decoding for external blocks is simple (only requires simple bit-pruning),
blocks that are external to an exported busdecoder shall be aligned to their size.
Request
^^^^^^^
hwif_out..req
When asserted, a read or write transfer will be initiated.
Qualifies all other request signals.
hwif_out..addr
Byte-address of the transfer.
Address is always relative to the block's local addressing. i.e: The first
byte within an external block is represented as ``hwif_out..addr`` == 0,
regardless of the absolute address of the block.
hwif_out..req_is_wr
If ``1``, denotes that the current transfer is a write. Otherwise transfer is
a read.
hwif_out..wr_data
Data to be written for the write transfer. This signal is ignored for read
transfers.
The bit-width of this signal always matches the CPUIF's bus width,
regardless of the contents of the block.
hwif_out..wr_biten
Active-high bit-level write-enable strobes.
Only asserted bit positions will change the register value during a write
transfer.
Read Response
^^^^^^^^^^^^^
hwif_in..rd_ack
Single-cycle strobe indicating a read transfer has completed.
Qualifies all other read response signals.
hwif_in..rd_data
Read response data.
Write Response
^^^^^^^^^^^^^^
hwif_in..wr_ack
Single-cycle strobe indicating a write transfer has completed.

View File

@@ -1,3 +1,2 @@
pygments-systemrdl pygments-systemrdl
sphinxcontrib-wavedrom
sphinx-book-theme sphinx-book-theme

View File

@@ -1,79 +0,0 @@
Introduction
============
Although the official SystemRDL spec defines numerous properties that allow you
to define complex register map structures, sometimes they are not enough to
accurately describe a necessary feature. Fortunately the SystemRDL spec allows
the language to be extended using "User Defined Properties" (UDPs).
Current UDP Support
-------------------
**Note:** PeakRDL-BusDecoder currently does not implement any User Defined Properties.
The focus of this tool is on bus decoding and address space routing rather than
field-level or register-level behavioral extensions.
If you need UDPs for field-level behaviors (such as buffering, signedness, or
fixed-point representations), consider using `PeakRDL-regblock <https://github.com/SystemRDL/PeakRDL-regblock>`_,
which is designed for comprehensive register block generation with extensive UDP support.
Extending with Custom UDPs
---------------------------
If your bus decoder design requires custom User Defined Properties, you can extend
PeakRDL-BusDecoder by:
1. **Define your UDP in SystemRDL**
Create a ``.rdl`` file that defines your custom properties:
.. code-block:: systemrdl
property my_custom_prop {
component = addrmap;
type = boolean;
};
2. **Implement the UDP in Python**
Create a Python UDP definition class in your project:
.. code-block:: python
from systemrdl.udp import UDPDefinition
class MyCustomUDP(UDPDefinition):
name = "my_custom_prop"
valid_components = {"addrmap"}
valid_type = bool
default = False
3. **Register the UDP with the compiler**
When using PeakRDL-BusDecoder programmatically, register your UDP:
.. code-block:: python
from systemrdl import RDLCompiler
from peakrdl_busdecoder import BusDecoderExporter
rdlc = RDLCompiler()
rdlc.register_udp(MyCustomUDP)
# Compile your RDL files
rdlc.compile_file("my_udp_defs.rdl")
rdlc.compile_file("my_design.rdl")
root = rdlc.elaborate()
# Export
exporter = BusDecoderExporter()
exporter.export(root, "output/")
4. **Access UDP values in your design**
UDP values can be accessed from nodes in the SystemRDL tree and used to
customize the generated bus decoder logic as needed.
For more information on creating User Defined Properties, see the
`SystemRDL Compiler documentation <https://systemrdl-compiler.readthedocs.io/en/stable/model_structure.html#user-defined-properties>`_.

View File

@@ -4,11 +4,14 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "peakrdl-busdecoder" name = "peakrdl-busdecoder"
version = "0.4.0" version = "0.6.7"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = ["jinja2>=3.1.6", "systemrdl-compiler~=1.30.1"] dependencies = [
"jinja2~=3.1",
"systemrdl-compiler~=1.30",
]
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" }
@@ -52,7 +55,6 @@ Documentation = "https://peakrdl-busdecoder.readthedocs.io/"
docs = [ docs = [
"pygments-systemrdl>=1.3.0", "pygments-systemrdl>=1.3.0",
"sphinx-book-theme>=1.1.4", "sphinx-book-theme>=1.1.4",
"sphinxcontrib-wavedrom>=3.0.4",
] ]
test = [ test = [
"parameterized>=0.9.0", "parameterized>=0.9.0",
@@ -62,7 +64,7 @@ test = [
"cocotb>=1.8.0", "cocotb>=1.8.0",
"cocotb-bus>=0.2.1", "cocotb-bus>=0.2.1",
] ]
tools = ["pyrefly>=0.37.0", "ruff>=0.14.0"] tools = ["ty>=0.0.7", "ruff>=0.14.0"]
[project.entry-points."peakrdl.exporters"] [project.entry-points."peakrdl.exporters"]
busdecoder = "peakrdl_busdecoder.__peakrdl__:Exporter" busdecoder = "peakrdl_busdecoder.__peakrdl__:Exporter"
@@ -97,15 +99,12 @@ ignore = [
quote-style = "double" quote-style = "double"
indent-style = "space" indent-style = "space"
# ---------------------- PYREFLY ---------------------- # ---------------------- TY ----------------------
[tool.pyrefly] [tool.ty.environment]
python-version = "3.10" python-version = "3.10"
# Default behavior: check bodies of untyped defs & infer return types. [tool.ty.src]
untyped-def-behavior = "check-and-infer-return-type" include = ["src"]
project-includes = ["src/**/*"]
project-excludes = ["**/__pycache__", "**/*venv/**/*"]
# ---------------------- PYTEST ---------------------- # ---------------------- PYTEST ----------------------
[tool.pytest.ini_options] [tool.pytest.ini_options]
@@ -114,3 +113,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

@@ -1,3 +1,5 @@
from __future__ import annotations
import functools import functools
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
@@ -5,7 +7,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 +26,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,
} }
@@ -69,7 +72,7 @@ class Exporter(ExporterSubcommandPlugin):
def get_cpuifs(self) -> dict[str, type[BaseCpuif]]: def get_cpuifs(self) -> dict[str, type[BaseCpuif]]:
return get_cpuifs(map(tuple, self.cfg["cpuifs"].items())) return get_cpuifs(map(tuple, self.cfg["cpuifs"].items()))
def add_exporter_arguments(self, arg_group: "argparse.ArgumentParser") -> None: # type: ignore def add_exporter_arguments(self, arg_group: argparse._ActionsContainer) -> None:
cpuifs = self.get_cpuifs() cpuifs = self.get_cpuifs()
arg_group.add_argument( arg_group.add_argument(
@@ -122,7 +125,7 @@ class Exporter(ExporterSubcommandPlugin):
""", """,
) )
def do_export(self, top_node: "AddrmapNode", options: "argparse.Namespace") -> None: def do_export(self, top_node: AddrmapNode, options: argparse.Namespace) -> None:
cpuifs = self.get_cpuifs() cpuifs = self.get_cpuifs()
x = BusDecoderExporter() x = BusDecoderExporter()

View File

@@ -1,3 +1,4 @@
from collections import deque
from typing import TYPE_CHECKING, overload from typing import TYPE_CHECKING, overload
from systemrdl.node import AddressableNode from systemrdl.node import AddressableNode
@@ -32,7 +33,7 @@ class APB3Cpuif(BaseCpuif):
def signal(self, signal: str, node: AddressableNode | None = None, indexer: str | None = None) -> str: def signal(self, signal: str, node: AddressableNode | None = None, indexer: str | None = None) -> str:
return self._interface.signal(signal, node, indexer) return self._interface.signal(signal, node, indexer)
def fanout(self, node: AddressableNode) -> str: def fanout(self, node: AddressableNode, array_stack: deque[int]) -> str:
fanout: dict[str, str] = {} fanout: dict[str, str] = {}
fanout[self.signal("PSEL", node, "gi")] = ( 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')}" 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')}"
@@ -44,13 +45,38 @@ class APB3Cpuif(BaseCpuif):
fanout[self.signal("PADDR", node, "gi")] = self.signal("PADDR") fanout[self.signal("PADDR", node, "gi")] = self.signal("PADDR")
fanout[self.signal("PWDATA", node, "gi")] = "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(f"assign {kv[0]} = {kv[1]};" for kv in fanout.items())
def fanin(self, node: AddressableNode | None = None) -> str: def fanin_wr(self, node: AddressableNode | None = None, *, error: bool = False) -> str:
fanin: dict[str, str] = {}
if node is None:
fanin["cpuif_wr_ack"] = "'0"
fanin["cpuif_wr_err"] = "'0"
if error:
fanin["cpuif_wr_ack"] = "'1"
fanin["cpuif_wr_err"] = "cpuif_wr_sel.cpuif_err"
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_wr_ack"] = f"{node.inst_name}_fanin_ready{array_idx}"
fanin["cpuif_wr_err"] = f"{node.inst_name}_fanin_err{array_idx}"
else:
fanin["cpuif_wr_ack"] = self.signal("PREADY", node, "i")
fanin["cpuif_wr_err"] = self.signal("PSLVERR", node, "i")
return "\n".join(f"{kv[0]} = {kv[1]};" for kv in fanin.items())
def fanin_rd(self, node: AddressableNode | None = None, *, error: bool = False) -> str:
fanin: dict[str, str] = {} fanin: dict[str, str] = {}
if node is None: if node is None:
fanin["cpuif_rd_ack"] = "'0" fanin["cpuif_rd_ack"] = "'0"
fanin["cpuif_rd_err"] = "'0" fanin["cpuif_rd_err"] = "'0"
fanin["cpuif_rd_data"] = "'0"
if error:
fanin["cpuif_rd_ack"] = "'1"
fanin["cpuif_rd_err"] = "cpuif_rd_sel.cpuif_err"
else: else:
# Use intermediate signals for interface arrays to avoid # Use intermediate signals for interface arrays to avoid
# non-constant indexing of interface arrays in procedural blocks # non-constant indexing of interface arrays in procedural blocks
@@ -59,27 +85,13 @@ class APB3Cpuif(BaseCpuif):
array_idx = "".join(f"[i{i}]" for i in range(len(node.array_dimensions))) 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_ack"] = f"{node.inst_name}_fanin_ready{array_idx}"
fanin["cpuif_rd_err"] = f"{node.inst_name}_fanin_err{array_idx}" fanin["cpuif_rd_err"] = f"{node.inst_name}_fanin_err{array_idx}"
fanin["cpuif_rd_data"] = f"{node.inst_name}_fanin_data{array_idx}"
else: else:
fanin["cpuif_rd_ack"] = self.signal("PREADY", node, "i") fanin["cpuif_rd_ack"] = self.signal("PREADY", node, "i")
fanin["cpuif_rd_err"] = self.signal("PSLVERR", node, "i") fanin["cpuif_rd_err"] = self.signal("PSLVERR", node, "i")
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") 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(f"{kv[0]} = {kv[1]};" for kv in fanin.items())
def fanin_intermediate_assignments( def fanin_intermediate_assignments(
self, node: AddressableNode, inst_name: str, array_idx: str, master_prefix: str, indexed_path: str self, node: AddressableNode, inst_name: str, array_idx: str, master_prefix: str, indexed_path: str

View File

@@ -1,8 +1,10 @@
from collections import deque
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 from ...sv_int import SVInt
from ...utils import clog2, get_indexed_path
from ..base_cpuif import BaseCpuif from ..base_cpuif import BaseCpuif
from .apb3_interface import APB3FlatInterface from .apb3_interface import APB3FlatInterface
@@ -33,36 +35,50 @@ class APB3CpuifFlat(BaseCpuif):
) -> str: ) -> str:
return self._interface.signal(signal, node, idx) return self._interface.signal(signal, node, idx)
def fanout(self, node: AddressableNode) -> str: def fanout(self, node: AddressableNode, array_stack: deque[int]) -> str:
fanout: dict[str, str] = {} fanout: dict[str, str] = {}
fanout[self.signal("PSEL", node)] = ( addr_comp = [f"{self.signal('PADDR')}"]
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')}" for i, stride in enumerate(array_stack):
) addr_comp.append(f"(gi{i}*{SVInt(stride, self.addr_width)})")
fanout[self.signal("PENABLE", node)] = self.signal("PENABLE")
fanout[self.signal("PWRITE", node)] = (
f"cpuif_wr_sel.{get_indexed_path(self.exp.ds.top_node, node, 'i')}"
)
fanout[self.signal("PADDR", node)] = self.signal("PADDR")
fanout[self.signal("PWDATA", node)] = "cpuif_wr_data"
return "\n".join(map(lambda kv: f"assign {kv[0]} = {kv[1]};", fanout.items())) 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")] = f"{{{'-'.join(addr_comp)}}}[{clog2(node.size) - 1}:0]"
fanout[self.signal("PWDATA", node, "gi")] = "cpuif_wr_data"
def fanin(self, node: AddressableNode | None = None) -> str: return "\n".join(f"assign {kv[0]} = {kv[1]};" for kv in fanout.items())
def fanin_wr(self, node: AddressableNode | None = None, *, error: bool = False) -> str:
fanin: dict[str, str] = {}
if node is None:
fanin["cpuif_wr_ack"] = "'0"
fanin["cpuif_wr_err"] = "'0"
if error:
fanin["cpuif_wr_ack"] = "'1"
fanin["cpuif_wr_err"] = "cpuif_wr_sel.cpuif_err"
else:
fanin["cpuif_wr_ack"] = self.signal("PREADY", node, "i")
fanin["cpuif_wr_err"] = self.signal("PSLVERR", node, "i")
return "\n".join(f"{kv[0]} = {kv[1]};" for kv in fanin.items())
def fanin_rd(self, node: AddressableNode | None = None, *, error: bool = False) -> str:
fanin: dict[str, str] = {} fanin: dict[str, str] = {}
if node is None: if node is None:
fanin["cpuif_rd_ack"] = "'0" fanin["cpuif_rd_ack"] = "'0"
fanin["cpuif_rd_err"] = "'0" fanin["cpuif_rd_err"] = "'0"
else:
fanin["cpuif_rd_ack"] = self.signal("PREADY", node)
fanin["cpuif_rd_err"] = self.signal("PSLVERR", node)
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" fanin["cpuif_rd_data"] = "'0"
if error:
fanin["cpuif_rd_ack"] = "'1"
fanin["cpuif_rd_err"] = "cpuif_rd_sel.cpuif_err"
else: else:
fanin["cpuif_rd_data"] = self.signal("PRDATA", node) fanin["cpuif_rd_ack"] = self.signal("PREADY", node, "i")
fanin["cpuif_rd_err"] = self.signal("PSLVERR", node, "i")
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(f"{kv[0]} = {kv[1]};" for kv in 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
@@ -48,7 +49,7 @@ class APB3FlatInterface(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 [{self.cpuif.data_width - 1}:0] {self.signal('PWDATA', child)}", f"output logic [{self.cpuif.data_width - 1}:0] {self.signal('PWDATA', child)}",
f"input logic [{self.cpuif.data_width - 1}:0] {self.signal('PRDATA', child)}", f"input logic [{self.cpuif.data_width - 1}:0] {self.signal('PRDATA', child)}",
f"input logic {self.signal('PREADY', child)}", f"input logic {self.signal('PREADY', child)}",

View File

@@ -19,8 +19,8 @@ assign cpuif_rd_addr = {{cpuif.signal("PADDR")}};
assign cpuif_wr_data = {{cpuif.signal("PWDATA")}}; assign cpuif_wr_data = {{cpuif.signal("PWDATA")}};
assign {{cpuif.signal("PRDATA")}} = cpuif_rd_data; assign {{cpuif.signal("PRDATA")}} = cpuif_rd_data;
assign {{cpuif.signal("PREADY")}} = cpuif_rd_ack; assign {{cpuif.signal("PREADY")}} = cpuif_rd_ack | cpuif_wr_ack;
assign {{cpuif.signal("PSLVERR")}} = cpuif_rd_err | cpuif_rd_sel.cpuif_err | cpuif_wr_sel.cpuif_err; assign {{cpuif.signal("PSLVERR")}} = cpuif_rd_err | cpuif_wr_err;
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// Fanout CPU Bus interface signals // Fanout CPU Bus interface signals
@@ -37,4 +37,4 @@ assign {{cpuif.signal("PSLVERR")}} = cpuif_rd_err | cpuif_rd_sel.cpuif_err | cpu
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// Fanin CPU Bus interface signals // Fanin CPU Bus interface signals
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
{{fanin|walk(cpuif=cpuif)}} {{fanin|walk(cpuif=cpuif)}}

View File

@@ -1,3 +1,4 @@
from collections import deque
from typing import TYPE_CHECKING, overload from typing import TYPE_CHECKING, overload
from systemrdl.node import AddressableNode from systemrdl.node import AddressableNode
@@ -33,7 +34,7 @@ class APB4Cpuif(BaseCpuif):
def signal(self, signal: str, node: AddressableNode | None = None, indexer: str | None = None) -> str: def signal(self, signal: str, node: AddressableNode | None = None, indexer: str | None = None) -> str:
return self._interface.signal(signal, node, indexer) return self._interface.signal(signal, node, indexer)
def fanout(self, node: AddressableNode) -> str: def fanout(self, node: AddressableNode, array_stack: deque[int]) -> str:
fanout: dict[str, str] = {} fanout: dict[str, str] = {}
fanout[self.signal("PSEL", node, "gi")] = ( 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')}" 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')}"
@@ -47,13 +48,39 @@ class APB4Cpuif(BaseCpuif):
fanout[self.signal("PWDATA", node, "gi")] = "cpuif_wr_data" fanout[self.signal("PWDATA", node, "gi")] = "cpuif_wr_data"
fanout[self.signal("PSTRB", node, "gi")] = "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(f"assign {kv[0]} = {kv[1]};" for kv in fanout.items())
def fanin(self, node: AddressableNode | None = None) -> str: def fanin_wr(self, node: AddressableNode | None = None, *, error: bool = False) -> str:
fanin: dict[str, str] = {}
if node is None:
fanin["cpuif_wr_ack"] = "'0"
fanin["cpuif_wr_err"] = "'0"
if error:
fanin["cpuif_wr_ack"] = "'1"
fanin["cpuif_wr_err"] = "cpuif_wr_sel.cpuif_err"
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_wr_ack"] = f"{node.inst_name}_fanin_ready{array_idx}"
fanin["cpuif_wr_err"] = f"{node.inst_name}_fanin_err{array_idx}"
else:
fanin["cpuif_wr_ack"] = self.signal("PREADY", node, "i")
fanin["cpuif_wr_err"] = self.signal("PSLVERR", node, "i")
return "\n".join(f"{kv[0]} = {kv[1]};" for kv in fanin.items())
def fanin_rd(self, node: AddressableNode | None = None, *, error: bool = False) -> str:
fanin: dict[str, str] = {} fanin: dict[str, str] = {}
if node is None: if node is None:
fanin["cpuif_rd_ack"] = "'0" fanin["cpuif_rd_ack"] = "'0"
fanin["cpuif_rd_err"] = "'0" fanin["cpuif_rd_err"] = "'0"
fanin["cpuif_rd_data"] = "'0"
if error:
fanin["cpuif_rd_ack"] = "'1"
fanin["cpuif_rd_err"] = "cpuif_rd_sel.cpuif_err"
else: else:
# Use intermediate signals for interface arrays to avoid # Use intermediate signals for interface arrays to avoid
# non-constant indexing of interface arrays in procedural blocks # non-constant indexing of interface arrays in procedural blocks
@@ -62,27 +89,13 @@ class APB4Cpuif(BaseCpuif):
array_idx = "".join(f"[i{i}]" for i in range(len(node.array_dimensions))) 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_ack"] = f"{node.inst_name}_fanin_ready{array_idx}"
fanin["cpuif_rd_err"] = f"{node.inst_name}_fanin_err{array_idx}" fanin["cpuif_rd_err"] = f"{node.inst_name}_fanin_err{array_idx}"
fanin["cpuif_rd_data"] = f"{node.inst_name}_fanin_data{array_idx}"
else: else:
fanin["cpuif_rd_ack"] = self.signal("PREADY", node, "i") fanin["cpuif_rd_ack"] = self.signal("PREADY", node, "i")
fanin["cpuif_rd_err"] = self.signal("PSLVERR", node, "i") fanin["cpuif_rd_err"] = self.signal("PSLVERR", node, "i")
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") 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(f"{kv[0]} = {kv[1]};" for kv in fanin.items())
def fanin_intermediate_assignments( def fanin_intermediate_assignments(
self, node: AddressableNode, inst_name: str, array_idx: str, master_prefix: str, indexed_path: str self, node: AddressableNode, inst_name: str, array_idx: str, master_prefix: str, indexed_path: str

View File

@@ -1,8 +1,10 @@
from collections import deque
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 from ...sv_int import SVInt
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
@@ -33,38 +35,51 @@ class APB4CpuifFlat(BaseCpuif):
) -> str: ) -> str:
return self._interface.signal(signal, node, idx) return self._interface.signal(signal, node, idx)
def fanout(self, node: AddressableNode) -> str: def fanout(self, node: AddressableNode, array_stack: deque[int]) -> str:
fanout: dict[str, str] = {} fanout: dict[str, str] = {}
fanout[self.signal("PSEL", node)] = ( addr_comp = [f"{self.signal('PADDR')}"]
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')}" for i, stride in enumerate(array_stack):
) addr_comp.append(f"(gi{i}*{SVInt(stride, self.addr_width)})")
fanout[self.signal("PENABLE", node)] = self.signal("PENABLE")
fanout[self.signal("PWRITE", node)] = (
f"cpuif_wr_sel.{get_indexed_path(self.exp.ds.top_node, node, 'i')}"
)
fanout[self.signal("PADDR", node)] = self.signal("PADDR")
fanout[self.signal("PPROT", node)] = self.signal("PPROT")
fanout[self.signal("PWDATA", node)] = "cpuif_wr_data"
fanout[self.signal("PSTRB", node)] = "cpuif_wr_byte_en"
return "\n".join(map(lambda kv: f"assign {kv[0]} = {kv[1]};", fanout.items())) 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")] = f"{{{'-'.join(addr_comp)}}}[{clog2(node.size) - 1}:0]"
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"
def fanin(self, node: AddressableNode | None = None) -> str: return "\n".join(f"assign {kv[0]} = {kv[1]};" for kv in fanout.items())
def fanin_wr(self, node: AddressableNode | None = None, *, error: bool = False) -> str:
fanin: dict[str, str] = {}
if node is None:
fanin["cpuif_wr_ack"] = "'0"
fanin["cpuif_wr_err"] = "'0"
if error:
fanin["cpuif_wr_ack"] = "'1"
fanin["cpuif_wr_err"] = "cpuif_wr_sel.cpuif_err"
else:
fanin["cpuif_wr_ack"] = self.signal("PREADY", node, "i")
fanin["cpuif_wr_err"] = self.signal("PSLVERR", node, "i")
return "\n".join(f"{kv[0]} = {kv[1]};" for kv in fanin.items())
def fanin_rd(self, node: AddressableNode | None = None, *, error: bool = False) -> str:
fanin: dict[str, str] = {} fanin: dict[str, str] = {}
if node is None: if node is None:
fanin["cpuif_rd_ack"] = "'0" fanin["cpuif_rd_ack"] = "'0"
fanin["cpuif_rd_err"] = "'0" fanin["cpuif_rd_err"] = "'0"
else:
fanin["cpuif_rd_ack"] = self.signal("PREADY", node)
fanin["cpuif_rd_err"] = self.signal("PSLVERR", node)
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" fanin["cpuif_rd_data"] = "'0"
if error:
fanin["cpuif_rd_ack"] = "'1"
fanin["cpuif_rd_err"] = "cpuif_rd_sel.cpuif_err"
else: else:
fanin["cpuif_rd_data"] = self.signal("PRDATA", node) fanin["cpuif_rd_ack"] = self.signal("PREADY", node, "i")
fanin["cpuif_rd_err"] = self.signal("PSLVERR", node, "i")
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(f"{kv[0]} = {kv[1]};" for kv in 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,6 @@
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
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 %} {%- endif %}
@@ -22,8 +20,8 @@ assign cpuif_wr_data = {{cpuif.signal("PWDATA")}};
assign cpuif_wr_byte_en = {{cpuif.signal("PSTRB")}}; assign cpuif_wr_byte_en = {{cpuif.signal("PSTRB")}};
assign {{cpuif.signal("PRDATA")}} = cpuif_rd_data; assign {{cpuif.signal("PRDATA")}} = cpuif_rd_data;
assign {{cpuif.signal("PREADY")}} = cpuif_rd_ack; assign {{cpuif.signal("PREADY")}} = cpuif_rd_ack | cpuif_wr_ack;
assign {{cpuif.signal("PSLVERR")}} = cpuif_rd_err | cpuif_rd_sel.cpuif_err | cpuif_wr_sel.cpuif_err; assign {{cpuif.signal("PSLVERR")}} = cpuif_rd_err | cpuif_wr_err;
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// Fanout CPU Bus interface signals // Fanout CPU Bus interface signals
@@ -40,4 +38,4 @@ assign {{cpuif.signal("PSLVERR")}} = cpuif_rd_err | cpuif_rd_sel.cpuif_err | cpu
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// Fanin CPU Bus interface signals // Fanin CPU Bus interface signals
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
{{fanin|walk(cpuif=cpuif)}} {{fanin|walk(cpuif=cpuif)}}

View File

@@ -1,3 +1,4 @@
from collections import deque
from typing import TYPE_CHECKING, overload from typing import TYPE_CHECKING, overload
from systemrdl.node import AddressableNode from systemrdl.node import AddressableNode
@@ -33,7 +34,7 @@ class AXI4LiteCpuif(BaseCpuif):
def signal(self, signal: str, node: AddressableNode | None = None, indexer: str | None = None) -> str: def signal(self, signal: str, node: AddressableNode | None = None, indexer: str | None = None) -> str:
return self._interface.signal(signal, node, indexer) return self._interface.signal(signal, node, indexer)
def fanout(self, node: AddressableNode) -> str: def fanout(self, node: AddressableNode, array_stack: deque[int]) -> str:
fanout: dict[str, str] = {} fanout: dict[str, str] = {}
wr_sel = f"cpuif_wr_sel.{get_indexed_path(self.exp.ds.top_node, node, 'gi')}" wr_sel = f"cpuif_wr_sel.{get_indexed_path(self.exp.ds.top_node, node, 'gi')}"
@@ -62,11 +63,38 @@ class AXI4LiteCpuif(BaseCpuif):
return "\n".join(f"assign {lhs} = {rhs};" for lhs, rhs in fanout.items()) return "\n".join(f"assign {lhs} = {rhs};" for lhs, rhs in fanout.items())
def fanin(self, node: AddressableNode | None = None) -> str: def fanin_wr(self, node: AddressableNode | None = None, *, error: bool = False) -> str:
fanin: dict[str, str] = {}
if node is None:
fanin["cpuif_wr_ack"] = "'0"
fanin["cpuif_wr_err"] = "'0"
if error:
fanin["cpuif_wr_ack"] = "'1"
fanin["cpuif_wr_err"] = "cpuif_wr_sel.cpuif_err"
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_wr_ack"] = f"{node.inst_name}_fanin_wr_valid{array_idx}"
fanin["cpuif_wr_err"] = f"{node.inst_name}_fanin_wr_err{array_idx}"
else:
# Read side: ack comes from RVALID; err if RRESP[1] is set (SLVERR/DECERR)
fanin["cpuif_wr_ack"] = self.signal("BVALID", node, "i")
fanin["cpuif_wr_err"] = f"{self.signal('BRESP', node, 'i')}[1]"
return "\n".join(f"{lhs} = {rhs};" for lhs, rhs in fanin.items())
def fanin_rd(self, node: AddressableNode | None = None, *, error: bool = False) -> str:
fanin: dict[str, str] = {} fanin: dict[str, str] = {}
if node is None: if node is None:
fanin["cpuif_rd_ack"] = "'0" fanin["cpuif_rd_ack"] = "'0"
fanin["cpuif_rd_err"] = "'0" fanin["cpuif_rd_err"] = "'0"
fanin["cpuif_rd_data"] = "'0"
if error:
fanin["cpuif_rd_ack"] = "'1"
fanin["cpuif_rd_err"] = "cpuif_rd_sel.cpuif_err"
else: else:
# Use intermediate signals for interface arrays to avoid # Use intermediate signals for interface arrays to avoid
# non-constant indexing of interface arrays in procedural blocks # non-constant indexing of interface arrays in procedural blocks
@@ -75,25 +103,10 @@ class AXI4LiteCpuif(BaseCpuif):
array_idx = "".join(f"[i{i}]" for i in range(len(node.array_dimensions))) 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_ack"] = f"{node.inst_name}_fanin_ready{array_idx}"
fanin["cpuif_rd_err"] = f"{node.inst_name}_fanin_err{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())
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}" fanin["cpuif_rd_data"] = f"{node.inst_name}_fanin_data{array_idx}"
else: else:
fanin["cpuif_rd_ack"] = self.signal("RVALID", node, "i")
fanin["cpuif_rd_err"] = f"{self.signal('RRESP', node, 'i')}[1]"
fanin["cpuif_rd_data"] = self.signal("RDATA", node, "i") 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())
@@ -106,4 +119,16 @@ class AXI4LiteCpuif(BaseCpuif):
f"assign {inst_name}_fanin_ready{array_idx} = {master_prefix}{indexed_path}.RVALID;", 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_err{array_idx} = {master_prefix}{indexed_path}.RRESP[1];",
f"assign {inst_name}_fanin_data{array_idx} = {master_prefix}{indexed_path}.RDATA;", f"assign {inst_name}_fanin_data{array_idx} = {master_prefix}{indexed_path}.RDATA;",
f"assign {inst_name}_fanin_wr_valid{array_idx} = {master_prefix}{indexed_path}.BVALID;",
f"assign {inst_name}_fanin_wr_err{array_idx} = {master_prefix}{indexed_path}.BRESP[1];",
]
def fanin_intermediate_declarations(self, node: AddressableNode) -> list[str]:
if not node.array_dimensions:
return []
array_str = "".join(f"[{dim}]" for dim in node.array_dimensions)
return [
f"logic {node.inst_name}_fanin_wr_valid{array_str};",
f"logic {node.inst_name}_fanin_wr_err{array_str};",
] ]

View File

@@ -1,8 +1,10 @@
from collections import deque
from typing import TYPE_CHECKING, overload from typing import TYPE_CHECKING, overload
from systemrdl.node import AddressableNode from systemrdl.node import AddressableNode
from ...utils import get_indexed_path from ...sv_int import SVInt
from ...utils import clog2, get_indexed_path
from ..base_cpuif import BaseCpuif from ..base_cpuif import BaseCpuif
from .axi4_lite_interface import AXI4LiteFlatInterface from .axi4_lite_interface import AXI4LiteFlatInterface
@@ -35,15 +37,21 @@ class AXI4LiteCpuifFlat(BaseCpuif):
def signal(self, signal: str, node: AddressableNode | None = None, indexer: str | None = None) -> str: def signal(self, signal: str, node: AddressableNode | None = None, indexer: str | None = None) -> str:
return self._interface.signal(signal, node, indexer) return self._interface.signal(signal, node, indexer)
def fanout(self, node: AddressableNode) -> str: def fanout(self, node: AddressableNode, array_stack: deque[int]) -> str:
fanout: dict[str, str] = {} fanout: dict[str, str] = {}
waddr_comp = [f"{self.signal('AWADDR')}"]
raddr_comp = [f"{self.signal('ARADDR')}"]
for i, stride in enumerate(array_stack):
offset = f"(gi{i}*{SVInt(stride, self.addr_width)})"
waddr_comp.append(offset)
raddr_comp.append(offset)
wr_sel = f"cpuif_wr_sel.{get_indexed_path(self.exp.ds.top_node, node, 'gi')}" wr_sel = f"cpuif_wr_sel.{get_indexed_path(self.exp.ds.top_node, node, 'gi')}"
rd_sel = f"cpuif_rd_sel.{get_indexed_path(self.exp.ds.top_node, node, 'gi')}" rd_sel = f"cpuif_rd_sel.{get_indexed_path(self.exp.ds.top_node, node, 'gi')}"
# Write address channel # Write address channel
fanout[self.signal("AWVALID", node, "gi")] = wr_sel fanout[self.signal("AWVALID", node, "gi")] = wr_sel
fanout[self.signal("AWADDR", node, "gi")] = self.signal("AWADDR") fanout[self.signal("AWADDR", node, "gi")] = f"{{{'-'.join(waddr_comp)}}}[{clog2(node.size) - 1}:0]"
fanout[self.signal("AWPROT", node, "gi")] = self.signal("AWPROT") fanout[self.signal("AWPROT", node, "gi")] = self.signal("AWPROT")
# Write data channel # Write data channel
@@ -56,7 +64,7 @@ class AXI4LiteCpuifFlat(BaseCpuif):
# Read address channel # Read address channel
fanout[self.signal("ARVALID", node, "gi")] = rd_sel fanout[self.signal("ARVALID", node, "gi")] = rd_sel
fanout[self.signal("ARADDR", node, "gi")] = self.signal("ARADDR") fanout[self.signal("ARADDR", node, "gi")] = f"{{{'-'.join(raddr_comp)}}}[{clog2(node.size) - 1}:0]"
fanout[self.signal("ARPROT", node, "gi")] = self.signal("ARPROT") fanout[self.signal("ARPROT", node, "gi")] = self.signal("ARPROT")
# Read data channel (master -> slave) # Read data channel (master -> slave)
@@ -64,23 +72,33 @@ class AXI4LiteCpuifFlat(BaseCpuif):
return "\n".join(f"assign {lhs} = {rhs};" for lhs, rhs in fanout.items()) return "\n".join(f"assign {lhs} = {rhs};" for lhs, rhs in fanout.items())
def fanin(self, node: AddressableNode | None = None) -> str: def fanin_wr(self, node: AddressableNode | None = None, *, error: bool = False) -> str:
fanin: dict[str, str] = {}
if node is None:
fanin["cpuif_wr_ack"] = "'0"
fanin["cpuif_wr_err"] = "'0"
if error:
fanin["cpuif_wr_ack"] = "'1"
fanin["cpuif_wr_err"] = "cpuif_wr_sel.cpuif_err"
else:
# Read side: ack comes from RVALID; err if RRESP[1] is set (SLVERR/DECERR)
fanin["cpuif_wr_ack"] = self.signal("BVALID", node, "i")
fanin["cpuif_wr_err"] = f"{self.signal('BRESP', node, 'i')}[1]"
return "\n".join(f"{lhs} = {rhs};" for lhs, rhs in fanin.items())
def fanin_rd(self, node: AddressableNode | None = None, *, error: bool = False) -> str:
fanin: dict[str, str] = {} fanin: dict[str, str] = {}
if node is None: if node is None:
fanin["cpuif_rd_ack"] = "'0" fanin["cpuif_rd_ack"] = "'0"
fanin["cpuif_rd_err"] = "'0" fanin["cpuif_rd_err"] = "'0"
fanin["cpuif_rd_data"] = "'0"
if error:
fanin["cpuif_rd_ack"] = "'1"
fanin["cpuif_rd_err"] = "cpuif_rd_sel.cpuif_err"
else: 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_ack"] = self.signal("RVALID", node, "i")
fanin["cpuif_rd_err"] = f"{self.signal('RRESP', node, 'i')}[1]" fanin["cpuif_rd_err"] = f"{self.signal('RRESP', node, 'i')}[1]"
return "\n".join(f"{lhs} = {rhs};" for lhs, rhs in fanin.items())
def readback(self, node: AddressableNode | None = None) -> str:
fanin: dict[str, str] = {}
if node is None:
fanin["cpuif_rd_data"] = "'0"
else:
fanin["cpuif_rd_data"] = self.signal("RDATA", node, "i") 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())

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
@@ -60,7 +61,7 @@ class AXI4LiteFlatInterface(FlatInterface):
# Write address channel # Write address channel
f"output logic {self.signal('AWVALID', child)}", f"output logic {self.signal('AWVALID', child)}",
f"input logic {self.signal('AWREADY', child)}", f"input logic {self.signal('AWREADY', child)}",
f"output logic [{self.cpuif.addr_width - 1}:0] {self.signal('AWADDR', child)}", f"output logic [{clog2(child.size) - 1}:0] {self.signal('AWADDR', child)}",
f"output logic [2:0] {self.signal('AWPROT', child)}", f"output logic [2:0] {self.signal('AWPROT', child)}",
# Write data channel # Write data channel
f"output logic {self.signal('WVALID', child)}", f"output logic {self.signal('WVALID', child)}",
@@ -74,7 +75,7 @@ class AXI4LiteFlatInterface(FlatInterface):
# Read address channel # Read address channel
f"output logic {self.signal('ARVALID', child)}", f"output logic {self.signal('ARVALID', child)}",
f"input logic {self.signal('ARREADY', child)}", f"input logic {self.signal('ARREADY', child)}",
f"output logic [{self.cpuif.addr_width - 1}:0] {self.signal('ARADDR', child)}", f"output logic [{clog2(child.size) - 1}:0] {self.signal('ARADDR', child)}",
f"output logic [2:0] {self.signal('ARPROT', child)}", f"output logic [2:0] {self.signal('ARPROT', child)}",
# Read data channel # Read data channel
f"input logic {self.signal('RVALID', child)}", f"input logic {self.signal('RVALID', child)}",

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,12 +24,25 @@
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 -%}
logic axi_wr_valid;
logic axi_wr_invalid;
logic cpuif_wr_ack_int;
logic cpuif_rd_ack_int;
assign axi_wr_valid = {{cpuif.signal("AWVALID")}} & {{cpuif.signal("WVALID")}};
assign axi_wr_invalid = {{cpuif.signal("AWVALID")}} ^ {{cpuif.signal("WVALID")}};
// Ready/acceptance follows the simplified single-beat requirement
assign {{cpuif.signal("AWREADY")}} = axi_wr_valid;
assign {{cpuif.signal("WREADY")}} = axi_wr_valid;
assign {{cpuif.signal("ARREADY")}} = {{cpuif.signal("ARVALID")}};
assign cpuif_req = {{cpuif.signal("AWVALID")}} | {{cpuif.signal("ARVALID")}}; assign cpuif_req = {{cpuif.signal("AWVALID")}} | {{cpuif.signal("ARVALID")}};
assign cpuif_wr_en = {{cpuif.signal("AWVALID")}} & {{cpuif.signal("WVALID")}}; assign cpuif_wr_en = axi_wr_valid;
assign cpuif_rd_en = {{cpuif.signal("ARVALID")}}; assign cpuif_rd_en = {{cpuif.signal("ARVALID")}};
assign cpuif_wr_addr = {{cpuif.signal("AWADDR")}}; assign cpuif_wr_addr = {{cpuif.signal("AWADDR")}};
@@ -42,12 +56,14 @@ assign cpuif_wr_byte_en = {{cpuif.signal("WSTRB")}};
// Read: ack=RVALID, err=RRESP[1] (SLVERR/DECERR), data=RDATA // Read: ack=RVALID, err=RRESP[1] (SLVERR/DECERR), data=RDATA
// //
assign {{cpuif.signal("RDATA")}} = cpuif_rd_data; assign {{cpuif.signal("RDATA")}} = cpuif_rd_data;
assign {{cpuif.signal("RVALID")}} = cpuif_rd_ack; assign cpuif_rd_ack_int = cpuif_rd_ack | cpuif_rd_sel.cpuif_err;
assign {{cpuif.signal("RVALID")}} = cpuif_rd_ack_int;
assign {{cpuif.signal("RRESP")}} = (cpuif_rd_err | cpuif_rd_sel.cpuif_err | cpuif_wr_sel.cpuif_err) ? 2'b10 : 2'b00; assign {{cpuif.signal("RRESP")}} = (cpuif_rd_err | cpuif_rd_sel.cpuif_err | cpuif_wr_sel.cpuif_err) ? 2'b10 : 2'b00;
// Write: ack=BVALID, err=BRESP[1] // Write: ack=BVALID, err=BRESP[1]
assign {{cpuif.signal("BVALID")}} = cpuif_wr_ack; assign cpuif_wr_ack_int = cpuif_wr_ack | cpuif_wr_sel.cpuif_err | axi_wr_invalid;
assign {{cpuif.signal("BRESP")}} = (cpuif_wr_err | cpuif_wr_sel.cpuif_err | cpuif_rd_sel.cpuif_err) ? 2'b10 : 2'b00; assign {{cpuif.signal("BVALID")}} = cpuif_wr_ack_int;
assign {{cpuif.signal("BRESP")}} = (cpuif_wr_err | cpuif_wr_sel.cpuif_err | cpuif_rd_sel.cpuif_err | axi_wr_invalid) ? 2'b10 : 2'b00;
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// Fanout CPU Bus interface signals // Fanout CPU Bus interface signals
@@ -64,4 +80,4 @@ assign {{cpuif.signal("BRESP")}} = (cpuif_wr_err | cpuif_wr_sel.cpuif_err | cpu
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// Fanin CPU Bus interface signals // Fanin CPU Bus interface signals
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
{{fanin|walk(cpuif=cpuif)}} {{fanin|walk(cpuif=cpuif)}}

View File

@@ -1,5 +1,6 @@
import inspect import inspect
import os import os
from collections import deque
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import jinja2 as jj import jinja2 as jj
@@ -81,15 +82,15 @@ class BaseCpuif:
loader=loader, loader=loader,
undefined=jj.StrictUndefined, undefined=jj.StrictUndefined,
) )
jj_env.tests["array"] = self.check_is_array # type: ignore jj_env.tests["array"] = self.check_is_array
jj_env.filters["clog2"] = clog2 # type: ignore jj_env.filters["clog2"] = clog2
jj_env.filters["is_pow2"] = is_pow2 # type: ignore jj_env.filters["is_pow2"] = is_pow2
jj_env.filters["roundup_pow2"] = roundup_pow2 # type: ignore jj_env.filters["roundup_pow2"] = roundup_pow2
jj_env.filters["address_slice"] = self.get_address_slice # type: ignore jj_env.filters["address_slice"] = self.get_address_slice
jj_env.filters["get_path"] = lambda x: get_indexed_path(self.exp.ds.top_node, x, "i") # type: ignore jj_env.filters["get_path"] = lambda x: get_indexed_path(self.exp.ds.top_node, x, "i")
jj_env.filters["walk"] = self.exp.walk # type: ignore jj_env.filters["walk"] = self.exp.walk
context = { # type: ignore context = {
"cpuif": self, "cpuif": self,
"ds": self.exp.ds, "ds": self.exp.ds,
"fanout": FanoutGenerator, "fanout": FanoutGenerator,
@@ -106,13 +107,13 @@ class BaseCpuif:
return f"({cpuif_addr} - 'h{addr:x})[{clog2(size) - 1}:0]" return f"({cpuif_addr} - 'h{addr:x})[{clog2(size) - 1}:0]"
def fanout(self, node: AddressableNode) -> str: def fanout(self, node: AddressableNode, array_stack: deque[int]) -> str:
raise NotImplementedError raise NotImplementedError
def fanin(self, node: AddressableNode | None = None) -> str: def fanin_wr(self, node: AddressableNode | None = None, *, error: bool = False) -> str:
raise NotImplementedError raise NotImplementedError
def readback(self, node: AddressableNode | None = None) -> str: def fanin_rd(self, node: AddressableNode | None = None, *, error: bool = False) -> str:
raise NotImplementedError raise NotImplementedError
def fanin_intermediate_assignments( def fanin_intermediate_assignments(
@@ -135,3 +136,7 @@ class BaseCpuif:
List of assignment strings List of assignment strings
""" """
return [] # Default: no intermediate assignments needed return [] # Default: no intermediate assignments needed
def fanin_intermediate_declarations(self, node: AddressableNode) -> list[str]:
"""Optional extra intermediate signal declarations for interface arrays."""
return []

View File

@@ -20,8 +20,8 @@ class FaninGenerator(BusDecoderListener):
self._stack: deque[Body] = deque() self._stack: deque[Body] = deque()
cb = CombinationalBody() cb = CombinationalBody()
cb += cpuif.fanin() cb += cpuif.fanin_wr()
cb += cpuif.readback() cb += cpuif.fanin_rd()
self._stack.append(cb) self._stack.append(cb)
def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None: def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None:
@@ -48,15 +48,13 @@ class FaninGenerator(BusDecoderListener):
self._stack.append(fb) self._stack.append(fb)
ifb = IfBody() ifb = IfBody()
with ifb.cm( with ifb.cm(f"cpuif_wr_sel.{get_indexed_path(self._cpuif.exp.ds.top_node, node)}") as b:
f"cpuif_rd_sel.{get_indexed_path(self._cpuif.exp.ds.top_node, node)} || cpuif_wr_sel.{get_indexed_path(self._cpuif.exp.ds.top_node, node)}" b += self._cpuif.fanin_wr(node)
) as b:
b += self._cpuif.fanin(node)
self._stack[-1] += ifb self._stack[-1] += ifb
ifb = IfBody() ifb = IfBody()
with ifb.cm(f"cpuif_rd_sel.{get_indexed_path(self._cpuif.exp.ds.top_node, node)}") as b: with ifb.cm(f"cpuif_rd_sel.{get_indexed_path(self._cpuif.exp.ds.top_node, node)}") as b:
b += self._cpuif.readback(node) b += self._cpuif.fanin_rd(node)
self._stack[-1] += ifb self._stack[-1] += ifb
return action return action
@@ -72,4 +70,14 @@ class FaninGenerator(BusDecoderListener):
super().exit_AddressableComponent(node) super().exit_AddressableComponent(node)
def __str__(self) -> str: def __str__(self) -> str:
wr_ifb = IfBody()
with wr_ifb.cm("cpuif_wr_sel.cpuif_err") as b:
b += self._cpuif.fanin_wr(error=True)
self._stack[-1] += wr_ifb
rd_ifb = IfBody()
with rd_ifb.cm("cpuif_rd_sel.cpuif_err") as b:
b += self._cpuif.fanin_rd(error=True)
self._stack[-1] += rd_ifb
return "\n".join(map(str, self._stack)) return "\n".join(map(str, self._stack))

View File

@@ -94,6 +94,9 @@ class FaninIntermediateGenerator(BusDecoderListener):
f"logic [{self._cpuif.data_width - 1}:0] {inst_name}_fanin_data{array_str};" f"logic [{self._cpuif.data_width - 1}:0] {inst_name}_fanin_data{array_str};"
) )
# Allow CPU interface to add extra intermediate declarations (e.g., write responses)
self._declarations.extend(self._cpuif.fanin_intermediate_declarations(node))
def _generate_intermediate_assignments(self, node: AddressableNode) -> str: def _generate_intermediate_assignments(self, node: AddressableNode) -> str:
"""Generate assignments from interface array to intermediate signals.""" """Generate assignments from interface array to intermediate signals."""
inst_name = node.inst_name inst_name = node.inst_name

View File

@@ -43,7 +43,7 @@ class FanoutGenerator(BusDecoderListener):
) )
self._stack.append(fb) self._stack.append(fb)
self._stack[-1] += self._cpuif.fanout(node) self._stack[-1] += self._cpuif.fanout(node, self._array_stride_stack)
return action return action

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,21 +64,24 @@ 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:
base = f"{base}_{'_'.join(map(str, child.current_idx))}" base = f"{base}_{'_'.join(map(str, child.current_idx))}" # ty: ignore
# Only add array dimensions if this should be treated as an array # Only add array dimensions if this should be treated as an array
if self.cpuif.check_is_array(child): if self.cpuif.check_is_array(child):
@@ -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,109 @@
from collections import deque
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, array_stack: deque[int]) -> 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_wr(self, node: AddressableNode | None = None, *, error: bool = False) -> 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 fanin_rd(self, node: AddressableNode | None = None, *, error: bool = False) -> str:
fanin: dict[str, str] = {}
if node is None:
fanin["cpuif_rd_ack"] = "'0"
fanin["cpuif_rd_err"] = "'0"
fanin["cpuif_rd_data"] = "'0"
if error:
fanin["cpuif_rd_ack"] = "'1"
fanin["cpuif_rd_err"] = "cpuif_rd_sel.cpuif_err"
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}"
fanin["cpuif_rd_data"] = f"{node.inst_name}_fanin_data{array_idx}"
else:
fanin["cpuif_rd_ack"] = self.signal("prdata", node, "i")
fanin["cpuif_rd_err"] = self.signal("pslverr", node, "i")
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,44 @@
{%- 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 | cpuif_wr_ack;
assign {{cpuif.signal("pslverr")}} = cpuif_rd_err | cpuif_wr_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

View File

@@ -1,5 +1,4 @@
import os import os
from datetime import datetime
from importlib.metadata import version from importlib.metadata import version
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Any, TypedDict from typing import TYPE_CHECKING, Any, TypedDict
@@ -59,9 +58,9 @@ class BusDecoderExporter:
loader=c_loader, loader=c_loader,
undefined=jj.StrictUndefined, undefined=jj.StrictUndefined,
) )
self.jj_env.filters["kwf"] = kwf # type: ignore self.jj_env.filters["kwf"] = kwf
self.jj_env.filters["walk"] = self.walk # type: ignore self.jj_env.filters["walk"] = self.walk
self.jj_env.filters["clog2"] = clog2 # type: ignore self.jj_env.filters["clog2"] = clog2
def export(self, node: RootNode | AddrmapNode, output_dir: str, **kwargs: Unpack[ExporterKwargs]) -> None: def export(self, node: RootNode | AddrmapNode, output_dir: str, **kwargs: Unpack[ExporterKwargs]) -> None:
""" """
@@ -99,7 +98,7 @@ class BusDecoderExporter:
else: else:
top_node = node top_node = node
self.ds = DesignState(top_node, kwargs) self.ds = DesignState(top_node, kwargs) # ty: ignore
cpuif_cls: type[BaseCpuif] = kwargs.pop("cpuif_cls", None) or APB4Cpuif cpuif_cls: type[BaseCpuif] = kwargs.pop("cpuif_cls", None) or APB4Cpuif
@@ -114,8 +113,7 @@ class BusDecoderExporter:
DesignValidator(self).do_validate() DesignValidator(self).do_validate()
# Build Jinja template context # Build Jinja template context
context = { # type: ignore context = {
"current_date": datetime.now().strftime("%Y-%m-%d"),
"version": version("peakrdl-busdecoder"), "version": version("peakrdl-busdecoder"),
"cpuif": self.cpuif, "cpuif": self.cpuif,
"cpuif_decode": DecodeLogicGenerator, "cpuif_decode": DecodeLogicGenerator,

View File

@@ -3,7 +3,6 @@
// Description: CPU Interface Bus Decoder // Description: CPU Interface Bus Decoder
// Author: PeakRDL-BusDecoder // Author: PeakRDL-BusDecoder
// License: LGPL-3.0 // License: LGPL-3.0
// Date: {{current_date}}
// Version: {{version}} // Version: {{version}}
// Links: // Links:
// - https://github.com/arnavsacheti/PeakRDL-BusDecoder // - https://github.com/arnavsacheti/PeakRDL-BusDecoder

View File

@@ -3,7 +3,6 @@
// Description: CPU Interface Bus Decoder Package // Description: CPU Interface Bus Decoder Package
// Author: PeakRDL-BusDecoder // Author: PeakRDL-BusDecoder
// License: LGPL-3.0 // License: LGPL-3.0
// Date: {{current_date}}
// Version: {{version}} // Version: {{version}}
// Links: // Links:
// - https://github.com/arnavsacheti/PeakRDL-BusDecoder // - https://github.com/arnavsacheti/PeakRDL-BusDecoder

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

@@ -62,7 +62,6 @@ def ref_is_internal(top_node: AddrmapNode, ref: Node | PropertyReference) -> boo
else: else:
current_node = ref current_node = ref
# pyrefly: ignore[bad-assignment] - false positive due to circular type checking
while current_node is not None: while current_node is not None:
if current_node == top_node: if current_node == top_node:
# reached top node without finding any external components # reached top node without finding any external components

View File

@@ -4,7 +4,7 @@ from systemrdl.node import AddressableNode, AddrmapNode, FieldNode, Node, Regfil
from systemrdl.rdltypes.references import PropertyReference from systemrdl.rdltypes.references import PropertyReference
from systemrdl.walker import RDLListener, RDLWalker, WalkerAction from systemrdl.walker import RDLListener, RDLWalker, WalkerAction
from .utils import is_pow2, ref_is_internal, roundup_pow2 from .utils import ref_is_internal
if TYPE_CHECKING: if TYPE_CHECKING:
from .exporter import BusDecoderExporter from .exporter import BusDecoderExporter
@@ -74,7 +74,9 @@ class DesignValidator(RDLListener):
f"instance '{node.inst_name}' must be a multiple of {alignment}", f"instance '{node.inst_name}' must be a multiple of {alignment}",
node.inst.inst_src_ref, node.inst.inst_src_ref,
) )
if node.is_array and (node.array_stride % alignment) != 0: # type: ignore # is_array implies stride is not none if node.is_array and (
node.array_stride is not None and (node.array_stride % alignment) != 0
): # is_array implies stride is not none
self.msg.error( self.msg.error(
"Unaligned registers are not supported. Address stride of " "Unaligned registers are not supported. Address stride of "
f"instance array '{node.inst_name}' must be a multiple of {alignment}", f"instance array '{node.inst_name}' must be a multiple of {alignment}",
@@ -159,27 +161,3 @@ class DesignValidator(RDLListener):
else: else:
# Exiting top addrmap. Resolve final answer # Exiting top addrmap. Resolve final answer
self.contains_external_block = contains_external_block self.contains_external_block = contains_external_block
if contains_external_block:
# Check that addressing follows strict alignment rules to allow
# for simplified address bit-pruning
if node.external:
err_suffix = "is external"
else:
err_suffix = "contains an external addrmap/regfile/mem"
req_align = roundup_pow2(node.size)
if (node.raw_address_offset % req_align) != 0:
self.msg.error(
f"Address offset +0x{node.raw_address_offset:x} of instance '{node.inst_name}' is not a power of 2 multiple of its size 0x{node.size:x}. "
f"This is required by the busdecoder exporter if a component {err_suffix}.",
node.inst.inst_src_ref,
)
if node.is_array:
assert node.array_stride is not None
if not is_pow2(node.array_stride):
self.msg.error(
f"Address stride of instance array '{node.inst_name}' is not a power of 2"
f"This is required by the busdecoder exporter if a component {err_suffix}.",
node.inst.inst_src_ref,
)

View File

@@ -1,4 +1,3 @@
from peakrdl_busdecoder.body import Body from peakrdl_busdecoder.body import Body
@@ -46,5 +45,3 @@ class TestBody:
outer += "outer2" outer += "outer2"
expected = "outer1\ninner1\ninner2\nouter2" expected = "outer1\ninner1\ninner2\nouter2"
assert str(outer) == expected assert str(outer) == expected

View File

@@ -1,4 +1,6 @@
from peakrdl_busdecoder.body import IfBody from peakrdl_busdecoder.body import IfBody
class TestIfBody: class TestIfBody:
"""Test the IfBody class.""" """Test the IfBody class."""
@@ -82,4 +84,3 @@ class TestIfBody:
assert "if (outer_cond)" in result assert "if (outer_cond)" in result
assert "if (inner_cond)" in result assert "if (inner_cond)" in result
assert "nested_statement;" in result assert "nested_statement;" in result

View File

@@ -1,17 +1,30 @@
"""APB3 smoke tests for generated multi-register design.""" """APB3 smoke tests generated from SystemRDL sources."""
from __future__ import annotations
from typing import Any
import cocotb import cocotb
from cocotb.triggers import Timer from cocotb.triggers import RisingEdge, Timer
WRITE_ADDR = 0x0 from tests.cocotb_lib.handle_utils import SignalHandle
READ_ADDR = 0x8 from tests.cocotb_lib.protocol_utils import (
WRITE_DATA = 0xCAFEBABE all_index_pairs,
READ_DATA = 0x0BAD_F00D find_invalid_address,
get_int,
load_config,
set_value,
start_clock,
)
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.PCLK = getattr(dut, f"{prefix}_PCLK", None)
self.PRESETn = getattr(dut, f"{prefix}_PRESETn", None)
self.PSEL = getattr(dut, f"{prefix}_PSEL") self.PSEL = getattr(dut, f"{prefix}_PSEL")
self.PENABLE = getattr(dut, f"{prefix}_PENABLE") self.PENABLE = getattr(dut, f"{prefix}_PENABLE")
self.PWRITE = getattr(dut, f"{prefix}_PWRITE") self.PWRITE = getattr(dut, f"{prefix}_PWRITE")
@@ -22,102 +35,276 @@ class _Apb3SlaveShim:
self.PSLVERR = getattr(dut, f"{prefix}_PSLVERR") self.PSLVERR = getattr(dut, f"{prefix}_PSLVERR")
class _Apb3MasterShim: def _build_master_table(dut, masters_cfg: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
def __init__(self, dut, base: str): table: dict[str, dict[str, Any]] = {}
self.PSEL = getattr(dut, f"{base}_PSEL") for master in masters_cfg:
self.PENABLE = getattr(dut, f"{base}_PENABLE") prefix = master["port_prefix"]
self.PWRITE = getattr(dut, f"{base}_PWRITE") entry = {
self.PADDR = getattr(dut, f"{base}_PADDR") "indices": [tuple(idx) for idx in master["indices"]] or [tuple()],
self.PWDATA = getattr(dut, f"{base}_PWDATA") "outputs": {
self.PRDATA = getattr(dut, f"{base}_PRDATA") "PSEL": SignalHandle(dut, f"{prefix}_PSEL"),
self.PREADY = getattr(dut, f"{base}_PREADY") "PENABLE": SignalHandle(dut, f"{prefix}_PENABLE"),
self.PSLVERR = getattr(dut, f"{base}_PSLVERR") "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"),
},
"inst_size": master["inst_size"],
"inst_address": master["inst_address"],
}
table[master["inst_name"]] = entry
return table
def _apb3_slave(dut): def _write_pattern(address: int, width: int) -> int:
return getattr(dut, "s_apb", None) or _Apb3SlaveShim(dut) mask = (1 << width) - 1
return ((address * 0x2041) ^ 0xCAFEBABE) & mask
def _apb3_master(dut, base: str): def _read_pattern(address: int, width: int) -> int:
return getattr(dut, base, None) or _Apb3MasterShim(dut, base) 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 await start_clock(slave.PCLK)
s_apb.PENABLE.value = 0 if slave.PRESETn is not None:
s_apb.PWRITE.value = 0 slave.PRESETn.value = 1
s_apb.PADDR.value = 0
s_apb.PWDATA.value = 0
for master in masters.values(): slave.PSEL.value = 0
master.PRDATA.value = 0 slave.PENABLE.value = 0
master.PREADY.value = 0 slave.PWRITE.value = 0
master.PSLVERR.value = 0 slave.PADDR.value = 0
slave.PWDATA.value = 0
await Timer(1, units="ns") for master_name, idx in all_index_pairs(masters):
entry = masters[master_name]
set_value(entry["inputs"]["PRDATA"], idx, 0)
set_value(entry["inputs"]["PREADY"], idx, 0)
set_value(entry["inputs"]["PSLVERR"], idx, 0)
# Write to reg1 if slave.PCLK is not None:
masters["reg1"].PREADY.value = 1 await RisingEdge(slave.PCLK)
s_apb.PADDR.value = WRITE_ADDR else:
s_apb.PWDATA.value = WRITE_DATA await Timer(1, unit="ns")
s_apb.PWRITE.value = 1
s_apb.PSEL.value = 1
s_apb.PENABLE.value = 1
await Timer(1, units="ns") addr_mask = (1 << config["address_width"]) - 1
assert int(masters["reg1"].PSEL.value) == 1, "reg1 should be selected for write" for txn in config["transactions"]:
assert int(masters["reg1"].PWRITE.value) == 1, "Write should propagate to master" master_name = txn["master"]
assert int(masters["reg1"].PADDR.value) == WRITE_ADDR, "Address should reach selected master" index = tuple(txn["index"])
assert int(masters["reg1"].PWDATA.value) == WRITE_DATA, "Write data should fan out" entry = masters[master_name]
for name, master in masters.items(): address = txn["address"] & addr_mask
if name != "reg1": write_data = _write_pattern(address, config["data_width"])
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" set_value(entry["inputs"]["PREADY"], index, 0)
assert int(s_apb.PSLVERR.value) == 0, "Write should not signal error" set_value(entry["inputs"]["PSLVERR"], index, 0)
s_apb.PSEL.value = 0 # ------------------------------------------------------------------
s_apb.PENABLE.value = 0 # Setup phase
s_apb.PWRITE.value = 0 # ------------------------------------------------------------------
masters["reg1"].PREADY.value = 0 slave.PADDR.value = address
await Timer(1, units="ns") slave.PWDATA.value = write_data
slave.PWRITE.value = 1
slave.PSEL.value = 1
slave.PENABLE.value = 0
# Read from reg3 dut._log.info(
masters["reg3"].PRDATA.value = READ_DATA f"Starting transaction {txn['label']} to {master_name}{index} at address 0x{address:08X}"
masters["reg3"].PREADY.value = 1 )
masters["reg3"].PSLVERR.value = 0 master_address = (address - entry["inst_address"]) % entry["inst_size"]
s_apb.PADDR.value = READ_ADDR if slave.PCLK is not None:
s_apb.PSEL.value = 1 await RisingEdge(slave.PCLK)
s_apb.PENABLE.value = 1 else:
s_apb.PWRITE.value = 0 await Timer(1, unit="ns")
await Timer(1, units="ns") assert get_int(entry["outputs"]["PSEL"], index) == 1, f"{master_name} should assert PSEL for write"
assert get_int(entry["outputs"]["PENABLE"], index) == 0, (
f"{master_name} must hold PENABLE low in setup"
)
assert get_int(entry["outputs"]["PWRITE"], index) == 1, f"{master_name} should see write direction"
assert get_int(entry["outputs"]["PADDR"], index) == master_address, (
f"{master_name} must receive write address"
)
assert get_int(entry["outputs"]["PWDATA"], index) == write_data, (
f"{master_name} must receive write data"
)
assert int(masters["reg3"].PSEL.value) == 1, "reg3 should be selected for read" for other_name, other_idx in all_index_pairs(masters):
assert int(masters["reg3"].PWRITE.value) == 0, "Read should clear write" if other_name == master_name and other_idx == index:
assert int(masters["reg3"].PADDR.value) == READ_ADDR, "Address should reach read target" continue
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']}"
)
for name, master in masters.items(): # ------------------------------------------------------------------
if name != "reg3": # Access phase
assert int(master.PSEL.value) == 0, f"{name} must idle during reg3 read" # ------------------------------------------------------------------
set_value(entry["inputs"]["PREADY"], index, 1)
slave.PENABLE.value = 1
assert int(s_apb.PRDATA.value) == READ_DATA, "Read data should return to slave" if slave.PCLK is not None:
assert int(s_apb.PREADY.value) == 1, "Read should acknowledge" await RisingEdge(slave.PCLK)
assert int(s_apb.PSLVERR.value) == 0, "Read should not signal error" else:
await Timer(1, unit="ns")
s_apb.PSEL.value = 0 assert get_int(entry["outputs"]["PSEL"], index) == 1, f"{master_name} must keep PSEL asserted"
s_apb.PENABLE.value = 0 assert get_int(entry["outputs"]["PENABLE"], index) == 1, (
masters["reg3"].PREADY.value = 0 f"{master_name} must assert PENABLE in access"
await Timer(1, units="ns") )
assert get_int(entry["outputs"]["PADDR"], index) == master_address, (
f"{master_name} must keep write address stable"
)
assert get_int(entry["outputs"]["PWDATA"], index) == write_data, (
f"{master_name} must keep write data stable"
)
assert int(slave.PREADY.value) == 1, "Slave ready should mirror selected master"
assert int(slave.PSLVERR.value) == 0, "Write should complete without error"
slave.PSEL.value = 0
slave.PENABLE.value = 0
slave.PWRITE.value = 0
set_value(entry["inputs"]["PREADY"], index, 0)
if slave.PCLK is not None:
await RisingEdge(slave.PCLK)
else:
await Timer(1, unit="ns")
# ------------------------------------------------------------------
# Read phase
# ------------------------------------------------------------------
read_data = _read_pattern(address, config["data_width"])
set_value(entry["inputs"]["PRDATA"], index, read_data)
set_value(entry["inputs"]["PREADY"], index, 0)
set_value(entry["inputs"]["PSLVERR"], index, 0)
# ------------------------------------------------------------------
# Setup phase
# ------------------------------------------------------------------
slave.PADDR.value = address
slave.PWRITE.value = 0
slave.PSEL.value = 1
slave.PENABLE.value = 0
if slave.PCLK is not None:
await RisingEdge(slave.PCLK)
else:
await Timer(1, unit="ns")
assert get_int(entry["outputs"]["PSEL"], index) == 1, f"{master_name} must assert PSEL for read"
assert get_int(entry["outputs"]["PENABLE"], index) == 0, (
f"{master_name} must hold PENABLE low in setup"
)
assert get_int(entry["outputs"]["PWRITE"], index) == 0, (
f"{master_name} should clear write during read"
)
assert get_int(entry["outputs"]["PADDR"], index) == master_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']}"
)
# ------------------------------------------------------------------
# Access phase
# ------------------------------------------------------------------
set_value(entry["inputs"]["PREADY"], index, 1)
slave.PENABLE.value = 1
if slave.PCLK is not None:
await RisingEdge(slave.PCLK)
else:
await Timer(1, unit="ns")
assert get_int(entry["outputs"]["PSEL"], index) == 1, f"{master_name} must keep PSEL asserted"
assert get_int(entry["outputs"]["PENABLE"], index) == 1, (
f"{master_name} must assert PENABLE in access"
)
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)
if slave.PCLK is not None:
await RisingEdge(slave.PCLK)
else:
await Timer(1, unit="ns")
@cocotb.test()
async def test_apb3_invalid_address_response(dut) -> None:
"""Ensure invalid addresses yield an error response and no master select."""
config = load_config()
slave = _Apb3SlaveShim(dut)
masters = _build_master_table(dut, config["masters"])
await start_clock(slave.PCLK)
if slave.PRESETn is not None:
slave.PRESETn.value = 1
slave.PSEL.value = 0
slave.PENABLE.value = 0
slave.PWRITE.value = 0
slave.PADDR.value = 0
slave.PWDATA.value = 0
for master_name, idx in all_index_pairs(masters):
entry = masters[master_name]
set_value(entry["inputs"]["PREADY"], idx, 0)
set_value(entry["inputs"]["PSLVERR"], idx, 0)
set_value(entry["inputs"]["PRDATA"], idx, 0)
invalid_addr = find_invalid_address(config)
if invalid_addr is None:
dut._log.warning("No unmapped address found; skipping invalid address test")
return
slave.PADDR.value = invalid_addr
slave.PWRITE.value = 1
slave.PWDATA.value = _write_pattern(invalid_addr, config["data_width"])
slave.PSEL.value = 1
slave.PENABLE.value = 0
if slave.PCLK is not None:
await RisingEdge(slave.PCLK)
else:
await Timer(1, unit="ns")
slave.PENABLE.value = 1
if slave.PCLK is not None:
await RisingEdge(slave.PCLK)
else:
await Timer(1, unit="ns")
for master_name, idx in all_index_pairs(masters):
entry = masters[master_name]
assert get_int(entry["outputs"]["PSEL"], idx) == 0, (
f"{master_name}{idx} must stay idle for invalid address"
)
assert int(slave.PREADY.value) == 1, "Invalid address should still complete the transfer"
assert int(slave.PSLVERR.value) == 1, "Invalid address should raise PSLVERR"

View File

@@ -1,5 +1,9 @@
"""Pytest wrapper launching the APB3 cocotb smoke test.""" """Pytest wrapper launching the APB3 cocotb smoke tests."""
from __future__ import annotations
import json
import logging
from pathlib import Path from pathlib import Path
import pytest import pytest
@@ -11,20 +15,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 colorize_cocotb_log, 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 +43,44 @@ 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( build_log_file = build_root / "build.log"
sources=sources, sim_log_file = build_root / "simulation.log"
hdl_toplevel=module_path.stem,
build_dir=build_dir,
)
runner.test( try:
hdl_toplevel=module_path.stem, runner.build(
test_module="tests.cocotb.apb3.smoke.test_register_access", sources=sources,
build_dir=build_dir, hdl_toplevel=module_path.stem,
log_file=str(tmp_path / "sim.log"), build_dir=sim_build,
) log_file=str(build_log_file),
)
except SystemExit as e:
# Print build log on failure for easier debugging
if build_log_file.exists():
logging.error(f"""
=== Build Log ===
{colorize_cocotb_log(build_log_file.read_text())}
=== End Build Log ===
""")
if e.code != 0:
raise
try:
runner.test(
hdl_toplevel=module_path.stem,
test_module="tests.cocotb.apb3.smoke.test_register_access",
build_dir=sim_build,
log_file=str(sim_log_file),
extra_env={"RDL_TEST_CONFIG": json.dumps(config)},
)
except SystemExit as e:
# Print simulation log on failure for easier debugging
if sim_log_file.exists():
logging.error(f"""
=== Simulation Log ===
{colorize_cocotb_log(sim_log_file.read_text())}
=== End Simulation Log ===
""")
if e.code != 0:
raise

View File

@@ -3,6 +3,8 @@
import cocotb import cocotb
from cocotb.triggers import Timer from cocotb.triggers import Timer
from tests.cocotb_lib.protocol_utils import apb_access, apb_setup
class _Apb3SlaveShim: class _Apb3SlaveShim:
def __init__(self, dut): def __init__(self, dut):
@@ -41,33 +43,28 @@ def _apb3_master(dut, base: str):
async def test_depth_1(dut): async def test_depth_1(dut):
"""Test max_decode_depth=1 - should have interface for inner1 only.""" """Test max_decode_depth=1 - should have interface for inner1 only."""
s_apb = _apb3_slave(dut) s_apb = _apb3_slave(dut)
# At depth 1, we should have m_apb_inner1 but not deeper interfaces # At depth 1, we should have m_apb_inner1 but not deeper interfaces
inner1 = _apb3_master(dut, "m_apb_inner1") inner1 = _apb3_master(dut, "m_apb_inner1")
# Default slave side inputs # Default slave side inputs
s_apb.PSEL.value = 0 s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0 s_apb.PENABLE.value = 0
s_apb.PWRITE.value = 0 s_apb.PWRITE.value = 0
s_apb.PADDR.value = 0 s_apb.PADDR.value = 0
s_apb.PWDATA.value = 0 s_apb.PWDATA.value = 0
inner1.PRDATA.value = 0 inner1.PRDATA.value = 0
inner1.PREADY.value = 0 inner1.PREADY.value = 0
inner1.PSLVERR.value = 0 inner1.PSLVERR.value = 0
await Timer(1, units="ns") await Timer(1, unit="ns")
# Write to address 0x0 (should select inner1) # Write to address 0x0 (should select inner1)
inner1.PREADY.value = 1 inner1.PREADY.value = 1
s_apb.PADDR.value = 0x0 await apb_setup(s_apb, 0x0, True, 0x12345678)
s_apb.PWDATA.value = 0x12345678 await apb_access(s_apb)
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.PSEL.value) == 1, "inner1 must be selected"
assert int(inner1.PWRITE.value) == 1, "Write should propagate" assert int(inner1.PWRITE.value) == 1, "Write should propagate"
assert int(s_apb.PREADY.value) == 1, "Ready should mirror master" assert int(s_apb.PREADY.value) == 1, "Ready should mirror master"
@@ -77,57 +74,47 @@ async def test_depth_1(dut):
async def test_depth_2(dut): async def test_depth_2(dut):
"""Test max_decode_depth=2 - should have interfaces for reg1 and inner2.""" """Test max_decode_depth=2 - should have interfaces for reg1 and inner2."""
s_apb = _apb3_slave(dut) s_apb = _apb3_slave(dut)
# At depth 2, we should have m_apb_reg1 and m_apb_inner2 # At depth 2, we should have m_apb_reg1 and m_apb_inner2
reg1 = _apb3_master(dut, "m_apb_reg1") reg1 = _apb3_master(dut, "m_apb_reg1")
inner2 = _apb3_master(dut, "m_apb_inner2") inner2 = _apb3_master(dut, "m_apb_inner2")
# Default slave side inputs # Default slave side inputs
s_apb.PSEL.value = 0 s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0 s_apb.PENABLE.value = 0
s_apb.PWRITE.value = 0 s_apb.PWRITE.value = 0
s_apb.PADDR.value = 0 s_apb.PADDR.value = 0
s_apb.PWDATA.value = 0 s_apb.PWDATA.value = 0
reg1.PRDATA.value = 0 reg1.PRDATA.value = 0
reg1.PREADY.value = 0 reg1.PREADY.value = 0
reg1.PSLVERR.value = 0 reg1.PSLVERR.value = 0
inner2.PRDATA.value = 0 inner2.PRDATA.value = 0
inner2.PREADY.value = 0 inner2.PREADY.value = 0
inner2.PSLVERR.value = 0 inner2.PSLVERR.value = 0
await Timer(1, units="ns") await Timer(1, unit="ns")
# Write to address 0x0 (should select reg1) # Write to address 0x0 (should select reg1)
reg1.PREADY.value = 1 reg1.PREADY.value = 1
s_apb.PADDR.value = 0x0 await apb_setup(s_apb, 0x0, True, 0xABCDEF01)
s_apb.PWDATA.value = 0xABCDEF01 await apb_access(s_apb)
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(reg1.PSEL.value) == 1, "reg1 must be selected for address 0x0"
assert int(inner2.PSEL.value) == 0, "inner2 should not be selected" assert int(inner2.PSEL.value) == 0, "inner2 should not be selected"
# Reset # Reset
s_apb.PSEL.value = 0 s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0 s_apb.PENABLE.value = 0
reg1.PREADY.value = 0 reg1.PREADY.value = 0
await Timer(1, units="ns") await Timer(1, unit="ns")
# Write to address 0x10 (should select inner2) # Write to address 0x10 (should select inner2)
inner2.PREADY.value = 1 inner2.PREADY.value = 1
s_apb.PADDR.value = 0x10 await apb_setup(s_apb, 0x10, True, 0x23456789)
s_apb.PWDATA.value = 0x23456789 await apb_access(s_apb)
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(inner2.PSEL.value) == 1, "inner2 must be selected for address 0x10"
assert int(reg1.PSEL.value) == 0, "reg1 should not be selected" assert int(reg1.PSEL.value) == 0, "reg1 should not be selected"
@@ -136,76 +123,61 @@ async def test_depth_2(dut):
async def test_depth_0(dut): async def test_depth_0(dut):
"""Test max_decode_depth=0 - should have interfaces for all leaf registers.""" """Test max_decode_depth=0 - should have interfaces for all leaf registers."""
s_apb = _apb3_slave(dut) s_apb = _apb3_slave(dut)
# At depth 0, we should have all leaf registers: reg1, reg2, reg2b # At depth 0, we should have all leaf registers: reg1, reg2, reg2b
reg1 = _apb3_master(dut, "m_apb_reg1") reg1 = _apb3_master(dut, "m_apb_reg1")
reg2 = _apb3_master(dut, "m_apb_reg2") reg2 = _apb3_master(dut, "m_apb_reg2")
reg2b = _apb3_master(dut, "m_apb_reg2b") reg2b = _apb3_master(dut, "m_apb_reg2b")
# Default slave side inputs # Default slave side inputs
s_apb.PSEL.value = 0 s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0 s_apb.PENABLE.value = 0
s_apb.PWRITE.value = 0 s_apb.PWRITE.value = 0
s_apb.PADDR.value = 0 s_apb.PADDR.value = 0
s_apb.PWDATA.value = 0 s_apb.PWDATA.value = 0
for master in [reg1, reg2, reg2b]: for master in [reg1, reg2, reg2b]:
master.PRDATA.value = 0 master.PRDATA.value = 0
master.PREADY.value = 0 master.PREADY.value = 0
master.PSLVERR.value = 0 master.PSLVERR.value = 0
await Timer(1, units="ns") await Timer(1, unit="ns")
# Write to address 0x0 (should select reg1) # Write to address 0x0 (should select reg1)
reg1.PREADY.value = 1 reg1.PREADY.value = 1
s_apb.PADDR.value = 0x0 await apb_setup(s_apb, 0x0, True, 0x11111111)
s_apb.PWDATA.value = 0x11111111 await apb_access(s_apb)
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(reg1.PSEL.value) == 1, "reg1 must be selected for address 0x0"
assert int(reg2.PSEL.value) == 0, "reg2 should not be selected" assert int(reg2.PSEL.value) == 0, "reg2 should not be selected"
assert int(reg2b.PSEL.value) == 0, "reg2b should not be selected" assert int(reg2b.PSEL.value) == 0, "reg2b should not be selected"
# Reset # Reset
s_apb.PSEL.value = 0 s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0 s_apb.PENABLE.value = 0
reg1.PREADY.value = 0 reg1.PREADY.value = 0
await Timer(1, units="ns") await Timer(1, unit="ns")
# Write to address 0x10 (should select reg2) # Write to address 0x10 (should select reg2)
reg2.PREADY.value = 1 reg2.PREADY.value = 1
s_apb.PADDR.value = 0x10 await apb_setup(s_apb, 0x10, True, 0x22222222)
s_apb.PWDATA.value = 0x22222222 await apb_access(s_apb)
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(reg2.PSEL.value) == 1, "reg2 must be selected for address 0x10"
assert int(reg1.PSEL.value) == 0, "reg1 should not be selected" assert int(reg1.PSEL.value) == 0, "reg1 should not be selected"
assert int(reg2b.PSEL.value) == 0, "reg2b should not be selected" assert int(reg2b.PSEL.value) == 0, "reg2b should not be selected"
# Reset # Reset
s_apb.PSEL.value = 0 s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0 s_apb.PENABLE.value = 0
reg2.PREADY.value = 0 reg2.PREADY.value = 0
await Timer(1, units="ns") await Timer(1, unit="ns")
# Write to address 0x14 (should select reg2b) # Write to address 0x14 (should select reg2b)
reg2b.PREADY.value = 1 reg2b.PREADY.value = 1
s_apb.PADDR.value = 0x14 await apb_setup(s_apb, 0x14, True, 0x33333333)
s_apb.PWDATA.value = 0x33333333 await apb_access(s_apb)
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(reg2b.PSEL.value) == 1, "reg2b must be selected for address 0x14"
assert int(reg1.PSEL.value) == 0, "reg1 should not be selected" assert int(reg1.PSEL.value) == 0, "reg1 should not be selected"
assert int(reg2.PSEL.value) == 0, "reg2 should not be selected" assert int(reg2.PSEL.value) == 0, "reg2 should not be selected"

View File

@@ -1,17 +1,30 @@
"""APB4 smoke tests using generated multi-register design.""" """APB4 smoke tests generated from SystemRDL sources."""
from __future__ import annotations
from typing import Any
import cocotb import cocotb
from cocotb.triggers import Timer from cocotb.triggers import RisingEdge, Timer
WRITE_ADDR = 0x4 from tests.cocotb_lib.handle_utils import SignalHandle
READ_ADDR = 0x8 from tests.cocotb_lib.protocol_utils import (
WRITE_DATA = 0x1234_5678 all_index_pairs,
READ_DATA = 0x89AB_CDEF find_invalid_address,
get_int,
load_config,
set_value,
start_clock,
)
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.PCLK = getattr(dut, f"{prefix}_PCLK", None)
self.PRESETn = getattr(dut, f"{prefix}_PRESETn", None)
self.PSEL = getattr(dut, f"{prefix}_PSEL") self.PSEL = getattr(dut, f"{prefix}_PSEL")
self.PENABLE = getattr(dut, f"{prefix}_PENABLE") self.PENABLE = getattr(dut, f"{prefix}_PENABLE")
self.PWRITE = getattr(dut, f"{prefix}_PWRITE") self.PWRITE = getattr(dut, f"{prefix}_PWRITE")
@@ -24,115 +37,296 @@ class _Apb4SlaveShim:
self.PSLVERR = getattr(dut, f"{prefix}_PSLVERR") self.PSLVERR = getattr(dut, f"{prefix}_PSLVERR")
class _Apb4MasterShim: def _build_master_table(dut, masters_cfg: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
def __init__(self, dut, base: str): table: dict[str, dict[str, Any]] = {}
self.PSEL = getattr(dut, f"{base}_PSEL") for master in masters_cfg:
self.PENABLE = getattr(dut, f"{base}_PENABLE") port_prefix = master["port_prefix"]
self.PWRITE = getattr(dut, f"{base}_PWRITE") entry = {
self.PADDR = getattr(dut, f"{base}_PADDR") "port_prefix": port_prefix,
self.PPROT = getattr(dut, f"{base}_PPROT") "indices": [tuple(idx) for idx in master["indices"]] or [tuple()],
self.PWDATA = getattr(dut, f"{base}_PWDATA") "outputs": {
self.PSTRB = getattr(dut, f"{base}_PSTRB") "PSEL": SignalHandle(dut, f"{port_prefix}_PSEL"),
self.PRDATA = getattr(dut, f"{base}_PRDATA") "PENABLE": SignalHandle(dut, f"{port_prefix}_PENABLE"),
self.PREADY = getattr(dut, f"{base}_PREADY") "PWRITE": SignalHandle(dut, f"{port_prefix}_PWRITE"),
self.PSLVERR = getattr(dut, f"{base}_PSLVERR") "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"),
},
"inst_size": master["inst_size"],
"inst_address": master["inst_address"],
}
table[master["inst_name"]] = entry
return table
def _apb4_slave(dut): def _write_pattern(address: int, width: int) -> int:
return getattr(dut, "s_apb", None) or _Apb4SlaveShim(dut) mask = (1 << width) - 1
return ((address * 0x1021) ^ 0x1357_9BDF) & mask
def _apb4_master(dut, base: str): def _read_pattern(address: int, width: int) -> int:
return getattr(dut, base, None) or _Apb4MasterShim(dut, base) 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 await start_clock(slave.PCLK)
s_apb.PSEL.value = 0 if slave.PRESETn is not None:
s_apb.PENABLE.value = 0 slave.PRESETn.value = 1
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 masters.values(): slave.PSEL.value = 0
master.PRDATA.value = 0 slave.PENABLE.value = 0
master.PREADY.value = 0 slave.PWRITE.value = 0
master.PSLVERR.value = 0 slave.PADDR.value = 0
slave.PPROT.value = 0
slave.PWDATA.value = 0
slave.PSTRB.value = 0
await Timer(1, units="ns") for master_name, idx in all_index_pairs(masters):
entry = masters[master_name]
set_value(entry["inputs"]["PRDATA"], idx, 0)
set_value(entry["inputs"]["PREADY"], idx, 0)
set_value(entry["inputs"]["PSLVERR"], idx, 0)
# ------------------------------------------------------------------ if slave.PCLK is not None:
# Write transfer to reg2 await RisingEdge(slave.PCLK)
# ------------------------------------------------------------------ else:
masters["reg2"].PREADY.value = 1 await Timer(1, unit="ns")
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") addr_mask = (1 << config["address_width"]) - 1
strobe_mask = (1 << config["byte_width"]) - 1
assert int(masters["reg2"].PSEL.value) == 1, "reg2 must be selected for write" for txn in config["transactions"]:
assert int(masters["reg2"].PWRITE.value) == 1, "Write strobes should propagate" master_name = txn["master"]
assert int(masters["reg2"].PADDR.value) == WRITE_ADDR, "Address should fan out" index = tuple(txn["index"])
assert int(masters["reg2"].PWDATA.value) == WRITE_DATA, "Write data should fan out" entry = masters[master_name]
for name, master in masters.items(): address = txn["address"] & addr_mask
if name != "reg2": write_data = _write_pattern(address, config["data_width"])
assert int(master.PSEL.value) == 0, f"{name} should remain idle on write"
assert int(s_apb.PREADY.value) == 1, "Ready should mirror selected master" # Prime master-side inputs for the write phase
assert int(s_apb.PSLVERR.value) == 0, "No error expected on successful write" set_value(entry["inputs"]["PREADY"], index, 0)
set_value(entry["inputs"]["PSLVERR"], index, 0)
# Return to idle # ------------------------------------------------------------------
s_apb.PSEL.value = 0 # Setup phase
s_apb.PENABLE.value = 0 # ------------------------------------------------------------------
s_apb.PWRITE.value = 0 slave.PADDR.value = address
masters["reg2"].PREADY.value = 0 slave.PWDATA.value = write_data
await Timer(1, units="ns") slave.PSTRB.value = strobe_mask
slave.PPROT.value = 0
slave.PWRITE.value = 1
slave.PSEL.value = 1
slave.PENABLE.value = 0
# ------------------------------------------------------------------ dut._log.info(
# Read transfer from reg3 f"Starting transaction {txn['label']} to {master_name}{index} at address 0x{address:08X}"
# ------------------------------------------------------------------ )
masters["reg3"].PRDATA.value = READ_DATA master_address = (address - entry["inst_address"]) % entry["inst_size"]
masters["reg3"].PREADY.value = 1
masters["reg3"].PSLVERR.value = 0
s_apb.PADDR.value = READ_ADDR if slave.PCLK is not None:
s_apb.PSEL.value = 1 await RisingEdge(slave.PCLK)
s_apb.PENABLE.value = 1 else:
s_apb.PWRITE.value = 0 await Timer(1, unit="ns")
await Timer(1, units="ns") assert get_int(entry["outputs"]["PSEL"], index) == 1, f"{master_name} should assert PSEL for write"
assert get_int(entry["outputs"]["PENABLE"], index) == 0, (
f"{master_name} must hold PENABLE low in setup"
)
assert get_int(entry["outputs"]["PWRITE"], index) == 1, f"{master_name} should see write intent"
assert get_int(entry["outputs"]["PADDR"], index) == master_address, (
f"{master_name} must receive write address"
)
assert get_int(entry["outputs"]["PWDATA"], index) == write_data, (
f"{master_name} must receive write data"
)
assert get_int(entry["outputs"]["PSTRB"], index) == strobe_mask, (
f"{master_name} must receive full strobes"
)
assert int(masters["reg3"].PSEL.value) == 1, "reg3 must be selected for read" for other_name, other_idx in all_index_pairs(masters):
assert int(masters["reg3"].PWRITE.value) == 0, "Read should deassert write" if other_name == master_name and other_idx == index:
assert int(masters["reg3"].PADDR.value) == READ_ADDR, "Read address should propagate" continue
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']}"
)
for name, master in masters.items(): # ------------------------------------------------------------------
if name != "reg3": # Access phase
assert int(master.PSEL.value) == 0, f"{name} should remain idle on read" # ------------------------------------------------------------------
set_value(entry["inputs"]["PREADY"], index, 1)
slave.PENABLE.value = 1
assert int(s_apb.PRDATA.value) == READ_DATA, "Read data should return from master" if slave.PCLK is not None:
assert int(s_apb.PREADY.value) == 1, "Ready must follow selected master" await RisingEdge(slave.PCLK)
assert int(s_apb.PSLVERR.value) == 0, "No error expected on successful read" else:
await Timer(1, unit="ns")
# Back to idle assert get_int(entry["outputs"]["PSEL"], index) == 1, f"{master_name} must keep PSEL asserted"
s_apb.PSEL.value = 0 assert get_int(entry["outputs"]["PENABLE"], index) == 1, (
s_apb.PENABLE.value = 0 f"{master_name} must assert PENABLE in access"
masters["reg3"].PREADY.value = 0 )
await Timer(1, units="ns") assert get_int(entry["outputs"]["PADDR"], index) == master_address, (
f"{master_name} must keep write address stable"
)
assert get_int(entry["outputs"]["PWDATA"], index) == write_data, (
f"{master_name} must keep write data stable"
)
assert get_int(entry["outputs"]["PSTRB"], index) == strobe_mask, (
f"{master_name} must keep write strobes stable"
)
assert int(slave.PREADY.value) == 1, "Slave ready should reflect selected master"
assert int(slave.PSLVERR.value) == 0, "No error expected during write"
# Return to idle for next transaction
slave.PSEL.value = 0
slave.PENABLE.value = 0
slave.PWRITE.value = 0
set_value(entry["inputs"]["PREADY"], index, 0)
if slave.PCLK is not None:
await RisingEdge(slave.PCLK)
else:
await Timer(1, unit="ns")
# ------------------------------------------------------------------
# Read phase
# ------------------------------------------------------------------
read_data = _read_pattern(address, config["data_width"])
set_value(entry["inputs"]["PRDATA"], index, read_data)
set_value(entry["inputs"]["PREADY"], index, 0)
set_value(entry["inputs"]["PSLVERR"], index, 0)
# ------------------------------------------------------------------
# Setup phase
# ------------------------------------------------------------------
slave.PADDR.value = address
slave.PWRITE.value = 0
slave.PSEL.value = 1
slave.PENABLE.value = 0
if slave.PCLK is not None:
await RisingEdge(slave.PCLK)
else:
await Timer(1, unit="ns")
assert get_int(entry["outputs"]["PSEL"], index) == 1, f"{master_name} must assert PSEL for read"
assert get_int(entry["outputs"]["PENABLE"], index) == 0, (
f"{master_name} must hold PENABLE low in setup"
)
assert get_int(entry["outputs"]["PWRITE"], index) == 0, (
f"{master_name} should deassert write for reads"
)
assert get_int(entry["outputs"]["PADDR"], index) == master_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']}"
)
# ------------------------------------------------------------------
# Access phase
# ------------------------------------------------------------------
set_value(entry["inputs"]["PREADY"], index, 1)
slave.PENABLE.value = 1
if slave.PCLK is not None:
await RisingEdge(slave.PCLK)
else:
await Timer(1, unit="ns")
assert get_int(entry["outputs"]["PSEL"], index) == 1, f"{master_name} must keep PSEL asserted"
assert get_int(entry["outputs"]["PENABLE"], index) == 1, (
f"{master_name} must assert PENABLE in access"
)
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)
if slave.PCLK is not None:
await RisingEdge(slave.PCLK)
else:
await Timer(1, unit="ns")
@cocotb.test()
async def test_apb4_invalid_address_response(dut) -> None:
"""Ensure invalid addresses yield an error response and no master select."""
config = load_config()
slave = _Apb4SlaveShim(dut)
masters = _build_master_table(dut, config["masters"])
await start_clock(slave.PCLK)
if slave.PRESETn is not None:
slave.PRESETn.value = 1
slave.PSEL.value = 0
slave.PENABLE.value = 0
slave.PWRITE.value = 0
slave.PADDR.value = 0
slave.PPROT.value = 0
slave.PWDATA.value = 0
slave.PSTRB.value = 0
for master_name, idx in all_index_pairs(masters):
entry = masters[master_name]
set_value(entry["inputs"]["PREADY"], idx, 0)
set_value(entry["inputs"]["PSLVERR"], idx, 0)
set_value(entry["inputs"]["PRDATA"], idx, 0)
invalid_addr = find_invalid_address(config)
if invalid_addr is None:
dut._log.warning("No unmapped address found; skipping invalid address test")
return
slave.PADDR.value = invalid_addr
slave.PWRITE.value = 1
slave.PWDATA.value = _write_pattern(invalid_addr, config["data_width"])
slave.PSTRB.value = (1 << config["byte_width"]) - 1
slave.PSEL.value = 1
slave.PENABLE.value = 0
if slave.PCLK is not None:
await RisingEdge(slave.PCLK)
else:
await Timer(1, unit="ns")
slave.PENABLE.value = 1
if slave.PCLK is not None:
await RisingEdge(slave.PCLK)
else:
await Timer(1, unit="ns")
for master_name, idx in all_index_pairs(masters):
entry = masters[master_name]
assert get_int(entry["outputs"]["PSEL"], idx) == 0, (
f"{master_name}{idx} must stay idle for invalid address"
)
assert int(slave.PREADY.value) == 1, "Invalid address should still complete the transfer"
assert int(slave.PSLVERR.value) == 1, "Invalid address should raise PSLVERR"

View File

@@ -1,5 +1,9 @@
"""Pytest wrapper launching the APB4 cocotb smoke test.""" """Pytest wrapper launching the APB4 cocotb smoke tests."""
from __future__ import annotations
import json
import logging
from pathlib import Path from pathlib import Path
import pytest import pytest
@@ -11,20 +15,28 @@ 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 colorize_cocotb_log, 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( logging.info(f"Running APB4 smoke test for {rdl_path} with top {top_name}")
str(repo_root / "tests" / "cocotb_lib" / "multiple_reg.rdl"), logging.info(f"Build root: {build_root}")
"multi_reg",
tmp_path, module_path, package_path, config = prepare_cpuif_case(
str(rdl_path),
top_name,
build_root,
cpuif_cls=APB4CpuifFlat, cpuif_cls=APB4CpuifFlat,
control_signal="PSEL",
) )
sources = get_verilog_sources( sources = get_verilog_sources(
@@ -34,17 +46,44 @@ 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"
runner.build( build_log_file = build_root / "build.log"
sources=sources, sim_log_file = build_root / "simulation.log"
hdl_toplevel=module_path.stem,
build_dir=build_dir,
)
runner.test( try:
hdl_toplevel=module_path.stem, runner.build(
test_module="tests.cocotb.apb4.smoke.test_register_access", sources=sources,
build_dir=build_dir, hdl_toplevel=module_path.stem,
log_file=str(tmp_path / "sim.log"), build_dir=sim_build,
) log_file=str(build_log_file),
)
except SystemExit as e:
# Print build log on failure for easier debugging
if build_log_file.exists():
logging.error(f"""
=== Build Log ===
{colorize_cocotb_log(build_log_file.read_text())}
=== End Build Log ===
""")
if e.code != 0:
raise
try:
runner.test(
hdl_toplevel=module_path.stem,
test_module="tests.cocotb.apb4.smoke.test_register_access",
build_dir=sim_build,
log_file=str(sim_log_file),
extra_env={"RDL_TEST_CONFIG": json.dumps(config)},
)
except SystemExit as e:
# Print simulation log on failure for easier debugging
if sim_log_file.exists():
logging.error(f"""
=== Simulation Log ===
{colorize_cocotb_log(sim_log_file.read_text())}
=== End Simulation Log ===
""")
if e.code != 0:
raise e

View File

@@ -3,6 +3,8 @@
import cocotb import cocotb
from cocotb.triggers import Timer from cocotb.triggers import Timer
from tests.cocotb_lib.protocol_utils import apb_access, apb_setup
class _Apb4SlaveShim: class _Apb4SlaveShim:
def __init__(self, dut): def __init__(self, dut):
@@ -45,10 +47,10 @@ def _apb4_master(dut, base: str):
async def test_depth_1(dut): async def test_depth_1(dut):
"""Test max_decode_depth=1 - should have interface for inner1 only.""" """Test max_decode_depth=1 - should have interface for inner1 only."""
s_apb = _apb4_slave(dut) s_apb = _apb4_slave(dut)
# At depth 1, we should have m_apb_inner1 but not deeper interfaces # At depth 1, we should have m_apb_inner1 but not deeper interfaces
inner1 = _apb4_master(dut, "m_apb_inner1") inner1 = _apb4_master(dut, "m_apb_inner1")
# Default slave side inputs # Default slave side inputs
s_apb.PSEL.value = 0 s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0 s_apb.PENABLE.value = 0
@@ -57,24 +59,18 @@ async def test_depth_1(dut):
s_apb.PWDATA.value = 0 s_apb.PWDATA.value = 0
s_apb.PPROT.value = 0 s_apb.PPROT.value = 0
s_apb.PSTRB.value = 0 s_apb.PSTRB.value = 0
inner1.PRDATA.value = 0 inner1.PRDATA.value = 0
inner1.PREADY.value = 0 inner1.PREADY.value = 0
inner1.PSLVERR.value = 0 inner1.PSLVERR.value = 0
await Timer(1, units="ns") await Timer(1, unit="ns")
# Write to address 0x0 (should select inner1) # Write to address 0x0 (should select inner1)
inner1.PREADY.value = 1 inner1.PREADY.value = 1
s_apb.PADDR.value = 0x0 await apb_setup(s_apb, 0x0, True, 0x12345678)
s_apb.PWDATA.value = 0x12345678 await apb_access(s_apb)
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.PSEL.value) == 1, "inner1 must be selected"
assert int(inner1.PWRITE.value) == 1, "Write should propagate" assert int(inner1.PWRITE.value) == 1, "Write should propagate"
assert int(s_apb.PREADY.value) == 1, "Ready should mirror master" assert int(s_apb.PREADY.value) == 1, "Ready should mirror master"
@@ -84,11 +80,11 @@ async def test_depth_1(dut):
async def test_depth_2(dut): async def test_depth_2(dut):
"""Test max_decode_depth=2 - should have interfaces for reg1 and inner2.""" """Test max_decode_depth=2 - should have interfaces for reg1 and inner2."""
s_apb = _apb4_slave(dut) s_apb = _apb4_slave(dut)
# At depth 2, we should have m_apb_reg1 and m_apb_inner2 # At depth 2, we should have m_apb_reg1 and m_apb_inner2
reg1 = _apb4_master(dut, "m_apb_reg1") reg1 = _apb4_master(dut, "m_apb_reg1")
inner2 = _apb4_master(dut, "m_apb_inner2") inner2 = _apb4_master(dut, "m_apb_inner2")
# Default slave side inputs # Default slave side inputs
s_apb.PSEL.value = 0 s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0 s_apb.PENABLE.value = 0
@@ -97,48 +93,36 @@ async def test_depth_2(dut):
s_apb.PWDATA.value = 0 s_apb.PWDATA.value = 0
s_apb.PPROT.value = 0 s_apb.PPROT.value = 0
s_apb.PSTRB.value = 0 s_apb.PSTRB.value = 0
reg1.PRDATA.value = 0 reg1.PRDATA.value = 0
reg1.PREADY.value = 0 reg1.PREADY.value = 0
reg1.PSLVERR.value = 0 reg1.PSLVERR.value = 0
inner2.PRDATA.value = 0 inner2.PRDATA.value = 0
inner2.PREADY.value = 0 inner2.PREADY.value = 0
inner2.PSLVERR.value = 0 inner2.PSLVERR.value = 0
await Timer(1, units="ns") await Timer(1, unit="ns")
# Write to address 0x0 (should select reg1) # Write to address 0x0 (should select reg1)
reg1.PREADY.value = 1 reg1.PREADY.value = 1
s_apb.PADDR.value = 0x0 await apb_setup(s_apb, 0x0, True, 0xABCDEF01)
s_apb.PWDATA.value = 0xABCDEF01 await apb_access(s_apb)
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(reg1.PSEL.value) == 1, "reg1 must be selected for address 0x0"
assert int(inner2.PSEL.value) == 0, "inner2 should not be selected" assert int(inner2.PSEL.value) == 0, "inner2 should not be selected"
# Reset # Reset
s_apb.PSEL.value = 0 s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0 s_apb.PENABLE.value = 0
reg1.PREADY.value = 0 reg1.PREADY.value = 0
await Timer(1, units="ns") await Timer(1, unit="ns")
# Write to address 0x10 (should select inner2) # Write to address 0x10 (should select inner2)
inner2.PREADY.value = 1 inner2.PREADY.value = 1
s_apb.PADDR.value = 0x10 await apb_setup(s_apb, 0x10, True, 0x23456789)
s_apb.PWDATA.value = 0x23456789 await apb_access(s_apb)
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(inner2.PSEL.value) == 1, "inner2 must be selected for address 0x10"
assert int(reg1.PSEL.value) == 0, "reg1 should not be selected" assert int(reg1.PSEL.value) == 0, "reg1 should not be selected"
@@ -147,12 +131,12 @@ async def test_depth_2(dut):
async def test_depth_0(dut): async def test_depth_0(dut):
"""Test max_decode_depth=0 - should have interfaces for all leaf registers.""" """Test max_decode_depth=0 - should have interfaces for all leaf registers."""
s_apb = _apb4_slave(dut) s_apb = _apb4_slave(dut)
# At depth 0, we should have all leaf registers: reg1, reg2, reg2b # At depth 0, we should have all leaf registers: reg1, reg2, reg2b
reg1 = _apb4_master(dut, "m_apb_reg1") reg1 = _apb4_master(dut, "m_apb_reg1")
reg2 = _apb4_master(dut, "m_apb_reg2") reg2 = _apb4_master(dut, "m_apb_reg2")
reg2b = _apb4_master(dut, "m_apb_reg2b") reg2b = _apb4_master(dut, "m_apb_reg2b")
# Default slave side inputs # Default slave side inputs
s_apb.PSEL.value = 0 s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0 s_apb.PENABLE.value = 0
@@ -161,67 +145,49 @@ async def test_depth_0(dut):
s_apb.PWDATA.value = 0 s_apb.PWDATA.value = 0
s_apb.PPROT.value = 0 s_apb.PPROT.value = 0
s_apb.PSTRB.value = 0 s_apb.PSTRB.value = 0
for master in [reg1, reg2, reg2b]: for master in [reg1, reg2, reg2b]:
master.PRDATA.value = 0 master.PRDATA.value = 0
master.PREADY.value = 0 master.PREADY.value = 0
master.PSLVERR.value = 0 master.PSLVERR.value = 0
await Timer(1, units="ns") await Timer(1, unit="ns")
# Write to address 0x0 (should select reg1) # Write to address 0x0 (should select reg1)
reg1.PREADY.value = 1 reg1.PREADY.value = 1
s_apb.PADDR.value = 0x0 await apb_setup(s_apb, 0x0, True, 0x11111111)
s_apb.PWDATA.value = 0x11111111 await apb_access(s_apb)
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(reg1.PSEL.value) == 1, "reg1 must be selected for address 0x0"
assert int(reg2.PSEL.value) == 0, "reg2 should not be selected" assert int(reg2.PSEL.value) == 0, "reg2 should not be selected"
assert int(reg2b.PSEL.value) == 0, "reg2b should not be selected" assert int(reg2b.PSEL.value) == 0, "reg2b should not be selected"
# Reset # Reset
s_apb.PSEL.value = 0 s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0 s_apb.PENABLE.value = 0
reg1.PREADY.value = 0 reg1.PREADY.value = 0
await Timer(1, units="ns") await Timer(1, unit="ns")
# Write to address 0x10 (should select reg2) # Write to address 0x10 (should select reg2)
reg2.PREADY.value = 1 reg2.PREADY.value = 1
s_apb.PADDR.value = 0x10 await apb_setup(s_apb, 0x10, True, 0x22222222)
s_apb.PWDATA.value = 0x22222222 await apb_access(s_apb)
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(reg2.PSEL.value) == 1, "reg2 must be selected for address 0x10"
assert int(reg1.PSEL.value) == 0, "reg1 should not be selected" assert int(reg1.PSEL.value) == 0, "reg1 should not be selected"
assert int(reg2b.PSEL.value) == 0, "reg2b should not be selected" assert int(reg2b.PSEL.value) == 0, "reg2b should not be selected"
# Reset # Reset
s_apb.PSEL.value = 0 s_apb.PSEL.value = 0
s_apb.PENABLE.value = 0 s_apb.PENABLE.value = 0
reg2.PREADY.value = 0 reg2.PREADY.value = 0
await Timer(1, units="ns") await Timer(1, unit="ns")
# Write to address 0x14 (should select reg2b) # Write to address 0x14 (should select reg2b)
reg2b.PREADY.value = 1 reg2b.PREADY.value = 1
s_apb.PADDR.value = 0x14 await apb_setup(s_apb, 0x14, True, 0x33333333)
s_apb.PWDATA.value = 0x33333333 await apb_access(s_apb)
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(reg2b.PSEL.value) == 1, "reg2b must be selected for address 0x14"
assert int(reg1.PSEL.value) == 0, "reg1 should not be selected" assert int(reg1.PSEL.value) == 0, "reg1 should not be selected"
assert int(reg2.PSEL.value) == 0, "reg2 should not be selected" assert int(reg2.PSEL.value) == 0, "reg2 should not be selected"

View File

@@ -1,15 +1,25 @@
"""AXI4-Lite smoke test ensuring address decode fanout works.""" """AXI4-Lite smoke test driven from SystemRDL-generated register maps."""
from __future__ import annotations
from typing import Any
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
READ_ADDR = 0x8 from tests.cocotb_lib.protocol_utils import (
WRITE_DATA = 0x1357_9BDF all_index_pairs,
READ_DATA = 0x2468_ACED find_invalid_address,
get_int,
load_config,
set_value,
)
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 +43,315 @@ class _AxilSlaveShim:
self.RRESP = getattr(dut, f"{prefix}_RRESP") self.RRESP = getattr(dut, f"{prefix}_RRESP")
class _AxilMasterShim: def _build_master_table(dut, masters_cfg: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
def __init__(self, dut, base: str): table: dict[str, dict[str, Any]] = {}
self.AWREADY = getattr(dut, f"{base}_AWREADY") for master in masters_cfg:
self.AWVALID = getattr(dut, f"{base}_AWVALID") prefix = master["port_prefix"]
self.AWADDR = getattr(dut, f"{base}_AWADDR") entry = {
self.AWPROT = getattr(dut, f"{base}_AWPROT") "indices": [tuple(idx) for idx in master["indices"]] or [tuple()],
self.WREADY = getattr(dut, f"{base}_WREADY") "outputs": {
self.WVALID = getattr(dut, f"{base}_WVALID") "AWVALID": SignalHandle(dut, f"{prefix}_AWVALID"),
self.WDATA = getattr(dut, f"{base}_WDATA") "AWADDR": SignalHandle(dut, f"{prefix}_AWADDR"),
self.WSTRB = getattr(dut, f"{base}_WSTRB") "AWPROT": SignalHandle(dut, f"{prefix}_AWPROT"),
self.BREADY = getattr(dut, f"{base}_BREADY") "WVALID": SignalHandle(dut, f"{prefix}_WVALID"),
self.BVALID = getattr(dut, f"{base}_BVALID") "WDATA": SignalHandle(dut, f"{prefix}_WDATA"),
self.BRESP = getattr(dut, f"{base}_BRESP") "WSTRB": SignalHandle(dut, f"{prefix}_WSTRB"),
self.ARREADY = getattr(dut, f"{base}_ARREADY") "ARVALID": SignalHandle(dut, f"{prefix}_ARVALID"),
self.ARVALID = getattr(dut, f"{base}_ARVALID") "ARADDR": SignalHandle(dut, f"{prefix}_ARADDR"),
self.ARADDR = getattr(dut, f"{base}_ARADDR") "ARPROT": SignalHandle(dut, f"{prefix}_ARPROT"),
self.ARPROT = getattr(dut, f"{base}_ARPROT") },
self.RREADY = getattr(dut, f"{base}_RREADY") "inputs": {
self.RVALID = getattr(dut, f"{base}_RVALID") "AWREADY": SignalHandle(dut, f"{prefix}_AWREADY"),
self.RDATA = getattr(dut, f"{base}_RDATA") "WREADY": SignalHandle(dut, f"{prefix}_WREADY"),
self.RRESP = getattr(dut, f"{base}_RRESP") "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"),
},
"inst_size": master["inst_size"],
"inst_address": master["inst_address"],
}
table[master["inst_name"]] = entry
return table
def _axil_slave(dut): def _write_pattern(address: int, width: int) -> int:
return getattr(dut, "s_axil", None) or _AxilSlaveShim(dut) mask = (1 << width) - 1
return ((address * 0x3105) ^ 0x1357_9BDF) & mask
def _axil_master(dut, base: str): def _read_pattern(address: int, width: int) -> int:
return getattr(dut, base, None) or _AxilMasterShim(dut, base) 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, unit="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(): set_value(entry["inputs"]["BVALID"], index, 1)
if name != "reg2": set_value(entry["inputs"]["BRESP"], index, 0)
assert int(master.AWVALID.value) == 0, f"{name} AWVALID should stay low"
assert int(master.WVALID.value) == 0, f"{name} WVALID should stay low"
# Release write channel slave.AWADDR.value = address
s_axil.AWVALID.value = 0 slave.AWPROT.value = 0
s_axil.WVALID.value = 0 slave.AWVALID.value = 1
s_axil.BREADY.value = 0 slave.WDATA.value = write_data
await Timer(1, units="ns") slave.WSTRB.value = strobe_mask
slave.WVALID.value = 1
slave.BREADY.value = 1
# -------------------------------------------------------------- dut._log.info(
# Read transaction targeting reg3 f"Starting transaction {txn['label']} to {master_name}{index} at address 0x{address:08X}"
# -------------------------------------------------------------- )
masters["reg3"].RVALID.value = 1 master_address = (address - entry["inst_address"]) % entry["inst_size"]
masters["reg3"].RDATA.value = READ_DATA
masters["reg3"].RRESP.value = 0
s_axil.ARADDR.value = READ_ADDR await Timer(1, unit="ns")
s_axil.ARPROT.value = 0
s_axil.ARVALID.value = 1
s_axil.RREADY.value = 1
await Timer(1, units="ns") assert get_int(entry["outputs"]["AWVALID"], index) == 1, f"{master_name} should see AWVALID asserted"
assert get_int(entry["outputs"]["AWADDR"], index) == master_address, (
f"{master_name} must receive AWADDR"
)
assert get_int(entry["outputs"]["WVALID"], index) == 1, f"{master_name} should see WVALID asserted"
assert get_int(entry["outputs"]["WDATA"], index) == write_data, f"{master_name} must receive WDATA"
assert get_int(entry["outputs"]["WSTRB"], index) == strobe_mask, f"{master_name} must receive WSTRB"
assert int(slave.AWREADY.value) == 1, "AWREADY should assert when write address/data are valid"
assert int(slave.WREADY.value) == 1, "WREADY should assert when write address/data are valid"
assert int(masters["reg3"].ARVALID.value) == 1, "reg3 ARVALID should follow slave" for other_name, other_idx in all_index_pairs(masters):
assert int(masters["reg3"].ARADDR.value) == READ_ADDR, "ARADDR should fan out" if other_name == master_name and other_idx == index:
continue
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']}"
)
for name, master in masters.items(): assert int(slave.BVALID.value) == 1, "Slave should observe BVALID from selected master"
if name != "reg3": assert int(slave.BRESP.value) == 0, "BRESP should indicate OKAY on write"
assert int(master.ARVALID.value) == 0, f"{name} ARVALID should stay low"
assert int(s_axil.RVALID.value) == 1, "Slave should raise RVALID when master responds" slave.AWVALID.value = 0
assert int(s_axil.RDATA.value) == READ_DATA, "Read data should return to slave" slave.WVALID.value = 0
assert int(s_axil.RRESP.value) == 0, "No error expected for read" slave.BREADY.value = 0
set_value(entry["inputs"]["BVALID"], index, 0)
await Timer(1, unit="ns")
# Return to idle read_data = _read_pattern(address, config["data_width"])
s_axil.ARVALID.value = 0 set_value(entry["inputs"]["RVALID"], index, 1)
s_axil.RREADY.value = 0 set_value(entry["inputs"]["RDATA"], index, read_data)
masters["reg3"].RVALID.value = 0 set_value(entry["inputs"]["RRESP"], index, 0)
await Timer(1, units="ns")
slave.ARADDR.value = address
slave.ARPROT.value = 0
slave.ARVALID.value = 1
slave.RREADY.value = 1
await Timer(1, unit="ns")
assert get_int(entry["outputs"]["ARVALID"], index) == 1, f"{master_name} should assert ARVALID"
assert get_int(entry["outputs"]["ARADDR"], index) == master_address, (
f"{master_name} must receive ARADDR"
)
assert int(slave.ARREADY.value) == 1, "ARREADY should assert when ARVALID is high"
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"]["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, unit="ns")
@cocotb.test()
async def test_axi4lite_invalid_write_handshake(dut) -> None:
"""Ensure mismatched AW/W valid signals raise an error and are ignored."""
config = load_config()
slave = _AxilSlaveShim(dut)
masters = _build_master_table(dut, config["masters"])
slave.AWVALID.value = 0
slave.AWADDR.value = 0
slave.AWPROT.value = 0
slave.WVALID.value = 0
slave.WDATA.value = 0
slave.WSTRB.value = 0
slave.BREADY.value = 0
slave.ARVALID.value = 0
slave.ARADDR.value = 0
slave.ARPROT.value = 0
slave.RREADY.value = 0
for master_name, idx in all_index_pairs(masters):
entry = masters[master_name]
set_value(entry["inputs"]["AWREADY"], idx, 0)
set_value(entry["inputs"]["WREADY"], idx, 0)
set_value(entry["inputs"]["BVALID"], idx, 0)
set_value(entry["inputs"]["BRESP"], idx, 0)
set_value(entry["inputs"]["ARREADY"], idx, 0)
set_value(entry["inputs"]["RVALID"], idx, 0)
set_value(entry["inputs"]["RDATA"], idx, 0)
set_value(entry["inputs"]["RRESP"], idx, 0)
await Timer(1, unit="ns")
if not config["transactions"]:
dut._log.warning("No transactions available; skipping invalid handshake test")
return
bad_addr = config["transactions"][0]["address"] & ((1 << config["address_width"]) - 1)
slave.AWADDR.value = bad_addr
slave.AWPROT.value = 0
slave.AWVALID.value = 1
slave.WVALID.value = 0
slave.BREADY.value = 1
await Timer(1, unit="ns")
for master_name, idx in all_index_pairs(masters):
entry = masters[master_name]
assert get_int(entry["outputs"]["AWVALID"], idx) == 0, (
f"{master_name}{idx} must not see AWVALID on invalid handshake"
)
assert get_int(entry["outputs"]["WVALID"], idx) == 0, (
f"{master_name}{idx} must not see WVALID on invalid handshake"
)
assert int(slave.AWREADY.value) == 0, "AWREADY must remain low on invalid write handshake"
assert int(slave.WREADY.value) == 0, "WREADY must remain low on invalid write handshake"
assert int(slave.BVALID.value) == 1, "Invalid write handshake should return BVALID"
assert int(slave.BRESP.value) == 2, "Invalid write handshake should return SLVERR"
@cocotb.test()
async def test_axi4lite_invalid_address_response(dut) -> None:
"""Ensure unmapped addresses return error responses and do not select a master."""
config = load_config()
slave = _AxilSlaveShim(dut)
masters = _build_master_table(dut, config["masters"])
slave.AWVALID.value = 0
slave.AWADDR.value = 0
slave.AWPROT.value = 0
slave.WVALID.value = 0
slave.WDATA.value = 0
slave.WSTRB.value = 0
slave.BREADY.value = 0
slave.ARVALID.value = 0
slave.ARADDR.value = 0
slave.ARPROT.value = 0
slave.RREADY.value = 0
for master_name, idx in all_index_pairs(masters):
entry = masters[master_name]
set_value(entry["inputs"]["AWREADY"], idx, 0)
set_value(entry["inputs"]["WREADY"], idx, 0)
set_value(entry["inputs"]["BVALID"], idx, 0)
set_value(entry["inputs"]["BRESP"], idx, 0)
set_value(entry["inputs"]["ARREADY"], idx, 0)
set_value(entry["inputs"]["RVALID"], idx, 0)
set_value(entry["inputs"]["RDATA"], idx, 0)
set_value(entry["inputs"]["RRESP"], idx, 0)
await Timer(1, unit="ns")
invalid_addr = find_invalid_address(config)
if invalid_addr is None:
dut._log.warning("No unmapped address found; skipping invalid address test")
return
# Invalid read
slave.ARADDR.value = invalid_addr
slave.ARPROT.value = 0
slave.ARVALID.value = 1
slave.RREADY.value = 1
await Timer(1, unit="ns")
for master_name, idx in all_index_pairs(masters):
entry = masters[master_name]
assert get_int(entry["outputs"]["ARVALID"], idx) == 0, (
f"{master_name}{idx} must stay idle for invalid read address"
)
assert int(slave.RVALID.value) == 1, "Invalid read should return RVALID"
assert int(slave.RRESP.value) == 2, "Invalid read should return SLVERR"
slave.ARVALID.value = 0
slave.RREADY.value = 0
await Timer(1, unit="ns")
# Invalid write
slave.AWADDR.value = invalid_addr
slave.AWPROT.value = 0
slave.AWVALID.value = 1
slave.WDATA.value = 0xA5A5_5A5A
slave.WSTRB.value = (1 << config["byte_width"]) - 1
slave.WVALID.value = 1
slave.BREADY.value = 1
await Timer(1, unit="ns")
for master_name, idx in all_index_pairs(masters):
entry = masters[master_name]
assert get_int(entry["outputs"]["AWVALID"], idx) == 0, (
f"{master_name}{idx} must stay idle for invalid write address"
)
assert get_int(entry["outputs"]["WVALID"], idx) == 0, (
f"{master_name}{idx} must stay idle for invalid write address"
)
assert int(slave.BVALID.value) == 1, "Invalid write should return BVALID"
assert int(slave.BRESP.value) == 2, "Invalid write should return SLVERR"

View File

@@ -1,5 +1,9 @@
"""Pytest wrapper launching the AXI4-Lite cocotb smoke test.""" """Pytest wrapper launching the AXI4-Lite cocotb smoke tests."""
from __future__ import annotations
import json
import logging
from pathlib import Path from pathlib import Path
import pytest import pytest
@@ -11,20 +15,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 colorize_cocotb_log, 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 +43,44 @@ 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( build_log_file = build_root / "build.log"
sources=sources, sim_log_file = build_root / "simulation.log"
hdl_toplevel=module_path.stem,
build_dir=build_dir,
)
runner.test( try:
hdl_toplevel=module_path.stem, runner.build(
test_module="tests.cocotb.axi4lite.smoke.test_register_access", sources=sources,
build_dir=build_dir, hdl_toplevel=module_path.stem,
log_file=str(tmp_path / "sim.log"), build_dir=sim_build,
) log_file=str(build_log_file),
)
except SystemExit as e:
# Print build log on failure for easier debugging
if build_log_file.exists():
logging.error(f"""
=== Build Log ===
{colorize_cocotb_log(build_log_file.read_text())}
=== End Build Log ===
""")
if e.code != 0:
raise
try:
runner.test(
hdl_toplevel=module_path.stem,
test_module="tests.cocotb.axi4lite.smoke.test_register_access",
build_dir=sim_build,
log_file=str(sim_log_file),
extra_env={"RDL_TEST_CONFIG": json.dumps(config)},
)
except SystemExit as e:
# Print simulation log on failure for easier debugging
if sim_log_file.exists():
logging.error(f"""
=== Simulation Log ===
{colorize_cocotb_log(sim_log_file.read_text())}
=== End Simulation Log ===
""")
if e.code != 0:
raise

View File

@@ -63,10 +63,10 @@ def _axil_master(dut, base: str):
async def test_depth_1(dut): async def test_depth_1(dut):
"""Test max_decode_depth=1 - should have interface for inner1 only.""" """Test max_decode_depth=1 - should have interface for inner1 only."""
s_axil = _axil_slave(dut) s_axil = _axil_slave(dut)
# At depth 1, we should have m_axil_inner1 but not deeper interfaces # At depth 1, we should have m_axil_inner1 but not deeper interfaces
inner1 = _axil_master(dut, "m_axil_inner1") inner1 = _axil_master(dut, "m_axil_inner1")
# Default slave side inputs # Default slave side inputs
s_axil.AWVALID.value = 0 s_axil.AWVALID.value = 0
s_axil.AWADDR.value = 0 s_axil.AWADDR.value = 0
@@ -79,7 +79,7 @@ async def test_depth_1(dut):
s_axil.ARADDR.value = 0 s_axil.ARADDR.value = 0
s_axil.ARPROT.value = 0 s_axil.ARPROT.value = 0
s_axil.RREADY.value = 0 s_axil.RREADY.value = 0
inner1.AWREADY.value = 0 inner1.AWREADY.value = 0
inner1.WREADY.value = 0 inner1.WREADY.value = 0
inner1.BVALID.value = 0 inner1.BVALID.value = 0
@@ -88,9 +88,9 @@ async def test_depth_1(dut):
inner1.RVALID.value = 0 inner1.RVALID.value = 0
inner1.RDATA.value = 0 inner1.RDATA.value = 0
inner1.RRESP.value = 0 inner1.RRESP.value = 0
await Timer(1, units="ns") await Timer(1, unit="ns")
# Write to address 0x0 (should select inner1) # Write to address 0x0 (should select inner1)
inner1.AWREADY.value = 1 inner1.AWREADY.value = 1
inner1.WREADY.value = 1 inner1.WREADY.value = 1
@@ -99,9 +99,9 @@ async def test_depth_1(dut):
s_axil.WVALID.value = 1 s_axil.WVALID.value = 1
s_axil.WDATA.value = 0x12345678 s_axil.WDATA.value = 0x12345678
s_axil.WSTRB.value = 0xF s_axil.WSTRB.value = 0xF
await Timer(1, units="ns") await Timer(1, unit="ns")
assert int(inner1.AWVALID.value) == 1, "inner1 write address valid must be set" 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" assert int(inner1.WVALID.value) == 1, "inner1 write data valid must be set"
@@ -110,11 +110,11 @@ async def test_depth_1(dut):
async def test_depth_2(dut): async def test_depth_2(dut):
"""Test max_decode_depth=2 - should have interfaces for reg1 and inner2.""" """Test max_decode_depth=2 - should have interfaces for reg1 and inner2."""
s_axil = _axil_slave(dut) s_axil = _axil_slave(dut)
# At depth 2, we should have m_axil_reg1 and m_axil_inner2 # At depth 2, we should have m_axil_reg1 and m_axil_inner2
reg1 = _axil_master(dut, "m_axil_reg1") reg1 = _axil_master(dut, "m_axil_reg1")
inner2 = _axil_master(dut, "m_axil_inner2") inner2 = _axil_master(dut, "m_axil_inner2")
# Default slave side inputs # Default slave side inputs
s_axil.AWVALID.value = 0 s_axil.AWVALID.value = 0
s_axil.AWADDR.value = 0 s_axil.AWADDR.value = 0
@@ -127,7 +127,7 @@ async def test_depth_2(dut):
s_axil.ARADDR.value = 0 s_axil.ARADDR.value = 0
s_axil.ARPROT.value = 0 s_axil.ARPROT.value = 0
s_axil.RREADY.value = 0 s_axil.RREADY.value = 0
for master in [reg1, inner2]: for master in [reg1, inner2]:
master.AWREADY.value = 0 master.AWREADY.value = 0
master.WREADY.value = 0 master.WREADY.value = 0
@@ -137,9 +137,9 @@ async def test_depth_2(dut):
master.RVALID.value = 0 master.RVALID.value = 0
master.RDATA.value = 0 master.RDATA.value = 0
master.RRESP.value = 0 master.RRESP.value = 0
await Timer(1, units="ns") await Timer(1, unit="ns")
# Write to address 0x0 (should select reg1) # Write to address 0x0 (should select reg1)
reg1.AWREADY.value = 1 reg1.AWREADY.value = 1
reg1.WREADY.value = 1 reg1.WREADY.value = 1
@@ -148,19 +148,19 @@ async def test_depth_2(dut):
s_axil.WVALID.value = 1 s_axil.WVALID.value = 1
s_axil.WDATA.value = 0xABCDEF01 s_axil.WDATA.value = 0xABCDEF01
s_axil.WSTRB.value = 0xF s_axil.WSTRB.value = 0xF
await Timer(1, units="ns") await Timer(1, unit="ns")
assert int(reg1.AWVALID.value) == 1, "reg1 must be selected for address 0x0" assert int(reg1.AWVALID.value) == 1, "reg1 must be selected for address 0x0"
assert int(inner2.AWVALID.value) == 0, "inner2 should not be selected" assert int(inner2.AWVALID.value) == 0, "inner2 should not be selected"
# Reset # Reset
s_axil.AWVALID.value = 0 s_axil.AWVALID.value = 0
s_axil.WVALID.value = 0 s_axil.WVALID.value = 0
reg1.AWREADY.value = 0 reg1.AWREADY.value = 0
reg1.WREADY.value = 0 reg1.WREADY.value = 0
await Timer(1, units="ns") await Timer(1, unit="ns")
# Write to address 0x10 (should select inner2) # Write to address 0x10 (should select inner2)
inner2.AWREADY.value = 1 inner2.AWREADY.value = 1
inner2.WREADY.value = 1 inner2.WREADY.value = 1
@@ -169,9 +169,9 @@ async def test_depth_2(dut):
s_axil.WVALID.value = 1 s_axil.WVALID.value = 1
s_axil.WDATA.value = 0x23456789 s_axil.WDATA.value = 0x23456789
s_axil.WSTRB.value = 0xF s_axil.WSTRB.value = 0xF
await Timer(1, units="ns") await Timer(1, unit="ns")
assert int(inner2.AWVALID.value) == 1, "inner2 must be selected for address 0x10" assert int(inner2.AWVALID.value) == 1, "inner2 must be selected for address 0x10"
assert int(reg1.AWVALID.value) == 0, "reg1 should not be selected" assert int(reg1.AWVALID.value) == 0, "reg1 should not be selected"
@@ -180,12 +180,12 @@ async def test_depth_2(dut):
async def test_depth_0(dut): async def test_depth_0(dut):
"""Test max_decode_depth=0 - should have interfaces for all leaf registers.""" """Test max_decode_depth=0 - should have interfaces for all leaf registers."""
s_axil = _axil_slave(dut) s_axil = _axil_slave(dut)
# At depth 0, we should have all leaf registers: reg1, reg2, reg2b # At depth 0, we should have all leaf registers: reg1, reg2, reg2b
reg1 = _axil_master(dut, "m_axil_reg1") reg1 = _axil_master(dut, "m_axil_reg1")
reg2 = _axil_master(dut, "m_axil_reg2") reg2 = _axil_master(dut, "m_axil_reg2")
reg2b = _axil_master(dut, "m_axil_reg2b") reg2b = _axil_master(dut, "m_axil_reg2b")
# Default slave side inputs # Default slave side inputs
s_axil.AWVALID.value = 0 s_axil.AWVALID.value = 0
s_axil.AWADDR.value = 0 s_axil.AWADDR.value = 0
@@ -198,7 +198,7 @@ async def test_depth_0(dut):
s_axil.ARADDR.value = 0 s_axil.ARADDR.value = 0
s_axil.ARPROT.value = 0 s_axil.ARPROT.value = 0
s_axil.RREADY.value = 0 s_axil.RREADY.value = 0
for master in [reg1, reg2, reg2b]: for master in [reg1, reg2, reg2b]:
master.AWREADY.value = 0 master.AWREADY.value = 0
master.WREADY.value = 0 master.WREADY.value = 0
@@ -208,9 +208,9 @@ async def test_depth_0(dut):
master.RVALID.value = 0 master.RVALID.value = 0
master.RDATA.value = 0 master.RDATA.value = 0
master.RRESP.value = 0 master.RRESP.value = 0
await Timer(1, units="ns") await Timer(1, unit="ns")
# Write to address 0x0 (should select reg1) # Write to address 0x0 (should select reg1)
reg1.AWREADY.value = 1 reg1.AWREADY.value = 1
reg1.WREADY.value = 1 reg1.WREADY.value = 1
@@ -219,20 +219,20 @@ async def test_depth_0(dut):
s_axil.WVALID.value = 1 s_axil.WVALID.value = 1
s_axil.WDATA.value = 0x11111111 s_axil.WDATA.value = 0x11111111
s_axil.WSTRB.value = 0xF s_axil.WSTRB.value = 0xF
await Timer(1, units="ns") await Timer(1, unit="ns")
assert int(reg1.AWVALID.value) == 1, "reg1 must be selected for address 0x0" 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(reg2.AWVALID.value) == 0, "reg2 should not be selected"
assert int(reg2b.AWVALID.value) == 0, "reg2b should not be selected" assert int(reg2b.AWVALID.value) == 0, "reg2b should not be selected"
# Reset # Reset
s_axil.AWVALID.value = 0 s_axil.AWVALID.value = 0
s_axil.WVALID.value = 0 s_axil.WVALID.value = 0
reg1.AWREADY.value = 0 reg1.AWREADY.value = 0
reg1.WREADY.value = 0 reg1.WREADY.value = 0
await Timer(1, units="ns") await Timer(1, unit="ns")
# Write to address 0x10 (should select reg2) # Write to address 0x10 (should select reg2)
reg2.AWREADY.value = 1 reg2.AWREADY.value = 1
reg2.WREADY.value = 1 reg2.WREADY.value = 1
@@ -241,20 +241,20 @@ async def test_depth_0(dut):
s_axil.WVALID.value = 1 s_axil.WVALID.value = 1
s_axil.WDATA.value = 0x22222222 s_axil.WDATA.value = 0x22222222
s_axil.WSTRB.value = 0xF s_axil.WSTRB.value = 0xF
await Timer(1, units="ns") await Timer(1, unit="ns")
assert int(reg2.AWVALID.value) == 1, "reg2 must be selected for address 0x10" 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(reg1.AWVALID.value) == 0, "reg1 should not be selected"
assert int(reg2b.AWVALID.value) == 0, "reg2b should not be selected" assert int(reg2b.AWVALID.value) == 0, "reg2b should not be selected"
# Reset # Reset
s_axil.AWVALID.value = 0 s_axil.AWVALID.value = 0
s_axil.WVALID.value = 0 s_axil.WVALID.value = 0
reg2.AWREADY.value = 0 reg2.AWREADY.value = 0
reg2.WREADY.value = 0 reg2.WREADY.value = 0
await Timer(1, units="ns") await Timer(1, unit="ns")
# Write to address 0x14 (should select reg2b) # Write to address 0x14 (should select reg2b)
reg2b.AWREADY.value = 1 reg2b.AWREADY.value = 1
reg2b.WREADY.value = 1 reg2b.WREADY.value = 1
@@ -263,9 +263,9 @@ async def test_depth_0(dut):
s_axil.WVALID.value = 1 s_axil.WVALID.value = 1
s_axil.WDATA.value = 0x33333333 s_axil.WDATA.value = 0x33333333
s_axil.WSTRB.value = 0xF s_axil.WSTRB.value = 0xF
await Timer(1, units="ns") await Timer(1, unit="ns")
assert int(reg2b.AWVALID.value) == 1, "reg2b must be selected for address 0x14" 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(reg1.AWVALID.value) == 0, "reg1 should not be selected"
assert int(reg2.AWVALID.value) == 0, "reg2 should not be selected" assert int(reg2.AWVALID.value) == 0, "reg2 should not be selected"

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,70 @@
"""Utilities for resolving cocotb signal handles across simulators."""
from __future__ import annotations
from collections.abc import Iterable
from typing import Any
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,101 @@
"""Shared helpers for cocotb smoke tests."""
from __future__ import annotations
import json
import os
from collections.abc import Iterable
from typing import Any
import cocotb
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge, Timer
from tests.cocotb_lib.handle_utils import resolve_handle
def load_config() -> dict[str, Any]:
"""Read the JSON payload describing the generated register topology."""
payload = os.environ.get("RDL_TEST_CONFIG")
if payload is None:
raise RuntimeError("RDL_TEST_CONFIG environment variable was not provided")
return json.loads(payload)
def resolve(handle, indices: Iterable[int]):
"""Index into hierarchical cocotb handles."""
return resolve_handle(handle, indices)
def set_value(handle, indices: Iterable[int], value: int) -> None:
resolve(handle, indices).value = value
def get_int(handle, indices: Iterable[int]) -> int:
return int(resolve(handle, indices).value)
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 find_invalid_address(config: dict[str, Any]) -> int | None:
"""Return an address outside any master/array span, or None if fully covered."""
addr_width = config["address_width"]
max_addr = 1 << addr_width
ranges: list[tuple[int, int]] = []
for master in config["masters"]:
inst_address = master["inst_address"]
inst_size = master["inst_size"]
n_elems = 1
if master.get("is_array"):
for dim in master.get("dimensions", []):
n_elems *= dim
span = inst_size * n_elems
ranges.append((inst_address, inst_address + span))
ranges.sort()
cursor = 0
for start, end in ranges:
if cursor < start:
return cursor
cursor = max(cursor, end)
if cursor < max_addr:
return cursor
return None
async def start_clock(clk_handle, period_ns: int = 2) -> None:
"""Start a simple clock if handle is present."""
if clk_handle is None:
return
clk_handle.value = 0
cocotb.start_soon(Clock(clk_handle, period_ns, unit="ns").start())
await RisingEdge(clk_handle)
async def apb_setup(slave, addr: int, write: bool, data: int, *, strobe_mask: int | None = None) -> None:
"""APB setup phase helper."""
if hasattr(slave, "PPROT"):
slave.PPROT.value = 0
if hasattr(slave, "PSTRB"):
if strobe_mask is None:
strobe_mask = (1 << len(slave.PSTRB)) - 1
slave.PSTRB.value = strobe_mask
slave.PADDR.value = addr
slave.PWDATA.value = data
slave.PWRITE.value = 1 if write else 0
slave.PSEL.value = 1
slave.PENABLE.value = 0
await Timer(1, unit="ns")
async def apb_access(slave) -> None:
"""APB access phase helper."""
slave.PENABLE.value = 1
await Timer(1, unit="ns")

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,13 +1,77 @@
"""Common utilities for cocotb testbenches.""" """Common utilities for cocotb testbenches."""
from __future__ import annotations
import re
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
RESET = "\x1b[0m"
DIM = "\x1b[2m"
LEVEL_COLORS = {
"DEBUG": "\x1b[35m", # magenta
"INFO": "\x1b[36m", # cyan
"WARNING": "\x1b[33m", # yellow
"ERROR": "\x1b[31m", # red
"CRITICAL": "\x1b[1;31m", # bold red
}
# Matches lines like:
# " 0.00ns INFO cocotb ..." or "-.--ns INFO gpi ..."
LINE_RE = re.compile(
r"^(?P<prefix>\s*)" # leading spaces
r"(?P<time>[-0-9\.]+[a-zA-Z]+)" # timestamp (e.g. 0.00ns, -.--ns)
r"\s+"
r"(?P<level>[A-Z]+)" # log level
r"(?P<rest>.*)$" # the rest of the line
)
def colorize_cocotb_log(text: str) -> str:
"""
Colorizes cocotb log lines for improved readability in terminal output.
Each log line is parsed to identify the timestamp and log level, which are then
colorized using ANSI escape codes. The timestamp is dimmed, and the log level
is colored according to its severity (e.g., INFO, WARNING, ERROR).
Args:
text: The input string containing cocotb log lines.
Returns:
A string with colorized log lines.
"""
def _color_line(match: re.Match) -> str:
prefix = match.group("prefix")
time = match.group("time")
level = match.group("level")
rest = match.group("rest")
level_color = LEVEL_COLORS.get(level, "")
# dim timestamp, color level
time_colored = f"{DIM}{time}{RESET}"
level_colored = f"{level_color}{level}{RESET}" if level_color else level
return f"{prefix}{time_colored} {level_colored}{rest}"
lines = []
for line in text.splitlines():
m = LINE_RE.match(line)
if m:
lines.append(_color_line(m))
else:
lines.append(line)
return "\n".join(lines)
def compile_rdl_and_export( def compile_rdl_and_export(
rdl_source: str, top_name: str, output_dir: Path, cpuif_cls: type[BaseCpuif], **kwargs: Any rdl_source: str, top_name: str, output_dir: Path, cpuif_cls: type[BaseCpuif], **kwargs: Any
@@ -65,3 +129,212 @@ 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
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(),
"inst_size": child.array_stride if child.is_array else child.size,
"inst_address": child.raw_absolute_address,
}
# 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(),
"inst_size": master.array_stride if master.is_array else master.size,
"inst_address": master.raw_absolute_address,
}
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"],
"inst_size": entry["inst_size"],
"inst_address": entry["inst_address"],
}
)
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

@@ -28,7 +28,7 @@ class TestDecodeLogicGenerator:
# Basic sanity check - it should initialize # Basic sanity check - it should initialize
assert gen is not None assert gen is not None
assert gen._flavor == DecodeLogicFlavor.READ # type: ignore assert gen._flavor == DecodeLogicFlavor.READ
def test_decode_logic_write(self, compile_rdl: Callable[..., AddrmapNode]) -> None: def test_decode_logic_write(self, compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test decode logic generation for write operations.""" """Test decode logic generation for write operations."""
@@ -48,7 +48,7 @@ class TestDecodeLogicGenerator:
gen = DecodeLogicGenerator(ds, DecodeLogicFlavor.WRITE) gen = DecodeLogicGenerator(ds, DecodeLogicFlavor.WRITE)
assert gen is not None assert gen is not None
assert gen._flavor == DecodeLogicFlavor.WRITE # type: ignore assert gen._flavor == DecodeLogicFlavor.WRITE
def test_cpuif_addr_predicate(self, compile_rdl: Callable[..., AddrmapNode]) -> None: def test_cpuif_addr_predicate(self, compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test address predicate generation.""" """Test address predicate generation."""

View File

@@ -1,4 +1,3 @@
from collections.abc import Callable from collections.abc import Callable
from systemrdl.node import AddrmapNode from systemrdl.node import AddrmapNode
@@ -9,7 +8,7 @@ from peakrdl_busdecoder.design_state import DesignState
class TestDesignState: class TestDesignState:
"""Test the DesignState class.""" """Test the DesignState class."""
def test_design_state_basic(self, compile_rdl:Callable[..., AddrmapNode])->None: def test_design_state_basic(self, compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test basic DesignState initialization.""" """Test basic DesignState initialization."""
rdl_source = """ rdl_source = """
addrmap test { addrmap test {
@@ -31,7 +30,7 @@ class TestDesignState:
assert ds.cpuif_data_width == 32 # Should infer from 32-bit field assert ds.cpuif_data_width == 32 # Should infer from 32-bit field
assert ds.addr_width > 0 assert ds.addr_width > 0
def test_design_state_custom_module_name(self, compile_rdl:Callable[..., AddrmapNode])->None: def test_design_state_custom_module_name(self, compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test DesignState with custom module name.""" """Test DesignState with custom module name."""
rdl_source = """ rdl_source = """
addrmap test { addrmap test {
@@ -50,7 +49,7 @@ class TestDesignState:
assert ds.module_name == "custom_module" assert ds.module_name == "custom_module"
assert ds.package_name == "custom_module_pkg" assert ds.package_name == "custom_module_pkg"
def test_design_state_custom_package_name(self, compile_rdl:Callable[..., AddrmapNode])->None: def test_design_state_custom_package_name(self, compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test DesignState with custom package name.""" """Test DesignState with custom package name."""
rdl_source = """ rdl_source = """
addrmap test { addrmap test {
@@ -68,7 +67,7 @@ class TestDesignState:
assert ds.package_name == "custom_pkg" assert ds.package_name == "custom_pkg"
def test_design_state_custom_address_width(self, compile_rdl:Callable[..., AddrmapNode])->None: def test_design_state_custom_address_width(self, compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test DesignState with custom address width.""" """Test DesignState with custom address width."""
rdl_source = """ rdl_source = """
addrmap test { addrmap test {
@@ -86,7 +85,7 @@ class TestDesignState:
assert ds.addr_width == 16 assert ds.addr_width == 16
def test_design_state_unroll_arrays(self, compile_rdl:Callable[..., AddrmapNode])->None: def test_design_state_unroll_arrays(self, compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test DesignState with cpuif_unroll option.""" """Test DesignState with cpuif_unroll option."""
rdl_source = """ rdl_source = """
addrmap test { addrmap test {
@@ -104,7 +103,7 @@ class TestDesignState:
assert ds.cpuif_unroll is True assert ds.cpuif_unroll is True
def test_design_state_64bit_registers(self, compile_rdl:Callable[..., AddrmapNode])->None: def test_design_state_64bit_registers(self, compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test DesignState with wider data width.""" """Test DesignState with wider data width."""
rdl_source = """ rdl_source = """
addrmap test { addrmap test {

View File

@@ -12,13 +12,13 @@ from peakrdl_busdecoder.cpuif.apb4 import APB4Cpuif
def test_instance_array_questa_compatibility(compile_rdl: Callable[..., AddrmapNode]) -> None: def test_instance_array_questa_compatibility(compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test that instance arrays generate Questa-compatible code. """Test that instance arrays generate Questa-compatible code.
This test ensures that: This test ensures that:
- Struct members for arrays use unpacked array syntax (name[dim]) - Struct members for arrays use unpacked array syntax (name[dim])
- NOT packed bit-vector syntax ([dim-1:0]name) - NOT packed bit-vector syntax ([dim-1:0]name)
- Struct is unpacked (not packed) - Struct is unpacked (not packed)
- Array indexing with loop variables works correctly - Array indexing with loop variables works correctly
This fixes the error: "Nonconstant index into instance array" This fixes the error: "Nonconstant index into instance array"
""" """
rdl_source = """ rdl_source = """
@@ -32,29 +32,29 @@ def test_instance_array_questa_compatibility(compile_rdl: Callable[..., AddrmapN
}; };
""" """
top = compile_rdl(rdl_source, top="test_map") top = compile_rdl(rdl_source, top="test_map")
with TemporaryDirectory() as tmpdir: with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter() exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif) exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif)
# Read the generated module # Read the generated module
module_file = Path(tmpdir) / "test_map.sv" module_file = Path(tmpdir) / "test_map.sv"
content = module_file.read_text() content = module_file.read_text()
# Should use unpacked struct # Should use unpacked struct
assert "typedef struct {" in content assert "typedef struct {" in content
assert "typedef struct packed" not in content assert "typedef struct packed" not in content
# Should use unpacked array syntax for array members # Should use unpacked array syntax for array members
assert "logic my_reg[4];" in content assert "logic my_reg[4];" in content
# Should NOT use packed bit-vector syntax # Should NOT use packed bit-vector syntax
assert "[3:0]my_reg" not in content assert "[3:0]my_reg" not in content
# Should have proper array indexing in decode logic # Should have proper array indexing in decode logic
assert "cpuif_wr_sel.my_reg[i0] = 1'b1;" in content assert "cpuif_wr_sel.my_reg[i0] = 1'b1;" in content
assert "cpuif_rd_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 # 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[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 assert "cpuif_wr_sel.my_reg[i0]" in content or "cpuif_rd_sel.my_reg[i0]" in content
@@ -73,21 +73,21 @@ def test_multidimensional_array_questa_compatibility(compile_rdl: Callable[...,
}; };
""" """
top = compile_rdl(rdl_source, top="test_map") top = compile_rdl(rdl_source, top="test_map")
with TemporaryDirectory() as tmpdir: with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter() exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif) exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif)
# Read the generated module # Read the generated module
module_file = Path(tmpdir) / "test_map.sv" module_file = Path(tmpdir) / "test_map.sv"
content = module_file.read_text() content = module_file.read_text()
# Should use unpacked struct with multidimensional array # Should use unpacked struct with multidimensional array
assert "typedef struct {" in content assert "typedef struct {" in content
# Should use unpacked array syntax for multidimensional arrays # Should use unpacked array syntax for multidimensional arrays
assert "logic my_reg[2][3];" in content assert "logic my_reg[2][3];" in content
# Should NOT use packed bit-vector syntax # Should NOT use packed bit-vector syntax
assert "[1:0][2:0]my_reg" not in content assert "[1:0][2:0]my_reg" not in content
assert "[5:0]my_reg" not in content assert "[5:0]my_reg" not in content
@@ -110,22 +110,22 @@ def test_nested_instance_array_questa_compatibility(compile_rdl: Callable[..., A
}; };
""" """
top = compile_rdl(rdl_source, top="outer_map") top = compile_rdl(rdl_source, top="outer_map")
with TemporaryDirectory() as tmpdir: with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter() exporter = BusDecoderExporter()
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif) exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif)
# Read the generated module # Read the generated module
module_file = Path(tmpdir) / "outer_map.sv" module_file = Path(tmpdir) / "outer_map.sv"
content = module_file.read_text() content = module_file.read_text()
# Should use unpacked struct # Should use unpacked struct
assert "typedef struct {" in content assert "typedef struct {" in content
# Inner should be an array # Inner should be an array
# The exact syntax may vary, but it should be unpacked # The exact syntax may vary, but it should be unpacked
# Look for the pattern of unpacked arrays, not packed bit-vectors # Look for the pattern of unpacked arrays, not packed bit-vectors
assert "inner[3]" in content or "logic inner" in content assert "inner[3]" in content or "logic inner" in content
# Should NOT use packed bit-vector syntax like [2:0]inner # Should NOT use packed bit-vector syntax like [2:0]inner
assert "[2:0]inner" not in content assert "[2:0]inner" not in content

49
tests/test_sv_int.py Normal file
View File

@@ -0,0 +1,49 @@
from peakrdl_busdecoder.sv_int import SVInt
def test_string_formatting() -> None:
"""SV literals should format width and value correctly."""
assert str(SVInt(0x1A, 8)) == "8'h1a"
assert str(SVInt(0xDEADBEEF)) == "'hdeadbeef"
assert str(SVInt(0x1FFFFFFFF)) == "33'h1ffffffff"
def test_arithmetic_width_propagation() -> None:
"""Addition and subtraction should preserve sizing rules."""
small = SVInt(3, 4)
large = SVInt(5, 6)
summed = small + large
assert summed.value == 8
assert summed.width == 6 # max width wins when both are sized
diff = large - small
assert diff.value == 2
assert diff.width == 6
unsized_left = SVInt(1)
mixed = unsized_left + small
assert mixed.width is None # any unsized operand yields unsized result
def test_length_and_to_bytes() -> None:
"""Length and byte conversion should reflect the represented value."""
sized = SVInt(0x3, 12)
assert len(sized) == 12
value = SVInt(0x1234)
assert len(value) == 13
assert value.to_bytes("little") == b"\x34\x12"
assert value.to_bytes("big") == b"\x12\x34"
def test_equality_and_hash() -> None:
"""Equality compares both value and width."""
a = SVInt(7, 4)
b = SVInt(7, 4)
c = SVInt(7)
assert a == b
assert hash(a) == hash(b)
assert a != c
assert (a == 7) is False # Non-SVInt comparisons fall back to NotImplemented

View File

@@ -1,10 +1,5 @@
"""Pytest fixtures for unit tests.""" """Pytest fixtures for unit tests."""
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import Iterable, Mapping, Optional
from collections.abc import Callable from collections.abc import Callable
import pytest import pytest

View File

@@ -12,7 +12,7 @@ from peakrdl_busdecoder.cpuif.apb4 import APB4Cpuif
def test_external_nested_components_generate_correct_decoder(external_nested_rdl: AddrmapNode) -> None: def test_external_nested_components_generate_correct_decoder(external_nested_rdl: AddrmapNode) -> None:
"""Test that external nested components generate correct decoder logic. """Test that external nested components generate correct decoder logic.
The decoder should: The decoder should:
- Generate select signals for multicast and port[16] - Generate select signals for multicast and port[16]
- NOT generate select signals for multicast.common[] or multicast.response - NOT generate select signals for multicast.common[] or multicast.response
@@ -25,21 +25,21 @@ def test_external_nested_components_generate_correct_decoder(external_nested_rdl
tmpdir, tmpdir,
cpuif_cls=APB4Cpuif, cpuif_cls=APB4Cpuif,
) )
# Read the generated module # Read the generated module
module_file = Path(tmpdir) / "buffer_t.sv" module_file = Path(tmpdir) / "buffer_t.sv"
content = module_file.read_text() content = module_file.read_text()
# Should have correct select signals # Should have correct select signals
assert "cpuif_wr_sel.multicast = 1'b1;" in content assert "cpuif_wr_sel.multicast = 1'b1;" in content
assert "cpuif_wr_sel.port[i0] = 1'b1;" in content assert "cpuif_wr_sel.port[i0] = 1'b1;" in content
# Should NOT have invalid nested paths # Should NOT have invalid nested paths
assert "cpuif_wr_sel.multicast.common" not in content assert "cpuif_wr_sel.multicast.common" not in content
assert "cpuif_wr_sel.multicast.response" not in content assert "cpuif_wr_sel.multicast.response" not in content
assert "cpuif_rd_sel.multicast.common" not in content assert "cpuif_rd_sel.multicast.common" not in content
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" in content assert "typedef struct" in content
assert "logic multicast;" in content assert "logic multicast;" in content
@@ -48,7 +48,7 @@ def test_external_nested_components_generate_correct_decoder(external_nested_rdl
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:
"""Test that external nested components generate correct interface ports. """Test that external nested components generate correct interface ports.
The module should have: The module should have:
- One master interface for multicast - One master interface for multicast
- Array of 16 master interfaces for port[] - Array of 16 master interfaces for port[]
@@ -61,15 +61,15 @@ def test_external_nested_components_generate_correct_interfaces(external_nested_
tmpdir, tmpdir,
cpuif_cls=APB4Cpuif, cpuif_cls=APB4Cpuif,
) )
# Read the generated module # Read the generated module
module_file = Path(tmpdir) / "buffer_t.sv" module_file = Path(tmpdir) / "buffer_t.sv"
content = module_file.read_text() content = module_file.read_text()
# Should have master interfaces for top-level external children # Should have master interfaces for top-level external children
assert "m_apb_multicast" in content assert "m_apb_multicast" in content
assert "m_apb_port [16]" in content or "m_apb_port[16]" in content assert "m_apb_port [16]" in content or "m_apb_port[16]" in content
# Should NOT have interfaces for nested external children # Should NOT have interfaces for nested external children
assert "m_apb_multicast_common" not in content assert "m_apb_multicast_common" not in content
assert "m_apb_multicast_response" not in content assert "m_apb_multicast_response" not in content
@@ -79,7 +79,7 @@ def test_external_nested_components_generate_correct_interfaces(external_nested_
def test_non_external_nested_components_are_descended(compile_rdl: Callable[..., AddrmapNode]) -> None: def test_non_external_nested_components_are_descended(compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test that non-external nested components are still descended into. """Test that non-external nested components are still descended into.
This is a regression test to ensure we didn't break normal nested This is a regression test to ensure we didn't break normal nested
component handling. component handling.
""" """
@@ -98,16 +98,16 @@ def test_non_external_nested_components_are_descended(compile_rdl: Callable[...,
}; };
""" """
top = compile_rdl(rdl_source, top="outer_block") top = compile_rdl(rdl_source, top="outer_block")
with TemporaryDirectory() as tmpdir: with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter() exporter = BusDecoderExporter()
# Use depth=0 to descend all the way down to registers # Use depth=0 to descend all the way down to registers
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif, max_decode_depth=0) 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"
content = module_file.read_text() content = module_file.read_text()
# Should descend into inner and reference inner_reg # Should descend into inner and reference inner_reg
assert "inner" in content assert "inner" in content
assert "inner_reg" in content assert "inner_reg" in content
@@ -123,7 +123,7 @@ def test_max_decode_depth_parameter_exists(compile_rdl: Callable[..., AddrmapNod
}; };
""" """
top = compile_rdl(rdl_source, top="simple") top = compile_rdl(rdl_source, top="simple")
with TemporaryDirectory() as tmpdir: with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter() exporter = BusDecoderExporter()
# Should not raise an exception # Should not raise an exception
@@ -133,8 +133,118 @@ def test_max_decode_depth_parameter_exists(compile_rdl: Callable[..., AddrmapNod
cpuif_cls=APB4Cpuif, cpuif_cls=APB4Cpuif,
max_decode_depth=2, max_decode_depth=2,
) )
# Verify output was generated # Verify output was generated
module_file = Path(tmpdir) / "simple.sv" module_file = Path(tmpdir) / "simple.sv"
assert module_file.exists() assert module_file.exists()
def test_unaligned_external_component_supported(compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test that external components can be at unaligned addresses.
This test verifies that external components don't need to be aligned
to a power-of-2 multiple of their size, as the busdecoder supports
unaligned access.
"""
rdl_source = """
mem queue_t {
name = "Queue";
mementries = 1024;
memwidth = 64;
};
addrmap buffer_t {
name = "Buffer";
desc = "";
external queue_t multicast @ 0x100; // Not power-of-2 aligned
};
"""
top = compile_rdl(rdl_source, top="buffer_t")
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
# Should not raise an alignment error
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif)
# Verify output was generated
module_file = Path(tmpdir) / "buffer_t.sv"
assert module_file.exists()
content = module_file.read_text()
# Verify the external component is in the generated code
assert "multicast" in content
def test_unaligned_external_component_array_supported(compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test that external component arrays with non-power-of-2 strides are supported.
This test verifies that external component arrays can have arbitrary strides,
not just power-of-2 strides.
"""
rdl_source = """
mem queue_t {
name = "Queue";
mementries = 256;
memwidth = 32;
};
addrmap buffer_t {
name = "Buffer";
desc = "";
external queue_t port[4] @ 0x0 += 0x600; // Stride of 0x600 (not power-of-2) to test unaligned support
};
"""
top = compile_rdl(rdl_source, top="buffer_t")
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
# Should not raise an alignment error
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif)
# Verify output was generated
module_file = Path(tmpdir) / "buffer_t.sv"
assert module_file.exists()
content = module_file.read_text()
# Verify the external component array is in the generated code
assert "port" in content
def test_unaligned_external_nested_in_addrmap(compile_rdl: Callable[..., AddrmapNode]) -> None:
"""Test that addrmaps containing external components can be at unaligned addresses.
This verifies that not just external components themselves, but also
non-external addrmaps/regfiles that contain external components can be
at unaligned addresses.
"""
rdl_source = """
mem queue_t {
name = "Queue";
mementries = 512;
memwidth = 32;
};
addrmap inner_block {
external queue_t ext_queue @ 0x0;
};
addrmap outer_block {
inner_block inner @ 0x150; // Not power-of-2 aligned
};
"""
top = compile_rdl(rdl_source, top="outer_block")
with TemporaryDirectory() as tmpdir:
exporter = BusDecoderExporter()
# Should not raise an alignment error
exporter.export(top, tmpdir, cpuif_cls=APB4Cpuif)
# Verify output was generated
module_file = Path(tmpdir) / "outer_block.sv"
assert module_file.exists()
content = module_file.read_text()
# Verify the nested components are in the generated code
assert "inner" in content

View File

@@ -36,7 +36,7 @@ def test_depth_1_generates_top_level_interface_only(compile_rdl: Callable[..., A
assert "m_apb_inner1" in content assert "m_apb_inner1" in content
# Should NOT have interface for reg1 # Should NOT have interface for reg1
assert "m_apb_reg1" not in content assert "m_apb_reg1" not in content
# Struct should have inner1 but not nested structure # Struct should have inner1 but not nested structure
assert "logic inner1;" in content assert "logic inner1;" in content
@@ -77,7 +77,7 @@ def test_depth_2_generates_second_level_interfaces(compile_rdl: Callable[..., Ad
# Should NOT have interface for inner1 or reg2 # Should NOT have interface for inner1 or reg2
assert "m_apb_inner1" not in content assert "m_apb_inner1" not in content
assert "m_apb_reg2" not in content assert "m_apb_reg2" not in content
# Struct should be hierarchical with inner1.reg1 and inner1.inner2 # Struct should be hierarchical with inner1.reg1 and inner1.inner2
assert "cpuif_sel_inner1_t" in content assert "cpuif_sel_inner1_t" in content
assert "logic reg1;" in content assert "logic reg1;" in content
@@ -125,7 +125,7 @@ def test_depth_0_decodes_all_levels(compile_rdl: Callable[..., AddrmapNode]) ->
# Should NOT have interfaces for addrmaps # Should NOT have interfaces for addrmaps
assert "m_apb_inner1" not in content assert "m_apb_inner1" not in content
assert "m_apb_inner2" not in content assert "m_apb_inner2" not in content
# Struct should be fully hierarchical # Struct should be fully hierarchical
assert "cpuif_sel_inner1_t" in content assert "cpuif_sel_inner1_t" in content
assert "cpuif_sel_inner2_t" in content assert "cpuif_sel_inner2_t" in content

View File

@@ -159,22 +159,21 @@ class TestGetIndexedPath:
addrmap my_addrmap { addrmap my_addrmap {
reg { reg {
field {} data; field {} data;
} always_reg; } always;
}; };
""" """
top = compile_rdl(rdl_source, top="my_addrmap") top = compile_rdl(rdl_source, top="my_addrmap")
reg_node = None reg_node = None
for child in top.children(): for child in top.children():
if child.inst_name == "always_reg": if child.inst_name == "always":
reg_node = child reg_node = child
break break
assert reg_node is not None assert reg_node is not None
# With keyword filter (default) - SystemRDL identifiers can use keywords but SV can't # With keyword filter (default) - SystemRDL identifiers can use keywords but SV can't
path = get_indexed_path(top, reg_node) path = get_indexed_path(top, reg_node)
# The path should contain always_reg assert path == "always_"
assert "always_reg" in path
# Without keyword filter # Without keyword filter
path = get_indexed_path(top, reg_node, skip_kw_filter=True) path = get_indexed_path(top, reg_node, skip_kw_filter=True)
assert path == "always_reg" assert path == "always"

View File

@@ -0,0 +1,60 @@
from collections.abc import Callable
from systemrdl.node import AddrmapNode
from systemrdl.rdltypes.references import PropertyReference
from peakrdl_busdecoder.utils import ref_is_internal
def _find_child_by_name(node: AddrmapNode, inst_name: str):
for child in node.children():
if child.inst_name == inst_name:
return child
raise AssertionError(f"Child with name {inst_name} not found")
class TestRefIsInternal:
"""Tests for ref_is_internal utility."""
def test_external_components_flagged(self, compile_rdl: Callable[..., AddrmapNode]) -> None:
"""External components should be treated as non-internal."""
rdl_source = """
reg reg_t {
field { sw=rw; hw=r; } data[7:0];
};
addrmap top {
external reg_t ext @ 0x0;
reg_t intrnl @ 0x10;
};
"""
top = compile_rdl(rdl_source, top="top")
internal_reg = _find_child_by_name(top, "intrnl")
assert ref_is_internal(top, internal_reg) is True
external_reg = _find_child_by_name(top, "ext")
assert external_reg.external is True
assert ref_is_internal(top, external_reg) is False
external_prop_ref = PropertyReference.__new__(PropertyReference)
external_prop_ref.node = external_reg
assert ref_is_internal(top, external_prop_ref) is False
def test_property_reference_without_node_defaults_internal(
self, compile_rdl: Callable[..., AddrmapNode]
) -> None:
"""Root-level property references should be treated as internal."""
rdl_source = """
addrmap top {
reg {
field { sw=rw; hw=r; } data[7:0];
} reg0 @ 0x0;
};
"""
top = compile_rdl(rdl_source, top="top")
prop_ref = PropertyReference.__new__(PropertyReference)
prop_ref.node = None
assert ref_is_internal(top, prop_ref) is True

1050
uv.lock generated

File diff suppressed because it is too large Load Diff