A four-entry FIFO shipped accepting a push while full; simulation never held it full under push and pop together. One property from the sentence "a full FIFO ignores a push" finds the fault. Job of this lesson: turn a sentence into a property with five questions, show each property failing on a planted fault, and teach the two questions that need more than $past: the window and one-to-one pairing.
Five questions precede any property. Trigger: which event starts the obligation. Response: what must hold. Window: the same cycle, the next, or within N cycles. Scope: which states the rule covers. One-to-one: what ties this response to that request. A same-cycle fact is a combinational check, reported at the step where the bad state exists; a registered update is a clocked check that reads history and is reported one step later. A property that reads $past skips the cycle after reset, whose "previous" cycle is the reset cycle.
Sentence | Trigger and window | Property | Fails on |
|---|---|---|---|
A full FIFO ignores a push | push, no pop, count 4: count unchanged next cycle | a_ovf_ign | BUG 1, step 7 |
Occupancy stays within 0 to 4 | always, same cycle | a_range | BUG 1, step 6 |
The flags match the count | always, same cycle | a_flags | BUG 3, step 4 |
Pointers and count agree (internal) | always, same cycle | a_ptr | BUG 4, step 3 (no output property fails) |
Reset empties the FIFO | the cycle after reset | a_reset (added below) | phantom word, step 2 |
The complete FIFO file proves at depth 4 (bounded check and induction step). With the fault of the first row switched on, a fifth push is accepted into a full FIFO:
// formal: prove 4 expect pass slang top sync_fifo
`define FORMAL
// sync_fifo: 4-entry, 8-bit synchronous FIFO. Running example of the Formal course.
// Synchronous active-high reset. count is an output so properties can see it.
// BUG selects a planted bug: 0 none, 1 push accepted when full, 2 pop accepted when
// empty, 3 full flag one entry early, 4 simultaneous push+pop miscounts, 5 write
// data bit 7 stuck at 0.
module sync_fifo #(parameter BUG = 0, parameter HELPER = 1) (
input wire clk,
input wire rst,
input wire push,
input wire [7:0] wdata,
input wire pop,
output wire [7:0] rdata,
output wire full,
output wire empty,
output reg [2:0] count
);
reg [7:0] mem [0:3];
reg [1:0] wptr, rptr;
assign full = (BUG == 3) ? (count >= 3'd3) : (count == 3'd4);
assign empty = (count == 3'd0);
assign rdata = mem[rptr];
wire do_push = push && (!full || BUG == 1);
wire do_pop = pop && (!empty || BUG == 2);
always @(posedge clk) begin
if (rst) begin
wptr <= 2'd0;
rptr <= 2'd0;
count <= 3'd0;
end else begin
if (do_push) begin
mem[wptr] <= (BUG == 5) ? {1'b0, wdata[6:0]} : wdata;
wptr <= wptr + 2'd1;
end
if (do_pop) rptr <= rptr + 2'd1;
if (BUG == 4 && do_push && do_pop) count <= count + 3'd1; // forgets the pop
else count <= count + {2'b0, do_push} - {2'b0, do_pop};
end
end
`ifdef FORMAL
// ---- harness: first cycle is reset, state before it is arbitrary ----
reg init = 1'b1;
always @(posedge clk) init <= 1'b0;
always @(*) if (init) assume (rst);
// ---- safety properties (after the reset cycle) ----
always @(*) if (!init) begin
a_range: assert (count <= 3'd4); // no overflow, no underflow wrap
a_flags: assert (full == (count == 3'd4) && empty == (count == 3'd0));
a_not_both: assert (!(full && empty));
// helper invariant: occupancy equals pointer distance (mod 4) unless full
a_ptr: assert (count[1:0] == (wptr - rptr));
end
// ---- transition properties (use history: skip the cycle after reset) ----
always @(posedge clk) if (!init && !$past(rst)) begin
// push alone when not full adds one; pop alone when not empty removes one
a_push_inc: assert (!($past(push) && !$past(pop) && !$past(full)) || count == $past(count) + 3'd1);
a_pop_dec: assert (!($past(pop) && !$past(push) && !$past(empty)) || count == $past(count) - 3'd1);
// push into a full FIFO (and no pop) is ignored: occupancy unchanged
a_ovf_ign: assert (!($past(push) && !$past(pop) && $past(count) == 3'd4) || count == $past(count));
// simultaneous push and pop (neither empty nor full) leaves occupancy unchanged
a_both: assert (!($past(push) && $past(pop) && !$past(full) && !$past(empty)) || count == $past(count));
end
// ---- data integrity by tracked-slot abstraction (one free slot, one shadow value) ----
// free constant: (* anyconst *) alone is not honoured by the front end named in the header, so the
// value is also assumed equal to its one-cycle-old copy (works in both front ends;
// a clocked assume ($stable) lags one step and leaks at the last step: do not use it)
(* anyconst *) wire [1:0] tslot;
reg [1:0] tslot_q;
always @(posedge clk) tslot_q <= tslot;
always @(*) if (!init) assume (tslot == tslot_q);
reg tv; // the tracked slot holds a value we are watching
reg [7:0] td; // the value that should be there
always @(posedge clk) begin
if (rst) tv <= 1'b0;
else begin
if (do_push && wptr == tslot) begin tv <= 1'b1; td <= wdata; end
else if (do_pop && rptr == tslot) tv <= 1'b0;
end
end
always @(*) if (!init && tv && !empty && rptr == tslot) a_data: assert (rdata == td);
// helper invariant needed for the induction step of a_data
generate if (HELPER) begin : g_helper
always @(*) if (!init && tv) a_tracked: assert (mem[tslot] == td);
end endgenerate
// ---- covers: the properties are not vacuous ----
always @(*) if (!init) begin
c_full: cover (count == 3'd4);
c_empty_after_use: cover (empty && tv == 1'b0 && wptr == 2'd2);
end
always @(posedge clk) if (!init) c_both: cover (push && pop && !full && !empty);
`endif
endmodule// formal: bmc 7 expect fail slang top sync_fifo define BUG=1
`define FORMAL
// sync_fifo: 4-entry, 8-bit synchronous FIFO. Running example of the Formal course.
// Synchronous active-high reset. count is an output so properties can see it.
// BUG selects a planted bug: 0 none, 1 push accepted when full, 2 pop accepted when
// empty, 3 full flag one entry early, 4 simultaneous push+pop miscounts, 5 write
// data bit 7 stuck at 0.
module sync_fifo #(parameter BUG = 0, parameter HELPER = 1) (
input wire clk,
input wire rst,
input wire push,
input wire [7:0] wdata,
input wire pop,
output wire [7:0] rdata,
output wire full,
output wire empty,
output reg [2:0] count
);
reg [7:0] mem [0:3];
reg [1:0] wptr, rptr;
assign full = (BUG == 3) ? (count >= 3'd3) : (count == 3'd4);
assign empty = (count == 3'd0);
assign rdata = mem[rptr];
wire do_push = push && (!full || BUG == 1);
wire do_pop = pop && (!empty || BUG == 2);
always @(posedge clk) begin
if (rst) begin
wptr <= 2'd0;
rptr <= 2'd0;
count <= 3'd0;
end else begin
if (do_push) begin
mem[wptr] <= (BUG == 5) ? {1'b0, wdata[6:0]} : wdata;
wptr <= wptr + 2'd1;
end
if (do_pop) rptr <= rptr + 2'd1;
if (BUG == 4 && do_push && do_pop) count <= count + 3'd1; // forgets the pop
else count <= count + {2'b0, do_push} - {2'b0, do_pop};
end
end
`ifdef FORMAL
// ---- harness: first cycle is reset, state before it is arbitrary ----
reg init = 1'b1;
always @(posedge clk) init <= 1'b0;
always @(*) if (init) assume (rst);
// ---- safety properties (after the reset cycle) ----
always @(*) if (!init) begin
a_range: assert (count <= 3'd4); // no overflow, no underflow wrap
a_flags: assert (full == (count == 3'd4) && empty == (count == 3'd0));
a_not_both: assert (!(full && empty));
// helper invariant: occupancy equals pointer distance (mod 4) unless full
a_ptr: assert (count[1:0] == (wptr - rptr));
end
// ---- transition properties (use history: skip the cycle after reset) ----
always @(posedge clk) if (!init && !$past(rst)) begin
// push alone when not full adds one; pop alone when not empty removes one
a_push_inc: assert (!($past(push) && !$past(pop) && !$past(full)) || count == $past(count) + 3'd1);
a_pop_dec: assert (!($past(pop) && !$past(push) && !$past(empty)) || count == $past(count) - 3'd1);
// push into a full FIFO (and no pop) is ignored: occupancy unchanged
a_ovf_ign: assert (!($past(push) && !$past(pop) && $past(count) == 3'd4) || count == $past(count));
// simultaneous push and pop (neither empty nor full) leaves occupancy unchanged
a_both: assert (!($past(push) && $past(pop) && !$past(full) && !$past(empty)) || count == $past(count));
end
// ---- data integrity by tracked-slot abstraction (one free slot, one shadow value) ----
// free constant: (* anyconst *) alone is not honoured by the front end named in the header, so the
// value is also assumed equal to its one-cycle-old copy (works in both front ends;
// a clocked assume ($stable) lags one step and leaks at the last step: do not use it)
(* anyconst *) wire [1:0] tslot;
reg [1:0] tslot_q;
always @(posedge clk) tslot_q <= tslot;
always @(*) if (!init) assume (tslot == tslot_q);
reg tv; // the tracked slot holds a value we are watching
reg [7:0] td; // the value that should be there
always @(posedge clk) begin
if (rst) tv <= 1'b0;
else begin
if (do_push && wptr == tslot) begin tv <= 1'b1; td <= wdata; end
else if (do_pop && rptr == tslot) tv <= 1'b0;
end
end
always @(*) if (!init && tv && !empty && rptr == tslot) a_data: assert (rdata == td);
// helper invariant needed for the induction step of a_data
generate if (HELPER) begin : g_helper
always @(*) if (!init && tv) a_tracked: assert (mem[tslot] == td);
end endgenerate
// ---- covers: the properties are not vacuous ----
always @(*) if (!init) begin
c_full: cover (count == 3'd4);
c_empty_after_use: cover (empty && tv == 1'b0 && wptr == 2'd2);
end
always @(posedge clk) if (!init) c_both: cover (push && pop && !full && !empty);
`endif
endmoduleStep | push | count | What the checks see |
|---|---|---|---|
0 | - | any | Reset assumed; assertions off |
1 to 4 | 1 | 0, 1, 2, 3 | Four pushes (the counts are the states before each push) |
5 | 1 | 4 | Full; the fifth push is accepted by the fault |
6 | - | 5 | a_range fails: occupancy 5 exists |
The other two rows of the table are planted faults in the same file: the flags wrong near full (BUG=3) and a count that forgets the pop when a push and a pop arrive together (BUG=4). Each fails only the property written for it: the flag fault a_flags at depth 5, which is step 4, and the count fault the internal a_ptr at depth 4, which is step 3, with no output property failing first.
// formal: bmc 5 expect fail slang top sync_fifo define BUG=3
`define FORMAL
// sync_fifo: 4-entry, 8-bit synchronous FIFO. Running example of the Formal course.
// Synchronous active-high reset. count is an output so properties can see it.
// BUG selects a planted bug: 0 none, 1 push accepted when full, 2 pop accepted when
// empty, 3 full flag one entry early, 4 simultaneous push+pop miscounts, 5 write
// data bit 7 stuck at 0.
module sync_fifo #(parameter BUG = 0, parameter HELPER = 1) (
input wire clk,
input wire rst,
input wire push,
input wire [7:0] wdata,
input wire pop,
output wire [7:0] rdata,
output wire full,
output wire empty,
output reg [2:0] count
);
reg [7:0] mem [0:3];
reg [1:0] wptr, rptr;
assign full = (BUG == 3) ? (count >= 3'd3) : (count == 3'd4);
assign empty = (count == 3'd0);
assign rdata = mem[rptr];
wire do_push = push && (!full || BUG == 1);
wire do_pop = pop && (!empty || BUG == 2);
always @(posedge clk) begin
if (rst) begin
wptr <= 2'd0;
rptr <= 2'd0;
count <= 3'd0;
end else begin
if (do_push) begin
mem[wptr] <= (BUG == 5) ? {1'b0, wdata[6:0]} : wdata;
wptr <= wptr + 2'd1;
end
if (do_pop) rptr <= rptr + 2'd1;
if (BUG == 4 && do_push && do_pop) count <= count + 3'd1; // forgets the pop
else count <= count + {2'b0, do_push} - {2'b0, do_pop};
end
end
`ifdef FORMAL
// ---- harness: first cycle is reset, state before it is arbitrary ----
reg init = 1'b1;
always @(posedge clk) init <= 1'b0;
always @(*) if (init) assume (rst);
// ---- safety properties (after the reset cycle) ----
always @(*) if (!init) begin
a_range: assert (count <= 3'd4); // no overflow, no underflow wrap
a_flags: assert (full == (count == 3'd4) && empty == (count == 3'd0));
a_not_both: assert (!(full && empty));
// helper invariant: occupancy equals pointer distance (mod 4) unless full
a_ptr: assert (count[1:0] == (wptr - rptr));
end
// ---- transition properties (use history: skip the cycle after reset) ----
always @(posedge clk) if (!init && !$past(rst)) begin
// push alone when not full adds one; pop alone when not empty removes one
a_push_inc: assert (!($past(push) && !$past(pop) && !$past(full)) || count == $past(count) + 3'd1);
a_pop_dec: assert (!($past(pop) && !$past(push) && !$past(empty)) || count == $past(count) - 3'd1);
// push into a full FIFO (and no pop) is ignored: occupancy unchanged
a_ovf_ign: assert (!($past(push) && !$past(pop) && $past(count) == 3'd4) || count == $past(count));
// simultaneous push and pop (neither empty nor full) leaves occupancy unchanged
a_both: assert (!($past(push) && $past(pop) && !$past(full) && !$past(empty)) || count == $past(count));
end
// ---- data integrity by tracked-slot abstraction (one free slot, one shadow value) ----
// free constant: (* anyconst *) alone is not honoured by the front end named in the header, so the
// value is also assumed equal to its one-cycle-old copy (works in both front ends;
// a clocked assume ($stable) lags one step and leaks at the last step: do not use it)
(* anyconst *) wire [1:0] tslot;
reg [1:0] tslot_q;
always @(posedge clk) tslot_q <= tslot;
always @(*) if (!init) assume (tslot == tslot_q);
reg tv; // the tracked slot holds a value we are watching
reg [7:0] td; // the value that should be there
always @(posedge clk) begin
if (rst) tv <= 1'b0;
else begin
if (do_push && wptr == tslot) begin tv <= 1'b1; td <= wdata; end
else if (do_pop && rptr == tslot) tv <= 1'b0;
end
end
always @(*) if (!init && tv && !empty && rptr == tslot) a_data: assert (rdata == td);
// helper invariant needed for the induction step of a_data
generate if (HELPER) begin : g_helper
always @(*) if (!init && tv) a_tracked: assert (mem[tslot] == td);
end endgenerate
// ---- covers: the properties are not vacuous ----
always @(*) if (!init) begin
c_full: cover (count == 3'd4);
c_empty_after_use: cover (empty && tv == 1'b0 && wptr == 2'd2);
end
always @(posedge clk) if (!init) c_both: cover (push && pop && !full && !empty);
`endif
endmodule// formal: bmc 4 expect fail slang top sync_fifo define BUG=4
`define FORMAL
// sync_fifo: 4-entry, 8-bit synchronous FIFO. Running example of the Formal course.
// Synchronous active-high reset. count is an output so properties can see it.
// BUG selects a planted bug: 0 none, 1 push accepted when full, 2 pop accepted when
// empty, 3 full flag one entry early, 4 simultaneous push+pop miscounts, 5 write
// data bit 7 stuck at 0.
module sync_fifo #(parameter BUG = 0, parameter HELPER = 1) (
input wire clk,
input wire rst,
input wire push,
input wire [7:0] wdata,
input wire pop,
output wire [7:0] rdata,
output wire full,
output wire empty,
output reg [2:0] count
);
reg [7:0] mem [0:3];
reg [1:0] wptr, rptr;
assign full = (BUG == 3) ? (count >= 3'd3) : (count == 3'd4);
assign empty = (count == 3'd0);
assign rdata = mem[rptr];
wire do_push = push && (!full || BUG == 1);
wire do_pop = pop && (!empty || BUG == 2);
always @(posedge clk) begin
if (rst) begin
wptr <= 2'd0;
rptr <= 2'd0;
count <= 3'd0;
end else begin
if (do_push) begin
mem[wptr] <= (BUG == 5) ? {1'b0, wdata[6:0]} : wdata;
wptr <= wptr + 2'd1;
end
if (do_pop) rptr <= rptr + 2'd1;
if (BUG == 4 && do_push && do_pop) count <= count + 3'd1; // forgets the pop
else count <= count + {2'b0, do_push} - {2'b0, do_pop};
end
end
`ifdef FORMAL
// ---- harness: first cycle is reset, state before it is arbitrary ----
reg init = 1'b1;
always @(posedge clk) init <= 1'b0;
always @(*) if (init) assume (rst);
// ---- safety properties (after the reset cycle) ----
always @(*) if (!init) begin
a_range: assert (count <= 3'd4); // no overflow, no underflow wrap
a_flags: assert (full == (count == 3'd4) && empty == (count == 3'd0));
a_not_both: assert (!(full && empty));
// helper invariant: occupancy equals pointer distance (mod 4) unless full
a_ptr: assert (count[1:0] == (wptr - rptr));
end
// ---- transition properties (use history: skip the cycle after reset) ----
always @(posedge clk) if (!init && !$past(rst)) begin
// push alone when not full adds one; pop alone when not empty removes one
a_push_inc: assert (!($past(push) && !$past(pop) && !$past(full)) || count == $past(count) + 3'd1);
a_pop_dec: assert (!($past(pop) && !$past(push) && !$past(empty)) || count == $past(count) - 3'd1);
// push into a full FIFO (and no pop) is ignored: occupancy unchanged
a_ovf_ign: assert (!($past(push) && !$past(pop) && $past(count) == 3'd4) || count == $past(count));
// simultaneous push and pop (neither empty nor full) leaves occupancy unchanged
a_both: assert (!($past(push) && $past(pop) && !$past(full) && !$past(empty)) || count == $past(count));
end
// ---- data integrity by tracked-slot abstraction (one free slot, one shadow value) ----
// free constant: (* anyconst *) alone is not honoured by the front end named in the header, so the
// value is also assumed equal to its one-cycle-old copy (works in both front ends;
// a clocked assume ($stable) lags one step and leaks at the last step: do not use it)
(* anyconst *) wire [1:0] tslot;
reg [1:0] tslot_q;
always @(posedge clk) tslot_q <= tslot;
always @(*) if (!init) assume (tslot == tslot_q);
reg tv; // the tracked slot holds a value we are watching
reg [7:0] td; // the value that should be there
always @(posedge clk) begin
if (rst) tv <= 1'b0;
else begin
if (do_push && wptr == tslot) begin tv <= 1'b1; td <= wdata; end
else if (do_pop && rptr == tslot) tv <= 1'b0;
end
end
always @(*) if (!init && tv && !empty && rptr == tslot) a_data: assert (rdata == td);
// helper invariant needed for the induction step of a_data
generate if (HELPER) begin : g_helper
always @(*) if (!init && tv) a_tracked: assert (mem[tslot] == td);
end endgenerate
// ---- covers: the properties are not vacuous ----
always @(*) if (!init) begin
c_full: cover (count == 3'd4);
c_empty_after_use: cover (empty && tv == 1'b0 && wptr == 2'd2);
end
always @(posedge clk) if (!init) c_both: cover (push && pop && !full && !empty);
`endif
endmoduleDepth 6 passes and depth 7 fails for the first fault: reset step, four fills, one attempt, one cycle to see it. The reset sentence has no property in the file: add one, and plant a fault only it sees (reset leaves one phantom word, pointers and count in agreement).
// formal: prove 4 expect pass slang top sync_fifo
`define FORMAL
// sync_fifo: 4-entry, 8-bit synchronous FIFO. Running example of the Formal course.
// Synchronous active-high reset. count is an output so properties can see it.
// BUG selects a planted bug: 0 none, 1 push accepted when full, 2 pop accepted when
// empty, 3 full flag one entry early, 4 simultaneous push+pop miscounts, 5 write
// data bit 7 stuck at 0.
module sync_fifo #(parameter BUG = 0, parameter HELPER = 1) (
input wire clk,
input wire rst,
input wire push,
input wire [7:0] wdata,
input wire pop,
output wire [7:0] rdata,
output wire full,
output wire empty,
output reg [2:0] count
);
reg [7:0] mem [0:3];
reg [1:0] wptr, rptr;
assign full = (BUG == 3) ? (count >= 3'd3) : (count == 3'd4);
assign empty = (count == 3'd0);
assign rdata = mem[rptr];
wire do_push = push && (!full || BUG == 1);
wire do_pop = pop && (!empty || BUG == 2);
always @(posedge clk) begin
if (rst) begin
wptr <= 2'd0;
rptr <= 2'd0;
count <= 3'd0;
end else begin
if (do_push) begin
mem[wptr] <= (BUG == 5) ? {1'b0, wdata[6:0]} : wdata;
wptr <= wptr + 2'd1;
end
if (do_pop) rptr <= rptr + 2'd1;
if (BUG == 4 && do_push && do_pop) count <= count + 3'd1; // forgets the pop
else count <= count + {2'b0, do_push} - {2'b0, do_pop};
end
end
`ifdef FORMAL
// ---- harness: first cycle is reset, state before it is arbitrary ----
reg init = 1'b1;
always @(posedge clk) init <= 1'b0;
always @(*) if (init) assume (rst);
// ---- safety properties (after the reset cycle) ----
always @(*) if (!init) begin
a_range: assert (count <= 3'd4); // no overflow, no underflow wrap
a_flags: assert (full == (count == 3'd4) && empty == (count == 3'd0));
a_not_both: assert (!(full && empty));
// helper invariant: occupancy equals pointer distance (mod 4) unless full
a_ptr: assert (count[1:0] == (wptr - rptr));
end
// ---- transition properties (use history: skip the cycle after reset) ----
always @(posedge clk) if (!init && !$past(rst)) begin
// push alone when not full adds one; pop alone when not empty removes one
a_push_inc: assert (!($past(push) && !$past(pop) && !$past(full)) || count == $past(count) + 3'd1);
a_pop_dec: assert (!($past(pop) && !$past(push) && !$past(empty)) || count == $past(count) - 3'd1);
// push into a full FIFO (and no pop) is ignored: occupancy unchanged
a_ovf_ign: assert (!($past(push) && !$past(pop) && $past(count) == 3'd4) || count == $past(count));
// simultaneous push and pop (neither empty nor full) leaves occupancy unchanged
a_both: assert (!($past(push) && $past(pop) && !$past(full) && !$past(empty)) || count == $past(count));
end
// ---- data integrity by tracked-slot abstraction (one free slot, one shadow value) ----
// free constant: (* anyconst *) alone is not honoured by the front end named in the header, so the
// value is also assumed equal to its one-cycle-old copy (works in both front ends;
// a clocked assume ($stable) lags one step and leaks at the last step: do not use it)
(* anyconst *) wire [1:0] tslot;
reg [1:0] tslot_q;
always @(posedge clk) tslot_q <= tslot;
always @(*) if (!init) assume (tslot == tslot_q);
reg tv; // the tracked slot holds a value we are watching
reg [7:0] td; // the value that should be there
always @(posedge clk) begin
if (rst) tv <= 1'b0;
else begin
if (do_push && wptr == tslot) begin tv <= 1'b1; td <= wdata; end
else if (do_pop && rptr == tslot) tv <= 1'b0;
end
end
always @(*) if (!init && tv && !empty && rptr == tslot) a_data: assert (rdata == td);
// helper invariant needed for the induction step of a_data
generate if (HELPER) begin : g_helper
always @(*) if (!init && tv) a_tracked: assert (mem[tslot] == td);
end endgenerate
// ---- covers: the properties are not vacuous ----
always @(*) if (!init) begin
c_full: cover (count == 3'd4);
c_empty_after_use: cover (empty && tv == 1'b0 && wptr == 2'd2);
end
always @(posedge clk) if (!init) c_both: cover (push && pop && !full && !empty);
// reset empties the FIFO: in the cycle after a reset cycle every control register holds its reset value
always @(posedge clk) if (!init && $past(rst)) a_reset: assert (count == 3'd0 && wptr == 2'd0 && rptr == 2'd0);
`endif
endmodule// formal: bmc 3 expect fail slang top sync_fifo
`define FORMAL
// sync_fifo: 4-entry, 8-bit synchronous FIFO. Running example of the Formal course.
// Synchronous active-high reset. count is an output so properties can see it.
// BUG selects a planted bug: 0 none, 1 push accepted when full, 2 pop accepted when
// empty, 3 full flag one entry early, 4 simultaneous push+pop miscounts, 5 write
// data bit 7 stuck at 0.
module sync_fifo #(parameter BUG = 0, parameter HELPER = 1) (
input wire clk,
input wire rst,
input wire push,
input wire [7:0] wdata,
input wire pop,
output wire [7:0] rdata,
output wire full,
output wire empty,
output reg [2:0] count
);
reg [7:0] mem [0:3];
reg [1:0] wptr, rptr;
assign full = (BUG == 3) ? (count >= 3'd3) : (count == 3'd4);
assign empty = (count == 3'd0);
assign rdata = mem[rptr];
wire do_push = push && (!full || BUG == 1);
wire do_pop = pop && (!empty || BUG == 2);
always @(posedge clk) begin
if (rst) begin
wptr <= 2'd1; // planted: reset leaves one phantom word
rptr <= 2'd0;
count <= 3'd1;
end else begin
if (do_push) begin
mem[wptr] <= (BUG == 5) ? {1'b0, wdata[6:0]} : wdata;
wptr <= wptr + 2'd1;
end
if (do_pop) rptr <= rptr + 2'd1;
if (BUG == 4 && do_push && do_pop) count <= count + 3'd1; // forgets the pop
else count <= count + {2'b0, do_push} - {2'b0, do_pop};
end
end
`ifdef FORMAL
// ---- harness: first cycle is reset, state before it is arbitrary ----
reg init = 1'b1;
always @(posedge clk) init <= 1'b0;
always @(*) if (init) assume (rst);
// ---- safety properties (after the reset cycle) ----
always @(*) if (!init) begin
a_range: assert (count <= 3'd4); // no overflow, no underflow wrap
a_flags: assert (full == (count == 3'd4) && empty == (count == 3'd0));
a_not_both: assert (!(full && empty));
// helper invariant: occupancy equals pointer distance (mod 4) unless full
a_ptr: assert (count[1:0] == (wptr - rptr));
end
// ---- transition properties (use history: skip the cycle after reset) ----
always @(posedge clk) if (!init && !$past(rst)) begin
// push alone when not full adds one; pop alone when not empty removes one
a_push_inc: assert (!($past(push) && !$past(pop) && !$past(full)) || count == $past(count) + 3'd1);
a_pop_dec: assert (!($past(pop) && !$past(push) && !$past(empty)) || count == $past(count) - 3'd1);
// push into a full FIFO (and no pop) is ignored: occupancy unchanged
a_ovf_ign: assert (!($past(push) && !$past(pop) && $past(count) == 3'd4) || count == $past(count));
// simultaneous push and pop (neither empty nor full) leaves occupancy unchanged
a_both: assert (!($past(push) && $past(pop) && !$past(full) && !$past(empty)) || count == $past(count));
end
// ---- data integrity by tracked-slot abstraction (one free slot, one shadow value) ----
// free constant: (* anyconst *) alone is not honoured by the front end named in the header, so the
// value is also assumed equal to its one-cycle-old copy (works in both front ends;
// a clocked assume ($stable) lags one step and leaks at the last step: do not use it)
(* anyconst *) wire [1:0] tslot;
reg [1:0] tslot_q;
always @(posedge clk) tslot_q <= tslot;
always @(*) if (!init) assume (tslot == tslot_q);
reg tv; // the tracked slot holds a value we are watching
reg [7:0] td; // the value that should be there
always @(posedge clk) begin
if (rst) tv <= 1'b0;
else begin
if (do_push && wptr == tslot) begin tv <= 1'b1; td <= wdata; end
else if (do_pop && rptr == tslot) tv <= 1'b0;
end
end
always @(*) if (!init && tv && !empty && rptr == tslot) a_data: assert (rdata == td);
// helper invariant needed for the induction step of a_data
generate if (HELPER) begin : g_helper
always @(*) if (!init && tv) a_tracked: assert (mem[tslot] == td);
end endgenerate
// ---- covers: the properties are not vacuous ----
always @(*) if (!init) begin
c_full: cover (count == 3'd4);
c_empty_after_use: cover (empty && tv == 1'b0 && wptr == 2'd2);
end
always @(posedge clk) if (!init) c_both: cover (push && pop && !full && !empty);
// reset empties the FIFO: in the cycle after a reset cycle every control register holds its reset value
always @(posedge clk) if (!init && $past(rst)) a_reset: assert (count == 3'd0 && wptr == 2'd0 && rptr == 2'd0);
`endif
endmodulea_reset fails at step 2 (depth 3; depth 2 passes): the bad state is visible at step 1 and a clocked check reports it one step later. Is each antecedent reached? Two covers answer it.
// formal: cover 6 expect reached slang top sync_fifo
`define FORMAL
// sync_fifo: 4-entry, 8-bit synchronous FIFO. Running example of the Formal course.
// Synchronous active-high reset. count is an output so properties can see it.
// BUG selects a planted bug: 0 none, 1 push accepted when full, 2 pop accepted when
// empty, 3 full flag one entry early, 4 simultaneous push+pop miscounts, 5 write
// data bit 7 stuck at 0.
module sync_fifo #(parameter BUG = 0, parameter HELPER = 1) (
input wire clk,
input wire rst,
input wire push,
input wire [7:0] wdata,
input wire pop,
output wire [7:0] rdata,
output wire full,
output wire empty,
output reg [2:0] count
);
reg [7:0] mem [0:3];
reg [1:0] wptr, rptr;
assign full = (BUG == 3) ? (count >= 3'd3) : (count == 3'd4);
assign empty = (count == 3'd0);
assign rdata = mem[rptr];
wire do_push = push && (!full || BUG == 1);
wire do_pop = pop && (!empty || BUG == 2);
always @(posedge clk) begin
if (rst) begin
wptr <= 2'd0;
rptr <= 2'd0;
count <= 3'd0;
end else begin
if (do_push) begin
mem[wptr] <= (BUG == 5) ? {1'b0, wdata[6:0]} : wdata;
wptr <= wptr + 2'd1;
end
if (do_pop) rptr <= rptr + 2'd1;
if (BUG == 4 && do_push && do_pop) count <= count + 3'd1; // forgets the pop
else count <= count + {2'b0, do_push} - {2'b0, do_pop};
end
end
`ifdef FORMAL
// ---- harness: first cycle is reset, state before it is arbitrary ----
reg init = 1'b1;
always @(posedge clk) init <= 1'b0;
always @(*) if (init) assume (rst);
// ---- safety properties (after the reset cycle) ----
always @(*) if (!init) begin
a_range: assert (count <= 3'd4); // no overflow, no underflow wrap
a_flags: assert (full == (count == 3'd4) && empty == (count == 3'd0));
a_not_both: assert (!(full && empty));
// helper invariant: occupancy equals pointer distance (mod 4) unless full
a_ptr: assert (count[1:0] == (wptr - rptr));
end
// ---- transition properties (use history: skip the cycle after reset) ----
always @(posedge clk) if (!init && !$past(rst)) begin
// push alone when not full adds one; pop alone when not empty removes one
a_push_inc: assert (!($past(push) && !$past(pop) && !$past(full)) || count == $past(count) + 3'd1);
a_pop_dec: assert (!($past(pop) && !$past(push) && !$past(empty)) || count == $past(count) - 3'd1);
// push into a full FIFO (and no pop) is ignored: occupancy unchanged
a_ovf_ign: assert (!($past(push) && !$past(pop) && $past(count) == 3'd4) || count == $past(count));
// simultaneous push and pop (neither empty nor full) leaves occupancy unchanged
a_both: assert (!($past(push) && $past(pop) && !$past(full) && !$past(empty)) || count == $past(count));
end
// ---- data integrity by tracked-slot abstraction (one free slot, one shadow value) ----
// free constant: (* anyconst *) alone is not honoured by the front end named in the header, so the
// value is also assumed equal to its one-cycle-old copy (works in both front ends;
// a clocked assume ($stable) lags one step and leaks at the last step: do not use it)
(* anyconst *) wire [1:0] tslot;
reg [1:0] tslot_q;
always @(posedge clk) tslot_q <= tslot;
always @(*) if (!init) assume (tslot == tslot_q);
reg tv; // the tracked slot holds a value we are watching
reg [7:0] td; // the value that should be there
always @(posedge clk) begin
if (rst) tv <= 1'b0;
else begin
if (do_push && wptr == tslot) begin tv <= 1'b1; td <= wdata; end
else if (do_pop && rptr == tslot) tv <= 1'b0;
end
end
always @(*) if (!init && tv && !empty && rptr == tslot) a_data: assert (rdata == td);
// helper invariant needed for the induction step of a_data
generate if (HELPER) begin : g_helper
always @(*) if (!init && tv) a_tracked: assert (mem[tslot] == td);
end endgenerate
// ---- covers: the properties are not vacuous ----
always @(*) if (!init) begin
c_full: cover (count == 3'd4);
c_empty_after_use: cover (empty && tv == 1'b0 && wptr == 2'd2);
end
always @(posedge clk) if (!init) c_both: cover (push && pop && !full && !empty);
// antecedent covers for the transition properties
always @(*) if (!init) begin
c_ovf: cover (push && !pop && full);
c_pop: cover (pop && !push && !empty);
end
`endif
endmoduleAll covers are reached within depth 6 (c_ovf, a push into a full FIFO, at step 5). A cover not reached within N is inconclusive, not proof of unreachability.
A response unit answers a request d + 2 cycles later, where d (0 to 3) comes with the request. "Within 5 cycles" is bounded response, a safety property; "eventually" is liveness and needs another engine. Three forms look alike:
Form | Needs | Catches |
|---|---|---|
Exactly N ago, | Nothing | Nothing: it fails on the good unit (latency 2 to 5) |
Within N cycles | A counter ( | A slow answer |
One to one | A counter of requests minus answers ( | A second answer, which a window never sees |
// formal: bmc 14 expect pass slang top rsp_unit define LAT=1
`define FORMAL
// rsp_unit: accepts one request at a time and answers it d + 2 cycles later (d is sampled with the request, so the
// latency is 2 to 5 cycles). BUG: 0 none, 1 slow (one extra cycle, 3 to 6), 2 duplicate answer, 3 answer dropped.
// ONE=1 checks one answer per request, EXACT=1 adds a wrong window, LAT=1 adds the exact-latency check.
module rsp_unit #(parameter BUG = 0, parameter ONE = 1, parameter EXACT = 0, parameter LAT = 0) (
input wire clk, input wire rst, input wire req, input wire [1:0] d, output reg rsp);
reg busy; reg [2:0] cnt; reg dup;
wire acc = req && !busy;
always @(posedge clk) begin
rsp <= 1'b0;
if (rst) begin busy <= 1'b0; cnt <= 3'd0; dup <= 1'b0; end
else begin
dup <= 1'b0;
if (acc) begin busy <= 1'b1; cnt <= {1'b0, d} + ((BUG == 1) ? 3'd1 : 3'd0); end
else if (busy && cnt != 3'd0) cnt <= cnt - 3'd1;
else if (busy) begin
busy <= 1'b0;
rsp <= (BUG != 3);
dup <= (BUG == 2);
end
if (dup) rsp <= 1'b1;
end
end
`ifdef FORMAL
reg init = 1'b1;
always @(posedge clk) init <= 1'b0;
always @(*) if (init) assume (rst);
// harness: waiting = a request is unanswered (cleared by the first answer), age = cycles it has waited,
// pend = requests accepted minus answers seen
reg waiting; reg [2:0] age; reg [1:0] pend;
always @(posedge clk) begin
if (rst) begin waiting <= 1'b0; age <= 3'd0; pend <= 2'd0; end
else begin
pend <= pend + {1'b0, acc} - {1'b0, rsp};
if (rsp && waiting) begin waiting <= acc; age <= acc ? 3'd1 : 3'd0; end
else if (acc) begin waiting <= 1'b1; age <= 3'd1; end
else if (waiting) age <= (age == 3'd7) ? age : age + 3'd1;
end
end
// bounded response (a safety property, not liveness): no request waits longer than 5 cycles
always @(*) if (!init) a_within5: assert (age <= 3'd5);
// exact latency: the answer comes d + 2 cycles after the request, where d is the value sampled WITH that request.
// dacc holds d from the last accepted request, which is what a gated past value would give.
reg [1:0] dacc;
always @(posedge clk) if (acc) dacc <= d;
generate if (LAT) begin : g_lat
always @(*) if (!init && rsp && waiting) a_lat: assert (age == {1'b0, dacc} + 3'd2);
end endgenerate
// one answer per request: an answer needs an outstanding request, and requests are not stacked
generate if (ONE) begin : g_one
always @(*) if (!init) begin
a_pend_max: assert (pend <= 2'd1);
a_no_phantom: assert (!rsp || pend != 2'd0);
end
end endgenerate
// the wrong window: "answered exactly 3 cycles after the request"
generate if (EXACT) begin : g_exact
always @(posedge clk) if (!init && !$past(rst)) a_exact3: assert (!rsp || $past(acc, 3));
end endgenerate
always @(*) if (!init) begin c_acc: cover (acc); c_rsp: cover (rsp); c_slow: cover (rsp && age == 3'd5); end
`endif
endmodule// formal: cover 7 expect reached slang top rsp_unit
`define FORMAL
// rsp_unit: accepts one request at a time and answers it d + 2 cycles later (d is sampled with the request, so the
// latency is 2 to 5 cycles). BUG: 0 none, 1 slow (one extra cycle, 3 to 6), 2 duplicate answer, 3 answer dropped.
// ONE=1 checks one answer per request, EXACT=1 adds a wrong window, LAT=1 adds the exact-latency check.
module rsp_unit #(parameter BUG = 0, parameter ONE = 1, parameter EXACT = 0, parameter LAT = 0) (
input wire clk, input wire rst, input wire req, input wire [1:0] d, output reg rsp);
reg busy; reg [2:0] cnt; reg dup;
wire acc = req && !busy;
always @(posedge clk) begin
rsp <= 1'b0;
if (rst) begin busy <= 1'b0; cnt <= 3'd0; dup <= 1'b0; end
else begin
dup <= 1'b0;
if (acc) begin busy <= 1'b1; cnt <= {1'b0, d} + ((BUG == 1) ? 3'd1 : 3'd0); end
else if (busy && cnt != 3'd0) cnt <= cnt - 3'd1;
else if (busy) begin
busy <= 1'b0;
rsp <= (BUG != 3);
dup <= (BUG == 2);
end
if (dup) rsp <= 1'b1;
end
end
`ifdef FORMAL
reg init = 1'b1;
always @(posedge clk) init <= 1'b0;
always @(*) if (init) assume (rst);
// harness: waiting = a request is unanswered (cleared by the first answer), age = cycles it has waited,
// pend = requests accepted minus answers seen
reg waiting; reg [2:0] age; reg [1:0] pend;
always @(posedge clk) begin
if (rst) begin waiting <= 1'b0; age <= 3'd0; pend <= 2'd0; end
else begin
pend <= pend + {1'b0, acc} - {1'b0, rsp};
if (rsp && waiting) begin waiting <= acc; age <= acc ? 3'd1 : 3'd0; end
else if (acc) begin waiting <= 1'b1; age <= 3'd1; end
else if (waiting) age <= (age == 3'd7) ? age : age + 3'd1;
end
end
// bounded response (a safety property, not liveness): no request waits longer than 5 cycles
always @(*) if (!init) a_within5: assert (age <= 3'd5);
// exact latency: the answer comes d + 2 cycles after the request, where d is the value sampled WITH that request.
// dacc holds d from the last accepted request, which is what a gated past value would give.
reg [1:0] dacc;
always @(posedge clk) if (acc) dacc <= d;
generate if (LAT) begin : g_lat
always @(*) if (!init && rsp && waiting) a_lat: assert (age == {1'b0, dacc} + 3'd2);
end endgenerate
// one answer per request: an answer needs an outstanding request, and requests are not stacked
generate if (ONE) begin : g_one
always @(*) if (!init) begin
a_pend_max: assert (pend <= 2'd1);
a_no_phantom: assert (!rsp || pend != 2'd0);
end
end endgenerate
// the wrong window: "answered exactly 3 cycles after the request"
generate if (EXACT) begin : g_exact
always @(posedge clk) if (!init && !$past(rst)) a_exact3: assert (!rsp || $past(acc, 3));
end endgenerate
always @(*) if (!init) begin c_acc: cover (acc); c_rsp: cover (rsp); c_slow: cover (rsp && age == 3'd5); end
`endif
endmodule// formal: bmc 8 expect fail slang top rsp_unit define BUG=1
`define FORMAL
// rsp_unit: accepts one request at a time and answers it d + 2 cycles later (d is sampled with the request, so the
// latency is 2 to 5 cycles). BUG: 0 none, 1 slow (one extra cycle, 3 to 6), 2 duplicate answer, 3 answer dropped.
// ONE=1 checks one answer per request, EXACT=1 adds a wrong window, LAT=1 adds the exact-latency check.
module rsp_unit #(parameter BUG = 0, parameter ONE = 1, parameter EXACT = 0, parameter LAT = 0) (
input wire clk, input wire rst, input wire req, input wire [1:0] d, output reg rsp);
reg busy; reg [2:0] cnt; reg dup;
wire acc = req && !busy;
always @(posedge clk) begin
rsp <= 1'b0;
if (rst) begin busy <= 1'b0; cnt <= 3'd0; dup <= 1'b0; end
else begin
dup <= 1'b0;
if (acc) begin busy <= 1'b1; cnt <= {1'b0, d} + ((BUG == 1) ? 3'd1 : 3'd0); end
else if (busy && cnt != 3'd0) cnt <= cnt - 3'd1;
else if (busy) begin
busy <= 1'b0;
rsp <= (BUG != 3);
dup <= (BUG == 2);
end
if (dup) rsp <= 1'b1;
end
end
`ifdef FORMAL
reg init = 1'b1;
always @(posedge clk) init <= 1'b0;
always @(*) if (init) assume (rst);
// harness: waiting = a request is unanswered (cleared by the first answer), age = cycles it has waited,
// pend = requests accepted minus answers seen
reg waiting; reg [2:0] age; reg [1:0] pend;
always @(posedge clk) begin
if (rst) begin waiting <= 1'b0; age <= 3'd0; pend <= 2'd0; end
else begin
pend <= pend + {1'b0, acc} - {1'b0, rsp};
if (rsp && waiting) begin waiting <= acc; age <= acc ? 3'd1 : 3'd0; end
else if (acc) begin waiting <= 1'b1; age <= 3'd1; end
else if (waiting) age <= (age == 3'd7) ? age : age + 3'd1;
end
end
// bounded response (a safety property, not liveness): no request waits longer than 5 cycles
always @(*) if (!init) a_within5: assert (age <= 3'd5);
// exact latency: the answer comes d + 2 cycles after the request, where d is the value sampled WITH that request.
// dacc holds d from the last accepted request, which is what a gated past value would give.
reg [1:0] dacc;
always @(posedge clk) if (acc) dacc <= d;
generate if (LAT) begin : g_lat
always @(*) if (!init && rsp && waiting) a_lat: assert (age == {1'b0, dacc} + 3'd2);
end endgenerate
// one answer per request: an answer needs an outstanding request, and requests are not stacked
generate if (ONE) begin : g_one
always @(*) if (!init) begin
a_pend_max: assert (pend <= 2'd1);
a_no_phantom: assert (!rsp || pend != 2'd0);
end
end endgenerate
// the wrong window: "answered exactly 3 cycles after the request"
generate if (EXACT) begin : g_exact
always @(posedge clk) if (!init && !$past(rst)) a_exact3: assert (!rsp || $past(acc, 3));
end endgenerate
always @(*) if (!init) begin c_acc: cover (acc); c_rsp: cover (rsp); c_slow: cover (rsp && age == 3'd5); end
`endif
endmodule// formal: bmc 5 expect fail slang top rsp_unit define BUG=1 define LAT=1
`define FORMAL
// rsp_unit: accepts one request at a time and answers it d + 2 cycles later (d is sampled with the request, so the
// latency is 2 to 5 cycles). BUG: 0 none, 1 slow (one extra cycle, 3 to 6), 2 duplicate answer, 3 answer dropped.
// ONE=1 checks one answer per request, EXACT=1 adds a wrong window, LAT=1 adds the exact-latency check.
module rsp_unit #(parameter BUG = 0, parameter ONE = 1, parameter EXACT = 0, parameter LAT = 0) (
input wire clk, input wire rst, input wire req, input wire [1:0] d, output reg rsp);
reg busy; reg [2:0] cnt; reg dup;
wire acc = req && !busy;
always @(posedge clk) begin
rsp <= 1'b0;
if (rst) begin busy <= 1'b0; cnt <= 3'd0; dup <= 1'b0; end
else begin
dup <= 1'b0;
if (acc) begin busy <= 1'b1; cnt <= {1'b0, d} + ((BUG == 1) ? 3'd1 : 3'd0); end
else if (busy && cnt != 3'd0) cnt <= cnt - 3'd1;
else if (busy) begin
busy <= 1'b0;
rsp <= (BUG != 3);
dup <= (BUG == 2);
end
if (dup) rsp <= 1'b1;
end
end
`ifdef FORMAL
reg init = 1'b1;
always @(posedge clk) init <= 1'b0;
always @(*) if (init) assume (rst);
// harness: waiting = a request is unanswered (cleared by the first answer), age = cycles it has waited,
// pend = requests accepted minus answers seen
reg waiting; reg [2:0] age; reg [1:0] pend;
always @(posedge clk) begin
if (rst) begin waiting <= 1'b0; age <= 3'd0; pend <= 2'd0; end
else begin
pend <= pend + {1'b0, acc} - {1'b0, rsp};
if (rsp && waiting) begin waiting <= acc; age <= acc ? 3'd1 : 3'd0; end
else if (acc) begin waiting <= 1'b1; age <= 3'd1; end
else if (waiting) age <= (age == 3'd7) ? age : age + 3'd1;
end
end
// bounded response (a safety property, not liveness): no request waits longer than 5 cycles
always @(*) if (!init) a_within5: assert (age <= 3'd5);
// exact latency: the answer comes d + 2 cycles after the request, where d is the value sampled WITH that request.
// dacc holds d from the last accepted request, which is what a gated past value would give.
reg [1:0] dacc;
always @(posedge clk) if (acc) dacc <= d;
generate if (LAT) begin : g_lat
always @(*) if (!init && rsp && waiting) a_lat: assert (age == {1'b0, dacc} + 3'd2);
end endgenerate
// one answer per request: an answer needs an outstanding request, and requests are not stacked
generate if (ONE) begin : g_one
always @(*) if (!init) begin
a_pend_max: assert (pend <= 2'd1);
a_no_phantom: assert (!rsp || pend != 2'd0);
end
end endgenerate
// the wrong window: "answered exactly 3 cycles after the request"
generate if (EXACT) begin : g_exact
always @(posedge clk) if (!init && !$past(rst)) a_exact3: assert (!rsp || $past(acc, 3));
end endgenerate
always @(*) if (!init) begin c_acc: cover (acc); c_rsp: cover (rsp); c_slow: cover (rsp && age == 3'd5); end
`endif
endmodule// formal: bmc 14 expect pass slang top rsp_unit define BUG=2 define ONE=0
`define FORMAL
// rsp_unit: accepts one request at a time and answers it d + 2 cycles later (d is sampled with the request, so the
// latency is 2 to 5 cycles). BUG: 0 none, 1 slow (one extra cycle, 3 to 6), 2 duplicate answer, 3 answer dropped.
// ONE=1 checks one answer per request, EXACT=1 adds a wrong window, LAT=1 adds the exact-latency check.
module rsp_unit #(parameter BUG = 0, parameter ONE = 1, parameter EXACT = 0, parameter LAT = 0) (
input wire clk, input wire rst, input wire req, input wire [1:0] d, output reg rsp);
reg busy; reg [2:0] cnt; reg dup;
wire acc = req && !busy;
always @(posedge clk) begin
rsp <= 1'b0;
if (rst) begin busy <= 1'b0; cnt <= 3'd0; dup <= 1'b0; end
else begin
dup <= 1'b0;
if (acc) begin busy <= 1'b1; cnt <= {1'b0, d} + ((BUG == 1) ? 3'd1 : 3'd0); end
else if (busy && cnt != 3'd0) cnt <= cnt - 3'd1;
else if (busy) begin
busy <= 1'b0;
rsp <= (BUG != 3);
dup <= (BUG == 2);
end
if (dup) rsp <= 1'b1;
end
end
`ifdef FORMAL
reg init = 1'b1;
always @(posedge clk) init <= 1'b0;
always @(*) if (init) assume (rst);
// harness: waiting = a request is unanswered (cleared by the first answer), age = cycles it has waited,
// pend = requests accepted minus answers seen
reg waiting; reg [2:0] age; reg [1:0] pend;
always @(posedge clk) begin
if (rst) begin waiting <= 1'b0; age <= 3'd0; pend <= 2'd0; end
else begin
pend <= pend + {1'b0, acc} - {1'b0, rsp};
if (rsp && waiting) begin waiting <= acc; age <= acc ? 3'd1 : 3'd0; end
else if (acc) begin waiting <= 1'b1; age <= 3'd1; end
else if (waiting) age <= (age == 3'd7) ? age : age + 3'd1;
end
end
// bounded response (a safety property, not liveness): no request waits longer than 5 cycles
always @(*) if (!init) a_within5: assert (age <= 3'd5);
// exact latency: the answer comes d + 2 cycles after the request, where d is the value sampled WITH that request.
// dacc holds d from the last accepted request, which is what a gated past value would give.
reg [1:0] dacc;
always @(posedge clk) if (acc) dacc <= d;
generate if (LAT) begin : g_lat
always @(*) if (!init && rsp && waiting) a_lat: assert (age == {1'b0, dacc} + 3'd2);
end endgenerate
// one answer per request: an answer needs an outstanding request, and requests are not stacked
generate if (ONE) begin : g_one
always @(*) if (!init) begin
a_pend_max: assert (pend <= 2'd1);
a_no_phantom: assert (!rsp || pend != 2'd0);
end
end endgenerate
// the wrong window: "answered exactly 3 cycles after the request"
generate if (EXACT) begin : g_exact
always @(posedge clk) if (!init && !$past(rst)) a_exact3: assert (!rsp || $past(acc, 3));
end endgenerate
always @(*) if (!init) begin c_acc: cover (acc); c_rsp: cover (rsp); c_slow: cover (rsp && age == 3'd5); end
`endif
endmodule// formal: bmc 5 expect fail slang top rsp_unit define BUG=2
`define FORMAL
// rsp_unit: accepts one request at a time and answers it d + 2 cycles later (d is sampled with the request, so the
// latency is 2 to 5 cycles). BUG: 0 none, 1 slow (one extra cycle, 3 to 6), 2 duplicate answer, 3 answer dropped.
// ONE=1 checks one answer per request, EXACT=1 adds a wrong window, LAT=1 adds the exact-latency check.
module rsp_unit #(parameter BUG = 0, parameter ONE = 1, parameter EXACT = 0, parameter LAT = 0) (
input wire clk, input wire rst, input wire req, input wire [1:0] d, output reg rsp);
reg busy; reg [2:0] cnt; reg dup;
wire acc = req && !busy;
always @(posedge clk) begin
rsp <= 1'b0;
if (rst) begin busy <= 1'b0; cnt <= 3'd0; dup <= 1'b0; end
else begin
dup <= 1'b0;
if (acc) begin busy <= 1'b1; cnt <= {1'b0, d} + ((BUG == 1) ? 3'd1 : 3'd0); end
else if (busy && cnt != 3'd0) cnt <= cnt - 3'd1;
else if (busy) begin
busy <= 1'b0;
rsp <= (BUG != 3);
dup <= (BUG == 2);
end
if (dup) rsp <= 1'b1;
end
end
`ifdef FORMAL
reg init = 1'b1;
always @(posedge clk) init <= 1'b0;
always @(*) if (init) assume (rst);
// harness: waiting = a request is unanswered (cleared by the first answer), age = cycles it has waited,
// pend = requests accepted minus answers seen
reg waiting; reg [2:0] age; reg [1:0] pend;
always @(posedge clk) begin
if (rst) begin waiting <= 1'b0; age <= 3'd0; pend <= 2'd0; end
else begin
pend <= pend + {1'b0, acc} - {1'b0, rsp};
if (rsp && waiting) begin waiting <= acc; age <= acc ? 3'd1 : 3'd0; end
else if (acc) begin waiting <= 1'b1; age <= 3'd1; end
else if (waiting) age <= (age == 3'd7) ? age : age + 3'd1;
end
end
// bounded response (a safety property, not liveness): no request waits longer than 5 cycles
always @(*) if (!init) a_within5: assert (age <= 3'd5);
// exact latency: the answer comes d + 2 cycles after the request, where d is the value sampled WITH that request.
// dacc holds d from the last accepted request, which is what a gated past value would give.
reg [1:0] dacc;
always @(posedge clk) if (acc) dacc <= d;
generate if (LAT) begin : g_lat
always @(*) if (!init && rsp && waiting) a_lat: assert (age == {1'b0, dacc} + 3'd2);
end endgenerate
// one answer per request: an answer needs an outstanding request, and requests are not stacked
generate if (ONE) begin : g_one
always @(*) if (!init) begin
a_pend_max: assert (pend <= 2'd1);
a_no_phantom: assert (!rsp || pend != 2'd0);
end
end endgenerate
// the wrong window: "answered exactly 3 cycles after the request"
generate if (EXACT) begin : g_exact
always @(posedge clk) if (!init && !$past(rst)) a_exact3: assert (!rsp || $past(acc, 3));
end endgenerate
always @(*) if (!init) begin c_acc: cover (acc); c_rsp: cover (rsp); c_slow: cover (rsp && age == 3'd5); end
`endif
endmodule// formal: bmc 5 expect fail slang top rsp_unit define EXACT=1
`define FORMAL
// rsp_unit: accepts one request at a time and answers it d + 2 cycles later (d is sampled with the request, so the
// latency is 2 to 5 cycles). BUG: 0 none, 1 slow (one extra cycle, 3 to 6), 2 duplicate answer, 3 answer dropped.
// ONE=1 checks one answer per request, EXACT=1 adds a wrong window, LAT=1 adds the exact-latency check.
module rsp_unit #(parameter BUG = 0, parameter ONE = 1, parameter EXACT = 0, parameter LAT = 0) (
input wire clk, input wire rst, input wire req, input wire [1:0] d, output reg rsp);
reg busy; reg [2:0] cnt; reg dup;
wire acc = req && !busy;
always @(posedge clk) begin
rsp <= 1'b0;
if (rst) begin busy <= 1'b0; cnt <= 3'd0; dup <= 1'b0; end
else begin
dup <= 1'b0;
if (acc) begin busy <= 1'b1; cnt <= {1'b0, d} + ((BUG == 1) ? 3'd1 : 3'd0); end
else if (busy && cnt != 3'd0) cnt <= cnt - 3'd1;
else if (busy) begin
busy <= 1'b0;
rsp <= (BUG != 3);
dup <= (BUG == 2);
end
if (dup) rsp <= 1'b1;
end
end
`ifdef FORMAL
reg init = 1'b1;
always @(posedge clk) init <= 1'b0;
always @(*) if (init) assume (rst);
// harness: waiting = a request is unanswered (cleared by the first answer), age = cycles it has waited,
// pend = requests accepted minus answers seen
reg waiting; reg [2:0] age; reg [1:0] pend;
always @(posedge clk) begin
if (rst) begin waiting <= 1'b0; age <= 3'd0; pend <= 2'd0; end
else begin
pend <= pend + {1'b0, acc} - {1'b0, rsp};
if (rsp && waiting) begin waiting <= acc; age <= acc ? 3'd1 : 3'd0; end
else if (acc) begin waiting <= 1'b1; age <= 3'd1; end
else if (waiting) age <= (age == 3'd7) ? age : age + 3'd1;
end
end
// bounded response (a safety property, not liveness): no request waits longer than 5 cycles
always @(*) if (!init) a_within5: assert (age <= 3'd5);
// exact latency: the answer comes d + 2 cycles after the request, where d is the value sampled WITH that request.
// dacc holds d from the last accepted request, which is what a gated past value would give.
reg [1:0] dacc;
always @(posedge clk) if (acc) dacc <= d;
generate if (LAT) begin : g_lat
always @(*) if (!init && rsp && waiting) a_lat: assert (age == {1'b0, dacc} + 3'd2);
end endgenerate
// one answer per request: an answer needs an outstanding request, and requests are not stacked
generate if (ONE) begin : g_one
always @(*) if (!init) begin
a_pend_max: assert (pend <= 2'd1);
a_no_phantom: assert (!rsp || pend != 2'd0);
end
end endgenerate
// the wrong window: "answered exactly 3 cycles after the request"
generate if (EXACT) begin : g_exact
always @(posedge clk) if (!init && !$past(rst)) a_exact3: assert (!rsp || $past(acc, 3));
end endgenerate
always @(*) if (!init) begin c_acc: cover (acc); c_rsp: cover (rsp); c_slow: cover (rsp && age == 3'd5); end
`endif
endmoduleThe first run is bounded to 14, with no counterexample; the cover run reaches the slowest answer (c_slow) within depth 7. The slow fault is caught by the window at step 7 (depth 7 passes). The duplicate answer passes the window checks to depth 14 and fails the one-to-one check at step 4: the second pulse has no outstanding request. Exactly-3 fails on the good unit at step 4.
The third argument of $past(x, n, gate) is a gating expression, not a window: $past(x, 1, 8) is $past(x, 1), since 8 is true. A gated past is the value at the last tick where the gate was true. The front end used here rejects it, so the runnable form is a hold register dacc, loaded on each accepted request. LAT=1 checks the answer comes d + 2 cycles after it; the slow fault fails that at step 4 (depth 5), before the window.
// not-run: the front end used here rejects the gating argument of $past
// a_lat: assert property (@(posedge clk) disable iff (rst) rsp |-> age == $past(d, 1, acc) + 2);
// $past(d, 1, acc): d at the previous tick at which acc was true. It is not "d within the last acc" cycles.
// Runnable equivalent in the unit above: reg [1:0] dacc; always @(posedge clk) if (acc) dacc <= d;Each property names its trigger and window, and says whether it is bounded response or liveness.
Every implication has a cover for its antecedent.
A pairing rule uses a counter or tracked item, never an implication alone.
Run each property on the good design first. A failure in the first two cycles is the missing reset guard, not a bug. Then run the planted fault the property was written for and note the step: a_range fails at depth 7 for the first fault, a_flags at depth 5, a_ptr at depth 4 and a_reset at depth 3. A property that does not fail on its own fault is not yet a check. Finally read the cover list: an antecedent cover that is not reached within its depth means the rule never ran, and that is inconclusive until the cover is explained.
The unit answers 2, 3, 4 or 5 cycles after the request: exactly 3 holds for one latency, within 5 for all four.