Print, pack and record a transaction

Give one transaction a readable description, a reversible field layout and a recorded lifetime, then catch a decoder that consumes every bit but changes its meaning.

Lesson
U2.4
Time
30 min
Runs on
Verilator with UVM
Check
5 questions

A completed incrementer transaction has tag=3, x=16 and y=17, but replay restores x=17 and y=16. Packing and unpacking report equal bit counts because the decoder exchanged two equal-width fields.

One completed transaction, three useful viewsThe main program formats tag 3, x 16 and y 17, then decodes the same packed stream twice. Arrows show calls or derived data, not copies of an object handle.
One completed transaction, three useful viewsThe main program formats tag 3, x 16 and y 17, then decodes the same packed stream twice. Arrows show calls or derived data, not copies of an object handle. transaction Completed tx tag=3, x=16, y=17 formatter Printer do_print suppliesnamed fields text Readable text Same values in a lineor a table buffer Packed stream Field order: tag 4bits → x 8 → y 8 restored Matching decoder tag=3, x=16, y=17 wrong result Swapped decoder tag=3, x=17, y=16 named fieldsformatted textpack fieldsread tag, x, yread tag, y, x
  1. Format as one line30 ns (illustrative)

    The printer calls do_print. The item supplies field names, widths and values; the line printer lays them out on one line.

    More context

    sprint(line_printer) returns the text rather than writing it; the program prints it after LINE:. This is the first of two visits to do_print.

    Follow the calls, values and waits
    1. call do_print(line printer): Printer → Completed tx
    2. data tag=3, x=16, y=17: Completed tx → Printer
    3. data one line of text: Printer → Readable text
  2. Format the same fields as a table30 ns (illustrative)

    A table printer visits the same do_print. Only the layout changes; the field names and values come from the same hook.

    More context

    print(table_printer) writes the table itself. The next output line counts both visits: print hooks=2 values: tag=3 x=16 y=17.

    Follow the calls, values and waits
    1. call do_print(table printer): Printer → Completed tx
    2. data tag=3, x=16, y=17: Completed tx → Printer
    3. data table rows: Printer → Readable text
  3. Write a field layout30 ns (illustrative)

    do_pack writes tag in 4 bits, x in 8, then y in 8. Field names are absent from that payload, so the decoder must already know this order.

    More context

    The stream carries field values only, 4 + 8 + 8 bits in the order do_pack wrote them. Nothing in it says which 8 bits are x.

    Follow the calls, values and waits
    1. data tag 3 | x 16 | y 17: Completed tx → Packed stream
  4. Restore the fields30 ns (illustrative)

    The matching decoder reads the same widths in the same order. All three field values survive the round trip.

    More context

    The example prints roundtrip: tag=3 x=16 y=17 counts_equal=1 fields_equal=1.

    Follow the calls, values and waits
    1. data read 3 | 16 | 17: Packed stream → Matching decoder
  5. Consume every bit, get the wrong answer30 ns (illustrative)

    The second decoder reads y before x. Both fields are 8 bits, so the consumed count still matches. Only the field-value check exposes x=17 and y=16.

    More context

    The example prints swapped: tag=3 x=17 y=16 counts_equal=1 fields_equal=0. A check of consumed bits alone passes this decoder.

    Follow the calls, values and waits
    1. data read 3 | 17 | 16: Packed stream → Swapped decoder
Read the diagram as text
  • Completed tx: tag=3, x=16, y=17
  • Printer: do_print supplies named fields
  • Readable text: Same values in a line or a table
  • Packed stream: Field order: tag 4 bits → x 8 → y 8
  • Matching decoder: tag=3, x=16, y=17
  • Swapped decoder: tag=3, x=17, y=16
  • Completed tx → Printer: named fields
  • Printer → Readable text: formatted text
  • Completed tx → Packed stream: pack fields
  • Packed stream → Matching decoder: read tag, x, y
  • Packed stream → Swapped decoder: read tag, y, x

Every step executes in the example. The stream label shows the field payload; UVM container metadata is outside this logical layout.

This experiment supplies y=x+1 at 30 ns, with a recording interval starting at 10 ns. It formats and decodes the completed item without a pin-level driver.

Let the printer lay out the fields

A printer is an object that controls how named fields appear. Write do_print once, then choose a table for scanning widths and values, a tree for nested objects, or a line for a compact report.

