Skip to content

SystemVerilog · Module 3

Fixed-Size Arrays

Packed vs unpacked, multi-dimensional, initialization, foreach, system functions.

Module 3 · Page 3.1

The Array Type You Use Without Thinking — Until You Can't

Fixed-size arrays are everywhere. The 256-deep register file in your APB peripheral? Static array. The 64-entry scoreboard lookup table in your testbench? Static array. The 8-byte packet buffer in your AXI driver? Static array. You use them constantly because they require no allocation, no deallocation, no size management — the compiler handles all of that at elaboration time.

But "simple" doesn't mean "trivial." SystemVerilog's array model has two orthogonal dimensions: packed and unpacked. That distinction determines whether your array is treated as a contiguous vector of bits or as a collection of independent objects — and it directly affects how the array connects to module ports, how it synthesizes, and what operators apply to it. Mixing them up silently produces the wrong hardware, and the tools don't always warn you.

The other issue: fixed-size arrays in SystemVerilog are initialized to X in simulation, not to zero. Every verification engineer who writes logic [7:0] buffer [64]; and immediately reads from it will get X on the first access. In C, uninitialized arrays are undefined behavior. In SV, they're explicit X — which propagates, poisons downstream logic, and produces the kind of waveform that takes a junior engineer an hour to trace.

Two Dimensions You Must Understand: Packed and Unpacked

SystemVerilog arrays have two independent dimension categories, and they stack on each other. Packed dimensions sit to the left of the variable name and define the bit-width of each element. Unpacked dimensions sit to the right of the variable name and define how many of those elements exist. A declaration like logic [7:0] mem [256] reads as: "256 elements (unpacked), each element is an 8-bit vector (packed)."

Packed Arrays — Contiguous Bit Vectors

A packed array is just a wider signal. logic [31:0] word is a packed 32-bit array — the dimensions describe bit positions within a single contiguous block of memory. You can apply any bitwise operator, slice any range, use it in arithmetic and comparisons. Synthesis tools have been handling packed arrays since Verilog-1995 — they map directly to wires and registers.

Unpacked Arrays — Collections of Objects

An unpacked array is a collection. logic [7:0] buf [4] is four separate 8-bit signals arranged in a named group. You index into it with buf[0], buf[1], etc. You can copy entire unpacked arrays with a single assignment, compare them with == and !=, and pass them to tasks and functions. But you cannot take bit-slices across element boundaries — buf[1:0] is an element slice (two elements), not a 16-bit concatenation of buf[0] and buf[1].

Packed Array

Contiguous bits. Treated as one wide vector. Supports all bit operators, slicing, arithmetic. Maps directly to RTL wires/regs. logic [N-1:0] name

Unpacked Array

Collection of elements. Indexed separately. Supports whole-array copy and comparison. Cannot bit-slice across elements. type name [N]

Packed + Unpacked

Each element is a packed vector. Most common form in RTL/TB. logic [7:0] mem [256] = 256 bytes of memory.

Multi-Dimensional

Packed dimensions stack left, unpacked stack right. logic [7:0] m[4][4] = 4×4 matrix of bytes.

Syntax Reference — Every Form You'll Use

SystemVerilog — Fixed-Size Array Syntax
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── PACKED ARRAY (single wide vector) ────────────────────────────
logic [31:0] word;              // 32-bit packed: one contiguous signal
logic [7:0] byte_val;           // 8-bit packed: standard byte
 
// ── UNPACKED ARRAY (N elements, each of given type) ───────────────
logic [7:0] mem    [256];       // 256 elements × 8-bit  (0..255)
logic [7:0] fifo   [0:63];      // 64 elements  × 8-bit  (explicit range)
int           scores [16];       // 16 integers (signed 32-bit each)
 
// ── MULTI-DIMENSIONAL UNPACKED ────────────────────────────────────
logic [7:0] matrix [4][4];      // 4 rows × 4 cols of bytes
logic        bitmask[8][8];      // 8×8 array of single bits
 
// ── PACKED MULTI-DIMENSIONAL ──────────────────────────────────────
logic [3:0][7:0] reg_file;      // one 32-bit signal (4 packed bytes)
// reg_file[3] = bits [31:24], reg_file[0] = bits [7:0]
 
// ── INITIALIZATION AT DECLARATION ────────────────────────────────
int           defaults[4] = '{0, 1, 2, 3};       // array literal
logic [7:0] cleared [4] = '{4{8'h00}};          // all zeros
logic [7:0] preset  [3] = '{8'hAA, 8'hBB, 8'hCC};
 
// ── ACCESS ────────────────────────────────────────────────────────
mem[0]      = 8'hFF;            // write element 0
matrix[2][3] = 8'hAB;           // write row 2, col 3
 
