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 in this course's checks on its own. It needs the fifo RTL, which ships separately, and the environment classes from the listings below.
// not-run: fragment: needs the fifo RTL (shipped separately) and the env classes from the other listings
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();
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 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.
The transaction carries two constraint layers. The legality layer encodes what is always true, and the scenario layer uses soft constraints and knob fields so tests dial traffic shape without new classes.
These listings are class declarations with no top module, so this course's checks do not run them.
// not-run: fragment: class declarations only, no top module
typedef enum bit [1:0] { OP_PUSH, OP_POP, OP_BOTH, OP_IDLE } op_e;
class fifo_txn #(parameter int WIDTH = 8);
// randomized fields
rand op_e op;
rand bit [WIDTH-1:0] data;
rand int unsigned gap; // idle cycles before this op
// scenario knobs — set by the generator, not randomized
int push_wt = 5; // relative op weights
int pop_wt = 5;
int both_wt = 2;
int idle_wt = 1;
int max_gap = 3;
// legality layer (hard)
constraint c_gap_legal { gap <= max_gap; }
// scenario layer (weights via knobs; soft data shaping)
constraint c_op_dist {
op dist { OP_PUSH := push_wt, OP_POP := pop_wt,
OP_BOTH := both_wt, OP_IDLE := idle_wt };
}
constraint c_data_interesting {
if (WIDTH > 1 && WIDTH < 30) {
soft data dist { '0 := 1, '1 := 1, [1:(2**WIDTH)-2] :/ 8 };
} else {
// WIDTH 1 has no middle values; very wide WIDTH skips the
// enumerated middle range (2**WIDTH overflows an int).
soft data dist { '0 := 1, '1 := 1 };
}
}
function void display(string tag);
$display("[%0t] %s op=%s data=0x%0h gap=%0d",
$time, tag, op.name(), data, gap);
endfunction
endclass// not-run: fragment: needs fifo_txn from the listing above and has no top module
typedef enum { SCN_SMOKE, SCN_BURST_FILL, SCN_DRAIN,
SCN_MIXED, SCN_BACKPRESSURE } scenario_e;
class generator #(parameter int WIDTH = 8);
mailbox #(fifo_txn #(WIDTH)) gen2drv;
int n_txns = 200;
function new(mailbox #(fifo_txn #(WIDTH)) mb);
gen2drv = mb;
endfunction
// configure one txn's knobs for a scenario phase
function void shape(fifo_txn #(WIDTH) t, scenario_e s);
case (s)
SCN_BURST_FILL: begin t.push_wt=10; t.pop_wt=0; t.both_wt=0; t.idle_wt=0; t.max_gap=0; end
SCN_DRAIN: begin t.push_wt=0; t.pop_wt=10; t.both_wt=0; t.idle_wt=0; t.max_gap=0; end
SCN_MIXED: begin t.push_wt=5; t.pop_wt=5; t.both_wt=3; t.idle_wt=1; t.max_gap=2; end
SCN_BACKPRESSURE: begin t.push_wt=8; t.pop_wt=1; t.both_wt=0; t.idle_wt=1; t.max_gap=1; end
default: begin t.push_wt=6; t.pop_wt=4; t.both_wt=1; t.idle_wt=1; t.max_gap=3; end
endcase
endfunction
task run_scenario(scenario_e s, int count);
fifo_txn #(WIDTH) t;
repeat (count) begin
t = new();
shape(t, s);
if (!t.randomize())
$fatal(1, "generator: randomize failed in scenario %0d", s);
gen2drv.put(t); // bounded mailbox: backpressure for free
end
endtask
// default mix: fill, drain, then sustained mixed traffic
task run();
run_scenario(SCN_BURST_FILL, n_txns/4); // drive toward full (F-02)
run_scenario(SCN_DRAIN, n_txns/4); // drive toward empty (F-03)
run_scenario(SCN_MIXED, n_txns/2); // hammer simultaneity (F-07)
endtask
endclassFILL LEVEL OVER ONE DEFAULT RUN
DEPTH ┤ ╭────╮ burst-fill parks at full ── F-02, F-05 hit
│ ╱ ╲
│ ╱ ╲ drain to empty ── F-03, F-06 hit
│ ╱ ╲ ╭─╮ ╭──╮
│ ╱ ╲ ╱ ╲╱ ╲ mixed: boundary crossings,
0 ┤───╯ ╰──╯ ╰─ simultaneity, wrap-around
└────────────────────────────────────────► time
flat 50/50 instead: ~~~~~ half-full noise, boundaries never seenA flat 50/50 push/pop mix hovers around half-full forever. Phased scenarios walk the fill level to each boundary instead.
Re-shaping traffic is an assignment to knobs, not a new class. Directed tests pin data because the distribution is soft.
The generator checks every randomize call. A knob typo fails loudly instead of freezing stimulus.
The driver translates each transaction into pin wiggles on the driver clocking block. The environment builds every mailbox, starts every thread, and wires reset and end-of-test discipline.
These listings are class declarations that depend on the other listings, with no top module, so this course's checks do not run them.
// not-run: fragment: needs fifo_if and fifo_txn from the listings above and has no top module
class fifo_driver #(parameter int WIDTH = 8);
virtual fifo_if #(WIDTH).DRV vif;
mailbox #(fifo_txn #(WIDTH)) gen2drv;
function new(virtual fifo_if #(WIDTH).DRV v,
mailbox #(fifo_txn #(WIDTH)) mb);
vif = v;
gen2drv = mb;
endfunction
task run();
fifo_txn #(WIDTH) t;
forever begin
gen2drv.get(t);
repeat (t.gap) @(vif.drv_cb); // pacing gap: idle cycles before the op
if (!vif.rst_n) continue; // reset-skip: never drive into reset
case (t.op)
OP_PUSH: begin vif.drv_cb.in_valid <= 1'b1; vif.drv_cb.in_data <= t.data; vif.drv_cb.out_ready <= 1'b0; end
OP_POP: begin vif.drv_cb.in_valid <= 1'b0; vif.drv_cb.out_ready <= 1'b1; end
OP_BOTH: begin vif.drv_cb.in_valid <= 1'b1; vif.drv_cb.in_data <= t.data; vif.drv_cb.out_ready <= 1'b1; end
OP_IDLE: begin vif.drv_cb.in_valid <= 1'b0; vif.drv_cb.out_ready <= 1'b0; end
endcase
@(vif.drv_cb); // hold one cycle so the handshake can complete
vif.drv_cb.in_valid <= 1'b0; // return to idle: every op is one cycle wide
vif.drv_cb.out_ready <= 1'b0;
end
endtask
endclass// not-run: fragment: needs the generator, driver, monitor and scoreboard classes and has no top module
class fifo_env #(parameter int WIDTH = 8, parameter int DEPTH = 16);
virtual fifo_if #(WIDTH) vif;
mailbox #(fifo_txn #(WIDTH)) gen2drv;
mailbox #(logic [WIDTH-1:0]) mb_in, mb_out; // 4-state: an X word survives
generator #(WIDTH) gen;
fifo_driver #(WIDTH) drv;
fifo_in_mon #(WIDTH) in_mon;
fifo_out_mon #(WIDTH) out_mon;
fifo_sb #(WIDTH) sb;
int n_txns = 200;
function new(virtual fifo_if #(WIDTH) v);
vif = v;
gen2drv = new(1); // bounded: generator blocks when driver stalls
mb_in = new();
mb_out = new();
gen = new(gen2drv);
drv = new(vif, gen2drv);
in_mon = new(vif, mb_in);
out_mon = new(vif, mb_out);
sb = new(mb_in, mb_out);
gen.n_txns = n_txns;
endfunction
// reset asserted mid-traffic: the model drops in-flight data (spec rule 8)
task reset_watcher();
forever begin
@(negedge vif.rst_n);
sb.flush();
end
endtask
task run();
fork
drv.run();
in_mon.run();
out_mon.run();
sb.run();
reset_watcher();
join_none
gen.run(); // blocking: returns after n_txns
// num() is a method call, not a signal, so wait() on it may never wake: poll per clock
while (gen2drv.num() != 0 || mb_in.num() != 0 || mb_out.num() != 0) @(vif.mon_cb);
repeat (5) @(vif.mon_cb); // settle: last beats cross the scoreboard
// the pins carry no count, so the test supplies DUT occupancy
// with a hierarchical probe (or the L4 coverage mirror cov.count)
sb.report(tb_top.dut.count);
endtask
endclassEvery op maps to pins explicitly: push and pop each hold for one cycle, then return to idle.
The bounded mailbox couples generator and driver loosely. A slow DUT stalls the generator, not the test.
Reset has two wirings: the driver skips reset cycles, and the watcher flushes the scoreboard on reset assert.
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
Full. Sustained pushes with no pops park at full, targeting F-02 (full exact) and F-05 (push blocked).
data inside {'0, '1};. A directed test then pins data to A5 with an inline constraint and randomize fails. What is wrong?Scenario shaping went into a hard constraint, so it fights the directed test. Shaping must be soft, as c_data_interesting is, so directed tests override it without conflict.