Skip to content

1:N Socket

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

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:

Ports of the 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.

Creating the IO of the module

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.

Creating the logic of the module

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_owhich 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.

Error Responder (TL_ErrResp)

This module is used to drive the error response back to the host connected with the 1:N socket.

Creating the IO of the module

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.

Creating the logic of the module

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:

timing diagram

Clock Cycle: 1

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.

Clock Cycle: 2

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.

Clock Cycle: 3

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.

Clock Cycle: 4

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.