Counter, Memory and Parameter Abstraction

Replace a long counter by the few states that matter, prove the replacement faithful, and shrink a parameter without losing the bug it hides.

Lesson
F4.5
Time
25 min
Runs on
SymbiYosys
Check
5 questions

The wake-up proof closed on a 10-bit test build. The real counter has 16 bits, and the same PDR run never came back. The engineer cut the counter, got a trace in which the link came up before calibration, and spent a day showing that trace was impossible.

A Counter Is Mostly Waiting

A 16-bit Wake Counter as Five StatesThe design acts on two counter values only: CAL, where calibration fires, and the last count, where the link goes up. The waits between them shrink to states the solver may leave whenever it likes.
A 16-bit wake counter as five statesThe design acts on two counter values only: CAL, where calibration fires, and the last count, where the link goes up. The waits between them shrink to states the solver may leave whenever it likes. counter real counter cnt = 0, 1, 2 … 2^N− 1, one value percycle state ZERO one cycle state BEFORE 1 … CAL − 1: staysuntil f_go state AT_CAL one cycle: calibrationfires state AFTER CAL + 1 … last − 1:stays until f_go state AT_LAST the link goes up andstays f_gof_gorefines
Read the diagram as text
  • real counter: cnt = 0, 1, 2 … 2^N − 1, one value per cycle
  • ZERO: one cycle
  • BEFORE: 1 … CAL − 1: stays until f_go
  • AT_CAL: one cycle: calibration fires
  • AFTER: CAL + 1 … last − 1: stays until f_go
  • AT_LAST: the link goes up and stays
  • ZERO → BEFORE
  • BEFORE → AT_CAL: f_go
  • AT_CAL → AFTER
  • AFTER → AT_LAST: f_go
  • real counter → ZERO: refines

The order of the regions is kept; only their lengths become free. A plain cut keeps neither.

Run on phy_wakeMeasured result
real counter, 10 bits, k-induction prove 20UNKNOWN at depth 20, 0.5 s
real counter, 10 bits, abc pdrProve PASS, 5.3 s
real counter, 10 bits, cover of up, depth 40cover unreached within depth 40, 0.6 s
real counter, 12 bits, abc pdr, SBY directlyProve PASS, 43.0 s
real counter, 16 bits, abc pdr, SBY directlyTIMEOUT at 60 s, no verdict
both decodes cut free, bmc 10FAIL at step 3, 0.5 s
abstract counter, abc pdrProve PASS, 0.5 s
abstract counter, cover of up, depth 10cover reached at step 7, 0.5 s

The property: the link is never up before calibration is done. CAL is 32 at 10 bits.

The design acts on two counter values; everything between them is waiting. Each extra bit doubles the wait, and PDR’s time grows with it. The abstraction has five states at any width: Prove PASS, 0.5 s, and the cover of up is reached at step 7.

0123clkfreeat_calfreeat_lastcal_doneup

Measured counterexample with both decodes cut free (bmc, depth 10): FAIL at step 3, 0.5 s.

A plain cut forgets the order: at_last may come before at_cal, and up rises with no calibration. That failure is false. The abstraction keeps the order and lets the solver choose how long each wait lasts, which covers every real timing.

Prove the Abstraction Is Faithful

