-
Notifications
You must be signed in to change notification settings - Fork 10
1:N Socket
This page deals with one bus primitive that is a 1:N Socket for connecting a single master or host to multiple (N) devices.
Below is the visual representation of the 1:4 Socket:
Signal | Name | Direction | Description |
---|---|---|---|
tl_h_i | TL-UL Host Bundle | Input | The TL-UL host bundle which receives the request from the host. |
tl_h_o | TL-UL Device Bundle | Output | The TL-UL device bundle that gives the response of the device back to the output. |
dev_sel | Device select | Input | The input coming from the address decoder which tells which device to respond back to the host request. |
tl_d_o[n] | TL-UL Host Bundle | Output | N TL-UL host bundles going out from the socket to route the request from the host to the devices |
tl_d_i[n] | TL-UL Device Bundle | Input | N TL-UL device bundles receiving the response from the devices and routing them back to the host. |
class TLSocket1_N(N: Int)(implicit val conf: TLConfiguration) extends Module {
val io = IO(new Bundle {
val tl_h_i = Flipped(new TL_H2D)
val tl_h_o = new TL_D2H
val tl_d_o = Vec(N, new TL_H2D)
val tl_d_i = Flipped(Vec(N, new TL_D2H))
val dev_sel = Input(UInt(log2Ceil(N+1).W))
})
}
According to the diagram above our 1:N socket has an input port of TL-UL host bundle tl_h_i
which takes all the Channel A inputs from the Host connected outside this socket. Since the bundle class TL_H2D
has wires created as Output
we need to flip them to convert them into Input
ports using the Flipped()
.
The socket also has an output bundle tl_h_o
which has all the Channel D wires that contain the response from one of the N devices connected with the socket. This response is outputted and received by the Host outside the socket. Here we don't use the Flipped
while creating the bundle val tl_h_o = new TL_D2H
because the bundle class TL_D2H
already has wires created as Output
ports.
The socket also has N output bundles tl_d_o
of type TL_H2D
which carry the Channel A signals received from the host input and route them to the output of this socket so that devices can receive the request from the host. The Vec
class is used to create N bundles of tl_d_o
so that N devices outside the socket can receive the request from the host.
The socket has N input bundle tl_d_i
of type TL_D2H
which carry the Channel D signals produced by the devices back to the host. The N devices outside the socket will receive the request from the host through the tl_d_o
ports, process the request and then respond back to the tl_d_i
ports of the socket. These bundles will all be Input
ports of the socket hence we used Flipped
to initialise them.
We have a dev_sel
input coming in from the address decoder block. This device select signal dev_sel
tells the logic inside the 1:N socket to route which device's data back to the host port tl_h_o
. It's width is log2Ceil(N+1)
bits. Which means if we have 4 devices then log2Ceil(5) = 3
. Since log2Ceil(4) = 2
, the log2Ceil(5)
will give an output > 2 but in floating point which will then be rounded up to 3 . This is what the function log2Ceil
does. We did this to ensure the dev_sel
can have bits available to identify an address error. This will get more clearer when we look at the code. If the host provides an address which the 1:N socket cannot route to any devices, then the address decoder generates dev_sel = N
or in this case dev_sel = 4
. Since devices will be identified as tl_d_o(0) ... tl_d_o(3)
the dev_sel = 4
will indicate to route the request of host to the error responder module which will generate an error response and route it back to the host. Again if this isn't clear right now, we will discuss it later.
val tl_err_h_o = Wire(new TL_H2D)
val tl_err_d_i = Wire(Flipped(new TL_D2H))
First we create two inner wire bundles that are not ports of the module but created inside the module. These bundles are used to route the request from the host to the error responder module rather than to the tl_d_o
ports in case of an error in addressing indicated by the dev_sel
signal.
The tl_err_h_o
is an TL_H2D
bundle with default wiring configuration as Output
. This bundle of wires take the input from the tl_h_i
port and will connect it with the error responder module.
The tl_err_d_i
is an TL_D2H
bundle with wiring configuration as Input
due to Flipped(new TL_D2H)
. This bundle of wires receive the response from the error responder module and connects it with the tl_h_o
port.
Refer to the diagram above to get a better idea.
io.tl_h_o <> tl_err_d_i
Connecting the response of the 1:N Socket by default with the error bundle from the Error Response module. This would be connected in future with the correct device according to the dev_sel
. If dev_sel
does not match any device then it will remain connected with this error bundle. This is shown above by the bulk connection like this io.tl_h_o <> tl_err_d_i
. This connects all the wires from the tl_err_d_i
bundle with all the wires of the tl_h_o
bundle.
for(i <- 0 until N) {
io.tl_d_o(i).a_valid := io.tl_h_i.a_valid && (io.dev_sel === i.asUInt)
io.tl_d_o(i).a_opcode := io.tl_h_i.a_opcode
io.tl_d_o(i).a_param := io.tl_h_i.a_param
io.tl_d_o(i).a_size := io.tl_h_i.a_size
io.tl_d_o(i).a_source := io.tl_h_i.a_source
io.tl_d_o(i).a_address := io.tl_h_i.a_address
io.tl_d_o(i).a_mask := io.tl_h_i.a_mask
io.tl_d_o(i).a_data := io.tl_h_i.a_data
io.tl_d_o(i).d_ready := io.tl_h_i.d_ready
}
Then we do a for loop to connect the tl_d_o
output ports with the tl_h_i
port. The request received from the host on the tl_h_i
port is routed to every device connected with the socket through the tl_d_o
ports.
Note: The until
in the for loop runs the iteration from 0 to N-1. So that we correctly address all the device ports from tl_d_o(0) ... tl_d_o(3)
The logic for setting the a_valid
as io.tl_h_i.a_valid && (io.dev_sel === i.asUInt)
to ensure only the device we need to address will get the a_valid = true
. We check if dev_sel === i
which will then set a_valid
of that device to be true.
for(id <- 0 until N) {
when(io.dev_sel === id.asUInt) {
io.tl_h_o.a_ready := io.tl_h_i.a_valid && io.tl_d_i(id).a_ready
io.tl_h_o.d_valid := io.tl_d_i(id).d_valid
io.tl_h_o.d_opcode := io.tl_d_i(id).d_opcode
io.tl_h_o.d_param := io.tl_d_i(id).d_param
io.tl_h_o.d_size := io.tl_d_i(id).d_size
io.tl_h_o.d_source := io.tl_d_i(id).d_source
io.tl_h_o.d_sink := io.tl_d_i(id).d_sink
io.tl_h_o.d_data := io.tl_d_i(id).d_data
io.tl_h_o.d_error := io.tl_d_i(id).d_error
}
}
Now for routing the response from the tl_d_i
ports back to the tl_h_o
port, we need to route the response from any one device whose id
matches with the dev_sel
signal. So here we again loop from 0 to N-1 and see if the dev_sel
matches with any device port. If it doesn't it means the address is not routable by the 1:N socket and it will be by default connected with the error bundle as shown in the previous code explanation above.
Since we have wired all the device ports tl_d_o[n]
and the error responder port tl_err_d_i
with the host port tl_h_i
what remains is connecting the tl_h_i
port wires with Tl_H2D
bundle tl_err_h_o
which will then send the request from the host to the error responder module.
tl_err_h_o.a_valid := io.tl_h_i.a_valid && (io.dev_sel === N.asUInt)
tl_err_h_o.a_opcode := io.tl_h_i.a_opcode
tl_err_h_o.a_param := io.tl_h_i.a_param
tl_err_h_o.a_size := io.tl_h_i.a_size
tl_err_h_o.a_source := io.tl_h_i.a_source
tl_err_h_o.a_address := io.tl_h_i.a_address
tl_err_h_o.a_mask := io.tl_h_i.a_mask
tl_err_h_o.a_data := io.tl_h_i.a_data
tl_err_h_o.d_ready := io.tl_h_i.d_ready
Note: The valid signal is only active when the host has send a valid request tl_h_i.a_valid == true
and if the dev_sel == N
and does not match any existing device port.
val tl_errResp = Module(new TL_ErrResp)
tl_errResp.io.tl_h_i <> tl_err_h_o
tl_err_d_i <> tl_errResp.io.tl_d_o
Finally now we create the Error Responder module tl_errResp
and wire it's input bundle tl_errResp.io.tl_h_i
with the tl_err_h_o
bundle that we wired above and then we wire the output of tl_errResp
module with the tl_err_d_i
bundle which is then connected with the tl_h_o
of the socket as we saw above.
This module is used to drive the error response back to the host connected with the 1:N socket.
class TL_ErrResp(implicit val conf: TLConfiguration) extends Module {
val io = IO(new Bundle {
val tl_h_i = Flipped(new TL_H2D)
val tl_d_o = new TL_D2H
})
As we know that we are going to receive an input bundle of type TL_H2D
from the tl_err_h_o
bundle so we create a Flipped(new TL_H2D)
bundle named tl_h_i
to configure the bundle wires as Input
rather than Output
. Further we also know that this module will send a response so we create a TL_D2H
bundle named tl_d_o
which is by default configured as Output
.
val err_opcode = RegInit(TL_A_Opcode.get)
val err_source = RegInit(0.U)
val err_size = RegInit(0.U)
val err_reqPending = RegInit(false.B)
val err_rspPending = RegInit(false.B)
Here we create some initial registers of opcode, source, size, reqPending and rspPending. Upon reset
the err_opcode
will have the default value of Get
. The err_source
will be 0, the err_size
will be 0, the err_reqPending
will be false and the err_rspPending
will be false as well.
when(io.tl_h_i.a_valid && io.tl_d_o.a_ready) {
err_reqPending := true.B
err_source := io.tl_h_i.a_source
err_opcode := io.tl_h_i.a_opcode
err_size := io.tl_h_i.a_size
} .elsewhen(!err_rspPending) {
err_reqPending := false.B
}
when((err_reqPending || err_rspPending) && !io.tl_h_i.d_ready) {
err_rspPending := true.B
} .otherwise {
err_rspPending := false.B
}
io.tl_d_o.a_ready := ~err_rspPending & ~(err_reqPending & ~io.tl_h_i.d_ready)
io.tl_d_o.d_valid := err_reqPending || err_rspPending
io.tl_d_o.d_data := Fill(conf.TL_DW/4, "hf".U) //Return all F. If dw = 32 then 32/4 = 8 characters all 0xf
io.tl_d_o.d_source := err_source
io.tl_d_o.d_sink := 0.U
io.tl_d_o.d_param := 0.U
io.tl_d_o.d_size := err_size
io.tl_d_o.d_opcode := Mux(err_opcode === TL_A_Opcode.get, TL_D_Opcode.accessAckData, TL_D_Opcode.accessAck)
io.tl_d_o.d_error := true.B
Let's first analyse the flow of control signals which control the Error Responder module's response (tl_d_o.d_valid
, tl_d_o.a_ready
). These control signals are tl_h_i.a_valid
, tl_h_i.d_ready
, tl_d_o.a_ready
, err_reqPending
, err_rspPending
. The best way to understand the logic above is by viewing the timing diagram shown below:
The host sends a valid request tl_h_i.a_valid = true
and sets it's ready tl_h_i.d_ready = true
to indicate it is ready to accept the response. Error Responder's ready is set tl_d_o.a_ready = true
which indicates that it is ready to accept the host's request. The d_error
is by default set to true since host's request is only connected with the Error Responder if the address accessed does not exist. The d_error
is provided by the Error Responder module when d_valid
is set. As it can be seen above tl_d_o.d_valid
depends upon err_reqPending
or err_rspPending
. First when
block gets executed and the err_reqPending
register is set which updates it's value in the next clock cycle.
The err_reqPending
register's value gets updated to high and consequently the d_valid
gets set as well which means in this clock cycle the host receives the error response.
The host then lowers it's valid and ready signals which means it will not accept any more responses from the Error Responder. The above logic then sets the err_reqPending
to low which will be updated in the next clock cycle. The above logic also sets a_ready
to low.
The err_reqPending
register's value gets updated to low and consequently the d_valid
gets low as well. The logic sets a_ready
to high so that the Error Responder is able to accept any other invalid host request and respond to it with an error.