43 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
Copilot
d7481e71ba Fix max_decode_depth to control decoder hierarchy and port generation (#18)
* Initial plan

* Fix max_decode_depth to properly control decoder hierarchy and port generation

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

* Fix test that relied on old depth behavior

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

* Update documentation for max_decode_depth parameter

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

* fix format

* Add variable_depth RDL file and smoke tests for max_decode_depth parameter

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

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

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

* fix

* fix

* bump

---------

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

* Fix nonconstant index error by using unpacked structs and arrays

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

* Add comprehensive tests for Questa compatibility with instance arrays

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

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

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

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

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

* Fix type checking and formatting issues in fanin intermediate generator

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

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

* Fix test collection when cocotb is not installed

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

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

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

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

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

* add default for unpacked struct

* cocotb is test dependancy

* Refactor fanin intermediate signal logic to cpuif level

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

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

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

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>
2025-10-28 22:03:57 -07:00
Arnav Sacheti
f829e3894f format xargs 2025-10-27 20:34:41 -07:00
Arnav Sacheti
74eb2344b1 change to pypi gh action 2025-10-26 19:53:13 -07:00
Arnav Sacheti
ae17384b3b version bump 2025-10-26 19:06:29 -07:00
Copilot
b80f166997 Export master interface address widths in package parameters (#16)
* Initial plan

* Add master address width parameters to exported package

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>
2025-10-26 19:05:45 -07:00
Copilot
95fda3abaa Refactor cpuif classes to use Interface abstraction (#14)
* Initial plan

* Refactor cpuif classes to use Interface abstraction

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

* Fix type annotation consistency in Interface.signal()

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

* Add runtime validation and documentation for indexer types

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

* Remove unused variable in SVInterface.signal()

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

* Fix master port directions in APB3 and APB4 flat interfaces

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

* Fix AXI4LiteCpuifFlat and apply code formatting

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

* PSELx -> PSEL

* cleanup marker warnings

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>
2025-10-26 18:47:11 -07:00
Arnav Sacheti
1eababe1ab remove cocotb -sim.yml 2025-10-26 18:00:53 -07:00
Arnav Sacheti
b1f1bf983a Refactor tests (better grouping + cocotb support) (#15)
* initial refactor

* fix cocotb tests

* fix typecheck

* install verilator
2025-10-26 17:56:35 -07:00
Arnav Sacheti
93276ff616 fix (#13)
* fix

* fix pyrefly

* remove tests

* Update tests/unit/test_exporter.py

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

* Update src/peakrdl_busdecoder/listener.py

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

* Update tests/unit/test_exporter.py

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

* fix iter

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-26 14:40:03 -07:00
Copilot
c9addd6ac2 Fix decoder generation for external nested addressable components and add max-decode-depth parameter (#12)
* Initial plan

* Fix bus decoder to skip external nested components

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

* Optimize external children check using generator expressions

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

* Add max-decode-depth CLI argument

Added --max-decode-depth argument that:
- Is added to CLI arguments in __peakrdl__.py
- Piped into design state via ExporterKwargs and DesignStateKwargs
- Used to control max depth in listener.py
- All 66 tests pass including new test for the parameter

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>
2025-10-25 19:56:49 -07:00
Copilot
04971bdb8e Fix non-synthesizable code generation for nested addrmaps with arrays (#11)
* Initial plan

* Fix non-synthesizable code for nested addrmaps with arrays

Fixed bug where array dimensions were used instead of strides in decode logic.
For nested addrmaps with arrays like inner[4] @ 0x0 += 0x100, the generated
code was incorrectly using the dimension (4) instead of the stride (0x100).
This resulted in non-synthesizable SystemVerilog with incorrect address decoding.

The fix calculates proper strides for each dimension, including support for
multi-dimensional arrays like [2][3] where each dimension has a different stride.

Added comprehensive tests to prevent regression.

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

* Improve code comments for stride calculation clarity

Added more detailed comments explaining the stride calculation logic,
including a concrete example showing how strides are calculated for
multi-dimensional arrays.

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>
2025-10-24 10:35:28 -07:00
Copilot
9b6dbc30e2 Fix APB4 assertion syntax for Questa 2025 compatibility (#10)
* Initial plan

* Fix APB4 assertion syntax for Questa 2025 compatibility

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>
2025-10-24 09:00:32 -07:00
Copilot
4dc61d24ca Add cocotb testbench for validating generated bus decoder RTL across APB3, APB4, and AXI4-Lite interfaces (#9)
* Initial plan

* Add cocotb test infrastructure and testbenches for APB3, APB4, and AXI4-Lite

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

* Add integration tests, examples, and documentation for cocotb testbenches

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

* Address code review feedback: use relative imports and update installation docs

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

* Add implementation summary document

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

* Merge cocotb dependencies into test group

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

* Add optional cocotb simulation workflow with Icarus Verilog

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com>
2025-10-23 23:46:51 -07:00
Arnav Sacheti
0b98165ccc update tmpl 2025-10-23 23:42:17 -07:00
129 changed files with 7511 additions and 3705 deletions

22
.devcontainer/Dockerfile Normal file
View File

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

View File

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

View File

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

View File

@@ -7,10 +7,15 @@ on:
branches: [ main ]
workflow_dispatch:
workflow_call:
schedule:
# Run weekly on Monday at 00:00 UTC
- cron: '0 0 * * 1'
jobs:
test:
runs-on: ubuntu-latest
container:
image: verilator/verilator:latest
permissions:
contents: read
strategy:
@@ -24,13 +29,30 @@ jobs:
uses: astral-sh/setup-uv@v3
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
- name: Check Verilator version
run: verilator --version
- name: Install Python development packages
run: |
apt-get update && apt-get install -y python3-dev libpython3-dev
- name: Install dependencies
run: |
uv sync --group test
uv sync --all-extras --group test
- 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
uses: codecov/codecov-action@v4

View File

@@ -22,5 +22,5 @@ jobs:
- name: Install package
run: uv sync --extra cli
- name: Run pyrefly type check
run: uvx pyrefly check src/
- name: Run ty type check
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/workflows/build/badge.svg)](https://github.com/arnavsacheti/PeakRDL-BusDecoder/actions?query=workflow%3Abuild+branch%3Amain)
[![Coverage Status](https://coveralls.io/repos/github/arnavsacheti/PeakRDL-BusDecoder/badge.svg?branch=main)](https://coveralls.io/github/arnavsacheti/PeakRDL-BusDecoder?branch=main)
[![Build](https://github.com/arnavsacheti/PeakRDL-BusDecoder/actions/workflows/build.yml/badge.svg)](https://github.com/arnavsacheti/PeakRDL-BusDecoder/actions/workflows/build.yml)
[![Test](https://github.com/arnavsacheti/PeakRDL-BusDecoder/actions/workflows/test.yml/badge.svg)](https://github.com/arnavsacheti/PeakRDL-BusDecoder/actions/workflows/test.yml)
[![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)
# PeakRDL-BusDecoder

View File

@@ -15,10 +15,11 @@ implementation from SystemRDL source.
.. code-block:: python
:emphasize-lines: 2-4, 29-33
import sys
from systemrdl import RDLCompiler, RDLCompileError
from peakrdl_busdecoder import BusDecoderExporter
from peakrdl_busdecoder.cpuif.axi4lite import AXI4Lite_Cpuif
from peakrdl_busdecoder.udps import ALL_UDPS
from peakrdl_busdecoder.cpuif.axi4lite import AXI4LiteCpuif
input_files = [
"PATH/TO/my_register_block.rdl"
@@ -27,10 +28,6 @@ implementation from SystemRDL source.
# Create an instance of the compiler
rdlc = RDLCompiler()
# Register all UDPs that 'busdecoder' requires
for udp in ALL_UDPS:
rdlc.register_udp(udp)
try:
# Compile your RDL files
for input_file in input_files:
@@ -46,5 +43,5 @@ implementation from SystemRDL source.
exporter = BusDecoderExporter()
exporter.export(
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.
Each section is automatically generated based on the source register model and
is rendered into the output SystemVerilog RTL module. The bus decoder serves as
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.
The generated RTL is a pure bus-routing layer. It accepts a single CPU interface
on the slave side and fans transactions out to a set of child interfaces on the
master side. No register storage or field logic is generated.
.. figure:: diagrams/arch.png
Although it is not completely necessary to know the inner workings of the
generated RTL, it can be helpful to understand the implications of various
exporter configuration options.
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
SystemRDL hierarchy.
CPU Interface
-------------
The CPU interface logic layer provides an abstraction between the
application-specific bus protocol and the internal register file logic.
This logic layer normalizes external CPU read & write transactions into a common
:ref:`cpuif_protocol` that is used to interact with the register file. When the
design contains multiple child addrmaps, the CPU interface handles fanout of
transactions to the appropriate sub-address space.
CPU Interface Adapter
---------------------
Each supported CPU interface protocol (APB3, APB4, AXI4-Lite) provides a small
adapter that translates the external bus protocol into internal request/response
signals. These internal signals are then used by the address decoder and fanout
logic.
If you write a custom CPU interface, it must implement the internal signals
described in :ref:`cpuif_protocol`.
Address Decode
--------------
A common address decode operation is generated which computes individual access
strobes for each software-accessible register or child addrmap in the design.
This operation is performed completely combinationally. The decoder determines
which sub-address space should handle each transaction based on the address range.
The address decoder computes per-child select signals based on address ranges.
The decode boundary is controlled by ``max_decode_depth``:
* ``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
-----------
This layer of the register block implements the storage elements and state-change
logic for every field in the design. Field state is updated based on address
decode strobes from software read/write actions, as well as events from the
hardware interface input struct.
This section also assigns any hardware interface outputs.
Fanout to Child Interfaces
--------------------------
For each decoded child, the bus decoder drives a master-side CPU interface.
All address, data, and control signals are forwarded to the selected child.
Arrayed children can be kept as arrays or unrolled into discrete interfaces using
``--unroll``. This only affects port structure and naming; decode semantics are
unchanged.
Readback
--------
The readback layer aggregates and reduces all readable registers into a single
read response. During a read operation, the same address decode strobes are used
to select the active register that is being accessed.
This allows for a simple OR-reduction operation to be used to compute the read
data response.
Fanin and Error Handling
------------------------
Read and write responses are muxed back from the selected child to the slave
interface. If no child is selected for a transaction, the decoder generates an
error response on the slave interface.
For designs with a large number of software-readable registers, an optional
fanin re-timing stage can be enabled. This stage is automatically inserted at a
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.
The exact error signaling depends on the chosen CPU interface protocol (e.g.,
``PSLVERR`` for APB, ``RRESP/BRESP`` for AXI4-Lite).

View File

@@ -32,9 +32,7 @@ author = "Arnav Sacheti"
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.napoleon",
"sphinxcontrib.wavedrom",
]
render_using_wavedrompy = True
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
@@ -42,7 +40,7 @@ templates_path = ["_templates"]
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# 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 -------------------------------------------------

View File

@@ -1,13 +1,13 @@
.. _peakrdl_cfg:
Configuring PeakRDL-BusDecoder
============================
==============================
If using the `PeakRDL command line tool <https://peakrdl.readthedocs.io/>`_,
some aspects of the ``busdecoder`` command have additional configuration options
available via the PeakRDL TOML file.
some aspects of the ``busdecoder`` command can be configured via the PeakRDL
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
@@ -24,22 +24,15 @@ All busdecoder-specific options are defined under the ``[busdecoder]`` TOML head
cpuifs.my-cpuif-name = "my_cpuif_module:MyCPUInterfaceClass"
.. data:: default_reset
Command-Line Options
--------------------
Choose the default style of reset signal if not explicitly
specified by the SystemRDL design. If unspecified, the default reset
is active-high and synchronous.
The following options are available on the ``peakrdl busdecoder`` command:
Choice of:
* ``rst`` (default)
* ``rst_n``
* ``arst``
* ``arst_n``
For example:
.. code-block:: toml
[busdecoder]
default_reset = "arst"
* ``--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

View File

@@ -20,7 +20,7 @@ Both APB3 and APB4 standards are supported.
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>`_
CPU interface.
@@ -29,19 +29,19 @@ The APB3 CPU interface comes in two i/o port flavors:
SystemVerilog Interface
* Command line: ``--cpuif apb3``
* 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
Flattens the interface into discrete input and output ports.
* Command line: ``--cpuif apb3-flat``
* Class: :class:`peakrdl_busdecoder.cpuif.apb3.APB3_Cpuif_flattened`
* Class: :class:`peakrdl_busdecoder.cpuif.apb3.APB3CpuifFlat`
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>`_
CPU interface.
@@ -50,10 +50,10 @@ The APB4 CPU interface comes in two i/o port flavors:
SystemVerilog Interface
* Command line: ``--cpuif apb4``
* 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
Flattens the interface into discrete input and output ports.
* 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
==============
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>`_
CPU interface.
@@ -12,21 +12,22 @@ The AXI4-Lite CPU interface comes in two i/o port flavors:
SystemVerilog Interface
* Command line: ``--cpuif axi4-lite``
* 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
Flattens the interface into discrete input and output ports.
* Command line: ``--cpuif axi4-lite-flat``
* Class: :class:`peakrdl_busdecoder.cpuif.axi4lite.AXI4Lite_Cpuif_flattened`
* Class: :class:`peakrdl_busdecoder.cpuif.axi4lite.AXI4LiteCpuifFlat`
Pipelined Performance
---------------------
This implementation of the AXI4-Lite interface supports transaction pipelining
which can significantly improve performance of back-to-back transfers.
Protocol Notes
--------------
The AXI4-Lite adapter is intentionally simplified:
In order to support transaction pipelining, the CPU interface will accept multiple
concurrent transactions. The number of outstanding transactions allowed is automatically
determined based on the register file pipeline depth (affected by retiming options),
and influences the depth of the internal transaction response skid buffer.
* AW and W channels must be asserted together for writes. The adapter does not
support decoupled address/data for writes.
* Only a single outstanding transaction is supported. Masters should wait for
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
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
def port_declaration(self) -> str:
# Override the port declaration text to use the alternate interface name and modport style
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
return "s_axil." + name.lower()
@@ -72,7 +72,7 @@ you can define your own.
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.
3. Use your new CPUIF definition when exporting.

View File

@@ -3,10 +3,10 @@
Internal CPUIF Protocol
=======================
Internally, the busdecoder generator uses a common CPU interface handshake
protocol. This strobe-based protocol is designed to add minimal overhead to the
busdecoder implementation, while also being flexible enough to support advanced
features of a variety of bus interface standards.
Internally, the bus decoder uses a small set of common request/response signals
that each CPU interface adapter must drive. This protocol is intentionally simple
and supports a single outstanding transaction at a time. The CPU interface logic
is responsible for holding request signals stable until the transaction completes.
Signal Descriptions
@@ -15,62 +15,49 @@ Signal Descriptions
Request
^^^^^^^
cpuif_req
When asserted, a read or write transfer will be initiated.
Denotes that the following signals are valid: ``cpuif_addr``,
``cpuif_req_is_wr``, and ``cpuif_wr_data``.
When asserted, a read or write transfer is in progress. Request signals must
remain stable until the transfer completes.
A transfer will only initiate if the relevant stall signal is not asserted.
If stalled, the request shall be held until accepted. A request's parameters
(type, address, etc) shall remain static throughout the stall.
cpuif_wr_en
When asserted alongside ``cpuif_req``, denotes a write transfer.
cpuif_addr
Byte-address of the transfer.
cpuif_rd_en
When asserted alongside ``cpuif_req``, denotes a read transfer.
cpuif_req_is_wr
If ``1``, denotes that the current transfer is a write. Otherwise transfer is
a read.
cpuif_wr_addr / cpuif_rd_addr
Byte address of the write or read transfer, respectively.
cpuif_wr_data
Data to be written for the write transfer. This signal is ignored for read
transfers.
Data to be written for the write transfer.
cpuif_wr_biten
Active-high bit-level write-enable strobes.
Only asserted bit positions will change the register value during a write
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.
cpuif_wr_byte_en
Active-high byte-enable strobes for writes. Some CPU interfaces do not
provide byte enables and may drive this as all-ones.
Read Response
^^^^^^^^^^^^^
cpuif_rd_ack
Single-cycle strobe indicating a read transfer has completed.
Qualifies that the following signals are valid: ``cpuif_rd_err`` and
``cpuif_rd_data``
Qualifies ``cpuif_rd_err`` and ``cpuif_rd_data``.
cpuif_rd_err
If set, indicates that the read transaction failed and the CPUIF logic
should return an error response if possible.
Indicates that the read transaction failed. The CPU interface should return
an error response if possible.
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
^^^^^^^^^^^^^^
cpuif_wr_ack
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
If set, indicates that the write transaction failed and the CPUIF logic
should return an error response if possible.
Indicates that the write transaction failed. The CPU interface should return
an error response if possible.
Transfers
@@ -78,155 +65,7 @@ Transfers
Transfers have the following characteristics:
* Only one transfer can be initiated per clock-cycle. This is implicit as there
is only one set of request signals.
* The register block implementation shall guarantee that only one response can be
asserted in a given clock cycle. Only one ``cpuif_*_ack`` signal can be
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.
* Only one outstanding transaction is supported.
* The CPU interface must hold ``cpuif_req`` and request parameters stable until
the corresponding ``cpuif_*_ack`` is asserted.
* Responses shall arrive in the same order as requests.

View File

@@ -2,16 +2,17 @@ Introduction
============
The CPU interface logic layer provides an abstraction between the
application-specific bus protocol and the internal register file logic.
When exporting a design, you can select from a variety of popular CPU interface
protocols. These are described in more detail in the pages that follow.
application-specific bus protocol and the internal bus decoder logic.
When exporting a design, you can select from supported CPU interface protocols.
These are described in more detail in the pages that follow.
Bus Width
^^^^^^^^^
The CPU interface bus width is automatically determined from the contents of the
design being exported. The bus width is equal to the widest ``accesswidth``
encountered in the design.
The CPU interface bus width is inferred from the contents of the design.
It is intended to be equal to the widest ``accesswidth`` encountered in the
design. If the exported addrmap contains only external components, the width
cannot be inferred and will default to 32 bits.
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,
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
of the register block. If needed, the address width can be overridden to a larger range.
By default, the bit-width of the address bus will be the minimum size to span the
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
============
PeakRDL-BusDecoder is a free and open-source bus decoder generator for hierarchical register address maps.
This code generator translates your SystemRDL register description into a synthesizable
SystemVerilog RTL module that decodes CPU interface transactions and routes them to
multiple sub-address spaces (child addrmaps). This is particularly useful for:
PeakRDL-BusDecoder is a free and open-source bus decoder generator for hierarchical
SystemRDL address maps. It produces a synthesizable SystemVerilog RTL module that
accepts a single CPU interface (slave side) and fans transactions out to multiple
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
* 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
The generated bus decoder provides:
* 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
* Configurable pipelining options for designs with fast clock rates
* Broad support for SystemRDL 2.0 features
* Support for APB3, APB4, and AXI4-Lite (plus plugin-defined CPU interfaces)
* Configurable decode depth and array unrolling
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
@@ -32,6 +38,20 @@ The easiest way to use PeakRDL-BusDecoder is via the `PeakRDL command line tool
# Export!
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?
-----------------
@@ -55,10 +75,8 @@ Links
self
architecture
hwif
configuring
limitations
faq
licensing
api
@@ -69,29 +87,5 @@ Links
cpuif/introduction
cpuif/apb
cpuif/axi4lite
cpuif/avalon
cpuif/passthrough
cpuif/internal_protocol
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?
--------------------------------------
---------------------------------------
The LGPL license is intended for the code generator itself. This includes all
Python sources, Jinja template files, as well as testcase infrastructure not
explicitly mentioned in the exemptions below.

View File

@@ -1,53 +1,47 @@
Known Limitations
=================
Not all SystemRDL features are supported by this exporter. For a listing of
supported properties, see the appropriate property listing page in the sections
that follow.
The busdecoder exporter intentionally focuses on address decode and routing.
Some SystemRDL features are ignored, and a few are explicitly disallowed.
Alias Registers
---------------
Registers instantiated using the ``alias`` keyword are not supported yet.
Address Alignment
-----------------
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
-------------------
All address offsets & strides shall be a multiple of the cpuif bus width used. Specifically:
* Bus width is inferred by the maximum accesswidth used in the busdecoder.
* Each component's address and array stride shall be aligned to the bus width.
Wide Registers
--------------
If a register is wider than its ``accesswidth`` (a multi-word register), its
``accesswidth`` must match the CPU interface data width. Multi-word registers
with a smaller accesswidth are not supported.
Uniform accesswidth
-------------------
All registers within a register block shall use the same accesswidth.
Fields Spanning Sub-Words
-------------------------
If a field spans multiple sub-words of a wide register:
One exception is that registers with regwidth that is narrower than the cpuif
bus width are permitted, provided that their regwidth is equal to their accesswidth.
* Software-writable fields must have write buffering enabled
* 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 {
regwidth = 32;
accesswidth = 32;
} reg_a @ 0x00; // OK. Regular 32-bit register
CPU Interface Reset Location
----------------------------
Only ``cpuif_reset`` signals instantiated at the top-level addrmap (or above)
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 {
regwidth = 8;
accesswidth = 8;
} reg_c @ 0x10; // OK. Is aligned to the cpuif bus width
Unsupported Properties
----------------------
The following SystemRDL properties are explicitly rejected:
reg {
regwidth = 32;
accesswidth = 8;
} bad_reg @ 0x14; // NOT OK. accesswidth conflicts with cpuif width
* ``sharedextbus`` on addrmap/regfile components

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
sphinxcontrib-wavedrom
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

@@ -2,6 +2,10 @@ interface apb3_intf #(
parameter DATA_WIDTH = 32,
parameter ADDR_WIDTH = 32
);
// Clocking
logic PCLK;
logic PRESETn;
// Command
logic PSEL;
logic PENABLE;
@@ -15,6 +19,9 @@ interface apb3_intf #(
logic PSLVERR;
modport master (
input PCLK,
input PRESETn,
output PSEL,
output PENABLE,
output PWRITE,
@@ -27,6 +34,9 @@ interface apb3_intf #(
);
modport slave (
input PCLK,
input PRESETn,
input PSEL,
input PENABLE,
input PWRITE,

View File

@@ -2,6 +2,10 @@ interface apb4_intf #(
parameter DATA_WIDTH = 32,
parameter ADDR_WIDTH = 32
);
// Clocking
logic PCLK;
logic PRESETn;
// Command
logic PSEL;
logic PENABLE;
@@ -17,6 +21,9 @@ interface apb4_intf #(
logic PSLVERR;
modport master (
input PCLK,
input PRESETn,
output PSEL,
output PENABLE,
output PWRITE,
@@ -31,6 +38,9 @@ interface apb4_intf #(
);
modport slave (
input PCLK,
input PRESETn,
input PSEL,
input PENABLE,
input PWRITE,

View File

@@ -2,6 +2,9 @@ interface axi4lite_intf #(
parameter DATA_WIDTH = 32,
parameter ADDR_WIDTH = 32
);
logic ACLK;
logic ARESETn;
logic AWREADY;
logic AWVALID;
logic [ADDR_WIDTH-1:0] AWADDR;
@@ -27,6 +30,9 @@ interface axi4lite_intf #(
logic [1:0] RRESP;
modport master (
input ACLK,
input ARESETn,
input AWREADY,
output AWVALID,
output AWADDR,
@@ -53,15 +59,18 @@ interface axi4lite_intf #(
);
modport slave (
input ACLK,
input ARESETn,
output AWREADY,
// input AWVALID,
// input AWADDR,
input AWVALID,
input AWADDR,
input AWPROT,
output WREADY,
// input WVALID,
// input WDATA,
// input WSTRB,
input WVALID,
input WDATA,
input WSTRB,
input BREADY,
output BVALID,
@@ -73,8 +82,8 @@ interface axi4lite_intf #(
input ARPROT,
input RREADY,
// output RVALID,
// output RDATA,
// output RRESP
output RVALID,
output RDATA,
output RRESP
);
endinterface

View File

@@ -4,11 +4,14 @@ build-backend = "setuptools.build_meta"
[project]
name = "peakrdl-busdecoder"
version = "0.1.0"
version = "0.6.7"
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"
readme = "README.md"
license = { text = "LGPLv3" }
@@ -52,20 +55,21 @@ Documentation = "https://peakrdl-busdecoder.readthedocs.io/"
docs = [
"pygments-systemrdl>=1.3.0",
"sphinx-book-theme>=1.1.4",
"sphinxcontrib-wavedrom>=3.0.4",
]
test = [
"parameterized>=0.9.0",
"pytest>=7.4.4",
"pytest-cov>=4.1.0",
"pytest-xdist>=3.5.0",
"cocotb>=1.8.0",
"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"]
busdecoder = "peakrdl_busdecoder.__peakrdl__:Exporter"
# ---------------------- RUFF ----------------------
[tool.ruff]
line-length = 110
target-version = "py310"
@@ -95,12 +99,18 @@ ignore = [
quote-style = "double"
indent-style = "space"
# ---------------------- PYREFLY ----------------------
[tool.pyrefly]
# ---------------------- TY ----------------------
[tool.ty.environment]
python-version = "3.10"
# Default behavior: check bodies of untyped defs & infer return types.
untyped-def-behavior = "check-and-infer-return-type"
[tool.ty.src]
include = ["src"]
project-includes = ["**/*"]
project-excludes = ["**/__pycache__", "**/*venv/**/*"]
# ---------------------- PYTEST ----------------------
[tool.pytest.ini_options]
python_files = ["test_*.py", "*_test.py"]
markers = [
"simulation: marks tests as requiring cocotb simulation (deselect with '-m \"not simulation\"')",
"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
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.exporter import ExporterSubcommandPlugin
from .cpuif import BaseCpuif, apb3, apb4, axi4lite
from .cpuif import BaseCpuif, apb3, apb4, axi4lite, taxi_apb
from .exporter import BusDecoderExporter
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,
"apb4": apb4.APB4Cpuif,
"apb4-flat": apb4.APB4CpuifFlat,
"taxi-apb": taxi_apb.TaxiAPBCpuif,
"axi4-lite": axi4lite.AXI4LiteCpuif,
"axi4-lite-flat": axi4lite.AXI4LiteCpuifFlat,
}
@@ -69,7 +72,7 @@ class Exporter(ExporterSubcommandPlugin):
def get_cpuifs(self) -> dict[str, type[BaseCpuif]]:
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()
arg_group.add_argument(
@@ -111,7 +114,18 @@ class Exporter(ExporterSubcommandPlugin):
""",
)
def do_export(self, top_node: "AddrmapNode", options: "argparse.Namespace") -> None:
arg_group.add_argument(
"--max-decode-depth",
type=int,
default=1,
help="""Maximum depth for address decoder to descend into nested
addressable components. Value of 0 decodes all levels (infinite depth).
Value of 1 decodes only top-level children. Value of 2 decodes top-level
and one level deeper, etc. Default is 1.
""",
)
def do_export(self, top_node: AddrmapNode, options: argparse.Namespace) -> None:
cpuifs = self.get_cpuifs()
x = BusDecoderExporter()
@@ -123,4 +137,5 @@ class Exporter(ExporterSubcommandPlugin):
package_name=options.package_name,
address_width=options.addr_width,
cpuif_unroll=options.unroll,
max_decode_depth=options.max_decode_depth,
)

View File

@@ -1,5 +1,3 @@
from __future__ import annotations
from textwrap import indent
from types import EllipsisType
@@ -50,7 +48,7 @@ class IfBody(Body):
# --- Context manager for a branch ---
class _BranchCtx:
def __init__(self, outer: IfBody, condition: SupportsStr | None) -> None:
def __init__(self, outer: "IfBody", condition: SupportsStr | None) -> None:
self._outer = outer
# route through __getitem__ to reuse validation logic
self._body = outer[Ellipsis if condition is None else condition]
@@ -66,7 +64,7 @@ class IfBody(Body):
) -> bool:
return False
def cm(self, condition: SupportsStr | None) -> IfBody._BranchCtx:
def cm(self, condition: SupportsStr | None) -> "IfBody._BranchCtx":
"""Use with: with ifb.cm('cond') as b: ... ; use None for else."""
return IfBody._BranchCtx(self, condition)

View File

@@ -1,49 +1,39 @@
from typing import overload
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 .apb3_interface import APB3SVInterface
if TYPE_CHECKING:
from ...exporter import BusDecoderExporter
class APB3Cpuif(BaseCpuif):
template_path = "apb3_tmpl.sv"
is_interface = True
def _port_declaration(self, child: AddressableNode) -> str:
base = f"apb3_intf.master m_apb_{child.inst_name}"
def __init__(self, exp: "BusDecoderExporter") -> None:
super().__init__(exp)
self._interface = APB3SVInterface(self)
# When unrolled, current_idx is set - append it to the name
if child.current_idx is not None:
base = f"{base}_{'_'.join(map(str, child.current_idx))}"
# Only add array dimensions if this should be treated as an array
if self.check_is_array(child):
assert child.array_dimensions is not None
return f"{base} {''.join(f'[{dim}]' for dim in child.array_dimensions)}"
return base
@property
def is_interface(self) -> bool:
return self._interface.is_interface
@property
def port_declaration(self) -> str:
slave_ports: list[str] = ["apb3_intf.slave s_apb"]
master_ports: list[str] = list(map(self._port_declaration, self.addressable_children))
return ",\n".join(slave_ports + master_ports)
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:
if node is None or indexer is None:
# Node is none, so this is a slave signal
return f"s_apb.{signal}"
return self._interface.signal(signal, node, indexer)
# Master signal
return f"m_apb_{get_indexed_path(node.parent, node, indexer, skip_kw_filter=True)}.{signal}"
def fanout(self, node: AddressableNode) -> str:
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')}"
@@ -55,24 +45,60 @@ class APB3Cpuif(BaseCpuif):
fanout[self.signal("PADDR", node, "gi")] = self.signal("PADDR")
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] = {}
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("PREADY", node, "i")
fanin["cpuif_rd_err"] = self.signal("PSLVERR", node, "i")
return "\n".join(map(lambda kv: f"{kv[0]} = {kv[1]};", fanin.items()))
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("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(
self, node: AddressableNode, inst_name: str, array_idx: str, master_prefix: str, indexed_path: str
) -> list[str]:
"""Generate intermediate signal assignments for APB3 interface arrays."""
return [
f"assign {inst_name}_fanin_ready{array_idx} = {master_prefix}{indexed_path}.PREADY;",
f"assign {inst_name}_fanin_err{array_idx} = {master_prefix}{indexed_path}.PSLVERR;",
f"assign {inst_name}_fanin_data{array_idx} = {master_prefix}{indexed_path}.PRDATA;",
]

View File

@@ -1,46 +1,31 @@
from collections import deque
from typing import TYPE_CHECKING
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 .apb3_interface import APB3FlatInterface
if TYPE_CHECKING:
from ...exporter import BusDecoderExporter
class APB3CpuifFlat(BaseCpuif):
template_path = "apb3_tmpl.sv"
is_interface = False
def _port_declaration(self, child: AddressableNode) -> list[str]:
return [
f"input logic {self.signal('PCLK', child)}",
f"input logic {self.signal('PRESETn', child)}",
f"input logic {self.signal('PSELx', child)}",
f"input logic {self.signal('PENABLE', child)}",
f"input logic {self.signal('PWRITE', child)}",
f"input logic [{self.addr_width - 1}:0] {self.signal('PADDR', child)}",
f"input logic [{self.data_width - 1}:0] {self.signal('PWDATA', child)}",
f"output logic [{self.data_width - 1}:0] {self.signal('PRDATA', child)}",
f"output logic {self.signal('PREADY', child)}",
f"output logic {self.signal('PSLVERR', child)}",
]
def __init__(self, exp: "BusDecoderExporter") -> None:
super().__init__(exp)
self._interface = APB3FlatInterface(self)
@property
def is_interface(self) -> bool:
return self._interface.is_interface
@property
def port_declaration(self) -> str:
slave_ports: list[str] = [
f"input logic {self.signal('PCLK')}",
f"input logic {self.signal('PRESETn')}",
f"input logic {self.signal('PSELx')}",
f"input logic {self.signal('PENABLE')}",
f"input logic {self.signal('PWRITE')}",
f"input logic [{self.addr_width - 1}:0] {self.signal('PADDR')}",
f"input logic [{self.data_width - 1}:0] {self.signal('PWDATA')}",
f"output logic [{self.data_width - 1}:0] {self.signal('PRDATA')}",
f"output logic {self.signal('PREADY')}",
f"output logic {self.signal('PSLVERR')}",
]
master_ports: list[str] = []
for child in self.addressable_children:
master_ports.extend(self._port_declaration(child))
return ",\n".join(slave_ports + master_ports)
return self._interface.get_port_declaration("s_apb_", "m_apb_")
def signal(
self,
@@ -48,53 +33,52 @@ class APB3CpuifFlat(BaseCpuif):
node: AddressableNode | None = None,
idx: str | int | None = None,
) -> str:
if node is None:
# Node is none, so this is a slave signal
return f"s_apb_{signal}"
return self._interface.signal(signal, node, idx)
# Master signal
base = f"m_apb_{node.inst_name}"
if not self.check_is_array(node):
# Not an array or an unrolled element
if node.current_idx is not None:
# This is a specific instance of an unrolled array
return f"{base}_{signal}_{'_'.join(map(str, node.current_idx))}"
return f"{base}_{signal}"
# Is an array
if idx is not None:
return f"{base}_{signal}[{idx}]"
return f"{base}_{signal}[N_{node.inst_name.upper()}S]"
def fanout(self, node: AddressableNode) -> str:
def fanout(self, node: AddressableNode, array_stack: deque[int]) -> str:
fanout: dict[str, str] = {}
fanout[self.signal("PSELx", node)] = (
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')}"
)
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"
addr_comp = [f"{self.signal('PADDR')}"]
for i, stride in enumerate(array_stack):
addr_comp.append(f"(gi{i}*{SVInt(stride, self.addr_width)})")
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] = {}
if node is None:
fanin["cpuif_rd_ack"] = "'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"
if error:
fanin["cpuif_rd_ack"] = "'1"
fanin["cpuif_rd_err"] = "cpuif_rd_sel.cpuif_err"
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

@@ -0,0 +1,57 @@
"""APB3-specific interface implementations."""
from systemrdl.node import AddressableNode
from ...utils import clog2
from ..interface import FlatInterface, SVInterface
class APB3SVInterface(SVInterface):
"""APB3 SystemVerilog interface."""
def get_interface_type(self) -> str:
return "apb3_intf"
def get_slave_name(self) -> str:
return "s_apb"
def get_master_prefix(self) -> str:
return "m_apb_"
class APB3FlatInterface(FlatInterface):
"""APB3 flat signal interface."""
def get_slave_prefix(self) -> str:
return "s_apb_"
def get_master_prefix(self) -> str:
return "m_apb_"
def _get_slave_port_declarations(self, slave_prefix: str) -> list[str]:
return [
f"input logic {slave_prefix}PCLK",
f"input logic {slave_prefix}PRESETn",
f"input logic {slave_prefix}PSEL",
f"input logic {slave_prefix}PENABLE",
f"input logic {slave_prefix}PWRITE",
f"input logic [{self.cpuif.addr_width - 1}:0] {slave_prefix}PADDR",
f"input logic [{self.cpuif.data_width - 1}:0] {slave_prefix}PWDATA",
f"output logic [{self.cpuif.data_width - 1}:0] {slave_prefix}PRDATA",
f"output logic {slave_prefix}PREADY",
f"output logic {slave_prefix}PSLVERR",
]
def _get_master_port_declarations(self, child: AddressableNode, master_prefix: str) -> list[str]:
return [
f"output logic {self.signal('PCLK', child)}",
f"output logic {self.signal('PRESETn', child)}",
f"output logic {self.signal('PSEL', child)}",
f"output logic {self.signal('PENABLE', child)}",
f"output logic {self.signal('PWRITE', 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"input logic [{self.cpuif.data_width - 1}:0] {self.signal('PRDATA', child)}",
f"input logic {self.signal('PREADY', child)}",
f"input logic {self.signal('PSLVERR', child)}",
]

View File

@@ -19,13 +19,20 @@ assign cpuif_rd_addr = {{cpuif.signal("PADDR")}};
assign cpuif_wr_data = {{cpuif.signal("PWDATA")}};
assign {{cpuif.signal("PRDATA")}} = cpuif_rd_data;
assign {{cpuif.signal("PREADY")}} = cpuif_rd_ack;
assign {{cpuif.signal("PSLVERR")}} = cpuif_rd_err | cpuif_rd_sel.cpuif_err | cpuif_wr_sel.cpuif_err;
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

View File

@@ -1,50 +1,40 @@
from typing import overload
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 .apb4_interface import APB4SVInterface
if TYPE_CHECKING:
from ...exporter import BusDecoderExporter
class APB4Cpuif(BaseCpuif):
template_path = "apb4_tmpl.sv"
is_interface = True
def _port_declaration(self, child: AddressableNode) -> str:
base = f"apb4_intf.master m_apb_{child.inst_name}"
def __init__(self, exp: "BusDecoderExporter") -> None:
super().__init__(exp)
self._interface = APB4SVInterface(self)
# When unrolled, current_idx is set - append it to the name
if child.current_idx is not None:
base = f"{base}_{'_'.join(map(str, child.current_idx))}"
# Only add array dimensions if this should be treated as an array
if self.check_is_array(child):
assert child.array_dimensions is not None
return f"{base} {''.join(f'[{dim}]' for dim in child.array_dimensions)}"
return base
@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."""
slave_ports: list[str] = ["apb4_intf.slave s_apb"]
master_ports: list[str] = list(map(self._port_declaration, self.addressable_children))
return ",\n".join(slave_ports + master_ports)
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:
if node is None or indexer is None:
# Node is none, so this is a slave signal
return f"s_apb.{signal}"
return self._interface.signal(signal, node, indexer)
# Master signal
return f"m_apb_{get_indexed_path(node.parent, node, indexer, skip_kw_filter=True)}.{signal}"
def fanout(self, node: AddressableNode) -> str:
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')}"
@@ -58,24 +48,61 @@ class APB4Cpuif(BaseCpuif):
fanout[self.signal("PWDATA", node, "gi")] = "cpuif_wr_data"
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] = {}
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("PREADY", node, "i")
fanin["cpuif_rd_err"] = self.signal("PSLVERR", node, "i")
return "\n".join(map(lambda kv: f"{kv[0]} = {kv[1]};", fanin.items()))
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("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(
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

@@ -1,50 +1,31 @@
from collections import deque
from typing import TYPE_CHECKING
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 .apb4_interface import APB4FlatInterface
if TYPE_CHECKING:
from ...exporter import BusDecoderExporter
class APB4CpuifFlat(BaseCpuif):
template_path = "apb4_tmpl.sv"
is_interface = False
def _port_declaration(self, child: AddressableNode) -> list[str]:
return [
f"input logic {self.signal('PCLK', child)}",
f"input logic {self.signal('PRESETn', child)}",
f"input logic {self.signal('PSELx', child)}",
f"input logic {self.signal('PENABLE', child)}",
f"input logic {self.signal('PWRITE', child)}",
f"input logic [{self.addr_width - 1}:0] {self.signal('PADDR', child)}",
f"input logic [2:0] {self.signal('PPROT', child)}",
f"input logic [{self.data_width - 1}:0] {self.signal('PWDATA', child)}",
f"input logic [{self.data_width // 8 - 1}:0] {self.signal('PSTRB', child)}",
f"output logic [{self.data_width - 1}:0] {self.signal('PRDATA', child)}",
f"output logic {self.signal('PREADY', child)}",
f"output logic {self.signal('PSLVERR', child)}",
]
def __init__(self, exp: "BusDecoderExporter") -> None:
super().__init__(exp)
self._interface = APB4FlatInterface(self)
@property
def is_interface(self) -> bool:
return self._interface.is_interface
@property
def port_declaration(self) -> str:
slave_ports: list[str] = [
f"input logic {self.signal('PCLK')}",
f"input logic {self.signal('PRESETn')}",
f"input logic {self.signal('PSELx')}",
f"input logic {self.signal('PENABLE')}",
f"input logic {self.signal('PWRITE')}",
f"input logic [{self.addr_width - 1}:0] {self.signal('PADDR')}",
f"input logic [2:0] {self.signal('PPROT')}",
f"input logic [{self.data_width - 1}:0] {self.signal('PWDATA')}",
f"input logic [{self.data_width // 8 - 1}:0] {self.signal('PSTRB')}",
f"output logic [{self.data_width - 1}:0] {self.signal('PRDATA')}",
f"output logic {self.signal('PREADY')}",
f"output logic {self.signal('PSLVERR')}",
]
master_ports: list[str] = []
for child in self.addressable_children:
master_ports.extend(self._port_declaration(child))
return ",\n".join(slave_ports + master_ports)
return self._interface.get_port_declaration("s_apb_", "m_apb_")
def signal(
self,
@@ -52,55 +33,53 @@ class APB4CpuifFlat(BaseCpuif):
node: AddressableNode | None = None,
idx: str | int | None = None,
) -> str:
if node is None:
# Node is none, so this is a slave signal
return f"s_apb_{signal}"
return self._interface.signal(signal, node, idx)
# Master signal
base = f"m_apb_{node.inst_name}"
if not self.check_is_array(node):
# Not an array or an unrolled element
if node.current_idx is not None:
# This is a specific instance of an unrolled array
return f"{base}_{signal}_{'_'.join(map(str, node.current_idx))}"
return f"{base}_{signal}"
# Is an array
if idx is not None:
return f"{base}_{signal}[{idx}]"
return f"{base}_{signal}[N_{node.inst_name.upper()}S]"
def fanout(self, node: AddressableNode) -> str:
def fanout(self, node: AddressableNode, array_stack: deque[int]) -> str:
fanout: dict[str, str] = {}
fanout[self.signal("PSELx", node)] = (
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')}"
)
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"
addr_comp = [f"{self.signal('PADDR')}"]
for i, stride in enumerate(array_stack):
addr_comp.append(f"(gi{i}*{SVInt(stride, self.addr_width)})")
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] = {}
if node is None:
fanin["cpuif_rd_ack"] = "'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"
if error:
fanin["cpuif_rd_ack"] = "'1"
fanin["cpuif_rd_err"] = "cpuif_rd_sel.cpuif_err"
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

@@ -0,0 +1,61 @@
"""APB4-specific interface implementations."""
from systemrdl.node import AddressableNode
from ...utils import clog2
from ..interface import FlatInterface, SVInterface
class APB4SVInterface(SVInterface):
"""APB4 SystemVerilog interface."""
def get_interface_type(self) -> str:
return "apb4_intf"
def get_slave_name(self) -> str:
return "s_apb"
def get_master_prefix(self) -> str:
return "m_apb_"
class APB4FlatInterface(FlatInterface):
"""APB4 flat signal interface."""
def get_slave_prefix(self) -> str:
return "s_apb_"
def get_master_prefix(self) -> str:
return "m_apb_"
def _get_slave_port_declarations(self, slave_prefix: str) -> list[str]:
return [
f"input logic {slave_prefix}PCLK",
f"input logic {slave_prefix}PRESETn",
f"input logic {slave_prefix}PSEL",
f"input logic {slave_prefix}PENABLE",
f"input logic {slave_prefix}PWRITE",
f"input logic [{self.cpuif.addr_width - 1}:0] {slave_prefix}PADDR",
f"input logic [2:0] {slave_prefix}PPROT",
f"input logic [{self.cpuif.data_width - 1}:0] {slave_prefix}PWDATA",
f"input logic [{self.cpuif.data_width // 8 - 1}:0] {slave_prefix}PSTRB",
f"output logic [{self.cpuif.data_width - 1}:0] {slave_prefix}PRDATA",
f"output logic {slave_prefix}PREADY",
f"output logic {slave_prefix}PSLVERR",
]
def _get_master_port_declarations(self, child: AddressableNode, master_prefix: str) -> list[str]:
return [
f"output logic {self.signal('PCLK', child)}",
f"output logic {self.signal('PRESETn', child)}",
f"output logic {self.signal('PSEL', child)}",
f"output logic {self.signal('PENABLE', child)}",
f"output logic {self.signal('PWRITE', 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 [{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"input logic [{self.cpuif.data_width - 1}:0] {self.signal('PRDATA', child)}",
f"input logic {self.signal('PREADY', child)}",
f"input logic {self.signal('PSLVERR', 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)
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
assert_wr_sel: assert (@(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 %}
@@ -22,13 +20,20 @@ assign cpuif_wr_data = {{cpuif.signal("PWDATA")}};
assign cpuif_wr_byte_en = {{cpuif.signal("PSTRB")}};
assign {{cpuif.signal("PRDATA")}} = cpuif_rd_data;
assign {{cpuif.signal("PREADY")}} = cpuif_rd_ack;
assign {{cpuif.signal("PSLVERR")}} = cpuif_rd_err | cpuif_rd_sel.cpuif_err | cpuif_wr_sel.cpuif_err;
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

View File

@@ -1,50 +1,40 @@
from typing import overload
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 .axi4_lite_interface import AXI4LiteSVInterface
if TYPE_CHECKING:
from ...exporter import BusDecoderExporter
class AXI4LiteCpuif(BaseCpuif):
template_path = "axi4lite_tmpl.sv"
is_interface = True
template_path = "axi4_lite_tmpl.sv"
def _port_declaration(self, child: AddressableNode) -> str:
base = f"axi4lite_intf.master m_axil_{child.inst_name}"
def __init__(self, exp: "BusDecoderExporter") -> None:
super().__init__(exp)
self._interface = AXI4LiteSVInterface(self)
# When unrolled, current_idx is set - append it to the name
if child.current_idx is not None:
base = f"{base}_{'_'.join(map(str, child.current_idx))}"
# Only add array dimensions if this should be treated as an array
if self.check_is_array(child):
assert child.array_dimensions is not None
return f"{base} {''.join(f'[{dim}]' for dim in child.array_dimensions)}"
return base
@property
def is_interface(self) -> bool:
return self._interface.is_interface
@property
def port_declaration(self) -> str:
"""Returns the port declaration for the AXI4-Lite interface."""
slave_ports: list[str] = ["axi4lite_intf.slave s_axil"]
master_ports: list[str] = list(map(self._port_declaration, self.addressable_children))
return ",\n".join(slave_ports + master_ports)
return self._interface.get_port_declaration("s_axil", "m_axil_")
@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, indexer: str | None = None) -> str: ...
def signal(self, signal: str, node: AddressableNode | None = None, indexer: str | None = None) -> str:
if node is None or indexer is None:
# Node is none, so this is a slave signal
return f"s_axil.{signal}"
return self._interface.signal(signal, node, indexer)
# Master signal
return f"m_axil_{get_indexed_path(node.parent, node, indexer, skip_kw_filter=True)}.{signal}"
def fanout(self, node: AddressableNode) -> str:
def fanout(self, node: AddressableNode, array_stack: deque[int]) -> str:
fanout: dict[str, str] = {}
wr_sel = f"cpuif_wr_sel.{get_indexed_path(self.exp.ds.top_node, node, 'gi')}"
@@ -73,23 +63,72 @@ class AXI4LiteCpuif(BaseCpuif):
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] = {}
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:
# 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:
fanin["cpuif_rd_data"] = self.signal("RDATA", node, "i")
return "\n".join(f"{lhs} = {rhs};" for lhs, rhs in fanin.items())
def fanin_intermediate_assignments(
self, node: AddressableNode, inst_name: str, array_idx: str, master_prefix: str, indexed_path: str
) -> list[str]:
"""Generate intermediate signal assignments for AXI4-Lite interface arrays."""
return [
f"assign {inst_name}_fanin_ready{array_idx} = {master_prefix}{indexed_path}.RVALID;",
f"assign {inst_name}_fanin_err{array_idx} = {master_prefix}{indexed_path}.RRESP[1];",
f"assign {inst_name}_fanin_data{array_idx} = {master_prefix}{indexed_path}.RDATA;",
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,58 +1,57 @@
from typing import overload
from collections import deque
from typing import TYPE_CHECKING, overload
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 .axi4_lite_interface import AXI4LiteFlatInterface
if TYPE_CHECKING:
from ...exporter import BusDecoderExporter
class AXI4LiteCpuifFlat(BaseCpuif):
template_path = "axi4lite_tmpl.sv"
is_interface = True
"""Verilator-friendly variant that flattens the AXI4-Lite interface ports."""
def _port_declaration(self, child: AddressableNode) -> str:
base = f"axi4lite_intf.master m_axil_{child.inst_name}"
template_path = "axi4_lite_tmpl.sv"
# When unrolled, current_idx is set - append it to the name
if child.current_idx is not None:
base = f"{base}_{'_'.join(map(str, child.current_idx))}"
def __init__(self, exp: "BusDecoderExporter") -> None:
super().__init__(exp)
self._interface = AXI4LiteFlatInterface(self)
# Only add array dimensions if this should be treated as an array
if self.check_is_array(child):
assert child.array_dimensions is not None
return f"{base} {''.join(f'[{dim}]' for dim in child.array_dimensions)}"
return base
@property
def is_interface(self) -> bool:
return self._interface.is_interface
@property
def port_declaration(self) -> str:
"""Returns the port declaration for the AXI4-Lite interface."""
slave_ports: list[str] = ["axi4lite_intf.slave s_axil"]
master_ports: list[str] = list(map(self._port_declaration, self.addressable_children))
return ",\n".join(slave_ports + master_ports)
return self._interface.get_port_declaration("s_axil_", "m_axil_")
@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, indexer: str | None = None) -> str: ...
def signal(self, signal: str, node: AddressableNode | None = None, indexer: str | None = None) -> str:
if node is None or indexer is None:
# Node is none, so this is a slave signal
return f"s_axil.{signal}"
return self._interface.signal(signal, node, indexer)
# Master signal
return f"m_axil_{get_indexed_path(node.parent, node, indexer, skip_kw_filter=True)}.{signal}"
def fanout(self, node: AddressableNode) -> str:
def fanout(self, node: AddressableNode, array_stack: deque[int]) -> 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')}"
rd_sel = f"cpuif_rd_sel.{get_indexed_path(self.exp.ds.top_node, node, 'gi')}"
# Write address channel
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")
# Write data channel
@@ -65,7 +64,7 @@ class AXI4LiteCpuifFlat(BaseCpuif):
# Read address channel
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")
# Read data channel (master -> slave)
@@ -73,23 +72,33 @@ class AXI4LiteCpuifFlat(BaseCpuif):
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] = {}
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:
# 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:
fanin["cpuif_rd_data"] = self.signal("RDATA", node, "i")
return "\n".join(f"{lhs} = {rhs};" for lhs, rhs in fanin.items())

View File

@@ -0,0 +1,85 @@
"""AXI4-Lite-specific interface implementations."""
from systemrdl.node import AddressableNode
from ...utils import clog2
from ..interface import FlatInterface, SVInterface
class AXI4LiteSVInterface(SVInterface):
"""AXI4-Lite SystemVerilog interface."""
def get_interface_type(self) -> str:
return "axi4lite_intf"
def get_slave_name(self) -> str:
return "s_axil"
def get_master_prefix(self) -> str:
return "m_axil_"
class AXI4LiteFlatInterface(FlatInterface):
"""AXI4-Lite flat signal interface."""
def get_slave_prefix(self) -> str:
return "s_axil_"
def get_master_prefix(self) -> str:
return "m_axil_"
def _get_slave_port_declarations(self, slave_prefix: str) -> list[str]:
return [
# Write address channel
f"input logic {slave_prefix}AWVALID",
f"output logic {slave_prefix}AWREADY",
f"input logic [{self.cpuif.addr_width - 1}:0] {slave_prefix}AWADDR",
f"input logic [2:0] {slave_prefix}AWPROT",
# Write data channel
f"input logic {slave_prefix}WVALID",
f"output logic {slave_prefix}WREADY",
f"input logic [{self.cpuif.data_width - 1}:0] {slave_prefix}WDATA",
f"input logic [{self.cpuif.data_width // 8 - 1}:0] {slave_prefix}WSTRB",
# Write response channel
f"output logic {slave_prefix}BVALID",
f"input logic {slave_prefix}BREADY",
f"output logic [1:0] {slave_prefix}BRESP",
# Read address channel
f"input logic {slave_prefix}ARVALID",
f"output logic {slave_prefix}ARREADY",
f"input logic [{self.cpuif.addr_width - 1}:0] {slave_prefix}ARADDR",
f"input logic [2:0] {slave_prefix}ARPROT",
# Read data channel
f"output logic {slave_prefix}RVALID",
f"input logic {slave_prefix}RREADY",
f"output logic [{self.cpuif.data_width - 1}:0] {slave_prefix}RDATA",
f"output logic [1:0] {slave_prefix}RRESP",
]
def _get_master_port_declarations(self, child: AddressableNode, master_prefix: str) -> list[str]:
return [
# Write address channel
f"output logic {self.signal('AWVALID', child)}",
f"input logic {self.signal('AWREADY', child)}",
f"output logic [{clog2(child.size) - 1}:0] {self.signal('AWADDR', child)}",
f"output logic [2:0] {self.signal('AWPROT', child)}",
# Write data channel
f"output logic {self.signal('WVALID', child)}",
f"input logic {self.signal('WREADY', child)}",
f"output logic [{self.cpuif.data_width - 1}:0] {self.signal('WDATA', child)}",
f"output logic [{self.cpuif.data_width // 8 - 1}:0] {self.signal('WSTRB', child)}",
# Write response channel
f"input logic {self.signal('BVALID', child)}",
f"output logic {self.signal('BREADY', child)}",
f"input logic [1:0] {self.signal('BRESP', child)}",
# Read address channel
f"output logic {self.signal('ARVALID', child)}",
f"input logic {self.signal('ARREADY', child)}",
f"output logic [{clog2(child.size) - 1}:0] {self.signal('ARADDR', child)}",
f"output logic [2:0] {self.signal('ARPROT', child)}",
# Read data channel
f"input logic {self.signal('RVALID', child)}",
f"output logic {self.signal('RREADY', child)}",
f"input logic [{self.cpuif.data_width - 1}:0] {self.signal('RDATA', child)}",
f"input logic [1:0] {self.signal('RRESP', child)}",
]

View File

@@ -15,6 +15,7 @@
$bits({{cpuif.signal("WDATA")}}), {{ds.package_name}}::{{ds.module_name|upper}}_DATA_WIDTH);
end
`ifdef PEAKRDL_ASSERTIONS
// Simple handshake sanity (one-cycle implication; relax/adjust as needed)
assert_rd_resp_enc: assert property (@(posedge {{cpuif.signal("ACLK")}})
{{cpuif.signal("RVALID")}} |-> (^{{cpuif.signal("RRESP")}} !== 1'bx))
@@ -24,11 +25,24 @@
{{cpuif.signal("BVALID")}} |-> (^{{cpuif.signal("BRESP")}} !== 1'bx))
else $error("BRESP must be a legal AXI response when BVALID is high");
`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_wr_en = {{cpuif.signal("AWVALID")}} & {{cpuif.signal("WVALID")}};
assign cpuif_wr_en = axi_wr_valid;
assign cpuif_rd_en = {{cpuif.signal("ARVALID")}};
assign cpuif_wr_addr = {{cpuif.signal("AWADDR")}};
@@ -42,17 +56,26 @@ assign cpuif_wr_byte_en = {{cpuif.signal("WSTRB")}};
// Read: ack=RVALID, err=RRESP[1] (SLVERR/DECERR), data=RDATA
//
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;
// Write: ack=BVALID, err=BRESP[1]
assign {{cpuif.signal("BVALID")}} = cpuif_wr_ack;
assign {{cpuif.signal("BRESP")}} = (cpuif_wr_err | cpuif_wr_sel.cpuif_err | cpuif_rd_sel.cpuif_err) ? 2'b10 : 2'b00;
assign cpuif_wr_ack_int = cpuif_wr_ack | cpuif_wr_sel.cpuif_err | axi_wr_invalid;
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|walk(cpuif=cpuif)}}
{%- if cpuif.is_interface %}
//--------------------------------------------------------------------------
// Intermediate signals for interface array fanin
//--------------------------------------------------------------------------
{{fanin_intermediate|walk(cpuif=cpuif)}}
{%- endif %}
//--------------------------------------------------------------------------
// Fanin CPU Bus interface signals

View File

@@ -1,5 +1,6 @@
import inspect
import os
from collections import deque
from typing import TYPE_CHECKING
import jinja2 as jj
@@ -7,6 +8,7 @@ from systemrdl.node import AddressableNode
from ..utils import clog2, get_indexed_path, is_pow2, roundup_pow2
from .fanin_gen import FaninGenerator
from .fanin_intermediate_gen import FaninIntermediateGenerator
from .fanout_gen import FanoutGenerator
if TYPE_CHECKING:
@@ -24,11 +26,7 @@ class BaseCpuif:
@property
def addressable_children(self) -> list[AddressableNode]:
return [
child
for child in self.exp.ds.top_node.children(unroll=self.unroll)
if isinstance(child, AddressableNode)
]
return self.exp.ds.get_addressable_children_at_depth(unroll=self.unroll)
@property
def addr_width(self) -> int:
@@ -84,19 +82,20 @@ class BaseCpuif:
loader=loader,
undefined=jj.StrictUndefined,
)
jj_env.tests["array"] = self.check_is_array # type: ignore
jj_env.filters["clog2"] = clog2 # type: ignore
jj_env.filters["is_pow2"] = is_pow2 # type: ignore
jj_env.filters["roundup_pow2"] = roundup_pow2 # type: ignore
jj_env.filters["address_slice"] = self.get_address_slice # type: ignore
jj_env.filters["get_path"] = lambda x: get_indexed_path(self.exp.ds.top_node, x, "i") # type: ignore
jj_env.filters["walk"] = self.exp.walk # type: ignore
jj_env.tests["array"] = self.check_is_array
jj_env.filters["clog2"] = clog2
jj_env.filters["is_pow2"] = is_pow2
jj_env.filters["roundup_pow2"] = roundup_pow2
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")
jj_env.filters["walk"] = self.exp.walk
context = { # type: ignore
context = {
"cpuif": self,
"ds": self.exp.ds,
"fanout": FanoutGenerator,
"fanin": FaninGenerator,
"fanin_intermediate": FaninIntermediateGenerator,
}
template = jj_env.get_template(self.template_path)
@@ -108,11 +107,36 @@ class BaseCpuif:
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
def fanin(self, node: AddressableNode | None = None) -> str:
def fanin_wr(self, node: AddressableNode | None = None, *, error: bool = False) -> str:
raise NotImplementedError
def readback(self, node: AddressableNode | None = None) -> str:
def fanin_rd(self, node: AddressableNode | None = None, *, error: bool = False) -> str:
raise NotImplementedError
def fanin_intermediate_assignments(
self, node: AddressableNode, inst_name: str, array_idx: str, master_prefix: str, indexed_path: str
) -> list[str]:
"""Generate intermediate signal assignments for interface array fanin.
This method should be implemented by cpuif classes that use interfaces.
It returns a list of assignment strings that copy signals from interface
arrays to intermediate unpacked arrays using constant (genvar) indexing.
Args:
node: The addressable node
inst_name: Instance name for the intermediate signals
array_idx: Array index string (e.g., "[gi0][gi1]")
master_prefix: Master interface prefix
indexed_path: Indexed path to the interface element
Returns:
List of assignment strings
"""
return [] # Default: no intermediate assignments needed
def fanin_intermediate_declarations(self, node: AddressableNode) -> list[str]:
"""Optional extra intermediate signal declarations for interface arrays."""
return []

View File

@@ -20,13 +20,24 @@ class FaninGenerator(BusDecoderListener):
self._stack: deque[Body] = deque()
cb = CombinationalBody()
cb += cpuif.fanin()
cb += cpuif.readback()
cb += cpuif.fanin_wr()
cb += cpuif.fanin_rd()
self._stack.append(cb)
def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None:
action = super().enter_AddressableComponent(node)
should_generate = action == WalkerAction.SkipDescendants
if not should_generate and self._ds.max_decode_depth == 0:
for child in node.children():
if isinstance(child, AddressableNode):
break
else:
should_generate = True
if not should_generate:
return action
if node.array_dimensions:
for i, dim in enumerate(node.array_dimensions):
fb = ForLoopBody(
@@ -36,17 +47,14 @@ class FaninGenerator(BusDecoderListener):
)
self._stack.append(fb)
if action == WalkerAction.Continue:
ifb = IfBody()
with ifb.cm(
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)}"
) as b:
b += self._cpuif.fanin(node)
with ifb.cm(f"cpuif_wr_sel.{get_indexed_path(self._cpuif.exp.ds.top_node, node)}") as b:
b += self._cpuif.fanin_wr(node)
self._stack[-1] += ifb
ifb = IfBody()
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
return action
@@ -62,4 +70,14 @@ class FaninGenerator(BusDecoderListener):
super().exit_AddressableComponent(node)
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))

View File

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

View File

@@ -23,6 +23,17 @@ class FanoutGenerator(BusDecoderListener):
def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None:
action = super().enter_AddressableComponent(node)
should_generate = action == WalkerAction.SkipDescendants
if not should_generate and self._ds.max_decode_depth == 0:
for child in node.children():
if isinstance(child, AddressableNode):
break
else:
should_generate = True
if not should_generate:
return action
if node.array_dimensions:
for i, dim in enumerate(node.array_dimensions):
fb = ForLoopBody(
@@ -32,8 +43,7 @@ class FanoutGenerator(BusDecoderListener):
)
self._stack.append(fb)
if action == WalkerAction.Continue:
self._stack[-1] += self._cpuif.fanout(node)
self._stack[-1] += self._cpuif.fanout(node, self._array_stride_stack)
return action

View File

@@ -0,0 +1,202 @@
"""Interface abstraction for handling flat and non-flat signal declarations."""
import re
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
from systemrdl.node import AddressableNode
from ..utils import get_indexed_path
if TYPE_CHECKING:
from .base_cpuif import BaseCpuif
class Interface(ABC):
"""Abstract base class for interface signal handling."""
def __init__(self, cpuif: "BaseCpuif") -> None:
self.cpuif = cpuif
@property
@abstractmethod
def is_interface(self) -> bool:
"""Whether this uses SystemVerilog interfaces."""
...
@abstractmethod
def get_port_declaration(self, slave_name: str, master_prefix: str) -> str:
"""
Generate port declarations for the interface.
Args:
slave_name: Name of the slave interface/signal prefix
master_prefix: Prefix for master interfaces/signals
Returns:
Port declarations as a string
"""
...
@abstractmethod
def signal(
self,
signal: str,
node: AddressableNode | None = None,
indexer: str | int | None = None,
) -> str:
"""
Generate signal reference.
Args:
signal: Signal name
node: Optional addressable node for master signals
indexer: Optional indexer for arrays.
For SVInterface: str like "i" or "gi" for loop indices
For FlatInterface: str or int for array subscript
Returns:
Signal reference as a string
"""
...
class SVInterface(Interface):
"""SystemVerilog interface-based signal handling."""
slave_modport_name = "slave"
master_modport_name = "master"
@property
def is_interface(self) -> bool:
return True
def get_port_declaration(self, slave_name: str, master_prefix: str) -> str:
"""Generate SystemVerilog interface port declarations."""
slave_ports: list[str] = [f"{self.get_interface_type()}.{self.slave_modport_name} {slave_name}"]
master_ports: list[str] = []
for child in self.cpuif.addressable_children:
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
if child.current_idx is not None:
base = f"{base}_{'_'.join(map(str, child.current_idx))}" # ty: ignore
# Only add array dimensions if this should be treated as an array
if self.cpuif.check_is_array(child):
assert child.array_dimensions is not None
base = f"{base} {''.join(f'[{dim}]' for dim in child.array_dimensions)}"
master_ports.append(base)
return ",\n".join(slave_ports + master_ports)
def signal(
self,
signal: str,
node: AddressableNode | None = None,
indexer: str | int | None = None,
) -> str:
"""Generate SystemVerilog interface signal reference."""
# SVInterface only supports string indexers (loop variable names like "i", "gi")
if indexer is not None and not isinstance(indexer, str):
raise TypeError(f"SVInterface.signal() requires string indexer, got {type(indexer).__name__}")
if node is None or indexer is None:
# Node is none, so this is a slave signal
slave_name = self.get_slave_name()
return f"{slave_name}.{signal}"
# Master signal
master_prefix = self.get_master_prefix()
return f"{master_prefix}{get_indexed_path(node.parent, node, indexer, skip_kw_filter=True)}.{signal}"
@abstractmethod
def get_interface_type(self) -> str:
"""Get the SystemVerilog interface type name."""
...
@abstractmethod
def get_slave_name(self) -> str:
"""Get the slave interface instance name."""
...
@abstractmethod
def get_master_prefix(self) -> str:
"""Get the master interface name prefix."""
...
class FlatInterface(Interface):
"""Flat signal-based interface handling."""
@property
def is_interface(self) -> bool:
return False
def get_port_declaration(self, slave_name: str, master_prefix: str) -> str:
"""Generate flat port declarations."""
slave_ports = self._get_slave_port_declarations(slave_name)
master_ports: list[str] = []
for child in self.cpuif.addressable_children:
master_ports.extend(self._get_master_port_declarations(child, master_prefix))
return ",\n".join(slave_ports + master_ports)
def signal(
self,
signal: str,
node: AddressableNode | None = None,
indexer: str | int | None = None,
) -> str:
"""Generate flat signal reference."""
if node is None:
# Node is none, so this is a slave signal
slave_prefix = self.get_slave_prefix()
return f"{slave_prefix}{signal}"
# Master signal
master_prefix = self.get_master_prefix()
base = f"{master_prefix}{node.inst_name}"
if not self.cpuif.check_is_array(node):
# Not an array or an unrolled element
if node.current_idx is not None:
# This is a specific instance of an unrolled array
return f"{base}_{signal}_{'_'.join(map(str, node.current_idx))}"
return f"{base}_{signal}"
# Is an array
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}[N_{node.inst_name.upper()}S]"
@abstractmethod
def _get_slave_port_declarations(self, slave_prefix: str) -> list[str]:
"""Get slave port declarations."""
...
@abstractmethod
def _get_master_port_declarations(self, child: AddressableNode, master_prefix: str) -> list[str]:
"""Get master port declarations for a child node."""
...
@abstractmethod
def get_slave_prefix(self) -> str:
"""Get the slave signal name prefix."""
...
@abstractmethod
def get_master_prefix(self) -> str:
"""Get the master signal name prefix."""
...

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

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

View File

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

View File

@@ -1,5 +1,4 @@
import os
from datetime import datetime
from importlib.metadata import version
from pathlib import Path
from typing import TYPE_CHECKING, Any, TypedDict
@@ -17,6 +16,7 @@ from .identifier_filter import kw_filter as kwf
from .listener import BusDecoderListener
from .struct_gen import StructGenerator
from .sv_int import SVInt
from .utils import clog2
from .validate_design import DesignValidator
@@ -27,6 +27,7 @@ class ExporterKwargs(TypedDict, total=False):
address_width: int
cpuif_unroll: bool
reuse_hwif_typedefs: bool
max_decode_depth: int
if TYPE_CHECKING:
@@ -57,8 +58,9 @@ class BusDecoderExporter:
loader=c_loader,
undefined=jj.StrictUndefined,
)
self.jj_env.filters["kwf"] = kwf # type: ignore
self.jj_env.filters["walk"] = self.walk # type: ignore
self.jj_env.filters["kwf"] = kwf
self.jj_env.filters["walk"] = self.walk
self.jj_env.filters["clog2"] = clog2
def export(self, node: RootNode | AddrmapNode, output_dir: str, **kwargs: Unpack[ExporterKwargs]) -> None:
"""
@@ -84,6 +86,11 @@ class BusDecoderExporter:
cpuif_unroll: bool
Unroll arrayed addressable nodes into separate instances in the CPU
interface. By default, arrayed nodes are kept as arrays.
max_decode_depth: int
Maximum depth for address decoder to descend into nested addressable
components. A value of 0 decodes all levels (infinite depth). A value
of 1 decodes only top-level children. A value of 2 decodes top-level
and one level deeper, etc. By default, the decoder descends 1 level deep.
"""
# If it is the root node, skip to top addrmap
if isinstance(node, RootNode):
@@ -91,7 +98,7 @@ class BusDecoderExporter:
else:
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
@@ -106,8 +113,7 @@ class BusDecoderExporter:
DesignValidator(self).do_validate()
# Build Jinja template context
context = { # type: ignore
"current_date": datetime.now().strftime("%Y-%m-%d"),
context = {
"version": version("peakrdl-busdecoder"),
"cpuif": self.cpuif,
"cpuif_decode": DecodeLogicGenerator,

View File

@@ -1,6 +1,6 @@
from collections import deque
from systemrdl.node import AddressableNode
from systemrdl.node import AddressableNode, RegNode
from systemrdl.walker import RDLListener, WalkerAction
from .design_state import DesignState
@@ -12,15 +12,44 @@ class BusDecoderListener(RDLListener):
self._ds = ds
self._depth = 0
def should_skip_node(self, node: AddressableNode) -> bool:
"""Check if this node should be skipped (not decoded)."""
# Check if current depth exceeds max depth
# max_decode_depth semantics:
# - 0 means decode all levels (infinite)
# - 1 means decode only top level (depth 0)
# - 2 means decode top + 1 level (depth 0 and 1)
# - N means decode down to depth N-1
if self._ds.max_decode_depth > 0 and self._depth >= self._ds.max_decode_depth:
return True
# Check if this node only contains external addressable children
if node != self._ds.top_node and not isinstance(node, RegNode):
if any(isinstance(c, AddressableNode) for c in node.children()) and all(
c.external for c in node.children() if isinstance(c, AddressableNode)
):
return True
return False
def enter_AddressableComponent(self, node: AddressableNode) -> WalkerAction | None:
if node.array_dimensions:
assert node.array_stride is not None, "Array stride should be defined for arrayed components"
self._array_stride_stack.extend(node.array_dimensions)
current_stride = node.array_stride
self._array_stride_stack.append(current_stride)
# Work backwards from rightmost to leftmost dimension (fastest to slowest changing)
# Each dimension's stride is the product of its size and the previous dimension's stride
for dim in node.array_dimensions[-1:0:-1]:
current_stride = current_stride * dim
self._array_stride_stack.appendleft(current_stride)
self._depth += 1
if self._depth > 1:
# Check if we should skip this node's descendants
if self.should_skip_node(node):
return WalkerAction.SkipDescendants
return WalkerAction.Continue
def exit_AddressableComponent(self, node: AddressableNode) -> None:

View File

@@ -1,13 +1,11 @@
//==========================================================
// Module: {{ds.module_name}}
// Description: CPU Interface Bus Decoder
// Author: PeakRDL-busdecoder
// Author: PeakRDL-BusDecoder
// License: LGPL-3.0
// Date: {{current_date}}
// Version: {{version}}
// Links:
// - https://github.com/arnavsacheti/PeakRDL-busdecoder
// - https://github.com/arnavsacheti/PeakRDL-BusDecoder
//==========================================================
@@ -17,7 +15,6 @@ module {{ds.module_name}}
) {%- endif %} (
{{cpuif.port_declaration|indent(4)}}
);
//--------------------------------------------------------------------------
// CPU Bus interface logic
//--------------------------------------------------------------------------
@@ -46,14 +43,14 @@ module {{ds.module_name}}
//--------------------------------------------------------------------------
// Slave <-> Internal CPUIF <-> Master
//--------------------------------------------------------------------------
{{-cpuif.get_implementation()|indent(4)}}
{{cpuif.get_implementation()|indent(4)}}
//--------------------------------------------------------------------------
// Write Address Decoder
//--------------------------------------------------------------------------
always_comb begin
// Default all write select signals to 0
cpuif_wr_sel = '0;
cpuif_wr_sel = '{default: '0};
if (cpuif_req && cpuif_wr_en) begin
// A write request is pending
@@ -68,7 +65,7 @@ module {{ds.module_name}}
//--------------------------------------------------------------------------
always_comb begin
// Default all read select signals to 0
cpuif_rd_sel = '0;
cpuif_rd_sel = '{default: '0};
if (cpuif_req && cpuif_rd_en) begin
// A read request is pending

View File

@@ -1,10 +1,20 @@
// Generated by PeakRDL-busdecoder - A free and open-source SystemVerilog generator
// https://github.com/arnavsacheti/PeakRDL-busdecoder
//==========================================================
// Package: {{ds.package_name}}
// Description: CPU Interface Bus Decoder Package
// Author: PeakRDL-BusDecoder
// License: LGPL-3.0
// Version: {{version}}
// Links:
// - https://github.com/arnavsacheti/PeakRDL-BusDecoder
//==========================================================
package {{ds.package_name}};
localparam {{ds.module_name.upper()}}_DATA_WIDTH = {{ds.cpuif_data_width}};
localparam {{ds.module_name.upper()}}_MIN_ADDR_WIDTH = {{ds.addr_width}};
localparam {{ds.module_name.upper()}}_SIZE = {{SVInt(ds.top_node.size)}};
{%- for child in cpuif.addressable_children %}
localparam {{ds.module_name.upper()}}_{{child.inst_name.upper()}}_ADDR_WIDTH = {{child.size|clog2}};
{%- endfor %}
endpackage
{# (eof newline anchor) #}

View File

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

View File

@@ -1,3 +1,6 @@
from typing import Literal
class SVInt:
def __init__(self, value: int, width: int | None = None) -> None:
self.value = value
@@ -19,3 +22,27 @@ class SVInt:
return SVInt(self.value + other.value, max(self.width, other.width))
else:
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:
current_node = ref
# pyrefly: ignore[bad-assignment] - false positive due to circular type checking
while current_node is not None:
if current_node == top_node:
# 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.walker import RDLListener, RDLWalker, WalkerAction
from .utils import is_pow2, ref_is_internal, roundup_pow2
from .utils import ref_is_internal
if TYPE_CHECKING:
from .exporter import BusDecoderExporter
@@ -74,7 +74,9 @@ class DesignValidator(RDLListener):
f"instance '{node.inst_name}' must be a multiple of {alignment}",
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(
"Unaligned registers are not supported. Address stride of "
f"instance array '{node.inst_name}' must be a multiple of {alignment}",
@@ -159,27 +161,3 @@ class DesignValidator(RDLListener):
else:
# Exiting top addrmap. Resolve final answer
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,19 +1,27 @@
# Unit tests
# Tests
The bus decoder exporter now ships with a small unit test suite built around
`pytest`. The tests exercise the Python implementation directly and use the
[`systemrdl-compiler`](https://github.com/SystemRDL/systemrdl-compiler)
The bus decoder exporter includes comprehensive test suites to validate both the
Python implementation and the generated SystemVerilog RTL.
## Unit Tests
The unit test suite is built around `pytest` and exercises the Python implementation
directly using the [`systemrdl-compiler`](https://github.com/SystemRDL/systemrdl-compiler)
package to elaborate inline SystemRDL snippets.
## Install dependencies
### Install dependencies
Create an isolated environment if desired and install the minimal requirements:
```bash
python -m pip install -r tests/requirements.txt
# Using uv (recommended)
uv sync --group test
# Or using pip
python -m pip install -e . parameterized pytest pytest-cov pytest-xdist
```
## Running the suite
### Running the suite
Invoke `pytest` from the repository root (or the `tests` directory) and point it
at the unit tests:
@@ -25,3 +33,67 @@ pytest tests/unit
Pytest will automatically discover tests that follow the `test_*.py` naming
pattern and can make use of the `compile_rdl` fixture defined in
`tests/unit/conftest.py` to compile inline SystemRDL sources.
## Cocotb Integration Tests
The cocotb test suite validates the functionality of generated SystemVerilog RTL
through simulation. These tests generate bus decoders for different CPU interfaces
(APB3, APB4, AXI4-Lite) and verify that read/write operations work correctly.
### Install dependencies
```bash
# Install with cocotb support using uv (recommended)
uv sync --group test
# Or using pip
python -m pip install -e . parameterized pytest pytest-cov pytest-xdist cocotb cocotb-bus
# Install HDL simulator (choose one)
apt-get install iverilog # Icarus Verilog
apt-get install verilator # Verilator
```
### Running the tests
#### Integration tests (no simulator required)
These tests validate code generation without requiring an HDL simulator:
```bash
pytest tests/cocotb/testbenches/test_integration.py -v
```
#### Example code generation
Run examples to see generated code for different configurations:
```bash
python tests/cocotb/examples.py
```
#### Simulation layout
Simulation-oriented tests are grouped by CPU interface under
`tests/cocotb/<cpuif>/<group>/`. For example, the APB4 smoke test lives in
`tests/cocotb/apb4/smoke/` alongside its pytest runner module. Each runner
compiles the appropriate SystemRDL design, adds the interface wrapper from
`hdl-src/`, and invokes cocotb via Verilator.
#### Full simulation tests (requires simulator)
To execute the smoke tests for every supported interface:
```bash
pytest tests/cocotb/*/smoke/test_runner.py -v
```
To target a single interface, point pytest at that runner module:
```bash
pytest tests/cocotb/apb3/smoke/test_runner.py -v
pytest tests/cocotb/apb4/smoke/test_runner.py -v
pytest tests/cocotb/axi4lite/smoke/test_runner.py -v
```
For more information about cocotb tests, see [`tests/cocotb/README.md`](cocotb/README.md).

0
tests/body/__init__.py Normal file
View File

47
tests/body/test_body.py Normal file
View File

@@ -0,0 +1,47 @@
from peakrdl_busdecoder.body import Body
class TestBody:
"""Test the base Body class."""
def test_empty_body(self) -> None:
"""Test empty body returns empty string."""
body = Body()
assert str(body) == ""
assert not body # Should be falsy when empty
def test_add_single_line(self) -> None:
"""Test adding a single line to body."""
body = Body()
body += "line1"
assert str(body) == "line1"
assert body # Should be truthy when not empty
def test_add_multiple_lines(self) -> None:
"""Test adding multiple lines to body."""
body = Body()
body += "line1"
body += "line2"
body += "line3"
expected = "line1\nline2\nline3"
assert str(body) == expected
def test_add_returns_self(self) -> None:
"""Test that add operation returns self for chaining."""
body = Body()
body += "line1"
body += "line2"
# Chaining works because += returns self
assert len(body.lines) == 2
def test_add_nested_body(self) -> None:
"""Test adding another body as a line."""
outer = Body()
inner = Body()
inner += "inner1"
inner += "inner2"
outer += "outer1"
outer += inner
outer += "outer2"
expected = "outer1\ninner1\ninner2\nouter2"
assert str(outer) == expected

View File

@@ -0,0 +1,39 @@
from peakrdl_busdecoder.body import CombinationalBody, IfBody
class TestCombinationalBody:
"""Test the CombinationalBody class."""
def test_simple_combinational_block(self) -> None:
"""Test simple combinational block."""
body = CombinationalBody()
body += "assign1 = value1;"
body += "assign2 = value2;"
result = str(body)
assert "always_comb" in result
assert "begin" in result
assert "assign1 = value1;" in result
assert "assign2 = value2;" in result
assert "end" in result
def test_empty_combinational_block(self) -> None:
"""Test empty combinational block."""
body = CombinationalBody()
result = str(body)
assert "always_comb" in result
assert "begin" in result
assert "end" in result
def test_combinational_with_if_statement(self) -> None:
"""Test combinational block with if statement."""
cb = CombinationalBody()
ifb = IfBody()
with ifb.cm("condition") as b:
b += "assignment = value;"
cb += ifb
result = str(cb)
assert "always_comb" in result
assert "if (condition)" in result
assert "assignment = value;" in result

View File

@@ -0,0 +1,46 @@
from peakrdl_busdecoder.body import ForLoopBody
class TestForLoopBody:
"""Test the ForLoopBody class."""
def test_genvar_for_loop(self) -> None:
"""Test genvar-style for loop."""
body = ForLoopBody("genvar", "i", 4)
body += "statement1;"
body += "statement2;"
result = str(body)
assert "for (genvar i = 0; i < 4; i++)" in result
assert "statement1;" in result
assert "statement2;" in result
assert "end" in result
def test_int_for_loop(self) -> None:
"""Test int-style for loop."""
body = ForLoopBody("int", "j", 8)
body += "assignment = value;"
result = str(body)
assert "for (int j = 0; j < 8; j++)" in result
assert "assignment = value;" in result
assert "end" in result
def test_empty_for_loop(self) -> None:
"""Test empty for loop."""
body = ForLoopBody("genvar", "k", 2)
result = str(body)
# Empty for loop should still have structure
assert "for (genvar k = 0; k < 2; k++)" in result
def test_nested_for_loops(self) -> None:
"""Test nested for loops."""
outer = ForLoopBody("genvar", "i", 3)
inner = ForLoopBody("genvar", "j", 2)
inner += "nested_statement;"
outer += inner
result = str(outer)
assert "for (genvar i = 0; i < 3; i++)" in result
assert "for (genvar j = 0; j < 2; j++)" in result
assert "nested_statement;" in result

View File

@@ -0,0 +1,86 @@
from peakrdl_busdecoder.body import IfBody
class TestIfBody:
"""Test the IfBody class."""
def test_simple_if(self):
"""Test simple if statement."""
body = IfBody()
with body.cm("condition1") as b:
b += "statement1;"
result = str(body)
assert "if (condition1)" in result
assert "statement1;" in result
assert "end" in result
def test_if_else(self):
"""Test if-else statement."""
body = IfBody()
with body.cm("condition1") as b:
b += "if_statement;"
with body.cm(None) as b: # None for else
b += "else_statement;"
result = str(body)
assert "if (condition1)" in result
assert "if_statement;" in result
assert "else" in result
assert "else_statement;" in result
def test_if_elif_else(self):
"""Test if-elif-else chain."""
body = IfBody()
with body.cm("condition1") as b:
b += "statement1;"
with body.cm("condition2") as b:
b += "statement2;"
with body.cm(None) as b: # None for else
b += "statement3;"
result = str(body)
assert "if (condition1)" in result
assert "statement1;" in result
assert "else if (condition2)" in result
assert "statement2;" in result
assert "else" in result
assert "statement3;" in result
def test_multiple_elif(self):
"""Test multiple elif statements."""
body = IfBody()
with body.cm("cond1") as b:
b += "stmt1;"
with body.cm("cond2") as b:
b += "stmt2;"
with body.cm("cond3") as b:
b += "stmt3;"
result = str(body)
assert "if (cond1)" in result
assert "else if (cond2)" in result
assert "else if (cond3)" in result
def test_empty_if_branches(self):
"""Test if statement with empty branches."""
body = IfBody()
with body.cm("condition"):
pass
result = str(body)
assert "if (condition)" in result
def test_nested_if(self):
"""Test nested if statements."""
outer = IfBody()
with outer.cm("outer_cond") as outer_body:
inner = IfBody()
with inner.cm("inner_cond") as inner_body:
inner_body += "nested_statement;"
outer_body += inner
result = str(outer)
assert "if (outer_cond)" in result
assert "if (inner_cond)" in result
assert "nested_statement;" in result

View File

@@ -0,0 +1,59 @@
from peakrdl_busdecoder.body import StructBody
class TestStructBody:
"""Test the StructBody class."""
def test_simple_struct(self) -> None:
"""Test simple struct definition."""
body = StructBody("my_struct_t", packed=True, typedef=True)
body += "logic [7:0] field1;"
body += "logic field2;"
result = str(body)
assert "typedef struct packed" in result
assert "my_struct_t" in result
assert "logic [7:0] field1;" in result
assert "logic field2;" in result
def test_unpacked_struct(self) -> None:
"""Test unpacked struct definition."""
body = StructBody("unpacked_t", packed=False, typedef=True)
body += "int field1;"
result = str(body)
assert "typedef struct" in result
assert "packed" not in result or "typedef struct {" in result
assert "unpacked_t" in result
def test_struct_without_typedef(self) -> None:
"""Test struct without typedef."""
body = StructBody("my_struct", packed=True, typedef=False)
body += "logic field;"
result = str(body)
# When typedef=False, packed is not used
assert "struct {" in result
assert "typedef" not in result
assert "my_struct" in result
def test_empty_struct(self) -> None:
"""Test empty struct."""
body = StructBody("empty_t", packed=True, typedef=True)
result = str(body)
assert "typedef struct packed" in result
assert "empty_t" in result
def test_nested_struct(self) -> None:
"""Test struct with nested struct."""
outer = StructBody("outer_t", packed=True, typedef=True)
inner = StructBody("inner_t", packed=True, typedef=True)
inner += "logic field1;"
outer += "logic field2;"
outer += str(inner) # Include inner struct as a string
result = str(outer)
assert "outer_t" in result
assert "field2;" in result
# Inner struct should appear in the string
assert "inner_t" in result

0
tests/cocotb/__init__.py Normal file
View File

View File

View File

View File

@@ -0,0 +1,310 @@
"""APB3 smoke tests generated from SystemRDL sources."""
from __future__ import annotations
from typing import Any
import cocotb
from cocotb.triggers import RisingEdge, Timer
from tests.cocotb_lib.handle_utils import SignalHandle
from tests.cocotb_lib.protocol_utils import (
all_index_pairs,
find_invalid_address,
get_int,
load_config,
set_value,
start_clock,
)
class _Apb3SlaveShim:
"""Accessor for the APB3 slave signals on the DUT."""
def __init__(self, dut):
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.PENABLE = getattr(dut, f"{prefix}_PENABLE")
self.PWRITE = getattr(dut, f"{prefix}_PWRITE")
self.PADDR = getattr(dut, f"{prefix}_PADDR")
self.PWDATA = getattr(dut, f"{prefix}_PWDATA")
self.PRDATA = getattr(dut, f"{prefix}_PRDATA")
self.PREADY = getattr(dut, f"{prefix}_PREADY")
self.PSLVERR = getattr(dut, f"{prefix}_PSLVERR")
def _build_master_table(dut, masters_cfg: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
table: dict[str, dict[str, Any]] = {}
for master in masters_cfg:
prefix = master["port_prefix"]
entry = {
"indices": [tuple(idx) for idx in master["indices"]] or [tuple()],
"outputs": {
"PSEL": SignalHandle(dut, f"{prefix}_PSEL"),
"PENABLE": SignalHandle(dut, f"{prefix}_PENABLE"),
"PWRITE": SignalHandle(dut, f"{prefix}_PWRITE"),
"PADDR": SignalHandle(dut, f"{prefix}_PADDR"),
"PWDATA": SignalHandle(dut, f"{prefix}_PWDATA"),
},
"inputs": {
"PRDATA": SignalHandle(dut, f"{prefix}_PRDATA"),
"PREADY": SignalHandle(dut, f"{prefix}_PREADY"),
"PSLVERR": SignalHandle(dut, f"{prefix}_PSLVERR"),
},
"inst_size": master["inst_size"],
"inst_address": master["inst_address"],
}
table[master["inst_name"]] = entry
return table
def _write_pattern(address: int, width: int) -> int:
mask = (1 << width) - 1
return ((address * 0x2041) ^ 0xCAFEBABE) & mask
def _read_pattern(address: int, width: int) -> int:
mask = (1 << width) - 1
return ((address ^ 0x0BAD_F00D) + width) & mask
@cocotb.test()
async def test_apb3_address_decoding(dut) -> None:
"""Exercise the APB3 slave interface against sampled register addresses."""
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"]["PRDATA"], idx, 0)
set_value(entry["inputs"]["PREADY"], idx, 0)
set_value(entry["inputs"]["PSLVERR"], idx, 0)
if slave.PCLK is not None:
await RisingEdge(slave.PCLK)
else:
await Timer(1, unit="ns")
addr_mask = (1 << config["address_width"]) - 1
for txn in config["transactions"]:
master_name = txn["master"]
index = tuple(txn["index"])
entry = masters[master_name]
address = txn["address"] & addr_mask
write_data = _write_pattern(address, config["data_width"])
set_value(entry["inputs"]["PREADY"], index, 0)
set_value(entry["inputs"]["PSLVERR"], index, 0)
# ------------------------------------------------------------------
# Setup phase
# ------------------------------------------------------------------
slave.PADDR.value = address
slave.PWDATA.value = write_data
slave.PWRITE.value = 1
slave.PSEL.value = 1
slave.PENABLE.value = 0
dut._log.info(
f"Starting transaction {txn['label']} to {master_name}{index} at address 0x{address:08X}"
)
master_address = (address - entry["inst_address"]) % entry["inst_size"]
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} 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"
)
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} should remain idle during {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 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

@@ -0,0 +1,86 @@
"""Pytest wrapper launching the APB3 cocotb smoke tests."""
from __future__ import annotations
import json
import logging
from pathlib import Path
import pytest
from peakrdl_busdecoder.cpuif.apb3.apb3_cpuif_flat import APB3CpuifFlat
try: # pragma: no cover - optional dependency shim
from cocotb.runner import get_runner
except ImportError: # pragma: no cover
from cocotb_tools.runner import get_runner
from tests.cocotb_lib import RDL_CASES
from tests.cocotb_lib.utils import colorize_cocotb_log, get_verilog_sources, prepare_cpuif_case
@pytest.mark.simulation
@pytest.mark.verilator
@pytest.mark.parametrize(("rdl_file", "top_name"), RDL_CASES, ids=[case[1] for case in RDL_CASES])
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]
rdl_path = repo_root / "tests" / "cocotb_lib" / "rdl" / rdl_file
build_root = tmp_path / top_name
module_path, package_path, config = prepare_cpuif_case(
str(rdl_path),
top_name,
build_root,
cpuif_cls=APB3CpuifFlat,
control_signal="PSEL",
)
sources = get_verilog_sources(
module_path,
package_path,
[repo_root / "hdl-src" / "apb3_intf.sv"],
)
runner = get_runner("verilator")
sim_build = build_root / "sim_build"
build_log_file = build_root / "build.log"
sim_log_file = build_root / "simulation.log"
try:
runner.build(
sources=sources,
hdl_toplevel=module_path.stem,
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

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

View File

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

View File

View File

View File

@@ -0,0 +1,332 @@
"""APB4 smoke tests generated from SystemRDL sources."""
from __future__ import annotations
from typing import Any
import cocotb
from cocotb.triggers import RisingEdge, Timer
from tests.cocotb_lib.handle_utils import SignalHandle
from tests.cocotb_lib.protocol_utils import (
all_index_pairs,
find_invalid_address,
get_int,
load_config,
set_value,
start_clock,
)
class _Apb4SlaveShim:
"""Lightweight accessor for the APB4 slave side of the DUT."""
def __init__(self, dut):
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.PENABLE = getattr(dut, f"{prefix}_PENABLE")
self.PWRITE = getattr(dut, f"{prefix}_PWRITE")
self.PADDR = getattr(dut, f"{prefix}_PADDR")
self.PPROT = getattr(dut, f"{prefix}_PPROT")
self.PWDATA = getattr(dut, f"{prefix}_PWDATA")
self.PSTRB = getattr(dut, f"{prefix}_PSTRB")
self.PRDATA = getattr(dut, f"{prefix}_PRDATA")
self.PREADY = getattr(dut, f"{prefix}_PREADY")
self.PSLVERR = getattr(dut, f"{prefix}_PSLVERR")
def _build_master_table(dut, masters_cfg: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
table: dict[str, dict[str, Any]] = {}
for master in masters_cfg:
port_prefix = master["port_prefix"]
entry = {
"port_prefix": port_prefix,
"indices": [tuple(idx) for idx in master["indices"]] or [tuple()],
"outputs": {
"PSEL": SignalHandle(dut, f"{port_prefix}_PSEL"),
"PENABLE": SignalHandle(dut, f"{port_prefix}_PENABLE"),
"PWRITE": SignalHandle(dut, f"{port_prefix}_PWRITE"),
"PADDR": SignalHandle(dut, f"{port_prefix}_PADDR"),
"PPROT": SignalHandle(dut, f"{port_prefix}_PPROT"),
"PWDATA": SignalHandle(dut, f"{port_prefix}_PWDATA"),
"PSTRB": SignalHandle(dut, f"{port_prefix}_PSTRB"),
},
"inputs": {
"PRDATA": SignalHandle(dut, f"{port_prefix}_PRDATA"),
"PREADY": SignalHandle(dut, f"{port_prefix}_PREADY"),
"PSLVERR": SignalHandle(dut, f"{port_prefix}_PSLVERR"),
},
"inst_size": master["inst_size"],
"inst_address": master["inst_address"],
}
table[master["inst_name"]] = entry
return table
def _write_pattern(address: int, width: int) -> int:
mask = (1 << width) - 1
return ((address * 0x1021) ^ 0x1357_9BDF) & mask
def _read_pattern(address: int, width: int) -> int:
mask = (1 << width) - 1
return ((address ^ 0xDEAD_BEE5) + width) & mask
@cocotb.test()
async def test_apb4_address_decoding(dut) -> None:
"""Drive the APB4 slave interface and verify master fanout across all sampled registers."""
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"]["PRDATA"], idx, 0)
set_value(entry["inputs"]["PREADY"], idx, 0)
set_value(entry["inputs"]["PSLVERR"], idx, 0)
if slave.PCLK is not None:
await RisingEdge(slave.PCLK)
else:
await Timer(1, unit="ns")
addr_mask = (1 << config["address_width"]) - 1
strobe_mask = (1 << config["byte_width"]) - 1
for txn in config["transactions"]:
master_name = txn["master"]
index = tuple(txn["index"])
entry = masters[master_name]
address = txn["address"] & addr_mask
write_data = _write_pattern(address, config["data_width"])
# Prime master-side inputs for the write phase
set_value(entry["inputs"]["PREADY"], index, 0)
set_value(entry["inputs"]["PSLVERR"], index, 0)
# ------------------------------------------------------------------
# Setup phase
# ------------------------------------------------------------------
slave.PADDR.value = address
slave.PWDATA.value = write_data
slave.PSTRB.value = strobe_mask
slave.PPROT.value = 0
slave.PWRITE.value = 1
slave.PSEL.value = 1
slave.PENABLE.value = 0
dut._log.info(
f"Starting transaction {txn['label']} to {master_name}{index} at address 0x{address:08X}"
)
master_address = (address - entry["inst_address"]) % entry["inst_size"]
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} 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"
)
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} should remain idle during {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 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

@@ -0,0 +1,89 @@
"""Pytest wrapper launching the APB4 cocotb smoke tests."""
from __future__ import annotations
import json
import logging
from pathlib import Path
import pytest
from peakrdl_busdecoder.cpuif.apb4.apb4_cpuif_flat import APB4CpuifFlat
try: # pragma: no cover - optional dependency shim
from cocotb.runner import get_runner
except ImportError: # pragma: no cover
from cocotb_tools.runner import get_runner
from tests.cocotb_lib import RDL_CASES
from tests.cocotb_lib.utils import colorize_cocotb_log, get_verilog_sources, prepare_cpuif_case
@pytest.mark.simulation
@pytest.mark.verilator
@pytest.mark.parametrize(("rdl_file", "top_name"), RDL_CASES, ids=[case[1] for case in RDL_CASES])
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]
rdl_path = repo_root / "tests" / "cocotb_lib" / "rdl" / rdl_file
build_root = tmp_path / top_name
logging.info(f"Running APB4 smoke test for {rdl_path} with top {top_name}")
logging.info(f"Build root: {build_root}")
module_path, package_path, config = prepare_cpuif_case(
str(rdl_path),
top_name,
build_root,
cpuif_cls=APB4CpuifFlat,
control_signal="PSEL",
)
sources = get_verilog_sources(
module_path,
package_path,
[repo_root / "hdl-src" / "apb4_intf.sv"],
)
runner = get_runner("verilator")
sim_build = build_root / "sim_build"
build_log_file = build_root / "build.log"
sim_log_file = build_root / "simulation.log"
try:
runner.build(
sources=sources,
hdl_toplevel=module_path.stem,
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

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

View File

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

View File

View File

View File

@@ -0,0 +1,357 @@
"""AXI4-Lite smoke test driven from SystemRDL-generated register maps."""
from __future__ import annotations
from typing import Any
import cocotb
from cocotb.triggers import Timer
from tests.cocotb_lib.handle_utils import SignalHandle
from tests.cocotb_lib.protocol_utils import (
all_index_pairs,
find_invalid_address,
get_int,
load_config,
set_value,
)
class _AxilSlaveShim:
"""Accessor for AXI4-Lite slave ports on the DUT."""
def __init__(self, dut):
prefix = "s_axil"
self.AWREADY = getattr(dut, f"{prefix}_AWREADY")
self.AWVALID = getattr(dut, f"{prefix}_AWVALID")
self.AWADDR = getattr(dut, f"{prefix}_AWADDR")
self.AWPROT = getattr(dut, f"{prefix}_AWPROT")
self.WREADY = getattr(dut, f"{prefix}_WREADY")
self.WVALID = getattr(dut, f"{prefix}_WVALID")
self.WDATA = getattr(dut, f"{prefix}_WDATA")
self.WSTRB = getattr(dut, f"{prefix}_WSTRB")
self.BREADY = getattr(dut, f"{prefix}_BREADY")
self.BVALID = getattr(dut, f"{prefix}_BVALID")
self.BRESP = getattr(dut, f"{prefix}_BRESP")
self.ARREADY = getattr(dut, f"{prefix}_ARREADY")
self.ARVALID = getattr(dut, f"{prefix}_ARVALID")
self.ARADDR = getattr(dut, f"{prefix}_ARADDR")
self.ARPROT = getattr(dut, f"{prefix}_ARPROT")
self.RREADY = getattr(dut, f"{prefix}_RREADY")
self.RVALID = getattr(dut, f"{prefix}_RVALID")
self.RDATA = getattr(dut, f"{prefix}_RDATA")
self.RRESP = getattr(dut, f"{prefix}_RRESP")
def _build_master_table(dut, masters_cfg: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
table: dict[str, dict[str, Any]] = {}
for master in masters_cfg:
prefix = master["port_prefix"]
entry = {
"indices": [tuple(idx) for idx in master["indices"]] or [tuple()],
"outputs": {
"AWVALID": SignalHandle(dut, f"{prefix}_AWVALID"),
"AWADDR": SignalHandle(dut, f"{prefix}_AWADDR"),
"AWPROT": SignalHandle(dut, f"{prefix}_AWPROT"),
"WVALID": SignalHandle(dut, f"{prefix}_WVALID"),
"WDATA": SignalHandle(dut, f"{prefix}_WDATA"),
"WSTRB": SignalHandle(dut, f"{prefix}_WSTRB"),
"ARVALID": SignalHandle(dut, f"{prefix}_ARVALID"),
"ARADDR": SignalHandle(dut, f"{prefix}_ARADDR"),
"ARPROT": SignalHandle(dut, f"{prefix}_ARPROT"),
},
"inputs": {
"AWREADY": SignalHandle(dut, f"{prefix}_AWREADY"),
"WREADY": SignalHandle(dut, f"{prefix}_WREADY"),
"BVALID": SignalHandle(dut, f"{prefix}_BVALID"),
"BRESP": SignalHandle(dut, f"{prefix}_BRESP"),
"ARREADY": SignalHandle(dut, f"{prefix}_ARREADY"),
"RVALID": SignalHandle(dut, f"{prefix}_RVALID"),
"RDATA": SignalHandle(dut, f"{prefix}_RDATA"),
"RRESP": SignalHandle(dut, f"{prefix}_RRESP"),
},
"inst_size": master["inst_size"],
"inst_address": master["inst_address"],
}
table[master["inst_name"]] = entry
return table
def _write_pattern(address: int, width: int) -> int:
mask = (1 << width) - 1
return ((address * 0x3105) ^ 0x1357_9BDF) & mask
def _read_pattern(address: int, width: int) -> int:
mask = (1 << width) - 1
return ((address ^ 0x2468_ACED) + width) & mask
@cocotb.test()
async def test_axi4lite_address_decoding(dut) -> None:
"""Stimulate AXI4-Lite slave channels and verify master port selection."""
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")
addr_mask = (1 << config["address_width"]) - 1
strobe_mask = (1 << config["byte_width"]) - 1
for txn in config["transactions"]:
master_name = txn["master"]
index = tuple(txn["index"])
entry = masters[master_name]
address = txn["address"] & addr_mask
write_data = _write_pattern(address, config["data_width"])
set_value(entry["inputs"]["BVALID"], index, 1)
set_value(entry["inputs"]["BRESP"], index, 0)
slave.AWADDR.value = address
slave.AWPROT.value = 0
slave.AWVALID.value = 1
slave.WDATA.value = write_data
slave.WSTRB.value = strobe_mask
slave.WVALID.value = 1
slave.BREADY.value = 1
dut._log.info(
f"Starting transaction {txn['label']} to {master_name}{index} at address 0x{address:08X}"
)
master_address = (address - entry["inst_address"]) % entry["inst_size"]
await Timer(1, unit="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"
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"]["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']}"
)
assert int(slave.BVALID.value) == 1, "Slave should observe BVALID from selected master"
assert int(slave.BRESP.value) == 0, "BRESP should indicate OKAY on write"
slave.AWVALID.value = 0
slave.WVALID.value = 0
slave.BREADY.value = 0
set_value(entry["inputs"]["BVALID"], index, 0)
await Timer(1, unit="ns")
read_data = _read_pattern(address, config["data_width"])
set_value(entry["inputs"]["RVALID"], index, 1)
set_value(entry["inputs"]["RDATA"], index, read_data)
set_value(entry["inputs"]["RRESP"], index, 0)
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

@@ -0,0 +1,86 @@
"""Pytest wrapper launching the AXI4-Lite cocotb smoke tests."""
from __future__ import annotations
import json
import logging
from pathlib import Path
import pytest
from peakrdl_busdecoder.cpuif.axi4lite.axi4_lite_cpuif_flat import AXI4LiteCpuifFlat
try: # pragma: no cover - optional dependency shim
from cocotb.runner import get_runner
except ImportError: # pragma: no cover
from cocotb_tools.runner import get_runner
from tests.cocotb_lib import RDL_CASES
from tests.cocotb_lib.utils import colorize_cocotb_log, get_verilog_sources, prepare_cpuif_case
@pytest.mark.simulation
@pytest.mark.verilator
@pytest.mark.parametrize(("rdl_file", "top_name"), RDL_CASES, ids=[case[1] for case in RDL_CASES])
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]
rdl_path = repo_root / "tests" / "cocotb_lib" / "rdl" / rdl_file
build_root = tmp_path / top_name
module_path, package_path, config = prepare_cpuif_case(
str(rdl_path),
top_name,
build_root,
cpuif_cls=AXI4LiteCpuifFlat,
control_signal="AWVALID",
)
sources = get_verilog_sources(
module_path,
package_path,
[repo_root / "hdl-src" / "axi4lite_intf.sv"],
)
runner = get_runner("verilator")
sim_build = build_root / "sim_build"
build_log_file = build_root / "build.log"
sim_log_file = build_root / "simulation.log"
try:
runner.build(
sources=sources,
hdl_toplevel=module_path.stem,
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

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

View File

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

View File

@@ -0,0 +1,10 @@
"""Manifest of SystemRDL sources used by the cocotb simulations."""
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,22 @@
addrmap multi_reg {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} reg1 @ 0x0;
reg {
field {
sw=r;
hw=w;
} status[15:0];
} reg2 @ 0x4;
reg {
field {
sw=rw;
hw=r;
} control[7:0];
} reg3 @ 0x8;
};

View File

@@ -0,0 +1,8 @@
addrmap simple_test {
reg {
field {
sw=rw;
hw=r;
} data[31:0];
} test_reg @ 0x0;
};

Some files were not shown because too many files have changed in this diff Show More