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.
You can download/clone the uvm-401 code example from our repository @ GitHub.
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.
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 !
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