In Part 0 the PacketFIFO bench checked one hand-written packet. Here the checking becomes reusable: one monitor watches each port, rebuilds packets from the accepted bytes, and a scoreboard compares what went in with what came out.
Piece | Job in the PacketFIFO bench | Taught in |
|---|---|---|
| Header fields, payload queue, check byte, | |
| Counts a byte only when valid and ready are both 1 at the edge; publishes a packet at | |
| Pairs the n-th packet in with the n-th packet out | |
End-of-test block | Waits for every packet, then checks counts, leftovers and an empty FIFO |
The FIFO is the Part 0 design, unchanged. A small driver sends six packets of different kind, destination and length while the consumer drops out_ready one cycle in three, so the 4-entry FIFO fills and back-pressures the driver.
Driver: changes pins on the falling edge and holds each byte until in_ready is 1 at a rising edge.
Monitors: sample on the rising edge. The driver moves on the falling edge, so every sample sees settled values.
Scoreboard: the order is the check. The FIFO must keep packet order, so a plain queue compare is enough.
End of test: wait for the output monitor to see all six packets, drain four more cycles, then check.
// Part 7 PacketFIFO step: monitor + scoreboard for the Part 0 packet_fifo.
module packet_fifo #(
parameter WIDTH = 9,
parameter DEPTH = 4,
parameter AW = 2
) (
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 = (count != DEPTH);
assign out_valid = (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
interface pf_if (input logic clk);
logic in_valid, in_ready, out_valid, out_ready;
logic [8:0] in_data, out_data;
endinterface
// One packet as the testbench sees it: header fields, payload, check byte.
class pf_pkt;
bit [1:0] kind;
bit [1:0] dest;
bit [3:0] len;
bit [7:0] payload [$];
bit [7:0] chk;
function bit [7:0] header();
return {kind, dest, len};
endfunction
function void seal();
chk = header();
foreach (payload[i]) chk ^= payload[i];
endfunction
function bit equals(pf_pkt o);
return header() == o.header() && payload == o.payload && chk == o.chk;
endfunction
function string str();
string s = $sformatf("kind=%0d dest=%0d len=%0d chk=%02h data=", kind, dest, len, chk);
foreach (payload[i]) s = {s, $sformatf("%02h ", payload[i])};
return s;
endfunction
endclass
// Minimal driver: holds each byte until the FIFO accepts it.
class pf_driver;
virtual pf_if vif;
mailbox #(pf_pkt) in_mbx;
function new(virtual pf_if vif, mailbox #(pf_pkt) in_mbx);
this.vif = vif;
this.in_mbx = in_mbx;
endfunction
task send_byte(bit [7:0] b, bit last);
vif.in_valid = 1;
vif.in_data = {last, b};
do @(posedge vif.clk); while (!vif.in_ready); // accepted on this edge
@(negedge vif.clk);
endtask
task run();
pf_pkt p;
forever begin
in_mbx.get(p);
send_byte(p.header(), 1'b0);
foreach (p.payload[i]) send_byte(p.payload[i], 1'b0);
send_byte(p.chk, 1'b1);
vif.in_valid = 0;
repeat (2) @(negedge vif.clk);
end
endtask
endclass
// Passive monitor: counts only accepted bytes and publishes a packet at the last byte.
class pf_monitor;
virtual pf_if vif;
bit on_input; // 1: producer port, 0: consumer port
mailbox #(pf_pkt) out_mbx;
int unsigned pkts, bad;
function new(virtual pf_if vif, bit on_input, mailbox #(pf_pkt) out_mbx);
this.vif = vif;
this.on_input = on_input;
this.out_mbx = out_mbx;
endfunction
task run();
bit [7:0] bytes [$];
bit accepted, last;
bit [7:0] b;
forever begin
@(posedge vif.clk); // the driver moves on negedge, so posedge reads settled values
accepted = on_input ? (vif.in_valid === 1 && vif.in_ready === 1)
: (vif.out_valid === 1 && vif.out_ready === 1);
if (accepted) begin
b = on_input ? vif.in_data[7:0] : vif.out_data[7:0];
last = on_input ? vif.in_data[8] : vif.out_data[8];
bytes.push_back(b);
if (last) begin
build(bytes);
bytes.delete();
end
end
end
endtask
function void build(bit [7:0] bytes [$]);
pf_pkt p = new();
if (bytes.size() < 2) begin bad++; return; end
p.kind = bytes[0][7:6];
p.dest = bytes[0][5:4];
p.len = bytes[0][3:0];
for (int i = 1; i < bytes.size() - 1; i++) p.payload.push_back(bytes[i]);
p.chk = bytes[bytes.size() - 1];
pkts++;
void'(out_mbx.try_put(p)); // unbounded mailbox: never blocks the monitor
endfunction
endclass
// In-order scoreboard: the packet seen going in must be the packet seen coming out.
class pf_scoreboard;
mailbox #(pf_pkt) exp_mbx, act_mbx;
int unsigned compares, mismatches;
function new(mailbox #(pf_pkt) exp_mbx, mailbox #(pf_pkt) act_mbx);
this.exp_mbx = exp_mbx;
this.act_mbx = act_mbx;
endfunction
task run();
pf_pkt e, a;
forever begin
exp_mbx.get(e);
act_mbx.get(a);
compares++;
if (!e.equals(a)) begin
mismatches++;
$display("MISMATCH #%0d exp: %s", compares, e.str());
$display(" act: %s", a.str());
end
end
endtask
endclass
PASS: 6 packets, 6 compares, 0 differences, FIFO drainedlast byte?A byte counts only on an edge where in_valid and in_ready are both 1. While the FIFO is full the driver holds the same byte with in_ready low, and the monitor ignores those edges, so no byte is counted twice.
A scoreboard you have never seen fail is a guess. Change one line of the FIFO so that its output flips bit 0 whenever the read pointer is 1.
Replace assign out_data = mem[rptr]; with assign out_data = mem[rptr] ^ ((rptr == 2'd1) ? 9'h001 : 9'h000);
Run the same bench. The first two lines of its output are shown below.
MISMATCH #1 exp: kind=0 dest=2 len=2 chk=2b data=a5 ac
act: kind=0 dest=2 len=2 chk=2b data=a4 acThe dump shows the first difference at a glance: payload byte 0 arrived as a4 instead of a5. The final line is a FAIL, so a regression would catch the run.
omon.pkts and not on the driver finishing?The driver finishes when the last byte is accepted, but packets can still be inside the FIFO. Only the output monitor knows the last packet has left, so the drain waits on it, then adds a short window for stragglers.
Never for this FIFO, because order is part of its contract. A design that may reorder, such as an arbiter with several consumers, needs a by-id lookup instead.
How this was checked. Run with Verilator 5.048 (--binary --timing --assert); the PASS line above is its output, and the broken-FIFO output was produced the same way. The run proves the checks work for these six packets and one fault. It does not prove the FIFO correct for all packets or for random stalls.