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

  • How to Use This Course
  • Reading Paths
  • Concept Map
  • Meet DCRouter
  • Glossary
  • Part 0: What Verification Is8
  • Part 1: What Can Go Wrong11
    • A Taxonomy of Bugs
    • Specification Bugs
    • Corner Cases and Boundaries
    • Concurrency and Races
    • Integration and Interface Bugs
    • Configuration and Mode Bugs
    • Performance Bugs
    • Reset, Clock and Power Bugs
    • Where Bugs Hide
    • Classifying the Bug Zoo
    • Drills: What Can Go Wrong
  • Part 2: Specifications as Checkable Rules8
  • Part 3: Observability and Controllability7
  • Part 4: Stimulus11
  • Part 5: Checking11
  • Part 6: Coverage10
  • Part 7: Formal and Simulation10
  • Part 8: Verification Levels and Reuse8
  • Part 9: Planning and Risk9
  • Part 10: Emulation and Prototyping8
  • Part 11: Post-Silicon Validation8
  • Part 12: Bug Economics8
  • Part 13: Debug7
  • Part 14: Sign-Off7
  • Part 15: Verification in Context8
  • Part 16: Case Studies5
  • Part 17: Capstones5
  • Part 18: Interview Preparation6
Loading lesson

‹Part 1: What Can Go Wrong
  1. Topics
  2. First Principles of Verification Strategy
  3. Part 1: What Can Go Wrong
  4. Corner Cases and Boundaries

Part 1 · What Can Go Wrong · Working (W)

Corner Cases and Boundaries

Most logic is exercised in its middle and broken at its edges: full, empty, maximum length, wrap-around, first and last. A FIFO with an off-by-one full flag passes a mid-range test and fails the first time it is filled, simulated.