The hook supplies names, widths and values. UVM calls it through the selected printer.
function void do_print(uvm_printer printer);
  super.do_print(printer);
  printer.print_field_int("tag", tag, 4, UVM_DEC);
  printer.print_field_int("x", x, 8, UVM_DEC);
  printer.print_field_int("y", y, 8, UVM_DEC);
endfunction

Keep the text

string text = tx.sprint(line_printer);
`uvm_info("ITEM", text, UVM_MEDIUM)

sprint returns a string. The report decides whether to display it.

Write the text now

tx.print(table_printer);

print writes to the printer’s output file, the console by default. It returns no string.

The same do_print through the line printer and the table printer (measured, dashed rules left out). super.do_print adds the transaction times; UVM_DEC prints as 'd.
tx: { begin_time: 10 ns  end_time: 30 ns  tag: 'd3  x: 'd16  y: 'd17  }

Name          Type       Size  Value
tx            incr_item  -     -
  begin_time  time       64    10 ns
  end_time    time       64    30 ns
  tag         integral   4     'd3
  x           integral   8     'd16
  y           integral   8     'd17

The convert2string method from [U2.1](U2-1-uvm-transaction-copy.html) supplies a short summary you format yourself. It does not supply fields to sprint. Both mechanisms must be implemented if you want both forms. Call the public print or sprint method, so UVM sets up and finishes the printer around do_print.

Tune a printer without changing the transaction

Create a printer instance for the call. Use set_id_enabled(0) to hide the object identifier, or set_max_depth(n) to limit nested expansion. The print_field_int method accepts up to 64 bits; print_field accepts uvm_bitstream_t for wider fields. Avoid changing a shared default printer halfway through a test: later callers would inherit those settings.

Give the decoder the same layout

Packing converts fields into a stream that another item can restore. It helps with saved transactions or transport between testbench layers. It does not drive a bus, and the UVM stream is not a protocol’s wire format.

Write the fields

packer.pack_field_int(tag, 4);
packer.pack_field_int(x,   8);
packer.pack_field_int(y,   8);

The payload contains 4 + 8 + 8 bits, in this order.

Read the fields

tag = packer.unpack_field_int(4);
x   = packer.unpack_field_int(8);
y   = packer.unpack_field_int(8);

Order and width must both match. The bits carry no field names.

Use tx.pack(bits, packer) and restored.unpack(bits, packer); these methods prepare the packer and call your hooks. Create restored before unpacking. In each hook, call super so inherited fields are handled, then process the fields this class adds.

Use pack_field_int and unpack_field_int for fields up to 64 bits. Their pack_field and unpack_field counterparts also work here and support wider uvm_bitstream_t values.

Here pack_field_int writes the low bit first within each field: tag=3 contributes 1,1,0,0; x=16 contributes 0,0,0,0,1,0,0,0. The matching unpacker reads in that same order. The program uses one uvm_packer configuration at both ends.

Check the consumed count and the restored fields. A shorter decoder may leave data unread; swapping two 8-bit fields preserves the count and corrupts the meaning. For larger items, U2.1’s compare hook can check the fields. This run compares the three values directly.

Extend the layout to a variable-length payload

Write a bounded length before the elements, then unpack and validate that length before allocating the destination array. Both ends must agree on length width, element width, order and version. A round trip through your own two matching hooks can still preserve a shared mistake: compare against an external format example when interoperability matters.

Keep the lifetime as well as the values

The recorder keeps the interval and the final fieldsThe test starts a transaction at 10 ns, supplies y=17 at 30 ns, then ends it. Calls open and close the real UVM text recorder; named fields flow into its database.
The recorder keeps the interval and the final fieldsThe test starts a transaction at 10 ns, supplies y=17 at 30 ns, then ends it. Calls open and close the real UVM text recorder; named fields flow into its database. caller Test Owns begin_tr(tx)and end_tr(tx) transaction tx x=16; y changes from0 to 17 recorder UVM recorder One open interval:10–30 ns stored record Text database Start, end, tag=3,x=16, y=17 open / closeassign responsedo_record / fieldsdatabase records
  1. Open the interval10 ns (illustrative)

    Recording is enabled and a text database is installed. begin_tr opens a recorder and stores the start time. The response field is still zero.

    More context

    The example prints 10 ns begin: tag=3 x=16 y=0. begin_tr does not call do_record: the record counter is still 0.

    Follow the calls, values and waits
    1. call begin_tr(tx): Test → UVM recorder
    2. Waiting: UVM recorder — Open; waiting for end_tr
  2. Supply the response30 ns (illustrative)

    The test assigns y=x+1, giving 17. Keeping a recorder open does not sample every change to the object.

    More context

    This is an ordinary assignment 20 ns after begin_tr. Nothing observes tx in between, so only its value at end_tr can reach the database.

    Follow the calls, values and waits
    1. data y=17: Test → tx
  3. end_tr records the fields30 ns (illustrative)

    end_tr causes the item’s do_record hook to run once. It submits tag=3, x=16 and y=17 to the recorder, which writes them to the database.

    More context

    The fields reach the recorder by name and type, not as a packed stream. The hook's counter becomes 1 here.

    Follow the calls, values and waits
    1. call end_tr(tx): Test → UVM recorder
    2. call do_record(recorder): UVM recorder → tx
    3. data tag=3, x=16, y=17: tx → UVM recorder
    4. data named fields: UVM recorder → Text database
  4. The interval closes30 ns (illustrative)

    The recorder then closes the interval at 30 ns, and the database holds its start and end times beside the fields.

    More context

    The example prints 30 ns record: calls=1 saved_y=17 begin=10 end=30: one visit, the final response, and the 10–30 ns interval.

    Follow the calls, values and waits
    1. data begin 10 ns, end 30 ns: UVM recorder → Text database
    2. Released: UVM recorder
Read the diagram as text
  • Test: Owns begin_tr(tx) and end_tr(tx)
  • tx: x=16; y changes from 0 to 17
  • UVM recorder: One open interval: 10–30 ns
  • Text database: Start, end, tag=3, x=16, y=17
  • Test → UVM recorder: open / close
  • Test → tx: assign response
  • UVM recorder → tx: do_record / fields
  • UVM recorder → Text database: database records

This is the manual recording path used by the program. No sequence or driver also records this item. The printed counter confirms the hook ran; the database receives the typed fields.

A transaction record combines an interval with searchable fields. A log line can show y=17. Recording also preserves the transaction’s 10–30 ns interval. The begin_tr and end_tr methods mark that span; do_record submits fields to the recorder.

The same component begins and ends the item. The example checks that a recorder actually opened.
set_tr_database(database);       // an opened uvm_text_tr_database here
set_recording_enabled(1);
#10; handle = begin_tr(tx, "incrementer");
#20; tx.y = tx.x + 1;
end_tr(tx);

In this manual component path, end_tr calls record before closing the recorder. The do_record hook therefore sees the final y=17 once; opening the interval does not keep sampling the object. Its record_field_int calls preserve a name, width, radix and value for each field.

Timestamps can still change when recording is disabled. That is why this program checks the returned handle and the do_record call count as well as the begin and end times. Calling tx.record() with no recorder records nothing.

In a sequencer-driven bench, start_item and finish_item already begin and end each item on the sequencer, because automatic item recording is on by default. A driver that manages the item’s interval itself calls seq_item_port.disable_auto_item_recording() at the start of its run_phase. Otherwise the sequencer also begins and ends the same item, and whichever call comes last sets its times. [U6.1](U6-1-uvm-pipelined-driver.html) develops when a pipelined bus operation begins and ends.

Choose automation per operation

Field macros can generate these object operations instead of handwritten hooks. Put them between uvm_object_utils_begin and uvm_object_utils_end. Choose flags per field: a debugging cookie may be useful to print and record without becoming part of the packed transaction or an equality check.

An alternative implementation: debug_cookie still copies, prints and records, but pack/unpack and compare omit it.
`uvm_object_utils_begin(macro_item)
  `uvm_field_int(tag, UVM_DEFAULT | UVM_DEC)
  `uvm_field_int(x, UVM_DEFAULT | UVM_DEC)
  `uvm_field_int(y, UVM_DEFAULT | UVM_DEC)
  `uvm_field_int(debug_cookie, UVM_DEFAULT | UVM_NOPACK | UVM_NOCOMPARE)
