Search code examples
system-veriloguvm

Race condition with nonblocking assignment in UVM driver


I'm trying to execute for a synchronous driver, I aligned the driven signals to the clock edge as the below. I used nonblocking assignment to avoid race conditions in the UVM driver.

When I ran the driver, I got this printed out:

myCmd.start: 1
myCmd.start: 0

but, I expected as the below:

myCmd.start: 0
myCmd.start: 1

In the waveform viewer, I have checked the signal is correctly working. Only uvm_info() is not working correctly. If I add #1

#1; uvm_info(get_type_name(), $sformatf("myCmd.start:%h", my_master_vif.dut_command), UVM_LOW)

then I can print the correct value.

Is this not enough to handle the race condition for uvm_info() in the driver? How can I avoid this problem?

class my_master_driver_c extends uvm_driver#(my_frame_c);

  virtual my_if my_master_vif;
   //...
  task run_phase(uvm_phase phase);

    while (1) begin
      begin

        forever begin
          @(posedge my_master_vif.dut_clock_i iff (my_master_vif.reset))
          seq_item_port.get_next_item(req);
          drive_transfer(req);
          #10;
          seq_item_port.item_done(req);
        end
      end
    end
  endtask
  //...
  
  task drive_transfer (input my_frame_c trans);    
    
    @(posedge my_master_vif.dut_clock_i iff (my_master_vif.reset));
    my_master_vif.dut_command <= 0;
    `uvm_info(get_type_name(), $sformatf("myCmd.start:%h", my_master_vif.dut_command), UVM_LOW)
    
    @(posedge my_master_vif.dut_clock_i iff (my_master_vif.reset));
    my_master_vif.dut_command <= 1;
    `uvm_info(get_type_name(), $sformatf("myCmd.start:%h", my_master_vif.dut_command), UVM_LOW)
    //...

  endtask : drive_transfer

  //...

endclass                  

This is my interface:

interface my_if 
   (
    input clock_my, 
          reset,
    inout wire w_dut_command,
    output reg dut_clock_i,
    //...

    );

  timeunit 1ps;
  timeprecision 1ps;    

  assign dut_clock_i   = clock_my;

  logic dut_command = 'bz;
        assign w_dut_command  = dut_command;    

  clocking driver_cb @(posedge clock_my);
    default input #1 output #1;

    inout dut_command;

  endclocking    

endinterface : my_if

Solution

  • Point out an important problem, you are not doing synchronous driving, but just doing non-blocking assignment. Although synchronous drives use the same operator syntax as nonblocking variable assignments, they are not the same. Actually, you are not using your clocking block in your current code. To be precise, you just drive the interface signal asynchronously, but use clocking events for synchronization. Conceptually speaking, Synchronous drive only occurs at the output of the clocking block, In other words, you need to access the domain of the clocking block by my_master_vif.driver_cb.dut_command instead of my_master_vif.dut_command. The results of driving these two signals are different, you can try to drive them separately, and then view the waveforms of these two signals simultaneously in the simulation tool to compare. Ref SystemVerilog IEEE Std 1800-2017 14.16 Synchronous drives.

    In addition, as in the following structure, according to timing control, the execution of statement A and statement B will not theoretically occur at the same time, and there will be at least one clock cycle difference between them. There is no time information in the output of your question description.

    @(posedge clk);
    // ... ...
    // A statement
    // ... ...
    @(posedge clk);
    // ... ...
    // B statement
    // ... ...
    

    In your case, you can equate uvm_info to $display task. So actually your question becomes a question of who executes first between non-blocking assignment and $display task.

    dut_command <= 1;
    @(posedge clk);
    dut_command <= 0;                                      // ?
    $display("@%0t, dut_command: %h", $time, dut_command); // ?
    

    According to SystemVerilog's simulation scheduling semantics, this is unambiguous and there is no race. The $display is executed in the active region, and the update of the non-blocking assignment occurs in the NBA region. So the result will output dut_command: 1, refer to the picture below. If you want to print dut_command: 0, you can add #1 or replace $display with $strobe.

    dut_command <= 1;
    @(posedge clk);
    dut_command <= 0;
    $display("@%0t, dut_command: %h", $time, dut_command);       // 1
    // #1 $display("@%0t, dut_command: %h", $time, dut_command); // 0
    // $strobe("@%0t, dut_command: %h", $time, dut_command);     // 0
    

    Event scheduling regions

    For details to see SystemVerilog IEEE Std 1800-2017 chapter 4 Scheduling semantics and this paper: SystemVerilog Event Regions, Race Avoidance & Guidelines. In addition, race condition generally refers to situations that occur in the same event region and result in uncertain results. If you want to find a real case about race condition, you can also search related questions on SO, I have also answered a related Question, you can refer to the Answer.

    Finally, back to your question again, if the understanding is correct, your first transaction driver will output:

    myCmd.start: z
    myCmd.start: 0
    

    because your dut_command is initialized to 'bz. And each subsequent transaction driver will output:

    myCmd.start: 1
    myCmd.start: 0
    

    To avoid this problem, In addition to add #1 or replace uvm_info with $strobe, you can also just change all the non-blocking assignment to the blocking assignment like my_master_vif.dut_command = 1, which may be better solution.

    Even if you change to a synchronous drive my_master_vif.driver_cb.dut_command <= 1, the output will not change as you expected because the value of driver_cb.dut_command will change in the Re-NBA region by default. The read value of driver_cb.dut_command is still the last sampled value, and not the driven value just now, which is the feature of Synchronous drives. Therefore, if you drive the signal like this, you can refer to toolic's suggestion:

    It is more common to print information only at the start (or end) of a transaction, using req.print, for example, which shows all the desired properties of the transaction.