Bus Specific Implementation

This guide will cover the bus specific implementations that needs to be done by taking the already implemented Wishbone protocol as an example.

Each bus specific protocol should have a Host Adapter and a Device Adapter. The host behaves like the master of the bus while the device acts as the slave of the bus.

In the case of Wishbone implementation, it also has a WishboneHost adapter and a WishboneDevice adapter.

Furthermore, each protocol should have a Master Bundle that is concerned with the Host Adapter and a Slave Bundle that is concerned with the Device Adapter.

These bundles and adapters can then be parameterized with a common Config class that is passed down to each module.

Wishbone Example

This example will provide a brief overview regarding how the implementation of Wishbone protocol is done and might help to think about implementing other protocols in a similar manner.

Creating the configuration class

The WishboneConfig class is created here: main/scala/caravan/bus/wishbone/WishboneConfig.scala

case class WishboneConfig
(
/**
* addressWidth: the address width in bits
* dataWidth: the data width in bits
* granularity: the minimal data transfer size over the bus
* waitState: whether the host can produce wait states during the bus transfer cycle
*/
addressWidth: Int,
dataWidth: Int,
granularity: Int = 8,
waitState: Boolean = false
) extends BusConfig

This config class extends the BusConfig trait that is declared here: main/scala/caravan/bus/common/Transaction.scala

This class is then implicitly passed down to the different wishbone module in order to parameterize them.

Creating the master and slave bundles

As discussed above, each bus protocol should have a master bundle that is concerned with the host adapter and a slave bundle that is concerned with the device adapter. These bundles are defined in the following file: main/scala/caravan/bus/wishbone/WishboneBus.scala

class WishboneMaster(implicit val config: WishboneConfig) extends WBHost {
    /**
    * cyc ->  indicates that a valid bus cycle is in progress
    * stb ->  indicates a valid data transfer cycle
    * we ->  indicates whether the current bus cycle is a READ or WRITE cycle
    * adr ->  carries the address for the current bus cycle
    * dat ->  contains the data output from the master
    * sel -> the sel output which indicates where valid data lane is expected on the dat_i for READs or dat_o for WRITEs
    * */
    val cyc        = Bool()
    val stb        = Bool()
    val we         = Bool()
    val adr        = UInt(config.addressWidth.W)
    val dat        = UInt(config.dataWidth.W)
    val sel        = UInt((config.dataWidth/config.granularity).W)
}

class WishboneSlave(implicit val config: WishboneConfig) extends WBDevice {
    /**
    * ack ->  indicates a normal termination of bus cycle
    * dat ->  contains the data output from the slave
    * err ->  indicates an error termination of bus cycle
    */
    val ack = Bool()
    val dat = UInt(config.dataWidth.W)
    val err = Bool()
}

Note that these classes do not extend the Bundle type. This is due to the fact that WBHost and WBDevice themselves extends BusHost and BusDevice which finally extends the Bundle type.

The BusHost and BusDevice classes are defined here: main/scala/caravan/bus/common/Transaction.scala

class BusHost extends Bundle
class BusDevice extends Bundle

The WBHost and WBDevice then extend them in the Wishbone specific implementation file: main/scala/caravan/bus/wishbone/WishboneBus.scala.

case class WBHost() extends BusHost
case class WBDevice() extends BusDevice

The reason for creating this hierarchy is to use type parameterization effectively. One example of this is found in the implementation of the 1:N Switch present in main/scala/caravan/bus/common/Switch1ToN.scala

Creating the host and device adapters

Once the protocol specific master and slave bundles are ready, the adapters can be created.

Here is how the two adapters look next to each other:

../_images/wishbone-WB_Host.png

The WishboneHost adapter is created here: main/scala/caravan/bus/wishbone/WishboneHost.scala

class WishboneHost(implicit val config: WishboneConfig) extends HostAdapter {
    val io = IO(new Bundle {
        val wbMasterTransmitter = Decoupled(new WishboneMaster())
        val wbSlaveReceiver  = Flipped(Decoupled(new WishboneSlave()))
        val reqIn = Flipped(Decoupled(new Request()))
        val rspOut = Decoupled(new Response())
    })

