Simulation and Synthesis
Every Verilog file you write has two readers. One is the simulator, which runs it. The other is the synthesiser, which builds it. They read the same text and they do not agree about all of it, and the first rule of writing Verilog is to know which reader you are writing for.
The simulator
A simulator is an interpreter. It reads the Verilog, builds a model of the circuit and advances simulated time from event to event: a signal changes, a clock edge arrives, a delay expires. It reports what it sees as waveforms, printed values and pass or fail messages.
It will happily run things that have no hardware meaning at all: #100 to wait a hundred time units, $display to print, initial blocks that run once at time zero, forever loops, real numbers, force and release to override a signal for debugging. There is no circuit that “waits 100 ns in the abstract” or prints to a console. The simulator does not care. Its job is to execute.
The synthesiser
A synthesiser reads the same Verilog and produces a netlist: gates, flip-flops and the wires between them, which place and route then turns into standard cells or lookup tables. For every construct it asks what circuit this describes. If there is no answer it either ignores the construct or stops with an error.
It ignores delays entirely: assign #5 y = a & b; produces exactly the AND gate that assign y = a & b; does, and the 5 evaporates. It cannot do anything with $display. Most flows reject initial blocks in design code, because “run once at power-on” is not a gate or a flip-flop. It cannot build a loop whose bound is not a constant, because that would be an unbounded amount of hardware.
RTL is the overlap
Draw the constructs the simulator accepts as one circle and the constructs the synthesiser accepts as another. The overlap is register transfer level Verilog (Figure 1): what is stored in registers at each clock edge and what combinational logic sits between them. It maps straight onto flip-flops and gates, and it simulates without trouble. Every design module in this course is pure RTL.
| Construct | Simulates | Synthesises | Meaning |
|---|---|---|---|
assign y = a & b; |
yes | yes | a gate |
always @(posedge clk) |
yes | yes | flip-flops |
always @(*) |
yes | yes | combinational logic |
if / else |
yes | yes | a mux or priority logic |
case |
yes | yes | a mux or decoder |
for with constant bounds |
yes | yes | unrolled hardware |
wire, reg, parameter |
yes | yes | nets, signals, constants |
initial |
yes | no | testbench only |
#delay |
yes | ignored | testbench, or timing annotation |
$display, $monitor |
yes | no | testbench only |
forever |
yes | no | testbench clock generation |
while with a variable bound |
yes | error | avoid in RTL |
real |
yes | no | modelling only |
force / release, $random |
yes | no | debugging and stimulus |
Anything with a “no” in the third column goes in a testbench and nowhere else.
The mismatch trap
Here is a design module written by someone who wanted to model a gate delay:
`timescale 1ns / 1ps
`default_nettype none
// BAD: a delay in a design module
module bad_adder (
input wire [7:0] a,
input wire [7:0] b,
output wire [8:0] sum
);
assign #5 sum = a + b; // the simulator honours #5; the synthesiser drops it
endmodule
In simulation the sum appears 5 ns after the inputs change, which is what the designer wanted to see. The tests pass. In silicon there is no 5 ns; there is an adder whose delay is whatever the library and the routing make it. If anything downstream was relying on that 5 ns, the chip fails, and the simulation said it was fine. That is a simulation and synthesis mismatch, and it is among the nastier bugs in the trade because the evidence points the wrong way.
The same mistake in a different costume is always #5 clk = ~clk; inside a design module. In simulation it makes a 100 MHz clock. In synthesis it makes nothing, or an error. Clocks come from the testbench, or from the board.
The right split:
`timescale 1ns / 1ps
`default_nettype none
// GOOD: design module, pure logic, no delays
module good_adder (
input wire [7:0] a,
input wire [7:0] b,
output wire [8:0] sum
);
assign sum = a + b;
endmodule
`timescale 1ns / 1ps
`default_nettype none
// GOOD: testbench; simulation-only constructs belong here
module tb_good_adder;
reg [7:0] a, b;
wire [8:0] sum;
good_adder dut (
.a (a),
.b (b),
.sum (sum)
);
initial begin
$dumpfile("dump.vcd");
$dumpvars(0, tb_good_adder);
a = 8'd100; b = 8'd200; #10;
$display("sum = %0d (expected 300)", sum);
a = 8'd255; b = 8'd255; #10;
$display("sum = %0d (expected 510)", sum);
$finish;
end
endmodule
Everything in the testbench, initial, #10, $dumpfile, $display, $finish, is simulation only, and that is fine, because the testbench is a harness, not hardware.
The rule
Design modules use only synthesisable constructs. Testbenches use whatever is convenient. The two never share a file. A file whose name starts with tb_ is a testbench; any other file has no initial, no delays, no $display and no forever. It is a simple rule and code review can enforce it at a glance.
Synthesis tools warn about non-synthesisable constructs in design files, “ignoring initial block”, “ignoring delay”. A warning that fires every run becomes wallpaper. Set the linter, Verilator or whatever you use, to make those errors, so the build stops and someone fixes it.
The restriction is a discipline rather than a limitation. Without delays you have to think about clock edges and the logic between them. Without initial you have to think about reset. Without unbounded loops you have to think about how much hardware you are actually asking for. Every rule the synthesiser imposes is a fact about physical circuits, and the next article follows the design from that RTL all the way to a finished chip.