Why Learn Gate Level Modeling? Every synthesized design ends up as a netlist of gates and flip-flops, and that netlist is itself Verilog written at the gate level. Engineers read gate-level code when they debug synthesis results, run gate-level simulations with real timing, or make small fixes directly in a netlist late in a project. In this tutorial, you'll learn the built-in gate primitives and how to describe a circuit with them.

Most digital designs are written at a higher level of abstraction, such as RTL, but some small and deterministic circuits are easier to describe gate by gate, using elements such as AND and OR. Gate level modeling describes a circuit as a set of connected logic gates, so the Verilog code maps one to one onto the circuit's schematic.

12 min read | Beginner Level

What You'll Learn

  • The built-in Verilog gate primitives and their port order
  • Use multi-input gates, multi-output buffers and inverters, and tri-state gates
  • Build a small circuit, a 2 to 1 mux, entirely from primitives
  • How gate-level code relates to synthesized netlists, and common primitive mistakes

Gate Primitives

Verilog provides predefined gate primitives that are instantiated like modules, but without having to be declared. More complex behavior can be described with User Defined Primitives (UDPs).

GateSyntaxDescription
andand u0 (out, i1, i2, ...);Output is the AND of two or more inputs
oror u0 (out, i1, i2, ...);Output is the OR of two or more inputs
xorxor u0 (out, i1, i2, ...);Output is the XOR of two or more inputs
nandnand u0 (out, i1, i2, ...);Output is the inverted AND of two or more inputs
nornor u0 (out, i1, i2, ...);Output is the inverted OR of two or more inputs
xnorxnor u0 (out, i1, i2, ...);Output is the inverted XOR of two or more inputs
bufbuf u0 (out, in);Drives one or more outputs with the value of its single input
notnot u0 (out, in);Drives one or more outputs with the inverse of its single input
bufif1bufif1 u0 (out, in, control);Buffer that drives its output only when control is 1
notif1notif1 u0 (out, in, control);Inverter that drives its output only when control is 1
bufif0bufif0 u0 (out, in, control);Buffer that drives its output only when control is 0
notif0notif0 u0 (out, in, control);Inverter that drives its output only when control is 0

Rules for instantiating primitives:

  • The instance name, such as u0, is optional for primitives, unlike for modules.
  • Connections are always by position. Primitives have no port names, so .a(x) style connections are not allowed.
  • Outputs must connect to nets (wire), not to reg variables, because a primitive drives its output continuously.
  • An optional delay can follow the gate type, such as and #2 u0 (out, a, b);. See Verilog Gate Delay.

And/Or/Xor Gates

The and, or and xor primitives take any number of scalar inputs and drive a single scalar output. The first terminal in the list is always the output, and it updates whenever any of the inputs changes.


module gates (	input a, b,
				output c, d, e);

	and (c, a, b); 	// c is the output, a and b are inputs
	or  (d, a, b);	// d is the output, a and b are inputs
	xor (e, a, b); 	// e is the output, a and b are inputs
endmodule
Truth tables for the and, or and xor gate primitives

module tb;
	reg a, b;
	wire c, d, e;
	integer i;

	gates u0 ( .a(a), .b(b), .c(c), .d(d), .e(e));

	initial begin
		{a, b} = 0;

      $monitor ("[T=%0t a=%0b b=%0b c(and)=%0b d(or)=%0b e(xor)=%0b", $time, a, b, c, d, e);

		for (i = 0; i < 10; i = i+1) begin
			#1 	a <= $random;
				b <= $random;
		end
	end
