Skip to main content

5. Gate-level modeling

5.1 The idea​

At gate level you instantiate built-in primitives — and, or, nand, nor, xor, xnor, not, buf — and connect them with wires. You are describing a netlist by hand.

5.2 Primitive syntax​

gate_type [instance_name] (output, input1, input2, ...);

The output always comes first, then the inputs. This is the opposite of most people's intuition and is the single most common gate-level bug.

For not and buf there is one input:

not [instance_name] (output, input);
buf [instance_name] (output, input);

Several instances may share one line:

nand g1 (y1, a, b),
g2 (y2, c, d);

5.3 Complete primitive list​

TypeInputsFunction
and2+a & b & …
or2+a | b | …
nand2+~(a & b & …)
nor2+~(a | b | …)
xor2+a ^ b ^ …
xnor2+~(a ^ b ^ …)
not1~a
buf1a

5.4 Example 1 — 2-input NAND​

// examples/gate_nand.v
module gate_nand (a, b, y);

input a, b;
output y;

nand g1 (y, a, b);

endmodule

One instance named g1, output y first, inputs a and b after. This produces exactly the same circuit as the switch-level NAND of lesson 4.

Truth table:

aby
001
011
101
110

5.5 Example 2 — 1-bit full adder​

A full adder sums three bits and produces sum and carry:

sum = a ⊕ b ⊕ cin
carry = (a · b) + ((a ⊕ b) · cin)

Building it from primitives requires intermediate wires:

// examples/gate_full_adder.v
module gate_full_adder (a, b, cin, sum, carry);

input a, b, cin;
output sum, carry;

wire s1, c1, c2;

xor g1 (s1, a, b);
xor g2 (sum, s1, cin);
and g3 (c1, a, b);
and g4 (c2, s1, cin);
or g5 (carry, c1, c2);

endmodule

Truth table:

cinbasumcarry
00000
00110
01010
01101
10010
10101
11001
11111

This example demonstrates the essential gate-level skill: decomposing a Boolean expression into a netlist, declaring every intermediate signal as a wire, and connecting primitives in the right port order.

5.6 Structural hierarchy​

Gate-level modules can be instantiated inside other modules to build larger circuits. This is called structural or hierarchical modeling:

// A 2-bit ripple-carry adder built from two full adders
module ripple_adder2 (a, b, cin, sum, cout);

input [1:0] a, b;
input cin;
output [1:0] sum;
output cout;

wire c1;

gate_full_adder fa0 (.sum(sum[0]), .carry(c1), .a(a[0]), .b(b[0]), .cin(cin));
gate_full_adder fa1 (.sum(sum[1]), .carry(cout), .a(a[1]), .b(b[1]), .cin(c1));

endmodule

Named port connections (.port(signal)) are strongly preferred over positional ones: they are self-documenting and immune to port-order changes.

5.7 Common mistakes​

MistakeConsequenceFix
Inputs before outputWrong netlist, silent errorOutput comes first in gate primitives
Declaring the output as regCompile errorGate outputs drive wire
Forgetting to declare an intermediate signalImplicit 1-bit wire (a warning in -Wall)Declare every wire explicitly
Leaving an output unconnectedFloating netConnect every output

Compile with -Wall — Icarus Verilog will flag undeclared nets.

5.8 When to use gate level​

  • Teaching Boolean decomposition and netlists.
  • Reading synthesis output. Synthesis produces gate-level Verilog; you must be able to read it.
  • Small, timing-critical structures where you want a specific gate arrangement.
  • Library models (with timing annotations added).

For everything else, dataflow or behavioral is shorter, clearer, and lets the synthesis tool optimise.

5.9 Key takeaways​

  1. Gate primitives: gate_type [name] (output, inputs...); — output first.
  2. Intermediate signals must be declared as wire.
  3. Hierarchy is built by instantiating modules with named port connections.
  4. Gate level shows you what synthesis produces; it is not how you write production RTL.

Next: Dataflow modeling →