`uvm_object_utils_end

UVM_DEFAULT enables the operations; UVM_NOPACK excludes both packing and unpacking, while UVM_NOPRINT and UVM_NORECORD exclude only their named operations. Supply positive operation flags, or UVM_DEFAULT, as well as exclusions. An exclusion flag alone does not mean “everything else on”; uvm-core warns UVM/FIELDS/NO_FLAG once when it sees this.

Choose macros for regular fields and handwritten hooks for selective views or a controlled layout. Do not pack the same field through both: automation and the hook can each append it. Macros expand into operation dispatch and traversal code; large arrays and nested objects can make repeated print, pack and record calls expensive. Measure the operations used by your bench rather than assuming either style is always faster.

Try it

The line and table come from the same do_print implementation. The recording counter changes only when end_tr invokes do_record. Both decoders consume the whole stream, but only the matching decoder restores the fields.

Try changing x to 255: the eight-bit response wraps to zero. Then change only one unpack width, and use the restored fields and consumed count together to locate the mistake.

Transfer exercise: add a two-bit operation field before x. Update both hooks and the round-trip check. Explain how an old decoder would misread the new stream, and where a format version would let it reject that stream instead.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: 10 ns begin: tag=3 x=16 y=0 30 ns record: calls=1 saved_y=17 begin=10 end=30 print hooks=2 values: tag=3 x=16 y=17 roundtrip: tag=3 x=16 y=17 counts_equal=1 fields_equal=1 swapped: tag=3 x=17 y=16 counts_equal=1 fields_equal=0 UVM_ERROR : 0

Check yourself

ConceptA report needs the table-style fields as a string so its verbosity setting can suppress the message. Which approach supplies that string?
Predict the outputtag=3, x=16 and y=17 are packed in widths 4, 8 and 8. The restored item reads tag, then y, then x. What line is printed? Only the displayed line is part of the answer.
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;

class incr_item extends uvm_sequence_item;
  `uvm_object_utils(incr_item)
  bit [3:0] tag;
  bit [7:0] x, y;
  int print_calls = 0, record_calls = 0;
  bit [7:0] recorded_y;
  time recorded_at = 0;
  function new(string name = "incr_item"); super.new(name); endfunction

  function string convert2string();
    return $sformatf("tag=%0d x=%0d y=%0d", tag, x, y);
  endfunction

  function void do_print(uvm_printer printer);
    super.do_print(printer);             // includes inherited transaction times
    print_calls++;
    printer.print_field_int("tag", tag, 4, UVM_DEC);
    printer.print_field_int("x", x, 8, UVM_DEC);
    printer.print_field_int("y", y, 8, UVM_DEC);
  endfunction

  function void do_pack(uvm_packer packer);
    super.do_pack(packer);
    packer.pack_field_int(tag, 4);
    packer.pack_field_int(x,   8);
    packer.pack_field_int(y,   8);
  endfunction

  function void do_unpack(uvm_packer packer);
    super.do_unpack(packer);
    tag = packer.unpack_field_int(4);
    x   = packer.unpack_field_int(8);
    y   = packer.unpack_field_int(8);
  endfunction

  function void do_record(uvm_recorder recorder);
    super.do_record(recorder);
    record_calls++;                     // makes invocation observable
    recorded_y = y;
    recorded_at = $time;
    recorder.record_field_int("tag", tag, 4, UVM_DEC);
    recorder.record_field_int("x", x, 8, UVM_DEC);
    recorder.record_field_int("y", y, 8, UVM_DEC);
  endfunction
