Sometimes a module might have a variety of parameters that are semantically related but are also a mix of inputs and outputs. For example, an AXI stream port has three parameters: tready, tvalid, and tdata. The second two are from producer to consumer, and the first is from consumer to producer. (data is transferred when tready && tvalid)
which is okay, but when you get multiple such ports on a single module, it starts to become unwieldy:
module floating_point_mult(bit a_tvalid, bits<64> a_tdata, bit b_tvalid, bits<64> b_tdata, bit result_tready) ->
(bit a_tready, bit b_tready, result_tvalid, bits<64> result_tdata);
Not illegible per se, but also not great.
Many languages have a notion of a "struct," which is a combination of individual variables. We should have structs, but also (more pertinent for this issue) we should have ports, which is a combination of individual signals and their direction:
port axi_stream {
input bit tvalid;
output bit tready;
input bits<64> tdata;
}
A port can be used as either an input:
module my_sink(axi_stream a) -> () {
a.tready[t] = 1; // We were born ready
if a.tvalid[t] && a.tready[t] {
// Do something with a.tdata[t]
}
}
Notice how the "input"-ness of each variable has changed: Instead of reading from tdata and tvalid, and writing to tvalid, we write to tdata and tvalid and read from tready. The compiler does this automatically for the user. While a user could define a port however they wish, and it will still work, we will use the convention that the port definition is from the perspective of the "receiver" of the port.
Additionally, ports will be able to be connected to other ports easily, allowing easy plumbing:
module mario() -> () {
instantiate counter as src;
instantiate my_sink as sink;
my_sink.a = counter.result;
}
which both type checks that the ports are the same type and does all the necessary reversing required.
Note that this assignment doesn't have a timespec associated with it. It's not immediately obvious what it would mean to register a port - in the axi_stream example, if we treated my_sink.a[t] = counter.result[t-1] naively by adding registers on every assignment, it would very much break the protocol. So we don't have timespecs in this assignment.
Sometimes a module might have a variety of parameters that are semantically related but are also a mix of inputs and outputs. For example, an AXI stream port has three parameters:
tready
,tvalid
, andtdata
. The second two are from producer to consumer, and the first is from consumer to producer. (data is transferred whentready && tvalid
)In code this looks something like:
which is okay, but when you get multiple such ports on a single module, it starts to become unwieldy:
Not illegible per se, but also not great.
Many languages have a notion of a "struct," which is a combination of individual variables. We should have structs, but also (more pertinent for this issue) we should have ports, which is a combination of individual signals and their direction:
A port can be used as either an input:
Or as an output:
Notice how the "input"-ness of each variable has changed: Instead of reading from
tdata
andtvalid
, and writing totvalid
, we write totdata
andtvalid
and read fromtready
. The compiler does this automatically for the user. While a user could define a port however they wish, and it will still work, we will use the convention that the port definition is from the perspective of the "receiver" of the port.Additionally, ports will be able to be connected to other ports easily, allowing easy plumbing:
which both type checks that the ports are the same type and does all the necessary reversing required.
Note that this assignment doesn't have a timespec associated with it. It's not immediately obvious what it would mean to register a port - in the
axi_stream
example, if we treatedmy_sink.a[t] = counter.result[t-1]
naively by adding registers on every assignment, it would very much break the protocol. So we don't have timespecs in this assignment.