SystemVerilog data types are the foundation of everything you write — RTL and testbench alike. The choice between logic and bit, between 4-state and 2-state, between int and integer, has real consequences for simulation correctness and speed. This lesson covers every SV data type with concrete examples and tells you exactly when to use each one.
Every SV type falls into one of two categories: 4-state (can hold 0, 1, X, Z) or 2-state (can hold only 0 or 1). This distinction matters far more than it might seem. In a real chip simulation, an uninitialized register has the value X — unknown. If you use 2-state types everywhere in your testbench, that X silently becomes 0, hiding initialization bugs. Professional verification engineers use logic for anything connected to the DUT and bit only for pure testbench constructs.
An X value in SystemVerilog means the simulator cannot determine whether the signal is 0 or 1. X appears when:
logic variable is declared but never assigned (power-on X)Z) signal is sampled without a pull resistordefaultThe critical insight: X should propagate. If a register is never reset and you use it in logic, the output should become X — making the bug visible in simulation. If you use bit types, X silently snaps to 0 and the bug is hidden. This is called the X-pessimism vs X-optimism debate, and using logic in RTL is the professional standard.
Use $isunknown(expr) to test whether any bit of a value is X or Z. Use $isxunknown(expr) to test for X only (ignoring Z). Both return 1 if true, 0 if false. They are invaluable in testbench checkers:
logic [7:0] dut_data;
// Check before sampling in scoreboard
task check_output(logic [7:0] actual, logic [7:0] expected);
if ($isunknown(actual)) begin
$error("DUT output contains X or Z: %0b", actual);
return;
end
if (actual !== expected) // !== is 4-state inequality (X != X is false)
$error("Mismatch: got %0h, expected %0h", actual, expected);
else
$display("PASS: %0h", actual);
endtask
// In assertions: flag if data bus goes X during valid transfer
assert property (@(posedge clk) (valid && !$isunknown(data)))
else $error("Data bus is X during valid transfer!");In SystemVerilog, == is 2-state equality (X == X returns X, which is treated as false). === is 4-state equality (X === X returns 1). Use === when you want to check for exact X/Z matches. Use !== for 4-state inequality. In testbench checkers, !== is usually what you want — it will flag mismatches even when one value is X.
| Type | States | Width | Signed? | Typical Use |
|---|---|---|---|---|
| logic | 4-state (0,1,X,Z) | 1 bit (scalar or vector) | Unsigned | RTL signals, DUT ports, interfaces |
| wire | 4-state | 1 bit or vector | Unsigned | Nets driven by continuous assign; legacy |
| reg | 4-state | 1 bit or vector | Unsigned | Legacy Verilog — replaced by logic in SV |
| integer | 4-state | 32 bits | Signed | Verilog loop counters; use int in SV |
| time | 4-state | 64 bits | Unsigned | Simulation time storage |
| bit | 2-state (0,1) | 1 bit or vector | Unsigned | TB data structures, rand variables |
| byte | 2-state | 8 bits | Signed | ASCII characters, byte-wide data |
| shortint | 2-state | 16 bits | Signed | Small signed integers |
| int | 2-state | 32 bits | Signed | Loop counters, indices, counts in TB |
| longint | 2-state | 64 bits | Signed | Timestamps, large counters |
| real | 2-state | 64-bit IEEE 754 | Signed | Floating-point math, timing calculations |
| shortreal | 2-state | 32-bit IEEE 754 | Signed | Single-precision floating point |
| string | N/A | Dynamic | N/A | Messages, filenames, TB logging |
| void | N/A | 0 bits | N/A | Function return type when no value returned |
The rule is simple and consistent in industry: use logic for everything that touches the DUT, and use bit/int/byte for pure testbench data structures that never connect to RTL signals.
logicbit, int, byterand variables in classes → bit or int (2-state simplifies constraint solving)logicThe string type in SystemVerilog is a dynamically-sized sequence of characters. Unlike C strings, SV strings are not null-terminated arrays — they are first-class objects with built-in methods. They are simulation-only (not synthesizable).
| Method | Returns | Description |
|---|---|---|
| .len() | int | Number of characters in the string |
| .toupper() | string | Convert all characters to uppercase |
| .tolower() | string | Convert all characters to lowercase |
| .substr(i,j) | string | Extract substring from index i to j (inclusive) |
| .itoa(i) | void | Convert integer i and store in string |
| .atoi() | integer | Convert string to integer (stops at first non-digit) |
| .getc(i) | byte | Get the ASCII value of character at index i |
| .putc(i,c) | void | Set character at index i to ASCII value c |
| == / != | bit | String comparison (case-sensitive) |
| {s1,s2} | string | Concatenation using {} operator |
Static casting converts between compatible types at compile time. The syntax is TargetType'(expression). If the target type is narrower, the value is truncated. If wider, it is zero-extended (or sign-extended for signed types).
Dynamic casting with $cast(dest, src) is used primarily for polymorphic class handles. It returns 1 on success and 0 on failure, letting you handle the failure gracefully. You must use $cast when downcasting a base class handle to a derived class handle — static casting will not work.
This is one of the most common sources of subtle verification bugs. The key rules are:
logic, bit, and wire are unsigned by defaultbyte, shortint, int, longint are signed by defaultsigned'(expr) or declare logic signed [7:0] to force signed arithmeticIf you compare byte b = -1 (which is 8'hFF = 255 unsigned) with bit [7:0] x = 8'hFF using ==, they compare equal. But if you promote both to int first and one is signed (-1) and one is unsigned (255), you get different values. Always be explicit: use int for signed arithmetic in testbenches and bit [N:0] for unsigned counters with a guard bit.
// ============================================================
// data_types_demo.sv — SystemVerilog Data Types Demonstration
// EcrioniX · SV Verification Course · Day 2
// ============================================================
module data_types_demo;
// ----- 4-STATE TYPES -----
logic flag; // 1-bit 4-state, starts as X
logic [7:0] data_byte; // 8-bit vector, starts as 8'bXXXXXXXX
logic [31:0] data_word; // 32-bit vector
integer legacy_int; // 32-bit 4-state signed (Verilog legacy)
// ----- 2-STATE TYPES -----
bit b_flag; // 1-bit 2-state, starts as 0
bit [7:0] b_byte; // 8-bit 2-state vector
byte ascii_char; // 8-bit signed (byte = shortint restricted)
shortint s_int; // 16-bit signed
int counter; // 32-bit signed — use for loop counters
longint timestamp; // 64-bit signed
int unsigned u_count; // explicitly unsigned int
// ----- FLOATING POINT -----
real freq_hz; // 64-bit double-precision float
shortreal duty_cycle; // 32-bit single-precision float
// ----- STRING TYPE -----
string msg; // dynamically-sized, simulation-only
string logfile;
initial begin
// ---- 4-state: observe initial X ----
$display("flag initial = %b (X)", flag); // prints x
$display("data_byte init = %h (XX)", data_byte); // prints xx
flag = 1'b1;
data_byte = 8'hA5;
$display("flag after assign = %b", flag); // 1
$display("data_byte = 8'h%0h", data_byte); // A5
// ---- $isunknown / $isxunknown ----
logic [3:0] partial;
partial = 4'bXX10;
if ($isunknown(partial))
$display("partial has X or Z bits: %b", partial);
if ($isxunknown(partial))
$display("partial has X bits (not just Z): %b", partial);
// ---- 2-state: starts at 0, not X ----
$display("b_flag initial = %b (NOT X — silently 0)", b_flag);
// ---- Integer arithmetic ----
counter = 0;
timestamp = 64'd1_000_000;
for (counter = 0; counter < 5; counter++) begin
timestamp += 100;
end
$display("timestamp after loop = %0d", timestamp); // 1000500
// ---- Floating point ----
freq_hz = 100.0e6; // 100 MHz
duty_cycle = 0.5; // 50%
$display("Period = %0.2f ns", 1.0e9 / freq_hz); // 10.00 ns
$display("High time = %0.2f ns", (1.0e9 / freq_hz) * duty_cycle); // 5.00 ns
// ---- String methods ----
msg = "Hello SystemVerilog";
$display("Length : %0d", msg.len()); // 19
$display("Upper : %s", msg.toupper()); // HELLO SYSTEMVERILOG
$display("Lower : %s", msg.tolower()); // hello systemverilog
$display("Substr 6-7: %s", msg.substr(6,12)); // SystemV (indices 6..12)
logfile = {"run_", msg.substr(0,4), ".log"};
$display("Logfile : %s", logfile); // run_Hello.log
// ---- Static casting ----
real r_val;
int i_val;
r_val = 3.9;
i_val = int'(r_val); // truncates to 3, NOT rounds
$display("int'(3.9) = %0d", i_val); // 3
data_byte = logic [7:0]'(counter + 1000); // truncate to 8 bits
$display("truncated = 8'h%0h", data_byte); // low 8 bits of 1005
// ---- Signed vs Unsigned pitfall ----
byte signed_b = -1; // stored as 8'hFF
bit [7:0] unsigned_b;
unsigned_b = 8'hFF; // 255 unsigned
// Both hold 8'hFF, but interpreted differently in arithmetic
$display("signed byte -1 as unsigned: %0d", unsigned'(signed_b)); // 255
$display("int of signed -1: %0d", int'(signed_b)); // -1
// ---- 4-state equality vs 2-state equality ----
logic [3:0] a = 4'bXX10;
logic [3:0] b = 4'bXX10;
$display("a == b (2-state): %b", (a == b)); // 0 (X makes result X→0)
$display("a === b (4-state): %b", (a === b)); // 1 (exact match inc X)
$display("a !== b (4-state): %b", (a !== b)); // 0
$display("\nAll data types demo complete.");
$finish;
end
endmodulelogic for anything connected to RTL. Use bit/int for pure TB data.$isunknown(expr) checks for X or Z; $isxunknown(expr) checks for X only.=== (4-state equality) when comparing values that may be X.int'(3.9) truncates to 3. Dynamic cast: $cast(dest, src) for class handles.byte/int are signed; logic/bit are unsigned. Mixed expressions become unsigned — watch out!.len(), .toupper(), .substr(i,j), .atoi().logic is 4-state (0, 1, X, Z) — use it for RTL signals and anything connected to the DUT so that uninitialized or floating signals show as X. bit is 2-state (0, 1 only) — use it for testbench-only data structures, loop counters, and random variables in classes. 2-state types simulate ~10-20% faster and have no X/Z to worry about.
Use logic for interface signals, DUT ports, clocking block signals, and any wire where X propagation is meaningful. Use bit, int, byte for scoreboard entries, packet fields, loop counters, and rand variables in constraint classes. If in doubt on a testbench signal that touches the DUT: logic.
X means unknown — the simulator cannot determine whether the bit is 0 or 1. It appears on uninitialized registers, from bus contention, or when Z is sampled without a pull. X propagates through logic — good designs expose bugs this way. Use $isunknown(signal) to detect X/Z and === for 4-state comparison.
Static cast: int'(3.9) truncates a real to int (gives 3, not 4). logic [7:0]'(wide_int) takes the low 8 bits. Dynamic cast: $cast(derived_handle, base_handle) downcasts a polymorphic class handle and returns 1 on success. Always use $cast for class handles — never static cast.
void is the return type for a function that performs an action but returns no value — equivalent to C's void. Use function void check_result(...) instead of task when the function consumes no simulation time and you want the compiler to enforce the no-time-consumption rule.