Gate-level modeling is the layer where logic meets silicon — you connect named primitive gates with wires, just as a synthesis tool would after compiling RTL. Understanding it lets you read post-synthesis netlists, model propagation delays accurately, and appreciate what happens beneath the assign and always abstractions.
Verilog provides 26 built-in primitives. The most common fall into three groups:
| Group | Primitives |
|---|---|
| Basic logic | and, or, not, nand, nor, xor, xnor, buf |
| Tri-state | bufif0, bufif1, notif0, notif1 |
| MOS switches | nmos, pmos, cmos, rnmos, rpmos, rcmos, tran, tranif0, tranif1 |
MOS switch primitives model transistor-level behavior (charge sharing, bidirectional current) and are rarely used in RTL or verification. You will encounter the first two groups in gate-level netlists.
// Syntax: gate_type [#(delay)] instance_name (output, input1, input2, ...); and g1(y, a, b); // y = a & b or g2(y, a, b, c); // y = a | b | c (3-input) not g3(y, a); // y = ~a nand g4(y, a, b); // y = ~(a & b) xor g5(y, a, b); // y = a ^ b // buf can drive multiple outputs from one input buf g6(y1, y2, y3, a); // y1=y2=y3=a (fan-out) // tri-state: bufif1(output, data, enable) bufif1 g7(y, a, en); // y = a when en=1, y = Z when en=0 bufif0 g8(y, a, en); // y = a when en=0, y = Z when en=1
| a | b | and | or | nand | nor | xor | xnor |
|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 1 | 1 | 0 | 1 |
| 0 | 1 | 0 | 1 | 1 | 0 | 1 | 0 |
| 1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 |
| 1 | 1 | 1 | 1 | 0 | 0 | 0 | 1 |
| X | 0 | 0 | X | 1 | X | X | X |
| 1 | X | X | 1 | X | 0 | X | X |
Gates understand all four logic values (0, 1, X, Z). Verilog applies pessimistic X-propagation: any ambiguous input produces X output unless one controlling input forces a known output (e.g., AND with a 0 input is always 0).
| Primitive | enable | Output |
|---|---|---|
bufif1 | 1 | data |
| 0 | Z (high impedance) | |
| X | X or Z (uncertain) | |
bufif0 | 0 | data |
| 1 | Z (high impedance) | |
| X | X or Z (uncertain) |
// Simple bidirectional bus with tri-state drivers module bus_driver ( output bus, input d0, d1, d2, input en0, en1, en2 ); bufif1 b0(bus, d0, en0); // driver 0 bufif1 b1(bus, d1, en1); // driver 1 bufif1 b2(bus, d2, en2); // driver 2 endmodule
// Single delay — applies to all transitions and #5 g1(y, a, b); // Two delays — (rise_delay, fall_delay) and #(3,5) g2(y, a, b); // 3ns rise, 5ns fall // Three delays — (rise, fall, turn-off-to-Z) bufif1 #(2,3,4) b1(y, a, en); // 2ns to drive data, 3ns to go low, 4ns to release to Z // Min:typ:max delay — simulator picks one (default: typ) and #(2:3:5, 3:4:6) g3(y, a, b);
Rise delay: time from output becoming 0 to 1. Fall delay: time from 1 to 0. Turn-off delay: time to transition to high-impedance Z. Delays are simulation-only — synthesis ignores them and uses timing from the standard cell library instead.
A netlist is just Verilog with only wire declarations and gate instantiations — no always, no assign. Synthesis tools produce netlists like this, referencing cells from a technology library instead of built-in primitives.
module xor_from_nand (output y, input a, b); wire n1, n2, n3; nand g1(n1, a, b); nand g2(n2, a, n1); nand g3(n3, b, n1); nand g4(y, n2, n3); endmodule
This implements XOR using only NAND gates — a classic NAND-only circuit. The internal wires n1, n2, n3 carry intermediate signals between stages.
module full_adder ( output sum, cout, input a, b, cin ); wire s1, c1, c2; xor g1(s1, a, b); // half-adder sum and g2(c1, a, b); // half-adder carry xor g3(sum, s1, cin); // full-adder sum and g4(c2, s1, cin); // carry from second stage or g5(cout, c1, c2); // final carry out endmodule // 4-bit ripple-carry adder using gate-level full_adder module rca4 (output [3:0] sum, output cout, input [3:0] a, b, input cin); wire c1, c2, c3; full_adder fa0(sum[0], c1, a[0], b[0], cin); full_adder fa1(sum[1], c2, a[1], b[1], c1); full_adder fa2(sum[2], c3, a[2], b[2], c2); full_adder fa3(sum[3], cout, a[3], b[3], c3); endmodule
A 2-to-1 mux selects between inputs a and b based on select s. Boolean: y = (~s & a) | (s & b).
module mux2to1 (output y, input a, b, s); wire ns, t1, t2; not g1(ns, s); // ~s and g2(t1, a, ns); // a & ~s and g3(t2, b, s); // b & s or g4(y, t1, t2); // y = t1 | t2 endmodule
UDPs let you define custom gates using a truth table — useful for modeling non-standard cells or for simulation performance when the built-in primitives aren't a perfect fit.
primitive my_mux (y, a, b, s); output y; input a, b, s; table // a b s : y 0 ? 0 : 0; // s=0 → select a 1 ? 0 : 1; ? 0 1 : 0; // s=1 → select b ? 1 1 : 1; 0 0 ? : 0; // both same → output that value 1 1 ? : 1; endtable endprimitive
primitive d_latch (q, d, en); output reg q; // sequential: output is reg input d, en; initial q = 0; table // d en : q q_next 1 1 : ? : 1; // en=1, d=1 → latch 1 0 1 : ? : 0; // en=1, d=0 → latch 0 ? 0 : ? : -; // en=0 → hold (- means no change) endtable endprimitive
inputs : current_state : next_state. A - in next_state means "no change — hold current value". A ? matches any logic value (0, 1, or X).In a real VLSI flow you rarely write gate-level Verilog by hand. Instead:
always and assign).// Simplified post-synthesis netlist fragment (real names vary by PDK) module counter4_synth (clk, rst_n, q); input clk, rst_n; output [3:0] q; wire n1, n2, n3, n4; // Standard cell instances (names from 45nm cell library) DFFRX1 q_reg0 (.D(n1), .CK(clk), .RN(rst_n), .Q(q[0])); DFFRX1 q_reg1 (.D(n2), .CK(clk), .RN(rst_n), .Q(q[1])); XOR2X1 xor0 (.A(q[0]), .B(q[1]), .Y(n2)); INVX1 inv0 (.A(q[0]), .Y(n1)); // ... more cells ... endmodule
Gate-level simulation of this netlist with an SDF file checks that all flip-flops meet setup and hold times at the actual clock frequency — the final sign-off step before tapeout.