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. Power-Intent Bugs

Part 5 · Bug Classes · Senior (S)

Power-Intent Bugs

Missing isolation, missing retention and a wrong power-up order: what each looks like in a simulation with a supply model, the first three checks, a small power domain with both bugs in one self-checking bench, and dbglab's missing isolation clamp caught by an always-on check at the cycle it happens.

On this page

    Why this matters

    A power bug passes every test that never switches a domain off, then fails after the first sleep, often only in one simulator or only on silicon. It is cheap to find in simulation if you line the failure up with the power-state timeline, and very expensive once a chip forgets its configuration on every wake.

    After this lesson you can: tell missing isolation, missing retention and a wrong power-up order apart from a log or waveform, run the three checks that confirm each, and add the always-on assertion that catches it the cycle it happens.

    Note

    Level: Senior (S) · Debug-loop step: Localise, Hypothesise

    Before this: X in a Waveform, Bisection in Space

    The block that forgets after its first sleep

    A sensor hub passes its whole regression. The first time the firmware team adds a sleep and wake to a test, three things go wrong at once: the hub stops sending samples, its mode register reads 00, and the always-on interrupt counter shows one event nobody sent. In Icarus a pin goes X; in Verilator the same test hangs. Nothing in the RTL changed. Where do you start?

    What people usually do, and why it fails

    Two habits: call it X pessimism and add a reset, so the X goes away and the bug ships; or debug the data path, where the failure was reported. Both miss what is special here: the logic is correct in every cycle where the supply is on. The bug is in power intent, the rules for what happens at a domain's edges when its supply goes off and comes back.

    Plain RTL simulation has no supplies, so it cannot see these bugs. Real flows describe the intent in UPF and use a power-aware simulator; this page uses a hand-written supply model, as dbglab does: an off domain's outputs are X in a four-state simulator and noise in a two-state one.

    The signature

    Sub-class

    What you see

    When

    Missing isolation

    X (Icarus) or noise (Verilator) on the always-on side; phantom events, a hang, or a FIFO pointer gone X

    starts the cycle the domain powers off, while iso_en is high

    Missing retention

    a register reads its reset value after a wake; the block runs as if never configured

    first access after a wake; only with non-default settings before the sleep

    Wrong order

    X or a glitch for one to a few cycles, or a lost restore

    in the window around a power transition: isolation released before power, restore after isolation

    Here is the retention case as a waveform. Two copies of the same domain sleep and wake in the same cycles; mode_good has a retention shadow, mode_noret does not, and raw_noret is the domain's own output before the clamp.

    Read it as the power-state timeline first, then the data.

    • Slot 1: software writes 5b. Slot 3: isolation goes on, so both clamped outputs read 00 from here.

    • Slot 4: the domain is held in reset, so its own output drops to 0. Slots 5 to 7: the supply is off and the output inside the domain is X; outside, the clamp holds 00, which is what isolation is for.

    • Slot 10: restore. Slot 11: isolation comes off. mode_good reads 5b again; mode_noret reads 00, its reset value, not X and not garbage.

    The first three checks

    On the debug loop these checks are Localise: they turn "something broke after a sleep" into one boundary signal or one register.

    1. Line the first wrong value up with the power timeline. Print pwr_on, iso_en, the domain reset and save/restore around the first wrong cycle. Inside an off window points at isolation; just after a wake points at retention; at a transition edge points at order.

    2. List every signal that leaves the switched domain, and who reads it. For each one whose reader is powered differently, check that it is clamped while iso_en is high. A wrong comment is not evidence; the reader's supply is.

    3. For every state that must survive, check save, restore and order. Compare each value after the wake with its value before the sleep, and compare the sequence with the spec: save, isolate, reset, power off; then power on, release reset, restore, release isolation.

    One real reproduction: a small power domain

    The bench runs three copies of one domain through the same sleep and wake: correct, without retention on mode_q, and with a controller that releases isolation before the supply comes back. Two always-on checks watch each copy: the clamp must hold while the supply is off, and mode_o must read the same after a wake as before the sleep.

    pd_core.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
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    // pd_core: a block in a switchable power domain. mode_q is its configuration (bit 0 enables a
    // tick every fourth cycle). RET = 1 gives mode_q a retention shadow on the always-on supply:
    // save copies it, restore puts it back. RET = 0 is the bug: nothing keeps mode_q across power-off.
    // Supply model (simulation only): while pwr_on is low the outputs are not logic values, X in a
    // four-state simulator and noise in a two-state one.
    `timescale 1ns/1ps
    module pd_core #(parameter bit RET = 1) (
      input  logic       clk, pwr_on, pd_rst_n, save, restore, wr,
      input  logic [7:0] wdata,
      output logic [7:0] mode_p,
      output logic       tick_p
    );
      logic [7:0] mode_q, ret_q;
      logic [1:0] cnt_q;
      always_ff @(posedge clk or negedge pd_rst_n)
        if (!pd_rst_n)         begin mode_q <= 8'h00; cnt_q <= '0; end
        else begin
          if (restore && RET)  mode_q <= ret_q;
          else if (wr)         mode_q <= wdata;
          cnt_q <= cnt_q + 1'b1;
        end
      always_ff @(posedge clk) if (save) ret_q <= mode_q;          // always-on shadow
    `ifdef VERILATOR
      logic [8:0] noise = 9'h1a5;
      always_ff @(posedge clk) noise <= {noise[7:0], noise[8] ^ noise[4]};
      assign {mode_p, tick_p} = pwr_on ? {mode_q, mode_q[0] && cnt_q == 2'd3} : noise;
    `else
      assign {mode_p, tick_p} = pwr_on ? {mode_q, mode_q[0] && cnt_q == 2'd3} : 9'bx;
    `endif
    endmodule
    
    // pmu: the always-on power controller. Down: save, isolate, reset, power off.
    // Up (EARLY_ISO = 0): power on, release reset, restore, then release isolation.
    // EARLY_ISO = 1 is the second bug: isolation is released first, while the supply is still off.
    module pmu #(parameter bit EARLY_ISO = 0) (
      input  logic clk, rst_n, sleep_req, wake_req,
      output logic pwr_on, iso_en, pd_rst_n, save, restore, ready
    );
      typedef enum logic [3:0] {BOOT, ON, SAVE, ISO, RST, OFF, UP, UNRST, RESTORE} st_t;
      st_t st;
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) st <= BOOT;
        else case (st)
          BOOT:    st <= ON;
          ON:      if (sleep_req) st <= SAVE;
          SAVE:    st <= ISO;
          ISO:     st <= RST;
          RST:     st <= OFF;
          OFF:     if (wake_req) st <= UP;
          UP:      st <= UNRST;
          UNRST:   st <= RESTORE;
          default: st <= ON;
        endcase
      always_comb begin
        {pwr_on, iso_en, pd_rst_n, save, restore, ready} = 6'b101001;  // ON: powered, not isolated, ready
        case (st)
          BOOT:    {iso_en, pd_rst_n, ready} = 3'b100;
          SAVE:    {save, ready} = 2'b10;
          ISO:     {iso_en, ready} = 2'b10;
          RST:     {iso_en, pd_rst_n, ready} = 3'b100;
          OFF:     {pwr_on, iso_en, pd_rst_n, ready} = 4'b0100;
          UP:      {pwr_on, iso_en, pd_rst_n, ready} = EARLY_ISO ? 4'b0000 : 4'b1100;
          UNRST:   {iso_en, ready} = {!EARLY_ISO, 1'b0};
          RESTORE: {iso_en, restore, ready} = {!EARLY_ISO, 2'b10};
          default: ;
        endcase
        if (!rst_n) pd_rst_n = 1'b0;
      end
    endmodule
    
    // pd_unit: one domain, its controller and its isolation clamps (to 0) on the always-on side.
    module pd_unit #(parameter bit RET = 1, parameter bit EARLY_ISO = 0) (
      input  logic       clk, rst_n, sleep_req, wake_req, wr,
      input  logic [7:0] wdata,
      output logic [7:0] mode_o,
      output logic       tick_o, ready, iso_en, pwr_on
    );
      logic pd_rst_n, save, restore;
      logic [7:0] mode_p;
      logic       tick_p;
      pmu #(.EARLY_ISO(EARLY_ISO)) u_pmu (.clk, .rst_n, .sleep_req, .wake_req, .pwr_on, .iso_en,
                                          .pd_rst_n, .save, .restore, .ready);
      pd_core #(.RET(RET)) u_core (.clk, .pwr_on, .pd_rst_n, .save, .restore, .wr, .wdata, .mode_p, .tick_p);
      assign mode_o = iso_en ? 8'h00 : mode_p;
      assign tick_o = iso_en ? 1'b0  : tick_p;
    endmodule
    
    // pd_check: always-on checks. ISO: while the domain's supply is off, its outputs must reach the
    // always-on side as the clamp value. RET: after a wake, mode_o must read what it read before the sleep.
    module pd_check (
      input logic       clk, ready, iso_en, pwr_on,
      input logic [7:0] mode_o,
      input logic       tick_o,
      input logic [7:0] pre_mode
    );
      int    cycle = 0, n_iso = 0, n_ret = 0;
      string first = "";
      bit    was_ready = 1'b0;
      always @(posedge clk) begin
        #1;
        if (!pwr_on && (mode_o !== 8'h00 || tick_o !== 1'b0)) begin
          n_iso++;
          if (first == "") first = $sformatf("cycle %0d: ISO: mode_o=%h tick_o=%b with the supply off (iso_en=%b)",
                                             cycle, mode_o, tick_o, iso_en);
        end
        if (ready && !was_ready && cycle > 2 && mode_o !== pre_mode) begin
          n_ret++;
          if (first == "") first = $sformatf("cycle %0d: RET: mode_o=%h after wake, %h before sleep", cycle, mode_o, pre_mode);
        end
        was_ready = ready;
        cycle++;
      end
    endmodule
    
    Terminal
    $ iverilog -g2012 -o pd.vvp pd_tb.sv && vvp -n pd.vvp
    good      (retention, isolate until restored): clean
    no_ret    (mode_q not retained):               cycle 23: RET: mode_o=00 after wake, 5b before sleep
    early_iso (isolation released before power):   cycle 20: ISO: mode_o=xx tick_o=x with the supply off (iso_en=0)
    after wake: mode_o good=5b no_ret=00 early_iso=5b
    PASS: retention loss and early isolation release both caught; the good sequence is clean
    ...
    $ verilator --binary --timing --assert -Wno-fatal --top-module pd_tb -Mdir vl pd_tb.sv && ./vl/Vpd_tb
    ...
    good      (retention, isolate until restored): clean
    no_ret    (mode_q not retained):               cycle 23: RET: mode_o=00 after wake, 5b before sleep
    early_iso (isolation released before power):   cycle 20: ISO: mode_o=63 tick_o=1 with the supply off (iso_en=0)
    after wake: mode_o good=5b no_ret=00 early_iso=5b
    PASS: retention loss and early isolation release both caught; the good sequence is clean
    ...

    Line by line. no_ret fails the retention check at cycle 23, the first cycle it is ready again, with 00: its reset value, which looks like a configuration bug and is not one. early_iso fails at cycle 20, the one cycle in which isolation is already off and the supply is not yet on. In Icarus the value is X. In Verilator there is no X, so the same cycle carries noise: tick_o=1, an event the domain never sent. A consumer that counts ticks would count one more, which is the phantom interrupt from the scene.

    The same class in dbglab: B5, the missing clamp

    dbglab's periph_power test powers the peripheral's sink down and up with words waiting in the async FIFO. At HEAD it fails differently in each simulator, which is the signature in the table above:

    Terminal
    $ tb/run.sh periph_power 1
    dbglab: test=periph_power seed=1
    FAIL test=periph_power seed=1 cycle=431: STREAM: pstream_valid is unknown
    $ tb/run.sh periph_power 1 verilator
    dbglab: test=periph_power seed=1
    FAIL test=periph_power seed=1 cycle=2421: WATCHDOG: no bus or stream progress for 2000 cycles; cpu_valid=0 cpu_ready=0 cpu_addr=00003008

    Bisection in space and cone-of-influence tracing walk this failure back to one line, so here is only the class view. Check 2 asks for the domain's outputs and their readers. periph_pd has three: out_valid and out_data go to the pins, and rinc goes to the async FIFO's read side, which is always on. A check that states the isolation rule for all three, compiled next to the bench, finds the breach at its first cycle:

    iso_probe.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // not-run: needs dbglab's sources and bench; compile it next to them (command in the Evidence box)
    // iso_probe: the isolation rule as an always-on check. While iso_en is high, every output of the
    // switched domain periph_pd must be at its clamp value (0). It only reads the design.
    `timescale 1ns/1ps
    module iso_probe;
      int n = 0;
      always @(posedge dbglab_tb.dut.u_per.u_pd.pclk) begin
        #0.1;
        if (dbglab_tb.dut.u_per.u_pd.iso_en &&
            (dbglab_tb.dut.u_per.u_pd.rinc !== 1'b0 || dbglab_tb.dut.u_per.u_pd.out_valid !== 1'b0
             || dbglab_tb.dut.u_per.u_pd.out_data !== 32'h0)) begin
          if (n == 0)
            $display("ISO-CHECK cycle %0d: iso_en=1 but rinc=%b out_valid=%b out_data=%h (pwr_on=%b)",
                     dbglab_tb.cycle, dbglab_tb.dut.u_per.u_pd.rinc, dbglab_tb.dut.u_per.u_pd.out_valid,
                     dbglab_tb.dut.u_per.u_pd.out_data, dbglab_tb.dut.u_per.u_pd.pwr_on);
          n++;
        end
      end
      final if (n) $display("ISO-CHECK: %0d pclk edges with an unclamped output", n);
    endmodule
    Terminal
    $ ../probes/run-probe.sh ../probes/iso_probe.sv periph_power 1
    dbglab: test=periph_power seed=1
    ISO-CHECK cycle 76: iso_en=1 but rinc=x out_valid=0 out_data=00000000 (pwr_on=0)
    FAIL test=periph_power seed=1 cycle=431: STREAM: pstream_valid is unknown
    ISO-CHECK: 86 pclk edges with an unclamped output
    $ grep -n "assign rinc" rtl/periph_pd.sv
    34:  assign rinc      = rinc_p;          // internal: only feeds our own async FIFO

    Cycle 76, with the supply off and isolation on: rinc is X while the two pin outputs are clamped. Cycle 76 is when this probe first looks, not when rinc went X. Cycles count the bus clock: the supply drops at 725 ns (cycle 73) and rinc goes X at that instant, the 73 that cone-of-influence tracing reads from the waveform; the falling-pclk monitor in bisection in space first sees it at 740 ns (cycle 74); this probe samples just after rising pclk edges, the first at 759 ns (cycle 76). Even that is 355 cycles before the bench's own check, in the right domain and on the right signal. The comment on line 34 is the mistake in words: rinc stays inside the peripheral, but its reader is not in the switched domain.

    The fix pattern

    • Missing isolation: clamp the signal at the domain boundary with the controller's iso_en, to the value its reader treats as idle. For B5: assign rinc = iso_en ? 1'b0 : rinc_p;. With it, periph_power passes in both simulators (cycles=469) and the probe is silent.

    • Missing retention: make the state retained (a retention flop or a save/restore shadow), or make software reprogram it after every wake and say so in the spec. Do not paper over it with a different reset value.

    • Wrong order: fix the controller's sequence, not the consumer. Making the reader ignore one bad cycle hides the bug until the timing changes.

    Preventing it

    • An always-on isolation check per domain, generated from the list of boundary ports, not typed by hand: while iso_en, every crossing output equals its clamp. The probe above is that check for one domain.

    • A retention check after every wake that compares each state that must survive with its value before the sleep, as pd_check does.

    • A power test with non-default settings. Most retention bugs survive because every test sleeps straight after reset, when the reset value and the saved value are the same.

    • The review question: for each output of a switchable domain, which supply powers its reader? In a real flow, a static power-intent check answers it for every port; see the low-power verification course.

    Traps

    Common pitfall

    Silencing the X. Giving the reader a reset, or $isunknown filters in the bench, makes the Icarus failure go away and leaves the Verilator hang, which is the closer picture of silicon: a real unpowered output is a random level, not X.

    Common pitfall

    Sleeping only straight after reset. If the saved value equals the reset value, a missing retention flop is invisible. Write a non-default value first, as the bench does with 5b.

    Why this matters

    Senior lens. When a failure differs between a four-state and a two-state simulator, rank power intent next to uninitialised state: X in one, noise or a hang in the other is exactly what an unpowered output does. The simulator matrix puts B5 in that row.

    In a triage, ask for the power-state timeline before the data trace: five signals per domain usually place the bug in one sub-class within minutes.

    Interview

    Asked: "After a power-down and power-up, a block's configuration register reads its reset value. What is wrong and how do you confirm it?"

    Weak answer: The reset is probably glitching during power-up, so I would look at the reset synchroniser and maybe add a filter.

    Strong answer: A reset value after a wake usually means the register was not retained: the domain lost it while off and the reset on power-up loaded the default. I would check three things: is the register in the retention list, did the controller pulse save before power-off and restore after power-on, and was the restore before isolation was released. I would confirm it with a test that writes a non-default value, sleeps and checks the value after the wake, because a test that sleeps straight after reset cannot tell retention from reset.

    Follow-ups: (1) How would the same bug look if the restore happened after isolation was released? (2) Why can a missing clamp show up as a hang in Verilator but an X in Icarus?

    Check yourself An always-on counter of interrupts from a power-gated block is one higher than expected after each sleep, but only in Verilator. In Icarus the interrupt line is X for one cycle. Which sub-class, and what do you check first?

    Wrong order or missing isolation: a one-cycle X in four-state, a real pulse in two-state. Check the power timeline at that cycle: if iso_en is already low while pwr_on is still low (or the restore has not happened), it is an order bug in the controller, as early_iso showed. If iso_en is high, the interrupt line is not clamped.

    Check yourself A colleague fixes B5 by adding rinc to the clamp list and asks whether the other two outputs need checking again. What do you answer?

    Check the list, not the fix: the bug was a port missing from the list, so the review question is whether any other signal leaves periph_pd for an always-on reader. Here there are only three ports, and the isolation probe checks all three on every pclk edge while iso_en is high; it stays silent with the fix.

    Remember

    • Missing isolation: X or noise on the always-on side, starting the cycle a domain powers off. In a two-state simulator it can show up as a hang or a phantom event.

    • Missing retention: after a wake the register reads its reset value, not garbage. Only tests that sleep with non-default settings see it.

    • Order bugs live in a window of a few cycles around a power transition. Check the sequence against the spec before you look at the data.

    Rule of thumb: for every signal that leaves a switchable domain, ask which supply powers its reader; if it is a different one, the signal needs a clamp.

    How this was checked

    pd_tb.sv as shown, 2026-10-18: iverilog -g2012 -o pd.vvp pd_tb.sv && vvp -n pd.vvp (Icarus Verilog 13.0) and verilator --binary --timing --assert -Wno-fatal --top-module pd_tb -Mdir vl pd_tb.sv && ./vl/Vpd_tb (Verilator 5.048) print the lines shown and PASS; the only difference is the early_iso line (xx/x in Icarus, 63/1 in Verilator). The waveform is generated from wave-src/power-intent-bugs/retention.sv with COURSE=dbg node scripts/tools/gen-waves.mjs bug-classes:power-intent-bugs.

    dbglab at HEAD 49c5f04 (rebuilt with dbglab/build-history.sh): tb/run.sh periph_power 1 fails at cycle 431 in Icarus and at cycle 2421 (WATCHDOG) in Verilator. iso_probe.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 iso_probe.sv, vvp -n build/probe.vvp +TEST=periph_power +SEED=1) prints its first line at cycle 76 for seeds 1, 2 and 3. In a scratch copy with assign rinc = iso_en ? 1'b0 : rinc_p; the test passes in both simulators (cycles=469) and the probe is silent on smoke, periph_stream, periph_power and random, seeds 1 to 3.

    Limits: the supply model is hand-written RTL (X or noise on the outputs of an off domain), not UPF in a power-aware simulator, which also corrupts internal state and checks the power intent itself. No silicon, no static power-intent checker. The isolation check covers the ports listed in it; a port missing from the list is unchecked.

    Keep going

    • Practice: Power-gating controller: retention, isolation and safe sequencing

    • Practice: Isolation clamp mismatch on power-gate entry and exit

    • Practice: Retention save and restore mismatch across power-down

    • Code lab: Isolation cell model

    • Code lab: Retention flop model

    • Code lab: Retention save and restore

    • Interview: run a mock interview on this topic

    Related topics

    • Part 5 · Bug Classes

    • Low-power verification: state-loss scenarios

    • Low-power verification: power-mode sequencing

    • Low-power verification: corruption and retention in simulation

    • RTL: power domains and isolation intent

    • RTL: low-power design

    • Part 10: case studies, including the missing isolation cell

    Previous

    52 / 107

    Next: DFT-Mode Bugs

    Up next

    Up nextDFT-Mode 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
    pwr_oniso_enrestoreraw_noret05b0x0mode_noret05b0mode_good05b05b
    One sleep and wake. The good domain comes back with 5b; the one without retention comes back with its reset value 00.