Skip to content

RacesInTestbenches

Brent Nelson edited this page May 15, 2020 · 1 revision

Verilog Testbench/DUT Races - An Introduction

To understand the context for this, note that it was originally created for ECEn 620 at BYU which was focused on design verification and validation at the time.

Introduction

A VHDL designer transitioning to Verilog may think that the languages share similar semantics and therefore all they need do is learn new syntax and they can immediately be productive. Once they get into learning and using Verilog, the possible presence of races in their designs and the wide range of creative ways of getting around those races can be, well, a bewildering experience. This short intro to Verilog Races is written somewhat from the viewpoint of a VHDL designer and is designed to help you quickly get your hands around some of the concepts you need to know so that much of your VHDL understanding of HDL simulation semantics will be useful to you as you transition to Verilog.

Simulation of HDL's

HDL's are designed to describe inherently parallel and concurrent hardware systems. However, these concurrent designs must be simulated using serial computers using serial languages. The designers of modern HDL's have taken great care to specify the precise semantics of the various language constructs and which a simulation program must exactly mimic.

HDL designs consist of a collection of process blocks which describe the system's behavior and structure. In VHDL these are called "process" blocks, and in Verilog they take the form of either "initial" or "always" blocks. For purposes of this description I will use the term "process" to refer to either one.

A key issue is that these process must behave in simulation as if they were executed concurrently. However, in real life they will be executed serially during a simulation. Any real system will contain a large collection of concurrent processes. The result of a simulation should be the same regardless of the order in which the processs are executed.

In VHDL, this is mostly true just due to how the language is structured. However, Verilog contains various language constructs which make it possible to write code which gives very different results depending on what order the simulator chooses to execute the processes. This leads to what are called "race conditions".

No small amount of energy has been expended over the years by designers tinkering with their Verilog code to achieve a consistent order of execution and therefore consistency of results. To a VHDL designer, the existence of such races can seem baffling, but they can exist in VHDL as well, but this seems to be to a much smaller extent and is limited to certain scenarios.

Rather than attempt to cover all the places where races could get one into trouble, I will focus on a few which, if understood, will allow one to design in a way that races can largely be ignored.

A Race-Free System

Consider the code below:

  // A race-free DUT and testbench
  module DUT(input d, input clock,output reg q); 
    always @(posedge clock)
      q <= d;
  endmodule 
  
  module testbench(); 
    logic d, q;
    bit clk;
    always clk <= #5 ~clk;
     
      // Instantiate the DUT
      DUT dut_i(d,clk,q); 
  
      // Stimulus - drive data into the DUT
      initial begin 
        @(posedge clk) d <= 1; 
        @(posedge clk) d <= 0; 
      end 
  
      // Monitor output of DUT
      always @(posedge clk)
        $display("Value of D when clock occurs: %b", d);
  endmodule 

