Features

This page describes the key features of the Zuspec SystemVerilog backend and how they work.

Component Translation

Basic Components

Zuspec Components are translated to SystemVerilog modules with appropriate ports:

@zdc.dataclass
class Adder(zdc.Component):
    a : zdc.bit32 = zdc.input()
    b : zdc.bit32 = zdc.input()
    sum : zdc.bit32 = zdc.output()

Generates:

module Adder(
  input logic [31:0] a,
  input logic [31:0] b,
  output logic [31:0] sum
);
  // ...
endmodule

Parameterization

Const Fields as Parameters

Const fields in components become module parameters:

@zdc.dataclass
class ConfigurableAdder(zdc.Component):
    DATA_WIDTH : int = zdc.const(default=32)

    a : zdc.int = zdc.input(width=lambda s: s.DATA_WIDTH)
    b : zdc.int = zdc.input(width=lambda s: s.DATA_WIDTH)
    sum : zdc.int = zdc.output(width=lambda s: s.DATA_WIDTH)

Generates:

module ConfigurableAdder #(
  parameter int DATA_WIDTH = 32
)(
  input logic [(DATA_WIDTH-1):0] a,
  input logic [(DATA_WIDTH-1):0] b,
  output logic [(DATA_WIDTH-1):0] sum
);
  // ...
endmodule

Width Expressions

Lambda width expressions are converted to SystemVerilog parameter expressions:

# Zuspec
data : zdc.int = zdc.input(width=lambda s: s.DATA_WIDTH // 8)
// SystemVerilog
input logic [(DATA_WIDTH/8-1):0] data

Parameter Overrides

Instance parameters can be overridden using kwargs:

@zdc.dataclass
class Top(zdc.Component):
    DATA_WIDTH : int = zdc.const(default=32)

    adder : ConfigurableAdder = zdc.inst(
        kwargs=lambda s: dict(DATA_WIDTH=s.DATA_WIDTH + 4)
    )

Generates:

module Top #(
  parameter int DATA_WIDTH = 32
)(
  // ...
);

  ConfigurableAdder #(.DATA_WIDTH(DATA_WIDTH+4)) adder (
    // port connections
  );

endmodule

Clocked Processes

@sync Methods

Methods decorated with @zdc.sync are converted to always blocks:

@zdc.dataclass
class Register(zdc.Component):
    clock : zdc.bit = zdc.input()
    reset : zdc.bit = zdc.input()
    d : zdc.bit32 = zdc.input()
    q : zdc.bit32 = zdc.output()

    @zdc.sync(clock=lambda s:s.clock, reset=lambda s:s.reset)
    def _register(self):
        if self.reset:
            self.q = 0
        else:
            self.q = self.d

Generates:

always @(posedge clock or posedge reset) begin
  if (reset) begin
    q <= 0;
  end else begin
    q <= self.d;
  end
end

Match Statements

Python match/case statements are converted to SystemVerilog case statements:

@zdc.sync(clock=lambda s:s.clock)
def _fsm(self):
    match self.state:
        case 0:
            self.output = 1
            self.state = 1
        case 1:
            self.output = 0
            self.state = 0

Generates:

always @(posedge clock) begin
  case (state)
    0: begin
      output <= 1;
      state <= 1;
    end
    1: begin
      output <= 0;
      state <= 0;
    end
  endcase
end

Async Processes

@process Methods

Methods decorated with @zdc.process become initial blocks for testbench stimulus:

@zdc.dataclass
class Testbench(zdc.Component):
    clock : zdc.bit = zdc.output()
    data : zdc.bit32 = zdc.output()

    @zdc.process
    async def _stimulus(self):
        for i in range(10):
            self.data = i
            await self.posedge(self.clock)

Generates:

initial begin
  for (int i = 0; i < 10; i++) begin
    data = i;
    @(posedge clock);
  end
end

Timing Controls

Await expressions are converted to SystemVerilog timing controls:

  • await self.posedge(signal)@(posedge signal);

  • await self.wait(Time.ns(10))#10ns;