endclass

// A second decoder, with the complete three-field layout replaced.
class swapped_item extends incr_item;
  `uvm_object_utils(swapped_item)
  function new(string name = "swapped_item"); super.new(name); endfunction
  function void do_unpack(uvm_packer packer);
    // Calling incr_item.do_unpack here would consume all three fields first.
    tag = packer.unpack_field_int(4);
    y   = packer.unpack_field_int(8);
    x   = packer.unpack_field_int(8);
  endfunction
endclass

class object_methods_test extends uvm_test;
  `uvm_component_utils(object_methods_test)
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  
  task run_phase(uvm_phase phase);
    incr_item tx = new("tx");
    swapped_item restored = new("restored");
    uvm_packer packer = new("packer");
    bit bits[];
    int written, consumed;
    phase.raise_objection(this);
    tx.tag = 3; tx.x = 16; tx.y = 17;
    written = tx.pack(bits, packer);
    consumed = restored.unpack(bits, packer);
    $display("x=%0d y=%0d counts_equal=%0b", restored.x, restored.y, written == consumed);
    phase.drop_objection(this);
  endtask
endclass

module tb;
  initial begin
    $timeformat(-9, 0, " ns", 0);
    run_test("object_methods_test");
  end
endmodule
Spot the bugThe writer packs tag, x and y in widths 4, 8 and 8. Each option replaces the body after super.do_unpack. The test checks consumed count and all three restored values for tag=3, x=16, y=17. Which decoder fails?
Predict the outputRecording is enabled on the test with an opened text database. It calls begin_tr at 10 ns, changes y from 0 to 17 at 30 ns, and immediately calls end_tr. No other code records the item. What line is printed?
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;

