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. Timing-Only Bugs

Part 5 · Bug Classes · Senior (S)

Timing-Only Bugs

Bugs that zero-delay RTL simulation cannot show: a multicycle path assumed in the constraints but not built into the control, hold violations, and inputs with no timing constraint. A behavioural timing model of a multiply fails where zero-delay simulation passes, with the signature of a setup failure: high bits wrong, gone at a slower clock.

On this page

    Why this matters

    Zero-delay RTL simulation treats every path as instant, so a design can pass every test and static timing can pass every check while silicon computes the wrong answer. Recognising a timing-only failure early sends the hunt to the constraints and the netlist instead of the logic.

    After this lesson you can: recognise a setup, hold or exception failure from how it moves with frequency and which bits fail, reproduce a multicycle-path bug with a timing model, and check the design keeps what the constraints assume.

    Note

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

    Before this: When Simulation Cannot Reproduce It, Differential Debugging

    Right in every simulation, wrong on the chip

    A DSP block multiplies 8-bit samples. Its RTL regression is clean, static timing closed with positive slack, and the first silicon gives products whose low byte is right and whose high byte is often wrong. Run the part at a slower clock and the errors stop.

    The RTL has not changed since sign-off, and simulation still passes. Where do you look for a bug that only a clock period can see?

    What people usually do, and why it fails

    They rerun the RTL tests with the failing operands, which pass, and conclude the silicon is marginal. Or they search the multiplier for a logic bug that does not exist. Zero-delay simulation computes a * b in no time, so it cannot fail for lack of time. Static timing passed because it checked the path against the time the constraints allowed it, not the time the design gave it.

    The signature

    Bug

    What you see

    How it moves

    Setup (path too slow)

    the bits at the end of a long path are wrong: high bits of a sum or product, the last stage of a mux tree

    goes away at a slower clock or higher voltage; usually worse when hot (at low voltage on newer processes, cold can be worse)

    Hold (path too fast)

    a register takes the next value one cycle early: a shift register skips a stage, a pipeline passes data through twice

    stays at every frequency; changes with voltage, temperature and skew

    Exception the design does not keep

    static timing clean, gate-level simulation with timing or silicon fails on a path covered by a multicycle or false path

    like setup or hold on exactly the excepted paths

    Unconstrained input

    an input from a pin or another block is captured wrongly on some boards, cables or temperatures

    follows the board and the environment, not the stimulus

    On the debug loop this is Localise and Hypothesise: the way the failure moves with frequency, voltage and temperature tells you the class before you open the netlist. The bring-up course covers measuring that on a real part.

    The first three checks

    1. Move the clock. Does the failure disappear at a lower frequency (setup-type) or stay at every frequency (hold-type)? Which bits fail, and are they the slow ones?

    2. Read the exceptions on the failing path. List every multicycle path, false path and clock-group constraint that covers it, and for each one find the RTL that makes it true: an enable, a hold condition, a synchroniser. No such RTL means the exception is a wish.

    3. Simulate with time. Run gate-level simulation with the delay file on the suspect path, or a behavioural delay model of it, and compare with zero-delay simulation on the same stimulus.

    The picture: when the product arrives

    The operands load at 0 ns. Bit 0 of the product settles at 6 ns, bit 15 at 15 ns. The buggy unit captures at 10 ns, when bits 7 to 15 still show the old product; the fixed unit captures at 20 ns.

    One real reproduction: the multicycle path that was never built

    The constraint file says the multiply from the operand registers to the product register is a two-cycle multicycle path, so static timing allows it 20 ns at a 10 ns clock. In the buggy unit nobody changed the control: it takes new operands every cycle and captures the product one cycle after loading, so the path really has 10 ns. The fixed unit keeps the promise: it loads only when idle and captures two cycles later, with in_ready low in between.

    mul_path stands in for the gate-level netlist. With TIMED = 0 it is ordinary zero-delay RTL. With TIMED = 1, bit i of the product shows a new value 6 + 0.6 i ns after the operands change, a transport delay like a carry chain's. The bench sends 300 random products into four units, each design zero-delay and timed.

    mul_path.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
    `timescale 1ns/1ps
    // mul_path: the combinational 8x8 multiply between the operand registers and the product
    // register. TIMED = 0 is zero-delay RTL. TIMED = 1 is a behavioural timing model of a carry
    // chain: output bit i shows a new product 6 + 0.6*i ns after the operands change (bit 0 after
    // 6 ns, bit 15 after 15 ns). It keeps the last 8 operand changes and, every 0.1 ns, gives each
    // bit the value of the newest product that has had time to reach it (a transport delay).
    module mul_path #(parameter bit TIMED = 0) (input logic [7:0] a, b, output logic [15:0] p);
      if (!TIMED) begin : g_zero
        assign p = a * b;
      end else begin : g_timed
        real         t_h [8];
        logic [15:0] p_h [8];
        initial for (int k = 0; k < 8; k++) begin t_h[k] = -1000.0; p_h[k] = '0; end
        always @(a or b) begin                            // a new product starts on its way
          for (int k = 7; k > 0; k--) begin t_h[k] = t_h[k - 1]; p_h[k] = p_h[k - 1]; end
          t_h[0] = $realtime; p_h[0] = 16'(a) * 16'(b);
        end
        initial begin
          p = '0;
          #0.05;                                          // off the clock edges, so no race
          forever begin
            for (int i = 0; i < 16; i++)
              for (int k = 0; k < 8; k++)                 // newest first
                if (t_h[k] + 6.0 + 0.6 * i <= $realtime) begin p[i] = p_h[k][i]; break; end
            #0.1;
          end
        end
      end
    endmodule
    
    // mul_unit: operands in, product out. The constraints say the multiply is a two-cycle
    // multicycle path, so the multiply may take up to 20 ns at a 10 ns clock.
    // FIX = 0: the control was never changed to match. It takes new operands every cycle and
    //          captures the product one cycle after loading, so the path really has 10 ns.
    // FIX = 1: the design matches the constraint: it loads only when idle and captures two cycles
    //          after loading (in_ready is low in between), so the path has two cycles.
    module mul_unit #(parameter bit FIX = 0, parameter bit TIMED = 0) (
      input  logic        clk, rst_n, in_valid,
      input  logic [7:0]  a, b,
      output logic        in_ready, out_valid,
      output logic [15:0] p
    );
      logic [7:0]  a_q, b_q;
      logic [15:0] p_path;
      logic [1:0]  ph;                                    // FIX = 1: 0 idle, 1 and 2 multiplying
      logic        v1;
      mul_path #(.TIMED(TIMED)) u_mul (.a(a_q), .b(b_q), .p(p_path));
      assign in_ready = FIX ? (ph == 2'd0) : 1'b1;
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n) begin
          a_q <= '0; b_q <= '0; p <= '0; ph <= '0; v1 <= 1'b0; out_valid <= 1'b0;
        end else if (!FIX) begin
          if (in_valid) begin a_q <= a; b_q <= b; end
          v1 <= in_valid;
          out_valid <= v1;
          if (v1) p <= p_path;                            // one cycle after the load
        end else begin
          out_valid <= 1'b0;
          case (ph)
            2'd0: if (in_valid) begin a_q <= a; b_q <= b; ph <= 2'd1; end
            2'd1: ph <= 2'd2;
            default: begin p <= p_path; out_valid <= 1'b1; ph <= 2'd0; end   // two cycles after
          endcase
        end
    endmodule
    

    Zero-delay passes, timed fails

    Terminal
    $ iverilog -g2012 -o m.vvp mcp_tb.sv
    $ vvp -n m.vvp
    zero-delay: bug 0 wrong of 300, fix 0 wrong of 300
    timed:      bug 298 wrong of 300, fix 0 wrong of 300
    PASS: zero-delay RTL passes the bug; only the timed run shows it; the fix passes both
    mcp_tb.sv:131: $finish called at 10006000 (1ps)
    $ vvp -n m.vvp +BAD
    FAIL time=45.0 ns: product #1 is 0020, expected 1b20 (low byte right, high byte wrong)
    FATAL: mcp_tb.sv:111: wrong product
           Time: 45000  Scope: mcp_tb.g_u[2].$unm_blk_16
    $ vvp -n m.vvp +FIX
    PASS: timed fixed unit, 300 products, 0 wrong, clock 10.0 ns
    mcp_tb.sv:124: $finish called at 10006000 (1ps)

    The first line is the RTL regression: both units pass, so a logic bug is ruled out. The second is the same stimulus with time in the path: the buggy unit gets 298 of 300 products wrong and the fixed unit none. Product #1 should be 1b20 and came out 0020: the low byte is right and the high byte still holds the previous product's (zero, from reset). Bits 0 to 6 settle in under 10 ns (6 + 0.6 × 6 = 9.6 ns); bits 7 to 15 do not. That is the scene's signature, reproduced. The two products that were right had high bytes equal to the product before.

    Check 1: move the clock

    Terminal
    $ for pp in 12000 14000 15000 16000; do echo "$pp: $(vvp -n m.vvp +BAD +PERIOD_PS=$pp | head -1)"; done
    12000: FAIL time=54.0 ns: product #1 is 0320, expected 1b20 (low byte right, high byte wrong)
    14000: FAIL time=105.0 ns: product #5 is 383c, expected 783c (low byte right, high byte wrong)
    15000: FAIL time=187.5 ns: product #10 is 5b66, expected db66 (low byte right, high byte wrong)
    16000: PASS: timed buggy unit, 300 products, 0 wrong, clock 16.0 ns

    As the period grows, fewer bits are late and the first failure comes later: at 15 ns only bit 15 is still late (db66 against 5b66 differ in that bit alone). At 16 ns the slowest bit has time and the unit passes. A failure that goes away at a slower clock is setup-type, and the period at which it goes away measures the real path delay, here just over 15 ns.

    What the evidence says

    The logic is correct; the time is not. The constraint promised 20 ns, the control gave 10, and static timing checked the promise rather than the design. The bug is in the agreement between RTL and constraints, which is why neither the RTL regression nor timing sign-off could see it alone. dbglab has no bug of this class.

    The fix pattern

    • Exception not kept: make the RTL keep it (load, then capture two cycles later, with a ready signal so the source waits, as the fix does) or remove the exception and meet single-cycle timing, for example by pipelining the multiply. The fix halves throughput; a pipeline keeps it at the cost of latency and flops.

    • Setup: shorten the path (pipeline, restructure, faster cells) or lower the clock.

    • Hold: add delay on the short path or reduce the skew; never by slowing the clock, which does not appear in the hold check (a hold violation, worked).

    • Unconstrained input: constrain it (input delay against the right clock), or synchronise it if it is asynchronous (CDC bugs).

    Preventing it

    An assertion per exception turns the promise into something the RTL regression can check. For this multicycle path: after the operands change, they stay stable for the next cycle. Added inside mul_unit in a scratch copy of the bench, it fires on the first product of the zero-delay buggy unit, a run that showed no wrong product, and stays silent on the fixed units:

    design.sv
    // snippet: the multicycle promise as an assertion, inside mul_unit
      a_mcp: assert property (@(posedge clk) disable iff (!rst_n) ($changed(a_q) || $changed(b_q)) |=> $stable(a_q) && $stable(b_q))
        else $error("MCP: operands changed in the cycle after a load");
    Terminal
    $ obj_ma/Vmcp_tb                      # Verilator 5.048 build of the scratch copy
    [45000] %Error: mcp_assert.sv:50: Assertion failed in mcp_tb.g_u[0].dut.a_mcp: MCP: operands changed in the cycle after a load
    %Error: mcp_assert.sv:50: Verilog $stop
    Aborting...
    • Review every exception with its RTL owner. Each multicycle or false path names the enable or protocol that makes it true; forgotten multicycle hold and overbroad false path are the classic constraint bugs.

    • Timed simulation on the excepted paths, at gate level with the delay file, at least once before tape-out.

    • Review question: "What in the RTL stops this path from being used every cycle?"

    Traps

    Common pitfall

    "Timing is clean, so it is not timing." Static timing proves the paths meet the constraints. A wrong constraint, a multicycle the design does not keep or an overbroad false path, makes it prove the wrong thing. When silicon fails like a timing bug and STA is clean, audit the exceptions first.

    Common pitfall

    Gate-level simulation without timing. A netlist simulated with zero delay behaves like the RTL and passes this bug. Gate-level simulation finds timing bugs only with the delay file, and only on paths the stimulus exercises.

    Why this matters

    Senior lens. Exceptions are where verification and timing hand a problem to each other and both assume the other checked it. Keep the exception list under review like RTL, with an assertion or a written argument for each line, and treat a new exception late in a project as a design change, not a timing fix.

    Interview

    Asked: "Silicon gives wrong results at speed and correct ones at a lower clock. RTL simulation and static timing both pass. How do you approach it?"

    Weak answer: It sounds like a marginal part or a noisy supply. I would run more RTL simulations with the failing data and ask the timing team to recheck their reports.

    Strong answer: A failure that goes away at a lower clock is setup-type, and with STA clean I first suspect the constraints rather than the logic: a multicycle or false path the RTL does not guarantee, or a missing clock or input constraint. I would find which bits or registers fail, map them to paths, and list the exceptions on those paths with the RTL that is supposed to make each true. Then I would reproduce it with gate-level simulation with timing, or a delay model of the path, which fails where zero-delay passes. The fix is to make the RTL keep the exception or remove it and meet timing, plus an assertion for the exception.

    Follow-ups: (1) How would a hold violation look different? (2) Why did gate-level simulation without a delay file pass?

    Check yourself A shift register in silicon sometimes passes a bit through two stages in one cycle, at every clock frequency the lab can set. RTL simulation is clean. Which class, and what would you check?

    Hold-type: data reaches the next flop before that flop's hold window closes, usually because of clock skew on a short path, and slowing the clock cannot help because the period is not in the hold check. Check the hold slack and the clock arrival times on that path in the timing report, and whether the path is excluded by an exception. The fix is delay on the short path or less skew.

    Check yourself A designer adds set_false_path from a configuration register to the datapath because "software only writes it when idle". What must be true in the RTL, and how would you check it?

    Something in the RTL must make it true, such as the datapath ignoring the register while busy, or software being unable to write it while busy; otherwise a write during traffic is a real path with no timing check. Check it with an assertion (no change of the register while busy), and review the exception against that assertion, as the multicycle assertion above does.

    Remember

    • A failure that goes away at a slower clock is setup-type. One that stays at every frequency is hold-type. RTL simulation shows neither.

    • A multicycle or false path is a promise the RTL must keep. Static timing believes it; only the design can make it true.

    • High bits wrong and low bits right, on an arithmetic path, is a long carry chain arriving late.

    Rule of thumb: for every timing exception, find the line of RTL that makes it true.

    How this was checked

    mcp_tb.sv as shown, 2026-10-18: iverilog -g2012 -o m.vvp mcp_tb.sv and vvp -n m.vvp (Icarus Verilog 13.0, 3.7 s): zero-delay, 0 wrong of 300 for both units; timed, bug 298 of 300 wrong, fix 0 of 300; PASS. +BAD fails at product #1 as shown; +FIX passes. +BAD +PERIOD_PS=<n>: fails at 12000, 14000 and 15000 (products #1, #5, #10), passes at 16000.

    verilator --binary --timing --assert -Wno-fatal --top-module mcp_tb (Verilator 5.048, 0.45 s) prints the same lines for the default run, +BAD, +FIX and the four periods.

    Assertion: a_mcp as shown, added inside mul_unit in a scratch copy (mcp_assert.sv), verilator --binary --timing --assert -Wno-fatal --top-module mcp_tb: fails at 45000 ps in g_u[0], the zero-delay buggy unit; on the fixed units only, silent and PASS. A probe copy printed the two right products of the timed buggy unit, #32 (0550 after 0540) and #114 (0d93 after 0dc8).

    Limits: the timing model is behavioural (bit i settles 6 + 0.6 i ns after the operands change), not a synthesised netlist with SDF; no static timing tool is installed, so no timing report is shown and the constraint is described, not run. The hold-violation case is shown in the RTL course, not re-run here.

    Keep going

    • Practice: Gate-level simulation: functional-only semantics

    • Code lab: Pipelined multiplier stage

    • Code lab: Multi-cycle divider step

    • Interview: run a mock interview on this topic

    Related topics

    • Part 5 · Bug Classes

    • STA: debug playbook

    • STA: multicycle paths

    • RTL: a hold violation, worked

    Previous

    47 / 107

    Next: Protocol Violations

    Up next

    Up nextProtocol Violations

    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