Skip to content

M:1 Socket

Muhammad Hadir Khan edited this page Aug 31, 2020 · 6 revisions

This page deals with the M:1 Socket implementation. This bus primitive is used to connect M hosts with a single device.

The 4:1 socket is shown below:

Ports of the socket

Signal Name Direction Description
tl_h_i[n] TL-UL Host Bundle Input M TL-UL host bundles which receives the request from M hosts.
tl_h_o[n] TL-UL Device Bundle Output M TL-UL device bundles that connects the response of the device back to the M hosts.
tl_d_o TL-UL Host Bundle Output TL-UL host bundle going out from the socket to route the request from the hosts to the device.
tl_d_i TL-UL Device Bundle Input TL-UL device bundle receiving the response from the device and routing it back to the hosts.

Creating the IO of the module

class TLSocketM_1(M: Int)(implicit val conf:TLConfiguration) extends Module {
  val io = IO(new Bundle {
    val tl_h_i = Vec(M, Flipped(new TL_H2D))
    val tl_h_o = Vec(M, new TL_D2H)
    val tl_d_o = new TL_H2D
    val tl_d_i = Flipped(new TL_D2H)
  })

As shown in the diagram above this socket takes the requests from multiple hosts and the number of hosts are identified by the parameter M, so we create a Chisel3.Vec of bundles tl_h_i that are of type TL_H2D and are Flipped due to being used as inputs to the socket from the M hosts. Similarly the socket will also provide the response from the device to any one the requesting M hosts so we need to have bundles tl_h_o of type TL_D2H connected on the outputs for each host to receive the response from the device. Further, we also create a bundle tl_d_o of type TL_H2D for sending the request of any one host at the output of the socket which will then be received by the attached device and we create a bundle tl_d_i of type TL_D2H that takes the response from the device as input to the module and hence use the Flipped construct.

Declaring the wires inside the module

val STIDW = log2Ceil(M) // the bits required for identifying hosts.
val tl_h2d = Wire(Vec(M, Flipped(new TL_H2D))) // create an intermediate bundles of wire for capturing the host request
val hRequest = Wire(Vec(M, Bool())) // this bundle of wires is used to have the valid signals of all hosts.
val respReady = Wire(Vec(M, Bool()))
val respValid = Wire(Vec(M, Bool()))

The STIDW is used ahead for defining the bit width required for identifying the hosts connected with the socket. This is found out by the function log2Ceil(M) which gives us the bits required for identifying hosts. For e.g if we have 4 hosts connected then log2Ceil(4) = 2 which means 2 bits required for identifying 4 hosts, host1 = 'b00 host2 = 'b01 host3 = 'b10 host4 = 'b11.

The tl_h2d wires bundles are used to carry the request from all the M hosts and will pass forward the values to the arbiter with some modifications that will be covered later in the code.

The hRequest wires are used to carry the a_valid request signals from all the hosts. These will be then provided to the Arbiter module indicating which hosts want to send a request to the device.

The respReady wires are used to indicate the device that out of M hosts any one is ready to accept the response from the device.

The respValid wires are used to indicate the host that the data being sent by the device is valid.

The usage of these wires will be more clearer when we advance forward and look at the logic of the module.

Creating the logic of the module

for(i <- 0 until M) {
    val reqid_sub = Wire(UInt(STIDW.W))
    val shifted_id = Wire(UInt(conf.TL_AIW.W))

    reqid_sub := i.asUInt // this will be used to identify hosts connected with the socket
    shifted_id := Cat(io.tl_h_i(i).a_source((conf.TL_AIW-STIDW)-1,0), reqid_sub)
    tl_h2d(i).a_valid := io.tl_h_i(i).a_valid
    tl_h2d(i).a_opcode := io.tl_h_i(i).a_opcode
    tl_h2d(i).a_param := io.tl_h_i(i).a_param
    tl_h2d(i).a_size := io.tl_h_i(i).a_size
    tl_h2d(i).a_source := shifted_id
    tl_h2d(i).a_address := io.tl_h_i(i).a_address
    tl_h2d(i).a_mask := io.tl_h_i(i).a_mask
    tl_h2d(i).a_data := io.tl_h_i(i).a_data
    tl_h2d(i).d_ready := io.tl_h_i(i).d_ready
  }

This for loop is used to provide value to the tl_h2d bundle which takes the inputs from all the connected hosts. There is just one property that is not directly passed from the host requests and that is a_source, we rather calculate our shifted_id and provide it as a_source. This is for a good reason. Let's analyse it:

We are using the hosts and devices without the queues after their input ports, which means the host can only initiate a single request at a time and wait for the device to acknowledge it before sending another request. Though TileLink does not restrict the host with this functionality and therefore the a_source property exists. The host can use the a_source to attach tags to different requests being sent before waiting for the response from the device to come in. So for the first transaction host can use a_source = 0 and rather than waiting for the response from the device it can send another request with a_source = 1. These requests then get stored in the queue of the device. The device can recognise that it has to respond to the two requests from the host. But since we are not using the queue we do not support multiple transactions. So, by default every request the host initiate will have a_source = 0. Using this concept let's consider the situation we have with the 4:1 socket shown above in the diagram. If host(0) and host(1) wants to communicate with the device, both of them would have a_source = 0. This means the response routing from the slave would be ambiguous as to which host to respond to, since host(0) and host(1) both have a_source = 0. For this purpose we shift the a_source value to the MSB and prepend our own id on the LSB of a_source. These LSB bits are then used to steer the response back to the correct host.

Now coming back to the for loop, in the first statement we declare a wire reqid_sub which has bit width equal to STIDW since this wire would be used to have the id of the hosts.

In the second line we declare a wire shifted_id which has bit width equal to the TL_AIW property. This property defines the bit width for the a_source property and since we will replace the a_source with our custom shifted_id we keep the bit width same. The TL_AIW property is set to have value of 4. Which means we can use 4 bits for the a_source property.

Next we assign the id of each host to the reqid_sub wire. This will assign id 0 to the first host, id 1 to the second host and so on.

Now in the next statement we actually calculate the value for our shifted_id wire. As discussed above we will place the a_source value bits in the MSB and the reqid_sub to identify the hosts in the LSB of the shifted_id. So we extract the bits from the incoming host's a_source property as follow io.tl_h_i(i).a_source((conf.TL_AIW-STIDW)-1,0). We start extracting from the 0th bit and up till the (TL_AIW - STIDW)-1th bit. We know TL_AIW = 4 and if we have 4 hosts then STIDW = 2 then it means we extract from 0 to 1 bit io.tl_h_i(i).a_source(1,0). Then we prepend the LSB bits with the reqid_sub, this is done inside the Cat() utility provided by Chisel. Let's assume we have 2 hosts then STIDW = 1 which means we extract from 0th bit to 2nd bit io.tl_h_i(i).a_source(2,0) and then append 1 bit of reqid_sub at the LSB which makes sense since we only need 1 bit to identify two hosts.

for(i <- 0 until M) {
    hRequest(i) := tl_h2d(i).a_valid
  }

We then wire all the a_valids of the host requests coming in with the hRequest bundle.

for(i <- 0 until M) {
    respValid(i) := io.tl_d_i.d_valid && (io.tl_d_i.d_source(STIDW-1,0) === i.asUInt)
    respReady(i) := tl_h2d(i).d_ready && (io.tl_d_i.d_source(STIDW-1,0) === i.asUInt) && io.tl_d_i.d_valid
  }

respValid

As told in the beginning the respValid wires are used to tell the host that the response coming from the device is valid or not. For this purpose we first check if the device is sending a valid response from the device input port tl_d_i and then we see if the LSB bits of d_source i.e in case of 4 hosts d_source(STIDW-1,0) = d_source(1,0) which contains the reqid_sub field matches with the actual host to respond to which if true, sets the respValid of that host to true.

respReady

The respReady wires are used to indicate the device that out of M hosts any one is ready to accept the response from it. We first check if the host is ready to receive the response by checking the tl_h2d bundle's d_ready signals which takes the requests from all the hosts attached with the M:1 socket and then compare the reqid_sub by extracting it from d_source of the device and see if it matches with the actual host to respond to and then also check if the incoming response from the device is valid as well, if all of this is true then we set the respReady of that host to true.

Talking of Arbiter let's discuss it briefly. We will just assume it's internal working for now and analyse it later when we are done with understanding this M:1 socket first.

Here is the brief overview diagram of the Arbiter and it's connection with the hosts and device. There are certain signals not explicitly shown in the diagram, just an overview is provided. The sole purpose of Arbiter is to grant any single host the access to the device if multiple hosts wants to communicate with the device in the same cycle.

val arb = Module(new Arbiter(M))
arb.io.req_i := hRequest
arb.io.ready_i := io.tl_d_i.a_ready
arb.io.data_i <> tl_h2d

Here is the Arbiter module instantiated. It takes three inputs:

