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, contiguousAn 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;
endmem 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
endA 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 unchangedThis 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
endDynamic 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
endLeave 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());
endAn 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}
endCommon 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 type | Size | Indexed by | Typical use |
|---|---|---|---|
| Packed array | Fixed at compile time | Bit index, usable as a single vector | A signal/register's bit field |
| Unpacked array (fixed) | Fixed at compile time | Contiguous integer index | Memory modeling, a fixed-count set of elements |
| Dynamic array | Decided at runtime via new[N] | Contiguous integer index | A set of same-typed data whose count depends on runtime input |
| Associative array | Grows as needed, no upfront declaration | A key of any type (int, string, etc.) | Lookup by ID/name, sparse storage |
| Queue | Grows/shrinks dynamically | Contiguous integer index, insert/remove at both ends | Pending 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());
endSummary
- 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-selectvec[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[];plusnew[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?