    // protocol specific logic here
}

The host is not extended with Module rather it is extended by HostAdapter which is an abstract class that itself extends Module. This also follows for the WishboneDevice which extends DeviceAdapter rather than Module. These abstract classes are present here: main/scala/caravan/bus/common/Transaction.scala

abstract class DeviceAdapter extends Module
abstract class HostAdapter extends Module

The implementation details are protocol specific and are not shown here. However, the important thing to note are the interfaces. As discussed, the host adapter needs to communicate with the user’s IP as well as with the wishbone slave.

It has a wbMasterTransmitter interface to send requests to the wishbone slave and a wbSlaveReceiver to receive the response from the wishbone slave.

It also has a reqIn and rspOut interface to receive the request from the user’s IP and send the response back respectively.

The WishboneDevice adapter is created here: main/scala/caravan/bus/wishbone/WishboneDevice.scala

class WishboneDevice(implicit val config: WishboneConfig) extends DeviceAdapter {
    val io = IO(new Bundle {
        val wbSlaveTransmitter = Decoupled(new WishboneSlave())
        val wbMasterReceiver = Flipped(Decoupled(new WishboneMaster()))
        val reqOut = Decoupled(new Request())
        val rspIn = Flipped(Decoupled(new Response()))
    })

    // protocol specific logic here
}

The WishboneDevice acts in an opposite direction, i.e it receives the host adapter’s request through the wbMasterReceiver interface and sends back the response through the wbSlaveTransmitter interface.

This time it uses the reqOut interface to send the request to the user’s IP (this maybe some peripheral registers or memory) and receives the rspIn back from the user’s IP (which then is sent back as a response to the host adapter).

Creating the Harness

The testing harness is created to connect the adapters together and verify the correct functionality. In this harness a single point-point interconnection scheme based on wishbone is tested. The host adapter would communicate with the user’s IP (in our case the test-bench stimuli for now) and the device adapter would also communicate with the user’s IP (a dummy memory that we created in the harness itself).

../_images/harness.png

The dummy memory interface is implemented inside the harness here: main/scala/caravan/bus/wishbone/Harness.scala

class DummyMemController(programFile: String)(implicit val config: WishboneConfig) extends Module {
    val io = IO(new Bundle {
        val req = Flipped(Decoupled(new Request()))
        val rsp = Decoupled(new Response())
    })

    // implementation specific logic
}

The Harness module is created here as well: main/scala/caravan/bus/wishbone/Harness.scala

class Harness(programFile: String)(implicit val config: WishboneConfig) extends Module {
    val io = IO(new Bundle {
        val valid = Input(Bool())
        val addrReq = Input(UInt(config.addressWidth.W))
        val dataReq = Input(UInt(config.dataWidth.W))
        val byteLane = Input(UInt((config.dataWidth/config.granularity).W))
        val isWrite = Input(Bool())

        val validResp = Output(Bool())
        val dataResp = Output(UInt(32.W))
    })

    val wbHost = Module(new WishboneHost())
    val wbSlave = Module(new WishboneDevice())
    val memCtrl = Module(new DummyMemController(programFile))

    wbHost.io.rspOut.ready := true.B  // IP always ready to accept data from wb host

    wbHost.io.wbMasterTransmitter <> wbSlave.io.wbMasterReceiver
    wbSlave.io.wbSlaveTransmitter <> wbHost.io.wbSlaveReceiver

    wbHost.io.reqIn.valid := Mux(wbHost.io.reqIn.ready, io.valid, false.B)
    wbHost.io.reqIn.bits.addrRequest := io.addrReq
    wbHost.io.reqIn.bits.dataRequest := io.dataReq
    wbHost.io.reqIn.bits.activeByteLane := io.byteLane
    wbHost.io.reqIn.bits.isWrite := io.isWrite

    wbSlave.io.reqOut <> memCtrl.io.req
    wbSlave.io.rspIn <> memCtrl.io.rsp

    io.dataResp := wbHost.io.rspOut.bits.dataResponse
    io.validResp := wbHost.io.rspOut.valid

}