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
// ── 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)| Declaration | Packed bits | Unpacked elements | Total bits | Access form |
|---|---|---|---|---|
logic [31:0] w | 32 | 1 | 32 | w, w[7:0] |
logic [7:0] m[4] | 8 | 4 | 32 | m[0], m[3][5] |
logic [3:0][7:0] r | 32 (packed) | 1 | 32 | r, r[2], r[2][3:0] |
logic [7:0] x[4][4] | 8 | 4×4 = 16 | 128 | x[0][0], x[3][3] |
int arr[8] | 32 (int) | 8 | 256 | arr[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.
| Index | Variable | Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 | Value |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | buf[0] | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 8'hAB |
| 1 | buf[1] | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 8'hCD |
| 2 | buf[2] | 1 | 1 | 1 | 0 | 1 | 1 | 1 | 1 | 8'hEF |
| 3 | buf[3] | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 8'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.
| Selector | Bit range in 32-bit word | Access | Example value |
|---|---|---|---|
reg_file[3] | [31:24] — MSB byte | 8-bit slice of the packed vector | 8'hDE |
reg_file[2] | [23:16] | 8-bit slice | 8'hAD |
reg_file[1] | [15:8] | 8-bit slice | 8'hBE |
reg_file[0] | [7:0] — LSB byte | 8-bit slice | 8'hEF |
reg_file | [31:0] — whole word | Full 32-bit packed value | 32'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'h00 | 8'h01 | 8'h02 | 8'h03 | 8'h04 | 8'h05 | 8'h06 | 8'h07 |
| cache[1] | 8'h10 | 8'h11 | 8'h12 | 8'h13 | 8'h14 | 8'h15 | 8'h16 | 8'h17 |
| cache[2] | 8'h20 | 8'h21 | 8'h22 | 8'h23 | 8'h24 | 8'h25 | 8'h26 | 8'h27 |
| cache[3] | 8'h30 | 8'h31 | 8'h32 | 8'h33 | 8'h34 | 8'h35 | 8'h36 | 8'h37 |
Green cell: cache[2][5] = 8'h25
Code Examples — From Basics to Real Verification
Example 1 — Beginner: Declare, Write, Read, Iterate
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
endmoduleExpected output:
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 = 376Example 2 — Intermediate: Register File Model (RTL-Style)
// 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
endmoduleExample 3 — Verification: Packet Buffer with Integrity Check
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
endmoduleExpected output:
ERROR: PKT[3][7]: exp=0x37 got=0xFF
FAIL: 1 mismatches detectedExample 4 — Corner Case: Initialization, Out-of-Bounds, Packed Access
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
endmoduleExpected output:
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]=FFSimulation 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
| Scenario | Simulation result | Synthesis result | What to do |
|---|---|---|---|
Read index > $high (static index) | Returns X, no error by default | Elaboration warning or error | Add bound check: if (i < $size(arr)) |
Read index > $high (runtime index) | Returns X silently | Tool-dependent — may generate extra mux logic | Assert bounds at runtime |
Write index > $high | Ignored silently (no write occurs) | Elaboration error | Guard writes with bounds check |
| Negative index on unsigned array | X (unsigned wrap-around) | Error | Declare index as int and guard for negative |
Synthesis Implications
| Usage | Synthesized hardware | Notes |
|---|---|---|
logic [7:0] r [4] in always_ff | 4 × 8-bit flip-flop registers | Standard register array — synthesizes cleanly |
| Constant index access | Direct wire to specific register bits | Zero extra logic |
| Variable index access (read) | Mux tree selecting from all elements | Timing-critical; depth grows with array size |
| Variable index access (write) | Demux — enable decode for each element | Area grows linearly with array size |
Packed array [N-1:0] | Single wide register or wire | No mux overhead |
Where Fixed-Size Arrays Show Up in Real Verification Work
// ── 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
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 '{}
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]=00Bug 3 — Packed vs Unpacked Port Mismatch
// 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
endmoduleBug 4 — Multi-Dimensional Index Order Confusion
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 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.
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
endmoduleSection 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.
A packed array sliced like a vector returned X, and only in one branch
PACKED-SLICE-OUT-OF-RANGEA 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.
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] dimensionPrinting both forms of the same field side by side made it immediate:
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 dimensionhdr 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.
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.
// 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:
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");
endTwo 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:
logic [31:0] flat = bus; // packed array -> vector: legal
... flat[15:8] ... // now a real part-selectPrefer 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,$rightand$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.
| Task | Preferred approach | Avoid |
|---|---|---|
| Initialize all elements to a value | arr = '{default: val} | Manual loop — more code, same result |
| Iterate over all elements | foreach (arr[i]) | for (int i=0; i<SIZE; i++) — error-prone size magic |
| Copy entire array | dst = src (whole-array assign) | Element-by-element loop — verbose |
| Compare entire array | if (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 testbench | Associative array or dynamic array | Static 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.
logicarrays start as X,bit/intarrays 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.
foreachis 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