Monday, October 2, 2017

Designing Standard-protocol Interfaces with Chisel Bundles




Standard interfaces are all around us, and enhance interoperability between devices created by different organizations. While some standard interfaces are quite niche in nature, others, like the unbiquitous phono jack, have been used for many applications that are only slightly related. 
When it comes to design and reuse of design IP, using higher-level interfaces (certainly higher-level that just a set of wires) helps to make use and reuse of the IP easier. An IP that connects with the rest of the design via interfaces is easier to understand than a block that has a wire-level interface -- even if those hundreds of wires are equivalent to several high-level interfaces. Connecting an IP with top-level interfaces to the rest of the design is much easier and trouble-free than individually connecting hundreds of signals. 
SystemVerilog provides the interface construct as both a design and a verification feature. A SystemVerilog interface describes the low-level signals of which the interface is composed. The ways in which those signals can be used (eg initiator vs target) are captured using a modport. 

interface wb_if #(
              parameter int WB_ADDR_WIDTH = 32,
              parameter int WB_TGA_WIDTH = 1,
              parameter int WB_DATA_WIDTH = 32,
              parameter int WB_TGD_WIDTH = 1,
              parameter int WB_TGC_WIDTH = 1
              );
      
       reg[(WB_ADDR_WIDTH-1):0]                 ADR;
       reg[(WB_TGA_WIDTH-1):0]                  TGA;
       reg[2:0]                                 CTI;
       reg[1:0]                                 BTE;
       reg[(WB_DATA_WIDTH-1):0]                 DAT_W;
       reg[(WB_TGD_WIDTH-1):0]                  TGD_W;
       reg[(WB_DATA_WIDTH-1):0]                 DAT_R;
       reg[(WB_TGD_WIDTH-1):0]                  TGD_R;
       reg                                      CYC;
       reg[(WB_TGC_WIDTH-1):0]                  TGC;
       reg                                      ERR;
       reg[(WB_DATA_WIDTH/8)-1:0]               SEL;
       reg                                      STB;
       reg                                      ACK;
       reg                                      WE;

       modport master(
                     output        ADR,
                     output        TGA,
                     output        CTI,
                     output        BTE,
                     output        DAT_W,
                     output        TGD_W,
                     input         DAT_R,
                     output        TGD_R,
                     output        CYC,
                     output        TGC,
                     input         ERR,
                     output        SEL,
                     output        STB,
                     input         ACK,
                     output        WE);
      
    ...             

endinterface

An example of a Wishbone SV interface is shown above, with just the 'master' modport shown. As you can see, parameters are specified on the interface declaration, core signals are declared without direction, and directions for different uses of the signals are specified via modport declarations.

Chisel provides the Bundle construct to group signals together. While the concept and high-level use of a Chisel Bundle is quite similar to a SystemVerilog interface, there are some significant differences. This blog captures the best practices that I've discovered thus far while describing Chisel bundles for standard interfaces.

SV Interfaces vs Chisel Bundles

If you've spent time working with SystemVerilog interfaces already, understanding the differences between SV Interfaces and Chisel Bundles will likely make the best practices below make more sense. 

While SystemVerilog provides the modport construct for describing a usage of a interface, Chisel doesn't have a similar notion. All signals in a Chisel bundle are given a direction. Bundles may be instantiated as-is, or instantiated 'Flipped' with reversed signal directions.

Chisel bundles can be hierarchical, so a bundle type can be composed of several instances of other bundle types. In contrast, a SystemVerilog interface must effectively be single-level.

Being an object-oriented language, Chisel allows methods to be defined on a bundle type that assign values to the bundle signals. This can be very useful by making it easy for the user of a bundle type to drive the bundle signals to a useful state.

Chisel Bundle Best Practices

At the end of this blog post is a Chisel description of a Wishbone interface, which I'll refer to in the best practices description below.

Describe from the Initiator's Perspective

Since signal directions are specified on the signals of a Chisel bundle, it's helpful to be consistent in picking either the initiator or the target and describing all interfaces in those terms. I've picked the initiator as the standard perspective to use. 
Note that the Wishbone signal directions are captured from the initiator's (master's) perspective. For example, ADR and CYC are outputs, while DAT_R and ACK are inputs.

Collect Related Signals in a Sub-Bundle

