SystemVerilog Basics

Chapter 6 of 16

Arrays: Packed vs. Unpacked, Dynamic Arrays, Associative Arrays, and Queues

Meet SystemVerilog's array family: the difference between packed and unpacked arrays, dynamic arrays whose size is only known at runtime, associative arrays indexed by an arbitrary key, and queues that can grow and shrink, plus when to reach for each.

So far we've dealt with single variables or a single struct. But whether it's hardware design (a memory) or verification code (a batch of pending transactions), you almost always need to work with a group of data, not one isolated value. This chapter tours the array types SystemVerilog provides, and when each one fits.

Packed arrays vs. unpacked arrays: the fundamental split

The logic [7:0] from the last chapter is itself already a packed array — 8 bits laid out contiguously, treated as a single 8-bit vector for assignment and arithmetic. A packed array's dimensions go before the variable name:

logic [7:0]        byte_val;        // one 8-bit packed array (a vector)
logic [3:0][7:0]   nibble_bytes;    // 2D packed array: 4 groups of 8 bits, contiguous

An unpacked array's dimensions go after the variable name, representing "a set of independent elements" — closer to the array/memory model you'd expect from software:

logic [7:0] mem [0:255];   // a 256-element memory, each element 8 bits wide
int         scores [4];    // 4 int elements (an equivalent alternate syntax)
 
initial begin
  mem[3]    = 8'hFF;
  scores[0] = 100;
end

mem is a classic way to model a memory (ROM/RAM): each element is a packed 8-bit vector (logic [7:0]), and 256 of those elements form an unpacked array, each independently addressable by index. The two dimension kinds can also combine — logic [7:0] mem [0:255] is exactly that: "an unpacked array whose elements are packed vectors."

Bit-select and part-select: pulling a piece out of a packed vector

A packed vector can also be indexed by individual bits — bit-select vec[i] pulls out a single bit, and part-select vec[msb:lsb] pulls out a contiguous range:

logic [7:0] byte_val;
 
initial begin
  byte_val = 8'hA5;  // binary 1010_0101
 
  $display("bit 0 = %b", byte_val[0]);          // 1
  $display("bit 7 = %b", byte_val[7]);          // 1
  $display("low nibble  = %h", byte_val[3:0]);  // 5
  $display("high nibble = %h", byte_val[7:4]);  // a
end

A part-select's bound order must match how the vector was declared — since byte_val is declared [7:0] (MSB first), a selection must be written high-to-low, like [7:4], not reversed as [4:7]. Bit-select and part-select can also appear on the left side of an assignment, modifying just part of the vector:

byte_val[3:0] = 4'hF;  // only changes the low 4 bits, high 4 bits stay unchanged

This is extremely common in verification code — for example, splitting address and data fields out of a wider bus word:

logic [31:0] bus_word;
logic [15:0] addr, data;
 
initial begin
  bus_word = 32'h1234_5678;
  addr     = bus_word[31:16];  // upper 16 bits
  data     = bus_word[15:0];   // lower 16 bits
end

Dynamic arrays: size decided at runtime

The arrays above have their size fixed at declaration time. Often, especially in verification code, you don't know how big an array needs to be until runtime — that's what dynamic arrays are for:

int dyn_arr[];  // declared, but not yet sized
 
initial begin
  dyn_arr = new[10];        // allocate 10 elements at runtime
  dyn_arr[0] = 42;
  $display("size = %0d", dyn_arr.size());  // 10
 
  dyn_arr = new[20](dyn_arr);  // resize to 20, keeping the existing contents
  $display("size = %0d", dyn_arr.size());  // 20
end

Leave the brackets empty at declaration (int dyn_arr[];), then allocate with new[N]; the .size() method reports the current element count. The new[N](old_array) form resizes while copying over the old array's contents.

Associative arrays: indexed by an arbitrary key, not sequential index

Sometimes the "index" you actually want isn't a contiguous integer at all — maybe not even an integer — like looking something up by transaction ID or by a string name. Associative arrays exist for exactly this:

bit [31:0] expected_data [int];  // key type is int (e.g. a transaction ID)
 
initial begin
  expected_data[101] = 32'hDEAD_BEEF;
  expected_data[205] = 32'hCAFE_BABE;
 
  if (expected_data.exists(101))
    $display("txn 101 expects %0h", expected_data[101]);
 
  expected_data.delete(101);
  $display("size after delete = %0d", expected_data.size());
end

An associative array doesn't need its size declared upfront — assigning a new key simply adds a new entry. The type inside the brackets (int here) determines the key type, and it could just as easily be string or another type. Common methods: .exists(key) checks whether a key is present, .delete(key) removes an entry, .size() reports the current entry count. This shows up constantly in verification code — e.g. recording "expected results" keyed by transaction ID, then looking the table up when the actual result arrives.

Queues: a dynamically growable/shrinkable ordered list

A queue sits between an "array" and a "linked list": indexed sequentially like an array, but able to grow and shrink at both ends like a list:

int q[$];  // declare an int queue
 
initial begin
  q.push_back(1);
  q.push_back(2);
  q.push_front(0);
  $display("q = %p, size = %0d", q, q.size());  // q = '{0, 1, 2}, size = 3
 
  void'(q.pop_front());  // cast to void to explicitly discard the return value on purpose
  $display("q = %p", q);  // q = '{1, 2}
end

Common methods: push_back/push_front insert at the tail/head, pop_back/pop_front remove and return the element at the tail/head, .size() reports the length, and you can also index directly with q[i] like an array. Verification code frequently uses queues to model a "pending transactions" list — first-in-first-out or last-in-first-out, as needed.

Which one do I use?

Array typeSizeIndexed byTypical use
Packed arrayFixed at compile timeBit index, usable as a single vectorA signal/register's bit field
Unpacked array (fixed)Fixed at compile timeContiguous integer indexMemory modeling, a fixed-count set of elements
Dynamic arrayDecided at runtime via new[N]Contiguous integer indexA set of same-typed data whose count depends on runtime input
Associative arrayGrows as needed, no upfront declarationA key of any type (int, string, etc.)Lookup by ID/name, sparse storage
QueueGrows/shrinks dynamicallyContiguous integer index, insert/remove at both endsPending lists, FIFO/LIFO scenarios

Putting it together: a packet queue

Combining last chapter's struct with this chapter's queue gives a pattern that shows up constantly in verification code — a queue representing "a batch of pending packets":

typedef struct {
  int addr;
  int data;
} pkt_t;
 
pkt_t inbox [$];  // a queue of pending packets
 
initial begin
  pkt_t p;
 
  p.addr = 32'h2000;
  p.data = 32'hCAFE_BABE;
  inbox.push_back(p);
 
  $display("queue size = %0d", inbox.size());
  $display("first packet addr = %0h", inbox[0].addr);
 
  void'(inbox.pop_front());
  $display("queue size after pop = %0d", inbox.size());
end

Summary

  • A packed array (dimensions before the name) is contiguous bits usable as a single vector; an unpacked array (dimensions after the name) is a set of independently addressed elements — the two can combine.
  • Bit-select vec[i] pulls out a single bit, part-select vec[msb:lsb] pulls out a contiguous range, and the bound order must match the declaration; both can also appear on the left side of an assignment to modify part of a vector.
  • A dynamic array (int arr[]; plus new[N]) fits "fixed element type, but the count is only known at runtime."
  • An associative array (int arr[key_type];) fits sparse lookups keyed by ID, name, or other non-contiguous keys.
  • A queue (int q[$];) fits an ordered list that needs dynamic insertion/removal at both ends, commonly used for pending-item lists in verification code.

In the declaration logic [7:0] mem [0:255];, what do the 256 and the 8 each correspond to?

Which scenario is the best fit for an associative array instead of a plain fixed-size array?

Which method adds an element to the back of a queue? (lowercase, no parentheses)

After logic [15:0] w;, what does w[15:8] represent?