Verilog · Chapter 8.4 · System Tasks & Functions
Conditional Simulation in Verilog — Compile-Time vs Run-Time Control
Conditional simulation is the discipline of controlling what the simulator builds and runs, such as debug instrumentation, testbench-only stimulus, and fast versus full regression modes, without changing the hardware the source describes. It rides on the same ifdef and ifndef directives that the conditional-compilation page drills, but the skill here is not the syntax; it is knowing which of three conditional layers a problem actually wants: a compile-time macro that pulls text in or out before compilation, an elaboration-time parameter that is per-instance and still real hardware, or a run-time if that is evaluated every cycle and always present in the netlist. Reach for the wrong layer and you get real RTL hidden behind a simulator flag, or an undefined macro that silently deletes a check so a regression reports green while testing nothing. This page is the judgement layer: what conditional simulation is, when each layer is correct, and the failure modes that ship sim-only bugs to silicon.
Foundation18 min readVerilogConditional SimulationifdefSimulation ControlVerificationBuild Targets
Chapter 8 · Section 8.4 · System Tasks & Functions
1. The Engineering Problem
A design passes every simulation. It is taped out. The silicon is dead on arrival — a FIFO overflows under back-pressure that the testbench exercised and the simulator never flagged a problem. The post-mortem finds one line:
`ifdef SIMULATION
// "temporary" full-flag fix while the real one is reviewed
assign full = (count >= DEPTH - 1);
`endifA junior had a real RTL bug — an off-by-one in the FIFO full flag — and wrote the fix inside a `ifdef SIMULATION guard so they could test it before the review landed. The review stalled. The guard stayed. In every simulation build the macro SIMULATION was defined, the fix existed, and the FIFO behaved. In the synthesis build the macro was not defined, the preprocessor stripped the assignment, and the netlist taped out with the original broken flag. Simulation and silicon ran different hardware.
This is the defining hazard of conditional simulation, and it is the mirror image of the Chapter 7.4 hazard. There, the danger was debug junk leaking into synthesis. Here, the danger is real logic vanishing from synthesis. Both come from the same root confusion: treating a compile-time text switch as if it controlled behaviour rather than which source the tool sees.
`ifdef does not mean "do this in simulation." It means "let this text exist in builds where the macro is defined." If that text is hardware, then a build without the macro is missing hardware. The skill this page teaches is recognising — every time you reach for a conditional — which layer you actually want, so simulation and synthesis describe the same chip.
2. What "Conditional Simulation" Actually Means
Conditional simulation is build and simulator control — not a design construct. It answers questions like:
- "How do I keep debug instrumentation in my sim runs but out of the netlist?"
- "How do I run the same testbench in a 5-minute smoke mode and an 8-hour soak mode?"
- "How do I compile in a behavioural reference model that exists only to check the DUT?"
Every one of those is about what the tool does with the source, not about what the chip does. That distinction is the whole topic. A clean mental test:
If a conditional changes the manufactured hardware, it does not belong in a
`ifdef SIMULATION. If it only changes what the simulator observes, controls, or compares, it does.
Conditional simulation is therefore primarily a verification and build-flow skill. It lives in testbenches, in shared debug headers, and in the thin instrumentation layer wrapped around RTL — never in the synthesizable description of the design itself. Chapter 7.4 taught the directive family as a build-target mechanism; this page narrows to the simulation-control application and the judgement that keeps it from corrupting the design.
3. Mental Model — The Three Conditional Layers
4. The Simulation View — Two Tools, Two Different Source Files
The reason conditional simulation works at all is that the simulator and the synthesizer read the same file through different macro sets, and therefore effectively compile different source.
| Construct | Simulator build (+define+SIMULATION) | Synthesis build (no SIMULATION) |
|---|---|---|
$display / $monitor / $strobe | present, runs | must be absent (gate it) |
| Behavioural reference model | present, checks the DUT | absent — not part of the chip |
| Testbench stimulus / clock gen | present | absent |
| Synthesizable RTL | present | must also be present ← never gate this |
| Assertions / coverage | present | absent (or tool-handled) |
The first three rows should differ between the two builds — that is the point of conditional simulation. The fourth row must never differ: if the synthesizable design is not byte-for-byte identical (in behaviour) across both builds, simulation is verifying a chip you are not manufacturing. Everything in this chapter is about keeping rows 1–3 conditional and row 4 unconditional.
5. Syntax — Assumed, Not Re-Derived
The five directives — `ifdef, `ifndef, `else, `elsif, `endif — and the +define+ command-line mechanism are drilled in Conditional Compilation (7.4). This page assumes them. The only syntactic point that matters here is the shape of a well-formed simulation guard:
// Observation and checking — present in sim, gone in synth.
`ifdef SIMULATION
always @(posedge clk)
if (rd_en)
$display("[t=%0t] read addr=%h data=%h", $time, addr, rdata);
`endif
// The design itself stays OUTSIDE every simulation guard.
always @(posedge clk)
rdata <= mem[addr]; // real hardware — unconditionalTwo rules carry the whole idiom: the guard wraps only the instrumentation, and the design sits outside it. Every pitfall in §12 is a violation of one of those two rules.
6. Compile-Time vs Run-Time — The Decision Framework
The single most common interview question on this topic is "when do you use `ifdef versus a normal if?" The answer is a two-question test.
Question 1 — Should this code exist in the manufactured chip?
- No (it is observation, stimulus, or a reference model) → it must be removable, so use the compile-time layer:
`ifdef SIMULATION. - Yes (it is part of the design's function) → it must always be present, so it cannot live behind
`ifdef. Use a run-timeifor an elaboration-timeparameter.
Question 2 — If it stays in the chip, does the choice vary per instance at build time, or per cycle at run time?
- Per instance, fixed at build → elaboration-time
parameter/generate(e.g.WIDTH,HAS_PARITY). - Per cycle, while running → run-time
if/case(e.g.if (mode == FAST)).
| What you are gating | Layer | Construct |
|---|---|---|
| Debug prints, assertions, coverage | Compile-time | `ifdef SIMULATION |
| Testbench-only stimulus / clock | Compile-time | `ifdef SIMULATION |
| Behavioural reference vs vendor primitive | Compile-time | `ifdef SYNTHESIS / `else |
| Per-instance design configuration | Elaboration-time | parameter / generate |
| Per-cycle operating-mode behaviour | Run-time | if / case |
The trap is answering Question 1 wrong: deciding "this is for testing, so `ifdef it" when the code is actually part of the design. The §1 FIFO fix failed exactly there — a real full-flag computation answered "yes, it's hardware" but was gated as if the answer were "no."
7. Macros vs Parameters — Stop Reaching for `ifdef
A second, quieter failure is using compile-time macros for things that elaboration-time parameters do better. They look similar — both configure a build — but parameters are almost always the right tool for design configuration.
// ANTI-PATTERN — a macro per width variant.
`ifdef WIDTH_8
reg [7:0] data;
`elsif WIDTH_16
reg [15:0] data;
`elsif WIDTH_32
reg [31:0] data;
`endif
// Needs a recompile per variant; only ONE width per compilation;
// two instances of different widths are impossible in one build.
// CORRECT — a parameter.
module buffer #(parameter integer WIDTH = 8) (
input wire [WIDTH-1:0] d,
output reg [WIDTH-1:0] q
);
always @(posedge clk) q <= d;
endmodule
// buffer #(.WIDTH(8)) and buffer #(.WIDTH(32)) coexist in ONE build,
// no recompile, no macro, fully synthesizable.The decision rule:
- Configuration of the design (widths, depths, feature toggles that ship in silicon) →
parameter(parameter, 6.1). Per-instance, no recompile, two variants coexist. - Presence of simulation-only code (does this debug block exist at all) →
`ifdef.
Reaching for `ifdef to configure hardware produces macro sprawl: a combinatorial blow-up of WIDTH_8 × HAS_PARITY × FAST_MODE macros, one recompile per combination, and the impossibility of two differently-configured instances in a single build. parameter exists precisely to avoid that. Conditional simulation uses macros; conditional hardware uses parameters.
8. Visual Explanation
Three figures anchor the model.
8.1 Visual A — the three conditional layers on a timeline
Three conditional layers — when each resolves and what it touches
data flow8.2 Visual B — the simulation/synthesis view split
8.3 Visual C — the decision matrix
9. Worked Examples
Three patterns cover almost all legitimate conditional simulation.
9.1 Tiered debug verbosity
A single source with three observation levels — silent, normal, verbose — selected at compile time so production synthesis sees none of it.
`ifdef SIMULATION
always @(posedge clk) begin
`ifdef DEBUG_VERBOSE
$display("[t=%0t] state=%0d a=%h b=%h result=%h",
$time, state, a, b, result);
`elsif DEBUG
$display("[t=%0t] state=%0d result=%h", $time, state, result);
`endif
end
`endifThree build targets from one file: silent (+define+SIMULATION), normal (+SIMULATION +DEBUG), verbose (+SIMULATION +DEBUG_VERBOSE). The synthesis build defines none of them and the whole block disappears. Note the nested guard — DEBUG_VERBOSE inside SIMULATION — so verbose logging can never accidentally exist in a non-simulation build.
9.2 Testbench-only conditional stimulus
Stimulus that should run only in certain test modes, gated so it never reaches any design build.
initial begin
apply_reset();
`ifdef INJECT_ERRORS
// Fault-injection stimulus — only in the error-handling regression.
force dut.parity_bit = ~dut.parity_bit;
#100 release dut.parity_bit;
`endif
run_main_sequence();
endforce / release are simulation-only constructs to begin with; gating them on INJECT_ERRORS means the default regression runs clean stimulus and a dedicated job (+define+INJECT_ERRORS) runs the fault campaign — same testbench source.
9.3 Reference model vs DUT primitive
The behavioural model exists only to check the DUT; the synthesizable path uses the vendor primitive.
`ifdef SYNTHESIS
// Silicon path — vendor hard macro.
spram_2kx32 u_mem (.clk(clk), .addr(addr), .din(din), .dout(dout), .we(we));
`else
// Simulation path — portable behavioural model + golden check.
reg [31:0] mem [0:2047];
always @(posedge clk) begin
if (we) mem[addr] <= din;
dout <= mem[addr];
end
`endifHere the conditional does swap part of the design — but legitimately, because both branches implement the same function and the choice is which implementation compiles, not whether a piece of logic exists. The discipline: when a `ifdef straddles real hardware, both branches must be functionally equivalent and synthesizable-or-modelled in parity. That is the one safe case of gating near the design, and it is exactly the case the §1 bug violated (its `ifdef had a real branch and an empty one).
10. Trace — Same Testbench, Two Builds
Conditional simulation has no waveform of its own; its observable effect is that the same run produces different logs under different defines. The trace below is one testbench compiled two ways.
$ vlog tb.v dut.v +define+SIMULATION +define+DEBUG_VERBOSE && vsim -c tb -do "run -all"
[t=10] state=0 a=0005 b=0003 result=0008
[t=20] state=1 a=0008 b=0002 result=000a
[t=30] state=2 a=000a b=00ff result=0109
TEST PASSED
$ vlog tb.v dut.v +define+SIMULATION && vsim -c tb -do "run -all"
TEST PASSED # DEBUG_VERBOSE undefined -> per-cycle lines gone
$ vlog tb.v dut.v && vsim -c tb -do "run -all"
# ** Error: tb.v: hierarchical reference into 'dut' — testbench stimulus
# was gated on SIMULATION and is now absent; nothing drives the clock.The third invocation is the teaching point: dropping SIMULATION removed the testbench's own driving code, so the run does nothing. That is correct for the design (a synthesis tool reading dut.v alone is exactly what you want) but a mistake if you expected to simulate without the macro. The macro set is part of the experiment; a result is only reproducible if the defines are pinned with it (§12.4).
11. Industry Usage
- Regression tiers. CI farms compile one testbench into smoke (
+SIMULATION), nightly (+SIMULATION +DEBUG), and soak (+SIMULATION +DEBUG +LONG_RUN) jobs. One source, three schedules. - Fault and corner campaigns.
+define+INJECT_ERRORS,+define+CLOCK_JITTERenable dedicated stimulus that the default run never sees. - Bring-up vs sign-off. Early bring-up builds carry heavy
$display/ assertion instrumentation; sign-off regressions strip verbosity for speed by dropping theDEBUGdefines — never by editing source. - Reference-model gating. Golden C/behavioural models compiled in under
`ifndef SYNTHESISto score the DUT, excluded from any build headed for a netlist. - Emulation / FPGA prototyping.
`ifdef FPGAswaps simulation-only constructs ($display, real-number models) for synthesizable equivalents so the same RTL runs on an emulator.
In every case the design RTL is untouched by the macros — only the surrounding observation, stimulus, and modelling layer is conditional.
12. Common Mistakes
Five mistakes, each a confusion of conditional layers.
12.1 Hiding synthesizable RTL behind a simulator flag
The §1 catastrophe. Any logic the chip needs — a flag computation, a state update, a corner-case branch — placed inside `ifdef SIMULATION exists in sim and vanishes in synth. Simulation verifies a chip you are not building. Rule: a `ifdef SIMULATION may contain observation, stimulus, or a reference model — never a line the design's function depends on.
12.2 Confusing compile-time `ifdef with run-time if
// WRONG — author expects to flip mode while the sim runs.
`ifdef FAST_MODE
assign latency = 1;
`else
assign latency = 4;
`endif`ifdef is resolved once, at compile time, for the whole build. You cannot change FAST_MODE mid-simulation, and you cannot have one instance fast and another slow. If the choice must vary per instance use a parameter; if it must vary at run time use if (fast_mode). `ifdef is the wrong layer for anything that is supposed to change while running.
12.3 Undefined macro silently deletes a check (false green)
`ifdef ENABLE_SCOREBOARD
if (dut_result !== expected) begin
$error("MISMATCH exp=%h got=%h", expected, dut_result);
fail = 1;
end
`endifIf the regression script forgets +define+ENABLE_SCOREBOARD, the entire comparison is stripped and the test passes by testing nothing — the most dangerous outcome in verification, because green is mistaken for correct. `ifndef makes the failure mode worse (typo the macro and the guard inverts). Defensive pattern: make the absence of a critical define loud —
`ifndef ENABLE_SCOREBOARD
initial $display("WARNING: scoreboard disabled — this run checks nothing");
`endif12.4 Inconsistent command-line defines across a regression farm
The same source gives different pass/fail results because two jobs were compiled with different +define+ sets — one machine had +define+RELAXED_TIMING, another did not, and a flaky test "passed here, failed there." Defines are part of the build's identity. Rule: pin the full +define+ set in the build script / filelist, store it with the results, and never pass defines ad hoc on an interactive command line for anything whose result you intend to trust.
12.5 Macros where parameters belong
Using `ifdef WIDTH_8 / WIDTH_16 / WIDTH_32 (or feature macros) to configure hardware instead of parameters. Symptoms: a recompile per variant, an explosion of macro combinations, and the impossibility of instantiating two configurations in one build. If the thing being configured ships in silicon, it is design configuration — use a parameter (§7), not a simulation macro.
13. Debugging Lab
Three conditional-simulation debug post-mortems
14. Coding Guidelines
- Never gate design logic. A
`ifdef SIMULATION(or any macro) may wrap observation, stimulus, or a parity reference model — never a line the chip's function depends on. If you would not delete it from the netlist, do not put it behind a simulation macro. - Answer "does this ship in silicon?" before choosing a conditional. No → compile-time
`ifdef. Yes →parameter(per instance) or run-timeif(per cycle). Never use`ifdeffor hardware that must always exist. - Configure hardware with parameters, not macros. Widths, depths, feature toggles that reach silicon are
parameters (6.1) — they avoid recompile-per-variant and let configurations coexist. - Make critical checks default-ON. Gate the disabling of a scoreboard or assertion, not its enabling, and print a loud warning when one is disabled — so a green run can never be mistaken for a checked run.
- Pin the
+define+set with the source. Defines belong in a committed filelist / build config consumed by every environment, never passed ad hoc — otherwise results are not reproducible. - Nest narrowly. Put
DEBUG_VERBOSEinsideSIMULATION, so simulation-only verbosity can never exist in a synthesis build by accident.
15. Interview Q&A
16. Exercises
Exercise 1 — Pick the conditional layer
For each requirement, name the correct layer (compile-time `ifdef, elaboration-time parameter, or run-time if) and justify in one line.
| # | Requirement |
|---|---|
| 1 | Print a transaction log line every cycle in nightly sim runs, nothing in synthesis |
| 2 | Build the same module as 8-bit in one instance and 32-bit in another, in one design |
| 3 | Switch a datapath between low-power and high-performance mode while the chip runs |
| 4 | Compile in a behavioural DRAM model that scores the DUT, excluded from the netlist |
| 5 | Select a single-cycle vs iterative multiplier, fixed per chip variant at build time |
| 6 | Inject a parity fault as stimulus only in the error-handling regression |
Exercise 2 — Find the layer-confusion bug
Each snippet uses the wrong conditional layer. Name the pitfall (from §12) and give the fix.
// Snippet A
`ifdef LOW_POWER
assign clk_en = sleep ? 1'b0 : 1'b1; // author wants to toggle per cycle
`else
assign clk_en = 1'b1;
`endif
// Snippet B
`ifdef ASSERT_ON
always @(posedge clk)
if (state == ILLEGAL) $error("illegal state");
`endif
// regression compile line never defines ASSERT_ON
// Snippet C — a real output's reset value
`ifdef SIMULATION
initial data_out = 8'h00; // "so sim starts clean"
`endif
// synth build leaves data_out powering up as X in gate sim / unknown in siliconExercise 3 — Design a regression-tier define scheme
A block has three regression tiers — smoke (5 min, basic checks), nightly (1 hr, full scoreboard + assertions), soak (8 hr, fault injection + long sequences) — and must also build clean for synthesis (no simulation code at all).
Produce: (a) the macro set and which tier defines which, (b) the guard structure in the testbench (which checks/stimulus sit behind which macro, and which are default-on), and (c) the committed defines.f filelist contents for each of the four builds. State explicitly which constructs must sit outside every guard and why.
17. Summary
Conditional simulation is build and simulator control, not RTL design. Its one job is to make simulation-only code — observation, stimulus, reference models, checks — present in the builds that need it and absent everywhere else, without ever altering the hardware the source describes.
The three conditional layers, and the rule for each:
- Compile-time
`ifdef/`ifndef— gates existence of source text, resolved before compilation, identical per instance. The only layer conditional simulation uses, and only for code that should not reach the netlist. - Elaboration-time
parameter/generate— gates hardware shape per instance at build. The right tool for design configuration (widths, depths, feature toggles). - Run-time
if/case— gates behaviour every cycle; always present in the netlist. The right tool for per-cycle operating modes.
The decision framework: Does this code ship in silicon? No → compile-time `ifdef. Yes → parameter (per instance) or run-time if (per cycle).
The failure modes, all layer confusions:
- Hiding synthesizable RTL behind a sim flag → sim passes, silicon fails (different hardware).
- Compile-time
`ifdefused for run-time intent → cannot change while running; cannot vary per instance. - Undefined macro deletes a check → false-green regression that verifies nothing.
- Drifting
+define+sets → non-reproducible, "flaky" pass/fail on the same commit. - Macros where parameters belong → recompile-per-variant macro sprawl.
The discipline in one sentence: conditional simulation gates the testbench, never the design — keep observation, stimulus, and checking conditional, keep the synthesizable RTL unconditional, and pin the macro set with the source.
The next section, 8.5 Simulation Control, closes Chapter 8 with $finish, $stop, and the run-control tasks that decide when a simulation ends. After 8.5, the curriculum turns to the RTL-core sequence — operators, continuous assignment, and procedural always blocks — where the conditional layers above become the everyday tools of design rather than of simulation control.
Related Tutorials
- Conditional Compilation — Chapter 7.4; the
`ifdef/`ifndef/`elsifdirective mechanics this page assumes and applies. - System Tasks & Functions — Chapter 8; the parent overview, including the
$displayfamily that conditional simulation most often gates. - $display vs $monitor vs $strobe vs $write — Chapter 8.1; the observation tasks that live inside simulation guards.
- parameter — Chapter 6.1; the elaboration-time layer that should replace macros for hardware configuration.