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.
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.
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 |
|---|---|---|---|
| waits in simulated time | ignored or rejected, never a hardware delay | testbench waits ( |
| 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 |
| print and stop the run | no hardware equivalent | testbench checks and the verdict line |
| models a register update | infers flip-flops | the counter and every design register |
| models combinational logic | infers gates | next-value and decode logic |
| returns a new value each call | no hardware equivalent | the testbench random phase |
| 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.
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.
// 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
endmoduleThis listing is the delay-based counter: complete and simulatable, but its pauses have no hardware meaning.
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.
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.
`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
endmoduleThis listing is the synthesizable counter: a clocked register with a reset branch and no simulation-only code.
# 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.
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.
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.
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.
#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.
$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.
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.