Systemverilog or UVM?

Hi
I’m actually new to constrained random testing, and I wonder whether it is better to stick with SystemVerilog, or make the effort and learn UVM. In Chris Spear’s book, I read the following passage:
“Are you in charge of verifying a 100 million-gate design with many communication protocols, complex error handling, and a library of IP? If so, UVM or VMM are the right tools for the job. However, if you are working on smaller modules with a single protocol, you may not need such a robust methodology.”
Well as we work only with FPGAs, we certainly don’t deal with 100 million-gate designs. Does this mean that UVM is bad for us? What’s the problem with using UVM to verify, say, a 1 million gate design? Is it just the learning overhead? Or there’s more? I mean, now that we’re making the effort to migrate to CRT from directed testing, why not go all the way and use UVM for our verifications?
I’m asking this because I couldn’t find some material which explains this clearly and concisely. And I can’t take all the time and learn UVM just to judge whether it’s good for us or not. I would really appreciate some guidance.
Thanks

In reply to Farhad:
I struggled with that question when testing small models for my assertions.
I came up with a solution that integrates the concepts of UVM, but without the interface, vif, and UVM classes, the environment, and tests. I use UVM for displays.
**** NOTE: ****
**** I show 2 variations for testing this counter, one simple one, the other, a bit more details and more a la UVM with tasks for the drivers once I detect the “kind” of transaction needed. Anyway, that should give a flavor of my approach.
Using classes will allow you to more easily transition to a full UVM.
Below are the models to verify a counter with strange requirements.
The counter model is at the end of the code. On the testbench I use the following:

  1. counter_pkg: for the enumeration type
  2. class data_item: Items to be randomized
  3. class transaction extends data_item; Other items to be randomized.
    Constraints are defined here.
    The extension is used as a demo, but is nice to have if needed
  4. class simple_random_sequence; task to randomize the items, It executes nothing, just randomizes per the constraints.
  5. module counter_max_tb_using_class; the testbench

The testbench includes:

  1. variables, DUT instantiation
  2. simple_random_sequence class instantiation
  3. test, includes reset, loop to randomize the variable, NBA assign the randomized data

// test using simple_random_sequence
always @ (posedge clk) begin : random1
rst_n <= 1'b1;
repeat(3) @ (negedge clk) rst_n <= 1'b0;
repeat(3) @ (negedge clk)rst_n <= 1'b1;
// dada and load
for (int i=0; i<40; i++) begin : for1
simple_rand_sequence_ld_mostly_hi.run();
data_in <= simple_rand_sequence_ld_mostly_hi.tx.data;
ld <= simple_rand_sequence_ld_mostly_hi.tx.ld;
@ (posedge clk);
end : for1
$display("End of test");
$finish;
end : random1

// **************** Counter pACKAGE
package counter_pkg;
  timeunit 1ns; timeprecision 100ps;
  `define TOP counter_tb
  typedef enum {CT_LOAD, CT_RESET, CT_WAIT, CT_DONE} ct_scen_e;
endpackage : counter_pkg

//************** Testbench *****************************
import uvm_pkg::*;
`include "uvm_macros.svh"
 
class data_item;
    rand logic[3:0] data;
endclass : data_item

class transaction extends data_item;
    rand logic ld;
    // 1'b0 80% of time, 1'b1 20% of the time
    constraint values { ld dist {1'b0 := 80, 1'b1 := 20}; } 
endclass : transaction

