Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • How to Use This Course
  • Reading Paths
  • Quick Start: Your First Green Build
  • Part 1: How Chips Get Made6
  • Part 2: Your Workstation5
  • Part 3: Specify and Architect3
  • Part 4: The Design Loop6
    • Compile, Elaborate, Simulate, Synthesize, Lint
    • First Simulation
    • Waveforms with GTKWave
    • Lint with Verilator
    • First Synthesis Check
    • Simulation Code vs Synthesizable Code
  • Part 5: The Verification Ladder6
  • Part 6: Automation and Reproducibility5
  • Part 7: Hand-Off to the Back-End3
  • Part 8: When Things Go Wrong3
  • Part 9: Project and Next Steps3
  • Reference4
Loading lesson

‹Part 4: The Design Loop
  1. Topics
  2. Start Here: RTL and Verification
  3. Part 4: The Design Loop
  4. Simulation Code vs Synthesizable Code

Part 4 · The Design Loop · Working (W)

Simulation Code vs Synthesizable Code

Sort simulation-only code from synthesizable code: delays, printing, random numbers and initial blocks versus the registers and logic that Yosys can turn into gates.

On this page

    Note

    Level: Working (W) · Answers: Some of the code in a testbench would never become hardware. Which constructs, and why does it matter?

    Before this: First Synthesis Check

    Some of the code in a testbench would never become hardware. Which constructs, and why does it matter?

    Open the course testbench and you will find lines no factory could build: waits measured in nanoseconds, printed messages, random numbers, a block that runs once at time zero. This lesson sorts every construct you have met into two columns: code that runs in simulation only, and code that can become hardware. The line between them decides what the synthesis check reads.

    Simulation can say anything; hardware must be built from registers and gates

    A simulator controls time itself. It can pause for a number of nanoseconds with a delay, print a message with $display, stop the run with $fatal or $finish, pull in stimulus from files, and draw random numbers with $random. An initial block runs once at time zero to set all of that up. Used this way, none of these describe a circuit. (A few flows read a plain initial value as a power-up value, which list of mistakes below returns to.) They describe the experiment around the circuit.

    Synthesizable code describes only two things: registers that hold values across clock edges, and the combinational logic that computes their next values. A register is an always_ff block whose assignments use the non-blocking form, so every register in the design updates from the same edge. Combinational logic is an always_comb block or a continuous assignment. Operators such as addition and comparison become gates. Everything else in the language is scaffolding for the experiment.

    The testbench idiom of waiting #1 after a clock edge belongs firmly in the first column. The check runs 1 ns after the edge so the new count has settled before the comparison reads it. That wait exists because the simulator samples signals in an order the designer cannot fully control, and the pause removes the race. No hardware counterpart exists, and none is needed: the Makefile's synth target reads only the design file, so the testbench never reaches Yosys at all.

    Two columns for every construct you have met

    The table below sorts each construct by what it does in simulation, what synthesis makes of it, and where you have seen it. Read the last column as a map back to code you already ran.

    Construct

    Simulation

    Synthesis

    Where you see it

    #delay

    waits in simulated time

    ignored or rejected, never a hardware delay

    testbench waits (#1) and clocks (#5)

    initial

    runs once at time zero

    no gates for the block itself; some flows (FPGA, Yosys) turn a plain initial value into a power-up value, so do not rely on it for reset

    testbench stimulus and setup

    $display and $fatal

    print and stop the run

    no hardware equivalent

    testbench checks and the verdict line

    always_ff with non-blocking assignment

    models a register update

    infers flip-flops

    the counter and every design register

    always_comb

    models combinational logic

    infers gates

    next-value and decode logic

    $random

    returns a new value each call

    no hardware equivalent

    the testbench random phase

    $finish

    ends the simulation

    no hardware equivalent

    the end of every testbench run

    The figure below draws the same split as two columns. The dashed boxes on the left never become gates; the solid boxes on the right do. When you read unfamiliar code, ask first which column each line belongs to.

    Two columns of constructs: simulation-only on the left with dashed boxes, synthesizable on the right with solid boxes. Find each new construct in one of the two columns before asking what it builds.

    What does a delay-based counter look like?

    The listing below counts the same sequence as the course counter, but it waits with delays instead of describing a register. It simulates: each rising edge advances the count after its pause. Nothing in it describes hardware.

    delay_counter_bad.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // bad-example
    `timescale 1ns/1ps
    // A counter that waits with delays instead of describing a register.
    module delay_counter_bad (
        input  logic       clk,
        input  logic       rst_n,
        output logic [3:0] count
    );
        always @(posedge clk) begin
            if (!rst_n) begin
                #2 count = 4'd0;
            end else begin
                #2 count = count + 4'd1;
            end
        end
    endmodule

    This listing is the delay-based counter: complete and simulatable, but its pauses have no hardware meaning.

    Predict first You hand the delay-based counter to Yosys. Does synthesis accept it?

    No. A delay is an instruction to the simulator, not a circuit element, so a synthesis tool ignores it or rejects the style around it. The good version below counts the same way, described as a register, and that is the fix.

    What does the synthesizable version look like?

    The listing below counts the same sequence with no delays at all. The always_ff block says this is a register, the non-blocking assignments say every update leaves from the same clock edge, and the reset branch says what the register holds after reset.

    delay_counter_good.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    `timescale 1ns/1ps
    // A counter that describes a register: reset, then count.
    module delay_counter_good (
        input  logic       clk,
        input  logic       rst_n,
        output logic [3:0] count
    );
        always_ff @(posedge clk) begin
            if (!rst_n)
                count <= 4'd0;
            else
                count <= count + 4'd1;
        end
    endmodule

    This listing is the synthesizable counter: a clocked register with a reset branch and no simulation-only code.

    Diagram
    # captured terminal output
    $ yosys -Q -T -p 'read_verilog -sv unsupported.sv' 2>&1 | tail -n 3
    1. Executing Verilog-2005 frontend: unsupported.sv
    unsupported.sv:1: ERROR: syntax error, unexpected TOK_INTERFACE
    Parsing SystemVerilog input from `unsupported.sv' to AST representation.

    This output came from Yosys 0.69+post on macOS. The failing file declares an interface where the frontend expects a module, and the frontend stops at the first unexpected token. The lesson generalizes: a synthesis frontend accepts a subset of SystemVerilog, and a different tool may accept what this one rejects. When synthesis chokes on code that simulates, the question is not whether the code runs but whether this tool builds that construct.

    The example project's testbench uses #1 waits and $random stimulus, and none of it is synthesized: the Makefile's synth target reads only the design file. That split is deliberate. Keep behavior that must build in the design, written as registers and logic, and keep everything else in the testbench, where the full language is welcome.

    Break it on purpose: a construct the frontend cannot parse

    The transcript above is the deliberate failure. A file containing an interface declaration reaches a frontend that does not accept it there, and the run ends with a syntax error naming the unexpected token. The fix matches the diagnosis: rewrite the boundary with plain logic ports the frontend accepts, the way the course design already does, and confirm the tool's documentation when you need a construct you have not synthesized before.

    Three mistakes that mix the columns

    • Assuming that simulating means synthesizing. A green simulation says the behavior ran as written. Synthesis asks a different question, whether the code describes registers and gates, and simulation-only code fails it.

    • Using initial blocks for reset in synthesizable code. An initial block runs once in simulation. Some flows, such as FPGA tools and Yosys, turn a plain initial value into a power-up value, but a typical ASIC flow has no such thing, so a design that depends on it resets in the testbench and may misbehave in silicon. Write reset as a branch inside the clocked block, the way the good counter does.

    • Relying on delays for behavior. A #1 in the design shifts simulation timing and builds nothing. If the design needs ordering, express it as registers and combinational paths, and leave delays in the testbench where they settle races between the driver and the check.

    Rule: code that runs is not code that builds; only registers and logic synthesize.

    Check yourself Rewrite the delay-based counter so it synthesizes.

    Use the good version: a clocked always_ff block, non-blocking assignments, a reset branch, and no delays. It counts the same sequence, but every line now describes a register or its next value instead of instructing the simulator.

    Check yourself The testbench waits #1 after each clock edge. Why is that line simulation-only?

    It pauses the experiment so the new count settles before the comparison reads it. The pause manages the simulator's sampling order and has no circuit equivalent. It never reaches synthesis anyway, because the synth target reads only the design file.

    Check yourself You call $random inside the design to pick a value. What happens in synthesis?

    There is no hardware equivalent of a simulator call that returns a new value on demand, so synthesis rejects it or builds something unintended. Move the randomness into the testbench stimulus, where $random belongs, and feed the design plain inputs.

    Interview

    Asked: "Why does a design that simulates fine fail in synthesis?"

    Weak answer: There must be a bug in the simulator or the synthesis tool.

    Strong answer: The design likely uses simulation-only constructs: delays, initial blocks, $display or $finish, $random, or file reads. All of those compile and run, but none describes registers or gates, so synthesis ignores or rejects them. The fix is to rewrite the behavior as clocked registers plus combinational logic and keep the rest in the testbench, which synthesis never reads.

    Follow-ups: (1) Which construct in our testbench would break first if it reached synthesis? (2) How does the Makefile keep testbench code away from Yosys?

    Key takeaways

    • Simulation-only code waits, prints, reads files, rolls random numbers, and runs once; none of it becomes gates.

    • Synthesizable code is registers updated on clock edges plus the combinational logic between them.

    • Keep the two apart: behavior that must build goes in the design, everything else stays in the testbench.

    Keep going

    Next: Lesson 5.1, Tests That Can Fail. You can now tell the two kinds of code apart. Next you climb the verification ladder, starting with the question every test must answer: can it fail?

    Related topics

    • RTL: Coding Lint

    Previous

    23 / 47

    Next: Part 5: The Verification Ladder

    Up next

    Up nextPart 5: The Verification Ladder

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Compile, Elaborate, Simulate, Synthesize, Lint
    • First Simulation
    • Waveforms with GTKWave