Bundle Handling

Automatic Flattening

Interface bundles are automatically flattened into individual ports:

@zdc.dataclass
class ValidReadyBundle(zdc.Struct):
    valid : zdc.bit = zdc.field(is_out=False)
    ready : zdc.bit = zdc.field(is_out=True)
    data : zdc.bit32 = zdc.field(is_out=False)

@zdc.dataclass
class Consumer(zdc.Component):
    io : ValidReadyBundle = zdc.field()

Generates:

module Consumer(
  input logic io_valid,
  output logic io_ready,
  input logic [31:0] io_data
);
  // ...
endmodule

Bundle Connections

Bundle-to-bundle connections are expanded into individual signal connections:

@zdc.dataclass
class System(zdc.Component):
    producer : Producer = zdc.inst()
    consumer : Consumer = zdc.inst()

    def __bind__(self):
        return {
            self.producer.io : self.consumer.io
        }

Generates:

module System(
  // ...
);

  Producer producer(
    .io_valid(consumer_io_valid),
    .io_ready(consumer_io_ready),
    .io_data(consumer_io_data)
  );

  Consumer consumer(
    .io_valid(consumer_io_valid),
    .io_ready(consumer_io_ready),
    .io_data(consumer_io_data)
  );

endmodule

Export Interfaces

Interface Generation

Export fields are converted to SystemVerilog interfaces with tasks:

from typing import Protocol

class DataIF(Protocol):
    async def send(self, value: int) -> int: ...

@zdc.dataclass
class Transactor(zdc.Component):
    data_if : DataIF = zdc.export()
    data_out : zdc.bit32 = zdc.output()

    def __bind__(self):
        return {
            self.data_if.send : self._send
        }

    async def _send(self, value: int) -> int:
        self.data_out = value
        await self.posedge(self.clock)
        return value + 1

Generates interface:

interface Transactor_data_if;

  logic [31:0] data_out = 0;

  task send(
    input logic [31:0] value,
    output logic [31:0] __ret);

    data_out = value;
    @(posedge clock);
    __ret = value + 1;

  endtask

endinterface

And module with interface instance:

module Transactor(
  // ports
);

  // Instantiate interface
  Transactor_data_if data_if();

  // Connect module signals to interface
  assign data_out = data_if.data_out;

endmodule

Instance Hierarchy

Component Instantiation

Component fields with zdc.inst() are instantiated as submodules:

@zdc.dataclass
class System(zdc.Component):
    clock : zdc.bit = zdc.input()

    counter1 : Counter = zdc.inst()
    counter2 : Counter = zdc.inst()

    def __bind__(self):
        return {
            self.counter1.clock : self.clock,
            self.counter2.clock : self.clock
        }

Generates:

module System(
  input logic clock
);

  Counter counter1 (
    .clock(clock)
  );

  Counter counter2 (
    .clock(clock)
  );

endmodule

External Components

External components (zdc.extern()) are also instantiated:

@zdc.dataclass
class RAM(zdc.Extern):
    addr : zdc.bit32 = zdc.input()
    data : zdc.bit32 = zdc.output()

@zdc.dataclass
class System(zdc.Component):
    ram : RAM = zdc.inst()

Generates instantiation with the external module name.

Debug Features

Source Location Annotations

Enable debug_annotations to include source file references:

generator = SVGenerator(
    output_dir=Path("output"),
    debug_annotations=True
)

Generates comments like:

// Generated from: test_smoke.py:15
module Counter(
  // ...
);

  // Source: test_smoke.py:20
  always @(posedge clock or posedge reset) begin
    // ...
  end

endmodule

Name Sanitization

Python names are sanitized to valid SystemVerilog identifiers:

  • Dots (.) → Double underscores (__)

  • Angle brackets (<, >) → Double underscores

  • Invalid characters → Underscores

  • Leading digits → Prefixed with underscore

Example: test_smoke.<locals>.Countertest_smoke__locals__Counter