-
Notifications
You must be signed in to change notification settings - Fork 10
M:1 Socket
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:
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. |
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.
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.
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)-1
th 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_valid
s 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
}
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.
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:
-
req_i: This is the input for taking all the valid signals from the hosts using the
hRequest
bundle. -
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. - 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.