Skip to content

How to Read the Router Example Continued

Chick Markley edited this page Jun 9, 2017 · 2 revisions

Here comes the DUT

 43  /**
 44    * routes packets by using their header as an index into an externally loaded and readable table,
 45    * The number of addresses recognized does not need to match the number of outputs
 46    */
 47  class Router extends Module {

As before we are creating a Scala class that in this case inherits from the Chisel Module class. Modules are generators of hardware descriptions, analogous to the Modules of Verilog etc. Modules should define IO ports, reset and clock behavior, and the circuit implementation.

 48    val depth = Router.routeTableSize
 49    val n     = Router.numberOfOutputs
 50    val io    = IO(new RouterIO(n))
 51    val tbl   = Mem(depth, UInt(BigInt(n).bitLength.W))

These first few lines define some variables that are really little more than alias for the more verbose parameters that are contained in the Router object. This is one way of passing in parameters, we have seen the alternative in RouterIO in which a parameter is passed as an argument to the class creation. We shall see this below. The way parameters are passed into module is somewhat a matter of taste. In general larger and more complex systems like Rocket have powerful and complex parameterization schemes, with implicit defaults and notation for local overrides.

 53    when(reset) {
 54      io.read_routing_table_request.nodeq()
 55      io.load_routing_table_request.nodeq()
 56      io.read_routing_table_response.noenq()
 57      io.in.nodeq()
 58      io.outs.foreach { out => out.noenq() }
 59    }

Starting with when(reset), when is the hardware equivalent of an if statement. The contents of when's code block (the stuff in the braces) will be generated with any connections to hardware components outside the when block gated with the necessary muxes based on, in this case, reset, in this case it is the Bool signal reset. reset is an implicit IO Bool input port, that is provided by the Module (There are other types of Module that can be used that do not provide these ports automatically)

 66    .elsewhen(io.load_routing_table_request.valid) {
 67      val cmd = io.load_routing_table_request.deq()
 68      tbl(cmd.addr) := cmd.data
 69      printf("setting tbl(%d) to %d\n", cmd.addr, cmd.data)
 70    }

The elsewhen here is pretty much like an else if, the code blocks connections will be gated by muxes based on the condition. In this case that condition is when the load_routing_table.valid signal is asserted.
val cmd = io.load_routing_table_request.deq() creates a local reference to the return value of deq(). Dequeue is a simple convenience method on a decoupled interface that sets the ready, and returns a reference to the data portion of the decouple interface. tbl(cmd.addr) := cmd.data sets the memory at the specified address cmd.addr to the specified output cmd.data. Because this is in an elsewhen block that connection will only happen when the condition is true. There is another printf here, probably used by the developer during the debugging process.

A note on the dot in front of elsewhen. The when operator is implemented a via companion object when that has an apply method that requires Bool parameter and a code block as its argument. That apply method returns an an instance of a class WhenContext. The WhenContext instance has a method elsewhen which, like when, requires a Bool and a code block. At FIRRTL generation time these additional blocks are emitted in a way that they are dependent upon the negation of all preceding connected when and elsewhen blocks. Dots are not required in front of methods when they follow on the same line, but for style reasons this developer chose to put the elsewhen on it's own line. Thus the dot in front of the elsewhen was necessary for the scala compiler to recognize it as a method.

 71    .elsewhen(io.in.valid) {
 72      val pkt = io.in.bits
 73      val idx = tbl(pkt.header(log2Ceil(Router.routeTableSize), 0))
 74      when(io.outs(idx).ready) {
 75        io.in.deq()
 76        io.outs(idx).enq(pkt)
 77        printf("got packet to route header %d, data %d, being routed to out(%d)\n", pkt.header, pkt.body, tbl(pkt.header))
 78      }
 79    }
 80  }

The final .elsewhen is gated by io.in.valid. The val pkt = io.in.bits gets a reference to the data in the incoming packet. It does not used the deq() method here, because we don't want to assert the ready on the input until we the output associated with the packet header is ready to be enqueued. val idx = tbl(pkt.header(log2Ceil(Router.routeTableSize), 0)) figures out the index (idx) that is associated with the packet header according to the current routing table. The tbl(pkt.header(log2Ceil(Router.routeTableSize), 0)) is doing a couple of notable things. tbl (the routing table) is being indexed by the packet header, the parenthesized argument to header is a specification of bits of the header to use as the index, the argument to the implicit apply method on pkt.header is (high start bit, low end bit) inclusive. The log2Ceil computes the high from the number of bits necessary to accommodate the parameterized size of the routing table.

Once the index is computed, a when(io.outs(idx).ready) { checks the ready bit on that output port and if so, io.in.deq() assert the ready on the input port indicating the value has packet had been processed and io.outs(idx).enq(pkt) puts the packet on the selected input and asserts valid on that port.