Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here6
  • SystemVerilog Language Foundations28
  • Object-Oriented SystemVerilog for Verification30
    • Classes, Objects & Handles
    • Inheritance and Polymorphism
    • Static, Encapsulation & Parameterized Classes
    • Virtual Interfaces: the Class-to-Pins Bridge
      • Why Virtual Interfaces Exist
      • Declaring & Binding a vif
      • Driving & Sampling via Clocking Blocks
      • vif Configuration Patterns
      • Multi-Instance & Parameterized vifs
    • Events, Semaphores, Mailboxes and Processes
    • OOP Patterns for Testbenches
  • Constraint Randomization — Interview-Critical Deep Dive36
  • SystemVerilog Assertions — Interview-Critical Deep Dive40
  • Functional Coverage in SystemVerilog22
  • Testbench Architecture in SystemVerilog23
  • Advanced Language & Integration25
  • Senior & Interview Preparation21
  • Principal-Level Verification28
Loading lesson

‹Virtual Interfaces: the Class-to-Pins Bridge
  1. Topics
  2. SystemVerilog
  3. Object-Oriented SystemVerilog for Verification
  4. Virtual Interfaces: the Class-to-Pins Bridge
  5. Driving & Sampling via Clocking Blocks

Part 2 · OOP for Verification · Intermediate

Driving & Sampling via Clocking Blocks

Drive and sample through vif clocking blocks for race-free class-based stimulus and checks.

