|   JHDL Home Page   |   Configurable Computing Lab   |   User's Manual (TOC)   | Search

Test Benches - Programmatic Control of Circuit Building, Simulation, and Netlisting

After you have designed your circuit, it's time to test it. In previous sections you learned about dtb, a tool which can take a circuit, load it into cvt, and help you simulate it. This is adequate for the simplest of designs but for many tasks a test bench will be required.

A Test Bench is a JHDL design you create which takes the place of dtb - it can load your circuit, apply stimulus to its inputs (including defining multiple clocks), check the outputs for correctness, and netlist the final design.

Because it is a program you write, you have much more control over how your circuit is built, simulated, and netlisted than you do with cvt. As a result, most designs other than simple ones you create while you are learning will make use of the test bench capability.

In addition to the above capabilities, a test bench can provide behavioral or structural models for everything on the PC board. In this way, it enables you to simulate the entire system including your FPGA design(s) as well as external bus interfaces, external memories, etc.

Finally, a testbench is what you would write to deploy the final application. Because it is simply a Java program, it can leverage all the features of Java - file I/O, complex data structures, GUI's, etc.

As a result, the JHDL developers encourage you to start early in your project with a test bench that contains many of these features as a way of helping you debug your design while still in the simulation stage.

A Simple Testbench

In this section a very simple TestBench is shown. Code is in red, its description is in black.


import byucc.jhdl.base.*;
import byucc.jhdl.Logic.*;
import byucc.jhdl.Xilinx.Virtex.*;

public class tb_FullAdder extends Logic implements TestBench {

In addition to importing both base and Logic, you must import the library for the chosen technology for the design. In addition, your testbench class should extends Logic and implements TestBench.


  static HWSystem hw;

  public static void main(String argv[]) {
    hw = new HWSystem();
    tb_FullAdder tb = new tb_FullAdder(hw);
    new cvt( tb );
  }

Because your test bench is a stand-alone Java program it must have a main routine. A number of things are going on in this main routine:

  1. The first step is to create a new hardware system (HWSystem). The variable hw was declared to be a static class variable so that it can be accessed later in the code.
  2. Next, an instance of this test bench (tb_FullAdder) is created with the HWSystem as its parent. You will see its constructor below. It builds the circuit to be tested.
  3. A cvt GUI environment is created. This is what you will use to exercise your circuit.

  private Wire a, b, sum, cout;

Next, some wires to be used to connect the test bench to the FullAdder which will be created below are declared.


  public tb_FullAdder(Node parent) {
    super(parent);

    setDefaultTechMapper(new VirtexTechMapper(true));

    a = wire(1, "a");
    b = wire(1, "b");
    sum = wire(1, "sum");
    cout = wire(1, "cout");

    new FullAdder(this, a, b, gnd(), sum, cout);

  }

This next section is the constructor for the test bench. After calling super() like all Logic cells must do, it sets the technology to be Virtex. More precisely, it creates a new Virtex techmapper and then tells JHDL that it should be used for all techmapping as the circuit is built. It then creates the wires to connect the test bench to the FullAdder and then creates the FullAdder. Note that since no carry in is desired, a gnd() call is used to tie that input to a low voltage.


  public void reset() {
    a.put(this, 0);
    b.put(this, 0);
    cnt = 0;
  }
 
  int cnt = 0;
  public void clock() {
    a.put(this, cnt & 0x1);
    b.put(this, (cnt>>1) & 0x1);
    cnt++;
  }
}

All test benches must provide stimulus to the circuit under test. In this case, the a and b wires must be driven to exercise the adder. This example shows the simplest way to do this. It is done by creating reset() and clock() methods. The reset method is called at the start of each simulation run. All it needs to do here is to drive 0's into the circuit. In addition, it resets the clock counter for use later.

The clock routine gets called after every single clock phase. What the code here does is count clock phases and use that value to derive values to drive into the a and b inputs to the FullAdder circuit. As you simulate you will see that this cycles through all 4 input combinations very nicely. The above file can be found as tb_FullAdder.java and the FullAdder as FullAdder.java if you want to experiment with them.

Table-Driven Stimulus and Correctness Checking

The above example uses the clock counter to help derive the pattern to drive into the circuit under test. Because the test bench is a normal Java program, any method that Java supports can be used to derive the values to drive into the circuit. One obvious example is to load up some arrays with values for a and b and access them using the clock counter. Another method would be to encode them as strings or to put the values into a file which the test bench code would open and read.