On this page

    Note

    Level: Working (W) · Answers: What can go wrong?

    Before this: A Taxonomy of Bugs

    A FIFO has passed its tests for weeks. Data goes in, data comes out, in order. Then a new test throttles the consumer so the FIFO actually fills, and the very first time it holds four entries and a fifth write arrives, the oldest word vanishes. The bug was there all along, one comparison off at the boundary. No earlier test had ever been at the boundary.

    Why do so many bugs live at boundaries, and how do you make sure tests reach them?

    The obvious answer, and why it breaks

    The obvious test checks typical use: push some words, pop them, compare. Typical use lives in the middle of every range: FIFOs half full, lengths in the middle, counters far from wrapping. Boundary behaviour is different code (or the same code taking a different branch), and it is exactly where designers make off-by-one mistakes: > instead of >=, a count that should stop at 4 stopping at 5. A test that never reaches the boundary cannot see a boundary bug, however long it runs.

    The idea: list the boundaries and go there on purpose

    Kind of boundary

    Examples

    Typical mistake

    Capacity

    FIFO full and empty, credits at zero and maximum

    Flag one entry early or late

    Range

    Minimum and maximum length, zero, all ones

    Maximum treated as zero, length 0 not rejected

    Wrap-around

    Pointer or counter rolling over

    Comparison wrong after the wrap

    First and last

    First flit, last flit, first transaction after reset

    Special case missing or doubled

    Saturation

    A counter that should stop at its maximum

    Wraps to zero instead

    Build it: one bug, two tests

    A 4-entry FIFO whose full flag compares count > 4 instead of count == 4. Test A pushes three words and pops them. Test B pushes five without popping, then pops everything. A reference queue checks every word that comes out.

    tb.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
    // A 4-entry FIFO whose full flag is one entry late: full = (count > 4) instead of
    // (count == 4), so it rises only after a fifth push has already overwritten the oldest entry.
    // Test A pushes 3 words and pops them: mid-range, it passes. Test B pushes 5 words
    // with no pops, the boundary case, then pops everything: word 1 has been overwritten.
    // The checker confirms test A saw no error and test B caught the overwrite.
    module tb;
      logic clk = 0;
      always #5 clk = ~clk;
      initial begin $dumpfile("wave.vcd"); $dumpvars(0, tb); end
    
      int cyc = 0;
      always @(posedge clk) cyc <= cyc + 1;
    
      logic [7:0] mem [0:3];
      logic [1:0] wp = 0, rp = 0;
      logic [2:0] count = 0;
      logic       push = 0, pop = 0, full, empty;
      logic [7:0] din = 0, dout;
      assign full  = (count > 3'd4);              // BUG: should be (count == 4)
      assign empty = (count == 3'd0);
      assign dout  = mem[rp];
      always @(posedge clk) begin
        if (push && !full) begin mem[wp] <= din; wp <= wp + 2'd1; end
        if (pop && !empty) rp <= rp + 2'd1;
        count <= count + ((push && !full) ? 3'd1 : 3'd0) - ((pop && !empty) ? 3'd1 : 3'd0);
      end
    
      // stimulus: test A (cycles 1-6), test B (cycles 8-18)
      always @(posedge clk) begin
        push <= (cyc >= 0 && cyc <= 2) || (cyc >= 7 && cyc <= 11);
        pop  <= (cyc >= 3 && cyc <= 5) || (cyc >= 12 && cyc <= 16);
        din  <= (cyc <= 2) ? 8'(8'h10 + cyc) : (cyc >= 7 && cyc <= 11) ? 8'(cyc - 6) : 8'h00;   // A: 10,11,12   B: 1,2,3,4,5
      end
    
      // checker: a reference queue of accepted words
      logic [7:0] q [$];
      int errs_a = 0, errs_b = 0;
      always @(posedge clk) begin
        if (pop && !empty) begin
          logic [7:0] e;
          e = q.pop_front();
          if (dout !== e) begin if (cyc <= 6) errs_a++; else errs_b++; end
        end
        if (push && !full) q.push_back(din);
      end
      initial begin
        #220;
        if (errs_a == 0 && errs_b > 0) $display("PASS: mid-range test A saw no error; boundary test B caught the overwrite (wrong words: %0d)", errs_b);
        else $display("FAIL: test A errors %0d, test B errors %0d", errs_a, errs_b);
        $finish;
      end
    endmodule

    Watch it run

    How to read it, slot by slot:

    • Slots 0 to 6, test A: three pushes (0x10, 0x11, 0x12), count rises to 3, three pops read them back in order. Mid-range: correct.

    • Slots 7 to 11, test B: five pushes (1 to 5). Count reaches 4 at slot 11 with full still low, so the fifth push is accepted into a full FIFO and overwrites word 1.

    • Slot 12: count reads 5 and full finally rises, one push too late.

    • Slots 12 to 16: the pops read 5, 2, 3, 4 and then 5 again: the first word out is the overwritten slot. The checker reports the wrong word, and the run prints PASS because test B caught what test A could not.

    Reaching boundaries deliberately

    Approach

    How

    Cost

    Directed boundary tests

    Write the case: fill to exactly full, send length 15

    Only boundaries someone listed

    Biased random

    Weight extreme values and slow consumers (Part 4)

    Less time in the middle

    Small configurations

    Shrink FIFOs and credits so limits are hit constantly

    Must stay a legal configuration

    Coverage at the limits

    Bins for full, empty, maximum, wrap (Part 6)

    Only measures; someone must act

    Formal

    Prove the property for all states, boundaries included (Part 7)

    Block-sized logic only

    How real teams find boundary bugs

    Practice

    Effect

    Boundary bins in every coverage model

    A limit never reached shows up as a hole

    Back-pressure and throttling in stimulus

    Queues actually fill

    Formal proofs on FIFOs and counters

    Off-by-ones found in minutes, for every state

    What breaks

    DCRouter has five planted corner-case bugs, including a full flag one entry late (BUG_1) and a payload length of 15 treated as 0 (BUG_3). The smoke test of lesson 0.7 never fills a FIFO and only sends 2-flit payloads, so it misses both. Part 4 measures how quickly random traffic finds them, with and without biased lengths and slow sinks.

    Why this matters

    Senior lens. For every block, write down its boundaries before writing a single test: every capacity, range, wrap and saturation point. Then make sure each one is reached by a test, measured by coverage, or proven formally. Boundary bugs are the cheapest class to prevent and among the most common to escape, because typical traffic stays in the middle.

    Interview

    Asked: "Where would you look first for bugs in a FIFO?"

    Weak answer: I'd run a lot of random data through it.

    Strong answer: At its boundaries: full and empty, and especially the cycle it becomes full or empty while a push or pop arrives, plus pointer wrap-around and simultaneous push and pop at each limit. Random data alone keeps a FIFO in the middle unless the consumer is throttled, so I'd add back-pressure, bias toward filling it, put bins for full, empty and wrap in coverage, and ideally prove the occupancy and ordering properties formally, which covers every boundary at once.

    Follow-ups: (1) How would you make sure random tests fill it? (2) What would you prove formally?

    Check yourself A counter should saturate at 255. Which values must tests reach?

    254 to 255 (reaching the limit), 255 with a further increment (must stay 255), and the clear back to 0; a counter that wraps instead of saturating fails only at 255.

    Check yourself In the waveform, why does test A pass with the same buggy FIFO?

    It never holds more than three words, so the full flag's comparison is never exercised; the bug needs the FIFO exactly full.

    Key takeaways

    • Bugs cluster at boundaries: capacity, range, wrap-around, first and last, saturation.

    • Typical traffic stays in the middle, so boundaries must be reached on purpose.

    • List every boundary, then test it, cover it or prove it.

    Keep going

    Next: Lesson 1.4, Concurrency and Races. Two events in one cycle, handled as if one came first.

    Related topics

    • Part 1 overview: What Can Go Wrong

    Previous

    16 / 160

    Next: Concurrency and Races

    Up next

    Up nextConcurrency and Races

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • A Taxonomy of Bugs
    • Integration and Interface Bugs
    • Configuration and Mode Bugs
    clkpushpopdin1011120123450count01232101234543210fulldoutx101112x1523452
    Simulation output: test A (slots 0 to 6) never fills the FIFO and passes; test B (slots 7 to 17) pushes five words, the fifth overwrites the first, and full rises one push too late.