On this page

    Why this matters

    A class driver set valid with a blocking assignment at the clock edge, and one simulator missed the first beat. Going through the clocking block makes the timing the same everywhere.

    After this lesson you can: drive and sample from class code through vif clocking blocks, use modport-selected vifs for monitors, and explain the one-cycle sampling lag.

    Where raw drives race

    A blocking drive (vif.valid = 1) at @(posedge vif.clk), or a raw read of DUT outputs there, runs in the same step as the DUT flops. Old or new value wins by scheduling order, which varies between simulators.

    A nonblocking drive (vif.valid <= 1) after the edge does not race: the DUT flops on that edge always see the old value. A clocking block goes further. It samples inputs before the edge and applies drives after it, so every component inherits the same explicit timing through vif.cb.

    The same timing as in Foundations: sample just before the edge, drive just after it.

    One interface, two views

    One canonical interface serves this lesson and every later one. It carries two clocking blocks and one modport per view.

    The driver syncs on @(vif.drv_cb) and drives with <=. The monitor takes a modport-selected vif, which makes driving through it impossible. Both wait with a small loop: do @(vif.drv_cb); while (!vif.drv_cb.ready);. The standard also has a one-line form, @(vif.drv_cb iff vif.drv_cb.ready), which means the same; the loop runs on every simulator.

    Predict first The responder below raises ready every other cycle and returns addr + 1. The driver sends A1, B2 and C3. What does the monitor print?

    Three lines: addr a1 data a2, addr b2 data b3, addr c3 data c4. Each beat is sampled once, at the edge where both valid and ready were high just before it.

    bus_if.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    interface bus_if (input logic clk);
      timeunit 1ns; timeprecision 1ps;
      logic [31:0] addr;
      logic [31:0] rdata;
      logic        valid;
      logic        ready;
    
      clocking drv_cb @(posedge clk);   // driver's view
        default input #1step output #2;
        output addr, valid;
        input  ready, rdata;
      endclocking
    
      clocking mon_cb @(posedge clk);   // monitor's view: samples everything
        default input #1step;
        input addr, valid, ready, rdata;
      endclocking
    
      modport DRV (clocking drv_cb);
      modport MON (clocking mon_cb);
    endinterface
    
    class driver;
      virtual bus_if vif;
    
      function new(virtual bus_if vif);
        this.vif = vif;
      endfunction
    
      task drive_one(bit [31:0] a);
        @(vif.drv_cb);                          // sync to the clocking event
        vif.drv_cb.addr  <= a;                  // clocked drive: applied with output skew
        vif.drv_cb.valid <= 1'b1;
        do @(vif.drv_cb); while (!vif.drv_cb.ready);   // wait for a sampled ready
        vif.drv_cb.valid <= 1'b0;
      endtask
    endclass
    
    class monitor;
      virtual bus_if.MON vif;   // modport-selected: input-only, driving is impossible
      bit [31:0] seen [$];
    
      function new(virtual bus_if.MON vif);
        this.vif = vif;
      endfunction
    
      task run();
        forever begin
          @(vif.mon_cb);
          if (vif.mon_cb.valid && vif.mon_cb.ready) begin
            seen.push_back(vif.mon_cb.addr);
            $display("monitor saw addr=%0h data=%0h", vif.mon_cb.addr, vif.mon_cb.rdata);
          end
        end
      endtask
    endclass
    
    // A responder that accepts on every other cycle and returns addr + 1.
    module dut (input logic clk, input logic valid, input logic [31:0] addr,
                output logic ready, output logic [31:0] rdata);
      timeunit 1ns; timeprecision 1ps;
      logic toggle = 0;
      always_ff @(posedge clk) toggle <= !toggle;
      assign ready = toggle;
      assign rdata = addr + 1;
    endmodule
    
    module tb_vif_cb;
      timeunit 1ns; timeprecision 1ps;
      logic clk = 0;
      always #5 clk = ~clk;
      bus_if bif (clk);
      dut u (.clk(clk), .valid(bif.valid), .addr(bif.addr), .ready(bif.ready), .rdata(bif.rdata));
      initial begin
        driver  d;
        monitor m;
        bif.valid = 0; bif.addr = 0;
        d = new(bif);
        m = new(bif);
        fork m.run(); join_none                 // the monitor watches in the background
        d.drive_one(32'hA1);
        d.drive_one(32'hB2);
        d.drive_one(32'hC3);
        repeat (2) @(posedge clk);
        if (m.seen.size() == 3 && m.seen[0] == 32'hA1 && m.seen[2] == 32'hC3)
          $display("PASS: the class drove 3 beats through drv_cb and the monitor sampled each once through mon_cb");
        else $fatal(1, "FAIL: monitor saw %p", m.seen);
        $finish;
      end
    endmodule
    Terminal
    monitor saw addr=a1 data=a2
    monitor saw addr=b2 data=b3
    monitor saw addr=c3 data=c4
    PASS: the class drove 3 beats through drv_cb and the monitor sampled each once through mon_cb
    • Sync with @(vif.cb), never with @(posedge vif.clk) in class code.

    • Drive with vif.cb.sig <= value; clocking drives take the output skew.

    • Sample by reading vif.cb.sig: the pre-edge value, identical on every simulator.

    • Never mix raw vif.sig and vif.cb.sig on one signal: the skews disagree.

    • Split roles with one clocking block and modport per component.

    Sampled values lag the wire by design

    Each slot below samples just before its rising edge, the instant a #1step input skew observes. The ready flop always shows what valid was one slot earlier.

    • Slot 0: valid 0, addr 00, ready 0. Idle; the flop holds 0.

    • Slot 1: valid 1, addr a1. First beat offered; ready still 0, not yet captured.

    • Slot 2: valid 1, addr b2. ready 1: the slot-1 beat is captured.

    • Slot 3: valid 0, addr holds b2. ready stays 1 on the held capture.

    • Slot 4: valid 1, addr c3. ready 0: the slot-3 idle is captured.

    • Slot 5: valid 1, addr holds c3. ready 1: the slot-4 beat is captured.

    That one-slot lag is the whole contract. A monitor never sees the live wire, only the settled values the DUT captured.

    Common mistakes

    Common pitfall

    A blocking raw drive (vif.valid = 1) at the clock edge works on one simulator and races on another. Mixing vif.cb.sig and vif.sig on the same signal reintroduces the race the clocking block removed.

    Interview

    “Why does the monitor see a value one cycle behind the wire?” Because input skew samples before the edge, the cb view is the pre-edge settled value. That is the definition of race-free sampling, not a bug.

    Check yourself The driver writes vif.valid = 1 (blocking) right after @(posedge vif.clk), and the DUT misses the first beat on one simulator only. What is wrong?

    The blocking drive races the DUT flops on that edge. Move the drive onto vif.drv_cb.valid so it takes the output skew on every simulator.

    Check yourself Monitor code reads vif.mon_cb.addr and vif.addr in one process and they disagree. Why?

    The two reads use different skews: pre-edge sample versus live wire. Pick the cb view and never mix them on one signal.

    Check yourself Why is the monitor's vif declared virtual bus_if.MON instead of virtual bus_if?

    The modport restricts it to the input-only clocking block. Driving through that vif cannot compile, so the monitor role is enforced by the type.

    Remember

    • Sync with @(vif.cb), drive with vif.cb.sig <= v, sample by reading vif.cb.sig.

    • Sampled values are the pre-edge values: one slot behind the wire, by design.

    • Never mix vif.sig and vif.cb.sig on one signal.

    Rule of thumb: class code touches pins only through a clocking block.

    How this was checked

    Run with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown, with no warnings. These class benches were run on Verilator only.

    Verilator 5.048 ignores the iff guard in @(vif.cb iff ...) when the clocking block is reached through a class's virtual interface (probed on 2026-10-18: the wait returned with the condition false), so the class code waits with an explicit loop. At module level the iff form works.

    Related topics

    • Next: vif Configuration Patterns

    Previous

    51 / 259

    Next: vif Configuration Patterns

    Up next

    Up nextvif Configuration Patterns

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Why Virtual Interfaces Exist
    • Multi-Instance & Parameterized vifs
    clkvalidreadyaddr0a1b2c3
    A settled drive, its DUT capture, and the monitor's one-slot-late view