Design Domains
DFHDL offers three key domain abstractions, dataflow (DF), register-transfer (RT), and event-driven (ED), all within a single HDL, as illustrated in the following figure. This unique capability allows developers to employ a cohesive syntax to seamlessly blend these abstractions: DF, RT, and ED. Each abstraction brings its own set of advantages in terms of control, synthesizability, simulation speed, and functional correctness.
The RT abstraction mirrors the capabilities found in languages like Chisel and Amaranth, while the ED abstraction aligns with the functionalities of VHDL and Verilog. Through an intelligent compilation process, the DFHDL compiler transitions from the higher-level DF abstraction through RT and ultimately to ED. The choice of compilation dialect (VHDL 93/2008 or Verilog/SystemVerilog) determines the final ED code representation.

Dataflow (DF) Domain
The dataflow domain provides the highest level of abstraction, focusing on data dependencies rather than timing.
Key Features
- Timing-agnostic descriptions
- Implicit state handling
- Token stream semantics
- History access via
.prev
Example
1 2 3 4 | |
Register Transfer (RT) Domain
The RT domain provides explicit control over registers and timing while maintaining hardware-friendly abstractions.
Domain Configuration
Clock, reset, and inter-domain relations are declared by attaching @hw.constraints.timing.*
annotations to the RTDesign / RTDomain. Each annotation field is optional; unset fields are
filled in from the global ElaborationOptions defaults at compile time, and from any
@timing.related ancestor.
Clock Annotation
1 2 3 4 5 6 7 8 9 10 | |
Reset Annotation
1 2 3 4 5 6 7 8 | |
Annotations may be partial: @timing.clock(rate = 100.MHz) overrides only the clock rate
and inherits the rest from the elaboration defaults. The empty form @timing.clock() /
@timing.reset() forces the slot to appear (e.g. on a combinational or blackbox owner) while
still deriving every field from the defaults.
Inclusion Policies
AsNeeded: Only emits clock/reset ports when actually used.AlwaysAtTop: Always emits the ports at the top level (silenced with@unusedif unused).
Domain Types
Basic RT Domain
1 2 3 4 | |
Multiple Clock Domains
1 2 3 4 5 6 7 8 9 | |
grpName distinguishes domains that should generate independent Clk_<grp> / Rst_<grp>
opaque port types and ports.
Related Domains
A domain whose clock/reset is inherited from another domain (sibling, parent, etc.) carries
@timing.related(target) instead of its own @timing.clock / @timing.reset. The compiler
omits the clock/reset ports for the related domain and reuses the target's.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
By default a related domain also shares the target's reset. Passing includeReset = false
keeps the shared clock but drops the reset: the domain's registers and memories are not driven
from a reset-initialization block and instead rely on their initial values (init) alone.
1 2 3 4 5 6 7 8 | |
Derived Clocks (Gated Clocks)
A related domain may declare its own clock port, either an input (Clk <> IN, consuming
the derived clock) or an output (Clk <> OUT, sourcing it):
1 2 3 4 5 6 7 | |
This declares a derived clock: a clock that is fully synchronous with the clock of the
related target (same source, same edges, phase-aligned), while the reset (subject to
includeReset) is still shared through the relation. The typical use is a gated clock:
an input port receives a gated version of the origin clock from outside, and an output
port exports one that the design gates internally (the design scope drives it, e.g.
active.clk <> gatedClk.as(active.Clk)). Because the domains are related, no
clock-domain-crossing discipline applies between them, and sharing an asynchronous reset
across the gated clocks is safe (a flop whose clock is gated off still sees the reset
assertion).
The identity of a derived clock is its design-relative name: domain active with port
clk identifies as active_clk, which is also its flattened port name. The connection
rule is deliberately narrow and predictable: same-named derived clocks within the same
clock group form one clock, automatically threaded across the hierarchy (through
automatically added pass-through ports, also named active_clk), with an output port or
an explicitly connected port as the source. A derived clock is never implicitly merged
onto its origin clock:
- Sourced somewhere: a
Clk <> OUTport (the internal gating site), or any port a parent explicitly connects (e.g.child.active.clk <> gatedClk.as(child.active.Clk)), sources every same-named port in scope. - Sourced nowhere: the derived clock surfaces as a top-level input port instead of silently taking the origin clock, so a forgotten gated-clock connection is visible in the port list rather than a silently dead or wrongly merged clock.
- The ungated form is an explicit choice: to run a derived clock from the origin clock
(as in an FPGA build of an ASIC design that removes clock gating), connect the two
explicitly, e.g. at a wrapper that declares its own root clock port:
core.active.clk <> clk.as(core.active.Clk).
Derived clocks nest: a related domain with its own clock port may itself be the target of another related domain, whose clock port then derives from the outer derived clock (gating a gated clock). A related domain without its own clock port that targets a clocked related domain uses that domain's derived clock, while its reset still resolves through the full relation chain to the origin.
Related Domain Shorthands and Regions
The most common related target is the enclosing design or domain itself, so every RT
container provides two shorthand domain classes and one scoping construct. Each is exactly
equivalent to a plain RTDomain with the corresponding annotations, and manifests as such
(printing, compilation, and naming see no difference):
| Construct | Equivalent to |
|---|---|
RTRelatedDomain |
@timing.related(this) new RTDomain |
RTDerivedClkDomain |
RTRelatedDomain with a val clk = Clk <> IN declaration |
RTDerivedClkDomainSrc |
RTRelatedDomain with a val clk = Clk <> OUT declaration |
RTRegion |
RTRelatedDomain with @flattenMode.transparent |
1 2 3 4 5 6 7 8 9 | |
All three are members of every RT container, so the related target is selected by the
instantiation path: a bare new RTRelatedDomain relates to the enclosing container, while
new gated.RTRelatedDomain (or new gated.RTRegion, etc.) relates to the gated domain
instead, equivalent to @timing.related(gated).
The two domain shorthands create a grouping with a footprint of its own:
RTRelatedDomainis the general grouping tool: it scopes a piece of logic under the same clock and reset without minting a new clock group, and its members flatten with the domain-name prefix. Use the annotation form (@timing.related(this, includeReset = false)) when the domain must opt out of the reset.RTDerivedClkDomaindeclares a derived (typically gated) clock as described in the previous section; itsclkport identifies by the domain's name (domainactiveyields theactive_clkidentity and flattened port name).RTDerivedClkDomainSrcis its sourcing variant (Clk <> OUT): the internal gating site, whose design scope drives the derived clock (e.g.active.clk <> icgOut.as(active.Clk)) and exports it to every same-named derived clock in scope.
An RTRegion is deliberately the opposite: a scoping construct with no observable
footprint of its own, neither a clock identity nor a naming one. It places logic under a
timing context while leaving every member's own name (and therefore the generated HDL)
untouched, which is what makes it useful where a design declares its domain configuration
once, around its ports, and internal logic is later regrouped without renaming anything.
(The variant that also opts out of the reset, e.g. to keep a memory outside the reset
scope, still uses the annotation form: @timing.related(this, includeReset = false)
together with @flattenMode.transparent.)
The Domain-and-Regions Pattern
Regions unfold their full value path-prefixed. The common pattern declares a timing context exactly once as a named domain, and then opens sparse regions of it wherever pieces of logic naturally live in the code, with none of them paying a naming cost:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
Every region's registers are clocked by active's derived clock and reset by the design's
shared reset, yet state and acc flatten under their own names, exactly as if the design
had a single domain. The regions can be scattered freely between free-running logic, so the
code order follows the design's dataflow rather than its clock grouping.
Register Types and Initialization
Register Declarations vs Aliases
1 2 3 4 5 6 7 8 9 10 | |
The Register Model
A register declared with VAR.REG or OUT.REG has two sides:
- The output is the declaration name itself (
reg). Reading it yields the value the register has held since the last clock edge. That value is stable for the whole cycle, no matter what the design body assigns, and it is immutable: you cannot assign to it. - The input is
reg.din. It is what the register will hold after the next clock edge, and it is the only assignable side.
The clock edge is what moves the input to the output. Everything the design body does within a cycle happens to the input side.
Register Access Patterns
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
Reading the Register Input
reg.din can also be read. It yields the register's pending value: whatever has been assigned to
it so far in the current cycle, or the register's output when nothing has been assigned yet. This
lets a register be built up in steps:
1 2 3 4 5 6 7 | |
The compiler gives the register a shadow variable holding its pending value, seeds it from the register at the top of the cycle, applies the assignments in order, and registers the result on the clock edge:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | |
r_din is a module-level logic assigned with blocking assignments inside
always_comb, so each statement sees the value left by the previous one.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | |
r_din_v), which does carry the value forward between statements, and it is
published to the r_din signal at the end of the process for the clocked process to register.
A .din read reflects the assignments above it, not the register's final input value. Reading it
before any assignment yields the register output, which is why the model above stays consistent:
1 2 3 4 5 6 7 8 | |
As with assignment, a partial selection comes before .din, not after:
1 2 3 4 5 | |
A .din read cannot be given a name
Binding a .din read to a Scala val is rejected at elaboration:
1 | |
The reason is that such a binding looks like a snapshot but behaves as a live view: d would keep
reporting the pending value at whatever point it is later read, so r.din := 5 between the binding
and its use would change what d yields. Apply .din directly where it is read instead, which
reads the same and leaves no room for the confusion:
1 | |
Register Composition
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
Advanced Register Patterns
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
Common Patterns and Pitfalls
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
Event-Driven (ED) Domain
The ED domain provides the lowest level of abstraction, with explicit process blocks and event sensitivity.
Process Types
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
Assignment Types
- Blocking (
:=): Immediate effect - Non-blocking (
:==): Scheduled update1 2 3
process(clk.rising): val temp = x // Blocking read y :== temp // Non-blocking write
Domain Interaction
Cross-Domain Communication
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Domain Flattening
During compilation, nested domains are flattened while preserving clock and reset relationships:
1 2 3 4 5 6 7 8 9 10 11 12 | |
Compilation Flow
- Domain Resolution:
- Flattens nested domains
- Resolves clock and reset configurations
-
Establishes domain hierarchies
-
State Management:
- Converts DF
.prevto explicit registers - Handles RT register declarations
-
Manages ED process state variables
-
Process Generation:
- Converts DF and RT to ED processes
- Optimizes sensitivity lists
-
Handles blocking/non-blocking assignments
-
Backend Generation:
- Generates VHDL or Verilog code
- Preserves timing relationships
- Maintains design hierarchy
Best Practices
- Domain Selection:
- Use DF for algorithmic descriptions
- Use RT for timing-critical paths
-
Use ED for low-level control
-
Clock Domain Crossing:
- Use explicit synchronization
- Maintain clear domain boundaries
-
Document clock relationships
-
State Management:
- Initialize all registers
- Use appropriate reset strategies
-
Consider reset domains
-
Performance Optimization:
- Balance domain abstractions
- Use appropriate clock domains
- Consider resource utilization