The Silicon Foundation & Simulation Engine Internals
Almost every Verilog bug that survives to silicon comes from one root cause: the engineer was still thinking in software. This volume rebuilds that mental model from the transistor up - what your code physically becomes, how the simulator actually schedules it, and the two coding habits that cause the overwhelming majority of simulation-synthesis mismatches.
1.1 Hardware concurrency vs Von Neumann sequential thinking
In C, a program is a list of instructions that a processor executes one after another. Time
is created by the sequence. In hardware there is no processor reading your Verilog - the
Verilog becomes the circuit. Every module, every assign, every
always block exists simultaneously and permanently, all of them computing at
once, continuously, forever.
Software is temporal: one unit of hardware (the ALU) reused across many moments in time. Hardware is spatial: many units of silicon area, all live at the same moment. That single inversion is the whole discipline.
What a gate physically is
The smallest useful unit is the CMOS inverter: one PMOS transistor pulling the output up to VDD and one NMOS pulling it down to ground. They are complementary - exactly one conducts at a time, which is why static CMOS burns almost no power when idle.
~A after a
propagation delay determined by how much capacitance it has to drive.
That propagation delay, tpd, is not a constant of the gate - it is set by the load. Every downstream gate input adds capacitance, and every millimetre of wire adds more. This is why fanout matters, why buffers get inserted during synthesis, and why the longest chain of gates between two flip-flops determines your maximum clock frequency. We make that quantitative in Volume 07.
Concurrency in code
Consider three continuous assignments. In software order would matter; here it cannot, because all three are wires that exist at the same time.
module concurrency (
input wire a, b, c,
output wire x, y, z
);
// These three lines describe three separate pieces of silicon.
// Reordering them changes NOTHING. There is no "first" statement.
assign z = y & c; // depends on y
assign y = x | b; // depends on x
assign x = a ^ b; // depends on inputs only
// Written in this order deliberately: a software reader expects z to be
// computed from a stale y. Hardware has no such concept - when a, b or c
// changes, all three gates re-settle together after their own t_pd.
endmodule
Now the mirror image. A for loop inside synthesizable Verilog is not a loop in
the software sense at all - it is a textual replication directive resolved
entirely at elaboration time.
module popcount8 (
input wire [7:0] data,
output reg [3:0] ones
);
integer i;
always @(*) begin
ones = 4'd0;
// The synthesis tool UNROLLS this completely. It does not build a
// counter, a sequencer, or a state machine. It builds eight adders
// wired in a chain - all of which settle within ONE clock cycle.
for (i = 0; i < 8; i = i + 1) begin
ones = ones + data[i];
end
end
endmodule
for (i = 0; i < 1024; i = i + 1)
and you have just asked for 1024 adders. The tool will either build them - consuming your
entire device - or fail. If you want iteration over time, you must build an FSM
or a counter explicitly. Hardware never gives you a loop for free.
Interview grilling - "Explain why software loops do not synthesize into clock cycles."
The one-line answer they want:
"A synthesizable for loop has static bounds and is fully unrolled at
elaboration into parallel combinational hardware - one instance of the body per
iteration. It consumes area, not clock cycles. Sequencing over time requires an explicit
FSM or counter."
The follow-up they will ask: "So when does a loop become sequential?"
Only when it is non-synthesizable testbench code, or when the loop bound is not statically known - in which case the tool rejects it. If you need one iteration per clock, you write a state machine whose state variable is the loop index.
1.2 The IEEE 1364 stratified event queue
Simulation has to impose an order on things that physically happen at once. The IEEE 1364 standard does this by splitting each simulation time step into ordered regions. Understanding these regions is what separates engineers who can debug a race condition from engineers who add random delays until the waveform looks right.
Within a single time step, the simulator drains regions in order. Crucially, executing events in a later region can schedule new events back into an earlier region - so the whole loop repeats until the time step is genuinely quiet.
Why $display lies and $strobe does not
$display executes in the Active region - potentially before the NBA
updates have been applied. So printing a flip-flop output with $display inside
a clocked block shows you the value from before the edge.
always @(posedge clk) begin
q <= d; // scheduled into the NBA region
$display("display: q = %b", q); // Active region -> prints the OLD q
$strobe ("strobe: q = %b", q); // Postponed region -> prints the NEW q
end
This is not a simulator quirk to work around. It is the queue behaving exactly as specified,
and it is Cummings' Golden Rule 7: use $strobe to display values assigned
non-blockingly.
The #0 anti-pattern
The Inactive region exists to service #0 delays. Engineers reach for
#0 to "push this to the end of the time step" and make a race go away. It does
make that particular race go away - and creates a new one, because if two processes both use
#0, their relative order is once again undefined.
#0
It is a symptom, not a fix. A #0 in RTL means the code has a genuine race that
should be solved by using non-blocking assignments in sequential blocks. It is Cummings'
Golden Rule 8, and it is a fast way to fail a code review.
Interview grilling - "What happens when two posedge blocks write to each other's inputs?"
Setup. Two always blocks on the same clock, each reading what the other writes:
always @(posedge clk) a <= b;
always @(posedge clk) b <= a;
The answer:
"With non-blocking assignments there is no race. Both right-hand sides - b
and a - are sampled in the Active region using their pre-edge values, and
both left-hand sides are updated together in the NBA region. The two values swap
cleanly, every cycle, which is exactly what two real flip-flops with crossed connections
would do."
Now the trap. Change both to blocking:
always @(posedge clk) a = b;
always @(posedge clk) b = a;
Now the result depends on which block the simulator happens to run first. Run
a = b first and both end up holding the old b; run
b = a first and both hold the old a. The standard does not
define the order, so two different simulators can legally disagree - and the synthesized
hardware will match neither reliably. This is the canonical Verilog race condition.
1.3 Data types, net types & strength modeling
Verilog models a wire with four states, not two. The extra two exist because real wires can
genuinely be in conditions that 0 and 1 cannot express.
| Value | Meaning | Physical reality | What it signals in a waveform |
|---|---|---|---|
| 0 | Logic low | Node driven to ground | Normal |
| 1 | Logic high | Node driven to VDD | Normal |
| X | Unknown / conflict | Uninitialised flop, or two drivers fighting | A bug. Trace it to its source |
| Z | High impedance | Nothing is driving the node at all | Fine on a tri-state bus, a bug anywhere else |
X. A real uninitialised flip-flop powers up as some definite
0 or 1 you cannot predict. X is the simulator refusing to guess - and it
propagates, so one unreset flop can turn a whole datapath red. That propagation is a
feature: it makes missing resets visible in simulation instead of in the lab.
wire vs reg - the worst-named keyword in the language
reg does not mean register. It means "a variable that holds its value between
assignments in procedural code". A reg assigned inside always @(*)
synthesizes to pure combinational logic with no storage whatsoever. Whether you get a
flip-flop depends entirely on the sensitivity list, never on the keyword.
wire (net) |
reg (variable) |
|
|---|---|---|
| Assigned by | assign, or a module port connection |
always / initial blocks only |
| Multiple drivers | Allowed - resolved by strength rules | Illegal - last write wins, races follow |
| Default value | Z (undriven) |
X (unknown) |
| Becomes a flip-flop? | Never | Only if assigned on a clock edge |
logic type replaces both - it can be driven procedurally or
continuously, and the compiler errors if you accidentally give it two drivers.
Volume 08
covers the migration. Learn wire/reg anyway: you will read legacy
RTL for your entire career.
Drive strengths and tri-state buses
When two drivers fight over one net, Verilog resolves the winner by strength. In descending
order: supply > strong > pull >
weak > highz. Equal strengths in opposite directions produce
X - which is the simulator telling you that you have built a short circuit
between VDD and ground.
The legitimate use of Z is a shared bus, where several devices take turns
driving one set of wires and everyone not driving must release cleanly.
module tristate_driver #(
parameter WIDTH = 8
) (
input wire oe, // output enable, active high
input wire [WIDTH-1:0] data_out, // value this device wants to drive
output wire [WIDTH-1:0] data_in, // value currently on the bus
inout wire [WIDTH-1:0] bus // the shared physical wires
);
// Drive the bus only when enabled; otherwise release it to high-Z so
// another device can take over. Forgetting the Z branch means two
// devices fight and BOTH read X.
assign bus = oe ? data_out : {WIDTH{1'bz}};
// Reading is unconditional - always sample what is on the wires.
assign data_in = bus;
endmodule
inout in production RTL.
1.4 Blocking (=) vs non-blocking (<=): the silicon reality
This is the single highest-yield topic in the volume, and the most reliable interview question in the entire field. The mechanics are simple; the consequences are not.
Blocking = |
Non-blocking <= |
|
|---|---|---|
| When RHS is evaluated | Immediately | Immediately (Active region) |
| When LHS is updated | Immediately, before the next statement | Deferred to the NBA region |
| Later statements see | The new value | The old value |
| Models | Combinational logic | Sequential logic (flip-flops) |
| Use in | always @(*) |
always @(posedge clk) |
The three-stage pipeline: same intent, different silicon
Here is the classic demonstration. Both blocks are legal Verilog. Both compile. They synthesize to completely different circuits.
always @(posedge clk) begin
q1 = d; // q1 gets d NOW
q2 = q1; // sees the NEW q1
q3 = q2; // sees the NEW q2
end
// All three collapse: d flows
// straight through in one cycle.
// Synthesis: ONE flip-flop.
always @(posedge clk) begin
q1 <= d; // samples d
q2 <= q1; // samples OLD q1
q3 <= q2; // samples OLD q2
end
// Each stage takes one cycle.
// Synthesis: THREE flip-flops
// in series - a shift register.
The non-blocking version is what a shift register physically does: at the clock edge, every flop simultaneously captures whatever was sitting at its input just before the edge. No flop can see its neighbour's new value, because all of them change at the same instant.
d rises after edge 1, so q1 only captures it at edge 2, and
q2 only at edge 3. With blocking assignments all three traces would rise
together at edge 2 - one flop, no pipeline.
Cummings' eight golden rules
Clifford Cummings' SNUG papers on this subject are the closest thing the industry has to settled law. Follow these eight rules and simulation-synthesis mismatches essentially stop happening.
| # | Rule |
|---|---|
| 1 | When modeling sequential logic, use non-blocking assignments. |
| 2 | When modeling latches, use non-blocking assignments. |
| 3 | When modeling combinational logic with an always block, use blocking assignments. |
| 4 | When modeling both sequential and combinational logic in the same always block, use non-blocking assignments. |
| 5 | Do not mix blocking and non-blocking assignments in the same always block. |
| 6 | Do not assign to the same variable from more than one always block. |
| 7 | Use $strobe to display values assigned non-blockingly. |
| 8 | Do not make assignments using #0 delays. |
Try it yourself - the two-register swap
Predict the output before you run it. What are a and b after
the first clock edge, given a = 1 and b = 0?
module swap_test;
reg clk = 0;
reg a = 1'b1, b = 1'b0;
always #5 clk = ~clk;
always @(posedge clk) begin
a <= b;
b <= a;
end
initial begin
$monitor("t=%0t a=%b b=%b", $time, a, b);
#25 $finish;
end
endmodule
Answer: they swap cleanly - a=0, b=1 - and keep swapping
every edge. Both right-hand sides are sampled before either left-hand side updates.
Now change both <= to = and run it again: the values no
longer swap, and which one survives depends on your simulator.
1.5 Accidental latch inference & DFT nightmares
A combinational always block makes a promise: for every possible combination of
inputs, every output gets a value. Break that promise on even one path and the synthesis
tool has no choice - it must insert a transparent latch to remember the
previous value.
The tool is not being clever or malicious. You wrote "when sel is 2, don't
change y", and the only hardware that can not-change something is memory.
y alone".
The three ways to infer a latch by accident
// TRAP 1 - if with no else
always @(*) begin
if (enable)
y = a; // y unassigned when !enable
end
// TRAP 2 - case with no default
always @(*) begin
case (sel)
2'b00: y = a;
2'b01: y = b;
2'b10: y = c; // 2'b11 not covered
endcase
end
// TRAP 3 - output missing from one branch
always @(*) begin
if (sel) begin
y = a; z = b;
end else begin
y = c; // z unassigned here
end
end
// FIX 1 - default assignment first
always @(*) begin
y = 1'b0; // every path now covered
if (enable)
y = a;
end
// FIX 2 - always include default
always @(*) begin
case (sel)
2'b00: y = a;
2'b01: y = b;
2'b10: y = c;
default: y = 1'b0;
endcase
end
// FIX 3 - assign every output on
// every branch
always @(*) begin
y = c; z = 1'b0; // defaults
if (sel) begin
y = a; z = b;
end
end
always block by assigning a default value to every
output it drives. Then write your conditional logic freely - later assignments simply
override the defaults. You cannot leave a path uncovered, because the first line already
covered them all.
Why latches are genuinely dangerous, not merely untidy
- Design-for-test breaks. Scan chains stitch flip-flops into a shift register so the tester can control and observe every state element. Latches are not scannable in the same way - coverage drops, and faults escape to the customer.
- Static timing analysis gets much harder. A latch is transparent while its gate is high, so timing can "borrow" across the boundary. Time borrowing is a legitimate advanced technique, but as an accident it produces timing reports nobody can interpret.
- Glitches become permanent. A momentary glitch on the data input while the gate is open gets captured and held. An edge-triggered flop would have ignored it.
- Clock gating gets blocked. Low-power flows rely on predictable edge-triggered structures; stray latches obstruct automatic clock-gating insertion.
always_comb makes the
tool check this for you and complain loudly - one more reason to move off plain
always @(*).
Interview grilling - "When would you deliberately use a latch?"
A genuinely good question, because "never" is the wrong answer and shows shallow knowledge. Latches are used deliberately in three places:
- Time borrowing on a critical path - a latch lets a slow path steal slack from the following stage, something a flop cannot do.
- Area and power - a latch is roughly half the transistors of a master-slave flip-flop, which matters in very large register files.
- Clock gating cells - the standard integrated clock gating cell is built around a latch, to prevent glitches on the gated clock.
The distinction to state clearly: "Latches are a deliberate tool used by physical design teams with explicit constraints. In RTL they should never appear by accident - an inferred latch means my code has an uncovered branch, not that I chose a latch."
Volume 01 recap
| Concept | The one thing to remember |
|---|---|
| Concurrency | Hardware is spatial. Loops cost area, never clock cycles. |
| Event queue | Every <= in the design updates together in the NBA region. |
| 4-state logic | X means conflict or uninitialised; Z means undriven. |
reg |
Not a register. The sensitivity list decides whether you get a flop. |
| Assignments | = for combinational, <= for sequential. Never mix. |
| Latches | Assign a default to every output at the top of every combinational block. |