SystemVerilog has a secret it does not print on the box: it is two languages wearing one syntax. One dialect — the synthesizable subset — describes hardware that a synthesis tool can turn into gates. The other dialect exists only in simulation: delays, test scaffolding, printing, spawning parallel processes. Both dialects compile; both simulate; only one becomes silicon. Nothing in the language stops you writing a design that simulates like a dream and synthesizes into garbage — or into nothing at all. Knowing exactly where the border runs is what separates people who write RTL from people who write wishes.
| Construct | Synthesizable? | Why |
|---|---|---|
assign, always_comb | yes | gate networks |
always_ff @(posedge clk) | yes | flip-flops |
if/case, operators, slices | yes | muxes and logic |
module instances, parameter, generate | yes | structure, frozen at elaboration |
#10 delays | no | silicon has no delay knob — timing comes from physics |
initial blocks | no | "run once at time zero" — that is a testbench's job |
$display, $error, $random | no | simulator services; there is no console in a chip |
fork…join, wait, file I/O | no | simulation processes, not hardware |
The sim-only rows are not defects — they are the verification half of the language, and the next module lives there happily. The sin is not using them; the sin is using them in the design and believing the simulation.
Four failure modes account for most synthesizability grief. You have met the first already:
1. Inferred latches. A combinational block that fails to assign an output on some
path forces the tool to build memory you never asked for. The cure is mechanical — a default
assignment at the top of the block, an else for every trailing if, a
default arm in every case — and always_comb warns you when
you slip.
2. Simulation/synthesis mismatch. The simulator and the synthesizer can honestly
disagree about what your text means. The classic sources: a legacy always @(a) block
that reads b too — the simulator only re-evaluates when a changes (stale
b!) while the synthesizer builds gates that respond to both; and zero-delay evaluation
order between blocks, which the simulator resolves by picking an order while real gates
just settle. Sprinkling #10 delays to "fix" a misbehaving design is the anti-pattern of
anti-patterns: it changes the simulation and leaves the hardware untouched.
3. X-propagation. Simulation's four-valued logic (0, 1, x, z) is an
honest fiction. Real silicon always holds a definite 0 or 1 — x means the
simulator does not know which. The fiction errs in both directions: x-optimism
(some constructs quietly swallow an x — a case falls to its default, an
if (x) takes the else — hiding a real bug the silicon will express), and
x-pessimism (an x spreads through logic that would in fact produce a
definite value: x & 0 is really 0). Treat any x reaching real logic as
radioactive; find the unreset register it leaked from.
4. Multiple drivers. Two always blocks assigning one signal is legal-ish
text describing two gate outputs soldered to the same wire — in silicon, a fight between a
transistor pulling high and one pulling low: a short circuit. One signal, one driving block,
always.
Here is trap 2 in miniature. Two combinational blocks: b = NOT a, and
y = a AND b. The hardware answer is eternally a changes can read a stale b:
Same description, two different simulated answers depending on an ordering the hardware does not
have. Real event-driven simulators are far cleverer than this naive one — but the lesson stands:
when your design's correctness depends on which block the simulator ran first, you have
described a race, not a circuit. (This is precisely why the iron rule from
<= in always_ff — exists: non-blocking assignment
removes the ordering from register updates.)
You do not police this border alone. A lint tool (open-source Verilator's lint mode, or the commercial linters every chip company runs) reads your RTL before simulation and flags the whole rogues' gallery: inferred latches, multiple drivers, width mismatches, incomplete cases, sim-only constructs in design files. Professional flows make lint-clean a precondition for even running a simulation. Treat the linter as your first code reviewer — the one who never gets tired and is never polite.
# delays, initial blocks or $-calls in design code;<= in always_ff, = in always_comb;
never legacy always @ with a hand-written sensitivity list;x as radioactive — find its source, don't paper over it;
Because a real gate's delay is not a number you choose — it is physics you suffer: a
function of the transistors' size, the wire lengths after placement, the supply voltage, the chip's
temperature, and manufacturing luck, varying part to part and hour to hour. That is why the
synthesizable world never says "wait 10 nanoseconds"; it says "wait one clock edge", and then a
separate discipline (static timing analysis, later in this course) proves that every combinational
path settles inside the clock period across all those conditions. The delays in your simulation are
cartoons for your convenience. When a design only works with a #10 in it, the
#10 is a confession: somewhere there is an ordering assumption the hardware will not
honour, and the fab will collect on it.
The deadliest sentence in chip design. Simulation checks the description as the simulator interprets it — including every sim-only construct, every x-optimism, every lucky evaluation order. Synthesis then builds a different interpretation: the subset, x-free, orderless. The overlap is large, and the industry's flows exist to make it total — lint, synthesizable-subset discipline, and later formal equivalence checks between RTL and gates. But the burden of proof runs one way: a clean simulation of undisciplined RTL proves nothing about the silicon. Chips have shipped with bugs that "could not happen" because they never happened in simulation — an unreset register that woke up differently on the bench than in the fab's silicon, a race between two always blocks that the simulator happened to order kindly. Discipline first; then simulate; then trust — a little.