Showing posts with label design abstraction. Show all posts
Showing posts with label design abstraction. Show all posts

Sunday, January 7, 2018

DVKit: Workspaces, Projects, and Legacy Code


In Part 1 of this series, I introduced DVKit, an Eclipse-based IDE for files types used by DV engineers. If you're used to editing your source files with a single-file editor like Vi/Vim or Emacs, moving to an integrated development environment can take some getting used to. Single-file editors don't require any context (in fact, do not take advantage of any context) beyond the file on which they have been invoked. In contrast, IDEs require context for a file in order to provide many of the project-centric features that they provide.

Eclipse, like many integrated development environments, provides a variety of flexible features with which to specify the context of the files being developed. In this post, I'll describe the best practice fundamentals I've developed over the years when setting up development projects. Future posts will explore a few additional features that are very helpful in some cases.


Projects 

Eclipse provides two fundamental features for working with code: projects and workspaces (shown above). A project is represented by a directory that includes the sources being developed and some meta-data files with information about the language (eg Java) being used and how that language should be processed (eg assume Java 7). Some of this information is captured in a file named '.project' that is present in all Eclipse projects, and some will be captured in toolchain-specific files.

An Eclipse session can have multiple projects open at any given time.

Workspaces

Eclipse uses a workspace to capture a development session. A workspace records the projects that are open, the windows that are open and their positions and sizes, etc. In contrast to projects, a workspace typically does not contain any actual code -- just the preferences and settings used while developing code.

Projects are associated with a workspace in two ways:
  • When a project is created, it is automatically added to the active workspace
  • An existing project can be imported into the active workspace
In both cases, a link is added from the workspace to where the project is located on the filesystem. The project and its files will be visible in the workspace, but you are actually editing the files within the project directory, as shown in the diagram above.



I typically create a workspace for each codeline that I'm working on. So, I might have a workspace for "sveditor-master" and one for "sveditor-fast-indexer".

Working with Existing Code

If you're getting started with an Eclipse-based IDE like DVKit, you likely have lots of existing code and no existing Eclipse projects. So where do you start? Well, the good news is that Eclipse projects fit very nicely around existing code. You just need to decide what the primary language for the project is, and create the right project.

Let's say I want to work on files from the Linux Device Tree Compiler (DTC), and that I've cloned a copy of the repository (git://git.kernel.org/pub/scm/utils/dtc/dtc.git) to c:/usr1/fun/dvkit/tutorial/dtc.
After launching DVKit, I would launch the 'New C Project' wizard (New->Project... ) to create a new project for editing C code.


The next wizard allows me to provide specifics about the project I want to work on.



  • What is the project named? 'dtc'
  • Where is it located? On the filesystem at c:/usr1/fun/dvkit/tutorial/dtc
  • How are the source files compiled? With a Makefile
After completing this wizard, the 'dtc' project will be visible in the workspace:



Now the source files from the 'dtc' project are visible in the workspace and available for editing. In the future, we can bring the 'dtc' project into another workspace by running the Eclipse Import wizard to import an existing project.

Conclusion

The Eclipse project construct allows project-specific settings to be associated with source code, while the workspace construct enables users to manage session-specific settings. Eclipse's ability to construct a project around existing source code makes it easy to use Eclipse to develop existing code.

As always, you can download the freely-available Eclipse-based DVKit here: https://sourceforge.net/projects/dvkit/files/

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]
  }   
}
}