This will be explained by writing a piece of code used to drive data to a D flip-flop.
A task in your driver that generates data will look something like this:
After running the simulation and looking at the waves the result is:
Notice anything wrong? The output follows the input, thus defeating the purpose of the flip-flop.Even though the RTL code is correct, the functionality seems wrong. Why?
This relates to SystemVerilog scheduling semantic and event regions. Simulator is not really parallel like hardware, and things are actually being calculated sequentially.
We have something called Active region set where most of our verification and RTL code is executed. This region is split into active, inactive, and non-blocking assignment regions.
Active is reserved for blocking and continuous assignments and for evaluation of the RHS (right-hand side) of non-blocking assignments, and for scheduling their change in the latter NBA region, based on current module activity.Inactive is reserved for #0 delays.NBA is used to update the values on the LHS of non-blocking assignments, based on the scheduling that happened in the active region.
So what happens in the driver if blocking assignment is used?
There is a race condition between the RTL and driver. If the intf.d = $urandom_range(1) is executed first, then the D is updated to a new value immediately. This new value is used to evaluate what the result of the non-blocking assignment in the RTL should be.
If NBA is used in driver, then the change in D is only scheduled to be executed in the NBA region. The RTL uses current module activity to evaluate the result, which is the “old” value of D at that point. The update of values then happens in the NBA region, resulting in D getting the new value, and Q following by using the old value of D.
To summarize: Do not make the mistake of using blocking assignments in a driver, as you will end up injecting races and getting the wrong simulation results. This is the way that the simulator is implemented.
Full example available here.