UVM
Interface Driving
The signal-level mechanics of driving the DUT — the virtual interface, and driving synchronously through a clocking block whose output skew avoids the race with the DUT's sampling at the clock edge.
Drivers · Module 12 · Page 12.3
The Engineering Problem
The previous chapter mapped a transaction's fields to signals (Module 12.2). This chapter is putting those values on the pins correctly — the signal-level mechanics — and it hinges on a problem that's invisible until it bites: the race condition. The testbench and the DUT both act on the same clock edge — the driver wants to drive a signal, the DUT wants to sample it — and if they touch the signal at the same instant, the order is undefined, so the DUT sometimes sees the old value and sometimes the new. The result is nondeterministic: works on one simulator, fails on another; passes one run, fails the next. Driving signals correctly means driving them in a way that's immune to this race.
Interface driving is the driver's access to the DUT's signals through a virtual interface handle, and — crucially — driving them synchronously through a clocking block. A clocking block defines signals relative to a clock with skews: an output skew delays the driver's drive to just after the clock edge, and an input skew samples just before it — so the driver's drive lands clear of the DUT's sample, eliminating the race. The discipline is drive (and sample) through the clocking block (vif.cb.sig <= value), not the raw signals (vif.sig), because raw driving at the active edge races the DUT. This chapter is interface driving: the virtual interface, the clocking block, modports, the race, and why the clocking block's skew is what makes synchronous driving deterministic.
How does a driver put values on the DUT pins correctly — accessing signals through a virtual interface, and driving synchronously through a clocking block whose output skew avoids the race with the DUT's sampling at the clock edge?
Motivation — why driving needs a clocking block
Correct interface driving needs the clocking block because the testbench-DUT boundary is a concurrency hazard, and the clocking block is the mechanism that resolves it:
- The testbench and DUT are concurrent, so the boundary races. Both run on the same clock; at the active edge, the driver drives and the DUT samples — concurrent actions on the same signal. SystemVerilog gives no defined order for an output written and read at the same instant, so the boundary is a race by default.
- A signal must be driven clear of when it's sampled, so skew is required. To avoid the race, the drive must happen at a different moment than the sample. A skew — driving after the edge — separates them in time, so the DUT reliably samples the intended value (old before the edge, new after).
- The skew must be principled, so the clocking block defines it. Rather than ad-hoc delays (fragile, error-prone), the clocking block declares the signals' timing relative to the clock — output skew for drives, input skew for samples — as a named, reusable construct, so all signals share a consistent, race-free discipline.
- The driver reaches signals from a class, so a virtual interface is required. The DUT's signals live in a module-based
interface; the class-based driver reaches them through a virtual interface handle (Module 7). The clocking block lives in the interface, so the driver drives throughvif.cb. - Directions must be enforced, so modports help. A modport declares which signals a component drives vs samples (a driver modport, a monitor modport), so the driver only drives its outputs and the structure is correct by construction.
The motivation, in one line: the testbench-DUT boundary is a concurrency hazard (both act on the same edge), so correct driving needs a clocking block — a principled, skewed, race-free timing discipline — accessed through a virtual interface, with modports enforcing direction, rather than racing the DUT by driving raw signals at the active edge.
Mental Model
Hold interface driving as passing a note at a scheduled moment, not snatching it at the bell:
Driving through a clocking block is passing a note at a scheduled moment just after the bell, so it never collides with the person grabbing it at the bell — driving the raw signal is snatching at the exact instant of the bell, which collides. Imagine the clock edge is a bell, and at the bell two people act on the same note on the table: you (the driver) want to put down a new note, and your partner (the DUT) wants to pick up the note. If you both reach at the exact instant of the bell, it's chaos — sometimes they grab your new note, sometimes the old one still there, unpredictably (the race). The clocking block is a rule that schedules the exchange: "you put your note down a moment after the bell (output skew), and you read a moment before the bell (input skew)." Now there's no collision: at the bell, your partner picks up the note that was there before (the old, settled value), and just after, you place the new one for next time. The virtual interface is your reach to the table (you can touch the notes from across the room); the clocking block is the etiquette for when you touch them. Snatch at the bell (drive the raw signal at the edge) and you collide; follow the schedule (drive through the clocking block) and the exchange is clean every time.
So interface driving is reach the signals (virtual interface) and use them on schedule (clocking block): drive just after the edge, sample just before, so you never touch a signal at the same instant the DUT does. The clocking block is the schedule that makes the concurrent exchange deterministic.
Visual Explanation — the virtual interface and clocking block
The defining picture is the path from the class-based driver to the DUT signals: through the virtual interface handle, through the clocking block (with its skews), to the pins.
The figure traces the correct path to the pins and where the race protection sits. The class-based driver holds a virtual interface handle — its reach from the class world to the module-based interface that carries the DUT's signals (set via config DB, Module 7). Inside the interface is the clocking block (cb) — the critical construct — which defines the signals relative to the clock with skews: an output skew for driving and an input skew for sampling. The driver drives through vif.cb.signal (the output skew lands the drive just after the clock edge) and samples through vif.cb.signal (the input skew samples just before) — so its accesses are clear of the DUT's sampling at the edge. A modport (not a separate node, but implied) declares directions (the driver's outputs vs inputs), so the structure is correct by construction. The crucial reading is the clocking block's position: it sits between the driver and the raw signals, and all synchronous access goes through it — vif.cb.sig, not vif.sig. That interposition is what separates the driver's drive in time from the DUT's sample, eliminating the race. The virtual interface gives the driver reach; the clocking block gives it race-free timing. The diagram is the discipline made spatial: drive through the clocking block (the vif.cb path), and the skews keep you clear of the DUT — drive the raw signal (vif.sig) and you'd bypass the protection and race.
RTL / Simulation Perspective — the interface, clocking block, and driving
Interface driving is defined in the interface (the clocking block, modports) and used in the driver (vif.cb.sig <= value). The code shows both, and the contrast with the racy raw drive.
// ── THE INTERFACE: signals + a clocking block with skews + a driver modport ──
interface my_if(input bit clk);
logic [31:0] addr, wdata, rdata;
logic wr, valid, ready;
clocking drv_cb @(posedge clk); // a clocking block, synchronized to posedge clk
default input #1step output #1ns; // INPUT skew (sample before edge) / OUTPUT skew (drive after)
output addr, wdata, wr, valid; // signals the driver DRIVES (with output skew)
input rdata, ready; // signals the driver SAMPLES (with input skew)
endclocking
modport drv_mp (clocking drv_cb); // the driver connects through this modport
endinterface
// ── THE DRIVER: drive and sample THROUGH the clocking block (race-free) ──
task my_driver::drive_transaction(bus_item tr);
@(vif.drv_cb); // wait for the clock edge (via the clocking block)
vif.drv_cb.addr <= tr.addr; // ✓ drive through cb → output skew → lands AFTER the edge
vif.drv_cb.wr <= !tr.is_read; // (no race with the DUT sampling AT the edge)
vif.drv_cb.valid <= 1;
@(vif.drv_cb);
if (tr.is_read) tr.rdata = vif.drv_cb.rdata; // ✓ sample through cb → input skew → stable value
vif.drv_cb.valid <= 0;
endtask
// ✗ RACY: vif.valid <= 1 on @(posedge vif.clk) — drives the RAW signal AT the edge, races the DUTThe code shows interface driving's two halves. In the interface: the signals are declared, and a clocking block (drv_cb @(posedge clk)) defines their timing — default input #1step output #1ns sets the input skew (sample 1step before the edge — the settled value) and the output skew (drive 1ns after the edge — clear of the sample); output/input lists which signals the driver drives vs samples. A modport (drv_mp) bundles the clocking block for the driver to connect through (correct directions by construction). In the driver: it waits for the edge via the clocking block (@(vif.drv_cb) — synchronizing to the clocking event), then drives through the clocking block (vif.drv_cb.addr <= tr.addr) — the output skew makes the drive land after the edge, not racing the DUT's sample at the edge — and samples through the clocking block (tr.rdata = vif.drv_cb.rdata) — the input skew gives the stable value (Module 12.2's read sampling, now correctly timed). The final comment is the bug: vif.valid <= 1 on @(posedge vif.clk) drives the raw signal at the edge — bypassing the clocking block and racing the DUT (the DebugLab). The shape to carry: define a clocking block in the interface (with input/output skews), connect through a modport, and drive/sample through vif.cb.sig — never the raw vif.sig at the active edge — so the skews keep the driver clear of the DUT, race-free.
Verification Perspective — the race, and how the clocking block solves it
The whole point of interface driving is race avoidance, and the contrast — raw driving (races) vs clocking-block driving (clean) — is the verification core. Understanding the race is understanding why the clocking block is non-negotiable.
The contrast is the reason the clocking block exists. With raw driving (top), the driver writes the signal at the active clock edge (vif.sig <= v on @(posedge clk)), and the DUT samples it at the same edge — two concurrent accesses to the same signal at the same instant, and SystemVerilog defines no order between them. So the DUT sometimes sees the new value (if the drive scheduled first) and sometimes the old (if the sample scheduled first) — a race, and worse, the outcome is simulator-dependent and seed-dependent, so it "works" until it doesn't (the DebugLab). With clocking-block driving (bottom), the output skew makes the driver's drive land just after the edge — so at the edge, the DUT samples the settled (old) value deterministically, and the driver's new value is ready for the next edge. The drive and the sample are separated in time by the skew, so they never contend — no race. The verification insight is that this race is not a rare corner case — it's the default behavior of any signal driven and sampled at the same edge, which is every synchronous interface. So every synchronous driver must use a clocking block (or an equivalent skew discipline); driving raw signals at the active edge is not a style choice but a bug that's latent (it may pass on your simulator and fail on the regression farm's). The clocking block converts the nondeterministic boundary into a deterministic one, which is the reason it's the standard way to drive an interface — and why "drive through the clocking block" is a rule, not a preference.
Runtime / Execution Flow — the clocking block's skew at the edge
At run time, the clocking block places the driver's drive and the DUT's sample at different points around the clock edge — the input skew before, the output skew after — so they don't contend. The flow shows the timing relationship.
The skew timing is what makes the boundary clean, and its ordering around each edge is the key. Input skew (step 1): the clocking block samples inputs just before the edge — capturing the settled values (what the DUT will act on), so the testbench reads stable data, not values mid-transition. Clock edge (step 2): the DUT acts — it samples the interface signals at the edge, seeing the values that were driven before it (the old, settled values). Output skew (step 3): the driver's new values are applied just after the edge — so they land clear of the DUT's sample (which already happened at the edge), and they settle before the next edge. Next edge (step 4): at the next edge, the DUT samples these new values (now settled). So the order around each edge is sample-before → edge → drive-after, and the driver's drive and the DUT's sample are on opposite sides of the edge — never contending. This is the mechanism of race avoidance: the skews place the two concurrent actions at different times, converting simultaneous (racy) access into sequenced (clean) access. The runtime insight is that the clocking block imposes a phase relationship — drive after, sample before — that's consistent across all the clocked signals, so the entire synchronous interface is race-free by construction, not signal-by-signal vigilance. Understanding this ordering — sample-before, drive-after — is understanding why vif.cb is safe and raw vif is not: the cb path applies the skews; the raw path drives at the edge, in the contention zone.
Waveform Perspective — clean clocking-block drive vs the race
The clocking block's effect is visible on a timeline: the drive lands after the edge (output skew), so the DUT samples cleanly — versus a raw drive at the edge, where the sampled value is ambiguous.
Clocking-block drive (lands after the edge, clean) vs raw drive (at the edge, racy)
12 cyclesThe waveform makes the race and its cure concrete. With the clocking block, cb_drive shows the driver's value changing just after each clock edge (the output skew) — so by the next edge it's settled, and sampled_clean (what the DUT samples) is well-defined: at each edge, the DUT sees the value that was driven before it and has settled. There's no ambiguity — the drive and the sample are on opposite sides of the edge. With the raw drive, raw_drive changes exactly at the edge, so the DUT's sample sampled_racy is ambiguous — shown as X at each edge, because whether the DUT sees the old or new value depends on simulator scheduling, not the design. That X is the race: a value that's undefined by the language, resolved differently by different simulators (or different runs), so the testbench's behavior is nondeterministic. The visual contrast is the whole lesson: the output skew (clocking block) moves the drive off the edge, so the sample is clean; the raw drive sits on the edge, so the sample collides and is undefined. The picture to carry is the separation: a correct synchronous drive never changes a signal at the active edge — it changes it after (output skew), so the DUT always samples a settled value. Reading a waveform for this — does the driven signal change at the edge (racy) or just after (clean)? — is how you spot the race condition that the clocking block exists to prevent.
DebugLab — the driver that worked on one simulator and failed on another
A driver that passed locally but failed on the regression farm — a clock-edge race from driving raw signals
A driver worked perfectly on the engineer's local simulator — every test passed. But on the regression farm (a different simulator), the same driver failed intermittently: the DUT sometimes sampled the wrong value, transactions were occasionally corrupted, and the failures moved with the seed. The driver's logic was correct (the right values, the right fields), and it passed locally every time — yet on another simulator it was nondeterministic. Nothing about the transaction or the DUT was wrong; only which simulator ran it changed the outcome.
The driver drove the raw interface signals at the clock edge — racing the DUT's sampling at the same edge — so the sampled value was undefined, resolved differently by different simulators:
driver (RACY — drives raw signals at the edge):
@(posedge vif.clk);
vif.valid <= 1; vif.addr <= tr.addr; // ✗ drives the RAW signals AT the edge
// the DUT ALSO samples valid/addr AT this same edge → concurrent access, UNDEFINED order
simulator A (local): schedules the drive before the sample → DUT sees NEW value → "works"
simulator B (farm): schedules the sample before the drive → DUT sees OLD value → "fails"
→ a RACE: the outcome depends on the simulator's scheduling, not the design — nondeterministic
fix — drive through a CLOCKING BLOCK (output skew lands the drive AFTER the edge):
// interface: clocking drv_cb @(posedge clk); default output #1ns; output valid, addr; ... endclocking
@(vif.drv_cb);
vif.drv_cb.valid <= 1; vif.drv_cb.addr <= tr.addr; // ✓ output skew → drive clear of the sample
// now the DUT deterministically samples the settled value at the edge — same on every simulatorThis is the clock-edge race, the classic interface-driving bug — and it's insidious because it passes until the simulator (or seed) changes. The driver drove the raw signals (vif.valid, vif.addr) at the active edge (@(posedge vif.clk)), and the DUT samples those signals at the same edge — two concurrent accesses to the same signals at the same instant, which SystemVerilog leaves unordered. So the outcome depends entirely on the simulator's scheduling: simulator A happened to schedule the drive before the sample (DUT saw the new value — "works"), while simulator B scheduled the sample first (DUT saw the old value — "fails"). It's a race, so the behavior is nondeterministic and simulator-dependent — which is exactly the symptom (passes locally, fails on the farm, moves with the seed). The driver's logic was correct; the timing discipline was wrong. The fix is to drive through a clocking block: define clocking drv_cb @(posedge clk) with an output skew (e.g., output #1ns), and drive vif.drv_cb.valid <= 1 — the output skew lands the drive just after the edge, clear of the DUT's sample at the edge, so the DUT deterministically samples the settled value on every simulator. The general lesson, and the chapter's thesis: a synchronous driver must never drive raw signals at the active clock edge, because that races the DUT's sampling and is nondeterministic; always drive (and sample) through a clocking block, whose skews separate the drive from the sample in time, making the boundary race-free and deterministic — the same on every simulator. A driver that races passes until the conditions that expose the race appear, which is why it's a latent bug that surfaces late (on a different simulator, a different seed, the regression farm) — and the clocking block is the standard, required cure.
The tell is correct logic that's nondeterministic or simulator-dependent. Diagnose clock-edge races:
- Correlate failures with the simulator or seed. A driver that passes on one simulator and fails on another (or moves with the seed) despite correct logic is the race signature — the outcome depends on scheduling, not the design.
- Check whether driving uses the clocking block. Look for raw signal drives (
vif.sig <= v) at the active edge (@(posedge vif.clk)) instead of clocking-block drives (vif.cb.sig <= v); the raw drive is the bug. - Inspect the waveform for edge-aligned changes. A driven signal that changes exactly at the active edge (rather than just after) is racing the DUT's sample.
- Confirm the DUT samples the same signals at the same edge. The race requires concurrent access; verify the DUT samples the raced signals at the edge the driver drives them.
Drive synchronously through a clocking block:
- Define a clocking block in the interface. With input and output skews (e.g.,
default input #1step output #1ns), listing the driver's outputs and inputs — the principled, race-free timing discipline. - Drive and sample through the clocking block, never the raw signals. Use
vif.cb.sigfor all synchronous access; reserve rawvif.sigfor genuinely asynchronous signals (like reset, Module 12.4). - Use a modport for direction. A driver modport bundling the clocking block enforces correct directions by construction.
- Test on multiple simulators / seeds. A race is latent; running on more than one simulator and many seeds exposes nondeterminism that a single setup hides.
The one-sentence lesson: driving raw interface signals at the active clock edge races the DUT's sampling at the same edge — an undefined, simulator-dependent outcome that passes until the simulator or seed changes — so always drive (and sample) through a clocking block, whose output skew lands the drive just after the edge, clear of the DUT's sample, making the boundary race-free and deterministic on every simulator.
Common Mistakes
- Driving raw signals at the clock edge. It races the DUT's sampling — nondeterministic and simulator-dependent; drive through a clocking block (
vif.cb.sig). - Sampling raw inputs at the edge. The sampled value can be mid-transition or raced; sample through the clocking block (input skew gives the settled value).
- No clocking block at all. A synchronous interface without a clocking block has no skew discipline, so every drive/sample races; define one in the interface.
- Reaching signals without a virtual interface. The class-based driver needs a virtual interface handle to the interface; get it via config DB (Module 7).
- Driving asynchronous signals through the clocking block (or vice versa). Reset and async controls are driven directly; synchronous signals through the clocking block — match the signal's nature (Module 12.4).
- Trusting a single-simulator pass. A race can pass on your simulator and fail elsewhere; the clocking block makes it deterministic everywhere.
Senior Design Review Notes
Interview Insights
The driver reaches the DUT's signals through a virtual interface handle and drives them synchronously through a clocking block. The virtual interface is the class-based driver's handle to the module-based interface that carries the DUT signals, obtained via the config database, which gives the driver its reach to the pins. The crucial part is the clocking block: inside the interface, a clocking block defines the signals relative to a clock with skews — an output skew for driving and an input skew for sampling. The driver drives signals through the clocking block, like vif.cb.signal assigned to a value, and the output skew makes that drive land just after the clock edge, clear of when the DUT samples at the edge; and it samples inputs through the clocking block, where the input skew captures the settled value just before the edge. The reason this matters is the race condition: the testbench and the DUT both act on the same clock edge — the driver wants to drive, the DUT wants to sample — and if the driver writes the raw signal at the edge, that's concurrent access with undefined order, so the DUT sometimes sees the old value and sometimes the new, nondeterministically and simulator-dependently. The clocking block resolves this by separating the drive from the sample in time via the skews, so the drive lands after the edge and the sample reads before it, eliminating the race. So driving correctly means: get a virtual interface handle, define a clocking block with skews, and drive and sample through the clocking block — vif.cb.signal — never the raw vif.signal at the active edge. A modport bundling the clocking block enforces the directions by construction.
A clocking block is a SystemVerilog construct, declared inside an interface, that defines a set of signals relative to a clock with input and output skews, providing a synchronous, race-free way to drive and sample those signals. It's necessary because of the race between the testbench and the DUT at the clock edge. Both run on the same clock, and at the active edge the driver wants to drive a signal while the DUT wants to sample it — two concurrent accesses to the same signal at the same instant, and SystemVerilog defines no order between them. So if the driver writes the raw signal at the edge, the DUT sometimes samples the new value and sometimes the old, depending on the simulator's scheduling, which is nondeterministic and simulator-dependent — the classic race that passes on one simulator and fails on another. The clocking block fixes this by introducing skews: an output skew delays the driver's drive to just after the edge, and an input skew samples just before the edge. So the driver's drive lands clear of the DUT's sample — at the edge the DUT sees the settled value, and the new value is applied after the edge, ready for the next one. The drive and the sample are placed on opposite sides of the edge, so they never contend, and the result is deterministic on every simulator. The clocking block declares this timing as a named, reusable construct, so all the clocked signals share a consistent skew discipline rather than ad hoc delays. This is why it's necessary, not optional: any signal driven and sampled at the same edge races by default, and that's every synchronous interface, so a clocking block, or an equivalent skew discipline, is required to make the testbench-DUT boundary race-free.
Exercises
- Drive through the clocking block. Sketch an interface with a clocking block (input/output skews) and a driver task that drives
addr/validthrough it, and state why it's race-free. - Spot the race. Given a driver that does
vif.valid <= 1on@(posedge vif.clk), explain the race and rewrite it correctly. - Explain the skews. Describe what the input skew and output skew each do relative to the clock edge, and how they prevent contention.
- Sync vs async. For (a) a data bus and (b) a reset, say which is driven through the clocking block and which directly, and why.
Summary
- Interface driving is the driver's signal-level access to the DUT — through a virtual interface handle (the class-to-module reach, via config DB) — and driving synchronously through a clocking block to avoid the race with the DUT.
- The race: the testbench and DUT both act on the same clock edge (driver drives, DUT samples); touching a signal at that edge gives undefined order — the DUT sees old or new nondeterministically, simulator-dependently.
- The clocking block fixes it with skews: an output skew drives just after the edge, an input skew samples just before — so the drive lands clear of the sample (sample-before → edge → drive-after), making the boundary race-free and deterministic.
- The discipline: drive and sample through the clocking block (
vif.cb.sig), never the raw signals (vif.sig) at the active edge — with a modport enforcing directions, and asynchronous signals (reset) driven directly. - The durable rule of thumb: drive every synchronous signal through a clocking block whose output skew lands the drive just after the clock edge — never the raw signal at the edge — because raw driving races the DUT's sampling and is nondeterministic and simulator-dependent (it passes until the simulator or seed changes), while the clocking block's skew makes it race-free and identical on every simulator.
Next — Timing Handling: the clocking block handles the synchronous boundary; the next chapter covers the broader timing a driver must manage — reset, multi-cycle protocols, wait states, read latency, and the asynchronous events that the clocking block alone doesn't cover.