What can you describe with Object-Oriented Programming? When I learned OOP, we had cute classes like animals, cars, and houses. That would be fine if I was taking a C++ or Java course, but this is SystemVerilog, and I am verifying hardware designs. What should my classes represent? This post shows two major categories. It also covers concepts that confused me when I first learned UVM, like sequence vs. sequencer.
This is the second post in a series. Here is the first post on Class Variables and Objects.
The UVM Way
UVM testbenches have two major groups of classes: transactions and components, plus minor utilities. All UVM classes are derived from uvm_object. In my last post, I recommended that you give every SystemVerilog object either a unique ID or name. uvm_object has both as seen in its constructor.
class uvm_object; function new(string name); m_inst_id = m_inst_count++; m_leaf_name = name; endfunction endclass
This code follows the convention that member variables start with the prefix of “m_”.
In the following diagram, all the boxes are components: test, environment, agent, monitor, driver, sequencer, and scoreboard. The gold dots are transactions that are generated in the gold sequence and flow to the driver, or are sampled in the monitor and sent to the scoreboard.
The OOP term “hierarchy” refers to the tree of base classes and their extensions, an is-a relationship. The UVM term “hierarchy” is a has-a relationship, like an agent has-a driver. To avoid confusion, I call this relationship the UVM “topology”.
UVM components have a fixed topology. A driver is always inside an agent, so UVM says that the driver’s parent is the agent. The agent’s parent is an environment, whose parent is the test.
You can see this in the constructor for the base component class which passes the name up to uvm_object and saves a copy of the parent handle. Neither the name or parent need a default.
class uvm_component extends uvm_object; function new(string name, uvm_component parent); super.new(name); m_parent = parent; endfunction endclass
Transactions have no fixed location, flowing from one component to the next. You can’t verify a design with a single transaction, so UVM groups one or more of them into a sequence. This is why transactions in UVM are derived from uvm_sequence_item. This class is derived from uvm_object, and its constructor does not have a parent argument. However the name must have a default value.
class uvm_sequence_item extends uvm_object; function new(string name=”uvm_sequence_item”); super.new(name); endfunction endclass
Life in the Tree of Components
Another dimension of your testbench is time. When are these objects constructed and then garbage collected? When you start simulation, UVM builds the components from the top down, from the test down to the driver and monitor. These objects exist for the entire simulation – you wouldn’t want the driver to get garbage collected halfway through the test! I call component objects “permanent”.
After the components are built and connected, UVM runs your test. Hundreds or thousands of transactions are created in sequences, flow through the testbench into the design, or captured by the monitor and sent to the scoreboard. When no longer needed, the objects are garbage collected. Transactions are very dynamic.
Who’s Jiving Who?
The final big difference between these two types is their focus. Transactions are passive containers of properties such as address and data. The have functions to print, copy, and compare the properties. This pseudo-UVM Packet class gives you the flavor. The calcCsm function calculates the checksum (csm) based on the src, dst, and data properties.
Components are active, operating on transactions. (Technically, they have class variables with handles to the transaction objects.) Components have tasks to send and receive transactions. You would never print, copy, clone, or compare a component.
Summary: UVM Categories
The following table compares the two major categories of classes in UVM, with examples based on the above code.
|UVM Topology||No fixed location||Fixed location|
|Constructor||function new(name=”default”)||function new(name, parent)|
|create call||pkt = packet::type_id::create(“pkt”)||drv = packet_driver::type_id::create(“driver”, this)|
|active/passive||Passive: method operate on itself||Active: methods operate on transaction objects|
|Methods||Functions to print, copy, clone, compare, this object’s properties||Tasks such as run_phase() and functions that operate on transactions|
This post goes over many concepts that confused me when I first learned UVM, such as why some constructors have one argument, and other have two. Another issue: what is the difference between a sequence and a sequencer? Just remember that a sequencer, like a driver, and monitor, is an active component – they all end in ‘r’. Hopefully you can get up to speed faster than me!
This describes a very simple system with the minimum number of classes. The code examples left out many details. Otherwise, this post would be twice as long and twice as confusing!
Here is the next post in this series.