Users of a standard interface will often benefit from working with sub-elements of the protocol. Declaring this sub-elements as part of the interface declaration can be very helpful. Since some Chisel elements (such as the Mux) expect all elements of a bundle to have the same direction, it's important that all elements of a sub-bundle have the same direction. In the Wishbone example above, I've created a 'ReqData' bundle to capture all signals related to the transaction request, and a 'RspData' bundle to capture all signals related to the transaction response.

Collect Protocol Parameters into a Parameters Class

Standard protocols are often parameters. For example, the Wishbone address, data, and tag widths are variable. Collecting protocol parameters into a class, instead of passing them individually to the bundle constructor, has two key benefits:
  • Less typing when creating multiple instances of the interface with the same parameterization
  • It's easier to create the 'cloneType' method (see next tip), and this can even be placed in a base class if you prefer

Define a cloneType Method

Chisel needs to clone Bundle objects for several reasons. A parameterized standard interface bundle must provide a cloneType method to ensure that the proper parameters are used when the interface bundle is cloned. You can see the definition of the cloneType method above.

Provide tieoff and tieoff_flipped Methods

It should be easy for any users of a standard interface to tie-off that interface. In other words, effectively disable the interface. The tieoff() method is used for initiator interfaces. As you can see, tieoff() drives the response signals to inactive values. The tieoff_flipped() method is used for target interfaces. As you can see, tieoff_flipped() drives the request signals (ADR, CYC, etc) to inactive values.

Note that if a clock or reset must be applied to an interface for it to function properly, the tieoff() method can accept handles to these required signals.

Provide Utility Methods

The ability to provide utility methods for driving interface signals to pre-defined states helps minimize the code an IP must write. In the case of Wishbone, setting the error-response state is done directly by the set_error() method. Any IP that needs to return an error can call this method to set the appropriate values.


I've found the best practices above to be helpful in structuring interfaces that are easily reusable. If you've been working with Chisel, what best practices have you discovered in working with Chisel bundles?

Chisel Bundle for a Wishbone Interface

class Wishbone(val p : Wishbone.Parametersextends Bundle {

  val req = new Wishbone.ReqData(p)
  val rsp = new Wishbone.RspData(p)
      
  override def cloneType() : this.type = {
         return new Wishbone(p).asInstanceOf[this.type]
  }   
  def tieoff() {
    rsp.tieoff()
  }
      
  def tieoff_flipped() {
    req.tieoff_flipped()
  }
}
object Wishbone {
class Parameters (
    val ADDR_WIDTH  :  Int=32,
    val DATA_WIDTH  :  Int=32,
    val TGA_WIDTH   :  Int=1,
    val TGD_WIDTH   :  Int=1,
    val TGC_WIDTH   :  Int=1) { }

class RspData(override val p : Wishbone.Parametersextends Bundle {
  val DAT_R = Input(UInt(p.DATA_WIDTH.W))
  val TGD_R = Input(UInt(p.TGD_WIDTH.W))
  val ERR = Input(Bool())
  val ACK = Input(Bool())   

  override def cloneType() : this.type = {
    return new RspData(p).asInstanceOf[this.type]
  }

  def tieoff() {
    DAT_R :0.asUInt();
    TGD_R :0.asUInt();
    ERR := Bool(false);
    ACK := Bool(false);
  }

  def set_error() {
    ERR := Bool(true);
    ACK := Bool(true);
  }
}

class ReqData(override val p : Wishbone.Parametersextends Bundle {
  val ADR = Output(UInt(p.ADDR_WIDTH.W))
  val TGA = Output(UInt(p.TGA_WIDTH.W))
  val CTI = Output(UInt(3.W))
  val BTE = Output(UInt(2.W))
  val DAT_W = Output(UInt(p.DATA_WIDTH.W))
  val TGD_W = Output(UInt(p.TGD_WIDTH.W))
  val CYC = Output(Bool())
  val TGC = Output(UInt(p.TGC_WIDTH.W))
  val SEL = Output(UInt((p.DATA_WIDTH/8).W))
  val STB = Output(Bool())
  val  WE = Output(Bool())
      
  def tieoff_flipped() {
    ADR := 0.asUInt()
    TGA := 0.asUInt()
    CTI := 0.asUInt()
    BTE := 0.asUInt()
    DAT_W :0.asUInt()
    TGD_W :0.asUInt()
    CYC := Bool(false)
    TGC := 0.asUInt()
    SEL := 0.asUInt()
    STB := Bool(false)
    CYC := Bool(false)
    WE := Bool(false)
  }

  override def cloneType() : this.type = {
    return new ReqData(p).asInstanceOf[this.type]
  }   
}
}