Examples

This page provides complete, working examples demonstrating various HDLSim backend use cases.

Example 1: Simple Counter

A basic counter testbench demonstrating the fundamentals.

RTL (counter.sv)

module counter #(
    parameter WIDTH = 8
) (
    input  logic             clock,
    input  logic             reset,
    input  logic             enable,
    output logic [WIDTH-1:0] count
);
    logic [WIDTH-1:0] count_reg;

    always_ff @(posedge clock) begin
        if (reset)
            count_reg <= '0;
        else if (enable)
            count_reg <= count_reg + 1;
    end

    assign count = count_reg;
endmodule

Testbench (counter_tb.py)

import zuspec.dataclasses as zdc
from zuspec.dataclasses.protocols import Extern, XtorComponent
from zuspec.dataclasses import Signal, annotation_fileset

# Clock/reset protocol
class ClkRstIf:
    async def reset_assert(self):
        """Assert reset."""
        ...

    async def reset_deassert(self):
        """Deassert reset."""
        ...

    async def wait_cycles(self, n: int):
        """Wait N clock cycles."""
        ...

# Clock/reset transactor
@zdc.dataclass
class ClkRstXtor(XtorComponent[ClkRstIf]):
    clock: Signal = zdc.output()
    reset: Signal = zdc.output()

# Counter wrapper
@zdc.dataclass
class CounterDut(zdc.Component, Extern):
    clock: Signal = zdc.input()
    reset: Signal = zdc.input()
    enable: Signal = zdc.input()
    count: Signal = zdc.output()

    @annotation_fileset(sources=["rtl/counter.sv"])
    def __post_init__(self):
        pass

# Control transactor protocol
class CounterControlIf:
    async def set_enable(self, enable: bool):
        """Set enable signal."""
        ...

    async def get_count(self) -> int:
        """Read current count value."""
        ...

# Control transactor
@zdc.dataclass
class CounterControl(XtorComponent[CounterControlIf]):
    enable: Signal = zdc.output()
    count: Signal = zdc.input()

# Top-level testbench
@zdc.dataclass
class CounterTB(zdc.Component):
    clkrst: ClkRstXtor = zdc.inst()
    dut: CounterDut = zdc.inst()
    ctrl: CounterControl = zdc.inst()

    def __bind__(self):
        return (
            (self.clkrst.clock, self.dut.clock),
            (self.clkrst.reset, self.dut.reset),
            (self.ctrl.enable, self.dut.enable),
            (self.dut.count, self.ctrl.count),
        )

Test (test_counter.py)

import pytest

@pytest.fixture
def tb():
    from counter_tb import CounterTB
    return CounterTB()

async def test_counter_reset(tb):
    """Test counter reset behavior."""
    # Reset the counter
    await tb.clkrst.reset_assert()
    await tb.clkrst.wait_cycles(5)
    await tb.clkrst.reset_deassert()

    # Check count is 0
    count = await tb.ctrl.get_count()
    assert count == 0

async def test_counter_enable(tb):
    """Test counter counting."""
    # Reset and enable
    await tb.clkrst.reset_assert()
    await tb.clkrst.wait_cycles(5)
    await tb.clkrst.reset_deassert()

    await tb.ctrl.set_enable(True)
    await tb.clkrst.wait_cycles(10)

    # Count should be 10
    count = await tb.ctrl.get_count()
    assert count == 10

async def test_counter_disable(tb):
    """Test counter stops when disabled."""
    await tb.clkrst.reset_assert()
    await tb.clkrst.wait_cycles(5)
    await tb.clkrst.reset_deassert()

    # Count for a while
    await tb.ctrl.set_enable(True)
    await tb.clkrst.wait_cycles(5)

    # Disable and wait
    await tb.ctrl.set_enable(False)
    count_before = await tb.ctrl.get_count()
    await tb.clkrst.wait_cycles(10)
    count_after = await tb.ctrl.get_count()

    # Count should not change
    assert count_before == count_after

Example 2: Wishbone Bus

A more complex example with a standard bus interface.

