Pages

Tuesday, December 12, 2017

Make Your Prototype Board Cloud-Accessible


FPGA prototype boards are an important component of the hardware development process, and the progress of synthesizing a design, uploading it to the prototype, and validating its behavior has definitely become easier over time. Modern development environments from the major FPGA vendors make it easy to upload FPGA bitstreams to the board via a JTAG connection or via a USB JTAG adapter (often directly on the prototype board). Standard prototype boards also provide standard interface connectors that enable interaction with the hardware being validated.

So, what's the problem?
Despite how easy it is to connect to modern FPGA prototype boards, it is necessary to be close to and connected to them. Working on an FPGA prototype from the local coffee shop really just isn't a good option.

FPGAMgr
The goal of the FPGAMgr project (https://github.com/mballance/fpgamgr) is to change this. FPGAMgr enables access to an FPGA prototype board via the network -- be that the local network or the internet.

FPGAMgr was developed using the CycloneV-based SocKit prototype board (https://rocketboards.org/foswiki/Documentation/ArrowSoCKitEvaluationBoard), but I'm not aware of any obstacle to making it work with a different vendor's FPGA or different prototype board. FPGAMgr currently provides two key services for interacting with a FPGA prototype board:

  • Programming the FPGA
  • Sending data to and receiving data from I/O interfaces on the FPGA. 


FPGAMgr Components
There are three components to FPGAMgr: The client, the server, and the board configuration.


The client is an API that provides functions for uploading a bitstream to the FPGA, as well as methods to exchange data with I/O interfaces on the FPGA. The server is device and environment agnostic code that processes messages. The board config consists of device- and environment-aware code that knows how to program the FPGA device and knows which I/O interfaces are available to FPGAMgr and how to interact with those interfaces.

FPGAMgr with SocKit
The CycloneV device was Altera's (now Intel's) first foray into pairing an ARM processor with an FPGA fabric. The Arrow SoCKit board (show below) provides an array of physical I/O interfaces connected to the CycloneV.
Since I'm not doing anything too involved with the ARM processor subsystem within the CycloneV, I'll actually run FPGAMgr on the ARM processor. FPGAMgr could also be run on a host workstation connected to the prototype board via JTAG cable and other cables for I/O.

Example
I developed a very simple example design to use in testing out FPGAMgr. Much less involved that what I plan to test using FPGAMgr, but hopefully it illustrates the concept. I wanted to show that I could both program the device, and prove that I'd done so, over the network from my laptop. One of the simplest ways to do so is with a UART-based design that echos back the data it receives. The design looks a bit like this:


The UART is a basic UART from the OpenCores site with a Wishbone bus. The Responder is a custom state machine that initializes the UART, waits for a character to be received, then transmits it back. The count register keeps track of the number of characters received.

The test code that runs on the remote machine is shown (minus some argument-parsing code) below.



The code:
  • Connects via the network to the FPGAMgr server
  • Registers a sideband-channel interface for communicating with the UART
  • Programs the FPGA with the simple design
  • Sends and receives a series of messages from the UART within the design

Demo
The short video below shows the process of connecting to, programming, and interacting with the prototype board from the host workstation.

  • The pane in the upper-left shows the prototype board via a camera pointed at the board.
  • The pane in the lower-left is a login session running on the ARM processor on the SoCKit board.
  • The right-hand pane shows the testbench C++ program running on my laptop.


The general demo process is as follows:

  • I launch the FPGAMgr server specific to the Altera SoCKit in the lower left-hand pane
  • I run the testbench program on my laptop that:
    • Uploads the design image to the FPGA
    • Connects to the UART I/O
    • Sends a series of messages to the UART and receives them back
  • You'll see the LEDs flashing on the prototype as the testbench program runs. The count displayed by the LEDs increments once for 16 characters received by the UART.




Conclusion
FPGAMgr makes it easy to access a prototype board across the network, enabling programming the FPGA and virtualized access to design I/O interfaces. What's present at the moment is proof of concept support for Altera/Intel devices and simple I/O interfaces. 
Do you virtualize access to your FPGA prototype? What are your approaches and key requirements?

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