Part 8 · Senior & Interview Prep · Intermediate
Q&A: Interfaces & Virtual Interfaces
Why interfaces exist, what modports enforce, clocking block skews, why classes need vifs, how the vif reaches the driver, multi-instance handling.
Q: Why do interfaces exist — what problem do they solve?
An interface bundles a related signal group into one named, reusable port . Without it, a 40-signal bus is 40 module ports duplicated at every level of hierarchy — adding one signal touches every file. With it, the bus is one line per connection, the protocol's signals live in one place, and the interface can also carry shared protocol assets: clocking blocks, modports, assertions, and protocol tasks.
interface fifo_if (input logic clk, input logic rst_n);
logic push, pop, full, empty;
logic [7:0] wdata, rdata;
clocking cb @(posedge clk);
default input #1step output #2ns;
output push, pop, wdata;
input full, empty, rdata;
endclocking
modport DUT (input push, pop, wdata, clk, rst_n,
output full, empty, rdata);
modport TB (clocking cb, input rst_n);
endinterface
// connection collapses from 8 ports to one:
fifo dut (.bus(fif.DUT));Follow-up: "What else belongs inside the interface besides signals?" — Clocking blocks (race-free TB timing), modports (direction contracts), concurrent assertions (protocol checks that travel with the bus), and reusable BFM-style tasks.
Junior vs senior: a junior says "groups signals." A senior frames it as the protocol's single source of truth and lists the four extra residents: clocking, modports, assertions, tasks.
Q: What do modports actually enforce?
Modports define direction contracts per connection point — which signals each side may drive and read. The compiler rejects a module that drives a signal its modport declares as input. Crucially this is a compile-time wiring check, not a runtime guarantee — and code that grabs the raw interface (not through a modport) bypasses the contract entirely.
modport DUT (input push, pop, wdata, output full, empty, rdata);
modport TB (output push, pop, wdata, input full, empty, rdata);
module fifo (fifo_if.DUT bus);
assign bus.push = 1'b0; // COMPILE ERROR: DUT modport says input
endmoduleFollow-up: "If modports are compile-time only, what is their real value?" — They catch contention bugs (two drivers on one signal) at compile instead of as X-soup at runtime, and they document intent: a reader of the modport knows the directionality of the whole protocol at a glance.
Junior vs senior: a junior says "modports set directions." A senior says compile-time contract, names the contention-bug class it kills, and notes the raw-interface bypass loophole.
Q: Explain clocking block input and output skews.
A clocking block defines when the TB samples inputs and drives outputs relative to the clock edge . input #1step samples in the Preponed region — the signal value just before the edge, exactly what a real flip-flop would capture. output #2ns drives 2ns after the edge — past hold, comfortably before the next setup. Together they make TB-DUT races structurally impossible.
CLOCKING BLOCK TIMING
setup edge hold
────────────────┬──┬──┬────────────────────► time
│ ▲ │
input #1step ──┘ │ └── output #2ns drives here
samples here │ (after hold window)
(Preponed: value clk
before the edge) rises
Without a clocking block: TB drives at the edge with NBA →
whether DUT sees old or new value depends on scheduler order = race.
With it: sample-before-edge / drive-after-edge by construction.Follow-up: "Why #1step and not #0?" — #0 samples in the Observed region of the current timestep, after the edge's NBA updates may have landed — you can read post-edge values. #1step is defined as the value at the end of the previous timestep: guaranteed pre-edge, exactly like silicon.
Junior vs senior: a junior says "clocking blocks avoid races." A senior explains Preponed-region sampling, why #1step differs from #0, and what the output skew clears (the hold window).
Q: Why do classes need virtual interfaces at all?
Classes are dynamic objects with no elaboration-time existence — they cannot have ports or directly name a static interface instance. A virtual interface is a runtime handle (a pointer) to a real interface instance, set after construction. It is the one sanctioned bridge between the dynamic class world and the static module world: the driver class drives vif.cb.push, and the actual pins move.
class driver;
virtual fifo_if vif; // handle, null until assigned
function new(virtual fifo_if vif);
this.vif = vif; // bind at construction
endfunction
task drive(fifo_txn t);
@(vif.cb); // sync via clocking block
vif.cb.push <= 1;
vif.cb.wdata <= t.data;
@(vif.cb);
vif.cb.push <= 0;
endtask
endclassFollow-up: "What is the most common vif crash?" — Null handle: the driver runs before anyone assigned vif, dying with a null-access fatal at the first vif reference. Defense: assert vif != null in new() or at run start, fail loudly with a message naming the missed connection.
Junior vs senior: a junior says "classes can't have ports." A senior explains the dynamic/static divide, shows the constructor-injection pattern, and pre-empts the null-vif crash with the check.
Q: How does the vif actually travel from tb_top to the driver?
In a class-based TB the standard chain is constructor injection : tb_top instantiates the real interface and passes it to the env constructor; env passes it down to agent, agent to driver and monitor. In UVM the same journey uses uvm_config_db set in tb_top and get in build_phase — same idea, lookup by hierarchical path instead of constructor arguments.
module tb_top;
fifo_if fif (.clk(clk), .rst_n(rst_n)); // REAL instance
fifo dut (.bus(fif.DUT));
initial begin
env e = new(fif); // handle flows down by constructor
e.run();
end
endmodule
class env;
driver drv;
monitor mon;
function new(virtual fifo_if vif);
drv = new(vif); // same instance, two handles
mon = new(vif);
endfunction
endclassFollow-up: "Driver and monitor share one vif — is that a conflict?" — No: the vif is just a handle, and many handles to one instance are fine. Conflicts come from what each does — driver drives through clocking-block outputs, monitor only samples inputs. Direction discipline, not handle count, prevents contention.
Junior vs senior: a junior says "pass it in new()." A senior traces the full tb_top→env→agent→driver chain, names the UVM config_db equivalent, and resolves the shared-handle concern by direction discipline.
Q: How do you handle multiple instances of the same interface?
Instantiate N real interfaces in tb_top, and give each agent its own vif handle bound to its own instance — typically via an array of virtual interfaces or per-agent constructor arguments. The classic bug is binding every agent to ports[0]: all agents drive one physical port while the DUT's other ports sit idle, and the scoreboard mysteriously sees triple traffic on port 0.
module tb_top;
fifo_if port_if[4] (.clk(clk), .rst_n(rst_n)); // 4 real instances
initial begin
virtual fifo_if vifs[4];
foreach (vifs[i]) vifs[i] = port_if[i]; // map each instance
env e = new(vifs);
e.run();
end
endmodule
class env;
agent agents[4];
function new(virtual fifo_if vifs[4]);
foreach (agents[i]) agents[i] = new(vifs[i]); // i-th vif → i-th agent
endfunction
endclassFollow-up: "How would UVM handle the same thing?" — Per-instance config_db keys: set "vif_0"/"vif_1"... scoped to each agent's path, so each agent's get retrieves only its own. The discipline is identical; only the plumbing differs.
Junior vs senior: a junior says "use an array." A senior names the all-bound-to-port-0 bug and its symptom (one port triple-driven, others silent) and gives both the constructor and config_db versions.
Key takeaways
Interface = the protocol's single source of truth: signals + clocking + modports + assertions.
Modports are compile-time direction contracts — they kill contention bugs early.
input #1step samples pre-edge (Preponed); output skew drives past hold — races impossible.
vif is the dynamic-to-static bridge; check non-null before first use.
One real instance per port, one distinct vif per agent — never share by accident.
Common pitfalls
Driving DUT pins from class code at the raw edge instead of through cb — races return.
Null vif at run time — driver dies at first access; assert the handle early.
Binding all agents to instance [0] — phantom triple traffic on one port.
Trusting modports as runtime protection — they are compile-time only.