Part 8 · Senior & Interview Prep · Intermediate
Step 2: Interface & Harness
fifo_if with clocking blocks for TB and monitor, and tb_top with clock/reset generation and DUT hookup — complete code.
Design decisions first
One interface carries both sides plus flags — the FIFO is one protocol, and the monitors need cross-side visibility (flags vs handshakes).
Two clocking blocks: drv_cb drives TB outputs with a skew past hold; mon_cb is input-only so monitors physically cannot drive.
The interface is parameterized by WIDTH so the same file serves every configuration in the regression.
rst_n stays outside the clocking blocks — reset logic needs raw asynchronous visibility.
fifo_if — complete code
interface fifo_if #(parameter int WIDTH = 8)
(input logic clk, input logic rst_n);
// 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);
endinterfaceCode walkthrough
input #1step — both clocking blocks sample Preponed (pre-edge) values, exactly what the DUT's flops see.
output #2ns on drv_cb — drives clear of the hold window; only the three TB-driven signals are outputs.
mon_cb lists every signal as input — compile-time guarantee the monitor never causes contention.
Modports bundle each role's clocking block plus raw rst_n for reset-aware code.
tb_top — complete code
module tb_top;
parameter int WIDTH = 8;
parameter int DEPTH = 16;
logic clk;
logic rst_n;
// clock: 100 MHz
initial clk = 0;
always #5ns clk = ~clk;
// reset: assert async, deassert synchronized to the clock
initial begin
rst_n = 0;
repeat (3) @(posedge 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: built in later steps (gen/drv/mon/sb/cov)
env #(.WIDTH(WIDTH), .DEPTH(DEPTH)) e;
initial begin
e = new(fif); // vif injected by constructor
wait (rst_n);
@(posedge clk);
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
endmoduleCode walkthrough
Reset asserts immediately at time 0 and deasserts with a nonblocking assign at an edge — clean, race-free release.
Exactly one fifo_if instance; every component receives a virtual handle to this same instance.
The env starts only after wait(rst_n) plus one edge — no stimulus into a DUT in reset.
The watchdog $fatal converts every hang class into a diagnosable failure with a timestamp.
Key takeaways
drv_cb for driving, mon_cb (input-only) for sampling — contention is impossible by construction.
Parameterize the interface; one file serves the whole regression's WIDTH sweep.
tb_top owns clock, reset, the real interface, the DUT, the env, and the watchdog — nothing else.
Common pitfalls
Driving DUT inputs from raw interface signals instead of drv_cb — the races return.
Putting rst_n inside a clocking block — reset handling needs raw asynchronous access.
Forgetting the watchdog — a hung corner test silently burns the nightly regression slot.