lkolbly / ripstop

Apache License 2.0
0 stars 0 forks source link

Ports #11

Open lkolbly opened 2 years ago

lkolbly commented 2 years ago

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)

In code this looks something like:

module null_sink(bit tvalid, bits<32> data) -> (bit tready) {
   ...
}

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]
    }
}

Or as an output:

module counter() -> (axi_stream result) {
    bits<64> counter;
    if result.tready[t] {
        counter[t] = counter[t-1] + 1;
        result.tvalid[t] = 1;
        result.tdata[t] = counter[t];
    } else {
        result.tvalid[t] = 0;
    }
}

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.