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 be0,1,x(unknown), orz(high-impedance). - 2-state types:
bit,byte,shortint,int,longint, and more — each bit can only be0or1, nox/z.
| Type | Width | 4-state? |
|---|---|---|
logic / reg | Specified at declaration (defaults to 1 bit) | Yes |
wire | Specified at declaration (defaults to 1 bit) | Yes |
integer | 32 bits | Yes |
bit | Specified at declaration (defaults to 1 bit) | No |
byte | 8 bits | No |
shortint | 16 bits | No |
int | 32 bits | No |
longint | 64 bits | No |
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
endmoduleThis 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());
endA few things worth noting:
typedef enum ... state_e;does two things at once: it defines an enumerated type, andtypedefgives it the namestate_e(more ontypedefitself 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 isint(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$displaydebug output than a bare number.
struct: bundling related fields together
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;
endSystemVerilog 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;
endtypedef: 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!");
endA 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());
endlog_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 representx/zand suit hardware signals; 2-state types (bit,int,byte, etc.) have nox/zand suit testbench-only uses like counters and loop variables. - Uninitialized 4-state variables default to
x; uninitialized 2-state variables default to0— that difference directly affects whether simulation can surface a "forgot to initialize" bug. enumreplaces bare numbers with named states, and comes with a.name()method for readable debug printing.structbundles related fields into one unit;packed structis a contiguous, synthesizable bit layout, whileunpacked structis more flexible and common in verification code.typedefnames a type, improving readability and letting future changes happen in one place.stringis 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?