Bus Protocol

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

    async def write(self, addr: int, data: int, sel: int = 0xF):
        """Single write transaction."""
        ...

    async def read(self, addr: int) -> int:
        """Single read transaction."""
        ...

    async def write_burst(self, addr: int, data: list[int]):
        """Burst write."""
        ...

    async def read_burst(self, addr: int, count: int) -> list[int]:
        """Burst read."""
        ...

Wishbone Transactor

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

    # Clock and reset
    clock: Signal = zdc.input()
    reset: Signal = zdc.input()

    # Wishbone signals
    wb_adr: Signal = zdc.output()
    wb_dat_o: Signal = zdc.output()
    wb_dat_i: Signal = zdc.input()
    wb_sel: Signal = zdc.output()
    wb_cyc: Signal = zdc.output()
    wb_stb: Signal = zdc.output()
    wb_we: Signal = zdc.output()
    wb_ack: Signal = zdc.input()
    wb_err: Signal = zdc.input()
    wb_rty: Signal = zdc.input()

Memory DUT

@zdc.dataclass
class WishboneMemory(zdc.Component, Extern):
    """Wishbone memory module."""

    clock: Signal = zdc.input()
    reset: Signal = zdc.input()

    wb_adr: Signal = zdc.input()
    wb_dat_i: Signal = zdc.input()
    wb_dat_o: Signal = zdc.output()
    wb_sel: Signal = zdc.input()
    wb_cyc: Signal = zdc.input()
    wb_stb: Signal = zdc.input()
    wb_we: Signal = zdc.input()
    wb_ack: Signal = zdc.output()
    wb_err: Signal = zdc.output()

    @annotation_fileset(
        sources=["rtl/wb_memory.sv"],
        incdirs=["rtl/include"]
    )
    def __post_init__(self):
        pass

Testbench

@zdc.dataclass
class WishboneTB(zdc.Component):
    clkrst: ClkRstXtor = zdc.inst()
    initiator: WishboneInitiator = zdc.inst()
    memory: WishboneMemory = zdc.inst()

    def __bind__(self):
        return (
            # Clock/reset
            (self.clkrst.clock, self.initiator.clock),
            (self.clkrst.clock, self.memory.clock),
            (self.clkrst.reset, self.initiator.reset),
            (self.clkrst.reset, self.memory.reset),

            # Wishbone bus
            (self.initiator.wb_adr, self.memory.wb_adr),
            (self.initiator.wb_dat_o, self.memory.wb_dat_i),
            (self.memory.wb_dat_o, self.initiator.wb_dat_i),
            (self.initiator.wb_sel, self.memory.wb_sel),
            (self.initiator.wb_cyc, self.memory.wb_cyc),
            (self.initiator.wb_stb, self.memory.wb_stb),
            (self.initiator.wb_we, self.memory.wb_we),
            (self.memory.wb_ack, self.initiator.wb_ack),
            (self.memory.wb_err, self.initiator.wb_err),
        )

Tests

async def test_wb_basic_write_read(tb):
    """Test basic write and read."""
    await tb.clkrst.reset_pulse(10)

    # Write data
    await tb.initiator.write(0x1000, 0xDEADBEEF)

    # Read back
    data = await tb.initiator.read(0x1000)
    assert data == 0xDEADBEEF

async def test_wb_burst_write(tb):
    """Test burst write operation."""
    await tb.clkrst.reset_pulse(10)

    test_data = [0x100, 0x200, 0x300, 0x400]
    base_addr = 0x2000

    # Burst write
    await tb.initiator.write_burst(base_addr, test_data)

    # Verify each location
    for i, expected in enumerate(test_data):
        actual = await tb.initiator.read(base_addr + i*4)
        assert actual == expected

Example 3: Multi-Component System

Demonstrates a more complex system with multiple components and monitors.

System Architecture

@zdc.dataclass
class UartTx(zdc.Component, Extern):
    """UART transmitter."""
    clock: Signal = zdc.input()
    reset: Signal = zdc.input()
    tx_data: Signal = zdc.input()
    tx_valid: Signal = zdc.input()
    tx_ready: Signal = zdc.output()
    tx_out: Signal = zdc.output()

    @annotation_fileset(sources=["rtl/uart_tx.sv"])
    def __post_init__(self): pass

