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 3: Pipeline and Equivalence
      • Stage 3 Pipeline Latency Properties
      • Stage 3 Retimed Twin Equivalence
      • Stage 3 ECO and the Intended Difference
    • Stage 4: Induction and Signoff
    • Solutions
    • Appendix: Run It Yourself on an Open-Source Flow
Loading lesson

←Stage 3: Pipeline and Equivalence
  1. Topics
  2. Formal Verification
  3. Capstone: A Chip-Block Verification Lab
  4. Stage 3: Pipeline and Equivalence
  5. Stage 3 Retimed Twin Equivalence
On this page

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

    Stage 3 Retimed Twin Equivalence

    Prove the retimed twin equal in a miter, find the planted twin fault, and measure the induction window with and without the helper.

    Why it matters

    The symptom: most operand values agree, but some inputs make two implementations that were supposed to match produce different outputs. Retiming moved logic across a register cut, so the two designs agree at the outputs while disagreeing on every internal register. The miter feeds both the same inputs and asserts equal outputs; what you learn here is what the induction needs in order to believe that.

    Inputs

    • The listing has three modules: the reference, the retimed twin with a BUG switch, and the miter pipe3_eq, which asserts equal outputs in every cycle after reset and carries one helper assertion across the moved cut under HELPER.

    • The twin's registers differ from the reference's: the reference holds a product where the twin holds its two factors.

    • The symptom of the twin variant W1 in the brief. Read the BUG == lines of the listing last, after you have read the trace.

    Common inputs and reset feed the reference pipeline and the retimed twin. Their outputs are compared every cycle. Dotted arrows show the stage-2 registers of both designs feeding a helper assertion across the cut.
    The miter: both designs see the same inputs, the outputs are compared in every cycle, and a helper relates the stage-2 registers across the moved cut.
    pipe3.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
    114
    115
    116
    117
    // formal: prove 5 expect pass slang top pipe3_eq
    `define FORMAL
    // pipe3: three-stage arithmetic pipeline, y = ((a + b)[3:0] * c[3:0]) + 1 (8 bits). Registers: stage 1
    // {s = a + b, c1 = c[3:0]}, stage 2 {p = s[3:0] * c1}, stage 3 {y = p + 1}. Latency 3 cycles. Synchronous
    // active-high reset clears every register. Only a[3:0], b[3:0], c[3:0] influence y.
    // pipe3_retimed: the same function and latency with the registers moved across the multiplier: stage 2
    // {s2 = s[3:0], c2 = c1}, stage 3 {y = s2 * c2 + 1}. BUG selects a variant of the twin.
    // pipe3_eq: a miter of pipe3 against pipe3_retimed with common inputs and reset; BUG goes to the twin.
    module pipe3 #(parameter CHECK = 1) (
      input  wire       clk,
      input  wire       rst,
      input  wire [7:0] a,
      input  wire [7:0] b,
      input  wire [7:0] c,
      output reg  [7:0] y,
      output wire [7:0] dbg_p          // stage-2 register, exposed for the miter's helper assertion
    );
      reg [7:0] s;
      reg [3:0] c1;
      reg [7:0] p;
      assign dbg_p = p;
      always @(posedge clk) begin
        if (rst) begin s <= 8'd0; c1 <= 4'd0; p <= 8'd0; y <= 8'd0; end
        else begin
          s  <= a + b;
          c1 <= c[3:0];
          p  <= s[3:0] * c1;
          y  <= p + 8'd1;
        end
      end
    
    `ifdef FORMAL
      // CHECK=0 switches the properties off (used when pipe3 sits inside the miter)
      generate if (CHECK) begin : g_chk
      reg init = 1'b1;
      always @(posedge clk) init <= 1'b0;
      always @(*) if (init) assume (rst);
    
      // hist counts clean cycles since reset (saturates at 3): y is only specified after 3
      reg [1:0] hist;
      always @(posedge clk) begin
        if (rst) hist <= 2'd0;
        else if (hist != 2'd3) hist <= hist + 2'd1;
      end
    
      // input history as plain registers
      reg [3:0] sa1, sb1, sc1, sa2, sb2, sc2, sa3, sb3, sc3;
      always @(posedge clk) begin
        sa1 <= a[3:0]; sb1 <= b[3:0]; sc1 <= c[3:0];
        sa2 <= sa1;    sb2 <= sb1;    sc2 <= sc1;
        sa3 <= sa2;    sb3 <= sb2;    sc3 <= sc2;
      end
      wire [3:0] sum3 = sa3 + sb3;       // low 4 bits of a + b three cycles ago
    
      // functional property with latency 3, checked only after 3 clean cycles
      always @(*) if (!init && hist == 2'd3) a_latency3: assert (y == (sum3 * sc3) + 8'd1);
      // the cycle after a reset the output register is 0
      always @(posedge clk) if (!init && $past(rst)) a_reset_zero: assert (y == 8'd0);
      always @(*) if (!init) c_y_nonzero: cover (hist == 2'd3 && y == 8'd7);
      end endgenerate
    `endif
    endmodule
    
    module pipe3_retimed #(parameter BUG = 0) (
      input  wire       clk,
      input  wire       rst,
      input  wire [7:0] a,
      input  wire [7:0] b,
      input  wire [7:0] c,
      output reg  [7:0] y,
      output wire [3:0] dbg_s2,        // stage-2 registers, exposed for the miter's helper assertion
      output wire [3:0] dbg_c2
    );
      reg [7:0] s;
      reg [3:0] c1;
      reg [3:0] s2, c2;
      assign dbg_s2 = s2;
      assign dbg_c2 = c2;
      always @(posedge clk) begin
        if (rst) begin s <= 8'd0; c1 <= 4'd0; s2 <= 4'd0; c2 <= 4'd0; y <= 8'd0; end
        else begin
          s  <= a + b;
          c1 <= c[3:0];
          s2 <= s[3:0];
          c2 <= c1;
          y  <= s2 * ((BUG == 1) ? {1'b0, c2[2:0]} : c2) + 8'd1;
        end
      end
    endmodule
    
    module pipe3_eq #(parameter BUG = 0, parameter HELPER = 1) (
      input  wire       clk,
      input  wire       rst,
      input  wire [7:0] a,
      input  wire [7:0] b,
      input  wire [7:0] c,
      output wire       same
    );
      wire [7:0] y1, y2, p_ref;
      wire [3:0] s2_ret, c2_ret;
      pipe3 #(.CHECK(0))     u_ref (.clk(clk), .rst(rst), .a(a), .b(b), .c(c), .y(y1), .dbg_p(p_ref));
      pipe3_retimed #(.BUG(BUG)) u_ret (.clk(clk), .rst(rst), .a(a), .b(b), .c(c), .y(y2), .dbg_s2(s2_ret), .dbg_c2(c2_ret));
      assign same = (y1 == y2);
    
    `ifdef FORMAL
      reg init = 1'b1;
      always @(posedge clk) init <= 1'b0;
      always @(*) if (init) assume (rst);
      always @(*) if (!init) a_equiv: assert (same);
      // helper assertion across the moved register cut: the reference's stage-2 register equals the
      // twin's stage-2 registers multiplied
      generate if (HELPER) begin : g_helper
        always @(*) if (!init) h_cut: assert (p_ref == s2_ret * c2_ret);
      end endgenerate
      always @(*) if (!init) c_nonzero: cover (y1 == 8'd7 && y2 == 8'd7);
    `endif
    endmodule
    pipe3.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
    114
    115
    116
    117
    // formal: cover 6 expect reached slang top pipe3_eq
    `define FORMAL
    // pipe3: three-stage arithmetic pipeline, y = ((a + b)[3:0] * c[3:0]) + 1 (8 bits). Registers: stage 1
    // {s = a + b, c1 = c[3:0]}, stage 2 {p = s[3:0] * c1}, stage 3 {y = p + 1}. Latency 3 cycles. Synchronous
    // active-high reset clears every register. Only a[3:0], b[3:0], c[3:0] influence y.
    // pipe3_retimed: the same function and latency with the registers moved across the multiplier: stage 2
    // {s2 = s[3:0], c2 = c1}, stage 3 {y = s2 * c2 + 1}. BUG selects a variant of the twin.
    // pipe3_eq: a miter of pipe3 against pipe3_retimed with common inputs and reset; BUG goes to the twin.
    module pipe3 #(parameter CHECK = 1) (
      input  wire       clk,
      input  wire       rst,
      input  wire [7:0] a,
      input  wire [7:0] b,
      input  wire [7:0] c,
      output reg  [7:0] y,
      output wire [7:0] dbg_p          // stage-2 register, exposed for the miter's helper assertion
    );
      reg [7:0] s;
      reg [3:0] c1;
      reg [7:0] p;
      assign dbg_p = p;
      always @(posedge clk) begin
        if (rst) begin s <= 8'd0; c1 <= 4'd0; p <= 8'd0; y <= 8'd0; end
        else begin
          s  <= a + b;
          c1 <= c[3:0];
          p  <= s[3:0] * c1;
          y  <= p + 8'd1;
        end
      end
    
    `ifdef FORMAL
      // CHECK=0 switches the properties off (used when pipe3 sits inside the miter)
      generate if (CHECK) begin : g_chk
      reg init = 1'b1;
      always @(posedge clk) init <= 1'b0;
      always @(*) if (init) assume (rst);
    
      // hist counts clean cycles since reset (saturates at 3): y is only specified after 3
      reg [1:0] hist;
      always @(posedge clk) begin
        if (rst) hist <= 2'd0;
        else if (hist != 2'd3) hist <= hist + 2'd1;
      end
    
      // input history as plain registers
      reg [3:0] sa1, sb1, sc1, sa2, sb2, sc2, sa3, sb3, sc3;
      always @(posedge clk) begin
        sa1 <= a[3:0]; sb1 <= b[3:0]; sc1 <= c[3:0];
        sa2 <= sa1;    sb2 <= sb1;    sc2 <= sc1;
        sa3 <= sa2;    sb3 <= sb2;    sc3 <= sc2;
      end
      wire [3:0] sum3 = sa3 + sb3;       // low 4 bits of a + b three cycles ago
    
      // functional property with latency 3, checked only after 3 clean cycles
      always @(*) if (!init && hist == 2'd3) a_latency3: assert (y == (sum3 * sc3) + 8'd1);
      // the cycle after a reset the output register is 0
      always @(posedge clk) if (!init && $past(rst)) a_reset_zero: assert (y == 8'd0);
      always @(*) if (!init) c_y_nonzero: cover (hist == 2'd3 && y == 8'd7);
      end endgenerate
    `endif
    endmodule
    
    module pipe3_retimed #(parameter BUG = 0) (
      input  wire       clk,
      input  wire       rst,
      input  wire [7:0] a,
      input  wire [7:0] b,
      input  wire [7:0] c,
      output reg  [7:0] y,
      output wire [3:0] dbg_s2,        // stage-2 registers, exposed for the miter's helper assertion
      output wire [3:0] dbg_c2
    );
      reg [7:0] s;
      reg [3:0] c1;
      reg [3:0] s2, c2;
      assign dbg_s2 = s2;
      assign dbg_c2 = c2;
      always @(posedge clk) begin
        if (rst) begin s <= 8'd0; c1 <= 4'd0; s2 <= 4'd0; c2 <= 4'd0; y <= 8'd0; end
        else begin
          s  <= a + b;
          c1 <= c[3:0];
          s2 <= s[3:0];
          c2 <= c1;
          y  <= s2 * ((BUG == 1) ? {1'b0, c2[2:0]} : c2) + 8'd1;
        end
      end
    endmodule
    
    module pipe3_eq #(parameter BUG = 0, parameter HELPER = 1) (
      input  wire       clk,
      input  wire       rst,
      input  wire [7:0] a,
      input  wire [7:0] b,
      input  wire [7:0] c,
      output wire       same
    );
      wire [7:0] y1, y2, p_ref;
      wire [3:0] s2_ret, c2_ret;
      pipe3 #(.CHECK(0))     u_ref (.clk(clk), .rst(rst), .a(a), .b(b), .c(c), .y(y1), .dbg_p(p_ref));
      pipe3_retimed #(.BUG(BUG)) u_ret (.clk(clk), .rst(rst), .a(a), .b(b), .c(c), .y(y2), .dbg_s2(s2_ret), .dbg_c2(c2_ret));
      assign same = (y1 == y2);
    
    `ifdef FORMAL
      reg init = 1'b1;
      always @(posedge clk) init <= 1'b0;
      always @(*) if (init) assume (rst);
      always @(*) if (!init) a_equiv: assert (same);
      // helper assertion across the moved register cut: the reference's stage-2 register equals the
      // twin's stage-2 registers multiplied
      generate if (HELPER) begin : g_helper
        always @(*) if (!init) h_cut: assert (p_ref == s2_ret * c2_ret);
      end endgenerate
      always @(*) if (!init) c_nonzero: cover (y1 == 8'd7 && y2 == 8'd7);
    `endif
    endmodule

    Expected outcomes: proved, then reached.

    Variant

    Edit the header of the block above to

    Expected category

    W1

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

    counterexample

    Deliverable

    Produce

    How

    The smallest induction window with the helper and without

    Sweep the depth of the proof with HELPER=1 and with HELPER=0. Report both and the failing assertion at the first window that fails.

    The twin fault

    Failing property, failing step, smallest failing depth, cause and fix.

    A statement about a bounded run one depth short

    What would a bounded pass at that depth have meant, and why is the evidence late?

    Rows

    twin-equivalence, the inconclusive row for the run without the helper, and the cover.

    Pass criterion

    • The miter is proved with the helper at the window you report.

    • Without the helper, a window shorter than the one you report is inconclusive and a longer one proves.

    • The twin variant gives a counterexample at the depth you report and not at one depth lower.

    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 = {
        "twin-prove": ("prove", 5, "proved"),
        "twin-cover": ("cover", 6, "reached"),
    }
    REQUIRED = ["twin-equivalence", "twin-needs-cut-helper", "twin-cover"]
    MUTANTS = ["twin-W1"]
    REGISTER = {"reset-at-step-0": None}                 # discharged by the pair rows of stage 2
    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 3 twin 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))

    Hint ladder

    • Hint 1: an equivalence step starts from any pair of states, including pairs that cannot occur together. Ask what pair of internal states would make the outputs differ a cycle later.

    • Hint 2: the helper relates the reference's stage-2 register to the twin's; a longer window makes the same relation hold without it.

    • Hint 3: for the fault, vary one operand bit at a time and compare the two outputs at the first step a result can appear.

    • Hint 4: the failing step is the first step at which the result of the first input vector can be seen; a run that ends one step before it sees nothing.

    Verify and debug

    When a miter fails everywhere, check alignment and reset before logic: a one-cycle skew or a reset-value difference drowns the real signal. When the step stalls, add the cut relation before adding depth, and keep the shorter bounded pass filed beside the longer counterexample so nobody re-learns the bound.

    State mapping incomplete after retiming: the tool reports non-equivalence although the design is correct. Symptom is an inconclusive step with outputs that actually agree. Cause is a start state that no reset sequence reaches, split across the moved cut. Fix: give the correspondence as an invariant across the cut, which is the helper here, or compare with a longer window.

    “Two implementations of the same block disagree. How do you scope it?” Answer: build the miter with common inputs and reset, check alignment and latency first, read the earliest mismatch cycle, and separate a cut artefact from a logic difference with the cross-cut relation. Quote the failing step of your twin run.

    Check yourself A bounded run on the faulty twin passes. What does that pass prove?

    Nothing about the twin: the bound ends where the evidence begins. It proves only that the run can reach the step before the first visible result.

    Check yourself Why do the two internal register sets never match, even on the correct twin?

    The multiplier moved across the cut: the reference holds the product where the twin holds the factors. The function is the same, so the outputs agree; the structure moved, so the registers differ.

    Related topics

    • Next: Stage 3 ECO and the Intended Difference

    • Earlier: Combinational Equivalence and the Miter

    • After you have filed your rows: Solutions, Pipeline, Twin and ECO

    Previous

    169 / 183

    Next: Stage 3 ECO and the Intended Difference

    Up next

    Up nextStage 3 ECO and the Intended Difference

    Sign in to save progress

    Practice this lessonPrevious lessonReport an error