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).
| Gate | Syntax | Description |
|---|---|---|
| and | and u0 (out, i1, i2, ...); | Output is the AND of two or more inputs |
| or | or u0 (out, i1, i2, ...); | Output is the OR of two or more inputs |
| xor | xor u0 (out, i1, i2, ...); | Output is the XOR of two or more inputs |
| nand | nand u0 (out, i1, i2, ...); | Output is the inverted AND of two or more inputs |
| nor | nor u0 (out, i1, i2, ...); | Output is the inverted OR of two or more inputs |
| xnor | xnor u0 (out, i1, i2, ...); | Output is the inverted XOR of two or more inputs |
| buf | buf u0 (out, in); | Drives one or more outputs with the value of its single input |
| not | not u0 (out, in); | Drives one or more outputs with the inverse of its single input |
| bufif1 | bufif1 u0 (out, in, control); | Buffer that drives its output only when control is 1 |
| notif1 | notif1 u0 (out, in, control); | Inverter that drives its output only when control is 1 |
| bufif0 | bufif0 u0 (out, in, control); | Buffer that drives its output only when control is 0 |
| notif0 | notif0 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 toregvariables, 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
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
$ /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
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
$ /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
$ /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
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
$ /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
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.
bufif1andnotif1drive their output whencontrolis 1.bufif0andnotif0drive their output whencontrolis 0.
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:
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