A side run on the real 16-bit counter alone: Prove PASS, 0.5 s at depth 2 (UNKNOWN at depth 1, 0.5 s at depth 1).
assert(region(cnt) == r_q || region(cnt) == r_q + 3'd1);   // stay, or step to the next region
if (r_q == 3'd0 || r_q == 3'd2) assert(region(cnt) == r_q + 3'd1);   // ZERO and AT_CAL last one cycle

An abstraction is an assumption in disguise, so justify it on the real logic. The side run proves that the counter’s region only stays or steps to the next one, and that ZERO and AT_CAL last one cycle. It is inductive at once, so 16 bits cost nothing.

Faulty twin: calibration needs cal_ok in its one cycleMeasured result
abstract counter, bmc 10FAIL at step 7, 0.5 s
real counter, 10 bits, abc pdrFAIL at step 1026, 4.0 s

Same bug, same verdict; the abstract trace reports at step 7, the real one at step 1,026.

With the side proof, every real trace is a trace of the abstract model, so a PASS there holds for the real design. The faulty twin fails on both, and the abstract counterexample is short enough to read.

Abstract counter with assume(!f_go) addedMeasured result
faulty twin, bmc 10BMC PASS to depth 10, 0.5 s
cover of up, depth 10cover unreached within depth 10, 0.5 s

One extra assumption and the abstract counter never leaves BEFORE: the bug is hidden, and only the cover says so.

Ask three questions of every abstraction. Does it admit every real behaviour? Which properties changed meaning? Can the abstract element still do what the properties need? The cover answers the third; without it, an abstraction is an over-constraint waiting to be found.

Shrink a Parameter

pfifoDepth 4Depth 16Depth 64
correct, abc pdrProve PASS, 0.5 sProve PASS, 1.3 sProve PASS, 11.8 s
correct, k-induction prove 20––UNKNOWN at depth 20, 6.6 s
count hard-coded to 3 bitsProve PASS, 0.5 s (abc pdr)–FAIL at step 10, 0.5 s (bmc 20)

A FIFO like the one in F2.4, with a DEPTH parameter. Properties: empty means equal pointers; full means the pointers differ only in the wrap bit.

A reduced parameter is sound when the small design is the large one, scaled. A constant written for one size breaks that: the count here is 3 bits wide, enough for depth 4. At depth 4 the proof passes; at depth 64 the count wraps after 8 words and bmc fails at step 10.

FIFO propertySurvives depth 64 → 4?Why
empty |-> wr_ptr == rd_ptryesno depth-dependent number
the popped word is the oldest (F4.4 tracker)yesordering does not scale with depth
a push and a pop together leave the count unchangedyeslocal to one cycle
count <= 64noat depth 4 the bound is never approached
full after 64 pushesnothe number is the parameter
a word reaches the output within 64 popsnoa latency that scales with depth

Write this list next to every reduced-parameter proof.

Prove at the reduced size, then check the full size by bmc or by coverage: every line reachable in the full design must be reachable in the reduced one. Memories reduce the same way, to one tracked row at a symbolic address, as the F4.4 tracker does for a FIFO.

Asked in Interviews

QuestionAnswer
What is counter abstraction?Replacing an N-bit counter by its few meaningful regions in order, with free lengths. Prove on the real counter that it moves through the regions in that order.
How is a memory abstracted?Keep one or a few rows at symbolic ($anyconst) addresses and return free data elsewhere; the checks then talk only about the tracked rows. Free data must never feed a check unguarded.
Can I reduce parameters and still trust the proof?Only if the logic scales with the parameter. Hard-coded widths and constants break it; run the full size in bmc or compare coverage.

Try It

The lab is pfifo at depth 4 under abc pdr. Measured: Prove PASS, 0.5 s. The twins include the wake counter, its abstraction and the side proof.

The wake rule in concurrent SVA.
assert property (@(posedge clk) disable iff (rst) up |-> cal_done);
  1. Scale the parameter

    Set DEPTH to 16 (pointers 5 bits, count 5 bits) and compare the time with depth 4.

  2. Hide a bug

    Make the count 3 bits wide at depth 4 and prove it; then set depth 64 and run bmc at depth 20.

  3. Abstract the counter

    In phy_wake, replace the counter with the five-state model and run abc pdr; then cut both decodes to $anyseq and read the false trace.

design.sv

props.sv

Use assert property (@(posedge clk) ...);. Leave blank for immediate assertions in the design.

Inputs are free unless constrained. Run to find a counterexample or check a proof.

Model: one positive-edge clock, binary data. Sampled history requires declaration-initialized variables; primary-input history is unsupported.

SBY summary and log

Check Yourself

Predict the outputphy_wake with at_cal and at_last cut to $anyseq, mode bmc, depth 10, smtbmc yices, reset assumed in step 0. At which step does SBY report the failure?
module phy_wake(input clk, rst, cal_ok, output reg up = 0, output reg cal_done = 0);
  wire at_cal = $anyseq, at_last = $anyseq;   // the counter cut away
  always @(posedge clk)
    if (rst) begin up <= 0; cal_done <= 0; end
    else begin
      if (at_cal) cal_done <= 1;
      if (at_last) up <= 1;
    end
  always @(posedge clk) begin
    if ($initstate) begin assume(rst); end else begin assume(!rst); end
  end
  always @(posedge clk) if (!rst) begin
    if (up) assert(cal_done);
    cover(up);
  end
endmodule
Predict the outputpfifo at depth 64 with the count hard-coded to 3 bits, mode bmc, depth 20, smtbmc yices, reset assumed in step 0. The verdict is:
module pfifo(input clk, rst, wr, rd, input [7:0] wdata, output [7:0] rdata, output empty, full);
  localparam DEPTH = 64;
  reg [7:0] mem [0:DEPTH-1];
  reg [6:0] wr_ptr = 0, rd_ptr = 0;
  reg [2:0] count = 0;   // written for the 4-deep build
  assign empty = count == 0;
  assign full = count == DEPTH;
  wire do_wr = wr && !full, do_rd = rd && !empty;
  assign rdata = mem[rd_ptr[5:0]];
  always @(posedge clk)
    if (rst) begin wr_ptr <= 0; rd_ptr <= 0; count <= 0; end
    else begin
      if (do_wr) begin mem[wr_ptr[5:0]] <= wdata; wr_ptr <= wr_ptr + 1'b1; end
      if (do_rd) rd_ptr <= rd_ptr + 1'b1;
      count <= count + do_wr - do_rd;
    end
  always @(posedge clk) begin
    if ($initstate) begin assume(rst); end else begin assume(!rst); end
  end
  always @(posedge clk) if (!rst) begin
    if (empty) assert(wr_ptr == rd_ptr);
    if (full) assert(wr_ptr[5:0] == rd_ptr[5:0] && wr_ptr != rd_ptr);
  end
endmodule
ConceptWhy does cutting the wake counter give a false failure while the abstraction does not?
ConceptWhat makes the abstraction’s PASS valid for the real design?
Read the waveformWhat does the measured cut trace show?
0123clkfreeat_calfreeat_lastcal_doneup

Measured counterexample with both decodes cut free (bmc, depth 10): FAIL at step 3, 0.5 s.

Remember

  • A counter abstraction keeps the meaningful values in order and frees the waits.
  • Prove the abstraction on the real counter in a side run.
  • A plain cut of a counter loses order and gives false failures.
  • Parameter reduction is sound only if nothing is hard-coded to one size.