A FIFO, End to End
Build the complete formal bench for a FIFO: a two-entry scoreboard for order and integrity, the glue invariants, covers, a fault matrix and two routes to a proof.
- Lesson
- F8.1
- Time
- 25 min
- Runs on
- SymbiYosys
- Check
- 5 questions
A FIFO had passed a constrained-random bench with data checking for a year. A new consumer read and wrote at exactly the right moments, and data came out in the wrong order once every few million transactions. The write pointer wrapped at the slot count while the read pointer wrapped at the depth. A formal order check finds it in seven steps.
| Spec sentence | Kind | Bench line (Imm form) |
|---|---|---|
| Nothing is lost or changed: the tracked entry comes out with its data | end-to-end | if (f_chk1) assert(read_valid && read_data == f_d1) |
| Data leaves in the order it entered | end-to-end | if (f_chk2) assert(read_valid && read_data == f_d2) |
full and empty tell the truth; the count never passes DEPTH | glue | assert(used <= DEPTH), assert(full == (used == DEPTH) ...) |
| The pointers stay in range and agree with the count | glue | assert(wp < DEPTH && rp < DEPTH), the pointer relation |
read_valid only after an accepted read | end-to-end | assert(read_valid == f_tr) |
| The FIFO fills, a write and read meet at full, it drains | covers | cover(full), cover(full && write_en && read_en), a fill-and-drain tracker |
The plan before the code (F7.5): each sentence is one property or one cover. fifo_f has 48 flops.
A Scoreboard of Two
Read the diagram as text
- write side: write_en, write_data
- first: f_d1: any accepted write, chosen by the free input f_pick; f_ahead entries in front
- second: f_d2: the very next accepted write
- read side: read_valid, read_data one cycle after an accepted read
- checks: f_d1 leaves when f_ahead reaches 0; f_d2 is the very next read
- write side → first: f_d1
- write side → second: f_d2
- first: f_d1 → read side: f_ahead reads later
- second: f_d2 → checks
- read side → checks
The choice is free, so the proof covers every pair of consecutive entries with every data value (F4.4). Two in order, for every pair, is the whole sequence in order.
Formal does not store every packet. It tracks two consecutive accepted writes that the solver may pick anywhere, copies their data, and counts the entries in front of the first. When the count reaches zero the next read must return the first value, and the read after it the second.
One tracked value proves integrity: nothing lost, nothing changed. It cannot tell whether an older entry jumped the queue. Order needs the second.
This FIFO refuses a write when full and a read when empty, so it needs no input assumption. A FIFO without those guards needs assume(!(write_en && full)) from its producer, written down in the sign-off.
The Fault Matrix
Measured with smtbmc yices: FAIL at reported step 7, 0.6 s. The write at step 4 lands in slot 3; the next read takes stale slot 0.
| Fault (one line changed) | Order pair | Count glue | Pointer glue | Valid |
|---|---|---|---|---|
| BUG_PTR: A8.1 fault 6, wp wraps at four | FAIL, step 7 | BMC PASS to 12 | FAIL, step 5 | BMC PASS to 12 |
| BUG_OVF: a write accepted when full | FAIL, step 7 | FAIL, step 6 | FAIL, step 6 | BMC PASS to 12 |
| A8.1 fault 2: read_valid never cleared | BMC PASS to 12 | BMC PASS to 12 | BMC PASS to 12 | FAIL, step 5 |
| A8.1 fault 4: every answer one cycle late | FAIL, step 4 | BMC PASS to 12 | BMC PASS to 12 | FAIL, step 4 |
Each cell is one bmc run at depth 12 with that property alone. Step = reported step.
Glue properties find pointer and count bugs early and point at the line. The order pair is the spec and catches what the glue cannot see, such as a late answer. Fault 2 escapes both and only the valid check sees it: every spec sentence needs its own line.
Two Routes to a Proof
| Correct fifo_f, all properties | Measured result |
|---|---|
| bmc depth 10 | BMC PASS to depth 10, 0.6 s |
| bmc depth 20 | time limit (20 s), no verdict, 20.1 s |
| prove k = 12, glue only | UNKNOWN at k = 12 (base case passed, induction step failed), 1.0 s |
| prove k = 1, glue and tracker helpers | UNKNOWN at k = 1 (base case passed, induction step failed), 0.5 s |
| prove k = 2, glue and tracker helpers | Prove PASS (k = 2), 0.5 s |
abc pdr, no helpers | Prove PASS (abc pdr), 1.3 s |
smtbmc yices unless noted. A bounded run at depth 20 does not finish in the budget; both proofs take about a second.
The glue is not enough. The induction step starts with the second tracked value inside (f_out1 = 1, f_in2 = 1), one entry left, and slot 2 holding some other byte. The missing helpers tie the tracker to the memory: where each tracked byte sits, and that it is still there (F4.6). Helpers may read inside the design; the checks themselves stay at the ports.
PDR proves the same set with no helpers in 1.3 s. The rule from F4.7: PDR to get the proof, helpers to understand it and to keep it when the design grows.
Asked in Interviews
| Question | Answer |
|---|---|
| End-to-end or glue for a FIFO? | Both. The order and integrity pair is the spec; the count and pointer relations are the helpers and the fast bug finders. |
| How does a formal scoreboard work without storing every packet? | Two tracked writes chosen freely by the solver, with modelling flops for their data and position. The proof covers every pair. |
| Why did induction fail on a correct FIFO? | The step started in a state no reset reaches: the tracker and the memory disagree. Helpers that tie them together close it. |
Try It
[options]
mode prove
[engines]
abc pdr
[script]
read_verilog -formal -sv design.sv
prep -top fifo_f
[files]
design.svassert property (@(posedge clk) disable iff (rst) f_chk1 |-> read_valid && read_data == f_d1);
assert property (@(posedge clk) disable iff (rst) f_chk2 |-> read_valid && read_data == f_d2);
assert property (@(posedge clk) disable iff (rst) used <= DEPTH && full == (used == DEPTH) && empty == (used == 0));
assert property (@(posedge clk) disable iff (rst) read_valid == $past(take_r));
cover property (@(posedge clk) disable iff (rst) full && write_en && read_en); always @(posedge clk) begin
if ($initstate) begin assume(rst); end else begin assume(!rst); end
end
// Modelling code: two consecutive accepted writes, picked by the solver, and their data.
wire f_pick = $anyseq;
reg f_in1 = 0, f_in2 = 0, f_out1 = 0, f_done = 0, f_chk1 = 0, f_chk2 = 0;
reg [1:0] f_ahead = 0; // entries in front of the first tracked one
reg [WIDTH-1:0] f_d1 = 0, f_d2 = 0;
reg f_tr = 0; // an accepted read last cycle
always @(posedge clk)
if (rst) begin f_in1 <= 0; f_in2 <= 0; f_out1 <= 0; f_done <= 0; f_chk1 <= 0; f_chk2 <= 0; f_tr <= 0; end
else begin
f_tr <= take_r; f_chk1 <= 0; f_chk2 <= 0;
if (!f_in1 && !f_out1 && !f_done && take_w && f_pick) begin
f_in1 <= 1; f_d1 <= write_data; f_ahead <= used - take_r;
end else if ((f_in1 || f_out1) && !f_in2 && take_w) begin f_in2 <= 1; f_d2 <= write_data; end
if (f_in1 && take_r) begin
if (f_ahead == 0) begin f_in1 <= 0; f_out1 <= 1; f_chk1 <= 1; end
else f_ahead <= f_ahead - 2'd1;
end
if (f_out1 && f_in2 && take_r) begin f_out1 <= 0; f_in2 <= 0; f_done <= 1; f_chk2 <= 1; end
end
reg [1:0] f_round = 0; // 0 nothing yet, 1 was empty then full, 2 empty again after that
always @(posedge clk) if (rst) f_round <= 0; else if (f_round == 0 && full) f_round <= 1; else if (f_round == 1 && empty) f_round <= 2;
always @(posedge clk) if (!rst) begin
if (f_chk1) assert(read_valid && read_data == f_d1); // nothing lost, nothing changed
if (f_chk2) assert(read_valid && read_data == f_d2); // the next one comes next
assert(used <= DEPTH); // glue: the count
assert(full == (used == DEPTH) && empty == (used == 0));
assert(wp < DEPTH && rp < DEPTH); // glue: the pointers
assert(used == (wp >= rp ? wp - rp : wp + DEPTH - rp) || used == DEPTH && wp == rp);
assert(read_valid == f_tr); // valid only after an accepted read
// helpers for k-induction: where the tracked data sits
if (f_in1) assert(f_ahead < used && mem[rp + f_ahead >= DEPTH ? rp + f_ahead - DEPTH : rp + f_ahead] == f_d1);
if (f_in1 && f_in2) assert(f_ahead + 3'd2 <= used && mem[rp + f_ahead + 1 >= DEPTH ? rp + f_ahead + 1 - DEPTH : rp + f_ahead + 1] == f_d2);
if (f_in1 && !f_in2) assert(f_ahead + 2'd1 == used);
if (f_out1 && f_in2) assert(used != 0 && mem[rp] == f_d2);
if (f_out1 && !f_in2) assert(used == 0);
if (f_chk1 || f_chk2) assert(f_tr);
assert(!(f_in1 && f_out1) && !(f_done && (f_in1 || f_out1 || f_in2)) && (!f_in2 || f_in1 || f_out1));
c_full: cover(full);
c_full_rw: cover(full && write_en && read_en);
c_round: cover(f_round == 2'd2 && empty);
endWiggle
Run the cover twin:
fullat step 5, a write and a read at full at step 5, fill and drain at step 9. Read each trace.Induction
Run k = 12 without helpers, read step 0 of the induction trace, then switch the helpers on and run k = 2.
Faults
Run both bug twins and fill the matrix for the properties each one breaks.
Sign off
Write the line: which engine proved which set, with which helpers, under no input assumption.
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
Remember
- Write the spec sentences first; each becomes one property or one cover.
- Two freely chosen tracked writes check integrity and order for every pair.
- Glue finds pointer bugs fast; end-to-end checks find what glue cannot; a fault matrix shows the gaps.
- Induction needs helpers that tie the tracker to the memory; PDR finds its own invariant.