// ── SYSTEM FUNCTIONS ─────────────────────────────────────────────
$size(mem)         // = 256   (total elements in first dimension)
$size(matrix, 1)   // = 4     (size of dimension 1 = rows)
$size(matrix, 2)   // = 4     (size of dimension 2 = cols)
$dimensions(mem)   // = 1     (one unpacked dimension)
$low(fifo)          // = 0     (lowest index)
$high(fifo)         // = 63    (highest index)
$left(mem)          // = 0     (leftmost declared index)
$right(mem)         // = 255   (rightmost declared index)
DeclarationPacked bitsUnpacked elementsTotal bitsAccess form
logic [31:0] w32132w, w[7:0]
logic [7:0] m[4]8432m[0], m[3][5]
logic [3:0][7:0] r32 (packed)132r, r[2], r[2][3:0]
logic [7:0] x[4][4]84×4 = 16128x[0][0], x[3][3]
int arr[8]32 (int)8256arr[0] to arr[7]

Memory Layout — Visualizing Packed vs Unpacked

Unpacked Array Memory Model

Declaration: logic [7:0] buf [4] — 4 independent 8-bit elements. Each element has its own storage. You access them by index, not by bit position across the whole array.

IndexVariableBit 7Bit 6Bit 5Bit 4Bit 3Bit 2Bit 1Bit 0Value
0buf[0]101010118'hAB
1buf[1]110011018'hCD
2buf[2]111011118'hEF
3buf[3]000000018'h01

Packed Multi-Dimensional Array — Bit Mapping

Declaration: logic [3:0][7:0] reg_file — 4 packed bytes forming one 32-bit signal. The leftmost packed dimension selects the byte; the rightmost selects the bit within that byte.

SelectorBit range in 32-bit wordAccessExample value
reg_file[3][31:24] — MSB byte8-bit slice of the packed vector8'hDE
reg_file[2][23:16]8-bit slice8'hAD
reg_file[1][15:8]8-bit slice8'hBE
reg_file[0][7:0] — LSB byte8-bit slice8'hEF
reg_file[31:0] — whole wordFull 32-bit packed value32'hDEADBEEF → use 32'hCAFEBABE instead

Multi-Dimensional Unpacked — Addressing Matrix

Declaration: logic [7:0] cache [4][8] — 4 rows, 8 columns, each cell holds a byte. Accessing row 2, column 5: cache[2][5]

Row \ Col[0][1][2][3][4][5][6][7]
cache[0]8'h008'h018'h028'h038'h048'h058'h068'h07
cache[1]8'h108'h118'h128'h138'h148'h158'h168'h17
cache[2]8'h208'h218'h228'h238'h248'h258'h268'h27
cache[3]8'h308'h318'h328'h338'h348'h358'h368'h37

Green cell: cache[2][5] = 8'h25

Code Examples — From Basics to Real Verification

Example 1 — Beginner: Declare, Write, Read, Iterate

Example 1 — Static Array Basics
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_array_basics;
 
  logic [7:0] data [8];          // 8-element byte array
  int           total;
  int           i;
 
  initial begin
 
    // ── Initialize all elements ──────────────────────────────────
    foreach (data[i])
      data[i] = i * 16;           // 0x00, 0x10, 0x20, ... 0x70
 
    // ── Read and display ─────────────────────────────────────────
    $display("Array contents:");
    foreach (data[i])
      $display("  data[%0d] = 0x%02h", i, data[i]);
 
    // ── System functions ─────────────────────────────────────────
    $display("$size(data)       = %0d", $size(data));        // 8
    $display("$low(data)        = %0d", $low(data));         // 0
    $display("$high(data)       = %0d", $high(data));        // 7
    $display("$dimensions(data) = %0d", $dimensions(data));  // 1
 
    // ── Whole-array copy ─────────────────────────────────────────
    logic [7:0] copy [8];
    copy = data;                   // single statement — copies all 8 elements
    $display("copy[3] = 0x%02h  (should be 0x30)", copy[3]);
 
    // ── Whole-array comparison ───────────────────────────────────
    if (copy == data)
      $display("copy and data are identical");
    data[0] = 8'hFF;
    if (copy != data)
      $display("copy and data now differ at [0]");
 
    // ── Sum using a loop ─────────────────────────────────────────
    total = 0;
    foreach (data[i]) total += data[i];
    $display("Sum = %0d", total);
 
    $finish;
  end
 
endmodule

Expected output:

Simulation Output
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Array contents:
  data[0] = 0x00
  data[1] = 0x10
  data[2] = 0x20
  data[3] = 0x30
  data[4] = 0x40
  data[5] = 0x50
  data[6] = 0x60
  data[7] = 0x70
$size(data)       = 8
$low(data)        = 0
$high(data)       = 7
$dimensions(data) = 1
copy[3] = 0x30  (should be 0x30)
copy and data are identical
copy and data now differ at [0]
Sum = 376

Example 2 — Intermediate: Register File Model (RTL-Style)