This code is VHDL-like in that all assignment statements are written using Verilog non-blocking assignments (NBA's) using the <= operator and all actions happen on the rising edge of the clock.

The semantics of the code are as follows:

  • At each rising edge of the clock, the DUT copies the value on the 'd' wire and assigns it to its 'q' wire. This is obviously an edge-triggered D flip flop design.

  • At each rising edge of the clock, the $display statement in the monitor reports the value of the 'd' signal at the time of the clock edge. This should be the exact same value that the DUT flip flop code sampled as well.

  • At each rising edge of the clock, the simulus process drives new values onto the 'd' signal. The key here is that they should happen only after the DUT flip flop code has sampled the old value of the 'd' signal.

The 'd' signal will initialize to an X value and so the values of 'd' on each clock edge will be X, 1, 0, 0, 0, ...

Pseudo-code for this is as follows:

  • Execute all processes which should be active at this point in time. These are processes whose sensitivity condition has been triggered. In this example, the clock signal has transitioned high and so all three processes are enabled for executing code.
  • As each process executes, read the current values of all signals to do the computation but write the new computed values to shadow values (in this case 'q_next' and 'd_next').
  • When all processes have been allowed to run, copy all the '_next' values to the real signal values.
  • If no '_next' values need copying in step 3 (nothing has changed as a result of the processes' execution) then the circuit has settled and so terminate the loop. Otherwise, go to step 1 and repeat.

In step 2 above, the assignment statements are executed but the new signal values are not copied onto the wires until step 3. Therefore, there are no races in the system --- the processes can be executed in step 2 in any order and obtain the same result since they will all do their computations using the exact same set of signal current values..

This model is reminiscent of the VHDL execution model which uses what are called "delta delays". In VHDL, when a signal assignment occurs, the actual results are not copied onto the wires until one delta delay has passed, similar to how the values are not copied onto Verilog wires until step 3 of the pseudo-code above.

A System with Races

Verilog has a second assignment operator in addition to <=. It is called a blocking assignment and its symbol is '='. When a blocking assignment statement is executed, the new value of the signal is updated immediately (before the simulation proceeds and thus the term 'blocking'). There is no "delta delay" on the signal assignment - it happens instantaneously.

With this in mind, consider the code below which has a very minor difference from the original code:

  // A racy DUT and testbench
  module DUT(input d, input clock,output reg q); 
    always @(posedge clock)
      q <= d;
  endmodule 
  
  module testbench(); 
     logic d, q;
     bit clk;
     always clk <= #5 ~clk;
       
       // Instantiate the DUT
       DUT dut_i(d,clk,q); 
    
       // Stimulus - drive data into the DUT
       initial begin 
         @(posedge clk) d = 1; 
         @(posedge clk) d = 0; 
       end 
  
       // Monitor output of DUT
       always @(posedge clk)
         $display("Value of D when clock occurs: %b", d);
  endmodule 

The changes are so minor they may be hard to spot --- the stimulus assignments to 'd' now use blocking assignments ('=') instead of non-blocking assignments. With this change, the order in which the various processes are executed by the simulator makes a huge difference.

First, assume that after a clock edge that the DUT and monitor processes execute first and the stimulus process executes last. The DUT and monitor get the same values of 'd' as before and the result is identical. When the stimulus process executes last it cannot mess things up since everyone else is now waiting for the next clock edge to occur.

Now, assume that the simulus process runs first and then the DUT and monitor processes run. Due to the use of '=' assignments in the stimulus, by the time the DUT and monitor get to run, the 'd' signal will have already been assigned a new value and so they will sample that new signal rather than the value 'd' had at the time of the clock edge. That is, the order of execution has allowed the simulus assignment to slip in front of the DUT process. This is an error and will not give correct (or expected) results.

This is clearly wrong - our D flip flop in this case is no longer a D flip flop - it no longer samples the value of 'd' at the clock edge, but rather the value of 'd' some time after the clock edge.

The problem is that the language standard does not provide any rule that dictates the order in which processes should execute. The ordering is left up to the simulator and so different simulators may simulate it differently.

You could write code like this and, because the simulator picked the 'right' order to execute the processes, you might get the right answer and never even notice your design has a race. But then, you might run the code on a different simulator (or even a new version of the same company's simulator) and it could produce different results. The race was always there - you just got lucky because the process execution order in the original simulator did what you wanted.

This is the classical testbench-DUT race.

In VHDL, you may have never encountered this because VHDL does not have blocking assignments like Verilog does. All signal assignment statements are non-blocking.

VHDL actually does have the ':=' operator, which behaves similarly to Verilog's '=', but these can only appear inside processes and only affect variable values, which are different than signals. Thus, in VHDL the problem in the code above cannot happen.

In addition to this kind of race between your testbench and DUT, you can create races within your DUT code just as easily. You can do this if you mix non-blocking and blocking assignments to signals in your DUT code. One suggestion that is popular is to never mix blocking and non-blocking assignments within a single process. However, if your DUT code has lots of processes and some use blocking and some use non-blocking, you can still get races and they can be very, very difficult to find!

If you are a VHDL designer you may be feeling smug and thinking that VHDL has no races. Not so! But they are more rare. Here is a case where you can observe a similar race in VHDL. Although the code below is Verilog code, you could easily create a similar structure in VHDL in a testbench.

  // Another racy design which CAN be reproduced in VHDL
  module race2();
    bit clk;
    always
      clk <= #5 ~clk;
     
    always @(posedge clk) 
      $display("first Random number is %d",$random()); 
  
    always @(posedge clk) 
      $display("second Random number is %d",$random());
     
  endmodule // race2

In this code, there are no '=' assignments. However, one of the two processes with the $display statements has to execute first and so the order of the output generated will depend on which process was executed first. Thus, order of execution can matter in VHDL. However, when using VHDL if you apply simulus in a clocked process and your DUT consumes it in clocked processes, no race can occur as in the testbench-DUT race described above.

Solving the Testbench-DUT Race Condition

At this point you probably already have a suggested solution: don't use blocking assignments! This is reasonable advice. However, this is NOT standard Verilog coding style. It is quite common to use blocking assignments in test bench code - it simply makes it easier to write in many cases (it looks and behaves like normal software when you do). But, you may also run across DUT code using blocking assignments regularly, often mixed together with non-blocking assignments in the same design. If you search the internet, you will find that different people/organizations have different recommended coding styles to avoid the race. In other cases you will find what looks like the designer monte-carlo-ed the test bench and design until he got something that seemed to work but which, in reality, is a hack.

One common way of of writing testbench code is to write it in a sequential style as in this testbench which generates a fibbonacci sequence of values on the 'c' signal:

  module testbench();
    
    wire[15:0] a, b, c;
  
    initial begin
      a = 0;
      b = 1;
      c = a+b;
    end
    
    always @(posedge clk) begin
      a = b;
      b = c;
      c = a+b;
    end
    
  endmodule

Using sequential code like this makes a lot of things easier to write for many people (it is more like C and Java) but can create a race when the 'c' value is driven into a DUT.

Verilog Design Practices to Avoid Testbench-DUT Races

I have seen stimulus code like this:

     // Stimulus - drive data into the DUT
     initial begin 
        @(posedge clk) d = #1 1; 
        @(posedge clk) d = #1 0; 
     end 

What this does is ensure that the new value of 'd' arrives 1 time unit after the clock edge, guaranteeing that DUT processes that are sampling 'd' on a clock edge will have done so before the new value arrives. This is a standard delayed signal assignment. As written, the #1 might be 1ns or 1ps, depending on what the timescale has been defined to be.

Another option I have seen is this:

     // Stimulus - drive data into the DUT
     initial begin 
        @(posedge clk) d = #0 1; 
        @(posedge clk) d = #0 0; 
     end 

What in the world is a #0 delay - is it really 0ns? It causes the assignment to take place a delta delay later and might (but might not exactly) approximate a non-blocking assignment using <=, so why not just use <=?

Another common option (which I tend to like) is this:

     // Stimulus - drive data into the DUT
     initial begin 
        @(negedge clk) d = 1; 
        @(negedge clk) d = 0; 
     end 

Here, the driving of new stimulus is on the opposite clock edge and so cannot interfere with the DUT, which operates on the other clock edge. However, if you have monitors such as:

     // Monitor output of DUT
     always @(negedge clk)
       $display("Value of D: %b", d);

there will still be a race possibility between the simulus and monitor processes which you would have to deal with. But, at least all the races will be within the test bench (as if that is a big improvement).

Another approach might be to do stimulus like this:

  always begin
    @(posedge clk) begin
      a = b;
      b = c;
      c <= a+b;
    end

That is, do all your stimulus computations in a serial mode if you like using blocking assignments. BUT, all values which must traverse wires to get to the DUT are assigned values using non-blocking assignments, thus eliminating the race. This is reminiscent of the VHDL style of using variables in processes to perform a computation followed by a <= assignment to a signal to convey the result of the computation to the rest of the circuit. Just be careful: if you then try to use signal c's value after the <= assignment above, remember it will not have changed for a delta delay so, in the end, it isn't really like C-code at all. As you create and edit and moved around code in your testbench it can be easy to end up getting caught by this and so, as good as this method looks, I recommend you don't use this method without creating a set of rules for coding style using it.

Final Thoughts

When doing simulation with stimulus values changing on the rising edge of the clock, your waveforms may be difficult to interpret for a number of reasons. In your waveforms, it will look as if the stimulus changed at the same time the DUT outputs changed. Are those DUT outputs in response to the changed inputs or the old inputs?

The kinds of timing diagrams we use in digital design courses look very different from these simulation waveform traces - they show the inputs changing well before the rising edge of the clock to give time for the input forming logic to function and to accommodate the setup time of the flip flop. If both stimulus and DUT outputs change at the clock edge, when do you monitor the outputs to check correctness? There is no time in the waveform that shows the stimulus values and then what the DUT did after the next clock edge.

In light of that, using the negative edge of the clock or some other method to move the timing of applying stimulus away from the rising edge of the clock will make your waveforms easier to understand and provide a time when your monitor process can sample both inputs and outputs and determine correctness. In Verilog, the negative edge of the clock is a common way to handle this. In SystemVerilog, something called clocking blocks can be used to allow you even more control over this - we will learn how to use those in a few weeks. For now, just be aware of races and choose a coding methodology to eliminate them.


Originally created by Brent Nelson, 2012.

Clone this wiki locally