Monday's regression has two failures that both mention reset in the triage notes. irq_w1c fails at cycle 84 on every seed, in Icarus and in Verilator. random seed 17 reads xxxxxxxx in Icarus and passes in Verilator. One engineer calls both "X problems"; another says the second is a simulator quirk.
Are they the same class, and is there one check that finds both before any test fails?
They treat every reset bug as an X bug, and every X as something to waive or initialise away. A wrong reset value never shows an X, so X checks miss it; a missing reset shows X only in four-state simulation, so a two-state run hides it. Both are found late, by whichever test happens to read the register first.
Run the failing test three ways: four-state (Icarus), two-state (Verilator, flops start at 0) and two-state with random start values (dbglab's XRAND=1). The pattern of results names the kind.
Kind | Four-state | Two-state | Random start | Shows |
|---|---|---|---|---|
No reset (and read before written) | X | often passes (0 happens to be right) | fails on some seeds, with a random value | when the value is first used |
Wrong reset value | fails | fails, same cycle | fails, same cycle | on the first read or use after reset |
Reset never acted (sync reset, no clock edge) | X | often passes | fails on some seeds | from the moment reset ends |
X hidden by an if/else | clean output from an X input | clean | depends on the value |
On the debug loop this is Observe: the three runs are an observation, not a fix, and they cost three commands.
Run four-state, two-state and random start. Same test, same seed. Fails everywhere at the same cycle: a wrong value. Fails only where the start is unknown: a register that was never reset.
Check every register one cycle after reset. Compare each one with its documented reset value, as a probe or a directed test. This finds both kinds at once, at the first cycle.
List the flops without a reset, with Yosys (the localisation checklist shows the query). For each one, show that a write always comes before the first read. Then check the clock runs while reset is held.
This fresh case is the third kind. A busy flag uses a synchronous reset. The bench holds reset while the clock is still stopped, as it is before a PLL locks, releases it, and only then starts the clock. FIX = 0 is the synchronous version; FIX = 1 (the default) resets asynchronously.
// syncrst: a busy flag whose reset needs a clock edge. The bench holds reset while the clock is
// still stopped (as before a PLL locks), releases it, then starts the clock. FIX = 0 builds the
// flag with a synchronous reset, which never sees an edge during reset; FIX = 1 (the default)
// uses an asynchronous reset.
`timescale 1ns/1ps
module busy_flag #(parameter bit FIX = 1) (input logic clk, rst_n, set, clr, output logic busy);
if (FIX) begin : g_async
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) busy <= 1'b0;
else if (set) busy <= 1'b1;
else if (clr) busy <= 1'b0;
end else begin : g_sync
always_ff @(posedge clk) // bad: reset only acts on a clock edge
if (!rst_n) busy <= 1'b0;
else if (set) busy <= 1'b1;
else if (clr) busy <= 1'b0;
end
endmodule
module tb;
parameter bit FIX = 1;
logic clk = 1'b0, clk_on = 1'b0, rst_n, set = 1'b0, clr = 1'b0, busy;
int bad = 0;
busy_flag #(.FIX(FIX)) u (.clk, .rst_n, .set, .clr, .busy);
always #5 if (clk_on) clk = ~clk; else clk = 1'b0;
initial begin
rst_n = 1'b0; // reset from time 0, clock stopped
#30 rst_n = 1'b1; // reset released at 30 ns, still no clock
#1 if (busy !== 1'b0) begin bad++; $display("at %0d ns, after reset, before the clock: busy=%b", $time, busy); end
#20 clk_on = 1'b1; // clock starts at 51 ns
repeat (3) @(posedge clk);
#1 if (busy !== 1'b0) begin bad++; $display("at %0d ns, three idle cycles later: busy=%b", $time, busy); end
set = 1'b1; @(posedge clk) #1 set = 1'b0;
if (busy !== 1'b1) bad++;
clr = 1'b1; @(posedge clk) #1 clr = 1'b0;
if (busy !== 1'b0) bad++;
if (bad) $fatal(1, "busy was not 0 after reset in %0d checks", bad);
$display("PASS: busy is 0 after reset with no clock, then sets and clears (FIX=%0d)", FIX);
$finish;
end
endmodule$ iverilog -g2012 -P tb.FIX=0 -o s0.vvp syncrst.sv && vvp -n s0.vvp
at 31 ns, after reset, before the clock: busy=x
at 76 ns, three idle cycles later: busy=x
FATAL: syncrst.sv:38: busy was not 0 after reset in 2 checks
Time: 96000 Scope: tb
$ verilator --binary --timing --assert -Wno-fatal --top-module tb -GFIX=0 -Mdir vl0 syncrst.sv > /dev/null 2>&1 && vl0/Vtb
PASS: busy is 0 after reset with no clock, then sets and clears (FIX=0)
...
$ verilator --binary --timing --assert -Wno-fatal --x-assign unique --x-initial unique --top-module tb -GFIX=0 -Mdir vlx syncrst.sv > /dev/null 2>&1
$ vlx/Vtb +verilator+rand+reset+2 +verilator+seed+1
at 31 ns, after reset, before the clock: busy=1
at 76 ns, three idle cycles later: busy=1
[96000] %Fatal: syncrst.sv:38: Assertion failed in tb: busy was not 0 after reset in 2 checks
...
$ vlx/Vtb +verilator+rand+reset+2 +verilator+seed+2
PASS: busy is 0 after reset with no clock, then sets and clears (FIX=0)
...
$ iverilog -g2012 -P tb.FIX=1 -o s1.vvp syncrst.sv && vvp -n s1.vvp
PASS: busy is 0 after reset with no clock, then sets and clears (FIX=1)Read the four runs as check 1 says. Four-state: busy is X after reset and still X three idle cycles later, because nothing writes it until set. Two-state: a pass, because the flop starts at 0, which is the reset value. Random start: seeds 1, 3, 5 and 7 power the flag up as 1 and fail, seeds 2, 4, 6 and 8 pass. That is the signature of a register that was never reset, although the RTL has a reset branch. The reset was there; no clock edge ever came while it was held. The asynchronous version passes in all three.
dbglab has one bug of each of the first two kinds. B1 (commit 9) changed CTRL's reset value from 0 to 1, and bisection in history found the commit. B4 (commit 14) removed FILL's reset, and X in a waveform traced its X. Their runs show the two signatures from the table:
$ tb/run.sh irq_w1c 1; tb/run.sh irq_w1c 1 verilator
dbglab: test=irq_w1c seed=1
FAIL test=irq_w1c seed=1 cycle=84: IRQ: irq=1 while CTRL=00 enables no interrupt
dbglab: test=irq_w1c seed=1
FAIL test=irq_w1c seed=1 cycle=84: IRQ: irq=1 while CTRL=00 enables no interrupt
$ tb/run.sh random 17; tb/run.sh random 17 verilator; XRAND=1 tb/run.sh random 17 verilator
dbglab: test=random seed=17
FAIL test=random seed=17 cycle=7949: SCOREBOARD: RAM word 816 (address 00001cc0) read xxxxxxxx, expected 00000000 (last written by DMA run #4)
dbglab: test=random seed=17
PASS test=random seed=17 cycles=18442 time=184416 ns
dbglab: test=random seed=17
FAIL test=random seed=17 cycle=7949: SCOREBOARD: RAM word 816 (address 00001cc0) read 5d88e2dd, expected 00000000 (last written by DMA run #4)B1 fails identically in both simulators: a wrong value. B4 is X, then a pass, then a random value: no reset. Now the second check, a probe that reads every register with a documented reset value one cycle after reset and compares it with the register map in dbglab's README:
// not-run: needs dbglab's sources and bench; compile it next to them (command in the Evidence box)
// reset_check: one cycle after reset release, compare every software-visible register that the
// register map gives a reset value with that value. It only reads the design.
`timescale 1ns/1ps
module reset_check;
task automatic chk(string name, logic [31:0] got, logic [31:0] want);
if (got !== want) $display("RSTCHK cycle=%0d: %s = %h after reset, register map says %h",
dbglab_tb.cycle, name, got, want);
else $display("RSTCHK cycle=%0d: %s = %h ok", dbglab_tb.cycle, name, got);
endtask
initial begin
@(posedge dbglab_tb.rst_n);
@(posedge dbglab_tb.clk) #1;
chk("CSR SCRATCH", dbglab_tb.dut.u_csr.scratch_q, 32'h0);
chk("CSR CTRL", {24'h0, dbglab_tb.dut.u_csr.ctrl_q}, 32'h0);
chk("CSR STATUS", {29'h0, dbglab_tb.dut.u_csr.err_q, dbglab_tb.dut.u_csr.done_q, 1'b0}, 32'h0);
chk("CSR FILL", dbglab_tb.dut.u_csr.fill_q, 32'h0);
chk("PER CTRL", {24'h0, dbglab_tb.dut.u_per.pctrl_q}, 32'h5);
end
endmodule$ ../probes/run-probe.sh ../probes/reset_check.sv irq_w1c 1
dbglab: test=irq_w1c seed=1
RSTCHK cycle=5: CSR SCRATCH = 00000000 ok
RSTCHK cycle=5: CSR CTRL = 00000001 after reset, register map says 00000000
RSTCHK cycle=5: CSR STATUS = 00000000 ok
RSTCHK cycle=5: CSR FILL = xxxxxxxx after reset, register map says 00000000
RSTCHK cycle=5: PER CTRL = 00000005 ok
FAIL test=irq_w1c seed=1 cycle=84: IRQ: irq=1 while CTRL=00 enables no interruptBoth bugs, at cycle 5, in a test that only ever sees one of them. B1's test fails 79 cycles later; B4's first failure, on seed 17, comes 7,944 cycles later and only on a seed that draws a zero fill. At commit 03ebdc1, before FILL lost its reset, the same probe reports CTRL alone. The register map is the reference, not the package of reset values: a check that imported the package would agree with B1.
No reset: give the register its reset back, or prove and assert that every read is preceded by a write. B4's fix puts FILL into the reset block.
Wrong value: fix the constant against the specification, and make the check read the specification's value, not the RTL's.
Reset never acted: use an asynchronous reset for state that must be valid before the clock runs, or keep reset asserted for a few cycles after the clock starts. Reset sequencing is a system rule; write it down.
A reset-value test that reads every register after reset against the register map. It would have caught B1 and B4 on the day they were committed.
Random start values in regression: run some seeds two-state with random initial values, so a missing reset fails somewhere even without a four-state simulator.
An assertion after reset: !$isunknown on every output and state register from the first clock after reset release.
Review questions: "What does this register hold before its first write, and who reads it before then?" and "Is the clock running while reset is held?"
Whether the clock was running while reset was held. A synchronous reset acts only on a clock edge, so a reset that ends before the first edge does nothing. Look at the clock during reset in the same run; if it is stopped, either reset asynchronously or hold reset for several cycles after the clock starts.
A register the probe does not cover: one with no documented reset value (a datapath register, a FIFO entry, an internal state) that is read before it is written. List the flops without reset with Yosys, and for each one show a write before the first read, or give it a reset.
syncrst.sv as shown, 2026-10-18: iverilog -g2012 -P tb.FIX=0 then vvp -n (Icarus Verilog 13.0) fails with busy=x; verilator --binary --timing --assert -Wno-fatal --top-module tb -GFIX=0 (Verilator 5.048) passes; built with --x-assign unique --x-initial unique and run with +verilator+rand+reset+2 +verilator+seed+N, seeds 1, 3, 5, 7 fail with busy=1 and 2, 4, 6, 8 pass. FIX=1 passes in all three, random seeds 1, 3, 5 checked.
dbglab at HEAD 49c5f04: reset_check.sv compiled next to the bench (iverilog -g2012 -Wno-timescale -I tb -o build/probe.vvp <rtl/files.f> tb/bus_rules.sv tb/dbglab_tb.sv reset_check.sv, vvp -n build/probe.vvp +TEST=irq_w1c +SEED=1) reports CTRL and FILL at cycle 5; at commit 03ebdc1 only CTRL. tb/run.sh irq_w1c 1 and 2 fail at cycle 84 in both simulators; tb/run.sh random 17 fails with an X, passes in Verilator, and fails with a random value with XRAND=1.
Limits: simulation only. Random start values are one model of power-up, not all of them. Reset synchronisers and reset-domain crossings are a separate class, covered in reset-domain crossing bugs.