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¶
Quick Start - Getting started guide
Components - Component types reference
DV Flow Manager Integration - DFM workflow setup