A testcase is a pattern to target checks and verify specific features of a design. Usually we come up with a verification plan that lists the number of items and features required to be verified, and then develop tests to cover each of them.

So we'll end up writing quite a lot of different tests to reach and exercise every corner of the design. Instead of writing the same code for different tests, we put the entire testbench into a container called an Environment, and use the same environment with a different configuration for each test. Each test can override, tweak knobs, enable/disable agents, change variable values in the configuration table and change default sequences for each sequencer in the verification environment.


Now, let's see an example of how a test is written.

   class base_test extends uvm_test;
      `uvm_component_utils (base_test)
      my_env   m_top_env;              // Testbench environment
      my_cfg   m_cfg0;                 // Configuration object
      function new (string name, uvm_component parent = null);
         super.new (name, parent);
      virtual function void build_phase (uvm_phase phase);
         super.build_phase (phase);
         m_top_env  = my_env::type_id::create ("m_top_env", this);
         m_cfg0     = my_cfg::type_id::create ("m_cfg0", this);
         // Configure the object
         set_cfg_params ();
         // Make the cfg object available to all components in agent
         uvm_config_db #(my_cfg) :: set (this, "m_top_env.my_agent", "m_cfg0", m_cfg0);
      virtual function void set_cfg_params ();
         // Get DUT interface from top module into the cfg object
         if (! uvm_config_db #(virtual dut_if) :: get (this, "", "dut_if", m_cfg0.vif)) begin
            `uvm_error (get_type_name (), "DUT Interface not found !")
         m_cfg0.m_verbosity    = UVM_HIGH;
         m_cfg0.active         = UVM_ACTIVE;
      virtual function void end_of_elaboration_phase (uvm_phase phase);
         // By now, the environment is all set up, just print the topology for debug
         uvm_top.print_topology ();
      function void start_of_simulation_phase (uvm_phase phase);
         super.start_of_simulation_phase (phase);
         // Assign a default sequence to be executed by the sequencer or look at the run_phase ...
                                          "default_sequence", base_sequence::type_id::get());
      // or.. start a sequence for this particular test
      virtual task run_phase (uvm_phase phase);
        my_seq m_seq = my_seq::type_id::create ("m_seq");
        phase.raise_objection (this);
        m_seq.start (m_env.seqr);
        phase.drop_objection (this);

Note the following from the example above:

  • base_test is extended from uvm_test and registered with factory using `uvm_component_utils
  • Testbench environment m_top_env and a configuration object m_cfg0 which holds all the tweak parameters are declared
  • During build_phase(), both the objects are created, config object is initialized with custom values and set as a variable in the database table of UVM using uvm_config_db::set ()
  • The config object is made available only to the agent my_agent inside m_top_env
  • Print the topology for debug purposes during end_of_elaboration_phase() - this phase is executed just before simluation and hence the entire hierarchy of class objects in the testbench will be built and visible by then
  • The sequencer inside my_agent is assigned a default sequence to execute upon start of simulation

Other Details

This is the base class for all user-defined tests and provides the flexibility to select which test gets executed using UVM_TESTNAME command-line argument. For any test to be executed during simulation, the run_test() task should be called within an initial block in the top module. This task also accepts a testname which can be any of the user-defined test classes derived from uvm_test. If the argument to run_test() is left blank, then it is necessary to specify the testname in command-line as mentioned before.

// Specify the testname as an argument to the run_test () task
   run_test (base_test);
// Else, leave the argument to run_test as blank and specify via command-line
   run_test ();
// At command prompt 
$> [simulator] -f list +UVM_TESTNAME=base_test

The second method is preferred because it allows more flexibility to choose different tests without modifying the Top Module system verilog code every time you want to run a different test. Also, it avoids the need for recompilation.

If the +UVM_TESTNAME is specified, then an object of type test_name is created by the factory and phasing begins. If the specified test is not found or not created by the factory, then a fatal error occurs. If no test is specified via command-line and the argument to the run_test() task is blank, then all the components constructed before the call to run_test() will be cycled through their simulation phases.

Refer to Class Definitions for more information regarding method declarations.

Was this article helpful ?