Example #2: pipelined MAC

This is a second trivial example, which tests a Multiply-Accumulate module with a configurable pipeline level. In this case, the generated testbench instantiates the DUT with a 3-stage pipeline (by setting a parameter in the Verilog module, or a generic in a VHDL entity). The testbench also creates a pipelined checker, which tests the Q output only when it is expected to be ready. The pipeline setup, test, and flush are handled automatically.

There are some other notable points in this test:

  • variables declared as varn are 4-state variables (each bit can take on any of the 4 values 01XZ), while variables declared as bitn are conventional 2-state variables (each bit can be 0 or 1). Both are unsigned. bit variables initialise to 0 when created, while var variables initialise to X
  • 'conventional' integers can be declared as int. These are signed, and are used primarily for general housekeeping operations, such as loop counting and indexing
  • The default level of type checking is approximately C-like, and the 8-bit multiply is carried out using an explicily-sized unsigned operator (in this case, *$8). What this means is that the 4-bit a and b inputs are zero-extended and attached to a multiplier with an 8-bit output. The output is then zero-extended to the 10-bit destination size when it is assigned to sum. This behaviour differs from both VHDL and Verilog (12)
  • Confused? If in doubt, and it's not hardware-related, then it probably does the same as C or C++
DUT {
   module MAC1                      // declare the module interface...
      @(.stages(3))                 // ...just paste in the Verilog module
       (input  RST, CLK,            //    declaration (replacing '#' with '@')
        input  [3:0] A, B,
        output [9:0] Q);
   
   [RST, CLK, A, B] -> [Q];
   create_clock CLK;                // default timing
}

main() {
   var4  a=0, b=0;                  // vars initialise to x
   var10 sum=4;
   [1, .C, -, -] ->3 [0];           // check that Q resets to 0 after 3 cycles
   [0, .C, 2, 2] ->3 [sum];         // sanity check: 2*2=4

   for(int i=0; i<16; i++) {
      for(int j=0; j<16; j++) {
         sum += (a *$8 b);
         [0, .C, a, b++] ->3 [sum]; // drive A and B, check that Q equals 'sum' 3 cycles later
      }
      ++a;
   }
}

If this code is saved as test2.tv and the MAC1 module is in mac1.v, then the test can be run as:

  $ rtv test2.tv mac1.v

If the DUT is correctly coded, the test reports:

  (Log)        (2600 ns) 258 vectors executed (258 passes, 0 fails)

The 258 passes refer to the reset test (Q resets to zero); the first MAC sanity test (Q=4); and the Q tests for the 256 input combinations.

The code can be made more compact and more readable by using the for all statement, rather than explicitly cycling through the values of i and j. See test2b.tv for the equivalent for all test:

  for all a {
     for all b {
        sum += (a *$8 b);
        [0, .C, a, b] ->3 [sum];
     }
   }