Why Learn About 2-State Data Types? A logic variable can hold four values, but most testbench code never needs the extra two. A packet length, a loop counter, an array index — none of these should ever legitimately be X or Z. SystemVerilog's 2-state types like bit, byte, and int exist for exactly this kind of data: plain numbers, modeled the same way a C program would.

4-state types and the logic keyword were already covered in SystemVerilog logic and Verilog Data Types. This article covers the other half: SystemVerilog's family of 2-state integer types.

8 min read | Beginner Level

What You'll Learn

  • Why 2-state types exist and how they differ from 4-state types
  • The five 2-state integer types and how their ranges compare
  • What happens when a 4-state value is assigned to a 2-state variable
  • How to work with signed and unsigned values, and how to cast between them

2-State Data Types

2-state data types like bit and int can only represent 0 and 1 — there's no X or Z. That simplification means each bit needs only one bit of simulator storage instead of two, which reduces memory use and speeds up simulation.

Data TypeSizeSigned RangeUnsigned RangeDescription
bit1-bit—0 and 12-state variable
byte8-bit-2⁷ to 2⁷-10 to 2⁸-12-state variable, similar to C char
shortint16-bit-2¹⁵ to 2¹⁵-10 to 2¹⁶-12-state variable, similar to C short int
int32-bit-2³¹ to 2³¹-10 to 2³²-12-state variable, similar to C int
longint64-bit-2⁶³ to 2⁶³-10 to 2⁶⁴-12-state variable, similar to C long int

2-state types suit more abstract levels of modeling than RTL, such as system-level or transaction-level code. When modeling a network packet with a header field for length, that length should be a plain number — there's no meaningful scenario where it should read as unknown or high-impedance.

When a 4-state value is converted to a 2-state value, any X or Z bits are converted to 0.


module tb;
  bit   [3:0] var_b;      // Declare a 4-bit variable of type "bit"  
  logic [3:0] x_val;     // Declare a 4-bit variable of type "logic"
  
  initial begin  
    x_val = 4'b01zx;
  
    // If a logic type or any 4-state variable assigns its value to a "bit" type
    // variable, then X and Z get converted to zero
    var_b = x_val;
    $display ("x_val=%b var_b = %b", x_val, var_b);
  end
endmodule

Output

Output
xcelium> run
x_val=01zx var_b = 0100
xmsim: *W,RNQUIE: Simulation is complete.

x_val holds a Z in bit 1 and an X in bit 0. Once that value is assigned into the 2-state var_b, both unknown bits collapse to 0, giving 0100. This silent collapsing is exactly why 2-state types are the wrong choice for RTL signals that genuinely need to represent an unknown or undriven state — see SystemVerilog logic for that side of the picture.

Quick Check

Q: What does a 2-state variable read back as, if a 4-state signal assigned into it was holding Z?

Show Answer

0. Any X or Z bit is silently converted to 0 the moment it's assigned into a 2-state type — there's no error or warning by default.

bit

The bit type is a 2-state type that defaults to 0. It's the most commonly used 2-state type in testbenches and other verification code, where the extra X/Z states aren't needed. A range specification ([MSB:LSB]) turns a single bit into a multi-bit vector.

Syntax


bit             signal_name;   // single-bit
bit [msb:lsb]   signal_name;   // multi-bit vector

module tb;
  bit       var_a;       // Declare a 1 bit variable of type "bit"
  bit [3:0] var_b;       // Declare a 4 bit variable of type "bit"
  
  logic [3:0] x_val;     // Declare a 4 bit variable of type "logic"
  
  initial begin
  
    // Initial value of "bit" data type is 0
    $display ("Initial value var_a=%0b var_b=0x%0h", var_a, var_b);
  
    // Assign new values and display the variable to see that it gets the new values
    var_a = 1;
    var_b = 4'hF;
    $display ("New values    var_a=%0b var_b=0x%0h", var_a, var_b);
  
    // If a "bit" type variable is assigned with a value greater than it can hold
    // the left most bits are truncated. In this case, var_b can hold only 4 bits
    // and hence 'h481 gets truncated leaving var_b with only 'ha;
    var_b = 16'h481a;
    $display ("Truncated value: var_b=0x%0h", var_b);  
  end
