1. The 4-State Logic System
Standard programming languages use only two states: true and false, 0 and 1. Hardware simulation requires four because wires in real silicon are not always cleanly driven to VDD or GND.
0 — Logic Zero
Driven low — wire connected to GND or output of a gate whose output is low. The nominal "off" state.
1 — Logic One
Driven high — wire connected to VDD or output of a gate whose output is high. The nominal "on" state.
X — Unknown
Uninitialized, multiple conflicting drivers, or result of an illegal operation. X propagates through gates to expose potential bugs.
Z — High-Z
Undriven (floating) wire, or tri-state buffer in disabled state. A wire left floating in real hardware is a bug.
The simulator propagates X through logic. If AND(X, 0) = 0 (X can't change the result) but AND(X, 1) = X (result is unknown), the X propagation makes bugs visible at the output where behavior is actually wrong — not just at the uninitialized register.
Simulation ≠ Synthesis for X: A synthesizer treats X as "don't care" — it may assign 0 or 1. A wire that is X in simulation may be deterministically 0 or 1 in real silicon, hiding the bug. Never assume your design is correct because it passes with X-producing initial conditions; always ensure resets drive every register to a known state.
2. logic vs bit vs reg vs wire
This is the most common interview distinction in SystemVerilog. Understanding when to use each type prevents subtle bugs.
| Type | States | Drivers | Use Case |
|---|---|---|---|
| logic | 4 (0,1,X,Z) | Single driver only | RTL ports, internal signals — replaces reg and wire for single-driver nets |
| bit | 2 (0,1) | Single driver | Testbench variables, transaction fields — faster simulation, no X/Z overhead |
| reg | 4 (legacy) | Procedural only | Verilog-2001 holdover — use logic instead in SV |
| wire | 4 | Multiple drivers OK | Nets with multiple drivers (tri-state buses, module interconnects with wired-OR) |
Rule of thumb: Use logic for everything in RTL. Use wire only when you intentionally have multiple drivers. Use bit in testbench code and class-based UVM components for simulation performance. Never use reg in new SystemVerilog code.
The silent X-to-0 conversion in bit
The most dangerous property of bit is that assigning X or Z silently resolves to 0 with no warning in most simulators. This matters when crossing from an RTL logic signal into a testbench bit variable — an uninitialized X in RTL appears as a clean 0 in the testbench, masking the initialization bug entirely.
logic my_logic; // starts as X (uninitialized) bit my_bit; // starts as 0 (always) my_bit = my_logic; // X silently converted to 0 — BUG HIDDEN if (my_bit === 1'b0) // passes even though logic was X $display("looks fine"); // WRONG — bug masked by bit type
3. Integer Data Types
SystemVerilog adds C-style fixed-width integers for verification code. Unlike logic vectors, these are always 2-state (no X/Z) and have defined signed/unsigned behavior.
| Type | Width | Signed? | Range | Common Use |
|---|---|---|---|---|
| byte | 8 bits | Yes (signed) | −128 to 127 | Short protocol fields, array indices |
| shortint | 16 bits | Yes | −32768 to 32767 | Counters, offsets |
| int | 32 bits | Yes | −2^31 to 2^31−1 | Loop variables, random seeds |
| longint | 64 bits | Yes | −2^63 to 2^63−1 | Timestamps, large counters |
| byte unsigned | 8 bits | No | 0 to 255 | Byte data, AXI data beats |
| int unsigned | 32 bits | No | 0 to 2^32−1 | Memory addresses, packet lengths |
| time | 64 bits | No (unsigned) | 0 to 2^64−1 | Simulation timestamps ($time) |
Signed vs Unsigned wrap-around
Signed arithmetic wraps at the MSB boundary. Adding 1 to a byte value of 127 (binary 0111_1111) sets the sign bit, giving −128 — not 128. This wraps back into the signed range instead of overflowing. For a byte unsigned (or bit [7:0]), 255 + 1 = 0 with an implicit carry that is simply discarded.
byte s = 127; // signed: max positive value byte unsigned u = 255; // unsigned: max value s = s + 1; // s = -128 (sign bit wraps — Overflow!) u = u + 1; // u = 0 (carry discarded — Wrap-around) // Detecting overflow int wide = s + 1; // promote before overflow: wide = 128 if (wide > 127) $display("overflow detected");
4. Packed vs Unpacked Arrays
Arrays in SystemVerilog come in two fundamentally different forms that affect how memory is laid out and what operations are valid.
Packed arrays
Dimensions declared before the variable name. All bits are contiguous in memory — the entire array is a single vector that you can part-select, slice, and apply bitwise operations to as if it were one big integer.
logic [31:0] data; // 32-bit packed vector logic [3:0][7:0] bytes; // 4 bytes packed into 32 bits data[15:8] = 8'hAB; // valid: part-select on packed bytes[2] = 8'hFF; // valid: access byte 2 bytes[2][3] = 1'b1; // valid: bit 3 of byte 2 if (data == 32'hDEAD_BEEF) // compare entire 32-bit word ...
Unpacked arrays
Dimensions declared after the variable name, like C arrays. Each element is a separate storage location. Bit-slicing across element boundaries is not allowed, but you can assign entire arrays element-by-element or use foreach loops.
logic mem [0:255]; // 256 separate 1-bit logic values int queue[1024]; // 1024 separate 32-bit integers byte pkt [0:63]; // 64 separate signed bytes pkt[0] = 8'hFF; // valid: element access // pkt[0][3] = 1'b1; // INVALID: cannot bit-select element of byte array foreach (queue[i]) queue[i] = i * 4; // initialize each element
5. Special Types: enum, struct, union
SystemVerilog adds abstract data types that make RTL and testbench code self-documenting and catch illegal value assignments in simulation.
enum — Named States
Enumerations associate symbolic names with integer values. The simulator can warn when a variable holds an out-of-range value. Essential for FSM state encoding.
typedef enum logic [1:0] { IDLE = 2'b00, BUSY = 2'b01, DONE = 2'b10, ERR = 2'b11 } state_t; state_t cur_state, nxt_state; always_ff @(posedge clk) cur_state <= nxt_state; always_comb unique case (cur_state) IDLE: nxt_state = wr_en ? BUSY : IDLE; BUSY: nxt_state = done ? DONE : BUSY; default: nxt_state = ERR; endcase
struct — Grouped Fields
Structures bundle multiple signals into a named record, ideal for AXI channel signals or packet headers.
typedef struct packed { logic [31:0] addr; logic [3:0] strb; logic valid; logic ready; } axi_aw_t; axi_aw_t aw_chan; aw_chan.addr = 32'hC000_0000; aw_chan.valid = 1; aw_chan.strb = 4'hF;
6. Type Casting and Conversion
SystemVerilog provides explicit cast operators to convert between types. Implicit conversion can hide bugs — use explicit casts when crossing type boundaries.
int i_val = -1; logic [31:0] u_val; // Implicit: -1 (32'hFFFFFFFF) assigned to logic — works, but intent unclear u_val = i_val; // Explicit cast — intent is clear u_val = logic[31:0]'(i_val); // = 32'hFFFFFFFF // Static cast: change width logic [7:0] byte_val = 8'(i_val); // truncate to 8 bits = 8'hFF // $cast: for enum — required when assigning from int to enum state_t s; if (!$cast(s, 2)) $error("invalid enum value");
7. RTL Best Practices
- Always use logic for module ports and internal RTL signals — avoid mixing
regandwire - Use bit only in testbench class properties and transaction objects where X-propagation is not needed
- Declare FSM state variables as enum with explicit encoding to get full simulation warnings
- Use packed struct for AXI channels — it keeps related signals together and allows whole-channel assignments
- Prefer always_ff, always_comb, always_latch over plain always — the synthesizer can verify the intent
- Use unique case and priority case to document and enforce case statement intent
- Declare loop variables as int (not
integer) in SystemVerilog —integeris 4-state,intis 2-state and simulates faster
4-State Logic Visualizer
Select a value to shift into both registers:
Integer Overflow Lab
Frequently Asked Questions
bit silently resolves to 0 — no warning, no error. Use logic for RTL (you want X-propagation to expose bugs). Use bit in testbenches where speed matters and X is not meaningful.logic [7:0] a) store all bits contiguously — you can bit-select, part-select, and apply bitwise operators across the whole vector. Unpacked arrays (logic a [7:0]) are like C arrays — separate storage elements that cannot be part-selected across boundaries but can be iterated with foreach. Packed arrays map directly to hardware wires; unpacked arrays are primarily a software-side organization structure.byte stores values from −128 to 127. The binary pattern 0111_1111 (127) plus 1 gives 1000_0000. In two's complement, a set MSB means negative, and 1000_0000 = −128. This is integer overflow — arithmetic wrapped around. To detect it, promote to a wider type before the operation: int wide = byte_val + 1;always_ff for flip-flop inference, always_comb for combinational logic, and always_latch for latches in SystemVerilog. The specialized keywords are not just style — the synthesizer and simulator check that your code matches the declared intent. always_comb automatically includes all right-hand-side signals in the sensitivity list. Plain always is legal but gives up these checks.=== operator compares all four logic states — including X and Z — exactly. logic a = 1'bX; if (a === 1'bX) is true. The regular == operator returns X if either operand is X or Z, and any comparison with X evaluates to X (which is treated as false in conditionals). Use === in testbenches when you want to check whether a signal is X or Z explicitly.