Example 2 — 32-Entry Register File
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Simplified 32-entry, 32-bit register file — common in RISC-V style cores
module reg_file (
  input  logic        clk,
  input  logic        we,          // write enable
  input  logic [4:0]  wr_addr,      // 5-bit = 32 entries
  input  logic [31:0] wr_data,
  input  logic [4:0]  rd_addr,
  output logic [31:0] rd_data
);
 
  logic [31:0] regs [0:31];          // 32 × 32-bit static array
 
  // Synchronous write
  always_ff @(posedge clk)
    if (we && wr_addr != 5'b0)       // reg[0] is hardwired to 0 in RISC-V
      regs[wr_addr] <= wr_data;
 
  // Asynchronous read (combinational)
  assign rd_data = (rd_addr == 5'b0) ? 32'h0 : regs[rd_addr];
 
endmodule
 
// Testbench
module tb_reg_file;
  logic        clk = 0;
  logic        we;
  logic [4:0]  wr_addr, rd_addr;
  logic [31:0] wr_data, rd_data;
 
  reg_file dut(.*);
  always #5 clk = ~clk;
 
  initial begin
    we = 1; wr_addr = 5'd5; wr_data = 32'hCAFE_BABE;
    @(posedge clk); #1;
 
    we = 0; rd_addr = 5'd5;
    #1;
    $display("reg[5] = 0x%08h  (expect CAFEBABE)", rd_data);
 
    // reg[0] always reads 0
    rd_addr = 5'd0; #1;
    $display("reg[0] = 0x%08h  (expect 00000000)", rd_data);
    $finish;
  end
endmodule

Example 3 — Verification: Packet Buffer with Integrity Check

Example 3 — TB Packet Buffer Using Static Array
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_packet_buffer;
 
  parameter int MAX_PKT = 8;
  parameter int PKT_LEN = 16;    // bytes per packet
 
  typedef logic [7:0] byte_t;
  typedef byte_t pkt_t [PKT_LEN];  // one packet = 16-byte array
 
  pkt_t  send_buf [MAX_PKT];       // buffer of 8 packets
  pkt_t  recv_buf [MAX_PKT];       // received copy
  int    mismatch_cnt;
  int    i, j;
 
  // Build expected packets (fill with known pattern)
  task automatic build_packets();
    foreach (send_buf[i, j])
      send_buf[i][j] = (i * PKT_LEN + j) & 8'hFF;
  endtask
 
  // Simulate DUT output (correct + one injected error)
  task automatic simulate_recv();
    recv_buf = send_buf;            // whole-array copy
    recv_buf[3][7] = 8'hFF;       // inject error: pkt 3, byte 7
  endtask
 
  // Compare and report
  task automatic check_packets();
    mismatch_cnt = 0;
    foreach (send_buf[i]) begin
      if (send_buf[i] != recv_buf[i]) begin   // compare entire packet
        foreach (send_buf[i, j]) begin
          if (send_buf[i][j] !== recv_buf[i][j]) begin
            $error("PKT[%0d][%0d]: exp=0x%02h got=0x%02h",
                   i, j, send_buf[i][j], recv_buf[i][j]);
            mismatch_cnt++;
          end
        end
      end
    end
    if (mismatch_cnt == 0)
      $display("PASS: all %0d packets match", MAX_PKT);
    else
      $display("FAIL: %0d mismatches detected", mismatch_cnt);
  endtask
 
  initial begin
    build_packets();
    simulate_recv();
    check_packets();
    $finish;
  end
 
endmodule

Expected output:

Simulation Output
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ERROR: PKT[3][7]: exp=0x37 got=0xFF
FAIL: 1 mismatches detected

Example 4 — Corner Case: Initialization, Out-of-Bounds, Packed Access

Example 4 — Corner Cases
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_corner_cases;
 
  logic [7:0] uninitialized [4];   // not initialized — will be X
  logic [7:0] packed_access [4];
 
  initial begin
 
    // ── Uninitialized = X ─────────────────────────────────────────
    $display("uninitialized[0] = %02h", uninitialized[0]);  // xx
    $display("uninitialized[0] === 8'hxx: %0b",
             uninitialized[0] === 8'hxx);               // 1
 
    // ── Out-of-bounds: no error, returns X ───────────────────────
    logic [7:0] uninitialized [4];
    // Accessing index 5 in a 4-element array (valid indices: 0-3)
    $display("out-of-bounds [5] = %02h", uninitialized[5]); // xx
    // Simulation does NOT throw an error — it just returns X
 
    // ── Packed slice on unpacked element ─────────────────────────
    packed_access[0] = 8'hAB;
    $display("lower nibble: %h", packed_access[0][3:0]);   // B
    $display("bit 7:        %b", packed_access[0][7]);     // 1
 
    // ── Array literal assignment ──────────────────────────────────
    packed_access = '{8'hAA, 8'hBB, 8'hCC, 8'hDD};
    $display("[0]=%02h [1]=%02h [2]=%02h [3]=%02h",
             packed_access[0], packed_access[1],
             packed_access[2], packed_access[3]);
    // Note: '{} assigns index 0 first (leftmost = index 0)
 
    // ── Default value in literal ──────────────────────────────────
    packed_access = '{default: 8'hFF};   // fill all elements with FF
    $display("after default fill: [2]=%02h", packed_access[2]); // FF
 
    $finish;
  end
 
endmodule

Expected output:

Simulation Output
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
uninitialized[0] = xx
uninitialized[0] === 8'hxx: 1
out-of-bounds [5] = xx
lower nibble: b
bit 7:        1
[0]=AA [1]=BB [2]=CC [3]=DD
after default fill: [2]=FF

Simulation Behavior — What the Simulator Actually Does

Initialization: X is the Truth

Every logic-type static array — whether module-level or in a static task/function — starts simulation at X. Not 0, not Z: X. This is intentional. SystemVerilog's four-state model uses X to represent "unknown initialization state," which is exactly what you have before any reset sequence drives the values.

The practical consequence: if your testbench initializes a reference model array with logic [7:0] ref_data [256] and then immediately compares it against DUT output, you will see mismatches everywhere because X !== 8'h00 is always true. Always initialize explicitly — either element-by-element in an initial block or via array literals at declaration.

Out-of-Bounds Access Behavior

ScenarioSimulation resultSynthesis resultWhat to do
Read index > $high (static index)Returns X, no error by defaultElaboration warning or errorAdd bound check: if (i < $size(arr))
Read index > $high (runtime index)Returns X silentlyTool-dependent — may generate extra mux logicAssert bounds at runtime
Write index > $highIgnored silently (no write occurs)Elaboration errorGuard writes with bounds check
Negative index on unsigned arrayX (unsigned wrap-around)ErrorDeclare index as int and guard for negative

Synthesis Implications

UsageSynthesized hardwareNotes
logic [7:0] r [4] in always_ff4 × 8-bit flip-flop registersStandard register array — synthesizes cleanly
Constant index accessDirect wire to specific register bitsZero extra logic
Variable index access (read)Mux tree selecting from all elementsTiming-critical; depth grows with array size
Variable index access (write)Demux — enable decode for each elementArea grows linearly with array size
Packed array [N-1:0]Single wide register or wireNo mux overhead

Where Fixed-Size Arrays Show Up in Real Verification Work

Verification Patterns Using Fixed-Size Arrays
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 1. SCOREBOARD: expected vs received transaction buffer ────────
logic [31:0] exp_data  [16];    // expected DUT output (pre-computed)
logic [31:0] got_data  [16];    // captured DUT output
 
if (got_data !== exp_data)       // whole-array !== catches X mismatches too
  $error("Buffer mismatch detected");
 
// ── 2. MONITOR: byte capture buffer for incoming AXI beat ─────────
logic [7:0] beat_bytes [64];    // 64-byte max beat
int           byte_count = 0;
 
// In the monitor's capture loop:
// beat_bytes[byte_count++] = captured_byte;
 
// ── 3. DRIVER: pre-loaded stimulus table ──────────────────────────
logic [31:0] stim_table [256] = '{default: 32'h0};
// Load from file at sim start:
// $readmemh("stim.hex", stim_table);
 
// ── 4. CONSTRAINT: fixed-size array inside rand class ─────────────
class axi_burst_txn;
  rand logic [7:0]  data [16];   // 16-byte fixed burst
  rand logic [7:0]  strb [16];   // strobe per byte
 
  constraint valid_strb {
    foreach (strb[i])
      strb[i] inside {8'h00, 8'hFF};   // either all-masked or all-valid
  }
endclass
 
// ── 5. SVA ASSERTION: check stable array contents ─────────────────
// property p_stable_lut;
//   @(posedge clk) !update_en |=> lut_array == $past(lut_array);
// endproperty
// assert property (p_stable_lut);
 
// ── 6. $readmemh / $writememh for file-based initialization ───────
logic [31:0] rom [1024];
initial
  $readmemh("rom_init.hex", rom);  // loads hex values from file into array
// rom_init.hex format: one hex value per line, e.g.:
// CAFEBABE
// 00000001
// ...

Bugs Engineers Actually Hit With Static Arrays

Bug 1 — Reading Before Initialization: Everything is X

Bug 1 — Uninitialized Array Read
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
logic [31:0] ref_model [64];     // BUGGY: no initialization
 
initial begin
  // DUT just completed — compare against reference model
  if (dut_output !== ref_model[0])   // ref_model[0] is X — always mismatches!
    $error("MISMATCH at [0]");            // fires even when DUT is correct
end
 
// FIXED: initialize before use
logic [31:0] ref_model [64] = '{default: 32'h0};  // all zeros
// OR load from file:
initial $readmemh("expected.hex", ref_model);

