UVM Testbench for Dual Port RAM

Complete Verification Environment with Coverage & Assertions

Praveen Kumar Vagala | 18 min read

1000

Introduction

UVM (Universal Verification Methodology) is the industry-standard verification methodology for complex digital designs. This blog demonstrates building a complete UVM testbench for the Dual Port RAM designed in Chapter 02, including sequences, coverage, and assertions.

Table of Contents

  1. UVM Testbench Architecture
  2. Interface & Transaction
  3. Driver & Monitor
  4. Agent & Sequencer
  5. Scoreboard & Coverage
  6. Sequences & Tests
  7. SVA Assertions

DUT: Dual Port RAM (from Chapter 02)

Features: 32-bit data, 10-bit address, 1024 depth, True Dual-Port

module dual_port_ram #(
    parameter DATA_WIDTH = 32,
    parameter ADDR_WIDTH = 10,
    parameter DEPTH      = 1024
)(
    // Port A
    input  wire                    clk_a,
    input  wire                    en_a,
    input  wire                    wr_en_a,
    input  wire [ADDR_WIDTH-1:0]   addr_a,
    input  wire [DATA_WIDTH-1:0]   wr_data_a,
    output reg  [DATA_WIDTH-1:0]   rd_data_a,
    
    // Port B
    input  wire                    clk_b,
    input  wire                    en_b,
    input  wire                    wr_en_b,
    input  wire [ADDR_WIDTH-1:0]   addr_b,
    input  wire [DATA_WIDTH-1:0]   wr_data_b,
    output reg  [DATA_WIDTH-1:0]   rd_data_b
);
    // Memory array
    reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];
    
    // Port A
    always @(posedge clk_a) begin
        if (en_a) begin
            if (wr_en_a) mem[addr_a] <= wr_data_a;
            rd_data_a <= mem[addr_a];
        end
    end
    
    // Port B
    always @(posedge clk_b) begin
        if (en_b) begin
            if (wr_en_b) mem[addr_b] <= wr_data_b;
            rd_data_b <= mem[addr_b];
        end
    end
endmodule

1. UVM Testbench Architecture

Key Learning: Component hierarchy, factory pattern, phasing

Block Diagram

+------------------------------------------------------------------+ | UVM Test | +------------------------------------------------------------------+ | UVM Env | | +---------------------------+ +---------------------------+ | | | Agent Port A | | Agent Port B | | | | +--------+ +--------+ | | +--------+ +--------+ | | | | |Sequencer| | Driver | | | |Sequencer| | Driver | | | | | +--------+ +--------+ | | +--------+ +--------+ | | | | +--------+ | | +--------+ | | | | | Monitor| | | | Monitor| | | | | +--------+ | | +--------+ | | | +---------------------------+ +---------------------------+ | | | | +---------------------------+ +---------------------------+ | | | Scoreboard | | Coverage Collector | | | +---------------------------+ +---------------------------+ | +------------------------------------------------------------------+ | +---------v---------+ | DUT (Dual Port | | RAM) | +-------------------+

2. Interface & Transaction

Key Learning: Virtual interface, transaction class, field macros

RAM Interface

interface ram_if #(
    parameter DATA_WIDTH = 32,
    parameter ADDR_WIDTH = 10
)(input logic clk);
    
    logic                    en;
    logic                    wr_en;
    logic [ADDR_WIDTH-1:0]   addr;
    logic [DATA_WIDTH-1:0]   wr_data;
    logic [DATA_WIDTH-1:0]   rd_data;
    
    // Clocking blocks for driver and monitor
    clocking driver_cb @(posedge clk);
        default input #1 output #1;
        output en, wr_en, addr, wr_data;
        input  rd_data;
    endclocking
    
    clocking monitor_cb @(posedge clk);
        default input #1 output #1;
        input en, wr_en, addr, wr_data, rd_data;
    endclocking
    
    // Modport definitions
    modport DRIVER  (clocking driver_cb);
    modport MONITOR (clocking monitor_cb);
    modport DUT     (input en, wr_en, addr, wr_data, output rd_data);
    
endinterface

Transaction Class