endmodule

Output

Output
ncsim> run
Initial value var_a=0 var_b=0x0
New values    var_a=1 var_b=0xf
Truncated value: var_b=0xa
ncsim: *W,RNQUIE: Simulation is complete.

var_b is only 4 bits wide, so assigning it 16'h481a keeps only the bottom 4 bits (a) and silently discards the rest — no error, no truncation warning by default.

This kind of silent truncation is one of the most common sources of "why is my value wrong" bugs. If a variable might receive a value wider than its declared width, check the width explicitly rather than relying on the assignment to catch it.

byte

The byte type is an 8-bit, 2-state integer, signed by default, using two's complement representation to hold both positive and negative values — a range of -128 to 127. It's a natural fit anywhere memory efficiency matters, such as large arrays of small values.


module tb;
   byte           m_var_s;      // By default signed, and can represent positive and negative numbers
  
  initial begin
  
    #10 m_var_s  = 0;           // assign 0
    #20 m_var_s  = 2**7 - 1; 	// assign 127
    #30 m_var_s  = 2**7;        // assign 128
    #40 m_var_s  = 2**8 - 1;    // assign 255
  end
  
  initial
    $monitor("[%0t] m_var_s = 'd%0d  (0x%0h)", $time, m_var_s, m_var_s);
endmodule

Output

Output
xcelium> run
[0] m_var_s = 'd0  (0x0)
[30] m_var_s = 'd127  (0x7f)
[60] m_var_s = 'd-128  (0x80)
[100] m_var_s = 'd-1  (0xff)
xmsim: *W,RNQUIE: Simulation is complete.

Assigning 128 (one past the signed maximum of 127) wraps around to -128, and 255 wraps to -1 — classic two's-complement rollover, the same behavior you'd see assigning out-of-range values to a signed char in C.

Quick Check

Q: Why does assigning 255 to a signed byte display as -1, not 255?

Show Answer

255 in binary is 8'hFF, all eight bits set to 1. In two's-complement signed representation, that bit pattern represents -1, not 255 — the value itself never changes, only how it's interpreted based on whether the type is signed or unsigned.

shortint

shortint is a 2-state, 16-bit signed integer, exactly like byte but wider.


module tb;
   shortint            m_var_s;      // By default signed, and can represent positive and negative numbers
  
  initial begin
  
    #10 m_var_s  = 0;             // assign 0
    #20 m_var_s  = 2**15 - 1;     // assign 32767
    #30 m_var_s  = 2**15;         // assign 32768 (wraps to -32768, signed 16-bit)
    #40 m_var_s  = 2**16 - 1;     // assign 65535 (wraps to -1, signed 16-bit)
  end
  
  initial
    $monitor("[%0t] m_var_s = 'd%0d  (0x%0h)", $time, m_var_s, m_var_s);
endmodule

Output

Output
xcelium> run
[0] m_var_s = 'd0  (0x0)
[30] m_var_s = 'd32767  (0x7fff)
[60] m_var_s = 'd-32768  (0x8000)
[100] m_var_s = 'd-1  (0xffff)
xmsim: *W,RNQUIE: Simulation is complete.

Just like byte, values that overflow the signed 16-bit range wrap around: 32768 becomes -32768, and 65535 becomes -1 — exactly what the output above shows.

int

int is a 2-state, 32-bit signed integer — the default choice for general-purpose counters, loop variables, and array indices in a testbench.


