SystemVerilog Basics

Chapter 5 of 16

More Data Types: 2-State vs. 4-State, enum, struct, and typedef

Understand the difference between 2-state and 4-state data types and when to use each, name states with enum, bundle related fields with struct, and give types meaningful names with typedef.

logic (and wire, reg) from the last chapter are all 4-state types: besides 0 and 1, each bit can also be x (unknown) or z (high-impedance). This chapter keeps going: first sorting out when to use 2-state vs. 4-state types, then learning to organize data with enum, struct, and typedef instead of a pile of loose, unrelated variables.

2-state vs. 4-state data types

SystemVerilog's integer types split into two camps:

  • 4-state types: logic, reg, wire, integer, and more — each bit can be 0, 1, x (unknown), or z (high-impedance).
  • 2-state types: bit, byte, shortint, int, longint, and more — each bit can only be 0 or 1, no x/z.
TypeWidth4-state?
logic / regSpecified at declaration (defaults to 1 bit)Yes
wireSpecified at declaration (defaults to 1 bit)Yes
integer32 bitsYes
bitSpecified at declaration (defaults to 1 bit)No
byte8 bitsNo
shortint16 bitsNo
int32 bitsNo
longint64 bitsNo

The most immediately visible difference is the default value when uninitialized:

module default_values_demo;
  logic   l;  // 4-state: logic, 1 bit
  integer n;  // 4-state: integer, 32 bits
  bit     b;  // 2-state: bit, 1 bit
  int     i;  // 2-state: int, 32 bits
 
  initial begin
    $display("l = %b", l);  // x
    $display("n = %b", n);  // 32 x's
    $display("b = %b", b);  // 0
    $display("i = %b", i);  // 32 zeros
  end
endmodule

This isn't just about how it prints: a 4-state type's x can surface real design or verification bugs in simulation — "this signal was never actually driven or initialized." A 2-state type just defaults to 0 when uninitialized, quietly papering over exactly that class of bug. A common rule of thumb follows from this: use 4-state types (logic) for RTL signals, where catching an unintended x matters; use 2-state types (int, byte, etc.) for testbench-only bookkeeping like counters, loop variables, and IDs, where they're both more efficient and semantically correct — those things should always have a definite value anyway.

enum: naming states

Representing state with bare integers hurts readability — looking at state == 2'd1 alone tells you nothing about which state it means; you'd have to go check the documentation to be sure. enum replaces those magic numbers with meaningful names:

typedef enum logic [1:0] {
  IDLE,
  RUN,
  DONE
} state_e;
 
state_e cur_state;
 
initial begin
  cur_state = IDLE;
  $display("current state: %s", cur_state.name());
end

A few things worth noting:

  • typedef enum ... state_e; does two things at once: it defines an enumerated type, and typedef gives it the name state_e (more on typedef itself in the next section).
  • logic [1:0] specifies the enum's underlying representation width and 4-state-ness — leave it out and the default underlying type is int (2-state, 32 bits). When describing a hardware state machine, it's common to explicitly specify just enough bits.
  • Every enum variable comes with a built-in .name() method that returns the name string for its current value — much more readable in $display debug output than a bare number.

When a few variables always show up and get passed around together — like a bus transfer's address, data, and valid bit — declaring them as three separate loose variables is both verbose and easy to pass in the wrong order. struct bundles them into a single unit:

// packed struct: contiguous bit layout, synthesizable, usable directly as a port/bus type
typedef struct packed {
  logic [7:0] addr;
  logic [7:0] data;
  logic       valid;
} bus_pkt_t;
 
bus_pkt_t pkt;
 
initial begin
  pkt.addr  = 8'h10;
  pkt.data  = 8'hAB;
  pkt.valid = 1'b1;
end

SystemVerilog structs come in two flavors:

  • packed struct (above): all fields are laid out as one contiguous block of bits, which can be treated as a single vector or used directly as a module port type — a good fit for data that becomes a real hardware signal (like a bus transaction).
  • unpacked struct: fields have no forced contiguous bit layout and can freely mix different widths and kinds of types, closer to a software struct. It's typically used in testbench code that doesn't need to be synthesized:
// unpacked struct: more freedom in field types, common in verification code
typedef struct {
  int addr_width;
  int data_width;
} bus_cfg_t;
 
bus_cfg_t cfg;
 
initial begin
  cfg.addr_width = 32;
  cfg.data_width = 64;
end

typedef: naming types

The previous two sections were already using typedef — its job is to give a type a new name, which you can then use just like a built-in type instead of repeating the full type definition every time. Beyond naming enums and structs, typedef also works directly on simple types:

typedef logic [7:0] byte_t;
 
byte_t data_in, data_out;

The benefit is straightforward: readability (byte_t communicates intent better than logic [7:0]), and consistency — if you later need to widen this type from 8 bits to 16, you only change the one typedef line, and every use of byte_t updates automatically instead of a project-wide search and replace.

string: a variable-length string type

Every type covered so far belongs to the fixed-width integer family. Verification code also constantly needs text — a transaction's name, a log message, error text — and that's exactly what string is for. It's a variable-length string type that behaves much like strings in higher-level languages, with no manual length bookkeeping or C-style null termination:

string name;
string msg;
 
initial begin
  name = "packet0";
  msg  = "unexpected value";
 
  $display("name = %s, length = %0d", name, name.len());
 
  if (name == "packet0")
    $display("name matches!");
end

A string variable can be compared directly with ==/!= (no need for a C-style strcmp), and .len() returns its current length; assigning a new string automatically adjusts the length, with no capacity to declare upfront. Like class (covered in chapter 10), string is a purely verification-oriented feature — it cannot be synthesized — and shows up constantly for transaction labels, configuration info, and assertion failure messages.

Putting it together: enum + struct + typedef

Combining all three gives you data structures that are both compact and readable — a pattern that shows up constantly in later, verification-focused chapters:

typedef enum logic [1:0] {
  PKT_OK,
  PKT_ERR,
  PKT_TIMEOUT
} pkt_status_e;
 
typedef struct {
  int          addr;
  int          data;
  pkt_status_e status;
} pkt_record_t;
 
pkt_record_t log_entry;
 
initial begin
  log_entry.addr   = 32'h1000;
  log_entry.data   = 32'hdead_beef;
  log_entry.status = PKT_OK;
  $display("addr=%0h data=%0h status=%s",
           log_entry.addr, log_entry.data, log_entry.status.name());
end

log_entry clearly communicates what fields a record contains, and because status is an enum, it prints as PKT_OK instead of a bare 0 — that's the readability payoff of combining enum, struct, and typedef.

Summary

  • 4-state types (logic, reg, integer) can represent x/z and suit hardware signals; 2-state types (bit, int, byte, etc.) have no x/z and suit testbench-only uses like counters and loop variables.
  • Uninitialized 4-state variables default to x; uninitialized 2-state variables default to 0 — that difference directly affects whether simulation can surface a "forgot to initialize" bug.
  • enum replaces bare numbers with named states, and comes with a .name() method for readable debug printing.
  • struct bundles related fields into one unit; packed struct is a contiguous, synthesizable bit layout, while unpacked struct is more flexible and common in verification code.
  • typedef names a type, improving readability and letting future changes happen in one place.
  • string is a variable-length string type supporting direct ==/!= comparison and .len(); it's purely verification-oriented and can't be synthesized.

What's the default value of an uninitialized/undriven 4-state variable (like logic) in simulation?

Which statement about packed vs. unpacked structs is correct?

Which built-in method, called on an enum variable, returns the name string for its current value? (method name only, no parentheses)

Which statement about SystemVerilog's string type is correct?