In the previous session, we built a sequencer, and monitor to work along with the driver. Now, lets put all the three components inside a block called an Agent. Moreover, we'll tweak certain aspects of how they are instantiated and connected within the agent. By doing so, the agent will become re-usable and it'll be easier to just plug it in any environment. Its always the amount of configurability without changing the base code that will determine how re-usable a component is. Also, we'll create a scoreboard that can receive transactions from the monitor.

TestBench

agent and scoreboard

Agent

It's better to put the Sequencer, Monitor and Driver inside a uvm component called agent. Usually you'll develop an agent for a particular protocol like USB, AXI, PCIE, etc so that the agent can be plugged into any verification environment and becomes re-usable. To create an agent, simply put all the code inside the uvm_env in our previous session, inside a uvm_agent block and it's all set. Another feature that we want an agent to have, is the ability to make it passive or active.

A passive agent is one that has only a monitor so that it passively sits by the interface and monitors the transactions. This is useful when there is nothing particular to be driven to the DUT. An active agent is one which has all the three components especially the driver and sequencer, so that data can be sent to the DUT.


   class my_agent extends uvm_agent;
      `uvm_component_utils (my_agent)

      my_cfg                     m_cfg0; 
      my_driver                  m_drv0;
      my_monitor                 m_mon0;
      uvm_sequencer #(my_data)   m_seqr0;
      
      function new (string name = "my_agent", uvm_component parent=null);
         super.new (name, parent);
      endfunction

      virtual function void build_phase (uvm_phase phase);
         super.build_phase (phase);

         // Get CFG obj from top to configure the agent
         if (! uvm_config_db #(my_cfg) :: get (this, "", "m_cfg0", m_cfg0)) begin
            `uvm_fatal (get_type_name (), "Didn't get CFG object ! Can't configure agent")
         end
        
         // If the agent is ACTIVE, then create monitor and sequencer, else create only monitor
         if (m_cfg0.active == UVM_ACTIVE) begin
            m_seqr0 = uvm_sequencer#(my_data)::type_id::create ("m_seqr0", this);
            m_drv0 = my_driver::type_id::create ("m_drv0", this);
         end
         m_mon0 = my_monitor::type_id::create ("m_mon0", this);
      endfunction

      virtual function void connect_phase (uvm_phase phase);
         // Assign interface handle in CFG bject to Driver and Monitor, if active
         if (m_cfg0.active == UVM_ACTIVE) 
            m_drv0.vif = m_cfg0.vif;
         m_mon0.vif = m_cfg0.vif;

         // Connect Sequencer to Driver, if the agent is active
         if (m_cfg0.active == UVM_ACTIVE) begin
            m_drv0.seq_item_port.connect (m_seqr0.seq_item_export);
         end
      endfunction

   endclass

Note that the virtual interface is obtained from the configuration object. We have placed the virtual interface inside another class called my_cfg derived from uvm_object, and this object is passed down to the agent. The agent will extract the virtual interface from the configuration object and pass it to the individual sub components. Refer to uvm-401 lab for more details.

Scoreboard

A Scoreboard is a checker element that keeps a tally on the input stimulus, and the expected output. It would typically have functions and tasks to calculate the expected output for a particular input stimulus. So, the whole flow is as follows.

When the driver unpacks the data it received from the sequencer, and drives DUT signals, it also sends the data packet to the scoreboard. The Monitor observes the DUT outputs, repacks the information into data packet form, and sends it to the scoreboard. The scoreboard then compares the data packet it received from the monitor with the expected output it calculated with its own functions and comes to a decision whether it passed or failed.


   class my_scoreboard extends uvm_scoreboard;
      `uvm_component_utils (my_scoreboard)
      
      `uvm_analysis_imp_decl (_data)

      uvm_analysis_imp_data    #(my_data, my_scoreboard)     data_export;    // Receive data from monitor

      function new (string name ="my_scoreboard", uvm_component parent=null);
         super.new (name, parent);
      endfunction

      virtual function void build_phase (uvm_phase phase);
         super.build_phase (phase);
         data_export   = new ("data_export", this);
      endfunction

      function void write_data (my_data data_obj);
         `uvm_info ("SCBD", "Received data item", UVM_HIGH)
         data_obj.print (uvm_default_line_printer);
      endfunction
   endclass

Note that we have used a function called write_data where _data is used in the function to declare an analysis port. So, `uvm_analysis_imp_decl tells the UVM infrastructure that _data will be used as a suffix for the uvm_analysis_imp function, and the function declares an analysis port parameterized to accept my_data class object. Click Using _decl macro in TLM to learn more.

You'll see that the monitor passes data to the scoreboard using the write() function, whose implementation is given in the Scoreboard - the component accepting the data. This is really good stuff, because now you can switch scoreboard with a different component that has a totally different write() method and the testbench setup will still work !

Monitor


   class my_monitor extends uvm_monitor;
      `uvm_component_utils (my_monitor)

      uvm_analysis_port #(my_data)  item_collected_port;        // Open analysis port to pass data to scoreboard
      
      virtual dut_if    vif;   
      my_data           data_obj;
   
      function new (string name, uvm_component parent= null);
         super.new (name, parent);
         item_collected_port = new ("item_collected_port", this);
      endfunction

      virtual function void build_phase (uvm_phase phase);
         super.build_phase (phase);
      endfunction

      task main_phase (uvm_phase phase);
         fork 
            collect_transaction ();
         join_none
      endtask

      virtual task collect_transaction ();
         data_obj = my_data::type_id::create ("data_obj", this);
         forever @(posedge vif.clk) begin
            if (vif.en & vif.rstn) begin
               if (vif.wr) begin
                  `uvm_info ("MON", $sformatf ("Monitor received data for WR operation"), UVM_HIGH)
                  data_obj.addr = vif.addr;
                  data_obj.data = vif.wdata;
               end else begin
                  `uvm_info ("MON", $sformatf ("Monitor received data for RD operation"), UVM_HIGH)
                  data_obj.addr = vif.addr;
                  data_obj.data = vif.rdata;
               end
               data_obj.print (uvm_default_table_printer);
               item_collected_port.write (data_obj);          // Pass data to the scoreboard
            end
         end
      endtask
   endclass