A driver programs a timer block at boot: control register, interrupt enable, then a timeout of 0x55. Later the block runs in a mode no code ever selected, and its control register reads 55. Every register reads back correctly right after it is written. The driver team blames a stray pointer in firmware; the RTL team says every register works. Who is right?
Register bugs reach debug late, as driver bugs or as wrong behaviour of the block they configure. The hunt then starts in software or in the block's datapath, far from the cause. When it does reach the registers, the usual test writes a register and reads the same one back. That test passes for every bug on this page except the crudest.
The second habit is a register model generated from, or checked against, the RTL. A model that shares the RTL's reset values or decode agrees with every mistake in them, as when the bug is in the test showed with dbglab's own bench.
Sub-class | What you see | When |
|---|---|---|
Wrong reset value | the first read after reset differs from the map; behaviour wrong only until software writes the register | early in a test, and only in tests that read or rely on the default |
Wrong access type | a status bit that never clears, clears when another field is written, or a read-only register that takes writes; interrupt storms or lost interrupts | the first clear or the first read-modify-write |
Decode overlap | writing one register changes another; the one written reads back fine; offsets differ by a power of two | only after the aliased register is written |
Byte strobes, endianness | a partial write corrupts the other bytes of the word, or bytes come back reversed (12345678 as 78563412) | only for sub-word writes, or one bus width or one host |
Decode overlap is the one that sends people to the wrong place, because the victim is not the register being written:
On the debug loop these are Localise checks, and each one is cheap enough to run before you look at a single waveform:
Read every register straight after reset and compare it with the map, typed from the spec. Any difference is a wrong reset value, or an alias whose victim has another register's reset value.
Walk the map: write a different value to every writable register, then read all of them. A register that changed without being written is aliased; check which address bits the decoder uses.
Test every access type with both values: write 0 and 1 to each W1C bit while hardware sets it, write each read-only register, read each read-to-clear register twice, and do partial writes with each byte-strobe pattern.
The block has the map in its header comment. BUG = 1 decodes the register with addr[3:2] only, so 0x10 lands on CTRL. BUG = 2 writes the W1C status bit like a RW one. The bench's model is the map itself, and it runs the three checks in order on all three versions.
// regs5: a five-register block. Register map (the spec):
// 0x00 CTRL RW reset 00 0x08 STATUS W1C [0] done, set by hardware; reset 00
// 0x04 IRQ_EN RW reset 00 0x0C SCRATCH RW reset 00
// 0x10 TIMEOUT RW reset 40 any other offset reads 0 and ignores writes
// BUG = 0: as specified.
// BUG = 1: the decoder looks at addr[3:2] only, so 0x10 is decoded as 0x00.
// BUG = 2: STATUS is written like a RW register instead of write-1-to-clear.
`timescale 1ns/1ps
module regs5 #(parameter int BUG = 0) (
input logic clk, rst_n, wr, done_set,
input logic [4:0] addr,
input logic [7:0] wdata,
output logic [7:0] rdata,
output logic irq
);
logic [7:0] ctrl_q, irq_en_q, scratch_q, timeout_q;
logic done_q;
logic [2:0] sel; // which register: 0..4, 7 = none
assign sel = (BUG == 1) ? {1'b0, addr[3:2]} // bad: drops addr[4]
: (addr[1:0] == 2'b00 && addr <= 5'h10) ? addr[4:2] : 3'd7;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin
ctrl_q <= 8'h00; irq_en_q <= 8'h00; scratch_q <= 8'h00; timeout_q <= 8'h40; done_q <= 1'b0;
end else begin
if (BUG == 2 && wr && sel == 3'd2) done_q <= wdata[0] | done_set; // bad: RW
else done_q <= done_set | (done_q & ~(wr && sel == 3'd2 && wdata[0])); // W1C, set wins
if (wr) case (sel)
3'd0: ctrl_q <= wdata;
3'd1: irq_en_q <= wdata;
3'd3: scratch_q <= wdata;
3'd4: timeout_q <= wdata;
default: ;
endcase
end
always_comb
case (sel)
3'd0: rdata = ctrl_q;
3'd1: rdata = irq_en_q;
3'd2: rdata = {7'h0, done_q};
3'd3: rdata = scratch_q;
3'd4: rdata = timeout_q;
default: rdata = 8'h00;
endcase
assign irq = done_q & irq_en_q[0];
endmodule
$ iverilog -g2012 -o regs.vvp regs_tb.sv && vvp -n regs.vvp
BUG=0 (as specified): clean
BUG=1 (decode drops a bit): reset TIMEOUT: read 00 at 0x10, expected 40
BUG=1 (decode drops a bit): walk CTRL: read 55 at 0x00, expected 11
BUG=2 (STATUS written RW): STATUS after writing 0: read 00 at 0x08, expected 01
BUG=2 (STATUS written RW): STATUS after writing 1: read 01 at 0x08, expected 00
PASS: the map-driven checks pass the correct block and catch both bugs
...Verilator 5.048 prints the same six lines. Read the BUG=1 lines carefully, because neither is the obvious message:
The reset check fails on TIMEOUT, not on CTRL. 0x10 reads 00, CTRL's reset value, instead of 40. It looks like a wrong reset value. The clue is that 00 is another register's reset value: suspect an alias before you edit a reset.
The walk fails on CTRL, not on TIMEOUT. The bench wrote 11 to CTRL and 55 to TIMEOUT; TIMEOUT reads back its 55 because it is CTRL. The victim is the register nobody touched, which is the scene's "mode nobody set".
BUG=2 fails both halves of the W1C check: writing 0 cleared the bit, and writing 1 set it. A driver that clears done with a write of 1 sees the interrupt stay high forever; one that writes STATUS for any other reason loses the event.
dbglab's B1 is a wrong reset value: CTRL resets to 01 instead of 00, enabling the done interrupt. Its own test sees it at cycle 84, when the first DMA run finishes and raises an interrupt nobody enabled (the debug of that run and the bisect are in Part 3). Check 1 is a test dbglab does not have. Here it is, added to a scratch copy, with the expected values typed from the register map in README.md:
// snippet: added to tb/tests.svh in a scratch copy, with one dispatch line in tb/dbglab_tb.sv
// reg_reset: read every readable register straight after reset and compare it with the reset
// value in the register map (README.md), typed in by hand from the spec, not taken from the RTL.
task automatic t_reg_reset();
logic [11:0] off [6] = '{CSR_ID, CSR_SCRATCH, CSR_CTRL, CSR_STATUS, CSR_ERR_ADDR, CSR_FILL};
logic [31:0] spec [6] = '{32'hDB1A_B001, 32'h0, 32'h0, 32'h0, 32'h0, 32'h0};
string name [6] = '{"ID", "SCRATCH", "CTRL", "STATUS", "ERR_ADDR", "FILL"};
logic [31:0] got;
int bad = 0;
for (int i = 0; i < 6; i++) begin
rd(CSR_BASE + off[i], got);
if (got !== spec[i]) begin
$display("RESET-VALUE cycle=%0d: %s (0x%03h) reads %08h, register map says %08h", cycle, name[i], off[i], got, spec[i]);
bad++;
end
end
if (bad) fail($sformatf("RESET-VALUE: %0d of 6 registers differ from the register map", bad));
endtaskRun at HEAD, it reports CTRL at cycle 16 (01, where the map says 00) and, in Icarus, FILL at cycle 25 (X), then fails: the same two bugs the reset-value probe found, now as a test anyone can keep in the regression. Cycle 16 instead of 84, with the register named. With CTRL_RST = 8'h00 only FILL is left; at commit 13, before B4, the corrected design passes it in both simulators.
Wrong reset value: fix the value at its one source (dbglab: CTRL_RST in dbglab_pkg.sv), and check that the bench's value came from the spec, not from the same package.
Wrong access type: build fields from a small set of reviewed primitives (RW, RO, W1C, RC) instead of hand-writing each one. The field access types lesson has the set.
Decode overlap: decode every address bit the map uses, and send unmapped offsets to an error response, as dbglab's csr_block does with SLVERR.
Byte lanes: apply each strobe to its own byte, and fix endianness at one boundary, not in every consumer.
Three tests before any other: reset values, a walk of the whole map, and access types per field. UVM's register layer ships them as uvm_reg_hw_reset_seq, uvm_reg_bit_bash_seq and uvm_reg_aliasing_seq; the point is the same with or without UVM.
One source for the map: generate RTL, documentation, the C header and the bench's expected values from it, and review the source, not the outputs. See the generated register workflow and the firmware side.
Lint for unused address bits: a decoder that ignores bits the map uses is an alias. With the BUG = 1 decoder on its own, verilator --lint-only -Wall says Bits of signal are not used: 'addr'[4,1:0] (UNUSEDSIGNAL), and bit 4 is the alias.
The review question: where did each expected value in the register test come from? If the answer is "the RTL package", the test checks nothing about it.
A decode overlap: 0x18 and 0x08 differ only in address bit 4, so the decoder probably ignores that bit and both offsets select the same flops. Write a unique value to every register, then read them all back: if writing 0x18 changes 0x08, it is the alias, as BUG=1's walk showed with 0x10 and 0x00.
Neither opinion decides it: the register map does. If the map says W1C, writing 1 must clear and writing 0 must leave the bit alone, so the RTL is wrong: it implements RW, like BUG=2. The access-type check with both values shows it in two writes.
regs_tb.sv as shown, 2026-10-18: iverilog -g2012 -o regs.vvp regs_tb.sv && vvp -n regs.vvp (Icarus Verilog 13.0) and verilator --binary --timing --assert -Wno-fatal --top-module regs_tb -Mdir vl regs_tb.sv && ./vl/Vregs_tb (Verilator 5.048) print the same six lines and PASS. verilator --lint-only -Wall on a copy of regs5 with only the BUG = 1 decoder (assign sel = {1'b0, addr[3:2]};) reports UNUSEDSIGNAL for 'addr'[4,1:0]; the parameterised version shown reports nothing, because the unused arm still reads addr.
dbglab (rebuilt with dbglab/build-history.sh), a scratch copy at HEAD 49c5f04 with the t_reg_reset task shown added to tb/tests.svh, one dispatch line in tb/dbglab_tb.sv, and RESET-VALUE added to the grep in tb/run.sh: tb/run.sh reg_reset 1 fails with CTRL (cycle 16) and FILL (cycle 25) in Icarus; with CTRL only in Verilator; with both under XRAND=1. With CTRL_RST = 8'h00, only FILL is left, and irq_w1c passes (cycles=316). At commit 13 (03ebdc1, before B4) only CTRL fails, and with CTRL_RST = 8'h00 the test passes in both simulators.
Limits: one small block and two of dbglab's bugs. Byte-strobe and endianness bugs are described, not run here: dbglab's bus has no byte strobes. The register maps are typed by hand; a real project generates them from one source.