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

AboutRoadmapCareersInternshipsFAQContactPrivacyTerms
Skip to lesson

Course contents

  • How to Use This Course
  • Reading Paths
  • Glossary
  • Metrics Reference
  • Formal Cheat Sheet
  • Formal Foundations19
  • Properties and SVA16
  • Property Development and Constraints19
  • Proof Engines and Convergence23
  • Formal Applications20
  • Equivalence Checking14
  • Formal Coverage and Signoff15
  • Methodology and Debug13
  • Interview Prep15
  • Capstone: A Chip-Block Verification Lab24
    • The Brief
    • Stage 1: FIFO and Handshake
    • Stage 2: Registers and Arbiter
      • Stage 2 CSR Proofs from the Table
      • Stage 2 Arbiter Grant Latency and Starvation
      • Stage 2 Reset Determinism and Connectivity
    • Stage 3: Pipeline and Equivalence
    • Stage 4: Induction and Signoff
    • Solutions
    • Appendix: Run It Yourself on an Open-Source Flow
Loading lesson

←Stage 2: Registers and Arbiter
  1. Topics
  2. Formal Verification
  3. Capstone: A Chip-Block Verification Lab
  4. Stage 2: Registers and Arbiter
  5. Stage 2 Arbiter Grant Latency and Starvation
