SystemVerilog Series · SV-03

SystemVerilog Series — SV-03: Data Types — VLSI Trainers
SystemVerilog Series · SV-03

Data Types

The complete SystemVerilog type system — integer types, 2-state vs 4-state, void, chandle, string methods, events, typedef, enumerations, structs, unions, tagged unions, classes, casting, $cast, and bit-stream casting.

📈 Type System Overview

SystemVerilog’s type system bridges two worlds: the 4-state hardware world of Verilog (0, 1, X, Z) and the 2-state software world of C (0, 1 only). Choosing the right type makes simulation faster, catches bugs earlier, and communicates design intent clearly.

2-State Types (no X/Z)
  • bit — 1-bit or user-width, unsigned
  • byte — 8-bit signed (like C char)
  • shortint — 16-bit signed
  • int — 32-bit signed (like C int)
  • longint — 64-bit signed
4-State Types (0/1/X/Z)
  • logic — 1-bit or user-width, unsigned
  • reg — Verilog-2001 4-state variable
  • integer — 32-bit signed 4-state
  • time — 64-bit unsigned 4-state
  • All net types: wire, tri, etc.
When to use 2-state vs 4-state: Use 4-state types (logic, integer) for RTL signals where X-propagation matters — simulation must reveal when uninitialised registers corrupt downstream logic. Use 2-state types (bit, int, byte) in testbench code where X/Z are meaningless and you want faster simulation with lower memory usage. A 2-state variable stores 1 bit per bit value; a 4-state variable needs 2 bits per bit value.

Integer Data Types

SystemVerilog adds five C-inspired 2-state integer types alongside the existing Verilog-2001 4-state types. The table below shows all integer types, their width, state count, and default signedness.

TypeWidthStateDefault signNotes
shortint16 bits2-statesignedNew in SV. Equivalent to C short.
int32 bits2-statesignedNew in SV. Equivalent to C int. Most common integer type in SV testbenches.
longint64 bits2-statesignedNew in SV. Equivalent to C long long.
byte8 bits2-statesignedNew in SV. ASCII character or small counter. Like C char.
bituser-defined2-stateunsignedNew in SV. Like logic but 2-state. Default width 1.
logicuser-defined4-stateunsignedNew in SV. Replaces reg and wire in most RTL contexts.
reguser-defined4-stateunsignedVerilog-2001. Still valid; logic preferred in new code.
integer32 bits4-statesignedVerilog-2001. Use int instead when X/Z not needed.
time64 bits4-stateunsignedVerilog-2001. For simulation time values.

Green rows = 2-state (new in SV). Blue rows = 4-state (from Verilog-2001).

Signed and Unsigned Declarations

// Default signedness — matches table above
int  signed_val   = -5;    // int defaults to signed
byte ascii_char  = "A";    // byte defaults to signed (8'h41 = 65)
bit  [7:0] flags = 8'hFF; // bit defaults to unsigned
logic[7:0] data  = 8'hFF; // logic defaults to unsigned

// Override signedness explicitly
int unsigned   ui;   // unsigned 32-bit (0 to 4_294_967_295)
logic signed [7:0] s8; // signed 8-bit (-128 to 127)
bit signed   [7:0] sb; // signed 2-state 8-bit

Automatic Type Conversions

// Widening: zero-extends (unsigned) or sign-extends (signed) — silent
bit [7:0] a  = 8'hFF;
int       b  = a;       // b = 32'h0000_00FF (zero-extended)
int signed[7:0] c = 8'hFF;
int       d  = c;       // d = 32'hFFFF_FFFF (sign-extended)

// Narrowing: truncates MSBs — tool should warn
int       wide = 32'hDEAD_BEEF;
byte      low8 = wide;  // low8 = 8'hEF (truncated — warning!)

// logic to bit: 1 stays 1, X and Z become 0
logic x_val = 1'bx;
bit   b_val = x_val;    // b_val = 0  (X collapses to 0)
int vs integer — the key difference: int is 2-state; it cannot hold X or Z. integer is 4-state; it can. If you write a loop counter for (int i = 0; i < 8; i++), the loop will always execute correctly because i can never become X. If you wrote for (integer i = 0; ...) and i somehow received an X, the comparison i < 8 would return X and the loop might not terminate. Use int (2-state) for testbench counters, indices, and loop variables.