Further, it would be a simple matter to augment the code to do correctness checking like this:


  public void clock() {

    int res = a.get(this) + b.get(this);

    if ((res & 0x1) != sum.get(this) ||
	((res>>1) & 0x1) != cout.get(this))
      System.err.println("Addition error: " +
			 a.get(this) + " " + b.get(this) + " " +
			 sum.get(this) + " " + cout.get(this));
    a.put(this, cnt & 0x1);
    b.put(this, (cnt>>1) & 0x1);
    cnt++;
  }
}

Since the clock routine is called only after the circuit has stabilized in simulation, it is a simple matter to get the values off the wires of interest and determine whether they were correct. This is done before putting the new values for the next cycle onto them.

As with stimulus above, it would be a simple matter to use arrays or files to contain the correct responses to the stimulus provided.

Simple Test Benches and Synchronous Circuits

In the case of synchronous circuits there will be a multi-phase clock used. That is, a two-phase clock will have two phases per cycle. This presents a problem in that the clock routine is called after every phase. Thus, some code must usually be used to have the signals change only once per cycle. This is readily done by counting calls to the clock routine and dividing by the length of the clock period. Here is an augmented version of the clock routine above to do this:

  int cnt = 0;
  public void clock() {
    boolean newcycle;
    if (hw.getCurrentStepCount() == 0)
      indx++;

      someWire.put(this, vals[indx]);

    cnt++;
  }

This works by asking the HWSystem the current step count and changing an index into the stimulus data each time a new cycle begins.

Using ProgrammaticTestBench For Stimulus

In the above test bench, the simulator will call the clock routine you have defined. It will do so after each clock phase. In this sense the simulator is in charge - your test bench only responds to these calls. An alternate method to the above is to have your test bench implement ProgrammaticTestBench instead of simply TestBench. This makes it possible for your test bench to drive the simulation. There are three steps to convert a TestBench into a ProgrammaticTestBench. The first is to have the test bench implement ProgrammaticTestBench instead of simply TestBench. The second step is to write an execute() routine which will drive the simulation and compute new values to be driven into the circuit. The third step is modify the clock routine of your testbench to put those values onto the circuit's wires.

Step 1 is easy:


  public class tb_NBitAdder extends Logic implements ProgrammaticTestBench {

Here is an example of step 2:


  private long a_val, b_val, sum_val;

  public void execute() {

    final long a_iterations = 1<<adder_width;
    final long b_iterations = 1<<adder_width;

    for (long i=0;i < a_iterations;i++) {
      for (long j=0;j < b_iterations;j++) {
	a_val = i;
	b_val = j;
	getSystem().cycle(1);      // Step system clock 1 cycle
	sum_val = sum.getL(this);
	long correct = a_val + b_val;
	if (sum_val != correct) {
	  System.out.println("NBitAdder failed miserably!");
	  System.out.println("a = " + a_val);
	  System.out.println("b = " + b_val);
	  System.out.println("sum = " + sum_val);
	  System.out.println("correct = " + correct);
	  return;
	}
      }
    }
    System.out.println("NBitAdder passed with flying colors.");
  }

The execute routine here uses doubly nested loops to exhaustively test the circuit by computing values for a and b based on the loop counters. It then asks the system to step the clock by 1 cycle after which it determines whether the sum was computed by the circuit correctly.

Another point of interest is that long integers are used for everything here. This is because above 32 bits wide, wire values won't fit into a conventional int. This test bench is designed to allow for the testing of NBitAdders wider than 32 bits wide. Thus, the sum.getL() is needed - getL will get the value of a wire more than 32 bits wide.

Note that above, the new values to be driven into the circuit are only computed. The reset and clock routines still must drive those values onto the wires. That is step 3. Here is the code:


  public void reset() {
    a.putL(this, 0);
    b.putL(this, 0);
  }
 
  public void clock() {
    a.putL(this, a_val);
    b.putL(this, b_val);
  }

Since the values to be driven onto the wires are longs, putL calls are used.

In summary, ProgrammaticTestBench makes it much easier to write pattern generators such as the exhaustive tester above. A complete ProgrammaticTestBench for the NBitAdder example is available as tb_NBitAdder.java. This file is also interesting because it uses command line arguments to let the user tell whether a cvt GUI environment is desired. It also uses command line arguments to control whether a netlist should be generated (ignore that part of the code for now - it is described later in the section on advanced netlisting).

What Else Do I Need to Know?

The downside of both TestBench and ProgrammaticTestBench is that the values to be put onto the wires must be determined at compile time since they are compiled into your Java program. An interactive alternative is provided by Stimulators - they are the preferred method for interactive simulation and are described in Using Stimulators.

|   JHDL Home Page   |   Configurable Computing Lab   |   Prev (Netlisting)   |   Next (Stimulators)   |


JHDL 0.3.45