Gate-Level Modeling
Gate-level modeling describes a circuit as an explicit netlist of primitive logic gates wired together — the closest Verilog gets to drawing a schematic in text. Verilog has a small set of built-in gate primitives that need no declaration, just instantiation, the same way or (cout, ha1_cout, ha2_cout); was used in Modules and Ports.
Built-in gate primitives
| Primitive | Function | Instantiation form |
|---|---|---|
and, nand | AND, NAND | and (out, in1, in2, ...); — 2+ inputs allowed |
or, nor | OR, NOR | or (out, in1, in2, ...); |
xor, xnor | XOR, XNOR | xor (out, in1, in2, ...); |
not | Inverter | not (out, in); — exactly one input |
buf | Non-inverting buffer | buf (out, in); |
Every primitive's first port is its output, followed by one or more inputs — the reverse of the port order Verilog uses for module ports, and worth double-checking the first few times you write one. An optional instance name is allowed but not required (and a1 (w, x, y); or just and (w, x, y);), unlike user-defined modules, which effectively always want one for waveform/debug purposes.
and/or/xor (and their inverted forms) take exactly one output followed by two or more inputs. buf and not flip that: they take exactly one input, but can drive multiple outputs from it — buf (out1, out2, in); and not (out1, out2, in); are both legal, each output independently following (or inverting) the single input.
The full adder, purely at the gate level
The full adder's minimized equations —
Sum = A ⊕ B ⊕ Cin
Cout = AB + BCin + ACin
— map onto primitive gates with no module hierarchy at all, one primitive per term:
module full_adder_gates (
input a,
input b,
input cin,
output sum,
output cout
);
wire w1; // a ^ b, an intermediate term shared by both outputs
wire p1, p2, p3; // the three AND terms of Cout
xor (w1, a, b);
xor (sum, w1, cin);
and (p1, a, b);
and (p2, b, cin);
and (p3, a, cin);
or (cout, p1, p2, p3);
endmodule
This is the literal gate-level circuit the equations describe:
a ──┬────┐
│ ┌─┴─┐
│ │XOR│── w1 ──┐
b ──┼──┤ │ ┌─┴─┐
│ └───┘ │XOR│──── sum
│ cin ──┤ │
│ └───┘
a ──┼────┬─────────────┐
│ ┌─┴─┐ │
b ──┴──│AND│── p1 ──┐ │
└───┘ │ │
b ────┬──────┐ │ │
┌─┴─┐ │ ┌─┴───┴─┐
cin ─┤AND│── p2 ───│ OR │──── cout
└───┘ │ (3-in)│
a ────┬──────┐ └───────┘
┌─┴─┐ │
cin ─┤AND│── p3 ──────┘
└───┘
Every wire in this module (w1, p1, p2, p3) is a physical net connecting exactly two gate terminals — there's no abstraction left to strip away; this is the transistor-adjacent circuit, modulo the actual gate-to-transistor mapping a standard-cell library performs.
Gate delays
Primitives accept an optional delay, modeling real propagation delay for simulation:
and #(2) a1 (p1, a, b); // p1 settles 2 time units after a or b changes
Delays on primitives are a simulation-only concept for visualizing timing behavior — synthesis tools ignore any delay you write and instead compute real delay from the target technology library after place-and-route. Never rely on a hand-written delay value to make synthesized hardware correct; that's what static timing analysis and constraints are for, not source-code delay annotations.
Why gate-level modeling matters despite being rarely hand-written
Almost no one hand-writes RTL at the gate-primitive level in modern chip design — it's exactly what a synthesis tool outputs, not what an engineer types. You'll encounter it reading a post-synthesis or post-place-and-route netlist, or in small structural glue logic (clock-gating cells, scan-chain stitching) where explicit gate-level control matters. Knowing the primitives is what lets you actually read that netlist instead of treating it as opaque tool output.
What's next
The full adder above is fully specified structurally — every gate named, every wire connected by hand. The next page covers dataflow modeling, which describes the same circuit as an equation instead of a gate list, using assign — the style already previewed informally in Operators and Expressions.