Data Types
Verilog has two fundamentally different categories of data type — nets and variables — and the distinction isn't cosmetic: it reflects whether a signal is driven continuously by connected hardware or assigned a value by procedural code. Confusing the two is one of the most common beginner errors, and one the compiler will reject outright rather than silently misinterpret.
Nets: wire and friends
A net represents a physical wire — it has no storage of its own; its value is whatever the hardware driving it currently outputs. If nothing drives a net, its value is z (high-impedance), not 0.
wire sum, cout; // single-bit wires
wire [3:0] result; // a 4-bit vector — a bus
wire is by far the most common net type — every port and every signal driven by an assign statement or a gate/module output is a wire unless declared otherwise. A few specialized net types exist for cases where multiple drivers connect to the same net and need a defined resolution rule:
| Net type | Resolves multiple drivers as |
|---|---|
wire | Illegal for most simulators unless drivers agree (or explicitly modeled as tri-state); the default, single-driver-expected net |
wand | Wired-AND — the net reads as the AND of all its drivers |
wor | Wired-OR — the net reads as the OR of all its drivers |
tri | Behaves like wire; the name is purely documentation that multiple drivers (e.g., a tri-state bus) are expected |
wand/wor are rare in modern RTL — most multi-driver situations (shared buses) are instead handled explicitly with tri-state buffers and an enable signal — but they still show up in legacy code and are worth recognizing on sight.
Variables: reg and friends
A variable holds a value that persists until a procedural statement (inside an always or initial block) explicitly assigns it a new one. Despite the name, reg does not always mean "this becomes a flip-flop":
reg [3:0] count; // a 4-bit variable — may synthesize to flip-flops, or to nothing at all
Whether a reg becomes real sequential hardware (a flip-flop/latch) or vanishes entirely as just combinational logic depends entirely on how it's assigned inside an always block — that rule is covered in full later in this topic, once always blocks themselves have been introduced. At this stage, the only thing to fix in memory is the syntax rule: anything assigned inside a procedural block (always/initial) must be declared as a variable (reg, or the wider types below), never a wire.
Nets and variables also differ in their default value before anything ever assigns or drives them: an undriven wire reads as z (high-impedance, as above), while a declared-but-not-yet-assigned reg reads as x (unknown) — never a silent, misleading 0. This is deliberate: Verilog's simulation values are four-state (0, 1, x for unknown, z for high-impedance), and a reg starting at x makes an accidentally-unassigned variable visibly obvious in a waveform instead of masquerading as a valid 0.
Other variable types, used far less often than reg but worth knowing:
| Type | Use |
|---|---|
integer | A 32-bit signed variable, commonly used as a loop counter (see Loops) — not intended to synthesize into hardware itself |
real | Floating-point — simulation-only, has no synthesizable hardware equivalent |
time | 64-bit, holds simulation time values (from $time) — simulation-only |
Vectors: multi-bit signals
Both nets and variables can be declared as vectors — multi-bit buses — by giving a bit-range in square brackets:
wire [3:0] a, b; // two 4-bit input buses
wire [4:0] sum; // 5 bits — room for the carry-out of a 4-bit add
[3:0] means the vector is indexed from bit 3 (most significant) down to bit 0 (least significant) — [msb:lsb], always in that order by convention, though Verilog also permits (and virtually no real code uses) the reversed [0:3] form. Individual bits and sub-ranges are accessed the same way in either a net or variable vector:
a[0] // bit 0 of a
a[3:2] // the top two bits of a, as a 2-bit sub-vector
Worked example: a 4-bit ripple-carry adder's wiring
Combinational Logic Design built an n-bit ripple-carry adder by chaining full adders, each one's carry-out feeding the next one's carry-in:
The A3 B3, A2 B2, etc. buses are naturally expressed as one 4-bit vector each, sliced per bit, and the inter-stage carries need their own wire vector to connect one full adder's cout to the next one's cin:
module ripple_adder_4bit (
input [3:0] a,
input [3:0] b,
input cin,
output [3:0] sum,
output cout
);
wire [3:0] carry; // carry[0] = FA0's cout = FA1's cin, and so on
full_adder fa0 (.a(a[0]), .b(b[0]), .cin(cin), .sum(sum[0]), .cout(carry[0]));
full_adder fa1 (.a(a[1]), .b(b[1]), .cin(carry[0]), .sum(sum[1]), .cout(carry[1]));
full_adder fa2 (.a(a[2]), .b(b[2]), .cin(carry[1]), .sum(sum[2]), .cout(carry[2]));
full_adder fa3 (.a(a[3]), .b(b[3]), .cin(carry[2]), .sum(sum[3]), .cout(cout));
endmodule
Every carry[n] wire in this code is a physical piece of copper (or metal, on a real chip) connecting exactly two gates — there's nothing abstract about it. Parameters and generate revisits this exact circuit to show how to make its width a parameter instead of hand-writing four instances.
Arrays
Verilog also supports arrays — a collection of same-typed elements, indexed separately from any bit-range:
reg [7:0] mem [0:255]; // 256 entries, each 8 bits wide — a small memory
Read this as "mem is an array of 256 elements ([0:255]), each one an 8-bit reg ([7:0])" — the two bracket pairs mean different things, and mixing them up is a common source of confusion. Arrays and Memories covers this in depth, including how this exact declaration is how ROM and RAM are modeled behaviorally.
What's next
Every example so far has quietly used operators (^, &) without explaining them. The next page covers Verilog's full operator set — arithmetic, logical, bitwise, relational, reduction, concatenation — the vocabulary needed to write the expressions on the right-hand side of an assign.