Packed & Unpacked Arrays
How arrays work in SystemVerilog — the packed vs unpacked distinction, multi-dimensional layout, indexing and slicing, and array querying functions — with detailed diagrams and worked examples at every step.
💡 Introduction & Array Types in SV
In Verilog-2001, all arrays were fixed-size. The dimension before the variable name set the vector width, and dimensions after the name set the array size. SystemVerilog keeps that model but gives it proper names and adds three new array kinds.
| Array Kind | Size | Syntax hint | Covered in |
|---|---|---|---|
| Fixed-size (packed/unpacked) | Compile-time constant | int a[8]; |
This article |
| Dynamic array | Set at runtime with new[] |
int a[]; |
SV-04b |
| Associative array | Grows per entry written | int a[string]; |
SV-04b |
| Queue | Grows/shrinks at runtime | int a[$]; |
SV-04b |
🔁 Packed vs Unpacked — The Core Distinction
The single most important concept in SV arrays: where you place the dimension declaration relative to the variable name determines whether it is packed or unpacked.
Packed — dimensions BEFORE the name
// type [dim] name; logic [7:0] data; bit [3:0] nibble; logic [31:0] word;
Unpacked — dimensions AFTER the name
// type name [dim]; int arr [8]; real vals [0:3]; logic[7:0] mem [0:15];
What this means in practice
A packed array is stored as a single contiguous bit vector. You can do arithmetic on the whole thing, take part-selects, and assign it to other integers. An unpacked array is a collection of separate elements — you work element-by-element.
data + 1 is legal). Unpacked arrays cannot — you must operate element by element (arr[0] + 1).
Side-by-side: what you can and cannot do
logic [7:0] packed8 = 8'hAB; int unpacked8[8]; // ── Packed: the whole array is an integer ───────────────── int sum = packed8 + 1; // OK — arithmetic on the vector bit hi4 = packed8[7:4]; // OK — part-select (4-bit result) logic bit3 = packed8[3]; // OK — single-bit select packed8[3:0] = 4'hF; // OK — write lower nibble // ── Unpacked: element-by-element only ───────────────────── unpacked8[0] = 42; // OK — write one element int a[8], b[8]; a = b; // OK — whole-array copy a[2:4] = b[2:4]; // OK — slice of unpacked // int x = unpacked8 + 1; // ERROR — unpacked is not an integer
✅ What Can Go Inside Each Array Type
The element type restrictions are different for packed and unpacked arrays.
Packed — element type restrictions
- ✓
bit,logic,reg - ✓ Any net type (
wire,tri…) - ✓ Other packed arrays
- ✓ Packed structs
- ✗
real,shortreal - ✗ Unpacked arrays nested inside
- ✗ Class handles, strings, events
Unpacked — element type: anything
- ✓ Any type:
int,byte,real - ✓
string,event - ✓ Class object handles
- ✓ Structs (packed or unpacked)
- ✓ Other unpacked arrays (nested)
- ✓ Packed arrays as elements
// Legal packed arrays — single-bit types only bit [7:0] a; // 8-bit packed bit vector logic [31:0] b; // 32-bit packed logic vector bit [3:0][7:0] c; // 4×8 = 32-bit packed 2D vector // Predefined-width types already occupy one packed dimension byte d; // same as bit signed [7:0] d int e; // same as logic signed [31:0] e longint f; // same as logic signed [63:0] f // Legal unpacked arrays — any element type real coeffs [8]; // 8 real values string names [0:3]; // 4 strings int matrix [4][4]; // 4×4 array of int // ILLEGAL — real cannot be packed // real [7:0] bad_arr; // Compile error
Signed vs Unsigned in Packed Arrays
// If the packed array is declared signed, arithmetic is signed bit signed [7:0] s8 = 8'hFF; // s8 = -1 (signed) int ext = s8; // ext = -1 (sign-extended to 32 bits) // But a PART-SELECT is always unsigned, even from a signed array int part = s8[3:0]; // part = 15 (unsigned 4-bit) // Default signing per type: // bit, logic, reg → unsigned by default // byte, shortint, int, longint, integer → signed by default
↦ Multiple Dimensions
An array can have both packed and unpacked dimensions at the same time. The golden rule is: packed dimensions always appear before the variable name; unpacked dimensions always appear after it.
Building up step by step
// Step 1: one packed dim (vector of bits) logic [7:0] byte1; // 8-bit packed vector // Step 2: one unpacked dim (array of bytes) logic [7:0] mem [0:15]; // 16 elements, each an 8-bit vector // Step 3: two packed dims (array of bit-vectors) bit [3:0][7:0] word32; // 4×8 = 32-bit packed vector // Step 4: packed + unpacked (array of 32-bit words) bit [3:0][7:0] joe [1:10]; // 10 entries, each a 32-bit packed word
joe[9] — selects one entire 32-bit word (the 9th entry) •
joe[9][3] — selects byte 3 within that word (bits 31:24) •
joe[9][3][5:2] — 4-bit part-select within that byte
Reading the access notation
bit [3:0][7:0] joe [1:10]; // Unpacked access first, then packed joe[9] // entire entry 9 → 32-bit packed word joe[9][2] // byte 2 of entry 9 → 8-bit packed slice joe[9][2][5:2] // bits 5:2 of byte 2 of entry 9 → 4-bit // Arithmetic on the packed dimension joe[9] = joe[8] + 1; // 32-bit add (joe[8] is a 32-bit int) joe[7][3:2] = joe[6][1:0]; // copy 2 bytes
C-style shorthand for unpacked size
// [N] is shorthand for [0:N-1] — just like C arrays int A[8]; // same as int A[0:7] int B[4][8]; // same as int B[0:3][0:7] // These are equivalent: int C[8][32]; int D[0:7][0:31];
Using typedef to build multi-dimensional types
// Build in stages — easier to read and reuse typedef bit [7:0] byte_t; // 8-bit packed type typedef byte_t row_t [0:3]; // row: 4 bytes typedef row_t frame_t[0:7]; // frame: 8 rows of 4 bytes frame_t frame; // 8×4 = 32 bytes frame[3][2] = 8'hAB; // row 3, byte 2 // Alternatively, multi-dim typedefs with packed dims typedef bit [1:5] bsix; // 5-bit packed type bsix [1:10] foo5; // 10 × 5 = 50-bit packed array
bit [7:0][31:0] foo7 [1:5][1:10], foo8 [0:255];
Both foo7 and foo8 have the packed spec [7:0][31:0], but different unpacked extents.
🔄 Dimension Variation Order
When you access an array element, understanding which dimension varies fastest (the most tightly coupled to storage) is essential for writing correct bit-manipulation code.
Worked example — four array declarations
// foo1: packed bit, unpacked row — bit varies fastest (Verilog style) bit [1:10] foo1 [1:5]; // foo2: all unpacked — rightmost index [1:10] varies fastest (C style) bit foo2 [1:5][1:10]; // foo3: all packed — [1:10] varies fastest bit [1:5][1:10] foo3; // foo4: mixed — [1:6] varies fastest (packed), then [1:5], // then [1:8], then slowest [1:7] (unpacked) bit [1:5][1:6] foo4 [1:7][1:8];
foo1: bit [1:10] foo1 [1:5];
Fastest → slowest: [1:10] then [1:5]
Access: foo1[row][bit] e.g. foo1[3][7]
foo2: bit foo2 [1:5][1:10];
Fastest → slowest: [1:10] then [1:5] (same order, all unpacked)
Access: foo2[row][col]
foo4: bit [1:5][1:6] foo4 [1:7][1:8];
Storage order (fastest to slowest):
packed[1:6] → packed[1:5] → unpacked[1:8] → unpacked[1:7]
Access: foo4[slow][fast_unp][fast_pk][fastest]
e.g. foo4[2][3][4][5]
Why this matters: walking a 2-D array efficiently
int mat[4][8]; // 4 rows, 8 columns — [8] varies fastest // EFFICIENT — inner loop over fastest-varying dimension for(int r=0; r<4; r++) for(int c=0; c<8; c++) mat[r][c] = r * 8 + c; // Using foreach (SV idiom — handles any dimension count automatically) foreach(mat[r,c]) mat[r][c] = r * 8 + c;
✂ Indexing & Slicing
Three levels of access are available: single-element selection, part-select of a packed dimension (range of bits), and slice of an unpacked dimension (range of elements). A fourth form — the variable part-select — lets you use a computed start position with a constant width.
1. Single element
int arr[8]; // unpacked byte val = arr[3]; // element 3 — one int bit [3:0][7:0] j; // packed 2-D byte k = j[2]; // select packed dimension 2 → 8-bit byte bit b5 = j[1][5]; // bit 5 of packed dimension 1
2. Part-select of a packed dimension (bits)
logic [31:0] word = 32'hDEAD_BEEF; word[7:0] // lower byte → 8'hEF word[15:8] // second byte → 8'hBE word[31:16] // upper halfword → 16'hDEAD // Part-selects are always unsigned (even if word is signed) bit signed [7:0] s = -1; // 8'hFF int ps = s[7:4]; // ps = 15, NOT -1
3. Slice of an unpacked dimension (elements)
// SV adds slices of UNPACKED arrays — this is new vs Verilog-2001 int busA [0:7]; // 8-element unpacked array int busB [0:2]; busB = busA[2:4]; // slice: copy elements 2, 3, 4 to busB[0:2] // Size must be constant; start position can be variable int pos = 3; busB = busA[pos:pos+2]; // OK — pos+2 is a const expression at runtime
4. Variable part-select (+: and -:)
The position is variable, but the width must be a constant. +: counts upward from the start; -: counts downward.
logic [31:0] data; int j = 8; // j +: 8 means: start at j, take 8 bits UPWARD → bits 15:8 logic [7:0] byte1 = data[j +: 8]; // bits [15:8] // j -: 8 means: start at j, take 8 bits DOWNWARD → bits 8:1 logic [7:0] byte2 = data[j -: 8]; // bits [8:1] // Common pattern: extract byte N from a word (N computed at runtime) function automatic byte get_byte(logic[31:0] w, int n); return w[n*8 +: 8]; // byte n — width always 8 (constant) endfunction // Loop over all 4 bytes of a word for(int i=0; i<4; i++) $display("byte[%0d] = %02h", i, data[i*8 +: 8]);
j +: 8 — the + means “count up from j” → selects bits j, j+1, … j+7.
j -: 8 — the - means “count down from j” → selects bits j, j-1, … j-7.
The result is always the same width (8 bits here), only the start changes.
What happens with an invalid index?
int arr[0:7]; // Out-of-bounds read → returns type default value, NO crash int x = arr[10]; // x = 'X for 4-state, '0 for 2-state // Out-of-bounds write → silently ignored arr[20] = 99; // no error, no effect — tool may warn // X or Z index → also treated as invalid logic [3:0] idx = 4'bx; int y = arr[idx]; // y = default value (not the element)
arr[-1] or arr[100] produces no error, but the data is silently lost. Always check bounds in synthesisable RTL and add assertions in testbenches.
Slices and array concatenation combined
int a[5:1]; int b[3:1]; int e = 99; // Build a new 5-element unpacked array by combining slices a = {b[3 -: 2], e}; // 2 elements from b (descending) + e = 3 elements // must match a's size exactly // Useful pattern: shift a pipeline register int pipeline[0:3]; pipeline = {pipeline[1:3], new_data}; // shift left, insert at end
🔎 Array Querying Functions
SystemVerilog provides seven built-in system functions that return metadata about any array dimension. These are especially valuable inside parameterised modules and testbench utility functions where the exact array bounds are not known at the point of use.
The seven functions
| Function | Returns | Example (for int arr[2:9]) |
|---|---|---|
| $left(arr, N) | Left bound of dim N | $left(arr,1) → 2 |
| $right(arr, N) | Right bound of dim N | $right(arr,1) → 9 |
| $low(arr, N) | Minimum of left/right | $low(arr,1) → 2 |
| $high(arr, N) | Maximum of left/right | $high(arr,1) → 9 |
| $increment(arr, N) | 1 if left≥right, -1 otherwise | $increment(arr,1) → -1 (2<9) |
| $size(arr, N) | Number of elements in dim N | $size(arr,1) → 8 |
| $dimensions(arr) | Total number of dimensions | $dimensions(arr) → 1 |
int m[0:3][0:7], dimension 1 is the [0:3] axis and dimension 2 is the [0:7] axis. If the dimension argument is omitted, dimension 1 is used.
Worked examples
// Example 1: simple 1-D array int arr[2:9]; $display($left(arr,1)); // 2 $display($right(arr,1)); // 9 $display($size(arr,1)); // 8 $display($dimensions(arr)); // 1 // Example 2: 2-D array int mat[0:3][0:7]; $display($size(mat,1)); // 4 (rows) $display($size(mat,2)); // 8 (columns) $display($dimensions(mat)); // 2 // Example 3: reversed-range array int rev[7:0]; // left=7, right=0 $display($low(rev,1)); // 0 (min regardless of left/right) $display($high(rev,1)); // 7 (max) $display($increment(rev,1)); // 1 (left >= right → incrementing)
Using $size in generic / parameterised code
// A utility task that works for ANY 1-D int array, any size task automatic print_array(input int arr[]); for(int i=0; i<$size(arr,1); i++) $display("arr[%0d] = %0d", i, arr[i]); endtask // Parameterised module — don't hardcode sizes, query them module zero_fill #(parameter int ROWS=4, COLS=8) ( output int mem[0:ROWS-1][0:COLS-1] ); initial for(int r=0; r<$size(mem,1); r++) for(int c=0; c<$size(mem,2); c++) mem[r][c] = 0; endmodule
$increment tells you which direction to loop
// Safely iterate an array regardless of which way its range runs task automatic safe_iterate(input int arr[]); int lo = $low(arr,1); int hi = $high(arr,1); for(int i=lo; i<=hi; i++) $display("%0d", arr[i]); endtask // Works for both arr[2:9] and arr[9:2] // In practice, foreach is the simplest: // foreach(arr[i]) $display("%0d", arr[i]);
📋 Quick Reference
Packed vs unpacked at a glance
| Packed | Unpacked | |
|---|---|---|
| Dimension placement | Before variable name | After variable name |
| Storage | Contiguous bit vector | Collection of separate elements |
| Usable as integer? | Yes — arithmetic, bitwise ops | No — element access only |
| Part-select? | Yes — [7:0] | No (only slice of elements) |
| Slice of elements? | No | Yes — arr[2:4] |
| Element types allowed | Single-bit types only | Any type |
| Signed flag applies to? | Whole vector | Individual elements |
Indexing syntax cheatsheet
| Access form | Syntax | What you get |
|---|---|---|
| Single element | arr[i] | One element of the array |
| Packed part-select | vec[7:0] | Bits 7 down to 0 — always unsigned result |
| Unpacked slice | arr[2:4] | Elements 2, 3, 4 as a 3-element sub-array |
| Variable ascending | vec[j +: 8] | Bits j through j+7 (width 8, constant) |
| Variable descending | vec[j -: 8] | Bits j down to j-7 (width 8, constant) |
| Multi-dim element | arr[r][c] | Element at row r, column c |
Array querying cheatsheet
| Function | Meaning |
|---|---|
| $size(arr, N) | Element count in dimension N (most common) |
| $dimensions(arr) | Total number of dimensions |
| $left(arr, N) | Left bound as declared |
| $right(arr, N) | Right bound as declared |
| $low(arr, N) | Lower bound (min of left/right) |
| $high(arr, N) | Upper bound (max of left/right) |
| $increment(arr, N) | +1 if left≥right, -1 if left<right |
