Build Your First Testbench defined reg_block and the reg_if interface that connects to it. Before any UVM component can drive or observe that interface, you need a way to represent one operation - one write or one read - as a single piece of data that can be created, randomized, and passed around inside the testbench. That's what a transaction class is for, and it's the first piece of actual UVM code in this tutorial.

7 min read | Intermediate Level

What You'll Learn

  • Why a transaction class extends uvm_sequence_item, not uvm_object directly
  • How to define reg_transaction with fields matching reg_if
  • How a constraint keeps a transaction from requesting a write and a read at once
  • What the utility and field macros are doing for you

From uvm_object to uvm_sequence_item

UVM Object introduced uvm_object as the base class for data that flows through a testbench, as opposed to uvm_component, which is for the structural pieces like drivers and monitors. A transaction is exactly that kind of data, so it makes sense that it sits in the uvm_object family. But it doesn't extend uvm_object directly.

UVM provides a more specific class in that same family: uvm_sequence_item. Here's the idea behind it: a transaction's whole job is to eventually reach a driver, usually traveling through a sequencer to get there. uvm_sequence_item simply adds a few extra fields that help with that trip - for example, a way to mark which sequence sent it. uvm_object doesn't have those fields, because plenty of uvm_objects never go anywhere near a sequencer. Since every transaction in this tutorial will eventually be sent to a driver, every transaction extends uvm_sequence_item instead of uvm_object directly.

None of the boxes on the left exist yet - the sequence, sequencer, and driver each get their own article in this chapter. This article only covers the small diamond: the reg_transaction object itself, the data that will eventually ride through that whole path. reg_if and reg_block on the right are already in place from Build Your First Testbench.

For now, the simple rule is: anything that will be sent to a driver extends uvm_sequence_item. The sequencer-related fields come into play in UVM Sequencer and UVM Sequence.

Where is This Used in Practice

Every protocol UVC you'll encounter - AXI, APB, AHB, a custom internal bus - defines exactly one transaction class this way: a single object representing one transfer, with fields for whatever that protocol needs to describe a transfer. The register block in this tutorial just has a smaller field list than most.

Defining reg_transaction

The transaction's fields should look familiar - they mirror the signals in reg_if almost exactly, because the whole point of this object is to carry the same information the interface carries, just as data instead of as wires:


class reg_transaction extends uvm_sequence_item;

  rand bit       wr_en;
  rand bit       rd_en;
  rand bit [3:0] addr;
  rand bit [7:0] wdata;
       bit [7:0] rdata;

  constraint wr_xor_rd_c {
    wr_en != rd_en;
  }

  `uvm_object_utils_begin(reg_transaction)
    `uvm_field_int(wr_en,  UVM_ALL_ON)
    `uvm_field_int(rd_en,  UVM_ALL_ON)
    `uvm_field_int(addr,   UVM_ALL_ON)
    `uvm_field_int(wdata,  UVM_ALL_ON)
    `uvm_field_int(rdata,  UVM_ALL_ON)
  `uvm_object_utils_end

  function new(string name = "reg_transaction");
    super.new(name);
  endfunction

endclass

A few things are worth calling out line by line:

  • wr_en, rd_en, addr, and wdata are marked rand because they describe what operation the testbench wants to request - these get randomized to generate stimulus.
  • rdata is deliberately not rand. It's a response field: something the monitor or driver fills in after a read happens, not something the testbench decides up front.
  • The widths match reg_if exactly - 4 bits for addr, 8 bits for wdata and rdata - because UVM Driver maps these fields onto those signals one for one.

Solving the wr_en/rd_en Ambiguity

The Quick Check in Build Your First Testbench asked what reg_block does if wr_en and rd_en are both high on the same edge - the answer was that the DUT just picks write and ignores read, since that case isn't meaningfully defined. The constraint in reg_transaction solves that problem at the source instead of leaving it to chance:


constraint wr_xor_rd_c {
  wr_en != rd_en;
}

Because != on two single bits is only true when exactly one of them is 1, this constraint forces every randomized transaction to request either a write or a read, never both and never neither. By the time a transaction reaches the driver, the ambiguous case the DUT never defined simply can't occur.

Quick Check

Q: If you removed the wr_xor_rd_c constraint, would randomize() still succeed?

Show Answer

Yes - without the constraint, wr_en and rd_en are two independent random bits, so all four combinations (including both 0 and both 1) are legal outcomes. randomize() wouldn't fail; it would just occasionally produce a transaction that doesn't clearly request a write or a read, which is exactly the ambiguous case the constraint exists to rule out.

What the Macros Are Doing

The block between uvm_object_utils_begin and uvm_object_utils_end does two things. First, it registers reg_transaction with the UVM factory, which is what lets you create instances of it using ::type_id::create() later on. Second, each uvm_field_int line tells UVM about one field, so that built-in operations know about it automatically - print() will show that field's value, copy() will copy it from one transaction to another, and compare() will check whether two transactions have the same value in it. You don't write any of that code yourself; listing the field once is enough. UVM_ALL_ON just means "turn all of those on for this field."

You'll see these same fields again from a different angle once the Field Macros chapter covers what each operation actually does and how to write the equivalent code by hand. For now, the macros are doing real, useful work even though their internals are out of scope here.

The constructor is short but mandatory: every uvm_object-derived class needs a new() function that takes a string name and passes it to super.new(). This is what lets the factory create named instances of reg_transaction later on.

Common Mistakes

Mistake #1: Forgetting the new(string name = ...) constructor

Every class derived from uvm_object needs this constructor defined explicitly, even though it does almost nothing beyond calling super.new(name). Without it, the factory has no way to create a properly named instance, and you'll typically see a compile error rather than a runtime one.

Mistake #2: Marking rdata as rand

It's tempting to mark every field rand out of habit, but rdata is a response, not a request - the testbench doesn't choose what comes back from a read, the DUT does. Randomizing it would let randomize() silently overwrite a value the driver or monitor is supposed to fill in after the fact.

Practice Exercise

Challenge: Add an Idle Request Field

Suppose you wanted a transaction that explicitly represents "drive neither a write nor a read this cycle" - an idle request, useful for inserting gaps between operations during testing. Add a field to reg_transaction that captures this, and adjust the constraint so an idle request can't be combined with a write or a read.

Hint

Add a new rand bit field, then extend the constraint block so that at most one of the three control bits is set at a time. Look at how wr_xor_rd_c already expresses mutual exclusion between two bits.

Solution
rand bit idle;

constraint one_op_c {
  (wr_en + rd_en + idle) == 1;
}
  

Adding the three bits together and constraining the sum to exactly 1 generalizes the same idea as wr_xor_rd_c: exactly one control bit is set per transaction, whichever fields you add later.

Summary

In this article, you learned:

  • Transactions extend uvm_sequence_item, a more specific descendant of uvm_object built for items that travel through a driver
  • reg_transaction's fields mirror reg_if's signals in name and width
  • A constraint can rule out an ambiguous combination of control signals before it ever reaches the DUT
  • The utility and field macros register the class with the factory and implement common object operations for you

Going Deeper

Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5