module tb;
   int            m_var_s;      // By default signed, and can represent positive and negative numbers
  
  initial begin
  
    #10 m_var_s  = 0;  
    #20 m_var_s  = 2**31 - 1;  
    #30 m_var_s  = 2**31;  
    #40 m_var_s  = 2**32 - 1;  
  end
  
  initial
    $monitor("[%0t] m_var_s = 'd%0d  (0x%0h)", $time, m_var_s, m_var_s);
endmodule

Output

Output
xcelium> run
[0] m_var_s = 'd0  (0x0)
[30] m_var_s = 'd2147483647  (0x7fffffff)
[60] m_var_s = 'd-2147483648  (0x80000000)
[100] m_var_s = 'd-1  (0xffffffff)
xmsim: *W,RNQUIE: Simulation is complete.

int is by far the most commonly used integer type in testbench code — loop counters, transaction counts, and array sizes almost always default to int unless there's a specific reason to pick a narrower or wider type.

longint

longint is a 2-state, 64-bit signed integer, useful for counters or addresses that could exceed the 32-bit range of a plain int.


module tb;
   longint            m_var_s;      // By default signed, and can represent positive and negative numbers
  
  initial begin
  
    #10 m_var_s  = 0;  
    #20 m_var_s  = 2**63 - 1;  
    #30 m_var_s  = 2**63;  
    #40 m_var_s  = 2**64 - 1;  
  end
  
  initial
    $monitor("[%0t] m_var_s = 'd%0d  (0x%0h)", $time, m_var_s, m_var_s);
endmodule

Output

Output
xcelium> run
[0] m_var_s = 'd0  (0x0)
[30] m_var_s = 'd9223372036854775807  (0x7fffffffffffffff)
[60] m_var_s = 'd-9223372036854775808  (0x8000000000000000)
[100] m_var_s = 'd-1  (0xffffffffffffffff)
xmsim: *W,RNQUIE: Simulation is complete.

Signed and Unsigned

Signed types can represent both positive and negative values; unsigned types can only represent zero and positive values. A signed type spends one bit (the MSB) on the sign, which halves its maximum positive value compared to an unsigned type of the same width — the total range stays the same size, just shifted.

Example

The sign can be set explicitly with the signed and unsigned keywords.


module tb;
  byte    			s_byte;   // By default byte is signed
  byte unsigned  	u_byte;   // Byte is set to unsigned
  
  initial begin
    $display ("Size s_byte=%0d, u_byte=%0d", $bits(s_byte), $bits(u_byte));
  
    // Assign the "assumed" maximum value to both variables
    #1 s_byte = 'h7F;
       u_byte = 'h7F;
  
    // Increment by 1, and see that s_byte rolled over to negative because
    // byte is signed by default. u_byte keeps increasing because it is
    // unsigned and can go upto 255
    #1 s_byte += 1;
       u_byte += 1;
  
    // Assign 255 (8'hFF) to u_byte -> this is the max value it can hold
    #1 u_byte = 'hFF;
  
    // Add 1 and see that it rolls over to 0
    #1 u_byte += 1;  
  end
  
  initial begin
    $monitor ("[%0t ns] s_byte=%0d u_byte=%0d", $time, s_byte, u_byte);
  end
endmodule

Output

Output
ncsim> run
Size s_byte=8, u_byte=8
[0 ns] s_byte=0 u_byte=0
[1 ns] s_byte=127 u_byte=127
[2 ns] s_byte=-128 u_byte=128
[3 ns] s_byte=-128 u_byte=255
[4 ns] s_byte=-128 u_byte=0
ncsim: *W,RNQUIE: Simulation is complete.

Both variables are 8 bits wide, but they interpret the same bit patterns differently: past 127, s_byte wraps to negative, while u_byte keeps counting up to 255 before it wraps to 0.

Quick Check

Q: Do signed and unsigned versions of the same type use a different number of bits?

Show Answer

No. As the example shows, $bits() returns 8 for both s_byte and u_byte — the width is identical. Only the interpretation of the top bit (as a sign, or as part of the magnitude) differs.

Sign Cast

