PacketFIFO is the FIFO from The PacketFIFO Project: a valid/ready port on each side that carries packets as bytes. This checker watches only its ports and adds a little modeling code, so it never reads the design's internal state.
Job | Rules | Taught in |
|---|---|---|
Hold the offer |
| |
Respect capacity | No byte accepted when full, none taken when empty | |
Follow the packet format |
| |
Prove it ran | Covers for stalls, a full FIFO and a packet end |
The file holds the FIFO with a BUG parameter, the checker, and a bench. The bench runs two legal packets with stalls on both ports first, then plants one bug at a time. Every rule reports through fail() so the run continues, because Verilator 5.048 stops at the first $error. Instance 0 is the legal design; instance 1 has in_ready stuck high, and instance 2 has out_valid stuck high.
// PacketFIFO checker: handshake holds, occupancy limits and packet rules on the ports of packet_fifo.
// BUG selects a planted design bug: 0 none, 1 in_ready stuck high (overflow), 2 out_valid stuck high (underflow).
module packet_fifo #(
parameter WIDTH = 9,
parameter DEPTH = 4,
parameter AW = 2,
parameter BUG = 0
) (
input wire clk,
input wire rst_n,
input wire in_valid,
output wire in_ready,
input wire [WIDTH-1:0] in_data,
output wire out_valid,
input wire out_ready,
output wire [WIDTH-1:0] out_data
);
reg [WIDTH-1:0] mem [0:DEPTH-1];
reg [AW-1:0] wptr, rptr;
reg [AW:0] count;
wire push = in_valid && in_ready;
wire pop = out_valid && out_ready;
assign in_ready = (BUG == 1) ? 1'b1 : (count != DEPTH);
assign out_valid = (BUG == 2) ? 1'b1 : (count != 0);
assign out_data = mem[rptr];
always @(posedge clk or negedge rst_n)
if (!rst_n) begin
wptr <= 0;
rptr <= 0;
count <= 0;
end else begin
if (push) begin
mem[wptr] <= in_data;
wptr <= wptr + 1'b1;
end
if (pop) rptr <= rptr + 1'b1;
case ({push, pop})
2'b10: count <= count + 1'b1;
2'b01: count <= count - 1'b1;
default: count <= count;
endcase
end
endmodule
module pf_checker #(
parameter int DEPTH = 4
) (
input logic clk, rst_n,
input logic in_valid, in_ready,
input logic [8:0] in_data,
input logic out_valid, out_ready,
input logic [8:0] out_data
);
int n_fail = 0, n_in_stall = 0, n_out_stall = 0, n_full = 0, n_pkt = 0;
function automatic void fail(string msg);
n_fail++;
$display("[%0t] pf_checker: %s", $time, msg);
endfunction
// Modeling code: occupancy, and where we are inside the current packet.
wire push = in_valid && in_ready;
wire pop = out_valid && out_ready;
int occ = 0;
logic hdr = 1'b1; // next accepted byte is a header
logic [4:0] left = 5'd0; // bytes still to come in this packet, check byte included
logic [7:0] xr = 8'h00; // XOR of the bytes accepted so far in this packet
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin
occ <= 0; hdr <= 1'b1; left <= 5'd0; xr <= 8'h00;
end else begin
occ <= occ + (push ? 1 : 0) - ((pop && occ > 0) ? 1 : 0);
if (push) begin
if (hdr) begin
hdr <= 1'b0; left <= in_data[3:0] + 5'd1; xr <= in_data[7:0];
end else begin
xr <= xr ^ in_data[7:0]; left <= left - 5'd1;
if (left == 5'd1) hdr <= 1'b1;
end
end
end
// Step 1: an offer is held, with the same byte, until it is accepted (both ports).
a_in_valid_hold: assert property (@(posedge clk) disable iff (!rst_n)
(in_valid && !in_ready) |-> ##1 in_valid)
else fail("in_valid dropped before in_ready");
a_in_data_hold: assert property (@(posedge clk) disable iff (!rst_n)
(in_valid && !in_ready) |-> ##1 $stable(in_data))
else fail("in_data changed while stalled");
a_out_valid_hold: assert property (@(posedge clk) disable iff (!rst_n)
(out_valid && !out_ready) |-> ##1 out_valid)
else fail("out_valid dropped before out_ready");
a_out_data_hold: assert property (@(posedge clk) disable iff (!rst_n)
(out_valid && !out_ready) |-> ##1 $stable(out_data))
else fail("out_data changed while stalled");
// Step 2: never accept into a full FIFO, never hand out from an empty one.
a_no_overflow: assert property (@(posedge clk) disable iff (!rst_n)
(occ == DEPTH) |-> !push)
else fail("overflow: byte accepted while full");
a_no_underflow: assert property (@(posedge clk) disable iff (!rst_n)
(occ == 0) |-> !pop)
else fail("underflow: byte taken while empty");
// Step 3: packet rules on the accepted bytes.
a_len_legal: assert property (@(posedge clk) disable iff (!rst_n)
(push && hdr) |-> (in_data[3:0] inside {[1:8]} && !in_data[8]))
else fail("header: len outside 1 to 8, or last set");
a_last_on_check: assert property (@(posedge clk) disable iff (!rst_n)
(push && !hdr) |-> (in_data[8] == (left == 5'd1)))
else fail("last is not on the check byte");
a_check_xor: assert property (@(posedge clk) disable iff (!rst_n)
(push && !hdr && left == 5'd1) |-> ((xr ^ in_data[7:0]) == 8'h00))
else fail("check byte is not the XOR of the packet");
// Step 4: covers prove the antecedents were reached.
c_in_stall: cover property (@(posedge clk) disable iff (!rst_n)
(in_valid && in_ready == 1'b0) ##1 (in_valid && in_ready)) n_in_stall++;
c_out_stall: cover property (@(posedge clk) disable iff (!rst_n)
(out_valid && out_ready == 1'b0) ##1 (out_valid && out_ready)) n_out_stall++;
c_full: cover property (@(posedge clk) disable iff (!rst_n)
occ == DEPTH) n_full++;
c_packet: cover property (@(posedge clk) disable iff (!rst_n)
push && in_data[8]) n_pkt++;
endmodule
two legal packets with stalls on both ports: 0 new report(s)
covers: in-stall 2, out-stall 3, full 9, packet end 2
[365] pf_checker: in_data changed while stalled
payload changed during a stall: 1 new report(s)
[565] pf_checker: header: len outside 1 to 8, or last set
len 0 header: 1 new report(s)
[695] pf_checker: check byte is not the XOR of the packet
wrong check byte: 1 new report(s)
[795] pf_checker: last is not on the check byte
last on a payload byte: 1 new report(s)
[945] pf_checker: overflow: byte accepted while full
[955] pf_checker: out_data changed while stalled
in_ready stuck high, fifth byte into a depth-4 FIFO: 2 report(s)
[975] pf_checker: underflow: byte taken while empty
out_valid stuck high, one read of an empty FIFO: 1 report(s)
PASS: legal packets passed with every cover hit, and each planted bug was reportedtwo legal packets with stalls on both ports: 0 new report(s)
covers: in-stall 2, out-stall 3, full 9, packet end 2
[365] pf_checker: in_data changed while stalled
payload changed during a stall: 1 new report(s)
[565] pf_checker: header: len outside 1 to 8, or last set
len 0 header: 1 new report(s)
[695] pf_checker: check byte is not the XOR of the packet
wrong check byte: 1 new report(s)
[795] pf_checker: last is not on the check byte
last on a payload byte: 1 new report(s)
[945] pf_checker: overflow: byte accepted while full
[955] pf_checker: out_data changed while stalled
in_ready stuck high, fifth byte into a depth-4 FIFO: 2 report(s)
[975] pf_checker: underflow: byte taken while empty
out_valid stuck high, one read of an empty FIFO: 1 report(s)
PASS: legal packets passed with every cover hit, and each planted bug was reportedLegal traffic: no reports, and all four covers hit.
One bug, one report: each planted packet or stall bug is reported by the rule that owns it.
Overflow: two reports, root cause first. The second is the oldest byte changing under a waiting out_valid.
Underflow: one report, from the rule that owns it.
c_full never hits. What can you not conclude about the overflow rule?That it works. With the FIFO never full, the overflow antecedent never matched, so the rule passed vacuously. A cover on occ == DEPTH is the proof that the case was reached.
in_ready stuck high in a separate instance instead of editing the legal one?The legal instance has to stay silent to show that the rules are not too strict. A separate buggy instance shows that they are not too loose, on the same clock and the same checker code.
Run with Verilator 5.048 (--binary --timing --assert, plus --coverage when a listing has a cover property) on 2026-10-18; every bench printed the PASS line shown. Verilator stops at the first $error or failing assertion without an action block, so the benches count failures in the action block. Icarus 13 has no concurrent assertions.
Instance 0 is legal; instance 1 has in_ready stuck high and instance 2 has out_valid stuck high. The overflow gave two reports (the overflow, then the overwritten oldest byte), every other planted bug gave one. Run on 2026-10-25.