Skip to content

Data Types

4-state vs. 2-state

Verilog's reg/wire model every bit as one of four states: 0, 1, X (unknown), Z (high-impedance). SystemVerilog keeps this as logic, but adds pure 2-state types (bit, byte, int, shortint, longint) that only hold 0/1.

logic [7:0] rtl_data;   // 4-state — can be X or Z, matches real hardware/simulation reality
bit   [7:0] tb_counter; // 2-state — only 0/1, faster to simulate, no X-propagation

This isn't a stylistic choice — it's the single most important type decision in a testbench:

Why this matters

If you drive a DUT input from a 2-state variable, you can never send it an X, even deliberately. A huge class of real RTL bugs (uninitialized registers, unconnected muxes, reset-timing issues) only shows up as X-propagation. A testbench that can't drive or check for X is blind to that entire bug class.

Rule of thumb: anything that connects to or represents DUT signals (logic), anything internal to the testbench that's pure bookkeeping (bit/int — loop counters, transaction counts, scoreboard tallies).

logic replaces reg/wire — mostly

logic can be driven by exactly one continuous assignment or one procedural block — same restriction as reg, but it can be used anywhere reg or wire could, removing the old "is this a wire or a reg" decision when declaring a signal. It cannot be used where multiple drivers are genuinely needed (a real bus with multiple tri-state drivers still needs wire/tri).

Packed vs. unpacked arrays

bit [7:0] packed_bytes [0:3];     // packed dimension [7:0], unpacked dimension [0:3]
bit [31:0] word;                  // a packed 32-bit vector — contiguous bits, one "thing"
int unpacked_array [0:31];        // 32 separate 32-bit ints — not contiguous, no bit-level ops across elements
  • Packed dimensions ([7:0]) describe bits that are contiguous in memory and can be treated as a single vector — sliced, concatenated, cast as one value.
  • Unpacked dimensions ([0:3]) are just an array of separate elements, each with its own storage.

A byte queue[$], an associative array int scores[string], and a fixed array bit data[0:63] are all unpacked-array variants — useful for testbench-side data structures (transaction queues, scoreboards) where you don't need bit-level packing.

Queues, dynamic arrays, associative arrays

These three cover almost every data-structure need in a testbench:

int q[$];                    // queue — dynamic size, push/pop from either end, ordered
int dyn[];                   // dynamic array — resizable, but no push/pop; use new[size]
int assoc[string];           // associative array — sparse, keyed by any type (string, int, enum...)

q.push_back(5);
q.push_front(1);
int x = q.pop_front();

dyn = new[10];                // allocate 10 elements
dyn.delete();                 // free it

if (assoc.exists("dut_id_3")) $display(assoc["dut_id_3"]);

Queues are the default choice for scoreboards and transaction pipes (expected-vs-actual comparison queues); associative arrays are the default for anything keyed by an ID that isn't densely packed (e.g. tracking outstanding transactions by a sparse tag/address).

Common pitfall

pop_front()/pop_back() on an empty queue is a runtime error in most simulators (returns default value with a warning, or a fatal depending on tool/mode) — always check q.size() before popping in code that isn't guaranteed to have an entry, e.g. a scoreboard that might get a DUT response before the expected-value generator has produced anything.