class ram_transaction extends uvm_sequence_item;
    
    // Transaction fields
    rand bit                    en;
    rand bit                    wr_en;
    rand bit [9:0]              addr;
    rand bit [31:0]             wr_data;
         bit [31:0]             rd_data;
    
    // Port identifier
    bit port_id;  // 0 = Port A, 1 = Port B
    
    // UVM field macros for automation
    `uvm_object_utils_begin(ram_transaction)
        `uvm_field_int(en,      UVM_ALL_ON)
        `uvm_field_int(wr_en,   UVM_ALL_ON)
        `uvm_field_int(addr,    UVM_ALL_ON)
        `uvm_field_int(wr_data, UVM_ALL_ON)
        `uvm_field_int(rd_data, UVM_ALL_ON)
        `uvm_field_int(port_id, UVM_ALL_ON)
    `uvm_object_utils_end
    
    // Constraints
    constraint valid_txn {
        en == 1;  // Always enabled for active transactions
    }
    
    constraint addr_range {
        addr inside {[0:1023]};
    }
    
    // Constructor
    function new(string name = "ram_transaction");
        super.new(name);
    endfunction
    
    // Convert to string for debug
    function string convert2string();
        return $sformatf("Port=%0d EN=%0b WR=%0b ADDR=0x%0h WDATA=0x%0h RDATA=0x%0h",
                         port_id, en, wr_en, addr, wr_data, rd_data);
    endfunction
    
endclass

3. Driver & Monitor

Key Learning: BFM implementation, transaction to signal conversion

RAM Driver

