One interface carries both sides plus the flags. The monitors need cross-side visibility, so splitting push and read sides would blind them.
The listing below is an interface with no testbench, so this course's checks do not run it on its own.
// not-run: fragment: an interface only, no top module
interface fifo_if #(parameter int WIDTH = 8)
(input logic clk, input logic rst_n);
timeunit 1ns; timeprecision 1ps;
// write (push) side
logic in_valid;
logic in_ready;
logic [WIDTH-1:0] in_data;
// read (pop) side
logic out_valid;
logic out_ready;
logic [WIDTH-1:0] out_data;
// status
logic full;
logic empty;
// driver clocking: sample pre-edge, drive 2ns after the edge
clocking drv_cb @(posedge clk);
default input #1step output #2ns;
output in_valid, in_data, out_ready;
input in_ready, out_valid, out_data, full, empty;
endclocking
// monitor clocking: inputs only — a monitor cannot drive, by construction
clocking mon_cb @(posedge clk);
default input #1step;
input in_valid, in_ready, in_data;
input out_valid, out_ready, out_data;
input full, empty;
endclocking
modport DRV (clocking drv_cb, input rst_n);
modport MON (clocking mon_cb, input rst_n);
endinterfaceBoth clocking blocks sample pre-edge values, exactly what the DUT flops see.
Only the three TB-driven signals are clocking outputs, driven 2ns past the edge, clear of hold.
The monitor block is input-only, so a monitor cannot cause contention by construction.
Reset stays outside the clocking blocks, since reset logic needs raw asynchronous access.
Watch in_ready and full. The fourth push parks the FIFO at full, and the 0xB1 offer is ignored while the pop proceeds. Each slot shows values just after its rising edge: the edge that starts slot 5 saw in_ready 0 from slot 4, so 0xB1 was not taken, and in_valid drops before the next edge.
Slot 0: reset: in_valid 0, in_ready 1, full 0, empty 1.
Slot 1: first push 0xA1, empty falls.
Slot 2: push 0xA2.
Slot 3: push 0xA3.
Slot 4: fourth push 0xA4 fills the FIFO: full 1, in_ready 0.
Slot 5: out_ready 1: the pop proceeds while in_valid is still 1, so 0xB1 is ignored and count falls to 3.
Slot 6: drain continues, count 2.
Slot 7: count 1, out_valid still 1.
tb_top owns the clock, the reset, the single interface instance, the DUT and the environment. Nothing else lives at the top.
This harness is not run on its own. It needs the fifo RTL from the last lesson of this topic and the environment classes from the next one.
// not-run: fragment: needs the fifo RTL (last capstone lesson) and the env classes (next lesson)
module tb_top;
timeunit 1ns; timeprecision 1ps;
parameter int WIDTH = 8;
parameter int DEPTH = 16;
logic clk;
logic rst_n;
// clock: 100 MHz
initial clk = 0;
always #5 clk = ~clk;
// reset: assert async, deassert at the inactive (falling) edge,
// so no flop sees rst_n change at the same edge it samples
initial begin
rst_n = 0;
repeat (4) @(posedge clk);
@(negedge clk);
rst_n = 1;
end
// the one real interface instance
fifo_if #(.WIDTH(WIDTH)) fif (.clk(clk), .rst_n(rst_n));
// DUT hookup
fifo #(.WIDTH(WIDTH), .DEPTH(DEPTH)) dut (
.clk (clk),
.rst_n (rst_n),
.in_valid (fif.in_valid),
.in_ready (fif.in_ready),
.in_data (fif.in_data),
.out_valid (fif.out_valid),
.out_ready (fif.out_ready),
.out_data (fif.out_data),
.full (fif.full),
.empty (fif.empty)
);
// environment: generator, driver, monitors, scoreboard (built below)
fifo_env #(.WIDTH(WIDTH), .DEPTH(DEPTH)) e;
initial begin
e = new(fif);
wait (rst_n == 1);
repeat (2) @(posedge clk); // two clean edges: no stimulus into reset
e.run();
$display("capstone run complete");
$finish;
end
// global watchdog — a hang becomes a report, not a lost regression slot
initial begin
#2ms;
$fatal(1, "GLOBAL TIMEOUT: test did not finish");
end
endmoduleThe test ends with $finish. Without it only the watchdog would stop the run.
The timeunit gives the unitless #5 its unit. #2ms is a time literal and carries its own.
Reset deasserts at a falling edge, away from the rising edge the flops sample on, and the environment waits two more edges.
Exactly one interface instance exists. Every component receives a virtual handle to it.
A depth-4 FIFO with a directed handshake test. It pushes four words, proves full, then pops all four in order and proves empty.
module fifo4 (
input logic clk,
input logic rst_n,
input logic in_valid,
output logic in_ready,
input logic [7:0] in_data,
output logic out_valid,
input logic out_ready,
output logic [7:0] out_data,
output logic full,
output logic empty
);
localparam int DEPTH = 4;
logic [7:0] mem [DEPTH];
logic [1:0] wr_ptr = 2'd0, rd_ptr = 2'd0;
logic [2:0] count = 3'd0;
wire push = in_valid && in_ready;
wire pop = out_valid && out_ready;
assign full = (count == 3'd4);
assign empty = (count == 3'd0);
assign in_ready = !full;
assign out_valid = !empty;
assign out_data = mem[rd_ptr];
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
count <= 3'd0;
wr_ptr <= 2'd0;
rd_ptr <= 2'd0;
end else begin
if (push) mem[wr_ptr] <= in_data;
if (push) wr_ptr <= wr_ptr + 2'd1;
if (pop) rd_ptr <= rd_ptr + 2'd1;
if (push && !pop) count <= count + 3'd1;
else if (pop && !push) count <= count - 3'd1;
end
end
endmodule
The monitors need cross-side visibility. Splitting the sides would blind them to each other, and the scoreboard needs both streams.
No. in_ready is low while full is high, so the offer is not taken that cycle. It is accepted only after the pop frees a slot and in_ready rises, per Q-003.