Volume 01 Beginner 5 sub-modules ~50 min read

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.

CMOS inverter transistor schematic alongside its logic gate symbol TRANSISTOR LEVEL VDD PMOS pull-up Y NMOS pull-down A LOGIC SYMBOL A Y Y = ~A propagation delay tpd set by load capacitance
Figure 1.1 - The CMOS inverter. Two transistors, permanently wired, permanently powered. Nothing "executes" it: the output settles to ~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
The cost of forgetting this The loop above costs area, not time. Write 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.

The stratified event queue regions within one Verilog simulation time step ONE SIMULATION TIME STEP ACTIVE blocking =, RHS eval, assign, $display INACTIVE #0 delays - anti-pattern, avoid NBA UPDATE non-blocking <= LHS finally written OBSERVED SVA assertion expressions sampled REACTIVE program blocks, assertion action code POSTPONED $strobe, $monitor - values are final iterate until no events remain Combinational logic settles here Every flop in the design updates here, simultaneously Only place where a printout is race-free
Figure 1.2 - The stratified event queue. The single most important fact on this diagram: every non-blocking assignment in your entire design lands in the same NBA region, after all right-hand sides everywhere have already been sampled. That is what makes flip-flops behave like flip-flops.

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.

Never use #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 is pessimism, not a value Silicon has no 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
SystemVerilog fixes this SystemVerilog's 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
Tri-state does not exist inside a modern ASIC Internal tri-state buses are effectively banned in modern digital design - they are hard to test, hard to time, and prone to floating nodes that burn static current. Real designs use a multiplexer instead. Tri-state survives only at chip I/O pads, which is exactly where you will see 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.

Timing waveform of a three-stage shift register using non-blocking assignments edge 1 edge 2 edge 3 edge 4 edge 5 clk d q1 q2 captured at edge 2 captured at edge 3 exactly 1 clock of latency per stage
Figure 1.3 - Non-blocking assignments produce real pipeline latency. 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
1When modeling sequential logic, use non-blocking assignments.
2When modeling latches, use non-blocking assignments.
3When modeling combinational logic with an always block, use blocking assignments.
4When modeling both sequential and combinational logic in the same always block, use non-blocking assignments.
5Do not mix blocking and non-blocking assignments in the same always block.
6Do not assign to the same variable from more than one always block.
7Use $strobe to display values assigned non-blockingly.
8Do 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.

How an incomplete conditional infers a transparent latch instead of pure combinational logic WHAT YOU INTENDED a sel PURE COMB no memory y Output depends only on current inputs. Fully testable, trivially timed. WHAT SYNTHESIS BUILT a sel D-LATCH D Q G (level) y holds previous value when sel = 2 Level-sensitive. Breaks scan chains, complicates STA, blocks clock gating.
Figure 1.4 - One missing branch turns combinational logic into state. The feedback path is the latch holding its own output - the hardware equivalent of "leave 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
The habit that eliminates all three Open every combinational 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

Catch them before review does Every synthesis tool prints a latch inference warning, and everyone ignores warnings. Set your flow to treat them as errors. In SystemVerilog, 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.