Back to Learn

Learn / Concept

Synthesis vs simulation mismatch

A synthesis/simulation mismatch is RTL that means one thing to the simulator and a different thing to the synthesis tool. It is one of the most painful bug classes in digital design precisely because the design passes a clean functional regression and then the fabricated gates behave differently — there is no testbench failure to chase, because the testbench saw the simulation semantics, not the gate semantics. This guide covers the four constructs that cause most of these mismatches: incomplete sensitivity lists, blocking assignments in clocked blocks, casex/casez, and the full_case/parallel_case pragmas.

Why simulation and synthesis can disagree at all

A simulator executes the RTL as a sequential program with event-driven scheduling: sensitivity lists decide when a block re-runs, blocking and non-blocking assignments decide ordering within a time step, and four-valued logic carries x and z. Synthesis ignores most of that procedural detail and infers the structural hardware the code describes — gates and flops — from a different set of rules. When the two rule sets imply different behaviour for the same source, you get a mismatch. The dangerous mismatches are the silent ones, where both tools accept the code without an error.

Incomplete sensitivity list

A combinational always @(…) block with an explicit sensitivity list that omits a signal it reads will hold a stale value in simulation: the block only re-evaluates when a listed signal toggles. Synthesis ignores the list and builds combinational logic that responds to every input. The fix is to never hand-maintain the list — use always @* or, in SystemVerilog, always_comb, which are complete by construction and also let the tool warn if a path leaves an output unassigned.

// SSM001: incomplete sensitivity list.
// 'b' is read but not listed, so simulation only re-evaluates when 'a'
// changes and holds a STALE 'y' when 'b' moves. Synthesis builds full
// combinational logic and reacts to both. Sim and gates disagree.
always @(a) begin            // BUG: should be @(a or b) or @*
  y = a & b;
end

// Fix: let the tool complete the list for you.
always @* begin              // or always_comb in SystemVerilog
  y = a & b;
end

Blocking assignment in a clocked block

Blocking (=) assignments take effect immediately, so a later statement in the same block sees the newly written value during that evaluation. Inside a clocked block the hardware is a set of registers that all sample their inputs on the same edge, so the gates use the old values. The result is a sim/synth ordering hazard whose symptom depends on the order you wrote the statements. The rule is mechanical: non-blocking (<=) in clocked logic, blocking in combinational logic. The deeper treatment is in blocking vs non-blocking.

// SSM002: blocking assignment in a clocked block.
// Blocking '=' takes effect immediately, so the read of q0 below sees
// the NEW value within the same evaluation in simulation, while the
// hardware these statements describe is two parallel registers.
always @(posedge clk) begin
  q0 = d;        // BUG: use <= in clocked logic
  q1 = q0;       // sim: q1 gets the new d; gates: q1 gets the OLD q0
end

// Fix: non-blocking in sequential logic gives the intended pipeline.
always @(posedge clk) begin
  q0 <= d;
  q1 <= q0;
end

casex / casez wildcards and full_case / parallel_case

casex treats x and z bits in the selector as don’t-cares, and casez does the same for z and ?. Because silicon never carries an x, the wildcard matching can land differently in the gates than it did in simulation. These constructs are frequently intentional, so they are worth a review, not an automatic rejection.

The full_case and parallel_case pragmas are higher-risk, because the simulator ignores them and synthesis trusts them. full_case lets synthesis assume every reachable selector value is listed and drop the default/latch logic; parallel_case lets it assume the items are mutually exclusive and build a flat mux instead of a priority structure. If either assumption is wrong, an unlisted or overlapping selector value behaves one way in sim and another in silicon. Modern practice is to avoid the pragmas and write an explicit default.

// SSM004 / SSM005: wildcard case + a full_case/parallel_case pragma.
// casez treats z and ? in the selector as don't-care. The pragma tells
// synthesis to trust that the listed items are exhaustive and exclusive
// and prune the priority/default logic; simulation ignores the pragma.
// On a selector value the designer did not anticipate, sim and silicon
// can disagree.
casez (sel) // synopsys full_case parallel_case
  3'b1??: out = a;
  3'b01?: out = b;
  3'b001: out = c;
endcase

What a structural mismatch analyzer sees

A structural synth/sim analyzer works on the parsed RTL, not on waveforms, and it reports each construct as evidence to review with a file and line — it never claims the design is correct or signed off. It flags an explicit sensitivity list that omits a read signal, a blocking assignment in a clocked block, a non-blocking assignment in a combinational block, a casex/casez wildcard, and a full_case/parallel_case pragma. What it does not do is prove behaviour: it cannot tell you a flagged wildcard is wrong, only that it is a known mismatch site worth a human decision. Constructs that another sound checker already covers (a missing-else inferred latch, for example) are deliberately left to that checker rather than double-reported.

How ChipVerify AI helps

ChipVerify AI (a pre-signoff RTL evidence tool, currently in closed beta, not a foundry signoff tool) runs a deterministic structural synth/sim mismatch analyzer over uploaded RTL and lists each of these constructs by file and line, with the severity and the reason it can diverge. It is the first pass that catches exactly the bugs a green simulation hides, and it ties back to the related concept pages so you can confirm the underlying cause.

Related reading

Try the public scanner at /tinytapeout or request access.

FAQ

Why does an incomplete sensitivity list pass simulation but break in silicon?

A simulator only re-evaluates an always @(list) block when a signal in the list changes, so a signal that is read but missing from the list holds a stale value in simulation. Synthesis ignores the sensitivity list entirely and builds full combinational logic that reacts to every input. The two therefore compute different functions: sim shows the stale value, the gates show the live one. Using always @* or always_comb removes the hazard because the sensitivity list is complete by construction.

Are casex and casez always bugs?

No — they are often intentional, which is why a structural analyzer surfaces them at low severity for review rather than flagging them as errors. The risk is that simulation treats x or z bits in the case selector as don't-care wildcards, while real silicon never carries an x and synthesis maps the wildcards differently. An unanticipated selector value can then behave differently in the gates than it did in simulation. casez is generally safer than casex because it only treats z (and ?) as wildcards.

What do full_case and parallel_case pragmas actually do?

They are assertions to the synthesis tool, not the simulator. full_case tells synthesis that every reachable selector value is listed, so it may drop the latch/default logic for the unlisted cases. parallel_case tells synthesis the case items are mutually exclusive, so it may build a flat mux instead of a priority structure. Simulation ignores both pragmas and uses ordinary case semantics. If either assertion is wrong, the gates behave differently from the sim on an unlisted or overlapping selector value — a classic mismatch.

Find synth/sim mismatches in your RTL

Sign in and point ChipVerify AI at your Verilog or SystemVerilog. It runs a deterministic structural analyzer that names incomplete sensitivity lists, blocking-in-clocked, casex/casez, and full_case/parallel_case by file and line — pre-signoff evidence to review, not a foundry signoff.