class incr_item extends uvm_sequence_item;
  `uvm_object_utils(incr_item)
  bit [3:0] tag;
  bit [7:0] x, y;
  int print_calls = 0, record_calls = 0;
  bit [7:0] recorded_y;
  time recorded_at = 0;
  function new(string name = "incr_item"); super.new(name); endfunction

  function string convert2string();
    return $sformatf("tag=%0d x=%0d y=%0d", tag, x, y);
  endfunction

  function void do_print(uvm_printer printer);
    super.do_print(printer);             // includes inherited transaction times
    print_calls++;
    printer.print_field_int("tag", tag, 4, UVM_DEC);
    printer.print_field_int("x", x, 8, UVM_DEC);
    printer.print_field_int("y", y, 8, UVM_DEC);
  endfunction

  function void do_pack(uvm_packer packer);
    super.do_pack(packer);
    packer.pack_field_int(tag, 4);
    packer.pack_field_int(x,   8);
    packer.pack_field_int(y,   8);
  endfunction

  function void do_unpack(uvm_packer packer);
    super.do_unpack(packer);
    tag = packer.unpack_field_int(4);
    x   = packer.unpack_field_int(8);
    y   = packer.unpack_field_int(8);
  endfunction

  function void do_record(uvm_recorder recorder);
    super.do_record(recorder);
    record_calls++;                     // makes invocation observable
    recorded_y = y;
    recorded_at = $time;
    recorder.record_field_int("tag", tag, 4, UVM_DEC);
    recorder.record_field_int("x", x, 8, UVM_DEC);
    recorder.record_field_int("y", y, 8, UVM_DEC);
  endfunction
endclass



class object_methods_test extends uvm_test;
  `uvm_component_utils(object_methods_test)
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  
  task run_phase(uvm_phase phase);
    incr_item tx = new("tx");
    uvm_text_tr_database database = new("database");
    uvm_tr_stream stream;
    int handle;
    phase.raise_objection(this);
    database.set_file_name("u24-quiz-recording.log");
    if (!database.open_db()) `uvm_fatal("RECORD", "cannot open transaction database")
    set_tr_database(database);
    set_recording_enabled(1);
    tx.tag = 3; tx.x = 16; tx.y = 0;
    #10;
    handle = begin_tr(tx, "incrementer");
    if (handle == 0) `uvm_fatal("RECORD", "no recorder was opened")
    #20;
    tx.y = 17;
    end_tr(tx);
    $display("calls=%0d saved_y=%0d begin=%0d end=%0d", tx.record_calls,
             tx.recorded_y, tx.get_begin_time(), tx.get_end_time());
    stream = get_tr_stream("incrementer", "TVM");
    free_tr_stream(stream);
    void'(database.close_db());
    #1;
    phase.drop_objection(this);
  endtask
endclass

module tb;
  initial begin
    $timeformat(-9, 0, " ns", 0);
    run_test("object_methods_test");
  end
endmodule
ConceptA debug_cookie must still copy, print and record, but must be omitted from packed data and comparison. The class uses field macros only for this field. Which flags express that choice?

Remember

  • do_print supplies named fields; sprint returns their formatted string and print writes it.
  • Pack and unpack must agree on field order, widths and packer settings. Check values as well as consumed size.
  • A UVM packed object is a testbench representation, not automatically a protocol wire encoding.
  • begin_tr/end_tr define a lifetime; enabled recording and do_record submit the fields to a real recorder.
  • Field macros select operations per field. Avoid processing the same field twice through automation and a hook.

Practice