Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Part 0 · What Debugging Is6
  • Part 1 · Reproduce8
  • Part 2 · Observe9
  • Part 3 · Localise10
  • Part 4 · Hypothesise and Test6
  • Part 5 · Bug Classes16
    • Logic Bugs: Off-by-One, Width and Sign
    • FSM Bugs
    • X and Reset Bugs
    • Races and Simulation-Synthesis Mismatches
    • Latches and Combinational Loops
    • CDC Bugs
    • Reset-Domain Crossing Bugs
    • Timing-Only Bugs
    • Protocol Violations
    • Hangs and Deadlocks
    • Starvation and Livelock
    • Performance Bugs
    • Power-Intent Bugs
    • DFT-Mode Bugs
    • Configuration and Register-Map Bugs
    • Testbench Bugs That Look Like Design Bugs
  • Part 6 · Debug by Stage9
  • Part 7 · Hangs and Deadlocks7
  • Part 8 · Intermittent Failures7
  • Part 9 · Fix, Prove, Prevent7
  • Part 10 · Case Studies12
  • Part 11 · Debug as a Team5
  • Part 12 · Interview Prep5
Loading lesson

‹Part 5 · Bug Classes
  1. Topics
  2. The Art of Hardware Debug
  3. Part 5 · Bug Classes
  4. Configuration and Register-Map Bugs

Part 5 · Bug Classes · Working (W)

Configuration and Register-Map Bugs

Wrong reset values, wrong field access types, address decode overlap and byte-strobe or endianness mistakes: their signatures, the first three checks, a five-register block with an aliasing decoder and a W1C bit written as RW, and dbglab's wrong reset value caught at cycle 16 by a test written from the register map.

On this page

    Why this matters

    Register bugs are cheap to find with three simple tests and expensive everywhere else: they surface as a driver that misbehaves, an interrupt that never clears or a mode nobody set. Each one breaks a line of the register map, so the map, not the RTL, is where the expected values must come from.

    After this lesson you can: recognise a wrong reset value, a wrong access type, a decode overlap and a byte-lane mistake from their symptoms, and run the reset, walk and access-type checks that find each one in minutes.

    Note

    Level: Working (W) · Debug-loop step: Localise, Hypothesise

    Before this: When the Bug Is in the Test, Bisection in History

    The mode nobody set

    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?

    What people usually do, and why it fails

    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.

    The signature

    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:

    A decoder that looks at addr[3:2] only: offsets 0x00 to 0x0C reach their registers, and 0x10 lands on CTRL as well.

    The first three checks

    On the debug loop these are Localise checks, and each one is cheap enough to run before you look at a single waveform:

    1. 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.

    2. 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.

    3. 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.

    One real reproduction: a five-register block

    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.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    // 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
    
    Terminal
    $ 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.

    The same class in dbglab: B1, the reset value

    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:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 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));
    endtask

    Run 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.

    The fix pattern

    • 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.

    Preventing it

    • 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.

    Traps

    Common pitfall

    Write-then-read-the-same-register tests. They pass for aliasing and for most access-type bugs. Write everything, then read everything.

    Common pitfall

    Fixing a reset value that is really an alias. BUG=1's first failure looks like a wrong reset on TIMEOUT. Editing TIMEOUT's reset value would change nothing; the walk tells you why.

    Why this matters

    Senior lens. Register bugs are cross-team bugs: the map is shared by RTL, DV, firmware and documentation, and each copy can drift. Insist on one source, and put the three map tests at the front of every regression so a broken map fails in seconds instead of as a driver bug weeks later.

    When a driver bug report mentions "a mode nobody set" or "an interrupt that will not clear", ask for a dump of every register after reset and after boot before anyone opens the RTL. Most of these bugs are visible in that dump.

    Interview

    Asked: "How would you verify a block's register map, and what bugs are you looking for?"

    Weak answer: I would write each register and read it back, and check the values match.

    Strong answer: I look for four classes: wrong reset values, wrong access types, decode overlap and byte-lane mistakes. So I read every register straight after reset against the spec, then write a different value to every writable register and read them all back, which catches aliasing that a write-and-read-same test misses. Then I test each field's access type with both values, for example writing 0 and 1 to a W1C bit while hardware sets it, and partial writes with each strobe. The expected values come from the spec or the generator's source, never from the RTL.

    Follow-ups: (1) Why does a write-then-read of the same register miss aliasing? (2) How would you find a wrong reset value that only matters in one test?

    Check yourself After reset, register 0x18 reads exactly the reset value of register 0x08, not its own. What do you suspect, and what one test confirms 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.

    Check yourself An interrupt handler clears STATUS.done by writing 1, and the interrupt never goes low. The RTL designer says the bit works because a write of 0 clears it. Which side is wrong?

    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.

    Remember

    • A wrong reset value only shows before software writes the register. Tests that always program it first never see it.

    • When writing one register changes another, the register you wrote reads back fine. Read every register after writing every register.

    • Test each access type with both values: write 0 and 1 to a W1C bit, write a read-only one, read a read-to-clear one twice.

    Rule of thumb: test the register block against the register map, typed from the spec, before any test that uses the block.

    How this was checked

    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.

    Keep going

    • Practice: Reset value testing of registers with UVM RAL

    • Practice: Aliased and overlapping registers in a RAL model

    • Practice: CSR side effects: W1C and RC constraints and ordering

    • Code lab: Register field access

    • Code lab: Register predict

    • Interview: run a mock interview on this topic

    Related topics

    • Part 5 · Bug Classes

    • RTL: register maps

    • RTL: the CSR contract and address map

    • RTL: byte strobes and partial writes

    • RTL: the firmware-side C driver

    • X and reset bugs

    Previous

    54 / 107

    Next: Testbench Bugs That Look Like Design Bugs

    Up next

    Up nextTestbench Bugs That Look Like Design Bugs

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Logic Bugs: Off-by-One, Width and Sign
    • FSM Bugs
    • X and Reset Bugs