Module 9: SystemVerilog for Design & Verification ยท 9.2 SystemVerilog Verification Basics

Toward a UVM-Ready Testbench

The last three articles built transactions, randomized them and constrained them. This one puts them into a structure.

Section 6.1 said a testbench has four parts: the DUT instantiation, a stimulus generator, a checker and a report. Everything in this section has been building toward reorganising those same parts into class-based components with names you will recognise the moment you open a real UVM testbench: the driver, the monitor and the scoreboard.

Old names, new roles

Section 6.1’s structure This section’s class-based equivalent
Stimulus generator A class generating randomized, constrained transaction objects
DUT instantiation Unchanged: still a plain Verilog module instance
Driving the DUT (Section 6.1.3) A driver class: takes a transaction object, drives its fields onto the DUT’s pins
Observing outputs (Section 6.1.4) A monitor class: watches the DUT’s pins, reconstructs transaction objects from what it sees
Checker / self-checking loop A scoreboard class: compares the monitor’s observed transactions against a reference model

Nothing here is a new concept. It is the same four responsibilities from Module 6, reorganised into named, reusable classes instead of loose procedural code in initial blocks.

A minimal driver

class alu_driver;
    virtual alu_if vif;   // a virtual interface handle: connects this class to real DUT signals

    task automatic drive(alu_transaction txn);
        vif.a  <= txn.operand_a;
        vif.b  <= txn.operand_b;
        vif.op <= txn.opcode;
        @(posedge vif.clk);
    endtask
endclass

The virtual alu_if vif line is the one new piece of syntax. A virtual interface is how a class, which exists in the software-like verification world, reaches out and touches real signals on an interface instance (Section 9.1) connected to the DUT. It is the bridge between the class world and the hardware pins, and every driver, monitor or other class that touches real signals holds one.

A minimal monitor

class alu_monitor;
    virtual alu_if vif;

    task automatic collect(output alu_transaction txn);
        txn = new();
        @(posedge vif.clk);
        txn.operand_a = vif.a;
        txn.operand_b = vif.b;
        txn.opcode    = vif.op;
        txn.expected_result = vif.result;   // captures the DUT's ACTUAL output, for the scoreboard to judge
    endtask
endclass

The monitor is purely passive. It watches the same pins the driver drives and reconstructs a transaction describing what happened, with no ability to influence the DUT, echoing the read-only monitor_port modport from the Section 9.1 exercise.

A minimal scoreboard

class alu_scoreboard;
    int pass_count = 0, fail_count = 0;

    function void check(alu_transaction txn);
        bit [7:0] golden_result = compute_expected(txn.operand_a, txn.operand_b, txn.opcode);
        if (txn.expected_result == golden_result) begin
            pass_count++;
        end else begin
            fail_count++;
            $display("FAIL: op=%b a=%h b=%h got=%h expected=%h",
                      txn.opcode, txn.operand_a, txn.operand_b,
                      txn.expected_result, golden_result);
        end
    endfunction

    function bit [7:0] compute_expected(bit [7:0] a, b, bit [2:0] op);
        case (op)
            3'b000: compute_expected = a + b;
            3'b001: compute_expected = a - b;
            // ... remaining opcodes, per Module 2's ALU
            default: compute_expected = 8'h00;
        endcase
    endfunction
endclass

This is the golden reference model from Section 6.4, relocated into a class method rather than an initial-block task, with the same gains in reusability and self-containment as every other piece of this reorganisation.

Assembling the pieces

module tb_alu_top;
    alu_if vif();
    alu dut (.a(vif.a), .b(vif.b), .op(vif.op), .result(vif.result), .clk(vif.clk));

    initial begin
        alu_driver     drv = new();
        alu_monitor    mon = new();
        alu_scoreboard sb  = new();
        alu_transaction stim_txn, observed_txn;

        drv.vif = vif;
        mon.vif = vif;

        repeat (1000) begin
            stim_txn = new();
            stim_txn.randomize();      // this section's randomization, doing the stimulus work
            drv.drive(stim_txn);        // Section 6.1's driving, as a class method
            mon.collect(observed_txn);  // Section 6.1's observing, as a class method
            sb.check(observed_txn);     // Section 6.4's self-checking loop, as a class method
        end

        $display("Results: %0d passed, %0d failed", sb.pass_count, sb.fail_count);
        $finish;
    end
endmodule

One thousand randomized, constrained transactions, driven, observed and scored: a substantial verification run assembled from pieces that are, individually, nothing more than the Module 6 structure and this section’s class tools, reorganised into named, reusable components.

Transaction, driver, DUT, monitor, scoreboard, connected through virtual interfaces
Figure 1: The driver, monitor and scoreboard architecture

This driver/monitor/scoreboard structure, connected through virtual interfaces, is precisely what UVM formalises into a standard class hierarchy: uvm_driver, uvm_monitor, uvm_scoreboard and dozens of supporting classes handling the communication and configuration between them. Nothing here is UVM itself, but every one of UVM’s structural ideas is visible here, stripped to essentials, which is why understanding this minimal version first makes UVM look like a natural extension rather than a framework learned from scratch.

Practice Exercise

Explain, in terms of the responsibilities in this article, why a driver should never contain the pass/fail check, even though it would be possible to add an if inside drive() comparing something to an expected value.

Solution

The driver’s single responsibility is translating a transaction into signal activity on the DUT’s pins. Mixing checking into it conflates two separate concerns: driving stimulus and judging the response. The separation is practical, not aesthetic. A driver needs to be reusable across many tests that may want different checking strategies: one test uses the golden-reference scoreboard shown here, another checks a different property such as timing or protocol compliance with the same driver and the same stimulus. With checking baked into the driver, every new checking strategy would mean modifying or duplicating it, rather than plugging in a different scoreboard against the same unchanged driver and monitor. That coupling is what this component structure, and UVM after it, is designed to avoid.

Every class-based tool in this section assembles into the same four responsibilities Module 6 started with: generate stimulus, drive it, observe the response, check it. Reorganising them into driver, monitor and scoreboard classes connected through virtual interfaces is not a new philosophy but the same one given a structure that scales, and that is the direct ancestor of UVM’s class hierarchy. That closes Section 9.2. In the next article, Section 9.3 adds a capability neither a scoreboard nor a directed test expresses easily: checking a multi-cycle relationship such as “if this happens, that must follow within N cycles”, directly and continuously, with assertions.

โ† Constraints and Constraint Solving Immediate Assertions โ†’