🔢 Real & shortreal

These two types model floating-point values. Both are IEEE 754 standard — real is a 64-bit double-precision float (same as C double), and shortreal is a 32-bit single-precision float (same as C float). The shortreal type is new in SystemVerilog.

real      pi       = 3.14159265358979;  // 64-bit, ~15 decimal digits
shortreal pi_f    = shortreal'(3.14159); // 32-bit, ~7 decimal digits
realtime  period  = 10.0ns;              // realtime = real with time units

// Conversion between types
int  i = int'(3.9);        // i = 3 (truncates, not rounds)
real r = real'(5);          // r = 5.0

// Use $shortrealtobits / $bitstoshortreal for lossless conversion
logic [31:0] bits = $shortrealtobits(3.14);
shortreal    back = $bitstoshortreal(bits);

🚫 void and chandle

void

The void type represents the absence of a return value. It is used as the return type of functions that are called purely for their side-effects. It can also appear as a member of a tagged union where a particular variant carries no data.

// void function — called for side-effect only
function void log_message(string msg);
  $display("%0t: %s", $time, msg);
endfunction

// Discard a function's return value with void cast
void'(tx.randomize());   // call randomize(), ignore return value

// void member in a tagged union (tag-only variant)
typedef union tagged {
  void    Invalid;   // no data when Invalid
  int     Valid;     // carries an int when Valid
} VInt;

chandle

A chandle is an opaque pointer type used exclusively to pass and receive C pointers across the DPI boundary. Its size is platform-dependent (at least large enough for a pointer on the host machine). It is always initialised to null.

chandle ctx;           // initialised to null automatically

// Legal operations on chandle:
if (ctx == null) ...   // equality with null or another chandle
if (ctx)         ...   // boolean test (0 if null, 1 otherwise)
ctx = null;            // assign null
ctx = other_ctx;       // assign from another chandle

// ILLEGAL: chandle cannot be used in arithmetic, ports,
// sensitivity lists, continuous assign, unions, or packed types.
chandle in practice: You will rarely see chandle outside DPI code. Its typical use is to capture an opaque C pointer returned by an import "DPI-C" function (e.g. an allocator that returns a void* context handle), store it in an SV variable, and pass it back to another DPI function later. Never try to inspect or arithmetic-operate on a chandle value — its bits are meaningless from the SV side.

💬 string Type & Methods

The string type is a variable-length, dynamically allocated array of bytes. It is fundamentally different from a packed byte array: a string grows and shrinks automatically, never truncates on assignment, and provides a rich set of built-in methods.

Declarations and Basic Operations

string s1 = "hello";     // initialised
string s2;               // auto-initialised to ""
string s3 = "";          // explicit empty string

// Operators
s2 = {s1, " world"};      // concatenation: "hello world"
s2 = {3{"ab"}};           // replication: "ababab"
byte ch = s1[0];          // index: 8'h68 ('h')
s1[0] = "H";              // assign char: "Hello"

if (s1 == "Hello") ...    // equality comparison
if (s1 < "World")  ...    // lexicographic comparison (like strcmp)

Built-In String Methods

len()
function int
Returns number of characters. "" returns 0.
putc(i, c)
task
Replace character at index i with c. Ignores out-of-range indices.
getc(i)
function int
Returns ASCII code at index i. Returns 0 for out-of-range.
toupper()
function string
Returns uppercase copy. Original unchanged.
tolower()
function string
Returns lowercase copy. Original unchanged.
compare(s)
function int
Case-sensitive strcmp. Returns 0 if equal, <0 or >0 otherwise.
icompare(s)
function int
Case-insensitive strcmp. Otherwise same as compare.
substr(i,j)
function string
Returns substring from index i to j inclusive.
atoi() / atohex() atooct() / atobin()
function integer
Parse string as decimal / hex / octal / binary integer.
atoreal()
function real
Parse string as real number (Verilog real constant syntax).
itoa(i) / hextoa(i) octtoa(i) / bintoa(i)
task
Store integer representation into string (inverses of atoi etc.).
realtoa(r)
task
Store real representation into string (inverse of atoreal).
// Practical method examples
string msg = "Hello World";
$display(msg.len());           // 11
$display(msg.toupper());        // "HELLO WORLD"
$display(msg.substr(0,4));      // "Hello"