Signed and unsigned values can be converted into one another with a cast, without changing the underlying bits.


module tb;
   shortint            m_var_s;      // By default signed, and can represent positive and negative numbers
   shortint unsigned   m_var_us;     // Explicitly set to 'unsigned' and represents only positive numbers  
  
  initial begin
    m_var_us = 2**16 - 1;
    m_var_s  = -1;
  
    // Convert an unsigned variable value to a signed variable value by casting  
    $display("m_var_us=%0d m_var_us=%0d", signed' (m_var_us), m_var_us);
    $display("m_var_s=%0d  m_var_s=%0d", unsigned' (m_var_s), m_var_s);
  end
endmodule

Output

Output
xcelium> run
m_var_us=-1 m_var_us=65535
m_var_s=65535  m_var_s=-1
xmsim: *W,RNQUIE: Simulation is complete.

m_var_us and m_var_s hold the exact same 16-bit pattern (all 1s), just declared with opposite signedness — that's why signed'(m_var_us) reads as -1 and unsigned'(m_var_s) reads as 65535. The cast doesn't touch a single bit; it only changes how those bits are interpreted for that one expression. Note the first $display's format string prints the label m_var_us twice, once for the cast result and once for the plain value — a labeling quirk to watch for, not a difference in what's being computed.

Operations on unsigned integers can be marginally faster in some tools, since there's no sign bit to account for during arithmetic — one more reason to reach for unsigned explicitly when a value can never legitimately go negative, like an index or a count.

Common Mistakes

Mistake #1: Assuming truncation or overflow raises a warning

As var_b = 16'h481a; showed, assigning a value that's too wide for the target variable silently keeps only the low bits — there's no default warning. The same applies to signed overflow, like byte wrapping from 127 to -128. Don't rely on the simulator to flag either case; check widths and ranges explicitly where it matters.

Mistake #2: Using a 2-state type where X/Z detection matters

A 2-state variable can never hold X or Z — any 4-state value assigned into one gets those bits silently zeroed. This makes 2-state types a poor choice for signals where catching an uninitialized or undriven condition is the whole point, such as most RTL signals. Use logic for those instead.

Mistake #3: Forgetting that signed and unsigned share the same width

byte and byte unsigned are both 8 bits — adding unsigned doesn't gain you an extra bit of storage, it only shifts which values that same 8 bits can represent (0 to 255 instead of -128 to 127).

Mistake #4: Assuming a cast changes the stored value

signed'(...) and unsigned'(...) only change how an expression's existing bits are interpreted for that one use — they don't modify the variable itself. Casting m_var_us doesn't turn m_var_us into a signed variable; it produces a signed view of its value for that expression only.

Practice Exercise

Challenge: Explore Unsigned Rollover

Declare a shortint unsigned variable named counter. Assign it its maximum possible value, then add 1 to it and print the result. Also print $bits(counter) to confirm the width.

Hint

The maximum value of a 16-bit unsigned type is 2**16 - 1. Adding 1 past that should roll over to 0, the same way u_byte did in the Signed and Unsigned example.

Solution

module tb;
  shortint unsigned counter;

  initial begin
    $display ("bits = %0d", $bits(counter));

    counter = 2**16 - 1;
    $display ("counter = %0d", counter);

    counter += 1;
    $display ("counter = %0d (rolled over)", counter);
  end
endmodule

Summary

In this tutorial, you learned:

  • 2-state types can only hold 0 and 1, use less simulator memory than 4-state types, and are best suited to abstract, non-RTL data like counters and packet fields
  • bit, byte, shortint, int, and longint range from 1 to 64 bits, each signed by default except bit
  • Assigning a 4-state value into a 2-state variable silently converts any X/Z bits to 0
  • Assigning a value wider than a variable's declared width silently truncates it, and signed overflow wraps around — neither raises a warning by default
  • signed'(...) and unsigned'(...) reinterpret an expression's existing bits without changing the underlying variable
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5