A snippet appears. The clock is ticking. Can you find the bug before time runs out? 10 rounds of real RTL traps — latches, blocking assignments, width mismatches and more.
The game above is built from the bugs that actually bite in real RTL — the ones reviewers hunt for and interviewers love to ask about. Here's a reference for each, with the symptom and the fix. Learn these and you'll catch most of them on sight.
✗ always @(posedge clk) q = d;
✓ always @(posedge clk) q <= d;
In clocked logic, use non-blocking (<=). All right-hand sides evaluate first, then update together at the edge — modelling real flip-flops. Blocking (=) in sequential code causes simulation race conditions and sim/synthesis mismatch, especially across multiple always blocks.
✗ always @(*) y <= a & b;
✓ always @(*) y = a & b;
The mirror image of bug 1. Combinational blocks should use blocking (=) so values propagate immediately within the block. Non-blocking in comb logic can cause extra simulation deltas and confusing behaviour.
if✗ always @(*) if (sel) y = a; (no else)
✓ add else y = b; or a default y = b; at the top
In combinational logic, if an output isn't assigned on every path, the synthesizer infers a latch to hold the old value. Latches are almost always unintended and cause timing headaches. Assign every output on every branch.
case✗ a case with no default that doesn't cover all values
✓ add a default: branch (or default assignments before the case)
Same root cause as #3, in a case statement. Always include a default, or pre-assign outputs above the case so they have a value on every path.
✗ always @(a or b) y = a & b & c; (c missing)
✓ always @(*) y = a & b & c;
Every signal read in a combinational block must trigger it. A missing signal makes simulation disagree with the synthesized hardware. Use always @(*) (or always_comb in SystemVerilog) to include them automatically.
reg✗ reg y; assign y = a & b;
✓ wire y; assign y = a & b; (or drive y in an always block)
assign drives a net (wire); a reg is driven procedurally inside an always block. Mixing them is a compile error or a sign of confused intent.
reg/logic✗ output y; always @(*) y = ~a;
✓ output reg y; (or output logic y;)
Anything assigned inside an always block must be a variable (reg in Verilog, logic in SystemVerilog), including module outputs.
✗ always @(posedge clk or rst_n)
✓ always @(posedge clk or negedge rst_n)
An asynchronous reset must be edge-sensitive in the sensitivity list. Listing the bare signal is illegal/ambiguous — specify negedge (active-low) or posedge (active-high).
✗ assign y = a; assign y = b;
✓ one continuous driver per net (use a mux: assign y = s ? a : b;)
Two continuous assignments fighting over one wire produce contention (X) in simulation and are illegal in synthesis. A net needs exactly one driver.
✗ reg [3:0] cnt; if (cnt == 16) ... (4-bit max is 15)
✓ size signals correctly; watch carries: a 4-bit + 4-bit needs a 5-bit sum
Truncation and impossible comparisons are silent killers. Mind the bit-widths of sums (carry-out), comparisons, and concatenations.
Want to test code for real? Run it in the browser Verilog Simulator or check it with the Verilog Lint tutorial. New to RTL? Start with Digital Electronics and the FSM Designer.
Non-blocking (<=) for sequential (clocked) logic; blocking (=) for combinational logic. Mixing them up is the #1 RTL bug.
An output in combinational logic not assigned on every path (if with no else, or case with no default). Fix by assigning on all paths.
A combinational block that doesn't list every signal it reads, causing sim/synthesis mismatch. Use always @(*).
Related: Verilog Simulator · Verilog Lint · FSM Designer · VLSI