SystemVerilog Race Condition Challenge Responses
As promised, here is my response to Siemens EDA’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 🧔🏻
Comments