On this page

    Part 10 of 10, lesson 7 of 24Part 10 of 10/Capstone: A Chip-Block Verification Lab/Lesson 7 of 24All levels

    Stage 2 Arbiter Grant Latency and Starvation

    Derive the arbiter's wait bound with a model, prove it, and find three planted faults, one of which shows twice.

    Why it matters

    The symptom: one requester gets every grant while the other three wait, and the structural checks, one-hot and no grant without a request, all pass. Structural checks are blind to starvation; only a bound on waiting sees it. This lesson makes you derive the bound from the arbiter's behaviour, prove it, and read what each planted fault does to it.

    Inputs

    • The arbiter below, with the BUG switch (W1 to W3) and the HELPER switch for the four helper assertions.

    • The property set: one-hot, no phantom grant, work conservation, four wait-bound properties on counters, four helpers, and six covers.

    • The symptoms of W1 to W3 in the brief; read the BUG == lines of the listing last, after you have read each trace.

    • No assumption on the requesters. The bound holds for every request pattern, so the fairness register for this design is empty; true liveness is out of reach of this toolchain and the lab uses the bounded form.

    rr_arbiter.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
    // formal: prove 4 expect pass slang top rr_arbiter
    `define FORMAL
    // rr_arbiter: 4-requester round-robin arbiter. The grant is combinational from req and the 2-bit
    // pointer last (the index granted most recently). The search starts at last + 1 and wraps. A grant is
    // given in the cycle a request is seen and lasts one cycle (no lock). Synchronous active-high reset:
    // last = 3, so requester 0 has priority first. BUG selects a variant (0 is the design as specified);
    // HELPER=0 removes the helper assertions.
    module rr_arbiter #(parameter BUG = 0, parameter HELPER = 1) (
      input  wire       clk,
      input  wire       rst,
      input  wire [3:0] req,
      output wire [3:0] grant
    );
      reg [1:0] last;
    
      wire [3:0] hi_mask = (last == 2'd0) ? 4'b1110 :
                           (last == 2'd1) ? 4'b1100 :
                           (last == 2'd2) ? 4'b1000 : 4'b0000;
      wire [3:0] hi_req  = req & hi_mask;
      wire [3:0] lo_req  = (BUG == 3) ? (req | (4'b0001 << last)) : req;
    
      function [3:0] lowest;           // lowest set bit, one-hot (0 if none)
        input [3:0] v;
        lowest = v & (~v + 4'd1);
      endfunction
    
      wire [3:0] g_hi = lowest(hi_req);
      wire [3:0] g_lo = lowest(lo_req) & {4{(hi_req == 4'b0) || (BUG == 2)}};
      assign grant = g_hi | g_lo;
    
      wire [1:0] gidx = grant[0] ? 2'd0 : grant[1] ? 2'd1 : grant[2] ? 2'd2 : 2'd3;
    
      always @(posedge clk) begin
        if (rst) last <= 2'd3;
        else if (|grant && !(BUG == 1 && grant[0])) last <= gidx;
      end
    
    `ifdef FORMAL
      reg init = 1'b1;
      always @(posedge clk) init <= 1'b0;
      always @(*) if (init) assume (rst);
    
      // wt[i]: consecutive cycles requester i has requested without being granted
      reg [2:0] wt0, wt1, wt2, wt3;
      always @(posedge clk) begin
        if (rst) begin wt0 <= 3'd0; wt1 <= 3'd0; wt2 <= 3'd0; wt3 <= 3'd0; end
        else begin
          wt0 <= (req[0] && !grant[0]) ? wt0 + 3'd1 : 3'd0;
          wt1 <= (req[1] && !grant[1]) ? wt1 + 3'd1 : 3'd0;
          wt2 <= (req[2] && !grant[2]) ? wt2 + 3'd1 : 3'd0;
          wt3 <= (req[3] && !grant[3]) ? wt3 + 3'd1 : 3'd0;
        end
      end
    
      always @(*) if (!init) begin
        a_onehot0:  assert ((grant & (grant - 4'd1)) == 4'd0);
        a_no_phantom: assert ((grant & ~req) == 4'd0);
        a_work_cons:  assert (!(|req) || (|grant));
        // bounded response (safety form of starvation freedom): nobody waits more than 3 cycles
        a_wt0: assert (wt0 <= 3'd3);
        a_wt1: assert (wt1 <= 3'd3);
        a_wt2: assert (wt2 <= 3'd3);
        a_wt3: assert (wt3 <= 3'd3);
      end
    
      // helper invariants for the induction step of the wait bounds
      function [1:0] ahead;             // how many grants can still precede requester i
        input [1:0] i;
        ahead = i - last - 2'd1;        // 0..3 (mod 4)
      endfunction
      generate if (HELPER) begin : g_helper
        always @(*) if (!init) begin
          h_wt0: assert (wt0 + {1'b0, ahead(2'd0)} <= 3'd3);
          h_wt1: assert (wt1 + {1'b0, ahead(2'd1)} <= 3'd3);
          h_wt2: assert (wt2 + {1'b0, ahead(2'd2)} <= 3'd3);
          h_wt3: assert (wt3 + {1'b0, ahead(2'd3)} <= 3'd3);
        end
      end endgenerate
    
      // covers
      always @(*) if (!init) begin
        c_g0: cover (grant[0]);
        c_g1: cover (grant[1]);
        c_g2: cover (grant[2]);
        c_g3: cover (grant[3]);
        c_contend: cover (req == 4'b1111 && wt3 == 3'd3);
        c_idle: cover (req == 4'b0000 && grant == 4'd0);
      end
    `endif
    endmodule

    Expected outcome: proved. The variants, by header edit. W1 is run twice, with and without the helpers.

    Variant

    Edit the header of the block above to

    Expected category

    W1, helpers on

    // formal: bmc 12 expect fail slang top rr_arbiter define BUG=1

    counterexample

    W1, helpers off

    // formal: bmc 12 expect fail slang top rr_arbiter define BUG=1 define HELPER=0

    counterexample

    W2

    // formal: bmc 12 expect fail slang top rr_arbiter define BUG=2

    counterexample

    W3

    // formal: bmc 12 expect fail slang top rr_arbiter define BUG=3

    counterexample

    Deliverable

    • The wait bound, derived with the model below and not typed in, and a statement that it is exact (a bound one smaller fails).

    • For each variant: the failing property, step and smallest failing depth; for W1, both the early failure and the requirement failure, and why they differ.

    • The covers reached, with the contention cover's step.

    • The rows: structural properties, the wait bound, the covers.

    rr_arbiter.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
    // formal: cover 6 expect reached slang top rr_arbiter
    `define FORMAL
    // rr_arbiter: 4-requester round-robin arbiter. The grant is combinational from req and the 2-bit
    // pointer last (the index granted most recently). The search starts at last + 1 and wraps. A grant is
    // given in the cycle a request is seen and lasts one cycle (no lock). Synchronous active-high reset:
    // last = 3, so requester 0 has priority first. BUG selects a variant (0 is the design as specified);
    // HELPER=0 removes the helper assertions.
    module rr_arbiter #(parameter BUG = 0, parameter HELPER = 1) (
      input  wire       clk,
      input  wire       rst,
      input  wire [3:0] req,
      output wire [3:0] grant
    );
      reg [1:0] last;
    
      wire [3:0] hi_mask = (last == 2'd0) ? 4'b1110 :
                           (last == 2'd1) ? 4'b1100 :
                           (last == 2'd2) ? 4'b1000 : 4'b0000;
      wire [3:0] hi_req  = req & hi_mask;
      wire [3:0] lo_req  = (BUG == 3) ? (req | (4'b0001 << last)) : req;
    
      function [3:0] lowest;           // lowest set bit, one-hot (0 if none)
        input [3:0] v;
        lowest = v & (~v + 4'd1);
      endfunction
    
      wire [3:0] g_hi = lowest(hi_req);
      wire [3:0] g_lo = lowest(lo_req) & {4{(hi_req == 4'b0) || (BUG == 2)}};
      assign grant = g_hi | g_lo;
    
      wire [1:0] gidx = grant[0] ? 2'd0 : grant[1] ? 2'd1 : grant[2] ? 2'd2 : 2'd3;
    
      always @(posedge clk) begin
        if (rst) last <= 2'd3;
        else if (|grant && !(BUG == 1 && grant[0])) last <= gidx;
      end
    
    `ifdef FORMAL
      reg init = 1'b1;
      always @(posedge clk) init <= 1'b0;
      always @(*) if (init) assume (rst);
    
      // wt[i]: consecutive cycles requester i has requested without being granted
      reg [2:0] wt0, wt1, wt2, wt3;
      always @(posedge clk) begin
        if (rst) begin wt0 <= 3'd0; wt1 <= 3'd0; wt2 <= 3'd0; wt3 <= 3'd0; end
        else begin
          wt0 <= (req[0] && !grant[0]) ? wt0 + 3'd1 : 3'd0;
          wt1 <= (req[1] && !grant[1]) ? wt1 + 3'd1 : 3'd0;
          wt2 <= (req[2] && !grant[2]) ? wt2 + 3'd1 : 3'd0;
          wt3 <= (req[3] && !grant[3]) ? wt3 + 3'd1 : 3'd0;
        end
      end
    
      always @(*) if (!init) begin
        a_onehot0:  assert ((grant & (grant - 4'd1)) == 4'd0);
        a_no_phantom: assert ((grant & ~req) == 4'd0);
        a_work_cons:  assert (!(|req) || (|grant));
        // bounded response (safety form of starvation freedom): nobody waits more than 3 cycles
        a_wt0: assert (wt0 <= 3'd3);
        a_wt1: assert (wt1 <= 3'd3);
        a_wt2: assert (wt2 <= 3'd3);
        a_wt3: assert (wt3 <= 3'd3);
      end
    
      // helper invariants for the induction step of the wait bounds
      function [1:0] ahead;             // how many grants can still precede requester i
        input [1:0] i;
        ahead = i - last - 2'd1;        // 0..3 (mod 4)
      endfunction
      generate if (HELPER) begin : g_helper
        always @(*) if (!init) begin
          h_wt0: assert (wt0 + {1'b0, ahead(2'd0)} <= 3'd3);
          h_wt1: assert (wt1 + {1'b0, ahead(2'd1)} <= 3'd3);
          h_wt2: assert (wt2 + {1'b0, ahead(2'd2)} <= 3'd3);
          h_wt3: assert (wt3 + {1'b0, ahead(2'd3)} <= 3'd3);
        end
      end endgenerate
    
      // covers
      always @(*) if (!init) begin
        c_g0: cover (grant[0]);
        c_g1: cover (grant[1]);
        c_g2: cover (grant[2]);
        c_g3: cover (grant[3]);
        c_contend: cover (req == 4'b1111 && wt3 == 3'd3);
        c_idle: cover (req == 4'b0000 && grant == 4'd0);
      end
    `endif
    endmodule
    example.py
    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
    def grant(req, last):
        # the search starts at last + 1 and wraps; req is a list of four booleans
        for off in range(1, 5):
            i = (last + off) % 4
            if req[i]:
                return i
        return None
    def trace(masks, last=3):
        out = []
        for m in masks:
            g = grant([bool(m >> i & 1) for i in range(4)], last)
            out.append(0 if g is None else 1 << g)
            if g is not None:
                last = g
        return out
    # the recorded behaviour of the design: ten cycles of requests and the one-hot grants
    assert trace([15, 15, 15, 15, 15, 0, 10, 10, 10, 1]) == [1, 2, 4, 8, 1, 0, 2, 8, 2, 1]
    def max_wait(masks, last=3):
        wait, worst = [0] * 4, 0
        for m in masks:
            g = grant([bool(m >> i & 1) for i in range(4)], last)
            for i in range(4):
                wait[i] = wait[i] + 1 if (m >> i & 1) and g != i else 0
                worst = max(worst, wait[i])
            if g is not None:
                last = g
        return worst
    BOUND = max_wait([15] * 40)               # derived: every requester requests in every cycle
    assert BOUND == 3, BOUND
    seed, worst = 7, 0                         # a deterministic pseudo-random sweep over request patterns
    for _ in range(3000):
        masks = []
        for _ in range(12):
            seed = (seed * 1103515245 + 12345) % (2 ** 31)
            masks.append(seed >> 8 & 15)
        worst = max(worst, max_wait(masks, last=seed % 4))
    assert worst <= BOUND, "no pattern waits longer than the derived bound"
    assert BOUND - 1 < max_wait([15] * 40), "a bound one smaller would be violated: the bound is exact"
    print("PASS: wait bound %d derived from the model, exact, and never exceeded by 3000 patterns" % BOUND)
    example.py
    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
    # ---- formal lab results ledger, engine v2: every row cites a typed run (kind, depth, outcome[, design]) ----
    STATUS = ("proven", "bounded", "cex", "cover-reached", "inconclusive")
    FROM_RUN = {"proven": {("prove", "proved")},
                "bounded": {("bmc", "bounded")},
                "cex": {("bmc", "counterexample"), ("prove", "counterexample")},
                "cover-reached": {("cover", "reached")},
                "inconclusive": {("prove", "unproven"), ("cover", "not-reached")}}
    NEED = {"sync_fifo": 9, "rr_arbiter": 5, "hs_bridge": 4, "csr_block": 3,
            "pipe3": 5, "pipe3_eq": 5, "mod10_counter": 15, "toggle_demo": 4}
    KEYS = ("property", "design", "status", "bound", "why", "run", "assumes", "cover", "kills", "rev")
    REV = 3
    ALWAYS = "reset-at-step-0"
    TRACKED = "tracked-constant-stable"
    def row_problems(e, runs, tracked):
        n = e.get("property", "?")
        miss = [k for k in KEYS if k not in e]
        if miss:
            return ["missing-field:%s:%s" % (n, ",".join(miss))]
        if e["status"] not in STATUS:
            return ["bad-status:" + n]
        p = []
        if e["rev"] != REV:
            p.append("stale:" + n)
        if e["run"] not in runs:
            return p + ["no-such-run:" + n]
        kind, depth, outcome = runs[e["run"]][:3]
        if len(runs[e["run"]]) > 3 and runs[e["run"]][3] != e["design"]:
            p.append("run-of-another-design:" + n)         # a run typed with its design may be cited only by that design's rows
        if (kind, outcome) not in FROM_RUN[e["status"]]:
            p.append("status-not-from-run:" + n)
        if e["bound"] != depth:
            p.append("bound-not-run-depth:" + n)
        has_cover = bool(str(e["cover"]).strip())
        if e["status"] in ("proven", "bounded") and not (has_cover and e["kills"]):
            p.append("no-evidence:" + n)
        if e["status"] == "cover-reached" and not has_cover:
            p.append("no-evidence:" + n)
        if e["status"] == "cex" and not e["kills"]:
            p.append("no-evidence:" + n)
        if e["status"] == "bounded":
            if e["design"] not in NEED:
                p.append("unknown-design:" + n)
            elif depth < NEED[e["design"]]:
                p.append("short-bound:" + n)
            if len(str(e["why"]).strip()) < 12:
                p.append("unjustified:" + n)
        if ALWAYS not in e["assumes"]:
            p.append("hidden-assume:%s:%s" % (n, ALWAYS))
        if n in tracked and TRACKED not in e["assumes"]:
            p.append("hidden-assume:%s:%s" % (n, TRACKED))
        return p
    def ledger_ok(ledger, runs, required, mutants, register, tracked, final=True):
        names = [e.get("property") for e in ledger]
        bad = ["duplicate:" + n for n in sorted({n for n in names if names.count(n) > 1})]
        bad += ["missing-row:" + r for r in sorted(set(required) - set(names))]
        bad += [q for e in ledger for q in row_problems(e, runs, tracked)]
        good = [e for e in ledger if not row_problems(e, runs, tracked)]      # only a row without complaints counts as evidence
        solid = {e["property"] for e in good if e["status"] in ("proven", "cover-reached")}
        for a, rows in sorted(register.items()):
            if rows is None:
                if final:
                    bad.append("deferred-assume:" + a)      # a stage may defer a discharge; a final package may not
            elif not (rows and all(r in solid for r in rows)):
                bad.append("open-assume:" + a)
        bad += ["unregistered-assume:%s:%s" % (e.get("property"), a) for e in ledger
                for a in e.get("assumes", []) if a not in register]
        killed = {m for e in good if e["status"] == "cex" for m in e["kills"]}
        bad += ["survivor:" + m for m in sorted(set(mutants) - killed)]
        bad += ["unsupported-kill:%s:%s" % (e.get("property"), m) for e in ledger
                if e.get("status") in ("proven", "bounded") for m in e.get("kills", []) if m not in killed]
        return bad
    def mutation_score(ledger, mutants):
        assert mutants, "no mutants registered"
        killed = {m for e in ledger if e.get("status") == "cex" for m in e.get("kills", [])}
        return len(killed & set(mutants)) / len(mutants)
    
    RUNS = {
        "arb-prove": ("prove", 4, "proved"),
        "arb-cover": ("cover", 6, "reached"),
    }
    REQUIRED = ["arbiter-structural", "arbiter-wait-bound", "arbiter-covers"]
    MUTANTS = ["arb-W%d" % i for i in range(1, 4)]
    REGISTER = {"reset-at-step-0": None}                 # discharged by the pair rows of lesson 3
    YOUR_ROWS = []          # fill from your own runs
    bad = ledger_ok(YOUR_ROWS, RUNS, REQUIRED, MUTANTS, REGISTER, set(), final=False)
    if YOUR_ROWS:
        assert bad == [], bad
        print("PASS: stage 2 arbiter package consistent")
    else:
        expected = ["missing-row:" + r for r in REQUIRED] + ["survivor:" + m for m in MUTANTS]
        assert sorted(bad) == sorted(expected), bad
        print("PASS: the empty package is rejected with %d complaints" % len(bad))

    Pass criterion

    • The good design is proved and the covers are reached.

    • The model derives the bound, reproduces the recorded grants, and the bound is exact.

    • Each variant gives a counterexample, with the smallest failing depth found by lowering the depth.

    • The package check passes for your rows.

    Hint ladder

    • Hint 1: the structural properties pass on one of the variants, so the fault is in time, not in shape: look at the wait properties.

    • Hint 2: run the variant with and without the helpers and compare the failing steps; ask which condition is already unrecoverable before any requirement is broken.

    • Hint 3: keep all four requests high from step 1 and count the ungranted cycles of each requester.

    • Hint 4: list the signals the failing property reads, find the logic that drives each of them in the listing, and compare it with the sentence of the header comment that governs it.

    Verify and debug

    Derive the bound before proving it. If the proof fails on the good design, check that the wait counters clear on a grant and on a request release: a counter that never clears is a harness fault shaped like starvation. Confirm each grant property against the cover that witnesses its trigger.

    Fairness assumed on a design output instead of the environment: a liveness proof passes because the assumption forces the answer. Symptom is a starvation proof that passes with a fault planted. Cause is constraining a grant or a ready the design drives. Fix: fairness and request rules sit on inputs and environment choices only; check every assume for the direction of its signal. Found by removing each assume in turn.

    “Liveness: every request is eventually granted.” Answer: true liveness needs fairness on the environment and a lasso argument, which this toolchain cannot run; use the bounded form, a wait cap of requesters minus one on a counter, and say that nothing is assumed about the requesters.

    Check yourself Why is the wait bound 3 and not 4?

    A continuously requesting line is granted in the fourth cycle, so it has waited through three: one grant per rival. Four would allow a full extra rotation, which round robin never needs.

    Check yourself Besides the failing property and its depth, what must you record for every variant?

    The step of each failure and which other properties fail at which step: one fault can be seen by several properties at different steps. The reference reading is in the Solutions topic.

    Related topics

    • Next: Stage 2 Reset Determinism and Connectivity

    • Earlier: Deadlock, Livelock and Starvation Checks

    • After you have filed your rows: Solutions, Arbiter Wait Bound

    Previous

    166 / 183

    Next: Stage 2 Reset Determinism and Connectivity

    Up next

    Up nextStage 2 Reset Determinism and Connectivity

    Sign in to save progress

    Practice this lessonPrevious lessonReport an error