class ram_driver extends uvm_driver #(ram_transaction);
    `uvm_component_utils(ram_driver)
    
    virtual ram_if vif;
    
    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction
    
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        if (!uvm_config_db#(virtual ram_if)::get(this, "", "vif", vif))
            `uvm_fatal("NOVIF", "Virtual interface not found")
    endfunction
    
    task run_phase(uvm_phase phase);
        ram_transaction txn;
        
        // Initialize signals
        vif.driver_cb.en      <= 0;
        vif.driver_cb.wr_en   <= 0;
        vif.driver_cb.addr    <= 0;
        vif.driver_cb.wr_data <= 0;
        
        forever begin
            seq_item_port.get_next_item(txn);
            drive_transaction(txn);
            seq_item_port.item_done();
        end
    endtask
    
    task drive_transaction(ram_transaction txn);
        @(vif.driver_cb);
        vif.driver_cb.en      <= txn.en;
        vif.driver_cb.wr_en   <= txn.wr_en;
        vif.driver_cb.addr    <= txn.addr;
        vif.driver_cb.wr_data <= txn.wr_data;
        
        @(vif.driver_cb);
        vif.driver_cb.en <= 0;
        
        `uvm_info("DRV", $sformatf("Drove: %s", txn.convert2string()), UVM_MEDIUM)
    endtask
    
endclass

RAM Monitor

class ram_monitor extends uvm_monitor;
    `uvm_component_utils(ram_monitor)
    
    virtual ram_if vif;
    uvm_analysis_port #(ram_transaction) ap;
    bit port_id;
    
    function new(string name, uvm_component parent);
        super.new(name, parent);
        ap = new("ap", this);
    endfunction
    
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        if (!uvm_config_db#(virtual ram_if)::get(this, "", "vif", vif))
            `uvm_fatal("NOVIF", "Virtual interface not found")
    endfunction
    
    task run_phase(uvm_phase phase);
        ram_transaction txn;
        
        forever begin
            @(vif.monitor_cb);
            
            if (vif.monitor_cb.en) begin
                txn = ram_transaction::type_id::create("txn");
                txn.port_id = port_id;
                txn.en      = vif.monitor_cb.en;
                txn.wr_en   = vif.monitor_cb.wr_en;
                txn.addr    = vif.monitor_cb.addr;
                txn.wr_data = vif.monitor_cb.wr_data;
                
                // Wait one cycle for read data
                @(vif.monitor_cb);
                txn.rd_data = vif.monitor_cb.rd_data;
                
                ap.write(txn);
                `uvm_info("MON", $sformatf("Captured: %s", txn.convert2string()), UVM_HIGH)
            end
        end
    endtask
    
endclass

4. Agent & Sequencer

Key Learning: Agent modes (active/passive), component hierarchy

class ram_agent extends uvm_agent;
    `uvm_component_utils(ram_agent)
    
    ram_driver    drv;
    ram_monitor   mon;
    uvm_sequencer #(ram_transaction) sqr;
    
    bit port_id;
    
    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction
    
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        
        mon = ram_monitor::type_id::create("mon", this);
        mon.port_id = port_id;
        
        if (get_is_active() == UVM_ACTIVE) begin
            drv = ram_driver::type_id::create("drv", this);
            sqr = uvm_sequencer#(ram_transaction)::type_id::create("sqr", this);
        end
    endfunction
    
    function void connect_phase(uvm_phase phase);
        super.connect_phase(phase);
        if (get_is_active() == UVM_ACTIVE) begin
            drv.seq_item_port.connect(sqr.seq_item_export);
        end
    endfunction
    
endclass

5. Scoreboard & Coverage

Key Learning: Reference model, functional coverage, analysis ports

Scoreboard with Reference Model

class ram_scoreboard extends uvm_scoreboard;
    `uvm_component_utils(ram_scoreboard)
    
    uvm_analysis_imp #(ram_transaction, ram_scoreboard) ap;
    
    // Reference memory model
    bit [31:0] ref_mem [0:1023];
    
    int pass_count, fail_count;
    
    function new(string name, uvm_component parent);
        super.new(name, parent);
        ap = new("ap", this);
    endfunction
    
    function void write(ram_transaction txn);
        if (txn.wr_en) begin
            // Write operation - update reference model
            ref_mem[txn.addr] = txn.wr_data;
            `uvm_info("SCB", $sformatf("Write: MEM[0x%0h] = 0x%0h", 
                      txn.addr, txn.wr_data), UVM_MEDIUM)
        end else begin
            // Read operation - compare with reference
            if (txn.rd_data == ref_mem[txn.addr]) begin
                pass_count++;
                `uvm_info("SCB", $sformatf("PASS: Addr=0x%0h Expected=0x%0h Got=0x%0h",
                          txn.addr, ref_mem[txn.addr], txn.rd_data), UVM_MEDIUM)
            end else begin
                fail_count++;
                `uvm_error("SCB", $sformatf("FAIL: Addr=0x%0h Expected=0x%0h Got=0x%0h",
                           txn.addr, ref_mem[txn.addr], txn.rd_data))
            end
        end
    endfunction
    
    function void report_phase(uvm_phase phase);
        `uvm_info("SCB", $sformatf("Scoreboard Summary: PASS=%0d FAIL=%0d", 
                  pass_count, fail_count), UVM_LOW)
    endfunction
    
endclass

Functional Coverage

class ram_coverage extends uvm_subscriber #(ram_transaction);
    `uvm_component_utils(ram_coverage)
    
    ram_transaction txn;
    
    // Covergroup
    covergroup ram_cg;
        // Address coverage
        addr_cp: coverpoint txn.addr {
            bins low_addr   = {[0:255]};
            bins mid_addr   = {[256:767]};
            bins high_addr  = {[768:1023]};
            bins boundaries = {0, 511, 512, 1023};
        }
        
        // Operation type
        wr_en_cp: coverpoint txn.wr_en {
            bins read  = {0};
            bins write = {1};
        }
        
        // Port coverage
        port_cp: coverpoint txn.port_id {
            bins port_a = {0};
            bins port_b = {1};
        }
        
        // Data patterns
        data_cp: coverpoint txn.wr_data {
            bins zeros    = {32'h0000_0000};
            bins ones     = {32'hFFFF_FFFF};
            bins walking1 = {32'h0000_0001, 32'h0000_0002, 32'h0000_0004};
            bins random   = default;
        }
        
        // Cross coverage
        addr_x_op: cross addr_cp, wr_en_cp;
        port_x_op: cross port_cp, wr_en_cp;
        
    endgroup
    
    function new(string name, uvm_component parent);
        super.new(name, parent);
        ram_cg = new();
    endfunction
    
    function void write(ram_transaction t);
        txn = t;
        ram_cg.sample();
    endfunction
    
    function void report_phase(uvm_phase phase);
        `uvm_info("COV", $sformatf("Coverage = %.2f%%", ram_cg.get_coverage()), UVM_LOW)
    endfunction
    
endclass

6. Sequences & Tests

Key Learning: Virtual sequences, test scenarios

Base Sequence

class ram_base_sequence extends uvm_sequence #(ram_transaction);
    `uvm_object_utils(ram_base_sequence)
    
    function new(string name = "ram_base_sequence");
        super.new(name);
    endfunction
    
    // Write task
    task write_mem(bit [9:0] addr, bit [31:0] data);
        ram_transaction txn = ram_transaction::type_id::create("txn");
        start_item(txn);
        txn.en      = 1;
        txn.wr_en   = 1;
        txn.addr    = addr;
        txn.wr_data = data;
        finish_item(txn);
    endtask
    
    // Read task
    task read_mem(bit [9:0] addr);
        ram_transaction txn = ram_transaction::type_id::create("txn");
        start_item(txn);
        txn.en      = 1;
        txn.wr_en   = 0;
        txn.addr    = addr;
        finish_item(txn);
    endtask
    
endclass

Write-Read Sequence

class write_read_sequence extends ram_base_sequence;
    `uvm_object_utils(write_read_sequence)
    
    function new(string name = "write_read_sequence");
        super.new(name);
    endfunction
    
    task body();
        `uvm_info("SEQ", "Starting Write-Read Sequence", UVM_LOW)
        
        // Write to addresses 0-9
        for (int i = 0; i < 10; i++) begin
            write_mem(i, i * 100);
        end
        
        // Read back and verify
        for (int i = 0; i < 10; i++) begin
            read_mem(i);
        end
        
        `uvm_info("SEQ", "Write-Read Sequence Complete", UVM_LOW)
    endtask
    
endclass

Random Sequence

class random_sequence extends uvm_sequence #(ram_transaction);
    `uvm_object_utils(random_sequence)
    
    rand int num_txns;
    
    constraint txn_count { num_txns inside {[50:200]}; }
    
    function new(string name = "random_sequence");
        super.new(name);
    endfunction
    
    task body();
        ram_transaction txn;
        
        `uvm_info("SEQ", $sformatf("Running %0d random transactions", num_txns), UVM_LOW)
        
        repeat (num_txns) begin
            txn = ram_transaction::type_id::create("txn");
            start_item(txn);
            if (!txn.randomize())
                `uvm_error("SEQ", "Randomization failed")
            finish_item(txn);
        end
    endtask
    
endclass

UVM Test

class ram_base_test extends uvm_test;
    `uvm_component_utils(ram_base_test)
    
    ram_env env;
    
    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction
    
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        env = ram_env::type_id::create("env", this);
    endfunction
    
    function void end_of_elaboration_phase(uvm_phase phase);
        uvm_top.print_topology();
    endfunction
    
endclass

class write_read_test extends ram_base_test;
    `uvm_component_utils(write_read_test)
    
    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction
    
    task run_phase(uvm_phase phase);
        write_read_sequence seq;
        
        phase.raise_objection(this);
        
        seq = write_read_sequence::type_id::create("seq");
        seq.start(env.agent_a.sqr);
        
        #100;
        phase.drop_objection(this);
    endtask
    
endclass

7. SVA Assertions

Key Learning: Property checking, temporal assertions

module ram_assertions (
    input logic        clk,
    input logic        en,
    input logic        wr_en,
    input logic [9:0]  addr,
    input logic [31:0] wr_data,
    input logic [31:0] rd_data
);

    // Property: Enable must be active for any operation
    property p_enable_required;
        @(posedge clk) wr_en |-> en;
    endproperty
    assert property (p_enable_required)
        else $error("Write enable without enable!");

    // Property: Address must be within valid range
    property p_valid_address;
        @(posedge clk) en |-> (addr < 1024);
    endproperty
    assert property (p_valid_address)
        else $error("Invalid address: %0d", addr);

    // Property: Read data stable after read
    property p_read_stable;
        @(posedge clk) (en && !wr_en) |=> $stable(rd_data);
    endproperty
    assert property (p_read_stable)
        else $warning("Read data changed unexpectedly");

    // Coverage: Track all address ranges accessed
    cover property (@(posedge clk) en && addr < 256);
    cover property (@(posedge clk) en && addr >= 256 && addr < 512);
    cover property (@(posedge clk) en && addr >= 512 && addr < 768);
    cover property (@(posedge clk) en && addr >= 768);

endmodule

✅ UVM Best Practices

  1. Use factory for all component/object creation
  2. Always use virtual interfaces
  3. Separate driver and monitor responsibilities
  4. Use analysis ports for loose coupling
  5. Implement functional coverage for closure
  6. Use sequences for test stimulus abstraction

UVM Interview Questions

  1. What is the difference between uvm_object and uvm_component? - Components have hierarchy and phases; objects are transient data
  2. Explain TLM ports in UVM. - Transaction-level connections between components (put, get, analysis)
  3. Why use virtual sequences? - To coordinate multiple sequencers from a single sequence
  4. What is the UVM factory? - Central registry for type creation, enabling overrides

Next Steps

Continue your VLSI learning journey with the complete blog series:

← Previous: Communication Next: RAL →
#UVM #SystemVerilog #Verification #Testbench #Coverage #Assertions #VLSI