Components

This document describes the component types supported by the HDLSim backend and how to use them effectively.

Component Types

Extern Components

Extern components wrap existing SystemVerilog modules for integration with Zuspec testbenches.

Key Features:

  • Inherit from Extern protocol

  • Reference pre-existing HDL modules

  • Provide source file information via @annotation_fileset

  • Define ports as Signal fields

  • Implemented entirely in SystemVerilog

Example:

from zuspec.dataclasses import dataclass, Component, Signal, input, output
from zuspec.dataclasses.protocols import Extern, annotation_fileset

@dataclass
class UartModule(Component, Extern):
    """Wrapper for existing uart.sv module."""

    # Port declarations
    clock: Signal = input()
    reset: Signal = input()
    tx_data: Signal = input()
    tx_valid: Signal = input()
    tx_ready: Signal = output()

    # Provide source files
    @annotation_fileset(sources=["rtl/uart.sv", "rtl/uart_pkg.sv"])
    def __post_init__(self):
        pass

Usage Guidelines:

  • Port names and directions should match the HDL module

  • Use @annotation_fileset to specify all required source files

  • Extern components can only connect to other SV-domain components

  • No Python-side implementation needed

XtorComponent Transactors

XtorComponent defines transactors that bridge the Python and SystemVerilog domains.

Key Features:

  • Inherit from XtorComponent[ProtocolType]

  • Protocol defines the transaction-level API

  • SystemVerilog implementation generated by zuspec-be-sv

  • Python proxy generated via PyHDL-IF

  • Access via xtor_if attribute in Python tests

Protocol Definition:

The protocol class defines the API methods available to Python tests:

class WishboneInitiatorIf:
    """Wishbone bus initiator protocol."""

    async def write(self, addr: int, data: int) -> None:
        """Write to address."""
        ...

    async def read(self, addr: int) -> int:
        """Read from address."""
        ...

    async def wait_ack(self) -> None:
        """Wait for acknowledge."""
        ...

Transactor Definition:

@dataclass
class WishboneInitiator(XtorComponent[WishboneInitiatorIf]):
    """Wishbone bus initiator transactor."""

    # Port declarations
    clock: Signal = input()
    reset: Signal = input()

    # Wishbone signals
    wb_adr: Signal = output()
    wb_dat_o: Signal = output()
    wb_dat_i: Signal = input()
    wb_cyc: Signal = output()
    wb_stb: Signal = output()
    wb_we: Signal = output()
    wb_ack: Signal = input()

Python Test Usage:

async def test_wishbone_write(tb):
    """Test Wishbone write transaction."""
    # Access transactor via xtor_if
    await tb.wb_init.xtor_if.write(addr=0x1000, data=0xDEADBEEF)

    # Read back
    value = await tb.wb_init.xtor_if.read(addr=0x1000)
    assert value == 0xDEADBEEF

Usage Guidelines:

  • Protocol methods should be async for transaction-level operations

  • Use sync methods for simple queries/configurations

  • Transactor state is maintained in SystemVerilog

  • Complex protocol implementations benefit from Protocol-driven design

Python Components

Standard Zuspec components that remain in the Python domain.

Key Features:

  • Standard @dataclass Zuspec components

  • Run entirely in pytest/Python

  • Cannot connect to signals (use TLM interfaces only)

  • Useful for test orchestration and high-level sequencing

Example:

@dataclass
class TestSequencer(Component):
    """High-level test sequence coordinator."""

    def __post_init__(self):
        self.test_count = 0

    async def run_basic_sequence(self, xtor):
        """Run a basic test sequence."""
        await xtor.write(0, 0x100)
        await xtor.write(1, 0x200)
        self.test_count += 1

    def get_stats(self):
        """Return test statistics."""
        return {"tests_run": self.test_count}

Usage Guidelines:

  • Use for test orchestration, sequence management

  • Can hold test state and statistics

  • Communicate with SV components via XtorComponent APIs

  • Cannot access signals directly

Component Bindings

Signal Connectivity

Use __bind__ to define signal connections between components:

@dataclass
class MyTestbench(Component):
    dut: UartModule = inst()
    tx_xtor: UartTxXtor = inst()
    clkrst: ClkRstXtor = inst()

    def __bind__(self):
        """Define signal bindings."""
        return (
            # Clock/reset connections
            (self.clkrst.clock, self.dut.clock),
            (self.clkrst.reset, self.dut.reset),

            # Data connections
            (self.tx_xtor.tx_data, self.dut.tx_data),
            (self.tx_xtor.tx_valid, self.dut.tx_valid),
            (self.dut.tx_ready, self.tx_xtor.tx_ready),
        )

Binding Rules:

  • Only SV-domain components (Extern, XtorComponent) can have signal bindings

  • Each binding is a tuple: (source_signal, dest_signal)

  • Direction must match (output -> input)

  • Python components cannot participate in signal bindings

Profile and Checker

HDLTestbench Profile

The HDLTestbench profile enforces domain separation rules:

from zuspec.be.hdlsim.profile import HDLTestbenchProfile
from zuspec.be.hdlsim.checker import HDLTestbenchChecker

# Get profile instance
profile = HDLTestbenchProfile

# Get checker
checker = profile.get_checker()

# Check component
checker.check_component(MyTestbench)

# Report errors
if checker.has_errors():
    for error in checker.get_errors():
        print(f"Error: {error}")

Validation Rules

The checker validates:

  1. Domain Separation: No signal connections between Python and SV domains

  2. Component Classification: Correct use of Extern, XtorComponent, Component

  3. Binding Validity: Signal direction and type compatibility

  4. Protocol Compliance: XtorComponent correctly implements Protocol

Best Practices

Design Guidelines

  • Minimize Signal Crossing: Keep SV components together, use TLM at boundaries

  • Protocol First: Design transaction APIs before implementing transactors

  • Composition: Build complex testbenches from smaller, reusable components

  • Separation: Keep test logic (Python) separate from signal protocols (SV)

Performance Considerations

  • Transaction Granularity: Coarser transactions reduce Python/SV crossing overhead

  • Buffering: Use FIFOs/buffers at domain boundaries

  • Batch Operations: Group related operations in single API calls

Debugging Tips

  • Use profile checker early and often during development

  • Generate SV files and inspect them to understand what’s being created

  • Test transactors independently before integration

  • Use PyHDL-IF logging to trace Python/SV communication

Common Patterns

Clock/Reset Management

@dataclass
class ClkRstXtor(XtorComponent[ClkRstIf]):
    clock: Signal = output()
    reset: Signal = output()

# In test:
await tb.clkrst.reset_pulse(cycles=10)
await tb.clkrst.wait_cycles(100)

Bus Interfaces

@dataclass
class BusInitiator(XtorComponent[BusInitiatorIf]):
    # Bus signals
    ...

# In test:
await tb.bus.write_burst(addr=base, data=[1,2,3,4])
result = await tb.bus.read_burst(addr=base, count=4)

Monitor Components

@dataclass
class ScoreboardMonitor(Component):
    """Python-domain monitor."""

    def __post_init__(self):
        self.transactions = []

    async def monitor(self, xtor):
        """Monitor transactions."""
        while True:
            trans = await xtor.wait_transaction()
            self.transactions.append(trans)