What is UVM environment ?
A UVM environment contains multiple, reusable verification components and defines their default configuration as required by the application. For example, a UVM environment may have multiple agents for different interfaces, a common scoreboard, a functional coverage collector, and additional checkers.
It may also contain other smaller environments that has been verified at block level and now integrated into a subsystem. This allows certain components and sequences used in block level verification to be reused in system level verification plan.
Why shouldn't verification components be placed directly in test class ?
It is technically possible to instantiate agents and scoreboards directly in a user defined
class base_test extends uvm_test; `uvm_component_utils(base_test) apb_agent m_apb_agent; spi_agent m_spi_agent; base_scoreboard m_base_scbd; virtual function void build_phase(uvm_phase phase); // Instantiate agents and scoreboard endfunction endclass
But, it is NOT recommended to do it this way because of the following drawbacks :
- Tests are not reusable because they rely on a specific environment structure
- Test writer would need to know how to configure the environment
- Changes to the topology will require updating of multiple test files and take a lot of time
Hence, it is always recommended to build the testbench class from
uvm_env, which can then be instantiated within multiple tests. This will allow changes in environment topology to be reflected in all the tests. Moreover, the environment should have knobs to configure, enable or disable different verification components for the desired task.
uvm_env is the base class for hierarchical containers of other components that make up a complete environment. It can be reused as a sub-component in a larger environment or even as a stand-alone verification environment that can instantiated directly in various tests.
Steps to create a UVM environment1. Create a custom class inherited from
uvm_env, register with factory, and call
2. Declare and build verification components
// my_env is user-given name for this class that has been derived from "uvm_env" class my_env extends uvm_env; // [Recommended] Makes this driver more re-usable `uvm_component_utils (my_env) // This is standard code for all components function new (string name = "my_env", uvm_component parent = null); super.new (name, parent); endfunction // Code for rest of the steps come here endclass
3. Connect verification components together
// apb_agnt and other components are assumed to be user-defined classes that already exist in TB apb_agnt m_apb_agnt; func_cov m_func_cov; scbd m_scbd; env_cfg m_env_cfg; // Build components within the "build_phase" virtual function void build_phase (uvm_phase phase); super.build_phase (phase); m_apb_agnt = apb_agnt::type_id::create ("m_apb_agnt", this); m_func_cov = func_cov::type_id::create ("m_func_cov", this); m_scbd = scbd::type_id::create ("m_scbd", this); // [Optional] Collect configuration objects from the test class if applicable if (uvm_config_db #(env_cfg)::get(this, "", "env_cfg", m_env_cfg)) `uvm_fatal ("build_phase", "Did not get a configuration object for env") // [Optional] Pass other configuration objects to sub-components via uvm_config_db endfunction
virtual function void connect_phase (uvm_phase phase); // A few examples: // Connect analysis ports from agent to the scoreboard // Connect functional coverage component analysis ports // ... endfunction
UVM Environment Example
This environment has 2 agents, 3 sub-environments and a scoreboard as represented in the block diagram shown above.
What is a monitor ?
A UVM monitor is responsible for capturing signal activity from the design interface and translate it into transaction level data objects that can be sent to other components.
In order to do so, it requires the following:
- A virtual interface handle to the actual interface that this monitor is trying to monitor
- TLM Analysis Port declarations to broadcast captured data to others.
What does a UVM monitor do ?
A UVM monitor is derived from
uvm_monitor base class and should have the following functions :
- Collect bus or signal information through a virtual interface
- Collected data can be used for protocol checking and coverage
- Collected data is exported via an analysis port
The UVM monitor functionality should be limited to basic monitoring that is always required.It may have knobs to enable/disable basic protocol checking and coverage collection. High level functional checking should be done outside the monitor, in a scoreboard.
Steps to create a UVM monitor1. Create custom class inherited from
uvm_monitor, register with factory and call
2. Declare analysis ports and virtual interface handles
// my_monitor is user-given name for this class that has been derived from "uvm_monitor" class my_monitor extends uvm_monitor; // [Recommended] Makes this monitor more re-usable `uvm_component_utils (my_monitor) // This is standard code for all components function new (string name = "my_monitor", uvm_component parent = null); super.new (name, parent); endfunction // Rest of the steps come here endclass
// Actual interface object is later obtained by doing a get() call on uvm_config_db virtual if_name vif; // my_data is a custom class object used to encapsulate signal information // and can be sent to other components uvm_analysis_port #(my_data) mon_analysis_port;
Functions and tasks in a class are recommended to be declared as "virtual" to enable child classes to override them - Click to read on Polymorphism !3. Build the UVM monitor
4. Code the
virtual function void build_phase (uvm_phase phase); super.build_phase (phase); // Create an instance of the declared analysis port mon_analysis_port = new ("mon_analysis_port", this); // Get virtual interface handle from the configuration DB if (! uvm_config_db #(virtual if_name) :: get (this, "", "vif", vif)) begin `uvm_error (get_type_name (), "DUT interface not found") end endfunction
// This is the main piece of monitor code which decides how it has to decode // signal information. For example, AXI monitors need to follow AXI protocol virtual task run_phase (uvm_phase phase); // Fork off multiple threads "if" required to monitor the interface, for example: fork // Thread 1: Monitor address channel // Thread 2: Monitor data channel, populate "obj" data object // Thread 3: Monitor control channel, decide if transaction is over // Thread 4: When data transfer is complete, send captured information // through the declared analysis port mon_analysis_port.write(obj); join_none endtask
UVM Monitor Example
A sequencer generates data transactions as class objects and sends it to the Driver for execution. It is recommended to extend
uvm_sequencer base class since it contains all of the functionality required to allow a sequence to communicate with a driver. The base class is parameterized by the request and response item types that can be handled by the sequencer.
What is a driver ?
UVM driver is an active entity that has knowledge on how to drive signals to a particular interface of the design. For example, in order to drive a bus protocol like APB, UVM driver defines how the signals should be timed so that the target protocol becomes valid. All driver classes should be extended from
uvm_driver, either directly or indirectly.
Transaction level objects are obtained from the Sequencer and the UVM driver drives them to the design via an interface handle.
Steps to create a UVM driver1. Create custom class inherited from
uvm_driver, register with factory and call
2. Declare virtual interface handle and get them in build phase
// my_driver is user-given name for this class that has been derived from "uvm_driver" class my_driver extends uvm_driver; // [Recommended] Makes this driver more re-usable `uvm_component_utils (my_driver) // This is standard code for all components function new (string name = "my_driver", uvm_component parent = null); super.new (name, parent); endfunction // Code for rest of the steps come here endclass
3. Code the
// Actual interface object is later obtained by doing a get() call on uvm_config_db virtual if_name vif; virtual function void build_phase (uvm_phase phase); super.build_phase (phase); if (! uvm_config_db #(virtual if_name) :: get (this, "", "vif", vif)) begin `uvm_fatal (get_type_name (), "Didn't get handle to virtual interface if_name") end endfunction
// This is the main piece of driver code which decides how it has to translate // transaction level objects into pin wiggles at the DUT interface virtual task run_phase (uvm_phase phase); // Loop the following steps // 1. Get next item from the sequencer // 2. Assign data from the received item into DUT interface // 3. Finish driving transaction endtask
UVM Driver-Sequencer handshake
UVM driver is a parameterized class which can drive a specific type of transaction object. The driver has a TLM port of type
uvm_seq_item_pull_port which can accept the parameterized request object from the
uvm_sequencer. It can also provide a response object back to the sequencer and usually the class type of both request and response items are the same. However, they can be different if explicitly specified.
The UVM driver uses the following methods to interact with the sequencer.
|get_next_item||Blocks until a request item is available from the sequencer. This should be followed by |
|try_next_item||Non-blocking method which will return |
|item_done||Non-blocking method which completes the driver-sequencer handshake. This should be called after |
How are driver/sequencer API methods used ?
The idea behind a driver/sequencer handshake mechanism is to allow the driver to get a series of transaction objects from the sequence and respond back to the sequence after it has finished driving the given item so that it can get the next item.1.
This use model allows the driver to get an object from the sequence, drive the item and then finish the handshake with the sequence by calling
item_done(). This is the preferred use model since the driver need to operate only when the sequencer has an object for the driver. Here,
finish_item call in the sequence finishes only after the driver returns
virtual task run_phase (uvm_phase phase); my_data req_item; forever begin // 1. Get next item from the sequencer seq_item_port.get_next_item (req_item); // 2. Drive signals to the interface @(posedge vif.clk); vif.en <= 1; // Drive remaining signals, put write data/get read data // 3. Tell the sequence that driver has finished current item seq_item_port.item_done(); end
The difference between this model and the previous one is that here, the driver gets the next item and sends back the sequence handshake in one go, before the UVM driver processes the item. Later on the driver uses the
put method to indicate that the item has been finished. So,
finish_item call in the sequence is finished as soon as
get() is done.
virtual task run_phase (uvm_phase phase); my_data req_item; forever begin // 1. finish_item in sequence is unblocked seq_item_port.get (req_item); // 2. Drive signals to the interface @(posedge vif.clk); vif.en = 1; // Drive remaining signals // 3. Finish item seq_item_port.put (rsp_item); end endtask
What is a UVM agent ?
An agent encapsulates a Sequencer, Driver and Monitor into a single entity by instantiating and connecting the components together via TLM interfaces. Since UVM is all about configurability, an agent can also have configuration options like the type of UVM agent (active/passive), knobs to turn on features such as functional coverage, and other similar parameters.
Types of Agents
How to find out if a UVM agent is active or passive ?
User-defined agent classes derived from
uvm_agent also have another function called
get_is_active() which will return the state of the requested UVM agent.
// Assume this is inside the user-defined agent class if (get_is_active()) begin // Build driver and sequencer end // Build monitor
Steps to create a UVM agent1. Create a custom class inherited from
uvm_agent, register with factory and call
2. Instantiate agent components
// my_agent is user-given name for this class that has been derived from "uvm_agent" class my_agent extends uvm_agent; // [Recommended] Makes this agent more re-usable `uvm_component_utils (my_agent) // This is standard code for all components function new (string name = "my_agent", uvm_component parent = null); super.new (name, parent); endfunction // Code for rest of the steps come here endclass
3. Instantiate and build components
// Create handles to all agent components like driver, monitor and sequencer // my_driver, my_monitor and agent_cfg are custom classes assumed to be defined // Agents can be configured via a configuration object that can be passed in from the test my_driver m_drv0; my_monitor m_mon0; uvm_sequencer #(my_data) m_seqr0; agent_cfg m_agt_cfg;
4. Connect agent components together
virtual function void build_phase (uvm_phase phase); // If this UVM agent is active, then build driver, and sequencer if (get_is_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 // Both active and passive agents need a monitor m_mon0 = my_monitor::type_id::create ("m_mon0", this); //[Optional] Get any agent configuration objects from uvm_config_db endfunction
virtual function void connect_phase (uvm_phase phase); // Connect the driver to the sequencer if this agent is Active if (get_is_active()) m_drv0.seq_item_port.connect (m_seqr0.seq_item_export); endfunction
What does a UVM agent do ?
Usually, it makes sense to create an agent that provides protocol specific tasks to generate transactions, check the results and perform coverage. For example, a UVM agent can be created for the WishBone protocol whose sequencer will generate data items which can be sent to the driver. The driver then converts the data item class object into actual pin level signals and drive them to the DUT. The monitor may passively collect the outputs from the DUT, convert them back into another data item class object and distribute it among all the components in the testbench waiting for the item.