Interfaces
An interface groups a set of related ports (and the parameters that shape them) into
a single, reusable bundle, and describes, through one or more views, how that bundle
is driven from each side of a connection. Interfaces let you declare a protocol such as a
streaming handshake or an AXI bus once, then wire it between designs with a single <>
connection instead of dozens of per-port connections.
Terminology
- interface - A Scala class extending
Interfacethat bundles related ports and parameters. It is purely structural: it carries ports and views but no behavior (no processes, connections, or assignments). - port - A value declared inside an interface (e.g.
Bits(8) <> VAR). Ports are internal to the interface and are exposed only through a view. - flippable port - A
<> VARport: undirected at declaration, given a relative direction by each view (opposite on the two ends of a connection). This is the default. - anchored-direction port - A
<> INor<> OUTport: its direction is absolute (the same for every side, never reversed byflip), like a clock or reset. - view - A named, directional projection of an interface's ports. A view assigns each
port a direction (
in/out) from the point of view of the design that uses it. An interface may declare several views over the same ports. - flip - The converse of a view: every flippable
inbecomesoutand everyoutbecomesin. Used to derive the opposite side of a protocol (e.g.subordinate = manager.flip). - interface instance - An interface class instantiated inside a design, exactly like a child design instance.
class Stream(width: Int <> CONST) extends Interface:
protected val data = Bits(width) <> VAR
protected val valid = Bit <> VAR
protected val ready = Bit <> VAR
// the producer drives data+valid and reads ready
val source = view.out(data, valid).in(ready)
// the consumer side is simply the converse
val sink = source.flip
What Interfaces Are For
Hardware protocols rarely consist of a single wire. A handshake carries data, valid,
and ready; an AXI channel carries a dozen signals that always travel together and always
have a fixed relative direction. Declaring and connecting those signals individually is
verbose and error-prone; a single flipped direction or forgotten port is a connectivity
bug.
Interfaces solve this by letting you:
- Bundle related ports into one named, parameterized unit that can be instantiated anywhere a child design can.
- Declare directionality once with a view, and obtain the opposite side for free with
flip(so amanager/subordinateorsource/sinkpair is always consistent). - Connect an entire protocol with one
<>, with the connection checked port-by-port through the whole (possibly nested) hierarchy. - Map to native target constructs (SystemVerilog
interface/modportand VHDL-2019 mode views) while remaining portable to every tool through an automatic flattening fallback (see Generated HDL).
Interfaces are the DFHDL analog of a SystemVerilog interface with modports, or a
VHDL-2019 record with mode views.
Declaring an Interface
Syntax
An interface declaration follows standard Scala class
syntax and extends the Interface base class:
/** _documentation_ */
[_modifiers_] class _name_(_params_) extends Interface:
protected val _port_ = _type_ <> VAR // one or more ports
val _view_ = view._dir_(_ports_)... // one or more views
end _name_ //optional `end` marker
-
_name_- The Scala class name for the interface. As with designs, this name is preserved by the DFHDL compiler and used in generated artifacts (the SystemVeriloginterfacename / VHDL record name) and in error messages. See naming. -
(_params_)- An optional parameter block, identical to a design parameter block. DFHDL parameters (_type_ <> CONST) are preserved through compilation and may shape port widths; pure Scala parameters are inlined during elaboration. Interface parameters must beprotected(see below). -
_port_- An interface port, declared just like a design variable/port. A<> VARport is flippable: undirected at declaration, with a view assigning its direction. A<> INor<> OUTport is anchored: its direction is fixed at declaration and never reversed by a view or byflip(see Anchored-Direction Ports). Ports must beprotected. -
_view_- A named view built with theviewbuilder (see Views).
Unlike a design, an interface is domain-neutral: there is a single Interface base
class rather than DF/RT/ED variants, and the same interface can be instantiated
inside a design of any domain.
Ports and Parameters Must Be protected
Every port and DFHDL parameter of an interface must be declared protected (or
private). This is enforced by the DFHDL compiler plugin:
class BadIfc extends Interface:
val data = Bits(8) <> VAR // error: interface ports must be `protected`
class GoodIfc extends Interface:
protected val data = Bits(8) <> VAR // OK
val mon = view.out(data) // views stay public; they are the public surface
Why ports are protected
A view, not a raw port, is the public surface of an interface. Ports are the internal
wiring, reachable only through a view's .ASIS projection. Marking them protected means
Scala's own access control (enforced by the typechecker) blocks any ifc.data selection
from outside the interface, and the ports are hidden from IDE autocomplete. This keeps every
connection going through a directed view, so the compiler can check directionality. The
rule applies transitively: ports declared in a subclass of an interface must be protected
too.
Example: a parameterized handshake
A reusable streaming handshake interface
The Stream interface below bundles a data bus with a valid/ready handshake. The
width of the data bus is a DFHDL parameter, so it is preserved in the generated backend
code. Two views describe the two sides of the protocol: source (the producer) drives
data and valid and reads ready; sink (the consumer) is simply its converse.
class Stream(protected val width: Int <> CONST = 8) extends Interface:
protected val data = Bits(width) <> VAR
protected val valid = Bit <> VAR
protected val ready = Bit <> VAR
val source = view.out(data, valid).in(ready)
val sink = source.flip
Views
A view is a named, directional projection of an interface's ports. Each port that
participates in a view is given a direction with view.in(...) or view.out(...), and an
interface may declare any number of views over the same ports (e.g. a producer view, a
consumer view, and a monitor view).
Defining views
Views are created with the view builder inside the interface body. Calls can be chained
to mix directions:
val v1 = view.out(a, b) // a, b are outputs of the using design
val v2 = view.in(a, b, c) // a, b, c are inputs of the using design
val v3 = view.in(a, b).out(c) // mixed: a, b inputs; c output
val v4 = view.in(a).inout(b) // b is bidirectional
The ports passed to view.in/view.out/view.inout must be ports declared in this
interface; the builder reads them by reference. A view is itself a public val; it is the
member you reach through from outside the interface. A port given inout keeps that direction
under flip (inout is its own converse).
Direction perspective
A view's directions are written from the point of view of the design that uses the view:
view.out(p): portpis an output of the using design (the design drivesp).view.in(p): portpis an input to the using design (the design readsp).
This is the same convention as a SystemVerilog modport (directions are from the consuming
module's viewpoint). It follows that the two ends of a connection must use opposite
directions for each shared port, which is exactly what flip produces.
flip: the converse view
flip returns the converse of a view: every in becomes out and every out becomes
in. It is the idiomatic way to declare the opposite side of a protocol without repeating
(and risking inconsistency in) the direction list:
val source = view.out(data, valid).in(ready)
val sink = source.flip // in(data, valid).out(ready)
flip is involutive (source.flip.flip is source) and maps directly onto the target
languages: a VHDL-2019 'converse alias, or an inverted SystemVerilog modport.
The generic view
Every interface instance also exposes a generic, undirected view through .ASIS directly
on the instance (without naming a declared view). The generic view carries all ports with
no enforced direction and is useful for internal wiring where directionality is not being
checked. Prefer a named, directed view for design boundaries. A generic projection also
supports the .MONITOR and .DRIVER terminals (see Projection terminals);
it has no .FLIP/.FLIPALL, because its ports carry no direction to flip.
Anchored-Direction Ports
Most interface ports are flippable (<> VAR): they have no direction of their own, and each
view gives them a relative direction that flip reverses. Some signals, though, have an
absolute direction that is the same for every side and must never be reversed. The canonical
example is a clock or reset: it enters an interface one way and is observed identically by
everyone connected to it.
For these, declare the port with an anchored direction, <> IN, <> OUT, or <> INOUT,
instead of <> VAR:
class Bus(protected val width: Int <> CONST) extends Interface:
protected val clk = Bit <> IN // anchored: always an input
protected val data = Bits(width) <> VAR // flippable
protected val valid = Bit <> VAR // flippable
val manager = view.in(clk).out(data, valid)
val subordinate = manager.flip // data/valid flip; clk stays `in`
An anchored port is the DFHDL counterpart of a SystemVerilog interface header port (the
(input clk, ...) list): the direction belongs to the signal itself, not to the view.
Anchored ports follow two rules:
- A view may use an anchored port only in its declared direction. Adding a
<> OUTport withview.in(...)(or a<> INport withview.out(...)) is a compile error. Flippable ports, by contrast, take whichever direction the view assigns. flipleaves anchored ports untouched. It reverses only the flippable ports of a view (recursing through nested views), so an anchoredclk : instaysinon both themanagerand thesubordinateside.
A bidirectional <> INOUT port is a special case: inout is its own converse, so flip (and
flipAll) leave it inout on both sides. It maps directly to a SystemVerilog inout modport
entry and a VHDL mode-view inout element (whose 'converse is also inout).
How anchored ports affect connections
Because an anchored port keeps its direction on both sides, what a <> connection wires up
depends on where the connection is made:
- Between sibling designs (a view connected to its
flip): only the flippable ports are connected. Anchored ports are left for you to wire separately, since both ends see the same direction (twoclk : inports cannot drive each other); a shared clock is typically routed from a common source. - From a parent design down into a child (a view connected to the same-orientation view, no
flip): all ports are connected, flippable and anchored alike, so the parent passes its anchored inputs (such asclk/rst) down into the child.
Using an Interface in a Design
Instantiation
An interface is instantiated inside a design exactly like a child design, by calling its constructor. Empty parentheses are required even when there are no parameters:
class Producer extends RTDesign:
val io = Stream(width = 8) // an interface instance
// ...
Projecting a view: instance.view.ASIS
To obtain a connectable value, select a declared view on the instance and call .ASIS.
This projects the view onto that specific instance:
val src = io.source.ASIS // the producer side of `io`
val snk = io.sink.ASIS // the consumer side of `io`
Because the interface ports are protected, io.data is rejected by the compiler; all
access goes through a view's projection terminal.
Projection terminals
.ASIS is one of a small family of projection terminals. Each one projects the view onto the
instance and bakes in a directionality, so you choose the terminal instead of transforming the
view first. On a named view:
| Terminal | Projects the view... |
|---|---|
.ASIS |
as declared |
.FLIP |
flipped, anchored ports respected (the same as projecting view.flip) |
.FLIPALL |
flipped, anchored ports inverted too |
.MONITOR |
with every port forced to an input (observe everything) |
.DRIVER |
with every port forced to an output (drive everything) |
.FLIP is the safe, anchor-respecting flip. .FLIPALL, .MONITOR, and .DRIVER are
overrides: they ignore the anchored/flippable distinction, so reach for them deliberately
(for example, a .MONITOR tap that observes a whole bus, or a .DRIVER stimulus block).
Unlike flip, these overrides exist only as projection terminals: there is no reusable
"forced" view, so an override is always visible at the connection site and can never be hidden
inside a shared named view.
On the generic projection only .ASIS, .MONITOR, and .DRIVER are
available: the flip terminals need declared relative directions to act on, and a generic
projection's ports carry none, so there is nothing for .FLIP/.FLIPALL to reverse.
Connecting views with <>
Two views are connected with the <> connection operator, just like
scalar ports. The connection is checked port-by-port through the entire (possibly nested)
hierarchy: each shared port must have complementary directions on the two sides. Pairing
a view with its flip is therefore always a legal connection:
class Link extends RTDesign:
val io = Stream(8)
val prod = Producer()
val cons = Consumer()
prod.out <> io.source.ASIS // producer drives the source side
cons.in <> io.sink.ASIS // consumer reads the sink side
A single <> here replaces one connection per port (data, valid, ready), and the
direction of every wire is guaranteed consistent by construction.
If the interface has anchored-direction ports, what a <> connects
depends on the connection site: a sibling-to-sibling connection wires only the flippable ports,
while a parent-to-child connection wires all ports, including the anchored ones.
Interfaces are purely structural
An interface body may contain only ports and views. Processes, <> connections, and :=
/:== assignments are not allowed inside an interface; these belong in designs.
Additionally, an interface must be a named class: anonymous instances such as
new Stream() {} are rejected by the compiler, because each interface needs a stable
identity to be emitted as a named SystemVerilog interface / VHDL record.
Nested Interfaces
An interface may instantiate other interfaces and compose their views into a higher-level
view. This is how multi-channel protocols such as AXI are modeled: one interface per
channel, then a top interface that nests them and composes their per-channel views with the
view(...) apply form (which takes already-directed sub-views).
AXI4-Lite: a nested, multi-channel interface
Each AXI4-Lite channel is its own interface with a manager view (directions written from
the manager/master point of view) and a subordinate = manager.flip. The top Axi4Lite
interface nests the five channels and composes their manager views into a single
manager view; subordinate flips recursively through every nested channel.
// One interface per AXI4-Lite channel (write-address channel shown).
class Axi4LiteAW(protected val addrWidth: Int <> CONST) extends Interface:
protected val AWADDR = UInt(addrWidth) <> VAR
protected val AWPROT = Bits(3) <> VAR
protected val AWVALID = Bit <> VAR
protected val AWREADY = Bit <> VAR
val manager = view.out(AWADDR, AWPROT, AWVALID).in(AWREADY)
val subordinate = manager.flip
// ... Axi4LiteW, Axi4LiteB, Axi4LiteAR, Axi4LiteR similarly ...
// Top interface nesting the five channels and composing their views.
class Axi4Lite(
protected val addrWidth: Int <> CONST,
protected val dataWidth: Int <> CONST
) extends Interface:
protected val aw = Axi4LiteAW(addrWidth)
protected val w = Axi4LiteW(dataWidth)
protected val b = Axi4LiteB()
protected val ar = Axi4LiteAR(addrWidth)
protected val r = Axi4LiteR(dataWidth)
val manager = view(aw.manager, w.manager, b.manager, ar.manager, r.manager)
val subordinate = manager.flip // flips recursively through every nested channel
A manager and a subordinate side then connect with a single <>:
val bus = Axi4Lite(addrWidth = 32, dataWidth = 32)
val mgr = bus.manager.ASIS
val sub = bus.subordinate.ASIS
mgr <> sub // legal: matches port-per-port through all five nested channels
Vector Views
A view can be replicated into a vector with the X operator, mirroring DFHDL
vector construction. This is useful for interconnects, for example a
1-to-N crossbar that exposes N manager-side buses. Individual elements are selected by
index:
val managers = Axi4Lite(32, 32).manager.ASIS X 4 // a vector of 4 manager-side views
val m0 = managers(0) // select element 0
How Interfaces Map to Generated HDL
DFHDL emits interfaces in one of two ways, depending on the target dialect.
Portable default (flattening). By default, the compiler flattens interfaces and views
into plain scalar ports and per-port connections before backend emission. This produces
valid, synthesizable HDL on every supported tool, with no reliance on
interface/modport or mode-view support. Nested interfaces are expanded with prefixed
signal names (aw_AWADDR, …). The top-level design boundary is always flattened to scalar
ports, regardless of dialect.
Native emission (opt-in). For dialects that support them, interfaces can be emitted natively, preserving the structure:
A DFHDL interface becomes a SystemVerilog interface, each view becomes a modport, and
flip becomes a modport with inverted directions:
interface Axi4LiteAW #(parameter ADDR_WIDTH = 32) ();
logic [ADDR_WIDTH-1:0] AWADDR;
logic [2:0] AWPROT;
logic AWVALID;
logic AWREADY;
modport manager (output AWADDR, AWPROT, AWVALID, input AWREADY);
modport subordinate (input AWADDR, AWPROT, AWVALID, output AWREADY);
endinterface
Nested interfaces are not portable for synthesis (e.g. Vivado does not support them), so they are flattened unless the target is known to accept them.
A DFHDL interface becomes a record type, each view becomes a mode view, and flip
becomes a 'converse alias:
type axi4lite_aw_t is record
AWADDR : unsigned;
AWPROT : std_logic_vector(2 downto 0);
AWVALID : std_logic;
AWREADY : std_logic;
end record;
view axi4lite_aw_manager of axi4lite_aw_t is
AWADDR, AWPROT, AWVALID : out;
AWREADY : in;
end view;
alias axi4lite_aw_subordinate is axi4lite_aw_manager'converse;
VHDL-2019 mode views are currently supported mainly by NVC. The portable default therefore stays flattened for VHDL; native mode-view emission is opt-in.
Restrictions Summary
- Interface ports and DFHDL parameters must be
protected(orprivate). - An interface body may contain only ports and views: no processes, connections, or assignments.
- Ports cannot be initialized (
init/initFile) inside an interface; an interface is purely structural and carries no initial values. - An anchored-direction port (
<> IN/<> OUT) may be added to a view only in its declared direction, andflipnever reverses it. - Interfaces must be named classes; anonymous instances (
new Ifc() {}) are rejected. - External access to ports is only through a view's projection terminal (
.ASIS,.FLIP,.MONITOR, etc.).