Structuring and De-Structuring Connections
The "structured connection" type (name-debate in #4, referred to as "bundle" here) is one of Hdl21 and similar libraries' primary productivity-improvement mechanisms for connecting Module
s to one another.
For example from the Hdl21 unit tests:
@h.bundle
class Jtag:
roles = HostDevice
tck, tdi, tms = h.Signals(3, src=roles.HOST, dest=roles.DEVICE)
tdo = h.Signal(src=roles.DEVICE, dest=roles.HOST)
@h.bundle
class Spi:
roles = HostDevice
sck, cs = h.Signals(2, src=roles.HOST, dest=roles.DEVICE)
dq = h.Signal(src=roles.DEVICE, dest=roles.HOST, width=4)
@h.module
class Chip:
spi = Spi(role=HostDevice.HOST, port=True)
jtag = Jtag(role=HostDevice.DEVICE, port=True)
... # much more internal content
Here Chip.spi
and Chip.jtag
are bundle-valued Port
s, each of which has several underlying (scalar) Signals. Each set of signals commonly travels together, and the bundle-type allows passing them around as a single entity rather than via each of their constituent elements.
Here
This issue is about creating ("structuring") and breaking up ("destructuring") these connection-objects. This will need to happen at both the top and bottom of Hdl21 hierarchies, for a few common reasons:
- At hierarchy-bottom,
Module
s will eventually need to connect to Primitive
elements, which do not include bundle-valued ports. Similarly many ExternalModule
s created in and instantiated from legacy tools will not include such a concept, and must be resolved to scalar ports.
- At hierarchy-top, exposing an IO interface to said legacy tools (with fully designer-controlled names), while using bundles internally, will require constructing bundles from scalar signals.
- In other cases, different bundles will require "re-wiring" between each other. These may be due to different signal-naming conventions, or to express boundary-changes in functionality (e.g. wiring each
tx
and rx
in a symmetric communications link).
Destructuring
The former is (I think) the easier of the two cases. A dot-access into a Bundle
produces a reference to its constituent Signal
s and/or sub-Bundle
s. These can then be connected to other instances, like so:
@h.bundle
class WholeBoardIo:
spi = Spi()
jtag = Jtag()
@h.module
class Board:
io = WholeBoardIo(port=True)
# Connect to a thing called `SpiFlash`, which only uses the `spi` part of `WholeBoardIo`
flash = SpiFlash(spi=io.spi)
# ...
This feature has been supported in Hdl21 essentially since the introduction of the structured-connection (Interface
/ Bundle
) type.
Structuring
How to create bundles from scalar signals is a more interesting policy decision. Considering this case:
@h.bundle
class Diff:
p = h.Signal()
n = h.Signal()
@h.module
class Child:
dport = Diff(port=True)
@h.module
class Parent:
p, n = h.Ports(2)
child = Child() # ... now what?
Module Parent
exposes scalar-valued ports, and instantiates module child
with one (or more) bundle-valued ports. (Similar to bullet 2, "top of hierarchy", in section "Here".)
There are a few policies I can imagine:
- Connect a signal at a time
- Create (something) "bundle-valued"
- Connections into
Bundle
instances
- All of the above
Connecting a Signal at a Time
The "signal at a time" concept would use nested left-hand-side dot-accesses to dictate instance connections. In the case of Parent
and Child
above, this would look like so:
# In `Parent` class-body
child = Child()
child.dport.p = p
child.dport.n = n
# ...
This is very similar to the typical semantics of Chisel connections.
This looks pretty good for the relatively simple case above. Noting however that bundles can be nested, as can bundle-valued ports. So larger cases would look more like:
# A more complicated port-type
child.bundle_port.sub_bundle1.sub_sub_bundle.some_signal = p
This would then require a "connection rules" policy similar to that discussed in #3. For example, imagine embedding the snippet above in a procedural set of connections, including something like so:
# Several connections to this `child.bundle_port`
child.bundle_port.sub_bundle1 = some_bundle
child.bundle_port.sub_bundle2 = some_other_bundle
child.bundle_port.sub_bundle3 = bundle3 #(1)
child.bundle_port.sub_bundle3.p = some_signal #(2) Hm, now what?
Here p
is assigned both by commented lines (1) and (2). Our resolution (thus far) to #3 has been "disallow this altogether", i.e. by not enabling the nested LHS assignment-connections.
Create Something Bundle-Valued
The primary alternative is for "instantiators" to create a structured bundle-like object to connect to each bundle-valued port. In the (more elaborate) case above in which child
has a port named bundle_port
, this would look something like:
child.bundle_port = Something( # Creating the `Something`-bundle-like object
sub_bundle1 = some_other_bundle,
sub_bundle3 = Something ( # Nesting another `Something`
p = some_signal,
# everything else comes from `bundle3`
)
)
The generally anonymous Something
here is very much like Bundle
, but has an anomyous bundle-type. These are essentially BundleInstance
s of anonymous types created at instantiation time. Instances then have a single connection per-port, including for bundle-valued ports.
Connections into Bundle Instances
SystemVerilog's interface
instead exposes select signals as externally-connectable ports, while maintaining other signals as internal and private. Its interfaces
are more like "modules without instances" in this sense. An example from https://www.doulos.com/knowhow/systemverilog/systemverilog-tutorials/systemverilog-interfaces-tutorial/:
interface ClockedBus (input Clk);
logic[7:0] Addr, Data;
logic RWn;
endinterface
module RAM (ClockedBus Bus);
always @(posedge Bus.Clk)
if (Bus.RWn)
Bus.Data = mem[Bus.Addr];
else
mem[Bus.Addr] = Bus.Data;
endmodule
// Using the interface
module Top;
reg Clock;
// Instance the interface with an input, using named connection
ClockedBus TheBus (.Clk(Clock));
RAM TheRAM (.Bus(TheBus));
...
endmodule
Here ClockedBus
has an external input-signal Clk
and several other internal, private signals. The top instance of ClockedBus
in module Top
gets (requires?) an external signal tied to Clk
. The ClockedBus
-valued port Bus
of module RAM
in contrast does not take such an external connection.
While admitting I don't know these connection-rules completely, I don't love this method.
Status
Commits as of af2c3ce (as of this writing, head of dev
) include the "Something
" bundle-valued concept, in which this type is named AnonymousBundle
. It does not include nested LHS assignments, nor SystemVerilog-style connections into BundleInstance
s.
I do not believe AnonymousBundle
should be the final name. But also don't have a better one immediately at hand. "Bundle
" in contrast is used for the bundle-type definitions.
This AnonymousBundle
implementation does not support connect-by-assignment, or replacement of connections by assigment. All connections are made at creation-time. This avoids needing support for a "nested connection" policy as outlined here and in #3, while perhaps making some implementations more cumbersome.