3 Commits

Author SHA1 Message Date
Byron Lathi
35d66db1b8 Change required systemrdl-compiler verison to ~1.31 to match peakrdl 2025-11-23 17:26:48 -08:00
Arnav Sacheti
f0f25a6d92 update devcontainer extensions 2025-11-12 07:20:11 +00:00
Arnav Sacheti
a9653c8497 Tests/cocotb (#19)
* wip

* reorg

* update sv int

* apb4 working

* apb3 working

* version bump + ignore runner warning

* remove redundant check

* adding log on failure

* cleaning up verilator version issue

* devcontainer

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

* Initial plan

* Install libpython in GitHub Actions for cocotb tests

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

---------

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

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
2025-11-10 23:00:28 -08:00
9 changed files with 216 additions and 70 deletions

22
.devcontainer/Dockerfile Normal file
View File

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

View File

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

View File

@@ -14,11 +14,13 @@ on:
jobs:
test:
runs-on: ubuntu-latest
container:
image: verilator/verilator:latest
permissions:
contents: read
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12', '3.13']
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
steps:
- uses: actions/checkout@v4
@@ -27,19 +29,21 @@ jobs:
uses: astral-sh/setup-uv@v3
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
- name: Install Verilator
- name: Check Verilator version
run: verilator --version
- name: Install Python development packages
run: |
sudo apt-get update
sudo apt-get install -y verilator
verilator --version
apt-get update && apt-get install -y python3-dev libpython3-dev
- name: Install dependencies
run: |
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 Codecov
uses: codecov/codecov-action@v4

View File

@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
[project]
name = "peakrdl-busdecoder"
version = "0.5.0"
version = "0.6.0"
requires-python = ">=3.10"
dependencies = ["jinja2>=3.1.6", "systemrdl-compiler~=1.30.1"]
dependencies = ["jinja2>=3.1.6", "systemrdl-compiler~=1.31"]
authors = [{ name = "Alex Mykyta" }]
authors = [{ name = "Arnav Sacheti" }]
description = "Generate a SystemVerilog bus decoder from SystemRDL for splitting CPU interfaces to multiple sub-address spaces"
readme = "README.md"
license = { text = "LGPLv3" }

View File

@@ -9,6 +9,7 @@ from typing import Any, Iterable
import cocotb
from cocotb.triggers import Timer
from tests.cocotb_lib.handle_utils import SignalHandle, resolve_handle
class _Apb3SlaveShim:
"""Accessor for the APB3 slave signals on the DUT."""
@@ -33,10 +34,7 @@ def _load_config() -> dict[str, Any]:
def _resolve(handle, indices: Iterable[int]):
ref = handle
for idx in indices:
ref = ref[idx]
return ref
return resolve_handle(handle, indices)
def _set_value(handle, indices: Iterable[int], value: int) -> None:
@@ -54,16 +52,16 @@ def _build_master_table(dut, masters_cfg: list[dict[str, Any]]) -> dict[str, dic
entry = {
"indices": [tuple(idx) for idx in master["indices"]] or [tuple()],
"outputs": {
"PSEL": getattr(dut, f"{prefix}_PSEL"),
"PENABLE": getattr(dut, f"{prefix}_PENABLE"),
"PWRITE": getattr(dut, f"{prefix}_PWRITE"),
"PADDR": getattr(dut, f"{prefix}_PADDR"),
"PWDATA": getattr(dut, f"{prefix}_PWDATA"),
"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": getattr(dut, f"{prefix}_PRDATA"),
"PREADY": getattr(dut, f"{prefix}_PREADY"),
"PSLVERR": getattr(dut, f"{prefix}_PSLVERR"),
"PRDATA": SignalHandle(dut, f"{prefix}_PRDATA"),
"PREADY": SignalHandle(dut, f"{prefix}_PREADY"),
"PSLVERR": SignalHandle(dut, f"{prefix}_PSLVERR"),
},
}
table[master["inst_name"]] = entry

View File

@@ -9,6 +9,7 @@ from typing import Any, Iterable
import cocotb
from cocotb.triggers import Timer
from tests.cocotb_lib.handle_utils import SignalHandle, resolve_handle
class _Apb4SlaveShim:
"""Lightweight accessor for the APB4 slave side of the DUT."""
@@ -37,10 +38,7 @@ def _load_config() -> dict[str, Any]:
def _resolve(handle, indices: Iterable[int]):
"""Index into hierarchical cocotb handles."""
ref = handle
for idx in indices:
ref = ref[idx]
return ref
return resolve_handle(handle, indices)
def _set_value(handle, indices: Iterable[int], value: int) -> None:
@@ -59,18 +57,18 @@ def _build_master_table(dut, masters_cfg: list[dict[str, Any]]) -> dict[str, dic
"port_prefix": port_prefix,
"indices": [tuple(idx) for idx in master["indices"]] or [tuple()],
"outputs": {
"PSEL": getattr(dut, f"{port_prefix}_PSEL"),
"PENABLE": getattr(dut, f"{port_prefix}_PENABLE"),
"PWRITE": getattr(dut, f"{port_prefix}_PWRITE"),
"PADDR": getattr(dut, f"{port_prefix}_PADDR"),
"PPROT": getattr(dut, f"{port_prefix}_PPROT"),
"PWDATA": getattr(dut, f"{port_prefix}_PWDATA"),
"PSTRB": getattr(dut, f"{port_prefix}_PSTRB"),
"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": getattr(dut, f"{port_prefix}_PRDATA"),
"PREADY": getattr(dut, f"{port_prefix}_PREADY"),
"PSLVERR": getattr(dut, f"{port_prefix}_PSLVERR"),
"PRDATA": SignalHandle(dut, f"{port_prefix}_PRDATA"),
"PREADY": SignalHandle(dut, f"{port_prefix}_PREADY"),
"PSLVERR": SignalHandle(dut, f"{port_prefix}_PSLVERR"),
},
}
table[master["inst_name"]] = entry

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
import json
from pathlib import Path
import logging
import pytest
from peakrdl_busdecoder.cpuif.apb4.apb4_cpuif_flat import APB4CpuifFlat
@@ -43,17 +43,38 @@ def test_apb4_smoke(tmp_path: Path, rdl_file: str, top_name: str) -> None:
runner = get_runner("verilator")
sim_build = build_root / "sim_build"
try:
runner.build(
sources=sources,
hdl_toplevel=module_path.stem,
build_dir=sim_build,
log_file=str(build_root / "build.log"),
)
except SystemExit as e:
# Print build log on failure for easier debugging
log_path = build_root / "build.log"
if log_path.exists():
logging.error("\n\n=== Build Log ===\n")
logging.error(log_path.read_text())
logging.error("\n=== End Build Log ===\n")
if e.code != 0:
raise
runner.build(
sources=sources,
hdl_toplevel=module_path.stem,
build_dir=sim_build,
)
runner.test(
hdl_toplevel=module_path.stem,
test_module="tests.cocotb.apb4.smoke.test_register_access",
build_dir=sim_build,
log_file=str(build_root / "simulation.log"),
extra_env={"RDL_TEST_CONFIG": json.dumps(config)},
)
try:
runner.test(
hdl_toplevel=module_path.stem,
test_module="tests.cocotb.apb4.smoke.test_register_access",
build_dir=sim_build,
log_file=str(build_root / "simulation.log"),
extra_env={"RDL_TEST_CONFIG": json.dumps(config)},
)
except SystemExit as e:
# Print simulation log on failure for easier debugging
log_path = build_root / "simulation.log"
if log_path.exists():
logging.error("\n\n=== Simulation Log ===\n")
logging.error(log_path.read_text())
logging.error("\n=== End Simulation Log ===\n")
if e.code != 0:
raise

View File

@@ -9,6 +9,7 @@ from typing import Any, Iterable
import cocotb
from cocotb.triggers import Timer
from tests.cocotb_lib.handle_utils import SignalHandle, resolve_handle
class _AxilSlaveShim:
"""Accessor for AXI4-Lite slave ports on the DUT."""
@@ -44,10 +45,7 @@ def _load_config() -> dict[str, Any]:
def _resolve(handle, indices: Iterable[int]):
ref = handle
for idx in indices:
ref = ref[idx]
return ref
return resolve_handle(handle, indices)
def _set_value(handle, indices: Iterable[int], value: int) -> None:
@@ -65,25 +63,25 @@ def _build_master_table(dut, masters_cfg: list[dict[str, Any]]) -> dict[str, dic
entry = {
"indices": [tuple(idx) for idx in master["indices"]] or [tuple()],
"outputs": {
"AWVALID": getattr(dut, f"{prefix}_AWVALID"),
"AWADDR": getattr(dut, f"{prefix}_AWADDR"),
"AWPROT": getattr(dut, f"{prefix}_AWPROT"),
"WVALID": getattr(dut, f"{prefix}_WVALID"),
"WDATA": getattr(dut, f"{prefix}_WDATA"),
"WSTRB": getattr(dut, f"{prefix}_WSTRB"),
"ARVALID": getattr(dut, f"{prefix}_ARVALID"),
"ARADDR": getattr(dut, f"{prefix}_ARADDR"),
"ARPROT": getattr(dut, f"{prefix}_ARPROT"),
"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": getattr(dut, f"{prefix}_AWREADY"),
"WREADY": getattr(dut, f"{prefix}_WREADY"),
"BVALID": getattr(dut, f"{prefix}_BVALID"),
"BRESP": getattr(dut, f"{prefix}_BRESP"),
"ARREADY": getattr(dut, f"{prefix}_ARREADY"),
"RVALID": getattr(dut, f"{prefix}_RVALID"),
"RDATA": getattr(dut, f"{prefix}_RDATA"),
"RRESP": getattr(dut, f"{prefix}_RRESP"),
"AWREADY": SignalHandle(dut, f"{prefix}_AWREADY"),
"WREADY": SignalHandle(dut, f"{prefix}_WREADY"),
"BVALID": SignalHandle(dut, f"{prefix}_BVALID"),
"BRESP": SignalHandle(dut, f"{prefix}_BRESP"),
"ARREADY": SignalHandle(dut, f"{prefix}_ARREADY"),
"RVALID": SignalHandle(dut, f"{prefix}_RVALID"),
"RDATA": SignalHandle(dut, f"{prefix}_RDATA"),
"RRESP": SignalHandle(dut, f"{prefix}_RRESP"),
},
}
table[master["inst_name"]] = entry

View File

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