class simple_random_sequence;
    transaction tx=new();
    // randomize 
    virtual task run();
    	if (!randomize(tx))  `uvm_error("MYERR", "This is a randomize error"); 
    	// if(!randomize(tx))  $error("randomization failure"); 
    endtask : run
endclass : simple_random_sequence

module counter_max_tb_using_class;
    parameter MAX_COUNT=9, MIN_COUNT=2; 
    logic[3:0] data_in=0, data=0;
    logic ld=0;
    logic[3:0] counter, counter2;
    logic clk=1'b1, rst_n=1'b1;
    simple_random_sequence simple_rand_sequence_ld_mostly_hi=new();
    counter_max counter_max1(.*); 
    // counter_max counter_max2(.counter(counter2), .*); 
    initial forever #10 clk=!clk; 
    
    // test using simple_random_sequence
    always @ (posedge clk) begin : random1
        rst_n <= 1'b1; 
        repeat(3) @ (negedge clk) rst_n <= 1'b0; 
        repeat(3) @ (negedge clk)rst_n <= 1'b1;
        // dada and load 
        for (int i=0; i<40; i++) begin : for1
            simple_rand_sequence_ld_mostly_hi.run(); 
            data_in <= simple_rand_sequence_ld_mostly_hi.tx.data;
            ld <= simple_rand_sequence_ld_mostly_hi.tx.ld; 
            @ (posedge clk); 
        end : for1
        $display("End of test"); 
        $finish;
    end : random1		
endmodule : counter_max_tb_using_class

You can go a bit more fancy on the drivers by doing something like more a la UVM


import counter_pkg::*;
class data_item;
    rand logic[3:0] data;
endclass : data_item

class transaction extends data_item;
    rand logic ld=0;
    int reset_cycles, idle_cycles;
    rand ct_scen_e kind;
    // constraint values { ld dist {1'b0 := 80, 1'b1 := 20}; } // 0 80% of time
    constraint data_range { data > 2 && data <=9; } 
    constraint cst_xact_kind {
        kind dist {
            CT_LOAD := 5, 
            CT_RESET := 2, 
            CT_WAIT := 97,
            CT_DONE := 1
        };
    } // cst_xact_kind
endclass : transaction

class simple_random_sequence;
    transaction tx=new();
    // randomize 
    virtual task run();
        ap_rand_seq: assert(randomize(tx)); 
    endtask : run
endclass : simple_random_sequence

 

module counter_max_tb_simple;
    parameter MAX_COUNT_TB=6, MIN_COUNT_TB=3; 
    // defparam counter_max1.MIN_COUNT=3; 
    // illegal because have parameter port list
    logic[3:0] data_in=0, data=0;
    logic ld=0;
    logic[3:0] counter, counter2;
    logic clk=1'b1, rst_n=1'b1;
    simple_random_sequence seq=new();
    counter_max #(.MAX_COUNT(MAX_COUNT_TB), 
        .MIN_COUNT(MIN_COUNT_TB)) counter_max1(.*); 
    // counter_max counter_max2(.counter(counter2), .*); 
    initial forever #10 clk=!clk; 
    
    // test using simple_random_sequence
    always @ (negedge clk) begin : random1
        rst_n <= 1'b1; ld <=1'b0; 
        repeat(3) @ (negedge clk) rst_n <= 1'b0; 
        repeat(3) @ (negedge clk)rst_n <= 1'b1;
        // dada and load 
        for (int i=0; i<100; i++) begin : for1
        	@ (negedge clk);
            seq.run(); 
            case (seq.tx.kind)
                CT_LOAD : begin 
                    // $display("PUSH"); 
                    //`ovm_info({get_type_name(),":drive_dut"},{"rsp_txn - pre",rsp_txn.convert2string()},OVM_LOW)
                    load_task(seq.tx.data); 
                end
                CT_RESET : begin 
                    // $display("POP"); 
                    //`ovm_info({get_type_name(),":drive_dut"},{"rsp_txn - pre",rsp_txn.convert2string()},OVM_LOW)
                    reset_task(); 
                end 
                CT_WAIT : begin 
                    //$display("PUSH_POP"); 
                    //`ovm_info({get_type_name(),":drive_dut"},{"rsp_txn -pre",rsp_txn.convert2string()},OVM_LOW)
                    idle_task(seq.tx.data); 
                end
                CT_DONE : begin 
                    // $display("IDLE");
                    //`ovm_info({get_type_name(),":drive_dut"},{"rsp_txn -pre",rsp_txn.convert2string()},OVM_LOW)
                    done_task(); 
                end // seq.tx.idle_cycles);
            endcase
            // @ (negedge clk); 
        end : for1
        $display("End of test"); 
        $display("counter_max1.cover_count=%d, counter_max1.fail_count=%d", 
            counter_max1.cover_count, counter_max1.fail_count);
        $finish;
    end : random1
    
    // load counter
    task load_task(int data);
        data_in <= data;
        ld <= 1'b1; 
        @ (negedge clk)ld <= 1'b0; 
        
    endtask : load_task
    
    // reset
    task reset_task();
        ld <= 1'b0; 
        repeat(1) rst_n <= 1'b0; 
        @ (negedge clk)rst_n <= 1'b1;
    endtask : reset_task
    
    // IDLE
    task idle_task(int data);// for now, make idle for 4 cycles 
        automatic logic[3:0] v=data; 
        for (int i=0; i<4; i++) begin
            ld <= 1'b0; 
            assert(randomize(v)); 
            data_in <= v;
            @ (negedge clk); 
        end
    endtask : idle_task
    
    // DONE, for now, a do nothing 
    task done_task();
        @ (negedge clk); 
        
    endtask : done_task
endmodule : counter_max_tb_simple

Ben Cohen
http://www.systemverilog.us/ ben@systemverilog.us
** SVA Handbook 4th Edition, 2016 ISBN 978-1518681448

  1. SVA Package: Dynamic and range delays and repeats SVA: Package for dynamic and range delays and repeats | Verification Academy
  2. Free books: Component Design by Example FREE BOOK: Component Design by Example … A Step-by-Step Process Using VHDL with UART as Vehicle | Verification Academy
    Real Chip Design and Verification Using Verilog and VHDL($3) Amazon.com
  3. Papers:

Udemy courses by Srinivasan Venkataramanan (http://cvcblr.com/home.html)
https://www.udemy.com/course/sva-basic/
https://www.udemy.com/course/sv-pre-uvm/

In reply to Farhad:

It really should not matter what the size of your design is. What matters is the size of your testbench, how many people will be involved in its development, and how long will it need to be maintained either as a single project, or in follow-on projects.

The UVM could actually shorten learning overhead in terms of all the time it would take someone else to learn your custom SystemVerilog testbench where you’ve basically re-invented all the things in the UVM anyways.

There is a lot of stuff in the UVM, but there is no need to use most of it when getting a simple testbench running. Here are the bare minimum UVM concepts I recommend learning first. You can get a fair amount of constrained random testing done with the UVM just by understand just these three basic concepts.

  • Components: Understand how to construct testbench hierarchy and the key components of any testench architecture: the top-level test, drivers and monitors. The basic phases needed for a class based testbench: build_phase, run_phase, and report_report, and how to determine when it’s time to end your test.
  • Reporting: gives you a mechanism for filtering messages as well as promoting and demoting severity levels. Another key benefit is giving you a quick summary count of all the messages
  • Configuration: A way of sharing data between components, especially the top-level testbanch component and the lower levels. Also gives you an easy way of setting test “knobs” from the command line without having to recompile your test

The next three concepts I highly recommend learning when getting started

  • Transactions: standardizing the way class objects get cloned, copied, compared, and printed.
  • Factory: Makes tests more reusable by overriding class types
  • Sequences: A way of organizing stimulus and responses into sets of procedures.

Of course there is a larger list of thing you will probably never use, but it’s there when you need it.

In reply to ben@SystemVerilog.us:

Thanks a lot for the comprehensive answer, Ben.

In reply to dave_59:

Thank you very much, Dave.