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
    • Reading a Waveform with a Question
    • Dump Strategy
    • Logging That Helps
    • Assertions as Always-On Probes
    • Scoreboards Report Late
    • Comparing a Good and a Bad Run
    • X in a Waveform
    • Counters and Performance Probes
    • Observability by Design
  • Part 3 · Localise10
  • Part 4 · Hypothesise and Test6
  • Part 5 · Bug Classes16
  • 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 2 · Observe
  1. Topics
  2. The Art of Hardware Debug
  3. Part 2 · Observe
  4. Observability by Design

Part 2 · Observe · Principal (P)

Observability by Design

What to build into a design before tape-out so it can be debugged in silicon: sticky errors, counters, trace buffers, state readback and hang detectors, their cost in area and verification, a review checklist, and a real sticky-error and trace-ring block with a self-checking bench.

On this page

    Why this matters

    In simulation you can see every signal; in silicon you see only what you built in before tape-out. A hang with no state readback, no sticky error and no trace can take weeks to reproduce, where a few hundred flops would have named the stage in an afternoon.

    After this lesson you can: choose the debug features a block needs before tape-out, estimate their area and verification cost, review a design against a checklist, and specify a sticky-error and trace-ring block that keeps the first failure.

    Note

    Level: Principal (P) · Debug-loop step: Observe

    Before this: Counters and Performance Probes, Timeouts and Watchdogs in Benches

    Two months before tape-out

    The design review for the DMA subsystem is going well until the bring-up lead asks one question: "When this hangs on the bench, what can I read?" The block owner answers honestly: the CSRs, if the bus works, and scan. Area is tight and the schedule tighter.

    In simulation the deadlock case study took one probe and a wait-for table. On silicon there is no probe. What should this block carry into silicon, and how do you defend its cost?

    What people usually do, and why it fails

    The usual plan is "we will catch everything in simulation", plus scan and a JTAG port for the lab. It fails in three ways. Bugs that escape simulation are the ones it did not provoke: rare orderings, long runs, real software. Scan dump is slow and destructive: it stops the clock and gives you one snapshot of every flop, with no history, after the interesting moment has passed. And the CSRs that would explain a hang are often behind the very bus that is hung.

    dbglab has that last flaw. Its CSR block is a slave on the shared bus, so in the back-pressure deadlock the CPU could not have read a single status register: its request was the one stuck waiting for the bus. Observability that depends on the thing you are debugging is not observability.

    The method: design the questions, then the registers

    Start from the failures, not the features. For each way the block can fail, write the question you will ask on the bench and the register that answers it:

    Feature

    Question it answers on silicon

    Shape

    Sticky error flags with first-error record

    What went wrong first, and when?

    One bit per source, set by hardware, cleared by software (W1C); code and timestamp of the first one

    Event counters

    How much work, how much waiting, since when?

    Handshakes and stall cycles per interface; wide or saturating

    Trace ring with trigger and freeze

    What happened just before (and after) the failure?

    N entries of event code and timestamp; stops N-post entries after the trigger

    State readback

    Where is every FSM and queue now?

    Read-only view of states, pointers, grant and lock, without side effects

    Hang detector

    Has something stopped making progress?

    A per-interface counter of cycles with a request pending and no completion; latches which one

    Side-band access

    Can I read all of this when the main bus is stuck?

    Debug registers on a separate path: a debug APB or a JTAG data register

    • Keep the first failure. A sticky bit set by the tenth error tells you nothing the first did not. Record the first error's source and time, and freeze the trace around it.

    • Read without disturbing. A debug read must not pop a FIFO, clear a flag or change arbitration. Clearing is a separate, deliberate write.

    • Give it a path that does not need the block. The debug registers sit on a port that still works when the functional bus, clock gating or the block's own FSM is stuck.

    • Budget it and test it. Every feature has an area cost and a verification cost. Untested debug logic fails exactly when you need it.

    On the debug loop this is Observe, moved to the start of the project: you are deciding now what the chip will be able to show the Observe step later. It also feeds Prevent: each escape analysis should ask which register would have shown the bug sooner.

    The picture: debug features around a block

    Five debug features watch the block and collect in read-only debug registers. The side-band port, not the main bus, is how the lab reads them when the block is stuck.

    Read it right to left, as the lab will: through the side-band port to the registers, each answering one question about the block.

    Do it: a sticky-error register and a freezing trace ring

    The block below holds two of the features. Four error sources set sticky bits; the first error after a clear also records which source and the timestamp. Every event goes into an 8-entry ring with its timestamp. The first error arms a freeze: two more events are stored, and then the ring stops, so it holds the history around the first failure. A later error neither overwrites the record nor restarts the trace. Clearing all sticky bits re-arms it. The bench checks each of those promises.

    dbg_monitor.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
    // dbg_monitor: two debug features a block can carry into silicon.
    //  - Sticky errors: err_i pulses set bits in sticky_o that stay set until software writes 1 to
    //    clear them (W1C). The first error after a clear also records its code and timestamp.
    //  - Trace ring: every event (ev_valid_i, ev_code_i) is stored with a timestamp in a DEPTH-entry
    //    ring. The first error arms a freeze: POST more events are stored, then the ring stops, so it
    //    holds the history around the first failure. Clearing the sticky bits re-arms it.
    // Read side: tr_idx_i = 0 is the oldest entry, DEPTH-1 the newest.
    `timescale 1ns/1ps
    module dbg_monitor #(parameter int DEPTH = 8, parameter int POST = 2, parameter int CW = 4) (
      input  logic          clk, rst_n,
      input  logic          ev_valid_i,
      input  logic [CW-1:0] ev_code_i,
      input  logic [3:0]    err_i,                       // one bit per error source
      input  logic [3:0]    w1c_i,                       // software clear, one pulse
      output logic [3:0]    sticky_o,
      output logic [3:0]    first_err_o,                 // which source failed first
      output logic [15:0]   first_ts_o,                  // and when
      output logic          frozen_o,
      input  logic [$clog2(DEPTH)-1:0] tr_idx_i,
      output logic [15:0]   tr_ts_o,
      output logic [CW-1:0] tr_code_o
    );
      localparam int AW = $clog2(DEPTH);
      logic [15:0]   ts;                                 // free-running timestamp
      logic [15:0]   ts_mem [DEPTH];
      logic [CW-1:0] code_mem [DEPTH];
      logic [AW-1:0] wptr;                               // next slot to write = oldest entry
      logic          armed;
      logic [$clog2(POST+1)-1:0] post_left;
    
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin
          ts <= '0; sticky_o <= '0; first_err_o <= '0; first_ts_o <= '0;
          wptr <= '0; armed <= 1'b0; frozen_o <= 1'b0; post_left <= '0;
        end else begin
          ts <= ts + 16'd1;
          // sticky bits: a new error wins over a clear in the same cycle
          sticky_o <= (sticky_o & ~w1c_i) | err_i;
          if (|err_i && sticky_o == 4'b0000) begin
            first_err_o <= err_i;
            first_ts_o  <= ts;
          end
          // trace ring with freeze
          if (ev_valid_i && !frozen_o) begin
            ts_mem[wptr]   <= ts;
            code_mem[wptr] <= ev_code_i;
            wptr <= wptr + 1'b1;
            if (armed) begin
              post_left <= post_left - 1'b1;
              if (post_left == 1) frozen_o <= 1'b1;
            end
          end
          if (|err_i && !armed && !frozen_o) begin
            armed <= 1'b1;
            post_left <= POST[$clog2(POST+1)-1:0];
          end
          if (|w1c_i && (sticky_o & ~w1c_i) == 4'b0000 && err_i == 4'b0000) begin
            armed <= 1'b0; frozen_o <= 1'b0;             // all clear: re-arm
          end
        end
    
      assign tr_ts_o   = ts_mem[AW'(wptr + tr_idx_i)];
      assign tr_code_o = code_mem[AW'(wptr + tr_idx_i)];
    endmodule
    // tb_dbg_monitor: 20 events (an idle cycle after every third), an error from source 2 with event 12, a later error from source 0.
    // Checks the sticky bits, the first-error record, the frozen trace and the re-arm after W1C.
    `timescale 1ns/1ps
    Terminal
    $ iverilog -g2012 -o t.vvp dbg_monitor_tb.sv && vvp -n t.vvp
    sticky=0101 first_err=0100 first_ts=16 frozen=1
      trace[0] ts=9 code=7
      trace[1] ts=10 code=8
      trace[2] ts=12 code=9
      trace[3] ts=13 code=10
      trace[4] ts=14 code=11
      trace[5] ts=16 code=12
      trace[6] ts=17 code=13
      trace[7] ts=18 code=14
    PASS: first error and the 8 events around it kept; later error did not overwrite them
    dbg_monitor_tb.sv:121: $finish called at 286000 (1ps)

    Read it as the lab would after a failure. sticky=0101: sources 2 and 0 both failed. first_err=0100 at timestamp 16: source 2 came first, and the record survived source 0's later error. The trace holds events 7 to 14 in order: five before the error, event 12 that came with it at timestamp 16, and two after. The gaps in the timestamps (10 to 12, 14 to 16) are the idle cycles, which is why a trace needs timestamps and not just an order. Events 15 to 19 are missing on purpose: they came after the freeze. Verilator 5.048 prints the same lines and PASS.

    A bench must be able to fail. Remove the freeze:

    Terminal
    $ sed 's/if (ev_valid_i && !frozen_o) begin/if (ev_valid_i) begin/' dbg_monitor_tb.sv > mutant.sv
    $ iverilog -g2012 -o m.vvp mutant.sv && vvp -n m.vvp
    ...
      trace[7] ts=25 code=3
    FAIL: trace[7] should be event 14
    FATAL: mutant.sv:119: 8 check(s) failed
    ...

    Without the freeze the ring keeps recording, and the events around the failure are gone by the time anyone reads it: all eight trace checks fail.

    What it costs

    Terminal
    $ yosys -q -p "read_verilog -sv dbg_monitor.sv; chparam -set DEPTH 8 dbg_monitor; synth -top dbg_monitor; tee -o stat8.txt stat"
    Warning: Replacing memory \code_mem with list of registers. See dbg_monitor.sv:46
    Warning: Replacing memory \ts_mem with list of registers. See dbg_monitor.sv:45
    $ yosys -q -p "read_verilog -sv dbg_monitor.sv; chparam -set DEPTH 32 dbg_monitor; synth -top dbg_monitor; tee -o stat32.txt stat"
    Warning: Replacing memory \code_mem with list of registers. See dbg_monitor.sv:46
    Warning: Replacing memory \ts_mem with list of registers. See dbg_monitor.sv:45
    $ grep -E "cells$|DFF" stat8.txt
          614 cells
           27   $_DFFE_PN0P_
          160   $_DFFE_PP_
           20   $_DFF_PN0_
    $ grep -E "cells$|DFF" stat32.txt
         2166 cells
           29   $_DFFE_PN0P_
          640   $_DFFE_PP_
           20   $_DFF_PN0_

    Eight entries of 20 bits cost 207 flip-flops and 614 generic cells; 32 entries cost 689 and 2,166. The 160 and 640 enable flops are the trace storage itself (8 or 32 entries times 20 bits); the rest, 47 flops at depth 8 and 49 at depth 32, is the sticky register, the first-error record, the timestamp and the pointers, and it barely grows. So the decision that matters is depth and width: how much history, and how many bits per event. Yosys's warnings say the same thing: with no RAM to map to, it turned the trace memory into registers. A deep trace usually goes into a small RAM instead.

    The verification cost is as real: thirteen checks and a mutation run for this block, and every shipped feature also needs a test that reads it through the lab's side-band path.

    What the evidence says

    A few hundred flops buy an answer to "what happened first, and what led up to it", which is the question every silicon hang starts with. The cost scales with trace depth, so spend it where the questions are hardest: interfaces between blocks, arbiters, anything with a FIFO that can fill. For dbglab, the review would add three things: grant, lock and DMA state readable on a side-band port; a hang detector on each master port; and a short trace of bus handshakes that freezes on the detector.

    A review checklist

    Ask

    A good answer looks like

    For each failure mode, which register shows it on silicon?

    A named register per mode: error source, stuck state, overflow

    Is the first failure kept?

    Sticky bits plus a first-error record; the trace freezes on a trigger

    Can it be read when the block or the bus is hung?

    A side-band path: debug APB or a JTAG data register, on an always-on clock

    Do reads have side effects?

    None; clears are explicit W1C writes

    Are counters wide enough or saturating?

    Width chosen for the longest test, or saturate and flag

    Can every FSM state and FIFO pointer be read?

    Yes, read-only, without stopping the clock

    Is there a hang detector per interface?

    A pending-without-progress counter with a programmable limit and a latched cause

    Is the debug logic verified and budgeted?

    Its own tests, a mutation or fault check, and a line in the area budget

    Is debug access locked in production parts?

    Access control agreed with security; trace contents reviewed for secrets

    Traps

    Common pitfall

    Debug registers behind the functional bus. They work in every test that does not need them and fail in the one that does. Check the read path for every hang you can imagine: a hung arbiter, a gated clock, a block held in reset.

    Common pitfall

    A trace that never stops. A ring buffer without a trigger and freeze holds the last N events before you read it, which is minutes after the failure. Decide the trigger at design time: the first sticky error, a hang detector, or a software write.

    Why this matters

    Principal lens. Debug features are a budget line, not an afterthought. As an illustrative figure, teams often hold a percent or two of a block's area for observability, and argue about every entry of trace depth. Spend it by asking which bugs would cost a respin to find without it: hangs at interfaces, rare orderings, anything that depends on software. Cut it where simulation coverage is strong and the logic is simple.

    Make observability a sign-off item. The design review should walk the checklist above for every block, and every escape analysis should end with "which register would have shown this earlier?" and a change to the next chip. The bring-up course covers the lab side: debug interfaces for on-chip trace, JTAG access and scan dump.

    Interview

    Asked: "You have two months before tape-out and a small area budget. What debug features would you add to a DMA and bus subsystem?"

    Weak answer: I would add as many counters and trace buffers as the area allows, and rely on scan and JTAG for anything else.

    Strong answer: I start from the failure modes, mostly hangs and wrong data at interfaces, and pick the register that shows each: readback of grant, lock and DMA state; a hang detector per master port; sticky errors with a first-error record; stall counters; and a short handshake trace that freezes on the first error. All readable through a side-band port, because the CSRs sit behind the bus that hangs. Each feature gets its own tests and a line in the area budget.

    Follow-ups: (1) Why must the trace freeze, and on what trigger? (2) How do you verify debug logic that functional tests never exercise?

    Check yourself A colleague proposes a 64-entry trace of full bus transactions (address, data, control: about 70 bits) to replace the 8-entry, 20-bit trace. From the Yosys numbers, roughly what does it cost, and what would you ask first?

    Storage is entries times width: 4,480 bits against 160 here, about 28 times the storage, which belongs in a RAM. First ask which question needs data values: a hang needs order and timing, not data. A narrow handshake trace with a deeper history often answers more for less.

    Check yourself In dbglab's back-pressure deadlock, which features from the checklist would have let the lab name the stuck stage on the first hung board, and why is the side-band path essential here?

    State readback of the DMA state, dreq, grant and lock; the FIFO's fullness; and a hang detector on the CPU port showing a request pending without progress. Together they give the wait-for table directly. The side-band path is essential because the CPU's own request is the one stuck: any register read through the main bus would hang too.

    Remember

    • Silicon shows only what you designed to be seen. Decide the questions you will ask of a hung chip, then build the registers that answer them.

    • Keep the first failure: sticky errors with a first-error record, and a trace that freezes on the trigger. Later errors must not overwrite the evidence.

    • Debug state must be readable when the main bus is hung, through a side-band port. Debug logic is logic: it needs tests and an area budget.

    Rule of thumb: for every way the block can fail, name the register that will show it on silicon, and a path to read it that does not need the block to work.

    How this was checked

    dbg_monitor and tb_dbg_monitor (one file), 2026-10-17: iverilog -g2012 -o t.vvp dbg_monitor_tb.sv && vvp -n t.vvp (Icarus Verilog 13.0) and verilator --binary --timing --assert -Wno-fatal --top-module tb_dbg_monitor (Verilator 5.048, no warnings) both print the trace shown and PASS.

    Mutation check: the same bench with the freeze removed (if (ev_valid_i) begin in place of if (ev_valid_i && !frozen_o) begin) fails 8 checks. Area: yosys -p "read_verilog -sv dbg_monitor.sv; chparam -set DEPTH 8 dbg_monitor; synth -top dbg_monitor; stat" (Yosys 0.69): 614 generic cells, 207 flip-flops; DEPTH 32: 2166 cells, 689 flip-flops.

    dbglab facts (HEAD 49c5f04): its CSR block is a slave on the same bus as the DMA, and in the dma_burst_backpressure hang the CPU's request waits for ever (see the case study), so no CSR could be read there.

    Limits: generic Yosys cells, not a standard-cell library, so the numbers compare sizes rather than give square microns. Percentages of die area in the lens are illustrative. No silicon, JTAG or trace hardware is run here; see the bring-up course.

    Keep going

    • Practice: Performance counters and CSR access

    • Practice: Inline checkers: X detection and handshake liveness

    • Practice: Deadlock and livelock: detection, debug and SVA

    • Code lab: Watchdog FSM

    • Code lab: Silicon bring-up smoke

    • Interview: run a mock interview on this topic

    Related topics

    • Part 2 · Observe

    • Bring-up: debug interfaces (trace, JTAG, scan dump)

    • Counters and performance probes

    • Case study: the deadlock under back-pressure

    • Part 11: debug as a team

    Previous

    23 / 107

    Next: Part 3 · Localise

    Up next

    Up nextPart 3 · Localise

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Reading a Waveform with a Question
    • Dump Strategy
    • Logging That Helps