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.
- 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 afterLINE:. This is the first of two visits to do_print.Follow the calls, values and waits
- call do_print(line printer): Printer → Completed tx
- data tag=3, x=16, y=17: Completed tx → Printer
- data one line of text: Printer → Readable text
- 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
- call do_print(table printer): Printer → Completed tx
- data tag=3, x=16, y=17: Completed tx → Printer
- data table rows: Printer → Readable text
- 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
- data tag 3 | x 16 | y 17: Completed tx → Packed stream
- 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
- data read 3 | 16 | 17: Packed stream → Matching decoder
- 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
- 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.
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);
endfunctionKeep 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.
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 'd17The 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
- 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
- call begin_tr(tx): Test → UVM recorder
- Waiting: UVM recorder — Open; waiting for end_tr
- 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
- data y=17: Test → tx
- 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
- call end_tr(tx): Test → UVM recorder
- call do_record(recorder): UVM recorder → tx
- data tag=3, x=16, y=17: tx → UVM recorder
- data named fields: UVM recorder → Text database
- 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
- data begin 10 ns, end 30 ns: UVM recorder → Text database
- 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.
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.
`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_endUVM_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.
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 : 0Check yourself
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
- Pack a frame whose payload has its own length Implement · Medium