string num_str = "255";
int    n       = num_str.atoi();  // n = 255

string hex_out;
hex_out.hextoa(255);              // hex_out = "ff"

// $sformat — format directly into a string (like sprintf)
string label;
$sformat(label, "addr=0x%0h", 32'hDEAD); // "addr=0xdead"

event Type

The event type in SystemVerilog is an enhanced version of Verilog named events. The key additions are: events have a persistent triggered state lasting the whole time step, two event variables can be aliased to the same synchronisation object, and events can be set to null.

// Declaration
event done;                   // new synchronisation object
event done_too = done;        // alias: both refer to same object
event never_blocks = null;   // null event: permanently triggered

// Triggering
->done;                        // trigger (non-blocking)
->>done;                       // trigger (non-blocking, NBA region)

// Waiting
@done;                         // wait for edge (next trigger)
wait(done.triggered);         // wait if not already triggered this step

// Persistent triggered state: in the same time step as ->done,
// both @done AND wait(done.triggered) will unblock.

// Aliasing demo
->done;                        // triggers done AND done_too (same object)
@done_too;                    // unblocks because done was triggered
Persistent triggered state explained: In Verilog, if a process is not waiting at the exact moment an event fires, it misses it completely. SystemVerilog's triggered property solves this: wait(ev.triggered) returns immediately if the event was already triggered earlier in the same time step. This eliminates a whole class of simulation race conditions where the trigger and the waiter are in the same time step but in different evaluation order.

🔧 User-Defined Types & typedef

typedef creates a new type name from an existing type. Types must be defined before use — but a forward declaration lets you declare a type name before its full definition exists (useful for mutually referencing classes).

// Basic typedef
typedef int intP;           // intP is an alias for int
intP a, b;

// typedef for complex types (often used for bus widths)
typedef logic [31:0] word_t;
word_t addr, data;

// Parameterised typedef (using localparams)
typedef logic [7:0] byte_t;

// Forward declaration — use type before defining it
typedef foo;          // forward: tells compiler "foo will be a type"
foo f = 1;             // can now use foo before its definition
typedef int foo;       // actual definition

// Forward declaration for class types (most common use case)
typedef class Transaction;   // forward-declare class
class Driver;
  Transaction t;             // use Transaction before it is fully defined
endclass
class Transaction;
  Driver d;
endclass

// typedef from an interface port type
interface intf_i;
  typedef int data_t;
endinterface

module sub(intf_i p);
  typedef p.data_t my_data_t;  // local re-def of interface type
  my_data_t data;
endmodule
typedef in packages — the recommended pattern: Define all your project-wide types (typedef enum, typedef struct, bus width aliases) inside a package, then import the package wherever needed. This is the SystemVerilog equivalent of a C header file. It gives clean namespacing, avoids redefining the same type in every module, and makes tool-specific elaboration more predictable.

🎉 Enumerations

Enumerations declare a set of named integral constants with strong type checking. You cannot accidentally assign a value outside the defined set without an explicit cast — a powerful bug-prevention mechanism for FSM state variables.

Basic Enum Syntax

// Anonymous enum (default base type: int)
enum {red, yellow, green} light1, light2;
// red=0, yellow=1, green=2

// Named typedef enum — the standard pattern
typedef enum logic [1:0] {
  IDLE  = 2'b00,
  FETCH = 2'b01,
  EXEC  = 2'b10,
  DONE  = 2'b11
} state_t;
state_t state, next_state;

// Auto-incrementing values
typedef enum {bronze=3, silver, gold} medal_t;
// silver=4, gold=5  (auto-incremented)

// 4-state enum allows X/Z members
typedef enum integer {IDLE=0, XX='x, S1=1, S2=2} fsm_t;
// Only valid on 4-state base types (integer, logic)

Enum Ranges

// Generate numbered sequences of constants
typedef enum { add=10, sub[5], jmp[6:8] } E1;
// add=10
// sub0=11, sub1=12, sub2=13, sub3=14, sub4=15
// jmp6=16, jmp7=17, jmp8=18

enum { register[2]=1, register[2:4]=10 } vr;
// register0=1, register1=2
// register2=10, register3=11, register4=12

Strong Type Checking

typedef enum { red, green, blue } Colors;
Colors c;

c = green;           // OK — assigning enum member
c = 1;               // ERROR — cannot assign integer without cast
c = Colors'(1);      // OK — explicit static cast (no runtime check)
$cast(c, 1);          // OK — dynamic cast (runtime check)

int i = c + 1;       // OK — c auto-cast to int in expressions
c = Colors'(c + 1); // OK — cast result back to Colors
c++;                 // ERROR — ++ on enum requires explicit cast

Enum Built-In Methods

first()
Returns the first enum member value.
last()
Returns the last enum member value.
next(N=1)
Returns the Nth next value. Wraps at end.
prev(N=1)
Returns the Nth previous value. Wraps at start.
num()
Returns total number of enum members.
name()
Returns string name of current value. Empty string if out-of-range.
// Iterating over all enum values — common testbench pattern
typedef enum { red, green, blue, yellow } Colors;
Colors c = c.first();
forever begin
  $display("%s = %0d", c.name(), c);
  if (c == c.last()) break;
  c = c.next();
end
// Output: red = 0 / green = 1 / blue = 2 / yellow = 3

📌 Structs & Unions

Structures group related fields of possibly different types. Unions overlay the same storage with multiple interpretations. Both follow C syntax without optional struct tags. The key SV additions are packed structs/unions (contiguous bit storage, synthesisable), and tagged unions (type-safe with runtime tag checking).

Unpacked Struct (Default)

// Anonymous unpacked struct
struct { bit [7:0] opcode; bit [23:0] addr; } IR;
IR.opcode = 8'h01;

// Named typedef struct — the standard pattern
typedef struct {
  bit [7:0]  opcode;
  bit [23:0] addr;
} instruction_t;

instruction_t IR;   // variable of struct type
IR = '{8'h01, 24'h1000}; // struct literal assignment

Packed Struct (Synthesisable)

A packed struct is stored as a contiguous vector. All members must be integral types (no real, no unpacked arrays). It can be sliced like a bit vector, used in arithmetic, and maps cleanly to hardware bus fields.

typedef struct packed signed {
  int       a;        // 32 bits — MSB
  shortint  b;        // 16 bits
  byte      c;        // 8 bits
  bit [7:0] d;        // 8 bits — LSB
} pack1_t;             // total: 64-bit signed vector

pack1_t p1;
p1.c       = 8'hFF;   // field access
p1[15:8] = 8'hAA;   // part-select (c field: bits 15:8)

// ATM cell header — a real-world packed struct
typedef struct packed {
  bit [3:0]         GFC;
  bit [7:0]         VPI;
  bit [11:0]        VCI;
  bit               CLP;
  bit [3:0]         PT;
  bit [7:0]         HEC;
  bit [47:0][7:0]   Payload;
  bit [2:0]         filler;
} s_atmcell;

Packed Union

All members of a packed union must have the same bit width. Writing one member and reading another is safe and gives a different interpretation of the same bits. This is perfect for union-type bus protocol headers.

typedef union packed {
  s_atmcell        acell;       // structured view
  bit [423:0]     bit_slice;  // raw bit view
  bit [52:0][7:0] byte_slice; // byte-array view
} u_atmcell;

u_atmcell u1;
byte b = u1.byte_slice[51];    // same as u1.bit_slice[415:408]
bit [3:0] gfc = u1.acell.GFC;  // structured field access

Tagged Union (Type-Safe)

A tagged union stores both a value and a tag indicating which member is currently active. Accessing the wrong member is a compile-time or runtime error. The void member holds no data — all information is in the tag itself.

// Simple tagged union: either Invalid (no data) or Valid (int)
typedef union tagged {
  void  Invalid;   // no data when tag = Invalid
  int   Valid;
} VInt;

// Complex: nested tagged union for instruction encoding
typedef union tagged {
  struct { bit[4:0] reg1, reg2, regd; } Add;
  union tagged {
    bit [9:0] JmpU;           // unconditional jump
    struct { bit[1:0] cc; bit[9:0] addr; } JmpC; // conditional
  } Jmp;
} Instr;
Packed struct sizing rule: If any member of a packed struct is 4-state (e.g. logic, integer), the entire struct is treated as 4-state. 2-state members within it are auto-converted as if cast. Mixed 2/4-state packed structs are allowed but the 4-state interpretation propagates to the whole struct. For purely combinational or synthesis-targeted packed structs, use only bit and bit[N:0] members.

📚 Class (Introduction)

A class bundles data (properties) and behaviour (methods) into an encapsulated object. This section introduces the class keyword in the context of the type system. Classes are covered in full in the Classes article (SV-17).

class Packet;
  int        address;          // properties
  bit [63:0] data;
  shortint   crc;
  Packet     next;            // handle to another Packet

  function new();              // constructor method
  function bit send();         // regular method
endclass : Packet

// Usage: allocate on the heap
Packet pkt = new();    // pkt is a handle to a Packet object
pkt.address = 32'h1000;
void'(pkt.send());
Classes are reference types: A class variable is a handle (like a pointer) to an object on the heap, not the object itself. Assigning one class variable to another copies the handle, not the object — both variables then point to the same object. To copy the object itself, you must implement a copy() method or use $cast.

🔂 Singular and Aggregate Types

SystemVerilog categorises all data types into two groups that operators and built-in methods use to define their behaviour:

Singular Types

Any type except an unpacked structure, union, or array. A singular variable represents a single value, symbol, or handle. All integral types are singular even though they can be sliced.

  • int, byte, bit[N:0], logic
  • real, shortreal
  • string, chandle, event
  • Class handles
  • Packed structs and packed arrays

Aggregate Types

Unpacked structures, unpacked unions, and unpacked arrays. An aggregate variable represents a collection of singular values.

  • Unpacked arrays: int arr[4]
  • Unpacked structs: struct { int a; real b; }
  • Unpacked unions
  • Dynamic arrays, queues, associative arrays
Why this matters: $cast only works on singular types. Some functions recursively process aggregate types until reaching singular values. Knowing which category a type falls into helps understand why certain operators or functions accept or reject a given type.

🔄 Casting

A static cast converts an expression to a different type at compile time using the type'(expr) syntax. It always succeeds at runtime (no checking) — it is a coercion, not a validation. Four varieties exist:

int'(2.0*3.0)
Type cast — convert to named type
17'(x - 2)
Size cast — change bit width
signed'(x)
Signedness cast — change sign
mytype'(foo)
User-defined type cast
// Type casts
int i   = int'(2.9);         // 2 (truncates)
real r  = real'(5);           // 5.0

// Size cast (17 bits)
logic [16:0] wide = 17'(x - 2);  // x-2 zero-extended/truncated to 17 bits

// Signedness cast — size unchanged
int s  = signed'(8'hFF);   // interprets 8'hFF as -1 (sign-extended)
bit [7:0] u = unsigned'(-1); // interprets -1 as 8'hFF

// Struct to bit array (lossless bit-pattern preservation)
typedef struct { bit isfloat; int n; } tagged_st;
typedef bit [$bits(tagged_st)-1:0] tagbits;
tagged_st a;
tagbits t = tagbits'(a);        // struct → packed bit array
a         = tagged_st'(t);       // packed bit array → struct (roundtrip)

// Enum cast (static — no runtime range check)
typedef enum {red, green, blue} Colors;
Colors c = Colors'(2);          // c = blue (no runtime check)
c         = Colors'(99);         // c = out-of-range (no error, undefined)
Static cast rules: When changing size, signedness passes through unchanged. When changing signedness, size passes through unchanged. When casting a real to an integer type, the value is rounded (not truncated). For lossless shortreal ↔ bits conversion, use $shortrealtobits and $bitstoshortreal instead of a cast.

$cast Dynamic Casting

$cast performs a runtime-checked cast that validates whether the assignment is legal before making it. It is particularly important for enum types and class handles where the assignment might be invalid at runtime.

typedef enum {red, green, blue, yellow, white, black} Colors;
Colors col;

// Called as a task: error on failure (col unchanged)
$cast(col, 2 + 3);          // col = black (5) — succeeds
$cast(col, 2 + 8);          // runtime error: 10 is not a Colors value

// Called as a function: returns 1 on success, 0 on failure
if (!$cast(col, 2 + 8))
  $error("Cast failed: 10 is not a valid Colors value");

// $cast on class handles (OOP polymorphism)
// Attempting to downcast a base class handle to a derived class:

class Animal;     endclass
class Dog extends Animal; endclass

Animal a = new Dog(); // base handle to derived object
Dog    d;
if ($cast(d, a)) // runtime check: is a actually a Dog?
  $display("Downcast succeeded");

Static cast: Colors'(expr)

  • Compile-time coercion — always "succeeds"
  • Out-of-range value stored: behaviour is undefined
  • Faster — no runtime overhead
  • Use when you are certain the value is valid

Dynamic cast: $cast(dest, src)

  • Runtime validation before assignment
  • Task form: error on failure, dest unchanged
  • Function form: returns 0 on failure, no error
  • Use when the value may be out of range

🔁 Bit-Stream Casting

Bit-stream casting extends normal casting to work with unpacked arrays and structs. Any type that can be serialised into a contiguous stream of bits is a bit-stream type, and values of different bit-stream types of the same total size can be freely converted between each other.

What is a Bit-Stream Type?

  • Any integral, packed, or string type
  • Unpacked arrays, structs, or classes of the above
  • Dynamically-sized arrays (dynamic arrays, associative arrays, queues) of the above
// Convert between two fixed-size structs of the same bit count
typedef struct { shortint addr; byte code; byte cmd[2]; } Control; // 2+1+2=5 bytes=40 bits
typedef bit Bits[36:1];         // unpacked array of 36 bits

Control p;
Bits    stream[$];

// Serialise Control packet into a bit stream
stream = {stream, Bits'(p)};      // struct → unpacked bit array, appended to queue

// Deserialise back to Control
Control q;
q = Control'(stream[0]);         // unpacked bit array → struct
stream = stream[1:$];            // remove first packet from queue

Dynamic Packet with Variable Payload

typedef struct {
  byte   length;      // payload size stored in packet header
  shortint address;
  byte   payload[];   // dynamic array — size set at runtime
  byte   chksum;
} Packet;

// Transmit over a byte-stream queue
typedef byte channel_t[$];
channel_t channel;
Packet    tx;
channel = {channel, channel_t'(tx)}; // struct → byte queue

// Receive: read header byte to find packet size, then extract
Packet rx;
int    size = channel[0] + 4;
rx          = Packet'(channel[0:size-1]); // byte queue → struct
channel     = channel[size:$];            // remove consumed bytes
Bit-stream ordering: When converting to a packed representation, the first element (index 0 for arrays, first field for structs) occupies the most significant bits. This is consistent with SV's left-to-right ordering convention. When a dynamic array or queue is serialised, index 0 is the MSB of the resulting bit stream. When an associative array is serialised, elements appear in index-sorted order with the smallest index at the MSB.
Size mismatch errors: If both source and destination are fixed-size types but have different total bit counts, the cast is a compile-time error. If either side includes a dynamically-sized type, the error can occur at runtime. The compiler will catch what it can; the rest surfaces as simulation runtime errors when sizes diverge.

Leave a Comment

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

Scroll to Top