  1. req_i: This is the input for taking all the valid signals from the hosts using the hRequest bundle.
  2. ready_i: This is the input taken from the device to see if it is ready or not using the tl_d_i.a_ready signal.
  3. data_i: These are the Channel A data bundles taken from all the hosts in order to forward any one of them to the device as shown in the figure above.
io.tl_d_o.a_valid := arb.io.valid_o
io.tl_d_o.a_opcode := arb.io.data_o.a_opcode
io.tl_d_o.a_param := arb.io.data_o.a_param
io.tl_d_o.a_size := arb.io.data_o.a_size
io.tl_d_o.a_source := arb.io.data_o.a_source
io.tl_d_o.a_address := arb.io.data_o.a_address
io.tl_d_o.a_mask := arb.io.data_o.a_mask
io.tl_d_o.a_data := arb.io.data_o.a_data
io.tl_d_o.d_ready := respReady.contains(true.B)

After providing the inputs to the Arbiter we assume it works and handles all the scenarios of multiple masters accessing the device at the same time successfully and generates some outputs. These outputs are then used to connect with the device as show in the diagram above as well. All the fields of the device port are provided by the Arbiter excluding the d_ready. As discussed above respReady is used to indicate the device that any one of the host is ready to accept the response. So we connect the tl_d_o.d_ready with respReady.contains(true.B) which returns true if there is any wire inside the bundle respReady that is set to true, indicating that any one host is ready to accept the data from the device.

for(i <- 0 until M) {
    io.tl_h_o(i).d_valid := respValid(i)
    io.tl_h_o(i).d_opcode := io.tl_d_i.d_opcode
    io.tl_h_o(i).d_param := io.tl_d_i.d_param
    io.tl_h_o(i).d_size := io.tl_d_i.d_size
    io.tl_h_o(i).d_source := Cat(0.U,io.tl_d_i.d_source(conf.TL_AIW-1,STIDW)) // making sure we pass the same a_source in d_source.
    io.tl_h_o(i).d_sink := io.tl_d_i.d_sink
    io.tl_h_o(i).d_data := io.tl_d_i.d_data
    io.tl_h_o(i).d_error := io.tl_d_i.d_error
    io.tl_h_o(i).a_ready := arb.io.gnt_o(i)
  }

Finally, we connect the response from the device with all the hosts by looping over them. The valid signal d_valid will only be high for the one whose a_source was forwarded by the Arbiter to the device which used it to return back the d_source which is then used in the logic of setting the respValid as discussed above.

Note: We do not simply pass the d_source received from the device as is to the output since it would contain those reqid_sub bits in the LSB. So we extract those bits and pass in the remaining d_source bits to the output for the host to be received.


This ends the description for the M:1 Socket. It has been pretty long so the documentation for the Arbiter has been moved to it's own separate page here.

Clone this wiki locally