Verilog Crash Course Lesson 8 / 8
Lesson 8 · Wrap-Up

Common Pitfalls & Where to Go Next

Learning objective Consolidate the small set of rules that cause almost all early Verilog bugs, learn one new one (multiple drivers), and know what to reach for once this crash course ends and your coursework goes deeper.

Nearly every bug you'll hit for the next several months is one of five things. Three you've already met; two are new.

1 & 2 — Latch inference and blocking/non-blocking (recap)

Latch inference (Lesson 3)

An always @(*) block with a path that doesn't assign every output. Fix: assign a default value to every output before any conditional logic.

Wrong assignment type (Lesson 4)

Blocking (=) in a clocked block, or non-blocking (<=) in a combinational one. Fix: @(posedge clk) → always <=; @(*) → always =.

3 — Multiple drivers on one signal

A wire (or a reg assigned in more than one procedural block) can only have one thing deciding its value. Drive it from two places and you get contention:

assign y = a;
assign y = b;   // y now has two drivers

Whenever a and b disagree, y resolves to X — Lesson 2's "unknown" value, here caused by a genuine conflict rather than an uninitialized signal. The same problem, with different symptoms, happens if you assign the same reg from two separate always blocks: instead of a clean electrical conflict, you get a race between the blocks that most simulators will resolve consistently but which is not something you should ever rely on. Cummings' guidelines (Lesson 4) state this directly: never assign the same variable from more than one always block. If you find yourself wanting to, it usually means the logic belongs in one block, or you need a multiplexer deciding which source wins, explicitly, in your own code.

4 — Trusting the old explicit sensitivity list

Pre-2001 Verilog required spelling out an always block's triggers by hand: always @(a or b or sel). Forget one signal, and simulation only re-evaluates the block when the signals you did list change — producing stale, wrong values in the simulator. But a synthesis tool doesn't simulate; it just builds combinational logic from every signal actually read in the block, forgotten or not. The result is a design that simulates wrong but synthesizes correctly (or occasionally the reverse) — a mismatch that's miserable to track down because each tool insists it's right. always @(*), which you've used since Lesson 3, computes the sensitivity list automatically and makes this entire failure mode impossible. There's no remaining reason to write an explicit combinational sensitivity list by hand.

5 — Signed vs. unsigned surprises

Every wire/reg is unsigned by default, even if you're using it to represent a signed quantity. Comparisons and arithmetic on plain vectors use unsigned rules unless you explicitly declare signed:

wire signed [7:0] a = -8'sd1;   // a = 8'b11111111, and Verilog now treats it as -1
wire [7:0]        b = -8'd1;    // b = 8'b11111111, but treated as 255 (unsigned)

The bit pattern is identical in both cases — signed only changes how comparisons, right-shifts, and arithmetic interpret that pattern. If a comparison mixes a signed and an unsigned operand, Verilog silently treats the whole expression as unsigned, which quietly reintroduces the exact bug you thought signed had fixed. Rule of thumb: if any operand in an expression needs to be signed, every operand in that expression should be declared signed, or you should assume unsigned semantics are actually what's happening.

Quick reference: what can and can't be synthesized

Synthesizable (real hardware)Simulation-only (never in your design)
assign, always @(*), always @(posedge clk) initial (except limited memory-init use in some flows)
if/case inside a procedural block # delays
parameter, localparam $display, $monitor, $strobe
generate/genvar/for $dumpfile, $dumpvars
Module instantiation $finish, $stop, force/release

Debugging technique: chase the X

When a simulated output is stubbornly X, work backward through this checklist, in order — it resolves the overwhelming majority of cases:

  1. Is the signal (or something feeding it) a reg that's never been assigned yet on this code path? (Lesson 2 — uninitialized state.)
  2. Does an always @(*) block have a path — an if with no else, or a case with no default — that skips assigning it? (Lesson 3.)
  3. Is it driven from two places at once — two assigns, or two always blocks? (Pitfall 3, above.)
  4. Has a clocked register simply not been reset or clocked yet at the point you're checking? (An X at time 0, before the first clock edge, is often completely expected.)
Diagnostic 1 — always @(*) y = a & b; exists in one always block, and a second, separate always @(*) y = c | d; exists elsewhere in the same module, both targeting the same reg y. What's wrong?
y has two drivers (two separate always blocks assigning it) — a race condition, not something with a well-defined single answer. This will infer a latch because the case isn't complete. This is a blocking/non-blocking mismatch.
Diagnostic 2 — a design simulates correctly with Icarus Verilog but a colleague's synthesis run produces different behavior on the FPGA. Both used an always block with an old-style explicit sensitivity list, e.g. always @(sel or a) where the block also reads b. What's the most likely cause?
FPGAs are inherently less reliable than simulation and this kind of mismatch is unavoidable. The missing signal (b) in the explicit sensitivity list makes the simulator use stale values when only b changes, while synthesis still builds logic sensitive to b — always @(*) would eliminate this mismatch entirely. The FPGA is running at a different clock frequency than the simulation.

Where this crash course ends, and your course likely continues

This covers what most intro digital design courses lean on for the first several weeks of Verilog labs. A few directions worth knowing exist, even without going deep on them yet:

For deeper, high-quality material beyond this crash course — canonical tutorials, the full Cummings paper, and curated YouTube channels — see the references page.

Capstone exercise Take every module you've written across this course — full_adder, mux4, traffic_light_fsm — and run each through a synthesis tool if you have access to one (Yosys is free and works from the command line). Read every warning it prints. If you followed the guidelines in Lessons 3, 4, and this lesson, you should see zero inferred-latch warnings and zero multiple-driver errors — if you do see one, you now have everything you need to diagnose exactly why.
Previous← Parameters & Generate Blocks Contents⌂ All lessons