@zdc.dataclass
class UartRx(zdc.Component, Extern):
    """UART receiver."""
    clock: Signal = zdc.input()
    reset: Signal = zdc.input()
    rx_in: Signal = zdc.input()
    rx_data: Signal = zdc.output()
    rx_valid: Signal = zdc.output()
    rx_ready: Signal = zdc.input()

    @annotation_fileset(sources=["rtl/uart_rx.sv"])
    def __post_init__(self): pass

Driver and Monitor

# Driver protocol
class UartDriverIf:
    async def send_byte(self, data: int): ...
    async def send_packet(self, data: bytes): ...

# Monitor protocol
class UartMonitorIf:
    async def recv_byte(self) -> int: ...
    async def recv_packet(self, length: int) -> bytes: ...

@zdc.dataclass
class UartDriver(XtorComponent[UartDriverIf]):
    clock: Signal = zdc.input()
    tx_data: Signal = zdc.output()
    tx_valid: Signal = zdc.output()
    tx_ready: Signal = zdc.input()

@zdc.dataclass
class UartMonitor(XtorComponent[UartMonitorIf]):
    clock: Signal = zdc.input()
    rx_data: Signal = zdc.input()
    rx_valid: Signal = zdc.input()
    rx_ready: Signal = zdc.output()

Python Scoreboard

@zdc.dataclass
class UartScoreboard(zdc.Component):
    """Pure Python scoreboard component."""

    def __post_init__(self):
        self.sent_packets = []
        self.recv_packets = []

    def record_sent(self, data: bytes):
        """Record sent packet."""
        self.sent_packets.append(data)

    def record_received(self, data: bytes):
        """Record received packet."""
        self.recv_packets.append(data)

    def check(self) -> bool:
        """Verify sent == received."""
        return self.sent_packets == self.recv_packets

Complete Testbench

@zdc.dataclass
class UartLoopbackTB(zdc.Component):
    """UART loopback testbench."""

    clkrst: ClkRstXtor = zdc.inst()
    driver: UartDriver = zdc.inst()
    monitor: UartMonitor = zdc.inst()
    tx: UartTx = zdc.inst()
    rx: UartRx = zdc.inst()
    scoreboard: UartScoreboard = zdc.inst()

    def __bind__(self):
        return (
            # Clock/reset
            (self.clkrst.clock, self.driver.clock),
            (self.clkrst.clock, self.monitor.clock),
            (self.clkrst.clock, self.tx.clock),
            (self.clkrst.clock, self.rx.clock),
            (self.clkrst.reset, self.tx.reset),
            (self.clkrst.reset, self.rx.reset),

            # Driver -> TX
            (self.driver.tx_data, self.tx.tx_data),
            (self.driver.tx_valid, self.tx.tx_valid),
            (self.tx.tx_ready, self.driver.tx_ready),

            # TX -> RX (serial)
            (self.tx.tx_out, self.rx.rx_in),

            # RX -> Monitor
            (self.rx.rx_data, self.monitor.rx_data),
            (self.rx.rx_valid, self.monitor.rx_valid),
            (self.monitor.rx_ready, self.rx.rx_ready),
        )

Advanced Test

async def test_uart_loopback_with_scoreboard(tb):
    """Test UART loopback with scoreboard checking."""
    await tb.clkrst.reset_pulse(10)

    # Test data
    test_packets = [
        b"Hello",
        b"World",
        b"Zuspec HDLSim!",
    ]

    # Send and monitor in parallel
    async def sender():
        for packet in test_packets:
            await tb.driver.send_packet(packet)
            tb.scoreboard.record_sent(packet)

    async def receiver():
        for _ in test_packets:
            packet = await tb.monitor.recv_packet(len(packet))
            tb.scoreboard.record_received(packet)

    # Run concurrently
    import asyncio
    await asyncio.gather(sender(), receiver())

    # Check scoreboard
    assert tb.scoreboard.check(), "Mismatch detected!"

See Also