The Sim/Synth Mismatch Checklist
The previous two articles covered conventions. This one collects a class of bug that conventions exist to prevent.
What makes this class uniquely dangerous is worth stating plainly first: every pattern here produces a simulation that runs, passes the testbench and looks like a correct design, while the synthesized hardware does something different. You will not find these by running more tests. You find them by knowing the list and reviewing the RTL against it. You have met every item already, spread across five modules; this article only puts them together.
The checklist
1. casex treating x as a wildcard (Section 5.3). Simulation can match a branch during reset that synthesis, where x never exists on silicon, would never reach the same way. Fix: never use casex in new RTL; use casez, which only wildcards ? and z.
2. Incomplete explicit sensitivity lists (Section 5.2). A signal read inside a combinational block but left out of a hand-written sensitivity list makes simulation show stale values, while synthesis, which infers logic from the block’s content rather than its declared list, builds a circuit that responds to every input. Fix: always always @(*) for combinational logic, never a hand-written list.
3. full_case and parallel_case pragmas making false promises (Section 5.3). If the claim that every case is covered or that branches never overlap is false, simulation (which ignores the pragma) and synthesis (which trusted it) build different behaviour for the same inputs. Fix: skip the pragmas; write an explicit default and make branches genuinely mutually exclusive.
4. Unintended latch inference from incomplete if or case coverage (Section 5.2). Simulation shows “held value” behaviour that looks intentional if you are not watching for it; a latch synthesizes as a real level-sensitive storage element with timing behaviour a flip-flop-based design was never verified against. Fix: assign a default to every output at the top of every combinational block.
5. initial blocks used to preload registers meant for silicon (Section 5.1). Simulation initialises the register; synthesis by default ignores initial blocks except for specific memory initialisation contexts (Section 6.2’s $readmemh, FPGA block RAM). A design that works in simulation because of an initial preload can power up in an undefined state on hardware. Fix: every register that needs a known starting value needs an actual reset path.
6. Mixing blocking and non-blocking assignments to the same signal across always blocks (Section 5.1). The standard does not say which of two concurrent always blocks runs first in a time step. A particular simulator may resolve the race consistently and hide the bug, while synthesis, which has no notion of execution order between concurrent hardware, builds something that behaves according to the values. Fix: the Section 5.1 rules, <= in sequential blocks, = in combinational blocks, and no signal written by both.
7. Signed and unsigned mixing silently going unsigned (Section 7.2). A comparison or shift meant to be signed evaluates as unsigned the moment any operand is not declared signed, correct-looking for the test values used, wrong for values where the interpretations diverge. Fix: consistent signedness across every operand, and explicit $signed() or $unsigned() at the point of use when you must mix.
Why this list and not a longer one
Every item traces to the same root: a construct where Verilog’s own semantics leave room for simulation and synthesis to legitimately interpret the same code differently. Neither tool is misbehaving. The RTL was ambiguous in a way that happened to resolve favourably in simulation and differently in synthesis. That is exactly why this class passes review by less experienced engineers and passes clean simulation runs.
The one-sentence discipline
If the whole checklist had to be compressed into one habit: write RTL whose meaning does not depend on which tool is reading it. Every fix above is an instance of that. Use the construct with one unambiguous interpretation (casez over casex, always @(*) over a hand-written list, an explicit default over a pragma’s promise, a real reset over an initial shortcut, the =/<= discipline over simulator-specific race resolution, consistent signedness over an implicit reversion rule) rather than the construct that happens to give the intended behaviour in the tool you tested with today.
Lint tools check essentially this list automatically: Verilator’s --lint-only mode, or commercial tools like SpyGlass, flag casex, incomplete sensitivity lists and several of the others before a design reaches synthesis. Running a linter as a mandatory step before every review is one of the highest-leverage quality gates a team can add, because this class is otherwise invisible to both a reviewer skimming code and a testbench that passes.
Practice Exercise
For each fragment below, identify which checklist item it violates.
// Fragment A
always @(state)
next_state = (state == IDLE) ? ACTIVE : IDLE;
// Fragment B
initial q = 1'b0;
always @(posedge clk) q <= d;
Solution
Fragment A violates item 2, a hand-written sensitivity list. The block reads only state today, so the list happens to be complete, but it is meant to be combinational and using @(state) instead of @(*) means any signal the expression comes to depend on after a later edit will be silently missed. That is a maintenance trap as much as an immediate bug, and it is why the rule is “always @(*)” rather than “remember to list everything”.
Fragment B violates item 5, an initial block standing in for a reset. q shows 1'b0 from time zero in simulation, but on hardware with no reset path its power-on value is undefined until the first clock edge loads whatever d happens to be. A real design needs if (!rst_n) q <= 1'b0; else q <= d;.
Every item here shares one root cause, an RTL construct ambiguous enough for simulation and synthesis to land on different readings, and every fix shares one discipline: prefer the construct whose meaning is pinned down by the language rather than by the tool reading it. That closes Section 8.1. In the next article, Section 8.2 goes the other way: not avoiding synthesis surprises, but understanding what the synthesis tool builds from the RTL you already write correctly, so you can guide it deliberately.