Interfaces and connections
The amaranth.lib.wiring
module provides a way to declare the interfaces between design components and connect them to each other in a reliable and convenient way.
Introduction
Overview
This module provides four related facilities:
Description and construction of interface objects via
Flow
(In
andOut
),Member
, andSignature
, as well as the associated container classSignatureMembers
. These classes provide the syntax used in defining components, and are also useful for introspection.Flipping of signatures and interface objects via
FlippedSignature
andFlippedInterface
, as well as the associated container classFlippedSignatureMembers
. This facility reduces boilerplate by adapting existing signatures and interface objects: the flip operation changes theIn
data flow of a member toOut
and vice versa.Connecting interface objects together via
connect()
. Theconnect()
function ensures that the provided interface objects can be connected to each other, and adds the necessary.eq()
statements to aModule
.Defining reusable, self-contained components via
Component
. Components areElaboratable
objects that interact with the rest of the design through an interface specified by their signature.
To use this module, add the following imports to the beginning of the file:
from amaranth.lib import wiring
from amaranth.lib.wiring import In, Out
The “Motivation” and “Reusable interfaces” sections describe concepts that are essential for using this module and writing idiomatic Amaranth code. The sections after describe advanced use cases that are only relevant for more complex code.
Motivation
Consider a reusable counter with an enable input, configurable limit, and an overflow flag. Using only the core Amaranth language, it could be implemented as:
class BasicCounter(Elaboratable):
def __init__(self):
self.en = Signal()
self.count = Signal(8)
self.limit = Signal.like(self.count)
self.overflow = Signal()
def elaborate(self, platform):
m = Module()
with m.If(self.en):
m.d.sync += self.overflow.eq(0)
with m.If(self.count == self.limit):
m.d.sync += self.overflow.eq(1)
m.d.sync += self.count.eq(0)
with m.Else():
m.d.sync += self.count.eq(self.count + 1)
return m
Nothing in this implementation indicates the directions of its ports (en
, count
, limit
, and overflow
) in relation to other parts of the design. To understand whether the value of a port is expected to be provided externally or generated internally, it is first necessary to read the body of the elaborate
method. If the port is not used within that method in a particular elaboratable, it is not possible to determine its direction, or whether it is even meant to be connected.
The amaranth.lib.wiring
module provides a solution for this problem: components. A component is an elaboratable that declares the shapes and directions of its ports in its signature. The example above can be rewritten to use the Component
base class (which itself inherits from Elaboratable
) to be:
class ComponentCounter(wiring.Component):
en: In(1)
count: Out(8)
limit: In(8)
overflow: Out(1)
def elaborate(self, platform):
m = Module()
with m.If(self.en):
m.d.sync += self.overflow.eq(0)
with m.If(self.count == self.limit):
m.d.sync += self.overflow.eq(1)
m.d.sync += self.count.eq(0)
with m.Else():
m.d.sync += self.count.eq(self.count + 1)
return m
The code in the constructor creating the signals of the counter’s interface one by one is now gone, replaced with the variable annotations declaring the counter’s interface. The inherited constructor, Component.__init__()
, creates the same attributes with the same values as before, and the elaborate
method is unchanged.
The major difference between the two examples is that the ComponentCounter
provides unambiguous answers to two questions that previously required examining the elaborate
method:
Which of the Python object’s attributes are ports that are intended to be connected to the rest of the design.
What is the direction of the flow of information through the port.
This information, aside from being clear from the source code, can now be retrieved from the .signature
attribute, which contains an instance of the Signature
class:
>>> ComponentCounter().signature
Signature({'en': In(1), 'count': Out(8), 'limit': In(8), 'overflow': Out(1)})
The shapes of the ports need not be static. The ComponentCounter
can be made generic, with its range specified when it is constructed, by creating the signature explicitly in its constructor:
class GenericCounter(wiring.Component):
def __init__(self, width):
super().__init__({
"en": In(1),
"count": Out(width),
"limit": In(width),
"overflow": Out(1)
})
# The implementation of the `elaborate` method is the same.
elaborate = ComponentCounter.elaborate
>>> GenericCounter(16).signature
Signature({'en': In(1), 'count': Out(16), 'limit': In(16), 'overflow': Out(1)})
Instances of the ComponentCounter
and GenericCounter
class are two examples of interface objects. An interface object is a Python object of any type whose a signature
attribute contains a Signature
with which the interface object is compliant (as determined by the is_compliant
method of the signature).
The next section introduces the concepts of directionality and connection, and discusses interface objects in more detail.
Reusable interfaces
Consider a more complex example where two components are communicating with a stream that is using ready/valid signaling, where the valid
signal indicates that the value of data
provided by the source is meaningful, and the ready
signal indicates that the sink has consumed the data word:
class DataProducer(wiring.Component):
en: In(1)
data: Out(8)
valid: Out(1)
ready: In(1)
def elaborate(self, platform): ...
class DataConsumer(wiring.Component):
data: In(8)
valid: In(1)
ready: Out(1)
# ... other ports...
def elaborate(self, platform): ...
Data would be transferred between these components by assigning the outputs to the inputs elsewhere in the design:
m = Module()
m.submodules.producer = producer = DataProducer()
m.submodules.consumer = consumer = DataConsumer()
...
m.d.comb += [
consumer.data.eq(producer.data),
consumer.valid.eq(producer.valid),
producer.ready.eq(consumer.ready),
]
Although this example is short, it is already repetitive and redundant. The ports on the producer and the consumer, which must match each other for the connection to be made correctly, are declared twice; and the connection itself is made in an error-prone manual way even though the signatures include all of the information required to create it.
The signature of a stream could be defined in a generic way:
class SimpleStreamSignature(wiring.Signature):
def __init__(self, data_shape):
super().__init__({
"data": Out(data_shape),
"valid": Out(1),
"ready": In(1)
})
def __eq__(self, other):
return self.members == other.members
>>> SimpleStreamSignature(8).members
SignatureMembers({'data': Out(8), 'valid': Out(1), 'ready': In(1)})
A definition like this is usable, depending on the data flow direction of the members, only in the producer (as in the code above) or only in the consumer. To resolve this problem, this module introduces flipping: an operation that reverses the data flow direction of the members of a signature or an interface object while leaving everything else about the object intact. In Amaranth, the (non-flipped) signature definition always declares the data flow directions appropriate for a bus initiator, stream source, controller, and so on. A bus target, stream sink, peripheral, and so on would reuse the source definition by flipping it.
A signature is flipped by calling sig.flip()
, and an interface object is flipped by calling flipped(intf)
. These calls return instances of the FlippedSignature
and FlippedInterface
classes, respectively, which use metaprogramming to wrap another object, changing only the data flow directions of its members and forwarding all other method calls and attribute accesses to the wrapped object.
The example above can be rewritten to use this definition of a stream signature as:
class StreamProducer(wiring.Component):
en: In(1)
source: Out(SimpleStreamSignature(8))
def elaborate(self, platform): ...
class StreamConsumer(wiring.Component):
sink: Out(SimpleStreamSignature(8).flip())
def elaborate(self, platform): ...
m = Module()
m.submodules.producer = producer = StreamProducer()
m.submodules.consumer = consumer = StreamConsumer()
The producer and the consumer reuse the same signature, relying on flipping to make the port directions complementary:
>>> producer.source.signature.members
SignatureMembers({'data': Out(8), 'valid': Out(1), 'ready': In(1)})
>>> producer.source.signature.members['data']
Out(8)
>>> consumer.sink.signature.members
SignatureMembers({'data': Out(8), 'valid': Out(1), 'ready': In(1)}).flip()
>>> consumer.sink.signature.members['data']
In(8)
In the StreamConsumer
definition above, the sink
member has its direction flipped explicitly because the sink is a stream input; this is the case for every interface due to how port directions are defined. Since this operation is so ubiquitous, it is also performed when In(...)
is used with a signature rather than a shape. The StreamConsumer
definition above should normally be written as:
class StreamConsumerUsingIn(wiring.Component):
sink: In(SimpleStreamSignature(8))
def elaborate(self, platform): ...
The data flow directions of the ports are identical between the two definitions:
>>> consumer.sink.signature.members == StreamConsumerUsingIn().sink.signature.members
True
If signatures are nested within each other multiple levels deep, the final port direction is determined by how many nested In(...)
members there are. For each In(...)
signature wrapping a port, the data flow direction of the port is flipped once:
>>> sig = wiring.Signature({"port": Out(1)})
>>> sig.members["port"]
Out(1)
>>> in1 = wiring.Signature({"sig": In(sig)})
>>> in1.members["sig"].signature.members["port"]
In(1)
>>> in2 = wiring.Signature({"sig": In(in1)})
>>> in2.members["sig"].signature.members["sig"].signature.members["port"]
Out(1)
Going back to the stream example, the producer and the consumer now communicate with one another using the same set of ports with identical shapes and complementary directions (the auxiliary en
port being outside of the stream signature), and can be connected using the connect()
function:
wiring.connect(m, producer.source, consumer.sink)
This function examines the signatures of the two provided interface objects, ensuring that they are exactly complementary, and then adds combinatorial .eq()
statements to the module for each of the port pairs to form the connection. Aside from the connectability check, the single line above is equivalent to:
m.d.comb += [
consumer.sink.data.eq(producer.source.data),
consumer.sink.valid.eq(producer.source.valid),
producer.source.ready.eq(consumer.sink.ready),
]
Even on the simple example of a stream signature it is clear how using the connect()
function results in more concise, readable, and robust code. The difference is proportionally more pronounced with more complex signatures. When a signature is being refactored, no changes to the code that uses connect()
is required.
This explanation concludes the essential knowledge necessary for using this module and writing idiomatic Amaranth code.
Forwarding interior interfaces
Consider a case where a component includes another component as a part of its implementation, and where it is necessary to forward the ports of the inner component, that is, expose them within the outer component’s signature. To use the SimpleStreamSignature
definition above in an example:
class DataProcessorImplementation(wiring.Component):
source: Out(SimpleStreamSignature(8))
def elaborate(self, platform): ...
class DataProcessorWrapper(wiring.Component):
source: Out(SimpleStreamSignature(8))
def elaborate(self, platform):
m = Module()
m.submodules.impl = impl = DataProcessorImplementation()
m.d.comb += [
self.source.data.eq(impl.source.data),
self.source.valid.eq(impl.source.valid),
impl.source.ready.eq(self.source.ready),
]
return m
Because forwarding the ports requires assigning an output to an output and an input to an input, the connect()
function, which connects outputs to inputs and vice versa, cannot be used—at least not directly. The connect()
function is designed to cover the usual case of connecting the interfaces of modules from outside those modules. In order to connect an interface from inside a module, it is necessary to flip that interface first using the flipped()
function. The DataProcessorWrapper
should instead be implemented as:
class DataProcessorWrapper(wiring.Component):
source: Out(SimpleStreamSignature(8))
def elaborate(self, platform):
m = Module()
m.submodules.impl = impl = DataProcessorImplementation()
wiring.connect(m, wiring.flipped(self.source), impl.source)
return m
In some cases, both of the two interfaces provided to connect()
must be flipped. For example, the correct way to implement a component that forwards an input interface to an output interface with no processing is:
class DataForwarder(wiring.Component):
sink: In(SimpleStreamSignature(8))
source: Out(SimpleStreamSignature(8))
def elaborate(self, platform):
m = Module()
wiring.connect(m, wiring.flipped(self.sink), wiring.flipped(self.source))
return m
Warning
It is important to wrap an interface with the flipped()
function whenever it is being connected from inside the module. If the elaborate
function above had made a connection using wiring.connect(m, self.sink, self.source)
, it would not work correctly. No diagnostic is emitted in this case.
Constant inputs
Sometimes, a component must conform to a particular signature, but some of the input ports required by the signature must have a fixed value at all times. This module addresses this case by allowing both Signal
and Const
objects to be used to implement port members:
class ProducerRequiringReady(wiring.Component):
source: Out(SimpleStreamSignature(8))
def __init__(self):
super().__init__()
self.source.ready = Const(1)
def elaborate(self, platform): ...
class ConsumerAlwaysReady(wiring.Component):
sink: In(SimpleStreamSignature(8))
def __init__(self):
super().__init__()
self.sink.ready = Const(1)
def elaborate(self, platform): ...
class ConsumerPossiblyUnready(wiring.Component):
sink: In(SimpleStreamSignature(8))
def elaborate(self, platform): ...
>>> SimpleStreamSignature(8).is_compliant(ProducerRequiringReady().source)
True
>>> SimpleStreamSignature(8).flip().is_compliant(ConsumerAlwaysReady().sink)
True
However, the connect()
function considers a constant input to be connectable only to a constant output with the same value:
>>> wiring.connect(m, ProducerRequiringReady().source, ConsumerAlwaysReady().sink)
>>> wiring.connect(m, ProducerRequiringReady().source, ConsumerPossiblyUnready().sink)
Traceback (most recent call last):
...
amaranth.lib.wiring.ConnectionError: Cannot connect to the input member 'arg0.ready' that has a constant value 1
This feature reduces the proliferation of similar but subtly incompatible interfaces that are semantically similar, only differing in the presence or absence of optional control or status signals.
Adapting interfaces
Sometimes, a design requires an interface with a particular signature to be used, but the only implementation available is either a component with an incompatible signature or an elaboratable with no signature at all. If this problem cannot be resolved by other means, interface adaptation can be used, where the existing signals are placed into a new interface with the appropriate signature. For example:
class LegacyAXIDataProducer(Elaboratable):
def __init__(self):
self.adata = Signal(8)
self.avalid = Signal()
self.aready = Signal()
def elaborate(self, platform): ...
class ModernDataConsumer(wiring.Component):
sink: In(SimpleStreamSignature(8))
data_producer = LegacyAXIDataProducer()
data_consumer = ModernDataConsumer()
adapted_data_source = SimpleStreamSignature(8).create()
adapted_data_source.data = data_producer.adata
adapted_data_source.valid = data_producer.avalid
adapted_data_source.ready = data_producer.aready
m = Module()
wiring.connect(m, adapted_data_source, data_consumer.sink)
When creating an adapted interface, use the create
method of the signature that is required elsewhere in the design.
Customizing signatures and interfaces
The amaranth.lib.wiring
module encourages creation of reusable building blocks. In the examples above, a custom signature, SimpleStreamSignature
, was introduced to illustrate the essential concepts necessary to use this module. While sufficient for that goal, it does not demonstrate the full capabilities provided by the module.
Consider a simple System-on-Chip memory bus with a configurable address width. In an application like that, additional properties and methods could be usefully defined both on the signature (for example, properties to retrieve the parameters of the interface) and on the created interface object (for example, methods to examine the control and status signals). These can be defined as follows:
from amaranth.lib import enum
class TransferType(enum.Enum, shape=1):
Write = 0
Read = 1
class SimpleBusSignature(wiring.Signature):
def __init__(self, addr_width=32):
self._addr_width = addr_width
super().__init__({
"en": Out(1),
"rw": Out(TransferType),
"addr": Out(self._addr_width),
"r_data": In(32),
"w_data": Out(32),
})
@property
def addr_width(self):
return self._addr_width
def __eq__(self, other):
return isinstance(other, SimpleBusSignature) and self.addr_width == other.addr_width
def __repr__(self):
return f"SimpleBusSignature({self.addr_width})"
def create(self, *, path=None, src_loc_at=0):
return SimpleBusInterface(self, path=path, src_loc_at=1 + src_loc_at)
class SimpleBusInterface(wiring.PureInterface):
def is_read_xfer(self):
return self.en & (self.rw == TransferType.Read)
def is_write_xfer(self):
return self.en & (self.rw == TransferType.Write)
This example demonstrates several important principles of use:
Defining additional properties for a custom signature. The
Signature
objects are mutable in a restricted way, and can be frozen with thefreeze
method. In almost all cases, the newly defined properties must be immutable, as shown above.Defining a signature-specific
__eq__
method. While anonymous (created from a dictionary of members) instances ofSignature
compare structurally, instances ofSignature
-derived classes compare by identity unless the equality operator is overridden. In almost all cases, the equality operator should compare the parameters of the signatures rather than their structures.Defining a signature-specific
__repr__
method. Similarly to__eq__
, the default implementation forSignature
-derived classes uses the signature’s identity. In almost all cases, the representation conversion operator should return an expression that constructs an equivalent signature.Defining a signature-specific
create
method. The default implementation used in anonymous signatures,Signature.create()
, returns a new instance ofPureInterface
. Whenever the custom signature has a corresponding custom interface object class, this method should return a new instance of that class. It should not have any required arguments beyond the ones thatSignature.create()
has (required parameters should be provided when creating the signature and not the interface), but may take additional optional arguments, forwarding them to the interface object constructor.
>>> sig32 = SimpleBusSignature(); sig32
SimpleBusSignature(32)
>>> sig24 = SimpleBusSignature(24); sig24
SimpleBusSignature(24)
>>> sig24.addr_width
24
>>> sig24 == SimpleBusSignature(24)
True
>>> bus = sig24.create(); bus
<SimpleBusInterface: SimpleBusSignature(24), en=(sig bus__en), rw=EnumView(TransferType, (sig bus__rw)), addr=(sig bus__addr), r_data=(sig bus__r_data), w_data=(sig bus__w_data)>
>>> bus.is_read_xfer()
(& (sig bus__en) (== (sig bus__rw) (const 1'd1)))
The custom properties defined for both the signature and the interface object can be used on the flipped signature and the flipped interface in the usual way:
>>> sig32.flip().addr_width
32
>>> wiring.flipped(bus).is_read_xfer()
(& (sig bus__en) (== (sig bus__rw) (const 1'd1)))
Note
Unusually for Python, when the implementation of a property or method is invoked through a flipped object, the self
argument receives the flipped object that has the type FlippedSignature
or FlippedInterface
. This wrapper object proxies all attribute accesses and method calls to the original signature or interface, the only change being that of the data flow directions. See the documentation for these classes for a more detailed explanation.
Warning
While the wrapper object forwards attribute accesses and method calls, it does not currently proxy special methods such as __getitem__
or __add__
that are rarely, if ever, used with interface objects. This limitation may be lifted in the future.
Paths
Whenever an operation in this module needs to refer to the interior of an object, it accepts or produces a path: a tuple of strings and integers denoting the attribute names and indexes through which an interior value can be extracted. For example, the path ("buses", 0, "cyc")
into the object obj
corresponds to the Python expression obj.buses[0].cyc
.
When they appear in diagnostics, paths are printed as the corresponding Python expression.
Signatures
- class amaranth.lib.wiring.Flow
Direction of data flow. This enumeration has two values,
Out
andIn
, the meaning of which depends on the context in which they are used.- Out
Outgoing data flow.
When included in a standalone
Signature
, a portMember
with anOut
data flow carries data from an initiator to a responder. That is, the signature describes the initiator driving the signal and the responder sampling the signal.When used as the flow of a signature
Member
, indicates that the data flow of the port members of the inner signature remains the same.When included in the
signature
property of anElaboratable
, the signature describes the elaboratable driving the corresponding signal. That is, the elaboratable is treated as the initiator.
- In
Incoming data flow.
When included in a standalone
Signature
, a portMember
with anIn
data flow carries data from an responder to a initiator. That is, the signature describes the initiator sampling the signal and the responder driving the signal.When used as the flow of a signature
Member
, indicates that the data flow of the port members of the inner signature is flipped.When included in the
signature
property of anElaboratable
, the signature describes the elaboratable sampling the corresponding signal. That is, the elaboratable is treated as the initiator, the same as in theOut
case.
- flip()
Flip the direction of data flow.
- amaranth.lib.wiring.Out = Flow.Out
A shortcut for importing
Flow.Out
asamaranth.lib.wiring.Out
.
- amaranth.lib.wiring.In = Flow.In
A shortcut for importing
Flow.In
asamaranth.lib.wiring.In
.
- class amaranth.lib.wiring.Member(flow, description, *, reset=None)
Description of a signature member.
This class is a discriminated union: its instances describe either a port member or a signature member, and accessing properties for the wrong kind of member raises an
AttributeError
.The class is created from a description: a
Signature
instance (in which case theMember
is created as a signature member), or a shape-like object (in which case it is created as a port member). After creation theMember
instance cannot be modified.When a
Signal
is created from a description of a port member, the signal’s reset value is taken from the member description. If this signal is never explicitly assigned a value, it will equalreset
.Although instances can be created directly, most often they will be created through
In
andOut
, e.g.In(unsigned(1))
orOut(stream.Signature(RGBPixel))
.- flip()
Flip the data flow of this member.
- Returns:
A new
member
withmember.flow
equal toself.flow.flip()
, and identical toself
other than that.- Return type:
- array(*dimensions)
Add array dimensions to this member.
The dimensions passed to this method are prepended to the existing dimensions. For example,
Out(1).array(2)
describes an array of 2 elements, whereas bothOut(1).array(2, 3)
andOut(1).array(3).array(2)
both describe a two dimensional array of 2 by 3 elements.Dimensions are passed to
array()
in the order in which they would be indexed. That is,.array(x, y)
creates a member that can be indexed up to[x-1][y-1]
.The
array()
method is composable: callingmember.array(x)
describes an array ofx
members even ifmember
was already an array.- Returns:
A new
member
withmember.dimensions
extended bydimensions
, and identical toself
other than that.- Return type:
- property is_port
Whether this is a description of a port member.
- Returns:
True
if this is a description of a port member,False
if this is a description of a signature member.- Return type:
- property is_signature
Whether this is a description of a signature member.
- Returns:
True
if this is a description of a signature member,False
if this is a description of a port member.- Return type:
- property shape
Shape of a port member.
- Returns:
The shape that was provided when constructing this
Member
.- Return type:
- Raises:
AttributeError – If
self
describes a signature member.
- property reset
Reset value of a port member.
- Returns:
The reset value that was provided when constructing this
Member
.- Return type:
- Raises:
AttributeError – If
self
describes a signature member.
- property signature
Signature of a signature member.
- Returns:
The signature that was provided when constructing this
Member
.- Return type:
- Raises:
AttributeError – If
self
describes a port member.
- exception amaranth.lib.wiring.SignatureError
This exception is raised when an invalid operation specific to signature manipulation is performed with
SignatureMembers
, such as adding a member to a frozen signature. Other exceptions, such asTypeError
orNameError
, will still be raised where appropriate.
- class amaranth.lib.wiring.SignatureMembers(members=())
Mapping of signature member names to their descriptions.
This container, a
collections.abc.Mapping
, is used to implement themembers
attribute of signature objects.The keys in this container must be valid Python attribute names that are public (do not begin with an underscore. The values must be instances of
Member
. The container is mutable in a restricted manner: new keys may be added, but existing keys may not be modified or removed. In addition, the container can be frozen, which disallows addition of new keys. Freezing a container recursively freezes the members of any signatures inside.In addition to the use of the superscript operator, multiple members can be added at once with the
+=
opreator.The
create()
method converts this mapping into a mapping of names to signature members (signals and interface objects) by creating them from their descriptions. The created mapping can be used to populate an interface object.- flip()
Flip the data flow of the members in this mapping.
- Returns:
Proxy collection
FlippedSignatureMembers(self)
that flips the data flow of the members that are accessed using it.- Return type:
- __eq__(other)
Compare the members in this and another mapping.
- Returns:
True
if the mappings contain the same key-value pairs,False
otherwise.- Return type:
- __getitem__(name)
Retrieves the description of a member with a given name.
- Return type:
- Raises:
TypeError – If
name
is not a string.NameError – If
name
is not a valid, public Python attribute name.SignatureError – If a member called
name
does not exist in the collection.
- __setitem__(name, member)
Stub that forbids addition of members to the collection.
- Raises:
SignatureError – Always.
- __delitem__(name)
Stub that forbids removal of members from the collection.
- Raises:
SignatureError – Always.
- __iter__()
Iterate through the names of members in the collection.
- Returns:
Names of members, in the order of insertion.
- Return type:
iterator of
str
- flatten(*, path=())
Recursively iterate through this collection.
Note
The paths returned by this method and by
Signature.flatten()
differ. This method yields a single result for eachMember
in the collection, disregarding their dimensions:>>> sig = wiring.Signature({ ... "items": In(1).array(2) ... }) >>> list(sig.members.flatten()) [(('items',), In(1).array(2))]
The
Signature.flatten()
method yields multiple results for such a member; see the documentation for that method for an example.
- create(*, path=None, src_loc_at=0)
Create members from their descriptions.
For each port member, this function creates a
Signal
with the shape and reset value taken from the member description, and the name constructed from the paths to the member (by concatenating path items with a double underscore,__
).For each signature member, this function calls
Signature.create()
for that signature. The resulting object can have any type if aSignature
subclass overrides thecreate
method.If the member description includes dimensions, in each case, instead of a single member, a
list
of members is created for each dimension. (That is, for a single dimension a list of members is returned, for two dimensions a list of lists is returned, and so on.)- Returns:
Mapping of names to actual signature members.
- Return type:
dict of
str
to value-like or interface object or a potentially nested list of these
- class amaranth.lib.wiring.FlippedSignatureMembers(unflipped)
Mapping of signature member names to their descriptions, with the directions flipped.
Although an instance of
FlippedSignatureMembers
could be created directly, it will be usually created by a call toSignatureMembers.flip()
.This container is a wrapper around
SignatureMembers
that contains the same members as the inner mapping, but flips their data flow when they are accessed. For example:members = wiring.SignatureMembers({"foo": Out(1)}) flipped_members = members.flip() assert flipped_members["foo"].flow == In
This class implements the same methods, with the same functionality (other than the flipping of the data flow), as the
SignatureMembers
class; see the documentation for that class for details.- flip()
Flips this mapping back to the original one.
- Returns:
unflipped
- Return type:
- class amaranth.lib.wiring.Signature(members)
Description of an interface object.
An interface object is a Python object that has a
signature
attribute containing aSignature
object, as well as an attribute for every member of its signature. Signatures and interface objects are tightly linked: an interface object can be created out of a signature, and the signature is used whenconnect()
ing two interface objects together. See the introduction to interfaces for a more detailed explanation of why this is useful.Signature
can be used as a base class to define customized signatures and interface objects.Important
Signature
objects are immutable. Classes inheriting fromSignature
must ensure this remains the case when additional functionality is added.- flip()
Flip the data flow of the members in this signature.
- Returns:
Proxy object
FlippedSignature(self)
that flips the data flow of the attributes corresponding to the members that are accessed using it.See the documentation for the
FlippedSignature
class for a detailed discussion of how this proxy object works.- Return type:
- property members
Members in this signature.
- Return type:
- __eq__(other)
Compare this signature with another.
The behavior of this operator depends on the types of the arguments. If both
self
andother
are instances of the baseSignature
class, they are compared structurally (the result isself.members == other.members
); otherwise they are compared by identity (the result isself is other
).Subclasses of
Signature
are expected to override this method to take into account the specifics of the domain. If the subclass has additional properties that do not influence themembers
dictionary but nevertheless make its instance incompatible with other instances (for example, whether the feedback is combinational or registered), the overridden method must take that into account.- Return type:
- flatten(obj)
Recursively iterate through this signature, retrieving member values from an interface object.
Note
The paths returned by this method and by
SignatureMembers.flatten()
differ. This method yield several results for eachMember
in the collection that has a dimension:>>> sig = wiring.Signature({ ... "items": In(1).array(2) ... }) >>> obj = sig.create() >>> list(sig.flatten(obj)) [(('items', 0), In(1), (sig obj__items__0)), (('items', 1), In(1), (sig obj__items__1))]
The
SignatureMembers.flatten()
method yields one result for such a member; see the documentation for that method for an example.- Returns:
Tuples of paths, flow, and the corresponding member values. A path yielded by this method is a tuple of strings or integers where each item is an attribute name or index (correspondingly) using which the member value was retrieved.
- Return type:
iterator of (
tuple
ofstr
orint
,Flow
, value-like)
- is_compliant(obj, *, reasons=None, path=('obj',))
Check whether an object matches the description in this signature.
This module places few restrictions on what an interface object may be; it does not prescribe a specific base class or a specific way of constructing the object, only the values that its attributes should have. This method ensures consistency between the signature and the interface object, checking every aspect of the provided interface object for compliance with the signature.
It verifies that:
obj
has asignature
attribute whose value aSignature
instance such thatself == obj.signature
;for each member,
obj
has an attribute with the same name, whose value:for members with
dimensions
specified, contains a list or a tuple (or several levels of nested lists or tuples, for multiple dimensions) satisfying the requirements below;for port members, is a value-like object casting to a
Signal
or aConst
whose width and signedness is the same as that of the member, and (in case of aSignal
) which is not reset-less and whose reset value is that of the member;for signature members, matches the description in the signature as verified by
Signature.is_compliant()
.
If the verification fails, this method reports the reason(s) by filling the
reasons
container. These reasons are intended to be human-readable: more than one reason may be reported but only in cases where this is helpful (e.g. the same error message will not repeat 10 times for each of the 10 ports in a list).- Parameters:
- Returns:
True
ifobj
matches the description in this signature,False
otherwise. IfFalse
andreasons
was notNone
, it will contain a detailed explanation why.- Return type:
- create(*, path=None, src_loc_at=0)
Create an interface object from this signature.
The default
Signature.create()
implementation consists of one line:def create(self, *, path=None, src_loc_at=0): return PureInterface(self, path=path, src_loc_at=1 + src_loc_at)
This implementation creates an interface object from this signature that serves purely as a container for the attributes corresponding to the signature members, and implements no behavior. Such an implementation is sufficient for signatures created ad-hoc using the
Signature({ ... })
constructor as well as simple signature subclasses.When defining a
Signature
subclass that needs to customize the behavior of the created interface objects, override this method with a similar implementation that references the class of your custom interface object:class CustomSignature(wiring.Signature): def create(self, *, path=None, src_loc_at=0): return CustomInterface(self, path=path, src_loc_at=1 + src_loc_at) class CustomInterface(wiring.PureInterface): @property def my_property(self): ...
The
path
andsrc_loc_at
arguments are necessary to ensure the generated signals have informative names and accurate source location information.The custom
create()
method may take positional or keyword arguments in addition to the two listed above. Such arguments must have a default value, because theSignatureMembers.create()
method will call theSignature.create()
member without these additional arguments when this signature is a member of another signature.
- class amaranth.lib.wiring.FlippedSignature(unflipped)
Description of an interface object, with the members’ directions flipped.
Although an instance of
FlippedSignature
could be created directly, it will be usually created by a call toSignature.flip()
.This proxy is a wrapper around
Signature
that contains the same description as the inner mapping, but flips the members’ data flow when they are accessed. It is useful becauseSignature
objects are mutable and may include custom behavior, and if one was copied (rather than wrapped) bySignature.flip()
, the wrong object would be mutated, and custom behavior would be unavailable.For example:
sig = wiring.Signature({"foo": Out(1)}) flipped_sig = sig.flip() assert flipped_sig.members["foo"].flow == In sig.attr = 1 assert flipped_sig.attr == 1 flipped_sig.attr += 1 assert sig.attr == flipped_sig.attr == 2
This class implements the same methods, with the same functionality (other than the flipping of the members’ data flow), as the
Signature
class; see the documentation for that class for details.It is not possible to inherit from
FlippedSignature
andSignature.flip()
must not be overridden. If aSignature
subclass defines a method and this method is called on a flipped instance of the subclass, it receives the flipped instance as itsself
argument. To distinguish being called on the flipped instance from being called on the unflipped one, useisinstance(self, FlippedSignature)
:class SignatureKnowsWhenFlipped(wiring.Signature): @property def is_flipped(self): return isinstance(self, wiring.FlippedSignature) sig = SignatureKnowsWhenFlipped({}) assert sig.is_flipped == False assert sig.flip().is_flipped == True
- __getattr__(name)
Retrieves attribute or method
name
of the unflipped signature.Performs
getattr(unflipped, name)
, ensuring that, ifname
refers to a property getter or a method, itsself
argument receives the flipped signature. A class method’scls
argument receives the class of the unflipped signature, as usual.
- __setattr__(name, value)
Assigns attribute
name
of the unflipped signature tovalue
.Performs
setattr(unflipped, name, value)
, ensuring that, ifname
refers to a property setter, itsself
argument receives the flipped signature.
- __delattr__(name)
Removes attribute
name
of the unflipped signature.Performs
delattr(unflipped, name)
, ensuring that, ifname
refers to a property deleter, itsself
argument receives the flipped signature.
- class amaranth.lib.wiring.SignatureMeta
Metaclass for
Signature
that makesFlippedSignature
its ‘virtual subclass’.The object returned by
Signature.flip()
is an instance ofFlippedSignature
. It implements all of the methodsSignature
has, and for subclasses ofSignature
, it implements all of the methods defined on the subclass as well. This makes it effectively a subtype ofSignature
(or a derived class of it), but this relationship is not captured by the Python type system:FlippedSignature
only hasobject
as its base class.This metaclass extends
issubclass()
andisinstance()
so that they take into account the subtyping relationship betweenSignature
andFlippedSignature
, described below.- __subclasscheck__(subclass)
Override of
issubclass(cls, Signature)
.In addition to the standard behavior of
issubclass()
, this override makesFlippedSignature
a subclass ofSignature
or any of its subclasses.
- __instancecheck__(instance)
Override of
isinstance(obj, Signature)
.In addition to the standard behavior of
isinstance()
, this override makesisinstance(obj, cls)
act asisinstance(obj.flip(), cls)
whereobj
is an instance ofFlippedSignature
.
Interfaces
- class amaranth.lib.wiring.PureInterface(signature, *, path=None, src_loc_at=0)
A helper for constructing ad-hoc interfaces.
The
PureInterface
helper primarily exists to be used by the default implementation ofSignature.create()
, but it can also be used in any other context where an interface object needs to be created without the overhead of defining a class for it.Important
Any object can be an interface object; it only needs a
signature
property containing a compliant signature. It is not necessary to usePureInterface
in order to create an interface object, but it may be used either directly or as a base class whenever it is convenient to do so.- __init__(signature, *, path=None, src_loc_at=0)
Create attributes from a signature.
The sole method defined by this helper is its constructor, which only defines the
self.signature
attribute as well as the attributes created from the signature members:def __init__(self, signature, *, path): self.__dict__.update({ "signature": signature, **signature.members.create(path=path) })
Note
This implementation can be copied and reused in interface objects that do include custom behavior, if the signature serves as the source of truth for attributes corresponding to its members. Although it is less repetitive, this approach can confuse IDEs and type checkers.
- class amaranth.lib.wiring.FlippedInterface(unflipped)
An interface object, with its members’ directions flipped.
An instance of
FlippedInterface
should only be created by callingflipped()
, which ensures that aFlippedInterface(FlippedInterface(...))
object is never created.This proxy wraps any interface object and forwards attribute and method access to the wrapped interface object while flipping its signature and the values of any attributes corresponding to interface members. It is useful because interface objects may be mutable or include custom behavior, and explicitly keeping track of whether the interface object is flipped would be very burdensome.
For example:
intf = wiring.PureInterface(wiring.Signature({"foo": Out(1)}), path=()) flipped_intf = wiring.flipped(intf) assert flipped_intf.signature.members["foo"].flow == In intf.attr = 1 assert flipped_intf.attr == 1 flipped_intf.attr += 1 assert intf.attr == flipped_intf.attr == 2
It is not possible to inherit from
FlippedInterface
. If an interface object class defines a method or a property and it is called on the flipped interface object, the method receives the flipped interface object as itsself
argument. To distinguish being called on the flipped interface object from being called on the unflipped one, useisinstance(self, FlippedInterface)
:class InterfaceKnowsWhenFlipped: signature = wiring.Signature({}) @property def is_flipped(self): return isinstance(self, wiring.FlippedInterface) intf = InterfaceKnowsWhenFlipped() assert intf.is_flipped == False assert wiring.flipped(intf).is_flipped == True
- property signature
Signature of the flipped interface.
- Returns:
unflipped.signature.flip()
- Return type:
- __eq__(other)
Compare this flipped interface with another.
- Returns:
True
ifother
is an instanceFlippedInterface(other_unflipped)
whereunflipped == other_unflipped
,False
otherwise.- Return type:
- __getattr__(name)
Retrieves attribute or method
name
of the unflipped interface.Performs
getattr(unflipped, name)
, with the following caveats:If
name
refers to a signature member, the returned interface object is flipped.If
name
refers to a property getter or a method, itsself
argument receives the flipped interface. A class method’scls
argument receives the class of the unflipped interface, as usual.
- __setattr__(name, value)
Assigns attribute
name
of the unflipped interface tovalue
.Performs
setattr(unflipped, name, value)
, with the following caveats:If
name
refers to a signature member, the assigned interface object is flipped.If
name
refers to a property setter, itsself
argument receives the flipped interface.
- __delattr__(name)
Removes attribute
name
of the unflipped interface.Performs
delattr(unflipped, name)
, ensuring that, ifname
refers to a property deleter, itsself
argument receives the flipped interface.
- amaranth.lib.wiring.flipped(interface)
Flip the data flow of the members of the interface object
interface
.If an interface object is flipped twice, returns the original object:
flipped(flipped(interface)) is interface
. Otherwise, wrapsinterface
in aFlippedInterface
proxy object that flips the directions of its members.See the documentation for the
FlippedInterface
class for a detailed discussion of how this proxy object works.
Making connections
- exception amaranth.lib.wiring.ConnectionError
Exception raised when the
connect()
function is requested to perform an impossible, meaningless, or forbidden connection.
- amaranth.lib.wiring.connect(m, *args, **kwargs)
Connect interface objects to each other.
This function creates connections between ports of several interface objects. (Any number of interface objects may be provided; in most cases it is two.)
The connections can be made only if all of the objects satisfy a number of requirements:
Every interface object must have the same set of port members, and they must have the same
dimensions
.For each path, the port members of every interface object must have the same width and reset value (for port members corresponding to signals) or constant value (for port members corresponding to constants). Signedness may differ.
For each path, at most one interface object must have the corresponding port member be an output.
For a given path, if any of the interface objects has an input port member corresponding to a constant value, then the rest of the interface objects must have output port members corresponding to the same constant value.
For example, if
obj1
is being connected toobj2
andobj3
, andobj1.a.b
is an output, thenobj2.a.b
andobj2.a.b
must exist and be inputs. Ifobj2.c
is an input and its value isConst(1)
, thenobj1.c
andobj3.c
must be outputs whose value is alsoConst(1)
. If no ports besidesobj1.a.b
andobj1.c
exist, then no ports except for those two must exist onobj2
andobj3
either.Once it is determined that the interface objects can be connected, this function performs an equivalent of:
m.d.comb += [ in1.eq(out1), in2.eq(out1), ... ]
Where
out1
is an output andin1
,in2
, … are the inputs that have the same path. (If no interface object has an output for a given path, no connection at all is made.)The positions (within
args
) or names (withinkwargs
) of the arguments do not affect the connections that are made. There is no difference in behavior betweenconnect(m, a, b)
andconnect(m, b, a)
orconnect(m, arbiter=a, decoder=b)
. The names of the keyword arguments serve only a documentation purpose: they clarify the diagnostic messages when a connection cannot be made.
Components
- class amaranth.lib.wiring.Component(*args, src_loc_at=0, **kwargs)
Base class for elaboratable interface objects.
A component is an
Elaboratable
whose interaction with other parts of the design is defined by its signature. Most if not all elaboratables in idiomatic Amaranth code should be components, as the signature clarifies the direction of data flow at their boundary. See the introduction to interfaces section for a practical guide to defining and using components.There are two ways to define a component. If all instances of a component have the same signature, it can be defined using variable annotations:
class FixedComponent(wiring.Component): en: In(1) data: Out(8)
The variable annotations are collected by the constructor
Component.__init__()
. Only public (not starting with_
) annotations withIn
orOut
objects are considered; all other annotations are ignored under the assumption that they are interpreted by some other tool.It is possible to use inheritance to extend a component: the component’s signature is composed from the variable annotations in the class that is being constructed as well as all of its base classes. It is an error to have more than one variable annotation for the same attribute.
If different instances of a component may need to have different signatures, variable annotations cannot be used. In this case, the constructor should be overridden, and the computed signature members should be provided to the superclass constructor:
class ParametricComponent(wiring.Component): def __init__(self, data_width): super().__init__({ "en": In(1), "data": Out(data_width) })
It is also possible to pass a
Signature
instance to the superclass constructor.Aside from initializing the
signature
attribute, theComponent.__init__()
constructor creates attributes corresponding to all of the members defined in the signature. If an attribute with the same name as that of a member already exists, an error is raied.- Raises:
- property signature
The signature of the component.