Most data inside a UVM testbench stays as SystemVerilog class objects from the moment a transaction is created to the moment it is consumed. But the instant that data needs to cross a boundary that doesn't understand SystemVerilog - a DPI call, a file, a socket modeling SPI or I2C traffic, or a co-simulation bridge to another language - it has to become a flat stream of bits first. pack and unpack are the UVM methods that make that conversion.
This article covers the three pack/unpack variants available through the automation macros, then shows the same behavior implemented by hand with do_pack/do_unpack - and clears up a detail the return value of unpack() that trips up almost everyone the first time they use it.
7 min read | Intermediate Level
What You'll Learn
- The difference between
pack,pack_bytes, andpack_ints - Why packing into a byte or int array can add extra zero bits that aren't part of your data
- What the return value of
unpack()actually represents - How to implement custom packing logic with
do_packanddo_unpack
UVM automation macros also include mechanisms to pack class variables into a bit or byte stream, and unpack a bit stream to populate the class contents back. There are three method pairs available, all operating on the same underlying bits but writing them into different array types:
| Method | Description |
|---|---|
pack | Concatenates the class contents bit by bit into an array of type bit |
pack_bytes | Concatenates the class contents into an array of type byte, rounding up and zero-padding to the next full byte |
pack_ints | Concatenates the class contents into an array of type int, rounding up and zero-padding to the next full 32-bit word |
unpack | Extracts property values from a bit array and stores them into the matching class variables |
unpack_bytes | Extracts property values from a byte array and stores them into the matching class variables |
unpack_ints | Extracts property values from an int array and stores them into the matching class variables |
As the diagram shows, all three pack variants encode the exact same bits - the only difference is the element type of the destination array, and whether that forces extra padding bits onto the end. The padding exists purely so the result lines up on a clean byte or word boundary; it carries no information of its own.
Using automation macros
Pack
A class called Packet is defined with some variables to store address and data, and is registered with `uvm_field_int macros to enable automation. UVM_DEFAULT specifies that all the default automation methods should be applied to the given variable.
Note that the order of variables as listed within `uvm_object_utils_* will be used for both packing and unpacking data.
class Packet extends uvm_object;
rand bit [3:0] m_addr;
rand bit [3:0] m_wdata;
rand bit [3:0] m_rdata;
rand bit m_wr;
`uvm_object_utils_begin(Packet)
`uvm_field_int(m_addr, UVM_DEFAULT)
`uvm_field_int(m_wdata, UVM_DEFAULT)
`uvm_field_int(m_rdata, UVM_DEFAULT)
`uvm_field_int(m_wr, UVM_DEFAULT)
`uvm_object_utils_end
function new(string name = "Packet");
super.new(name);
endfunction
endclass
Let us create a test class called pack_test where an object of Packet is created, randomized and packed.
class pack_test extends uvm_test;
`uvm_component_utils(pack_test)
function new(string name = "pack_test", uvm_component parent=null);
super.new(name, parent);
endfunction
// Declare some arrays to store packed data output from different pack functions
bit m_bits[];
byte unsigned m_bytes[];
int unsigned m_ints[];
virtual function void build_phase(uvm_phase phase);
// First create an object of class "Packet", randomize and print its contents
Packet m_pkt = Packet::type_id::create("Packet");
m_pkt.randomize();
m_pkt.print();
// Now, call "pack", "pack_bytes", and "pack_ints" and pass appropriate array type
m_pkt.pack(m_bits);
m_pkt.pack_bytes(m_bytes);
m_pkt.pack_ints(m_ints);
// Print the array contents
`uvm_info(get_type_name(), $sformatf("m_bits=%p", m_bits), UVM_LOW)
`uvm_info(get_type_name(), $sformatf("m_bytes=%p", m_bytes), UVM_LOW)
`uvm_info(get_type_name(), $sformatf("m_ints=%p", m_ints), UVM_LOW)
endfunction
endclass
module tb;
initial begin
run_test("pack_test");
end
endmodule
The function pack returned an array of bits, pack_bytes returned an array of bytes, and pack_ints returned an array of ints. Packet has 4+4+4+1 = 13 bits total, which fits exactly into 13 elements of m_bits with nothing left over. pack_bytes needs 2 bytes (16 bits) to hold those same 13 bits, so the remaining 3 bits are zero-padded; pack_ints needs a full 32-bit int to hold them, so 19 bits are zero-padded.
UVM_INFO @ 0: reporter [RNTST] Running test pack_test...
--------------------------------
Name Type Size Value
--------------------------------
Packet Packet - @1907
m_addr integral 4 'hd
m_wdata integral 4 'h9
m_rdata integral 4 'h6
m_wr integral 1 'h1
--------------------------------
UVM_INFO testbench.sv(74) @ 0: uvm_test_top [pack_test] m_bits='{'h1, 'h1, 'h0, 'h1, 'h1, 'h0, 'h0, 'h1, 'h0, 'h1, 'h1, 'h0, 'h1}
UVM_INFO testbench.sv(75) @ 0: uvm_test_top [pack_test] m_bytes='{'hd9, 'h68}
UVM_INFO testbench.sv(76) @ 0: uvm_test_top [pack_test] m_ints='{3647471616}
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_report_server.svh(847) @ 0: reporter [UVM/REPORT/SERVER]
The pack, pack_bytes, and pack_ints functions also return an int giving the total number of bits packed, the same way unpack does (covered next). Most code ignores that return value during packing since the array itself already has the data - it matters far more during unpacking.
Unpack
Let us create a different test class called unpack_test which will create two Packet objects, but randomize only one of them leaving the other empty. Then pack contents of the first object into an array, and then unpack it into the second object and display its contents. Repeat the same thing with other types of pack functions.
class unpack_test extends uvm_test;
`uvm_component_utils(unpack_test)
function new(string name = "unpack_test", uvm_component parent=null);
super.new(name, parent);
endfunction
// Declare a few arrays to hold packed data for each type of "pack" function
bit m_bits[];
byte unsigned m_bytes[];
int unsigned m_ints[];
// Variables to hold return value from pack function
int m_val1, m_val2, m_val3;
virtual function void build_phase(uvm_phase phase);
Packet m_pkt = Packet::type_id::create("Packet");
Packet m_pkt2 = Packet::type_id::create("Packet");
`uvm_info(get_type_name(), $sformatf("Start pack"), UVM_LOW)
// Randomize the first object, print and pack into bit array, then display
m_pkt.randomize();
m_pkt.print();
m_pkt.pack(m_bits);
`uvm_info(get_type_name(), $sformatf("packed m_bits=%p", m_bits), UVM_LOW)
// Randomize the first object, print and pack into byte array, then display
m_pkt.randomize();
m_pkt.print();
m_pkt.pack_bytes(m_bytes);
`uvm_info(get_type_name(), $sformatf("packed m_bytes=%p", m_bytes), UVM_LOW)
// Randomize the first object, print and pack into int array, then display
m_pkt.randomize();
m_pkt.print();
m_pkt.pack_ints(m_ints);
`uvm_info(get_type_name(), $sformatf("packed m_ints=%p", m_ints), UVM_LOW)
`uvm_info(get_type_name(), $sformatf("Start unpack"), UVM_LOW)
// Now unpack the packed bit array into the second object, and display
m_val1 = m_pkt2.unpack(m_bits);
`uvm_info(get_type_name(), $sformatf("unpacked m_val1=0x%0h", m_val1), UVM_LOW)
m_pkt2.print();
// Now unpack the packed byte array into the second object, and display
m_val2 = m_pkt2.unpack_bytes(m_bytes);
`uvm_info(get_type_name(), $sformatf("unpacked m_val2=0x%0h", m_val2), UVM_LOW)
m_pkt2.print();
// Now unpack the packed int array into the second object, and display
m_val3 = m_pkt2.unpack_ints(m_ints);
`uvm_info(get_type_name(), $sformatf("unpacked m_val3=0x%0h", m_val3), UVM_LOW)
m_pkt2.print();
endfunction
endclass
module tb;
initial begin
run_test("unpack_test");
end
endmodule
Remember that the order in which it is packed should be the same as the order in which it is unpacked. Any change in packer policy regarding order may cause the results to be different.
UVM_INFO @ 0: reporter [RNTST] Running test unpack_test...
UVM_INFO testbench.sv(93) @ 0: uvm_test_top [unpack_test] Start pack
--------------------------------
Name Type Size Value
--------------------------------
Packet Packet - @1907
m_addr integral 4 'hd
m_wdata integral 4 'h9
m_rdata integral 4 'h6
m_wr integral 1 'h1
--------------------------------
UVM_INFO testbench.sv(95) @ 0: uvm_test_top [unpack_test] packed m_bits='{'h1, 'h1, 'h0, 'h1, 'h1, 'h0, 'h0, 'h1, 'h0, 'h1, 'h1, 'h0, 'h1}
--------------------------------
Name Type Size Value
--------------------------------
Packet Packet - @1907
m_addr integral 4 'h8
m_wdata integral 4 'he
m_rdata integral 4 'h4
m_wr integral 1 'h0
--------------------------------
UVM_INFO testbench.sv(100) @ 0: uvm_test_top [unpack_test] packed m_bytes='{'h8e, 'h40}
--------------------------------
Name Type Size Value
--------------------------------
Packet Packet - @1907
m_addr integral 4 'h9
m_wdata integral 4 'he
m_rdata integral 4 'he
m_wr integral 1 'h1
--------------------------------
UVM_INFO testbench.sv(105) @ 0: uvm_test_top [unpack_test] packed m_ints='{2666004480}
UVM_INFO testbench.sv(109) @ 0: uvm_test_top [unpack_test] Start unpack
UVM_INFO testbench.sv(109) @ 0: uvm_test_top [unpack_test] unpacked m_val1=0xd
--------------------------------
Name Type Size Value
--------------------------------
Packet Packet - @1909
m_addr integral 4 'hd
m_wdata integral 4 'h9
m_rdata integral 4 'h6
m_wr integral 1 'h1
--------------------------------
UVM_INFO testbench.sv(113) @ 0: uvm_test_top [unpack_test] unpacked m_val2=0xd
--------------------------------
Name Type Size Value
--------------------------------
Packet Packet - @1909
m_addr integral 4 'h8
m_wdata integral 4 'he
m_rdata integral 4 'h4
m_wr integral 1 'h0
--------------------------------
UVM_INFO testbench.sv(117) @ 0: uvm_test_top [unpack_test] unpacked m_val3=0xd
--------------------------------
Name Type Size Value
--------------------------------
Packet Packet - @1909
m_addr integral 4 'h9
m_wdata integral 4 'he
m_rdata integral 4 'he
m_wr integral 1 'h1
--------------------------------
UVM_INFO /playground_lib/uvm-1.2/src/base/uvm_report_server.svh(847) @ 0: reporter [UVM/REPORT/SERVER]
Look closely at m_val1, m_val2, and m_val3 in the log above - all three print 0xd. That is not a coincidence about m_addr, and it is not a sign that the unpack worked because the value "looks right". unpack() and its variants return the number of bits that were read from the source array, not a data field from the object. Packet is exactly 4+4+4+1 = 13 bits, and 13 in hex is 0xd. The bit count happens to print as 0xd here purely because this particular class is 13 bits wide - change any field's width and m_val1 would change too, with no relationship to m_addr at all.
Quick Check
Q: If you added a 4-bit m_extra field to Packet (making it 17 bits total) and called unpack() again, what would the return value be?
Show Answer
17 (0x11 in hex) - the return value tracks the total number of bits consumed from the array, which grows with the object, not any particular field's value.
Automation macros introduce a lot of new code, and using them everywhere is not generally recommended - particularly for objects with many fields, where the macro-generated code adds noticeable simulation overhead compared to hand-written do_pack/do_unpack.
Where Is This Used?
Packing and unpacking shows up most often at protocol boundaries where the data has to leave the SystemVerilog domain entirely - serializing a transaction to drive bit-by-bit over a DPI link to a reference model written in C, writing transactions to a file for offline replay, or reconstructing a transaction object from raw bytes captured off a bus monitor that only sees wire-level activity.
Using do_pack/do_unpack
Just like print, copy, and compare, pack and unpack also have user-definable hooks called do_pack and do_unpack.
class Packet extends uvm_object;
rand bit [3:0] m_addr;
rand bit [3:0] m_wdata;
rand bit [3:0] m_rdata;
rand bit m_wr;
`uvm_object_utils(Packet)
// Define do_print, so that print method displays contents of this class
virtual function void do_print(uvm_printer printer);
super.do_print(printer);
printer.print_field_int("m_addr", m_addr, $bits(m_addr), UVM_HEX);
printer.print_field_int("m_wdata", m_wdata, $bits(m_wdata), UVM_HEX);
printer.print_field_int("m_rdata", m_rdata, $bits(m_rdata), UVM_HEX);
printer.print_field_int("m_wr", m_wr, $bits(m_wr), UVM_HEX);
endfunction
virtual function void do_pack(uvm_packer packer);
super.do_pack(packer);
packer.pack_field_int(m_addr, $bits(m_addr));
packer.pack_field_int(m_wdata, $bits(m_wdata));
packer.pack_field_int(m_rdata, $bits(m_rdata));
packer.pack_field_int(m_wr, $bits(m_wr));
endfunction
virtual function void do_unpack(uvm_packer packer);
super.do_unpack(packer);
m_addr = packer.unpack_field_int($bits(m_addr));
m_wdata = packer.unpack_field_int($bits(m_wdata));
m_rdata = packer.unpack_field_int($bits(m_rdata));
m_wr = packer.unpack_field_int($bits(m_wr));
endfunction
function new(string name = "Packet");
super.new(name);
endfunction
endclass
Notice do_unpack calls super.do_unpack(packer), mirroring how do_pack calls super.do_pack(packer). Calling the wrong super method here is an easy copy-paste mistake, and one that won't necessarily throw an error - it will just silently skip whatever the base class's unpack step was supposed to do.
The same set of tests shown above can be run with this class and will yield the same results.
Common Mistakes
Mistake #1: Treating the return value of unpack() as a field value
As shown above, unpack() returns the number of bits it consumed from the array - never a data field from the object. To read a field's value after unpacking, access the field itself (m_pkt.m_addr), not the function's return value.
Mistake #2: Packing and unpacking fields in different orders
Wrong: packing fields in the order m_addr, m_wdata, m_rdata, m_wr but unpacking as m_wdata, m_addr, m_rdata, m_wr
Right: keep the exact same field order on both sides
Why? The packer has no concept of field names once the data is in the array - it is just a stream of bits. If the unpack order doesn't match the pack order, every field after the first mismatch reads the wrong bits.
Mistake #3: Calling super.do_pack() inside do_unpack() (or vice versa)
Each hook must call its own counterpart in the base class - do_pack calls super.do_pack(packer), and do_unpack calls super.do_unpack(packer). Mixing them up compiles cleanly and runs without error, but silently breaks whatever base-class packing logic was supposed to run.
Practice Exercise
Challenge: Predict the Bit Count
A class has three fields: rand bit [7:0] header;, rand bit [15:0] payload;, and rand bit valid;. If you call pack() on an instance of this class, what value would unpack() later return when unpacking that same array?
Hint
Add up the bit widths of every field registered for packing - the return value has nothing to do with the field contents themselves.
Solution
8 + 16 + 1 = 25. unpack() would return 25, since that's the total number of bits packed (and therefore the number read back out), regardless of what header, payload, or valid actually contain.
Summary
In this tutorial, you learned:
pack,pack_bytes, andpack_intsall encode the same bits, differing only in the array element type and any zero-padding needed to reach a clean boundaryunpack,unpack_bytes, andunpack_intsextract those bits back into the object's fields, provided the unpack order matches the original pack order- The return value of every pack/unpack function is the number of bits processed, not a field's value - a detail easy to misread when it happens to match a field by coincidence
do_packanddo_unpackare the manual, user-definable hooks behind the automation macros, each required to call its own matchingsupermethod- Field automation macros are convenient but add overhead; hand-written
do_pack/do_unpackis the better choice for performance-sensitive or large objects