Bug 2 — Wrong Array Literal Syntax: {} vs '{}

Bug 2 — Concatenation vs Array Literal
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
logic [7:0] buf [4];
 
// BUGGY: { } is concatenation, not array literal assignment
buf = {8'hAA, 8'hBB, 8'hCC, 8'hDD};  // COMPILE ERROR or type mismatch
// Concatenation produces a packed 32-bit value, not an unpacked array
 
// FIXED: use '{ } for unpacked array literals
buf = '{8'hAA, 8'hBB, 8'hCC, 8'hDD};   // CORRECT: array literal
 
// Also valid for default fill:
buf = '{default: 8'hFF};                  // fills all 4 elements with 0xFF
 
// And for partial init with default:
buf = '{8'h01, 8'h02, default: 8'h00};   // [0]=01, [1]=02, [2]=00, [3]=00

Bug 3 — Packed vs Unpacked Port Mismatch

Bug 3 — Connecting Wrong Array Types at Port
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Module declares an unpacked port
module dut (input logic [7:0] data_in [4]);   // unpacked [4]
  // ...
endmodule
 
module tb;
  logic [31:0] packed_sig;    // 32-bit packed
  logic [7:0] unpack_arr [4]; // 4-element unpacked
 
  // BUGGY: connecting a packed 32-bit signal to an unpacked [4][8] port
  dut d1 (.data_in(packed_sig));    // TYPE ERROR at elaboration
 
  // CORRECT: connect matching unpacked array
  dut d2 (.data_in(unpack_arr));    // types match
 
  // If you genuinely need to convert between the two, use explicit assignment:
  always_comb begin
    foreach (unpack_arr[i])
      unpack_arr[i] = packed_sig[i*8 +: 8];  // slice bytes from packed sig
  end
