Manual testing

There are times when it may be inconvenient, or perhaps impossible, to derive an expected output value and test against it on a cycle-by-cycle basis. In these cases you can't use a drive statement to check the relevant output, and instead have to do something else (in other words, 'manual' testing, to distinguish it from the automated nature of drive statements). These cases might include, for example:

  • Your comms hardware might transmit a CRC before transmitting the data that the CRC was derived from. In this case, you can't immediately test the incoming CRC, and must instead record it, and check it a later time
  • When checking an incoming IPv4 packet you may not know in advance what the incoming protocol is (UDP, ICMP, and so on). In this case there is no 'right answer': you just record the protocol byte and adjust your check procedure according to the protocol

In cases like these, you can just read and write the DUT ports directly. All the ports and internal signals declared in the DUT section are directly available as global variables, so this is straightforward. Drive statements can still be used to drive device inputs, and to advance time on a cycle-by-cycle basis. test3.tv is the 'manual' version of the trivial register test from Example #1:

DUT {                            // declare the Device Under Test
   module reg4                   // can paste in the Verilog declaration directly
     (input  CLK,
      input  [3:0] D,
      output [3:0] Q);

   create_clock CLK;             // declare any clocks to be used (default timing)
   [CLK];                        // declare any drive statements to be used
}

void main() {
   for(bit4 i=0; i<10; i++) {
      D = i;                     // set up any inputs for the next clock edge
      [.C];                      // drive the clock, advance to the next OP
      if(Q == i) _passCount++;   // test the outputs at the OP
      else       _failCount++;
   }
}

This test can be run in exactly the same way as the Example #1 test, and produces the same output:

$ rtv test3.tv reg4.v

The code you execute in your function is executed at a specific time, which is known as the 'Operating Point', or OP. This is true both when executing drive statements, and when reading or writing global variables to access the DUT directly (13). This time is chosen by the compiler so that device outputs are valid and can be sampled and checked, and device inputs can be driven to set up for the next clock edge.


The Operating Point

In most circumstances, RTL simulations are carried out without timing information, as 'delta delay' simulations. In these cases you can simply use a default clock waveform, and there's no need to provide timing constraints in the DUT section, and you don't need to know about the OP.

When you advance time with the .C directive the testbench advances to 'just before' the next active (normally rising) clock edge. If you're testing combinatorial outputs, the compiler chooses a pseudo-'cycle time' which has the same effect. This time is the OP associated with this clock, and .C directives simply advance time from one OP to the next. You don't need to know anything else for the vast majority of simulations: the code you write executes at the OP, and everything else happens behind the scenes. In this case, the most complicated thing you will ever need to do is to change the clock cycle time if you want a more realistic waveform display, or if you have multiple clocks with different waveforms:

  DUT {
  ...
  // this DUT has two clocks:
  timescale ns;                   // this is the default timescale
  create_clock CLK1;              // default period (10ns) and waveform, rising edge first
  create_clock CLK2 -period 2.4;  // default waveform, period 2.4ns, rising edge first
  }

If the clock is a DUT input, the generated testbench creates and drives the clock. If the clock is a DUT output, however, the .C synchronises (9) to the device output. You can also declare and drive a 'virtual' clock (14) if necessary.


Timing simulations

If you are carrying out timing simulations things become a little more complicated. You will need to declare timing constraints (tSU, tIH, tOH, and tOD parameters) in the DUT section, and you may have to declare a specific clock waveform.

In this case, the compiler chooses an OP at the earliest tSU time. The .C will still advance between these OPs. Any ports or signals that you drive at the OP (with either a drive statement or 'manually') will be recorded by the TB, and will be automatically driven to the DUT at the correct setup time. However, with sufficiently large tSU and OD constraints, it is possible that one or more device outputs will now become valid after the OP. If this is the case, those outputs cannot be directly manually tested at the OP (a drive statement sets up a checker to run at the relevant time, so can still be used in this case). For manual testing, the 'last attribute can instead be applied to the signal or the output port to return the value which would have been sampled at the last clock edge.

When constraints are present, the DUT inputs are driven at the relevant tSU, and hold until the relevant tIH, and are driven to X at all other times. For DUT outputs, the testbench automatically confirms that DUT outputs are stable within the stability window defined by tOD through to the next tOH.