SYSTEMVERILOG SERIES · SV-04A

SystemVerilog Series — SV-04a: Packed & Unpacked Arrays — VLSI Trainers
SystemVerilog Series · SV-04a

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 KindSizeSyntax hintCovered 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
New terminology: SystemVerilog renames the Verilog-2001 vector width to packed array, and calls dimensions after the variable name an unpacked array. The distinction drives everything else in this article.

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

Fig 1 — Memory layout: packed 8-bit array vs unpacked 8-element array
logic [7:0] data  — packed, 8 bits in one vector
bit 7
7
6
5
4
3
2
1
0
→ one 8-bit integer. Can do: data + 1, data[3:0], data & mask
int arr [8]  — unpacked, 8 separate integers
arr[0]
32-bit integer element
→ individual int
arr[1]
32-bit integer element
→ individual int
arr[2] … arr[7]
Packed arrays can be used in arithmetic expressions like an integer (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
The "before vs after" rule in one sentence: Dimensions written before the variable name compress into a single vector (packed). Dimensions written after the variable name create a collection of separate elements (unpacked). When in doubt, look at which side of the variable name the brackets are on.

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
Fig 2 — bit [3:0][7:0] joe [1:10] — visualising the layout
Unpacked dim [1:10] joe[1] [3] [2] [1] [0] byte[3] → bits[31:24] byte[2] → bits[23:16] byte[1] → bits[15:8] byte[0] → bits[7:0] joe[2] another 32-bit word … (entries 3 through 10) ← packed dims [3:0][7:0] : 32 bits contiguous in each entry →
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
Multiple unpacked dims declared together: When you declare multiple arrays in a single statement, all variables share the same packed dimension spec but can have different unpacked dims: 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.

The rule: The rightmost dimension varies most rapidly. Packed dimensions always vary more rapidly than unpacked dimensions of the same array.

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];
Fig 3 — Variation speed from fastest (left) to slowest (right)
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]);
+: vs -: visual memory aid: Think of the colon as pointing in the direction of counting. 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)
No segfault, but bugs can hide: Unlike C, out-of-bounds reads in SV never crash the simulator — they silently return the type’s default value. This makes them dangerous: a loop bug or off-by-one that writes to 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

FunctionReturnsExample (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
Dimension numbering: Dimension 1 is the slowest-varying (outermost). Dimension 2 is the next fastest, and so on. For the declaration 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

 PackedUnpacked
Dimension placementBefore variable nameAfter variable name
StorageContiguous bit vectorCollection of separate elements
Usable as integer?Yes — arithmetic, bitwise opsNo — element access only
Part-select?Yes — [7:0]No (only slice of elements)
Slice of elements?NoYes — arr[2:4]
Element types allowedSingle-bit types onlyAny type
Signed flag applies to?Whole vectorIndividual elements

Indexing syntax cheatsheet

Access formSyntaxWhat you get
Single elementarr[i]One element of the array
Packed part-selectvec[7:0]Bits 7 down to 0 — always unsigned result
Unpacked slicearr[2:4]Elements 2, 3, 4 as a 3-element sub-array
Variable ascendingvec[j +: 8]Bits j through j+7 (width 8, constant)
Variable descendingvec[j -: 8]Bits j down to j-7 (width 8, constant)
Multi-dim elementarr[r][c]Element at row r, column c

Array querying cheatsheet

FunctionMeaning
$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
Coming next: The next article covers Dynamic arrays, Associative arrays, and Queues — the three variable-size array kinds introduced by SystemVerilog, along with their methods and manipulation functions.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top