endmodule

Bug 4 — Multi-Dimensional Index Order Confusion

Bug 4 — Row/Column Confusion in 2D Arrays
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
logic [7:0] matrix [4][8];    // [4 rows][8 cols]
 
// BUGGY: engineer wants row 2, column 5 but swaps dimensions
logic [7:0] val = matrix[5][2];  // reads col 5 of row... wait, index 5 on dim[4] is OUT OF BOUNDS
// matrix[5] returns X (out of bounds on first dim which only goes 0..3)
// matrix[5][2] = X — no error, silent wrong result
 
// CORRECT: first index = row (0..3), second = col (0..7)
val = matrix[2][5];             // row 2, column 5
 
// RULE: dimensions are indexed left-to-right as declared
// logic [7:0] matrix [ROWS][COLS]  →  matrix[row][col]
A packed vector supports bit slicing and ports, a packed multi-dimensional array supports lane indexing but not flat part-selects, and an unpacked array supports arbitrary element types but has no vector representationlogic [31:0] wpacked vector — one signalbit-slice · port ·arithmeticw[15:8] is a real part-selectlogic [3:0][7:0] buspacked, 4 lanes over 32 bitsindex by LANE: bus[1]bus[0] = LSB lane, bus[3] = MSBlanebus[15:8] selects OUT OFRANGErange applies to [3:0] → returns Xlogic [7:0] mem [4]unpacked — 4 independentvariablesany element typestructs · strings · handles ·arraysno vector representationno bit-slice · no plain port12
Figure 1 — the same 32 bits declared three ways, and what each declaration lets you do. A packed vector is one contiguous signal: it can be bit-sliced, connected to a port, and used in arithmetic. A packed multi-dimensional array is the same 32 contiguous bits organised into four addressable lanes, where index 0 is the least significant lane and index 3 the most significant; it can be indexed by lane and assigned from a vector, but a part-select applies its range to the outer dimension rather than to the underlying bits, so writing a bit range there selects out of bounds and returns X. An unpacked array is four independent variables that share a name: the total storage is the same 32 bits, but there is no single bit-level representation, so it cannot be assigned from a vector, cannot be bit-sliced, and cannot be a plain module port. What unpacked buys in exchange is an unconstrained element type — structs, strings and class handles are all legal, and none of them may appear in a packed array.

A Runnable Proof — Where the Bits Actually Live

Packed and unpacked dimensions read almost identically and produce completely different objects. This file establishes which is which, and settles one claim that is easy to get wrong.

