SystemVerilog Race Condition Challenge Responses

As promised, here is my response to Mentor’s SystemVerilog Race Condition Challenge


Race #1 Blocking and non-blocking assignments

  byte slam;
  bit dunk;
  initial begin
    forever begin
      @(posedge clk);
      dunk = ~dunk;
      slam += dunk;
    end
  end
  always @(posedge clk) basket <= slam + dunk;

Race #1 must be the number one most common race condition in Verilog/SystemVerilog. Hardware designers may be more familiar with this race, but verification engineers must deal with this as well. When you have multiple threads or processes running in parallel and they are all synchronized to the same event (a clock edge), there is a race between reading the old value or the updated value from a blocking assignment. That’s why you must always use a non-blocking assignment when one process writes, and another process reads the same variable synchronized to the same clock. Then you’re guaranteed to consistently read the previous value.  Most of the code examples here have this problem in common


Race #2 Unknowns at initialization

 logic pong;
  initial begin
    fork
      forever begin
        @(posedge clk);
        if (pong) ping = 0;
        else      ping = 1;
      end
      forever begin
        @(posedge clk);
        if (ping) pong = 0;
        else      pong = 1;
      end
    join_none
  end

This is basically the same as Race #1. Also, the way this is coded, the unknown values get treated the same as if they were 0. That’s not always the case if you tried to implement this using ping = !pong; Then everything would remain unknown.


Race #3 Procedural and continuous assignments

byte colours, stripes, bouncy;
always @(posedge clk) stripes += 1;
always begin
  @(posedge clk);
  colours += 1;
  beach_ball = bouncy;
end
assign bouncy = colours && stripes;

Continuous assignments behave as independent processes, and there’s no deterministic order of execution between any process. Whenever right-hand side operands of a continuous assignment change, there’s an assignment to the left-hand side. But if another process is making changes to those operands there’s no guarantee when the left-hand side updates either before or after the procedural assignment to beach_ball. Note that all the accumulate assignment operators like += or ++ are just shortcuts for blocking assignments. There are no non-blocking equivalent shortcuts, so you must expand it out:

always @(posedge clk) stripes <= stripes + 1;

Race #4 Incomplete sensitivity list

  bit score;
  byte fieldgoal, touchdown;
  byte down;
  always @(posedge clk) begin
    if (down < 4) begin
      score <= 1;
      fieldgoal <= fieldgoal + 3;
      touchdown <= touchdown + 7;
      down <= down + 1;
    end
    else begin
      score <= 0;
      down <= 0;
    end
  end
  always @(score) football = fieldgoal + touchdown;

I wouldn’t call this a race, just bad coding. In the earliest versions of Verilog, it was up to you to figure out the sensitivity list for an expression to mimic the behavior of a continuous assignment in an always. Even though this code makes an assignment to score every clock cycle, its value only changes for one half the clock cycles. So, it misses changes to fieldgoal and touchdown. That changed in Verilog-2001 with always @* and further improved with always_comb in SystemVerilog


Race #5 fork/join* that don’t consume time

  bit shot_put;
  bit javelin;
  initial begin
    @(posedge clk);
    fork
      shot_put = $random();
      javelin = $random();
    join_none
    @(shot_put or javelin)
      throw = (shot_put || javelin);
  end

This is not a race. The processes inside a fork/join_none are not supposed to start until its parent process suspends or terminates. This code will calculate throw based on the initial values for shot_put and javelin, which are 0, not the values from $random(). Finally, stop using the deprecated $random() with its poor distribution/stability, and switch to $urandom_range(1).


Race #6 assignments in more than 1 thread

  bit single, double, triple, homerun, cycle;
  initial begin
    forever begin
      @(posedge clk);
      single = $urandom_range(0,10) > 1;
      double = $urandom_range(0,10) > 2;
      triple = $urandom_range(0,10) > 4;
      homerun = $urandom_range(0,10) > 8;
    end
  end
 
  initial begin
    forever begin 
      #1 cycle = & { single, double, triple, homerun };
      @(posedge clk);
    end
  end
  always @(posedge clk) begin
    #1 baseball = baseball + homerun;
    if (cycle) begin
      #0 cycle = 0;
    end
  end

I cringe whenever I see #0 or #1 sprinkled in code. It usually means the coder did not understand SystemVerilog scheduling semantics well enough and throws these in. In this case the race has been moved one timeunit (#1) away from the clock edge. The assignment to cycle occurs simultaneously to its reading in the always block.


Race #7 Edge sensitive events

  bit [3:0] goal;
  always @(posedge clk) goal += 2;
  initial begin
    @(posedge clk);
    @(goal == 2) hockey = 1;
  end

This is another form of Race #1. One process is writing, and another process is reading the same variable. But here the read is an event control waiting the expression (goal==2) to change. So, depending on the ordering between the initial and always blocks, it either catches the rise from false(1’b0) to true(1’b1) on the first clock cycle, or the fall from true to false in the next cycle. Always use non-blocking assignments between synchronous processes as I explained in Race #1. And it is rare to use an expression in an edge sensitive event. You should use the following:

@(posedge clk iff (goal==2));

Race #8 Named events

  event wicket, batsman;
  initial begin
    @(posedge clk);
    fork
      forever begin
        @batsman;
        repeat (cricket+1) @(posedge clk);
        ->wicket;
      end
      forever begin
        ->batsman;
        @wicket;
        cricket += 1;
      end
    join_none
  end

Named events are synchronization objects that can suffer from the same kinds of problems as in the previous Race #7. You must be waiting for an event before triggering it. If the second forever block triggers batsman before the first forever block starts execution, you get into a deadlock where the first forever block is deadlocked waiting for an event that was already triggered. You can solve this particular race by using non-blocking triggers ->> to batsman and wicket. I would make sure you have a very good understanding of SystemVerilog’s event scheduling algorithm before using named events.


Race #9 NBA functions

  bit [3:0] bump, spike, side_out;
  function bit [3:0] set(bit [3:0] _bump);
    set <= _bump ^ (_bump << 1);
  endfunction
  initial begin
    forever begin
      @(posedge clk);
      bump += 1;
      spike = set(bump);
      side_out = bump < spike ? spike : 0;
    end
  end

This is a race where you always lose. If you use non-blocking assignments to the return of a function, or to the output arguments of any function or task, the current value gets copied out before the NBA has a chance to update the value. You must use blocking assignments here.


Race #10 Procedural force/release

  byte pushups, situps, reps;
  assign situps = 3 + pushups;
  always @(posedge clk) reps <= reps + 1;
  always begin
    @(posedge clk);
    pushups += reps;
    force situps = 0;
    @(posedge clk);
    release situps;
    muscles = pushups + situps;
  end
  initial #300 $finish();
endmodule

This is the same as Race #3. All continuous assignments are independent concurrent processes. You cannot depend on their order of execution within the same time region.


And that’s the conclusion of our little race condition challenge. I hope you were able to get something out of it. By the way, these code fragments all come from real customer designs that we’ve seen in the last year or so.

 

-dave_rich 🧔🏻

Leave a Reply