DUT Testing

In order to test a DUT, you need to provide a DUT section, and some drive statements. The code below is a complete test for a simple 4-bit up-counter, with a synchronous reset:

1  DUT {
2     module counter(input CLK, RST, output [3:0] Q);
3     create_clock CLK;             // declare the clock 
4     [CLK, RST] -> [Q];            // declare the drive statement format 
5  }
6 
7  void main() {
8     bit4 expected;                // 'expected' initialises to 0
9     [.C, 1] -> [expected];        // check that the Q output resets to 0
10    for(int i=0; i<16; i++)       // loop 16 times
11       [.C, 0] -> [++expected];   // count, with rollover 
12 }

The DUT section appears at the same level as a function definition; it's convenient to have it at the top of the source file. Maia needs to know at least 3 things about the DUT:

  • Line 2 is the DUT interface: the names of the inputs and the outputs, and their widths. This can generally be cut-and-pasted from your Verilog module definition, with no change. The reg keyword is optional and can be omitted.
  • Line 3 is a clock declaration; in general, you can add a waveform and period here, but the defaults are used in this case. Maia needs to know about clocks because drive statements may be clocked, by using the .C directive
  • Line 4 declares the drive statement to be used. The declaration allows the compiler to tie up the DUT port names against the expressions which are used in the drive statements on lines 9 and 11. You can declare as many drive statements as are required; this simple test uses only one.

The program starts executing at 'main', on line 7. Drive statements are executed sequentially as they are encountered. Drive statements can include a number of directives; the ones in this example use only the .C directive, which issues a clock pulse. There are only two drive statements in this example:

  1. The statement on line 9 sets the RST input to 1, applies a clock pulse to CLK, and checks that Q resets to 0. This simple example uses a default clock waveform and default timing; more complex parameters can be set in the DUT section.
  2. The statement on line 11 is enclosed in a for loop. It sets the RST input to 0, applies a clock pulse to CLK, and confirms that the Q port takes on the current value of the variable expected. Note that expected is incremented before each test, using the pre-increment (++) operator.

The entire program applies 17 clock pulses to the DUT. The first checks the synchronous reset, while the next 16 confirm that the output sets to 1, 2, and so on, through 15, and back to 0. The program will report 17 passes if the DUT behaves as expected, and will report failure otherwise, together with the line number of the failing vector (in this case, either 9 or 11), the time of the failure, and the expected and actual DUT outputs.

In this particular example, the drive statement input and output expressions were just directives (.C), constants (0 and 1), and variables (expected). In general, however, they may be either directives or arbitrary expressions.


Pipelining

This example showed a clocked drive statement, with a one-level pipeline delay: in other words, the outputs were expected to become valid after the next clock edge. In general, however, drive statements may also be combinatorial, or may be clocked, with an arbitrary pipeline depth. The drive statement on line 11 is equivalent to:

[.C, 0] ->1 [++expected];     // ->n, where n is the pipe delay

In general, the required pipeline level may be a constant, a variable, or an arbitrary bracketed expression. The expression is evaluated when the statement is encountered, and so may be dynamic.


Triggering

Pipelined testing is useful only when it is known in advance exactly when an output will become valid. This might be suitable when testing a filter, for example, when it is known that the outputs should be valid a fixed number of cycles after the inputs are applied. In general, however, it may be difficult to predict exactly when a particular sequence should arrive at a given set of outputs. When testing a multi-input bus switch, for example, it is known that a given input packet must eventually arrive at an output, but it may be difficult to determine in advance exactly when the packet should appear. This can be handled in two ways:

  • You can execute a function in a new thread (with the exec statement). This function can simply test the DUT outputs on every clock cycle, and respond to a packet when it appears
  • A simpler, and less versatile, alternative is to use a trigger statement to automatically wait for the condition which flags that a new packet has arrived. This is analogous to an oscilloscope trigger condition.

The trigger condition appears as part of a trigger statement, together with the name of a trigger function. The condition is remembered, and execution continues sequentially after the trigger statement. If the trigger condition is eventually satisified, the corresponding trigger function is executed, concurrently with the sequential program flow:

void main() {
  int a, b, c;
  ...
  // post function 'test_packet' for later execution; sample the current values
  // of a, b, and c as the formals
  trigger test_packet(a, b, c) when DATA_RDY && Q == 10;
  ... continue execution in thread 0. any changes to a, b, and c
  ... are not passed to 'test_packet'
}

// this concurrent routine is executed when the trigger condition becomes true
@test_packet(int x, int y, int z) {
  ...
}

The when syntax posts test_packet for a single execution when the condition (DATA_RDY && Q == 10) is sampled true. This is effectively 'one-shot' triggering. test_packet may alternatively be posted for multiple executions by using when all rather than when; this effectively re-arms the trigger whenever test_packet completes. Trigger functions do not have a 'caller' and so cannot return; the implicit thread associated with the trigger function terminates when the function completes.


Manual drive and test

There is no requirement that the DUT should be accessed exclusively through drive statements. The DUT ports (in this case, CLK, RST, and Q) are automatically declared as external variables, and may be manually read, or driven, anywhere in the testbench. If the DUT section includes any timing information the compiler ensures that the ports are read or driven at the correct time (as long as you don't arbitrarily use wait statements to advance time). The DUT section may also declare internal signals in the DUT (using the signal statement); these may also be manually read and driven, and the compiler again ensures that these operations are carried out with the correct timing.