fixed_array_layout_proof.sv — dimensions, layout, and what you may slice
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module fixed_array_layout_proof;
 
  // PACKED: one contiguous 32-bit vector, addressable as four byte lanes.
  logic [3:0][7:0] bus;
  // UNPACKED: four independent 8-bit variables. Same storage, different object.
  logic [7:0]      mem [4];
  // Both packed dimensions and an unpacked one.
  logic [1:0][7:0] pair [2];
 
  int fails = 0;
 
  task automatic expect (input string what, input bit cond);
    if (cond) $display("  PASS  %s", what);
    else begin $display("  FAIL  %s", what); fails++; end
  endtask
 
  initial begin
    $display("\n1. A packed array IS a vector; an unpacked one is not");
    bus = 32'hAA_BB_CC_DD;
    expect("$bits(bus) == 32", $bits(bus) == 32);
    expect("$bits(mem) == 32", $bits(mem) == 32);   // total storage, same
    // ...but only the packed one can be assigned from a vector directly:
    expect("packed assigned from a 32-bit literal", bus === 32'hAA_BB_CC_DD);
 
    $display("\n2. Index 0 is the LEAST significant lane of a packed array");
    $display("  bus[3]=%h bus[2]=%h bus[1]=%h bus[0]=%h",
             bus[3], bus[2], bus[1], bus[0]);
    expect("bus[3] is the MSB lane (AA)", bus[3] === 8'hAA);
    expect("bus[0] is the LSB lane (DD)", bus[0] === 8'hDD);
 
    $display("\n3. You may NOT slice a packed array as if it were flat");
    // bus[15:8] applies the range to the OUTER dimension [3:0], which only
    // has indices 0..3 - so 15:8 is out of range and returns X. It is NOT
    // equivalent to bus[1]. To slice the underlying vector, flatten first.
    begin
      logic [31:0] flat = bus;                 // packed -> vector: legal
      $display("  bus[1]     = %h", bus[1]);
      $display("  flat[15:8] = %h", flat[15:8]);
      expect("bus[1] and flat[15:8] agree", bus[1] === flat[15:8]);
      expect("flattening is how you bit-slice a packed array",
             flat[15:8] === 8'hCC);
    end
 
    $display("\n4. An unpacked array cannot be treated as one vector");
    mem[3] = 8'hAA; mem[2] = 8'hBB; mem[1] = 8'hCC; mem[0] = 8'hDD;
    // logic [31:0] bad = mem;   <- ILLEGAL: no packed representation.
    // Aggregate assignment between unpacked arrays IS legal, element-wise:
    begin
      logic [7:0] copy [4];
      copy = mem;                              // whole-array copy by value
      copy[0] = 8'h00;
      expect("unpacked assignment copies values", mem[0] === 8'hDD);
    end
 
    $display("\n5. Mixed dimensions read left-to-right: packed then unpacked");
    // `logic [1:0][7:0] pair [2]` is TWO elements, each a 16-bit packed pair.
    pair[0] = 16'h1122;  pair[1] = 16'h3344;
    expect("$bits of one element is 16", $bits(pair[0]) == 16);
    expect("$bits of the whole thing is 32", $bits(pair) == 32);
    expect("packed index selects within the element", pair[0][1] === 8'h11);
 
    $display("\n6. $size, $high, $low report the FIRST dimension");
    expect("$size(mem) == 4", $size(mem) == 4);
    expect("$high(mem) == 3", $high(mem) == 3);
    expect("$size(bus) == 4", $size(bus) == 4);   // the [3:0] dimension
 
    $display("\n%0s (%0d failures)\n",
             fails == 0 ? "ALL CHECKS PASSED" : "CHECKS FAILED", fails);
    if (fails) $fatal(1, "fixed_array_layout_proof failed");
    $finish;
  end
endmodule

Section 3 is the one worth running yourself. bus[15:8] looks like a bit slice and is not one — the range is applied to the outer [3:0] dimension, where 15 and 8 are out of range, so it returns X rather than the byte you wanted. Flattening into a vector first is what makes a genuine bit slice available.

1

A packed array sliced like a vector returned X, and only in one branch

PACKED-SLICE-OUT-OF-RANGE
Symptom

A protocol monitor extracted fields from a 32-bit packed header. Three of the four fields decoded correctly and one came back as 8'hxx — but only on some transactions, which made it look like a sampling or reset problem rather than a decode one.

The failing field was extracted with a different expression from the other three, which nobody noticed because both forms read as reasonable and the file mixed them freely.

Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
logic [3:0][7:0] hdr;      // four byte lanes over 32 bits
 
// Three fields extracted by lane index - correct.
assign f_type  = hdr[3];
assign f_len   = hdr[2];
assign f_flags = hdr[0];
 
// The fourth extracted by "bit slice" - looks equivalent, is not.
assign f_seq   = hdr[15:8];     // ✗ range applied to the [3:0] dimension
Diagnostic Evidence

Printing both forms of the same field side by side made it immediate:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  hdr        = 32'hAA_BB_CC_DD
  hdr[1]     = 8'hCC        <- the byte that was wanted
  hdr[15:8]  = 8'hxx        <- out-of-range select on the outer dimension

hdr has dimensions [3:0][7:0]. A part-select applies its range to the leftmost dimension, which is [3:0] — so hdr[15:8] asks for elements 15 down to 8 of a four-element array. Those indices do not exist, and an out-of-range read on a packed array returns X rather than erroring.

The intermittency was a red herring produced by the surrounding logic: the X only propagated to an observable output on transactions where a downstream mux happened to select that field. On the others it was absorbed, so the same constant bug appeared to come and go.

Root Cause

A packed multi-dimensional array is not addressed as a flat vector. Indices and part-selects apply to the declared dimensions from the left, so on logic [3:0][7:0] hdr the only legal top-level indices are 0 through 3.

The expression compiles because it is syntactically a valid part-select, and it returns X rather than failing because out-of-range reads on packed arrays are defined to do so. Nothing in the toolchain is obliged to object.

Two things make it survive review. The two forms look interchangeable — hdr[1] and hdr[15:8] are both "the second byte" to a reader thinking in terms of the underlying 32 bits — and mixing them in one file reads as stylistic variation rather than as two different operations. And the failure is an X, which in most designs is absorbed somewhere before it becomes visible, so the symptom appears far from the cause and often intermittently.

The mental model to correct is that the packed dimensions are part of the type, not a formatting convenience over a vector. The vector view exists, but you have to ask for it.

Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Either index by lane, which is what the type is for...
assign f_seq = hdr[1];
 
// ...or flatten explicitly when a genuine bit range is wanted.
logic [31:0] hdr_flat;
assign hdr_flat = hdr;              // packed array -> vector: legal, free
assign f_seq    = hdr_flat[15:8];

Both are correct; the first is preferable because it names the field's lane rather than restating its bit position, so a change to the header layout updates one typedef instead of every extraction site.

The test that catches this needs no DUT and no stimulus — assert the two forms agree on a known value:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
initial begin
  hdr = 32'hAA_BB_CC_DD;
  assert (hdr[1] === 8'hCC) else $fatal(1, "lane index wrong");
  assert (!$isunknown(hdr[1])) else $fatal(1, "extraction returned X");
end

Two habits generalise beyond this array.

Treat an unexpected X as a type error until proven otherwise. Out-of-range selects, uninitialised variables and unresolved comparisons all produce X, and all three are static problems rather than the dynamic sampling issues an intermittent X suggests.

Pick one extraction idiom per structure and use it everywhere. The bug here is not that one form is wrong in isolation — it is that two forms coexisted and only one of them meant what the reader assumed.

For the layout rules this rests on see packed structs, and for why an X survives to be seen only sometimes, 4-state versus 2-state types.

Interview Questions

logic and reg arrays initialise to X; bit and the other 2-state types initialise to 0.

The reason is that the type decides what values are representable. A 4-state type can hold "unknown", so an uninitialised variable holds exactly that — which is the honest answer and the useful one, because it makes a missing initialisation visible. A 2-state type has no representation for unknown, so it must start at something, and that something is 0.

The practical consequence is the one worth carrying: an unreset logic array shows X and an unreset bit array shows a valid-looking 0. The second is indistinguishable from a correctly-cleared array, so a bit declaration silently converts "I never initialised this" into "this was deliberately zeroed".

For anything modelling hardware, or anything sampled from a DUT, prefer logic. Use bit for testbench bookkeeping — loop counters, indices, flags — where an unknown has no meaning anyway.

Both hold 32 bits and they are different kinds of object.

logic [31:0] w is a packed vector: one contiguous signal. It supports bit-selects and part-selects, arithmetic and bitwise operations, and it can be a module port.

logic [7:0] m [4] is an unpacked array: four independent 8-bit variables that happen to share a name. It has no single bit-level representation, so it cannot be assigned from a vector, cannot be bit-sliced, and cannot be a plain module port.

What unpacked buys you is that the element type is unconstrained — an unpacked array can hold structs, strings, class handles or other arrays, none of which a packed array permits.

The rule of thumb follows the use: packed for anything that is a signal, because the layout is the specification; unpacked for anything that is a collection, because you want indexing rather than bit positions. A memory model is unpacked; a protocol header is packed.

A packed multi-dimensional array: 32 contiguous bits organised as four addressable byte lanes. bus[3] is bits [31:24], bus[0] is bits [7:0], so index 0 is the least significant lane.

The second byte is bus[1].

What it is not is bus[15:8]. A part-select applies its range to the leftmost dimension, which here is [3:0] — so bus[15:8] asks for elements 15 down to 8 of a four-element array. That is out of range, and an out-of-range read on a packed array returns X rather than erroring. It compiles, it looks like a bit slice, and it silently gives you nothing.

If you genuinely want a bit range, flatten first:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
logic [31:0] flat = bus;      // packed array -> vector: legal
... flat[15:8] ...            // now a real part-select

Prefer indexing by lane. It names the field rather than restating its bit position, so a layout change updates one typedef instead of every extraction site.

A read returns X and a write is silently ignored. Neither produces an error by default in simulation.

That is the dangerous part: the failure has no diagnostic of its own. The X from a bad read propagates until something absorbs it — X & 0 is 0, and an unselected mux leg vanishes entirely — so the symptom appears far from the cause and often intermittently, depending on whether the downstream logic happened to select that path. A dropped write produces no symptom at all until someone reads the location back.

Synthesis behaves differently, which is worth knowing. A constant out-of-range index is usually an elaboration error, while a runtime index is not — the tool builds a mux tree sized for the declared range and the out-of-range case simply cannot be selected, so simulation and synthesis can disagree about a design that neither flags.

The defence is a bound check where the index is not provably in range: if (i < $size(arr)), or an assertion assert (i inside {[0:$high(arr)]}). And treat an unexpected X as a type problem before treating it as a sampling problem — out-of-range selects and uninitialised variables are static faults wearing a dynamic disguise.

mem[3] is a constant index, so it is not hardware at all — the tool resolves it at elaboration and the result is a wire. Zero gates, zero delay.

mem[addr] with a runtime index infers a multiplexer tree: roughly one 2-to-1 mux stage per address bit, so a depth of log2(N) for an N-entry array. A 256-entry array is an eight-deep mux cascade, and that depth lands directly in the critical path.

The consequence is architectural rather than stylistic. A flop-based array indexed by a runtime address is a register file built out of muxes, and it stops closing timing at a size that surprises people — often well below where the area cost becomes the concern. Real register files use an SRAM macro or an explicitly decoded structure with a one-hot enable per entry, which trades the mux depth for a decode.

Two related points. Writing mem[addr] <= data infers the decode side of the same structure — an address decoder driving per-entry write enables. And an unpacked array indexed only by constants costs nothing at all, which is why breaking a wide structure into named constant-indexed fields is often better RTL than indexing a general array.

The dividing question is when the size is known.

Fixed-size arrays have a size fixed at compile time. They are the only kind that is synthesisable, they need no allocation, and their bounds are checkable at elaboration. Use them for anything in RTL, and in a testbench whenever the size genuinely is a constant.

Dynamic arrays are sized at run time with new[n] and resized by reallocation. They suit data whose extent is known once and then fixed — a packet payload whose length arrives in a header.

Queues are sized continuously and support push and pop at both ends cheaply. They suit anything with arrival-and-departure semantics: scoreboard storage, outstanding-transaction tracking, a model of a hardware FIFO.

The mistake worth avoiding is reaching for a queue in a testbench simply because it is flexible. A fixed-size array documents an invariant — "there are exactly four channels" — and the compiler enforces it, whereas a queue silently accommodates a fifth. Where the count is a property of the design, express it as one.

Note that all three copy by value on assignment, which distinguishes them from class handles and is what makes a fixed-size array a safe snapshot.

Where This Is Specified

  • IEEE 1800-2023 §7.4 — Packed and unpacked arrays. The distinction between packed dimensions, declared to the left of the identifier, and unpacked dimensions declared to the right; the requirement that packed array elements be integral types; and the rule that a packed array may be treated as a single vector.
  • IEEE 1800-2023 §7.4.6 — Indexing and slicing of arrays. Index and part-select expressions apply to the leftmost dimension, which is why a part-select on a multi-dimensional packed array is not a flat bit slice.
  • IEEE 1800-2023 §7.4.5 — Indexing and slicing of arrays out of bounds. A read of an out-of-range index returns X for 4-state and 0 for 2-state types; a write to an out-of-range index has no effect.
  • IEEE 1800-2023 §7.6 — Array assignment. Aggregate assignment between unpacked arrays of equivalent type, and the value semantics that make an array copy independent of its source.
  • IEEE 1800-2023 §20.7 — Array querying functions. $size, $high, $low, $left, $right and $dimensions, and the dimension they report by default.
  • IEEE 1800-2023 §10.10 — Unpacked array concatenation and §5.11 — Array literals, covering the '{...} assignment-pattern syntax that distinguishes an array literal from a concatenation.

Best Practices and Coding Guidelines

Always initialize arrays

Use '{default: val} at declaration or $readmemh in the initial block. Never read before writing. In RTL, use reset logic to drive arrays to known values.

Use foreach for portability

foreach (arr[i]) automatically respects the array bounds — no magic numbers. It works on any dimensionality and handles non-zero-based ranges correctly.

Match packed/unpacked at ports

If a module port uses an unpacked array, the connecting signal must also be the same unpacked type. Never assume a packed and unpacked array of equal total bits are interchangeable — they are not.

Guard variable index accesses

In simulation, always add an assertion or conditional check when using a runtime variable as an array index. Out-of-bounds returns X silently and the bug can travel far before anyone notices.

TaskPreferred approachAvoid
Initialize all elements to a valuearr = '{default: val}Manual loop — more code, same result
Iterate over all elementsforeach (arr[i])for (int i=0; i<SIZE; i++) — error-prone size magic
Copy entire arraydst = src (whole-array assign)Element-by-element loop — verbose
Compare entire arrayif (a !== b) (use !== to catch X)if (a != b) — misses X differences
Load from file$readmemh("file.hex", arr)Hardcoded values — unmaintainable
Large memory model in testbenchAssociative array or dynamic arrayStatic array of 1M entries — wastes simulator memory

Summary

Fixed-size arrays are the first tool you reach for whenever you know the array dimensions at design time. They synthesize directly to hardware, cost nothing at runtime, and — when used correctly — make RTL intent crystal clear. The two things worth internalizing deeply: the packed/unpacked distinction (it determines what operations apply and how ports connect), and the X-initialization default (it turns every unguarded read before write into a silent bug factory).

  • Packed dimensions go left of the name; unpacked go right. The split determines the type, not just the bit count.
  • logic arrays start as X, bit/int arrays start as 0. Choose the right type for your use case.
  • Whole-array copy and comparison are single-statement operations on unpacked arrays — use !== rather than != to catch X differences in scoreboards.
  • Variable index access synthesizes to a mux tree. That is usually the wrong choice for large memories — use SRAM macros or associative arrays depending on context.
  • foreach is the right loop for arrays. It respects bounds automatically and handles multi-dimensional indexing cleanly.

Part of SystemVerilog Fundamentals·Arrays·Lesson 14 of 53

View program

Continue learning