A small block has a 2-bit counter and a status flag. Software sets the flag; the block clears it when the counter reaches 3. The RTL regression passes, and the waveform shows exactly that, cycle after cycle. In the lab the flag is sometimes already clear when software reads it at count 2.
You had every signal in the design, at every cycle, and nothing was wrong. What could RTL simulation not have shown you?
Some engineers treat a clean, fully visible RTL run as the truth about the chip, so a lab failure must be the lab's fault: a bad board, a marginal part, a driver bug. Others go the opposite way and lose faith in simulation altogether, and start debugging on the bench, where they can see almost nothing.
Both are wrong in the same way. RTL simulation is exact about one thing, the model you wrote, and blind to whatever that model leaves out. Knowing what it leaves out tells you when a clean run proves something and when it cannot.
No later stage comes close to this, which is why the rest of Part 6 is about moving failures back here:
Full visibility. Every signal and every memory word, at every cycle, in any run. Nothing has to be planned in advance; you can dump or probe whatever the question needs.
Exact repeats. The same seed, commit and simulator give the same run, cycle for cycle (determinism and seeds). An experiment can be rerun with one change and nothing else.
Cheap changes. A probe, an assertion or a fix costs a recompile of seconds, not a re-spin or an FPGA build of hours.
Two simulators. Icarus and Verilator read the same RTL differently in places, and a disagreement is itself a clue (differential debugging).
Parts 1 to 4 taught the loop on RTL simulation, because it is the stage where every step is affordable. The short version, with where each step lives:
Step | In RTL simulation | Where it is taught |
|---|---|---|
Reproduce | one command with a seed, a commit and a simulator; shrink the test and the design | |
Observe | probes, logs, assertions at the point of use, X checks | |
Localise | walk back to the first wrong cycle; bisect in time, space, stimulus and history | |
Hypothesise and Test | a prediction you have not seen yet, one experiment, one change at a time |
RTL simulation runs your model in an idealised world. Three things are missing from that world, and each one has a lesson in this course that shows it with a real run:
It leaves out | So it cannot show | Shown in |
|---|---|---|
Time. Every wire and gate is instant; registers change exactly on the edge. | setup and hold failures, multicycle paths the design does not keep, glitches on asynchronous controls | Timing-only bugs, and the run below |
Real start-up values. Unreset state is X in a four-state simulator, 0 in a two-state one; an | a bug that needs a register to power up at 1; a bug hidden by X optimism | |
The world around the design. The bench is a model: ideal clocks, a tidy software sequence, no metastability, no noise, no temperature. | failures that need real clock relationships, real firmware timing or a marginal supply |
A fourth gap sits between the model and the netlist: synthesis may read the RTL differently from the simulator. That is covered in races and simulation-synthesis mismatches.
Read the bottom group: from the clock edge until bit 0 arrives, the decoder sees b1 = 1 and the old b0 = 1, which is the code for 3. The pulse is 300 ps wide, and an asynchronous clear does not care how short it is.
wrap_flag decodes "count is 3" with an AND gate and drives the flag's asynchronous reset with it. With WIRE_PS = 0 it is ordinary RTL. With WIRE_PS = 300, counter bit 0 reaches the decoder 300 ps after bit 1, which stands in for the routing of a real chip. The bench sets the flag while the count is 0 and checks it at count 2, three laps, for both copies.
`timescale 1ns/1ps
// wrap_flag: a 2-bit counter and a sticky flag. The flag must clear only when the counter is 3.
// The clear is decoded combinationally and drives the flag's asynchronous reset (the bug).
// WIRE_PS = 0 is RTL simulation: every wire is instant. WIRE_PS > 0 models the wire delays of
// the real circuit: counter bit 0 reaches the decoder WIRE_PS later than bit 1.
module wrap_flag #(parameter int WIRE_PS = 0) (
input logic clk, rst_n, set,
output logic [1:0] cnt,
output logic flag
);
logic b0, b1, clr_n;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) cnt <= '0;
else cnt <= cnt + 2'd1;
if (WIRE_PS == 0) begin : g_rtl
assign b0 = cnt[0];
assign b1 = cnt[1];
end else begin : g_wire
assign #(WIRE_PS * 1ps) b0 = cnt[0]; // the slower wire
assign b1 = cnt[1];
end
assign clr_n = rst_n & ~(b1 & b0); // bad: async clear from decode logic
always_ff @(posedge clk or negedge clr_n)
if (!clr_n) flag <= 1'b0;
else if (set) flag <= 1'b1;
endmodule
// tb: three laps of the counter. The flag is set while cnt is 0, so it is 1 from cnt = 1;
// at cnt = 2 it must still be 1. It may clear only when cnt reaches 3.
module tb;
logic clk = 1'b0, rst_n = 1'b0, set = 1'b0;
logic [1:0] cnt_r, cnt_w;
logic flag_r, flag_w;
int early_r = 0, early_w = 0;
always #5 clk = ~clk;
wrap_flag #(.WIRE_PS(0)) u_rtl (.clk, .rst_n, .set, .cnt(cnt_r), .flag(flag_r));
wrap_flag #(.WIRE_PS(300)) u_wire (.clk, .rst_n, .set, .cnt(cnt_w), .flag(flag_w));
always @(negedge flag_r) if (rst_n) $display("%6.1f ns rtl : flag cleared, cnt=%0d", $realtime, cnt_r);
always @(negedge flag_w) if (rst_n) $display("%6.1f ns wire: flag cleared, cnt=%0d", $realtime, cnt_w);
initial begin
@(negedge clk) rst_n = 1'b1;
repeat (12) begin
set = (cnt_r == 2'd0);
if (cnt_r == 2'd2) begin
$display("%6.1f ns cnt=2: rtl flag=%b wire flag=%b", $realtime, flag_r, flag_w);
if (!flag_r) early_r++;
if (!flag_w) early_w++;
end
@(negedge clk);
end
if (early_r != 0 || early_w != 3) $fatal(1, "the lesson's claims do not hold (rtl %0d, wire %0d)", early_r, early_w);
$display("PASS: zero-delay RTL keeps the flag until cnt=3; with 300 ps of wire skew it clears at cnt 1->2 in every lap");
$finish;
end
endmodule$ iverilog -g2012 -o g.vvp glitch.sv && vvp -n g.vvp 25.0 ns wire: flag cleared, cnt=2
30.0 ns cnt=2: rtl flag=1 wire flag=0
35.0 ns rtl : flag cleared, cnt=3
65.0 ns wire: flag cleared, cnt=2
70.0 ns cnt=2: rtl flag=1 wire flag=0
75.0 ns rtl : flag cleared, cnt=3
105.0 ns wire: flag cleared, cnt=2
110.0 ns cnt=2: rtl flag=1 wire flag=0
115.0 ns rtl : flag cleared, cnt=3
PASS: zero-delay RTL keeps the flag until cnt=3; with 300 ps of wire skew it clears at cnt 1->2 in every lapRead it in pairs of lines. The RTL copy clears the flag at 35 ns, on the edge where the count becomes 3, which is the specification. The wire copy clears it at 25 ns, on the edge where the count goes from 1 to 2, and at 30 ns software would read 0 where the RTL reads 1. It happens in every lap, at the same edge: deterministic, not random. Verilator 5.048 prints the same lines.
Nothing in the RTL run was hidden from view. There simply was no glitch to see, because zero-delay simulation cannot produce one: b1 and b0 change in the same instant. The bug lives in a gap of the model, not in a signal you forgot to look at.
Clear the flag synchronously, on the clock, from the decode of the count before it: if (cnt == 2'd2) flag <= 1'b0; else if (set) flag <= 1'b1; inside an ordinary clocked block with the real reset, so it clears on the edge where the count becomes 3, as specified. The decode can glitch as much as it likes between edges; only its settled value at the edge counts, and static timing checks that it settles in time. The rule that finds the original before any simulation is structural: no asynchronous reset or clock driven from combinational logic. Reset-domain and CDC checkers flag it; the glitchy reset release lesson covers the family.
The RTL model was correct by its own rules, so every RTL test passed and the waveform agreed with the specification. The failure needed time, which the model does not have. On the debug loop, this is the Reproduce step at a later stage: when a lab failure meets a clean RTL regression, the question is not "what did the waveform miss" but "which gap could hold this bug", and the answer decides the next instrument: a delay model, gate-level simulation with timing, or the lab.
With a Gray-coded counter, each step changes one bit, so the decode of a single value cannot pass through a false code between two steps: that argument is right for the counter's own transitions. It does not cover other inputs to the decoder, a counter that is loaded or reset to a new value (several bits change at once), or a decoder whose own gates glitch. A synchronous clear is safe whatever the encoding.
(a) Start-up values: some register without reset powers up to a value no simulation started from. (b) Time: a path too slow for the clock, so a setup-type failure. (c) The world around the design: the bench models the memory with ideal timing, and the real part differs, perhaps within its datasheet.
glitch.sv as shown, 2026-10-18: iverilog -g2012 -o g.vvp glitch.sv && vvp -n g.vvp (Icarus Verilog 13.0) and verilator --binary --timing --assert -Wno-fatal --top-module tb glitch.sv (Verilator 5.048): identical output; the zero-delay copy clears the flag at 35, 75 and 115 ns (cnt = 3); the copy with WIRE_PS = 300 clears it at 25, 65 and 105 ns (the 1 to 2 edge) and reads flag = 0 at cnt = 2 in all three laps; PASS.
The other limits are each shown, with real runs, in the lessons linked from the table: X optimism (dbglab B4 and a small design), timing (a multicycle path with a delay model), and the environment (a synchroniser model).
Limits: the wire delay is a behavioural assign # on one signal, a model of skew, not an extracted netlist with a delay file. No CDC or reset lint tool is installed, so the rule that catches this is described, not run.