Clock and Reset Domains¶
Domains are the mechanism by which zuspec.dataclasses propagates clock and
reset information through a component hierarchy without explicit CLK/RST
wiring. Every component that contains @zdc.sync logic should derive from
SyncComponent; the framework then connects the
right clock and reset automatically at elaboration time.
Conceptual overview¶
Every model has an implicit time domain (the Component base). Digital
logic typically also belongs to a clock domain and a reset domain.
Designs closer to implementation may additionally declare a power domain
(reserved for future elaboration passes).
Domains bind top-down by default: a child component inherits its parent’s clock and reset domain unless explicitly overridden.
Quick start — single-clock design¶
The simplest case: one clock, one reset, automatic propagation.
import zuspec.dataclasses as zdc
@zdc.dataclass
class Counter(zdc.SyncComponent):
"""32-bit counter — clock and reset are inherited."""
count: zdc.bit32 = zdc.output(reset=0)
@zdc.sync # fires on the inherited clock_domain
def tick(self):
self.count += 1
@zdc.dataclass
class Top(zdc.SyncComponent):
"""Board-level top. Declares the concrete clock and reset."""
CLK: zdc.bit = zdc.input()
RST_N: zdc.bit = zdc.input()
clock_domain = zdc.clock_domain(
clock=lambda s: s.CLK,
reset=lambda s: s.RST_N,
period=zdc.Time.ns(10),
name="sys_clk",
)
counter: Counter = zdc.inst() # inherits clock_domain / reset_domain
No __bind__ needed for the counter — domain flows automatically.
For designs without physical clock/reset ports (simulation-only or after domain resolution) use bare class-level declarations:
@zdc.dataclass
class Top(zdc.SyncComponent):
clock_domain = zdc.ClockDomain(period=zdc.Time.ns(10), name="sys_clk")
reset_domain = zdc.ResetDomain(style="none") # reset-free
counter: Counter = zdc.inst()
Named (multi-clock) domains¶
Declare extra ClockDomain class variables for each additional clock.
Bind @zdc.sync methods to a specific domain using domain=:
from typing import ClassVar
@zdc.dataclass
class DualClock(zdc.SyncComponent):
fast_clk: ClassVar[zdc.ClockDomain] = zdc.ClockDomain(
period=zdc.Time.ns(2), name="fast_clk"
)
slow_count: zdc.bit32 = 0
fast_count: zdc.bit32 = 0
@zdc.sync # fires on default clock_domain
def slow_step(self):
self.slow_count += 1
@zdc.sync(domain=lambda s: s.fast_clk) # fires on fast_clk only
def fast_step(self):
self.fast_count += 1
The default (bare @zdc.sync) domain is always clock_domain, inherited
from the parent if not overridden on the component.
Derived domains and zdc.super()¶
Express PLL or clock-divider relationships with
DerivedClockDomain:
@zdc.dataclass
class ClockGen(zdc.SyncComponent):
# Divide the parent-inherited clock by 4.
fast_clk = zdc.DerivedClockDomain(
source=zdc.super(), # relative to inherited clock_domain
div=4,
name="fast_div4",
)
super() is an alias for
InheritedDomain — it expresses “derive from
whatever domain my parent provides”. You can also use a lambda to derive
from a named sibling domain:
fast_clk = zdc.DerivedClockDomain(
source=lambda s: s.pll_out, # sibling domain on same component
mul=3,
)
Domain binding in __bind__¶
Override a child component’s domain from the parent’s __bind__:
@zdc.dataclass
class Top(zdc.SyncComponent):
fast_clk: ClassVar[zdc.ClockDomain] = zdc.ClockDomain(
period=zdc.Time.ns(2), name="fast_clk"
)
normal: Worker = zdc.inst() # stays on default clock_domain
fast: Worker = zdc.inst() # will be moved to fast_clk
def __bind__(self):
return {
self.fast.clock_domain: self.fast_clk,
}
After elaboration fast ticks only when fast_clk is driven; normal
ticks only when the default clock_domain is driven. The same syntax works
for reset_domain overrides.
SDC output¶
Generate Synopsys Design Constraints (Tcl/SDC) from an elaborated design:
from zuspec.dataclasses.sdc_emit import emit_sdc
async with zdc.simulate(Top) as top:
print(emit_sdc(top))
Example output:
# ---- Clock definitions ----
create_clock -name {sys_clk} -period 10.000
# ---- Derived / generated clocks ----
create_generated_clock -name {fast_div4} -source [get_clocks {sys_clk}] -divide_by 4
# ---- False paths (CDC crossings & reset sequencing) ----
# CDC crossing: slow_clk → fast_clk
set_false_path -from [get_clocks {slow_clk}] -to [get_clocks {fast_clk}]
emit_sdc() is a convenience wrapper.
SDCEmitPass provides finer control:
p = zdc.SDCEmitPass()
p.visit(top)
text = p.sdc_text()
CDC primitives¶
Use these classes for known-safe clock-domain crossings. They suppress the
corresponding set_false_path in SDC output automatically.
zdc.TwoFFSyncSynthesizable two-flip-flop synchronizer for single-bit CDC crossings.
zdc.AsyncFIFOStructural placeholder for multi-bit asynchronous FIFOs.
@zdc.cdc_unchecked(reason)Marks a class or field as a known-safe crossing that should be excluded from CDC analysis.
@zdc.cdc_unchecked("Gray-coded counter — safe by construction")
@zdc.dataclass
class GraySyncBus(zdc.Component): ...
Migration guide — from explicit CLK/RST to domains¶
Before (explicit wiring):
@zdc.dataclass
class Counter(zdc.Component):
CLK: zdc.bit = zdc.input()
RST: zdc.bit = zdc.input()
count: zdc.bit32 = zdc.output()
@zdc.sync(clock=lambda s: s.CLK, reset=lambda s: s.RST)
def tick(self):
if self.RST:
self.count = 0
else:
self.count += 1
@zdc.dataclass
class Top(zdc.Component):
CLK: zdc.bit = zdc.input()
RST: zdc.bit = zdc.input()
counter: Counter = zdc.inst()
def __bind__(self):
return (
(self.counter.CLK, self.CLK),
(self.counter.RST, self.RST),
)
After (domain-based):
@zdc.dataclass
class Counter(zdc.SyncComponent):
reset_domain = zdc.ResetDomain(style="sync")
count: zdc.bit32 = zdc.output(reset=0)
@zdc.sync # auto clock + reset via inherited domain
def tick(self):
self.count += 1 # reset handled by SyncComponent infrastructure
@zdc.dataclass
class Top(zdc.SyncComponent):
CLK: zdc.bit = zdc.input()
RST: zdc.bit = zdc.input()
clock_domain = zdc.clock_domain(
clock=lambda s: s.CLK,
reset=lambda s: s.RST,
period=zdc.Time.ns(10),
)
counter: Counter = zdc.inst() # no __bind__ needed
Key changes:
Counterinherits fromSyncComponentinstead ofComponent.The
CLK/RSTports are removed fromCounter.@zdc.syncbecomes bare (noclock=/reset=lambdas).reset=0on output fields provides the reset value; the framework injectsif self.rst: self.count = 0at synthesis time.Top.__bind__no longer needs to wire CLK/RST.
Power domains (future)¶
zdc.PowerDomain is a stub annotation for upcoming UPF/CPF power-aware
elaboration passes. Declare at class level to name the power domain:
@zdc.dataclass
class AlwaysOn(zdc.SyncComponent):
pwr_domain = zdc.PowerDomain(name="always_on", always_on=True)
Power-domain analysis is not yet implemented.