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, and pack_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_pack and do_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:

MethodDescription
packConcatenates the class contents bit by bit into an array of type bit
pack_bytesConcatenates the class contents into an array of type byte, rounding up and zero-padding to the next full byte
pack_intsConcatenates the class contents into an array of type int, rounding up and zero-padding to the next full 32-bit word
unpackExtracts property values from a bit array and stores them into the matching class variables
unpack_bytesExtracts property values from a byte array and stores them into the matching class variables
unpack_intsExtracts 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.

Output
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.

Output
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, and pack_ints all encode the same bits, differing only in the array element type and any zero-padding needed to reach a clean boundary
  • unpack, unpack_bytes, and unpack_ints extract 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_pack and do_unpack are the manual, user-definable hooks behind the automation macros, each required to call its own matching super method
  • Field automation macros are convenient but add overhead; hand-written do_pack/do_unpack is the better choice for performance-sensitive or large objects
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5