Virtual interfaces and driver configuration
Give each class-based driver a virtual interface handle to the pins it owns, pass it in explicitly, and check it before the first transaction.
- Lesson
- 6.4
- Time
- 20 min
- Runs on
- Verilator
- Check
- 5 questions
A driver class reached the pins through a hard-coded path, tb.port0.rvalid. When the testbench grew a second read port, it built a second driver for it. Both drivers drove port 0, port 1 sat idle for the whole regression, and coverage of port 1 stayed at zero until someone asked why.
Every beat meant for port 1 appeared on port 0.
Static pins, dynamic objects
Interface instances are part of the elaborated design: they exist before time 0 and never move. Class objects are created while the test runs, as many as it needs, so a class cannot simply contain an interface instance.
A virtual interface bridges the two. virtual rd_if vif; declares a variable that refers to an interface instance, much as a class handle refers to an object. Through it, vif.rvalid and vif.cb.rdata reach the real signals.
A virtual interface is a handle
Like a class handle, a virtual interface starts as null and refers to nothing. Assigning it, as in vif = port1;, makes it refer to that instance; it creates no new signals.
A virtual interface can be assigned again later, like any handle, but a driver should keep the one it was built with. Swapping pins under a running driver mixes the traffic of two ports.
Two handles assigned the same instance reach the same wires, so a change made through one is visible through the other. Two handles assigned different instances drive different pins, which is how one driver class serves any number of ports.
rd_if port0 (clk), port1 (clk);
Two interface instances exist from the start.
port0 instance 0port1 instance 1d0 = new("drv0", port0);
The constructor stores a reference to port 0.
d0.vif instance 0d1 = new("drv1", port1);
The second driver refers to the other instance.
d0.vif instance 0d1.vif instance 1d2 = new("drv2", null);
A driver built without a connection holds
null.d2.vif null
Pass the connection in
The top-level module knows which interface instance belongs to which driver, so it should hand each driver its connection, through the constructor or a configuration object. The driver then uses only vif, never a path such as tb.port0.
Hard-coded path inside the class
task send(logic [7:0] d);
tb.port0.rvalid = 1; // always port 0
...
endtask
Every driver object drives port 0, whatever port it was built for.
Virtual interface passed in
function new(string name, virtual rd_if vif);
this.vif = vif;
endfunction
task send(logic [7:0] d);
vif.cb.rvalid <= 1; // this driver's port
...
endtask
Each driver drives the port it was given.
UVM passes virtual interfaces through its configuration database, which is the same idea with a lookup by name; the UVM track covers it.
Check before the first transaction
A driver whose vif is still null fails at its first use with a null-handle error, often deep inside a task and far from the real cause. Check vif once, when the driver starts, and report which driver is missing its connection.
The check costs one line and turns a confusing crash into a message such as drv2: no interface configured. Treat it as fatal in real tests; the example only prints it so that the run can continue.
A configuration object
As a testbench grows, a driver needs more than its pins: a port number, a maximum stall, a switch for error injection. Collect them in a small configuration class that also holds the virtual interface, and pass that one object to the driver and the monitor of the same port.
The driver and the monitor then agree on the port by construction. Checking the configuration once, including vif != null, covers both components.
Keeping the virtual interface in the configuration also makes a later change painless. Moving a driver to another port means building a different configuration object, while the driver class itself stays the same.
Matching types and modports
A virtual interface's type must match the instance it refers to. For a parameterized interface, the parameters must match too: virtual rd_if #(.W(32)) cannot refer to an 8-bit instance.
A virtual interface can also name a modport, as in virtual rd_if.source vif;. The driver then has only the source role's access, so a driver that tries to write rready fails to compile, just as a module port with a modport would.
The same timing rules apply through a virtual interface as through a port. Drive through the clocking block and sample through it; the handle changes which pins you reach, not when you reach them.
Try it
Both drivers ran in parallel, each on the port it was given, so the same edge shows 0a on port 0 and 1b on port 1. The third driver, built without a connection, reported the problem instead of crashing.
Build d2 with port1 instead of null, and predict the line it adds and the time on it.
t=15 port0 carries 0a t=15 port1 carries 1b drv2: no interface configured; nothing sentCheck yourself
Remember
- Interface instances are static; a virtual interface is a handle that lets a class reach one.
- A virtual interface is
nulluntil assigned, and assigning it creates no signals. - Pass each driver its virtual interface; never hard-code a hierarchical path inside the class.
- Check for
nullwhen the driver starts and name the driver in the message. - Through a virtual interface, still drive and sample through the clocking block.
Practice
- Give each driver its own bus Implement · Easy
- The monitors that watched one bus Fix · Medium