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.
bit— 1-bit or user-width, unsignedbyte— 8-bit signed (like C char)shortint— 16-bit signedint— 32-bit signed (like C int)longint— 64-bit signed
logic— 1-bit or user-width, unsignedreg— Verilog-2001 4-state variableinteger— 32-bit signed 4-statetime— 64-bit unsigned 4-state- All net types:
wire,tri, etc.
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.
| Type | Width | State | Default sign | Notes |
|---|---|---|---|---|
| shortint | 16 bits | 2-state | signed | New in SV. Equivalent to C short. |
| int | 32 bits | 2-state | signed | New in SV. Equivalent to C int. Most common integer type in SV testbenches. |
| longint | 64 bits | 2-state | signed | New in SV. Equivalent to C long long. |
| byte | 8 bits | 2-state | signed | New in SV. ASCII character or small counter. Like C char. |
| bit | user-defined | 2-state | unsigned | New in SV. Like logic but 2-state. Default width 1. |
| logic | user-defined | 4-state | unsigned | New in SV. Replaces reg and wire in most RTL contexts. |
| reg | user-defined | 4-state | unsigned | Verilog-2001. Still valid; logic preferred in new code. |
| integer | 32 bits | 4-state | signed | Verilog-2001. Use int instead when X/Z not needed. |
| time | 64 bits | 4-state | unsigned | Verilog-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 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 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
"" returns 0.// 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
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 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
// 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;
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());
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],logicreal,shortrealstring,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
$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:
// 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)
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
