Nearly every bug you'll hit for the next several months is one of five things. Three you've already met; two are new.
An always @(*) block with a path that doesn't assign every output. Fix: assign a
default value to every output before any conditional logic.
Blocking (=) in a clocked block, or non-blocking (<=) in a
combinational one. Fix: @(posedge clk) → always <=;
@(*) → always =.
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.
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.
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.
| 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 |
When a simulated output is stubbornly X, work backward through this checklist, in order —
it resolves the overwhelming majority of cases:
reg that's never been assigned yet on this
code path? (Lesson 2 — uninitialized state.)always @(*) block have a path — an if with no else,
or a case with no default — that skips assigning it? (Lesson 3.)assigns, or two always blocks?
(Pitfall 3, above.)X at time 0, before the first clock edge, is often completely expected.)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?always @(sel or a) where the block also reads b. What's the most likely cause?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:
logic (Lesson 2),
always_comb/always_ff (which make the combinational/sequential rules from
Lessons 3–4 into compiler-enforced constructs instead of conventions), interfaces, and much richer
verification features. Many modern courses teach this dialect directly.For deeper, high-quality material beyond this crash course — canonical tutorials, the full Cummings paper, and curated YouTube channels — see the references page.
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.