endmodule
Output
$ /usr/bin/vvp simulation +random_seed+1492585514
[T=0 a=0 b=0 c(and)=0 d(or)=0 e(xor)=0
[T=1 a=0 b=1 c(and)=0 d(or)=1 e(xor)=1
[T=2 a=1 b=1 c(and)=1 d(or)=1 e(xor)=0
[T=4 a=1 b=0 c(and)=0 d(or)=1 e(xor)=1
[T=5 a=1 b=1 c(and)=1 d(or)=1 e(xor)=0
[T=6 a=0 b=1 c(and)=0 d(or)=1 e(xor)=1
[T=7 a=1 b=0 c(and)=0 d(or)=1 e(xor)=1
[T=10 a=1 b=1 c(and)=1 d(or)=1 e(xor)=0

Each line matches the truth tables: c is 1 only when both inputs are 1, d when at least one is 1, and e when they differ. $monitor prints only when a value changes, so time steps where the random inputs repeat, such as T=3, do not appear.

Nand/Nor/Xnor Gates

The inverted versions, nand, nor and xnor, use the same syntax. The design below is the previous example with each primitive replaced by its inverted form.


module gates (	input a, b,
				output c, d, e);

	// Use nand, nor, xnor instead of and, or and xor
	// in this example
	nand (c, a, b); 	// c is the output, a and b are inputs
	nor  (d, a, b);		// d is the output, a and b are inputs
	xnor (e, a, b); 	// e is the output, a and b are inputs
endmodule
Truth tables for the nand, nor and xnor gate primitives

module tb;
	reg a, b;
	wire c, d, e;
	integer i;

	gates u0 ( .a(a), .b(b), .c(c), .d(d), .e(e));

	initial begin
		{a, b} = 0;

      $monitor ("[T=%0t a=%0b b=%0b c(nand)=%0b d(nor)=%0b e(xnor)=%0b", $time, a, b, c, d, e);

		for (i = 0; i < 10; i = i+1) begin
			#1 	a <= $random;
				b <= $random;
		end
	end
endmodule
Output
$ /usr/bin/vvp simulation +random_seed+622900489
[T=0 a=0 b=0 c(nand)=1 d(nor)=1 e(xnor)=1
[T=1 a=0 b=1 c(nand)=1 d(nor)=0 e(xnor)=0
[T=2 a=1 b=1 c(nand)=0 d(nor)=0 e(xnor)=1
[T=4 a=1 b=0 c(nand)=1 d(nor)=0 e(xnor)=0
[T=5 a=1 b=1 c(nand)=0 d(nor)=0 e(xnor)=1
[T=6 a=0 b=1 c(nand)=1 d(nor)=0 e(xnor)=0
[T=7 a=1 b=0 c(nand)=1 d(nor)=0 e(xnor)=0
[T=10 a=1 b=1 c(nand)=0 d(nor)=0 e(xnor)=1

The input sequence is the same as before, and every output is now the inverse of the corresponding output in the previous log.

Like their non-inverted versions, these gates accept more than two inputs. The next example uses four-input and, or and nor gates.


module gates (	input a, b, c, d,
				output x, y, z);

  and (x, a, b, c, d); 	// x is the output, a, b, c, d are inputs
  or  (y, a, b, c, d);	// y is the output, a, b, c, d are inputs
  nor (z, a, b, c, d); 	// z is the output, a, b, c, d are inputs
endmodule

module tb;
	reg a, b, c, d;
	wire x, y, z;
	integer i;

  gates u0 ( .a(a), .b(b), .c(c), .d(d), .x(x), .y(y), .z(z));

	initial begin
      {a, b, c, d} = 0;

      $monitor ("[T=%0t a=%0b b=%0b c=%0b d=%0b x=%0b y=%0b z=%0b", $time, a, b, c, d, x, y, z);

		for (i = 0; i < 10; i = i+1) begin
			#1 	a <= $random;
				b <= $random;
          		c <= $random;
          		d <= $random;

		end
	end
endmodule
Output
$ /usr/bin/vvp simulation +random_seed+1394207508
[T=0 a=0 b=0 c=0 d=0 x=0 y=0 z=1
[T=1 a=0 b=1 c=1 d=1 x=0 y=1 z=0
[T=2 a=1 b=1 c=1 d=0 x=0 y=1 z=0
[T=3 a=1 b=1 c=0 d=1 x=0 y=1 z=0
[T=4 a=1 b=0 c=1 d=0 x=0 y=1 z=0
[T=5 a=1 b=0 c=1 d=1 x=0 y=1 z=0
[T=6 a=0 b=1 c=0 d=0 x=0 y=1 z=0
[T=7 a=0 b=1 c=0 d=1 x=0 y=1 z=0
[T=8 a=1 b=1 c=1 d=0 x=0 y=1 z=0
[T=9 a=0 b=0 c=0 d=1 x=0 y=1 z=0
[T=10 a=0 b=1 c=1 d=1 x=0 y=1 z=0

The last value on each line is the nor output z. It is 1 only at T=0, when all four inputs are 0. The 4-input and output x stays 0 because all four inputs are never 1 at the same time in this run.

Quick Check

Q: What is the output of nor (z, a, b, c, d); when a = 0, b = 0, c = 1 and d = 0?

Show Answer

0. The OR of the inputs is 1 because c is 1, and the nor inverts it. A NOR output is 1 only when every input is 0.

Buf/Not Gates

The buf and not primitives have a single scalar input and one or more outputs. A buffer passes its input to the output unchanged, and an inverter outputs the opposite value. In hardware, buffers are used to strengthen a signal so it can drive long wires or many loads.


module gates (	input a,
				output c, d);

  buf (c, a); 		// c is the output, a is input
  not (d, a);		// d is the output, a is input
endmodule
Truth tables for the buf and not gate primitives

module tb;
	reg a;
	wire c, d;
	integer i;

	gates u0 ( .a(a), .c(c), .d(d));

	initial begin
		a = 0;

      $monitor ("[T=%0t a=%0b c(buf)=%0b d(not)=%0b", $time, a, c, d);

		for (i = 0; i < 10; i = i+1) begin
			#1 	a <= $random;
		end
	end
endmodule
Output
$ /usr/bin/vvp simulation +random_seed+1384864616
[T=0 a=0 c(buf)=0 d(not)=1
[T=2 a=1 c(buf)=1 d(not)=0
[T=8 a=0 c(buf)=0 d(not)=1
[T=9 a=1 c(buf)=1 d(not)=0

For buf and not, the order of terminals is the opposite of the other gates: the last terminal is the input, and all terminals before it are outputs. This allows one gate to drive several outputs, although this form is rarely used.


module gates (	input  a,
				output c, d);

  not (c, d, a); 		// c,d is the output, a is input

endmodule
Output
xcelium> run
[T=0 a=0 c=1 d=1
[T=2 a=1 c=0 d=0
[T=8 a=0 c=1 d=1
[T=9 a=1 c=0 d=0
xmsim: *W,RNQUIE: Simulation is complete.

Both c and d always hold the inverse of a.

Where is Gate Level Code Used?

A synthesis tool's output is a gate-level netlist: a Verilog file that instantiates the standard cells of the target library, such as NAND gates, inverters and flip-flops, and connects them with wires. Verification teams simulate this netlist with real cell delays to confirm that the synthesized design behaves like the RTL and meets timing.

Bufif/Notif

The bufif and notif primitives are a buffer and an inverter with an extra control input. They have a valid output only when the control signal is active; otherwise their output is high impedance (z), which means the gate is disconnected from the wire.

  • bufif1 and notif1 drive their output when control is 1.
  • bufif0 and notif0 drive their output when control is 0.
Truth tables for bufif1 and notif1, with the output at z when control is 0 Truth tables for bufif0 and notif0, with the output at z when control is 1

Because a disabled gate drives z, several tri-state gates can share one wire, as long as only one of them is enabled at a time. This is how a shared bus works. In the example below, sel enables exactly one of two buffers, which makes a 2 to 1 multiplexer:


module mux2_tri (input a, b, sel, output y);

  bufif0 u0 (y, a, sel);    // Drives y with a when sel = 0
  bufif1 u1 (y, b, sel);    // Drives y with b when sel = 1

endmodule

Both gates drive the same net y. For each value of sel, one of them drives the value and the other drives z, so the net resolves to the driven value. If sel is x, both gates may drive, and y becomes x wherever a and b differ.

Quick Check

Q: What is the output of notif0 (y, in, ctrl); when in = 1 and ctrl = 1, and when in = 1 and ctrl = 0?

Show Answer

With ctrl = 1, the gate is disabled, so y is z. With ctrl = 0, it is enabled and inverts its input, so y is 0.

Tri-State I/O Pads

A bidirectional chip pin, such as a data pin on a memory bus or an I2C line, is driven through a tri-state buffer in the I/O pad. When the chip transmits, the buffer is enabled; when it receives, the buffer drives z so another device can drive the pin. Pad models in a netlist use primitives like bufif1 to describe this behavior.

Building a Circuit with Primitives

Larger circuits are built by connecting primitives with internal wires, exactly as in a schematic. A 2 to 1 multiplexer computes out = (a & ~sel) | (b & sel), which takes one inverter, two AND gates and one OR gate:

Gate-level 2 to 1 multiplexer: sel is inverted by not u0 to make sel_n; and u1 combines a with sel_n into a_sel; and u2 combines b with sel into b_sel; or u3 combines a_sel and b_sel into out

module mux2_gate (input a, b, sel, output out);

  wire sel_n, a_sel, b_sel;      // Internal connections between gates

  not u0 (sel_n, sel);           // sel_n = ~sel
  and u1 (a_sel, a, sel_n);      // a passes when sel = 0
  and u2 (b_sel, b, sel);        // b passes when sel = 1
  or  u3 (out, a_sel, b_sel);    // Combine the two paths

endmodule

Each line of code corresponds to one gate in the schematic, and each wire to one connection. Compare this with the same mux written in one line, assign out = sel ? b : a;, in Verilog 4 to 1 Multiplexer: the RTL version is shorter and leaves the choice of gates to the synthesis tool.

Synthesis result: Synthesis tools accept gate primitives and map each one to an equivalent cell of the target library, or, on an FPGA, merge them into LUTs. The tool may still optimize the logic, so the final netlist does not always contain one cell per primitive. If an exact structure must be kept, such as in a clock gating or synchronizer cell, engineers instantiate the specific library cells directly and mark them so the tool does not change them.

Late Fixes in Netlists

Close to the end of a chip project, a small logic bug may be fixed by editing the gate-level netlist directly instead of rerunning synthesis, because rerunning it could change timing across the whole design. These engineering change orders (ECOs) add or rewire a few gates, using spare cells that were placed on the chip in advance for this purpose.

Common Beginner Mistakes

Mistake #1: Putting the output in the wrong position

Wrong: and (a, b, c); intending c = a & b
Right: and (c, a, b);

Why? For multi-input gates, the first terminal is always the output. The wrong version makes a the output, driven by b & c, which creates a second driver on an input signal and results in x values. For buf and not, the rule is reversed: the last terminal is the input.

Mistake #2: Connecting a primitive output to a reg

Wrong: reg y; and (y, a, b);
Right: wire y; and (y, a, b);

Why? A primitive drives its output continuously, like an assign statement, so the output must be a net. A reg can only be assigned in procedural blocks, and the compiler reports an error.

Mistake #3: Using named port connections

Wrong: and u0 (.out(c), .in1(a), .in2(b));
Right: and u0 (c, a, b);

Why? Built-in primitives have no named ports, so they can only be connected by position. Named connections work for modules and for library cells, which are modules, but not for primitives.

Mistake #4: Enabling two tri-state drivers at once, or none

Wrong: Two bufif1 gates on the same wire with enables that can both be 1, or both be 0 with nothing else driving the wire
Right: Make the enables mutually exclusive, as in mux2_tri, and add a defined value for the case where no driver is enabled if the wire must not float

Why? Two enabled drivers with different values give x in simulation and short-circuit current in hardware. No enabled driver leaves the wire at z, and logic reading a floating wire sees an unpredictable value.

Practice Exercise

Challenge: Gate-Level Full Adder

Write a module full_adder_gate with inputs a, b and cin, and outputs sum and cout, using only gate primitives. Use sum = a ^ b ^ cin and cout = (a & b) | (cin & (a ^ b)).

Hint

Compute a ^ b once into an internal wire, and reuse it for both sum and cout. You need two XOR gates, two AND gates and one OR gate. See Verilog Full Adder for the RTL version.

Solution

module full_adder_gate (input a, b, cin, output sum, cout);

  wire ab_x, ab_a, c_and;

  xor u0 (ab_x, a, b);           // a ^ b
  xor u1 (sum, ab_x, cin);       // sum = a ^ b ^ cin
  and u2 (ab_a, a, b);           // Carry generated by a and b
  and u3 (c_and, ab_x, cin);     // Carry propagated from cin
  or  u4 (cout, ab_a, c_and);    // cout

endmodule

For a = 1, b = 1 and cin = 1: ab_x = 0, so sum = 1; ab_a = 1, so cout = 1. That gives binary 11, which is 3. The shared ab_x wire is the same sharing a synthesis tool would find when optimizing the RTL version.

Summary

In this tutorial, you learned:

  • Verilog has built-in primitives for and, or, xor, nand, nor, xnor, buf, not and the tri-state bufif and notif gates
  • For multi-input gates, the first terminal is the output; for buf and not, the last terminal is the input
  • Primitives connect by position only, their outputs must be nets, and their instance names are optional
  • Multi-input gates accept any number of inputs, and buf and not can drive several outputs
  • bufif and notif drive z when disabled, which lets several drivers share one wire
  • Circuits are built by connecting primitives with internal wires, matching the schematic gate for gate
  • Synthesized netlists are gate-level Verilog that instantiate library cells
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5