The previous article introduced the built-in gate primitives such as and, or, xor and not. This article puts them to work on four classic combinational circuits: a multiplexer, a full adder, a decoder and a priority encoder. To write Verilog code with gates, you first need to know how the gates connect, so each example starts from the Boolean equation and the schematic, and then translates them line by line into primitives.

12 min read | Beginner Level

What You'll Learn

  • Turn a Boolean equation into a gate-level Verilog module, one primitive per gate
  • Build a 2x1 mux, a full adder, a 2x4 decoder and a 4x2 priority encoder from primitives
  • Read the simulation logs to check each circuit against its truth table
  • What synthesis does with gate-level code, and the common mistakes in these circuits

Example #1: 2x1 Multiplexer

A 2x1 multiplexer passes one of two inputs to its output, chosen by a select line. In this version, sel = 1 picks a and sel = 0 picks b:

out = (a & sel) | (b & ~sel)

That equation needs one NOT gate for ~sel, two AND gates (one per data path) and one OR gate to merge the two paths.

Gate-level 2x1 multiplexer mux_2x1: sel is inverted by a not gate into sel_n; one and gate combines a with sel into out_0; another and gate combines b with sel_n into out_1; an or gate combines out_0 and out_1 into out

module mux_2x1 ( input a, b, sel,
                 output out);
  wire sel_n;     // Inverted select
  wire out_0;     // a path, active when sel = 1
  wire out_1;     // b path, active when sel = 0

  not (sel_n, sel);

  and (out_0, a, sel);
  and (out_1, b, sel_n);

  or (out, out_0, out_1);
endmodule

The output port out is a wire by default, which is what a primitive output needs. At any time only one AND gate can be enabled, so the OR gate simply forwards whichever path is active.

This mux uses the opposite select convention from the mux2_gate example in the previous article, where sel = 0 picked a. Neither is wrong, but always check which input the select picks before you connect a mux someone else wrote.

The testbench drives random values onto the three inputs once every time unit. $random returns a 32-bit number, and assigning it to a 1-bit reg keeps only the lowest bit, so each input gets a random 0 or 1.


module tb;
  reg a, b, sel;
  wire out;
  integer i;

  mux_2x1 u0 ( .a(a), .b(b), .sel(sel), .out(out));

  initial begin
    {a, b, sel} <= 0;

    $monitor ("T=%0t a=%0b b=%0b sel=%0b out=%0b", $time, a, b, sel, out);

    for (i = 0; i < 10; i = i+1) begin
      #1  a <= $random;
          b <= $random;
          sel <= $random;
    end
  end
endmodule
Output
ncsim> run
T=0 a=0 b=0 sel=0 out=0
T=1 a=0 b=1 sel=1 out=0
T=2 a=1 b=1 sel=1 out=1
T=3 a=1 b=0 sel=1 out=1
T=6 a=0 b=1 sel=0 out=1
T=7 a=1 b=1 sel=0 out=1
T=8 a=1 b=0 sel=0 out=0
T=9 a=0 b=1 sel=0 out=1
T=10 a=1 b=1 sel=1 out=1
ncsim: *W,RNQUIE: Simulation is complete.

At T=1, sel = 1 picks a = 0, so out = 0 even though b = 1. At T=6, sel = 0 picks b = 1. T=4 and T=5 are missing because the random values happened to repeat, and $monitor only prints when something changes.

Synthesis result: The four primitives describe a 2 to 1 multiplexer. A synthesis tool usually recognizes the pattern and maps it to a single MUX2 cell from the library, or into part of a LUT on an FPGA, rather than keeping four separate gates. The gate-level code fixes the function, not the final cells.

Quick Check

With a = 1, b = 0 and sel = 0, what are out_0, out_1 and out?

Show Answer

out_0 = a & sel = 0, out_1 = b & sel_n = 0 & 1 = 0, so out = 0. With sel = 0 the mux forwards b, which is 0. This is the T=8 line of the log.

Full Adder

A full adder adds three 1-bit inputs, a, b and a carry in cin, and produces a 1-bit sum and a carry out cout:

sum  = a ^ b ^ cin
cout = (a & b) | (cin & (a ^ b))

The term a ^ b appears in both equations, so it is computed once into the internal wire s1 and shared. net1 is the carry that a and b generate on their own, and net2 is the carry that passes through from cin when exactly one of a and b is 1.

Gate-level full adder fa: an xor gate combines a and b into s1; an and gate combines a and b into net1; a second xor gate combines s1 and cin into sum; a second and gate combines s1 and cin into net2; an or gate combines net1 and net2 into cout

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

  wire s1, net1, net2;

  xor (s1, a, b);         // s1 = a ^ b, shared by sum and cout
  and (net1, a, b);       // Carry generated by a and b

  xor (sum, s1, cin);     // sum = a ^ b ^ cin
  and (net2, s1, cin);    // Carry propagated from cin

  or (cout, net1, net2);

endmodule

module tb;
  reg a, b, cin;
  wire sum, cout;
  integer i;

  fa u0 ( .a(a), .b(b), .cin(cin),
          .sum(sum), .cout(cout));

  initial begin
    {a, b, cin} <= 0;

    $monitor ("T=%0t a=%0b b=%0b cin=%0b cout=%0b sum=%0b",
              $time, a, b, cin, cout, sum);

    for (i = 0; i < 10; i = i+1) begin
      #1  a <= $random;
          b <= $random;
          cin <= $random;
    end
  end
endmodule
Output
ncsim> run
T=0 a=0 b=0 cin=0 cout=0 sum=0
T=1 a=0 b=1 cin=1 cout=1 sum=0
T=2 a=1 b=1 cin=1 cout=1 sum=1
T=3 a=1 b=0 cin=1 cout=1 sum=0
T=6 a=0 b=1 cin=0 cout=0 sum=1
T=7 a=1 b=1 cin=0 cout=1 sum=0
T=8 a=1 b=0 cin=0 cout=0 sum=1
T=9 a=0 b=1 cin=0 cout=0 sum=1
T=10 a=1 b=1 cin=1 cout=1 sum=1
ncsim: *W,RNQUIE: Simulation is complete.

Read {cout, sum} as a 2-bit number and it is the count of 1s among the inputs. At T=2 all three inputs are 1, so {cout, sum} = 11, which is 3. At T=1 two inputs are 1, giving 10, which is 2.

Synthesis result: Synthesis produces two XOR-type cells and an AND-OR structure for the carry, often merged into a single AND-OR-INVERT or majority cell. Many standard cell libraries also contain a dedicated full adder cell, and the tool may replace all five gates with that one cell. On an FPGA, the adder maps onto the LUT and the dedicated carry chain.

Quick Check

For a = 1, b = 1 and cin = 0, what are s1, net1, net2, sum and cout?

Show Answer

s1 = 1 ^ 1 = 0, net1 = 1 & 1 = 1, net2 = 0 & 0 = 0, sum = 0 ^ 0 = 0 and cout = 1 | 0 = 1. One plus one gives binary 10, which matches the T=7 line of the log.

Adders in Every Datapath

The full adder is the building block of the adders in a processor's ALU, address calculation units and counters. Chaining N full adders, with each cout feeding the next cin, gives an N-bit ripple carry adder. Faster adders used in high-speed datapaths reorganize the same generate (a & b) and propagate (a ^ b) signals that appear here as net1 and s1.

2x4 Decoder

A 2x4 decoder turns a 2-bit input code into four outputs, where exactly one output is 1 for each code. An enable input en forces every output to 0 when it is low. Each output is a 3-input AND of en with the true or inverted form of x and y:

enxyabcd
0XX0000
1000001
1010010
1100100
1111000
Gate-level 2x4 decoder dec_2x4: two not gates make x_n and y_n; four 3-input and gates each combine en with one combination of x or x_n and y or y_n, giving output a for x=1 y=1, b for x=1 y=0, c for x=0 y=1 and d for x=0 y=0

module dec_2x4 ( input x, y, en,
                 output a, b, c, d);

  wire x_n, y_n;

  not (x_n, x);
  not (y_n, y);

  and (a,  x,   y,   en);    // Code 11
  and (b,  x,   y_n, en);    // Code 10
  and (c,  x_n, y,   en);    // Code 01
  and (d,  x_n, y_n, en);    // Code 00

endmodule

Note that the output names run in the opposite order to the codes: d is active for code 00 and a for code 11. The testbench sets en to 1 and then applies random codes.


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

  dec_2x4 u0 ( .x(x), .y(y), .en(en),
               .a(a), .b(b), .c(c), .d(d));

  initial begin
    {x, y, en} <= 0;
    $monitor ("T=%0t x=%0b y=%0b en=%0b a=%0b b=%0b c=%0b d=%0b",
              $time, x, y, en, a, b, c, d);

    en <= 1;
    for (i = 0; i < 10; i = i+1) begin
      #1  x <= $random;
          y <= $random;
    end
  end
endmodule
Output
ncsim> run
T=0 x=0 y=0 en=1 a=0 b=0 c=0 d=1
T=1 x=0 y=1 en=1 a=0 b=0 c=1 d=0
T=2 x=1 y=1 en=1 a=1 b=0 c=0 d=0
T=4 x=1 y=0 en=1 a=0 b=1 c=0 d=0
T=5 x=1 y=1 en=1 a=1 b=0 c=0 d=0
T=6 x=0 y=1 en=1 a=0 b=0 c=1 d=0
T=7 x=1 y=0 en=1 a=0 b=1 c=0 d=0
T=10 x=1 y=1 en=1 a=1 b=0 c=0 d=0
ncsim: *W,RNQUIE: Simulation is complete.

The log already shows en=1 at T=0 because both nonblocking assignments to en happen in the same time step, and the later one wins before $monitor prints. Every line has exactly one output at 1, which is the defining property of a decoder.

Synthesis result: Synthesis keeps the structure of two inverters and four 3-input AND functions, but the exact cells depend on the library. A common mapping uses NOR or NAND cells with the inverters absorbed, since x_n & y_n is the same as ~(x | y).

Quick Check

If the testbench never set en to 1, what would the outputs show?

Show Answer

All four outputs would stay 0 for every value of x and y, because en is an input to every AND gate. When a decoder output never goes high, check the enable first.

Address Decoding

On a system bus, the upper address bits pass through a decoder to produce one select signal per peripheral, such as a timer, a UART and a GPIO block. Only the selected peripheral responds to a read or write. The enable input is driven by a "bus transaction valid" signal, so no peripheral is selected while the bus is idle. Memory arrays use the same idea on a larger scale to select one word line out of thousands.

4x2 Priority Encoder

An encoder does the reverse of a decoder: it converts which input is active into a binary code. A priority encoder also decides what happens when several inputs are active at once, by reporting the one with the highest priority. Here d has the highest priority and a the lowest:

dcbaxy
1XXX11
01XX10
001X01
000X00

Reading the table column by column gives the equations:

x = d | c
y = d | (b & ~c)

x is 1 whenever one of the two upper inputs is active. y is 1 for d, or for b as long as c does not override it. Input a does not appear in either equation, because its code 00 is what the outputs show when nothing above it is active.

Gate-level 4x2 priority encoder enc_4x2: an or gate combines d and c into x; a not gate inverts c into c_n; an and gate combines b and c_n into b_and_cn; a second or gate combines d and b_and_cn into y; input a is not used

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

  wire c_n;        // ~c
  wire b_and_cn;   // b & ~c

  // x = d | c
  or  (x, d, c);

  // y = d | (b & ~c)
  not (c_n, c);
  and (b_and_cn, b, c_n);
  or  (y, d, b_and_cn);

endmodule

The testbench counts through every combination of the four inputs by assigning the loop variable to the concatenation {a, b, c, d}, so a is the most significant bit.


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

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

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

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

    for (i = 0; i <= 16; i = i+1) begin
      #1 {a, b, c, d} <= i;
    end
  end

endmodule
Output
ncsim> run
T=0 a=0 b=0 c=0 d=0 x=0 y=0
T=2 a=0 b=0 c=0 d=1 x=1 y=1
T=3 a=0 b=0 c=1 d=0 x=1 y=0
T=4 a=0 b=0 c=1 d=1 x=1 y=1
T=5 a=0 b=1 c=0 d=0 x=0 y=1
T=6 a=0 b=1 c=0 d=1 x=1 y=1
T=7 a=0 b=1 c=1 d=0 x=1 y=0
T=8 a=0 b=1 c=1 d=1 x=1 y=1
T=9 a=1 b=0 c=0 d=0 x=0 y=0
T=10 a=1 b=0 c=0 d=1 x=1 y=1
T=11 a=1 b=0 c=1 d=0 x=1 y=0
T=12 a=1 b=0 c=1 d=1 x=1 y=1
T=13 a=1 b=1 c=0 d=0 x=0 y=1
T=14 a=1 b=1 c=0 d=1 x=1 y=1
T=15 a=1 b=1 c=1 d=0 x=1 y=0
T=16 a=1 b=1 c=1 d=1 x=1 y=1
T=17 a=0 b=0 c=0 d=0 x=0 y=0
ncsim: *W,RNQUIE: Simulation is complete.

There are a few details worth reading in this log:

  • There is no T=1 line, because the loop applies i = 0 at T=1, which is the same value as before.
  • At T=8, b, c and d are all 1, and the output is 11 because d wins.
  • At T=7, b and c are both 1, and c wins with 10.
  • At T=17, i = 16 is 5'b10000, and only its lower 4 bits fit into {a, b, c, d}, so the inputs wrap back to 0000.

Synthesis result: x maps to a single 2-input OR cell. y = d | (b & ~c) is a classic AND-OR shape, which synthesis usually implements as one complex AND-OR-INVERT cell plus an inverter, rather than three separate gates. Input a drives nothing, so the tool removes it from the logic and may report it as an unused input.

Because a = 1 alone and "no input active" both give xy = 00, a real priority encoder usually adds a valid output that tells the two cases apart. With gates it takes one more output port, output valid, and one more primitive:


  // valid = 1 when at least one input is active
  or (valid, a, b, c, d);

For the RTL way of writing priority logic with if-else-if and casez, and for wider encoders, see Verilog Priority Encoder.

Quick Check

Without the valid output, can you tell the input a = 1, b = 0, c = 0, d = 0 apart from all inputs at 0?

Show Answer

No. Both give x = 0 and y = 0, as the T=9 and T=17 lines of the log show. Only a separate valid output, which is 1 when any input is active, separates "a is active" from "nothing is active".

Interrupt Controllers

When several peripherals raise interrupt requests at the same moment, an interrupt controller uses a priority encoder to choose which one the processor handles first. The encoder's code becomes the interrupt number, and the valid output tells the processor that an interrupt is pending at all. Arbiters that grant a shared bus to one of several requesters use the same priority logic.

Common Beginner Mistakes

Mistake #1: Assuming which input the mux select picks

Wrong: Connecting mux_2x1 with the default path on a, expecting sel = 0 to pick a
Right: Read the gates: and (out_0, a, sel) means a passes when sel = 1

Why? There is no standard rule for which mux input belongs to sel = 0. With gate-level code, the AND gate that receives sel rather than sel_n tells you which input is selected by 1. Getting it backwards produces a design that compiles and simulates, but passes the wrong data.

Mistake #2: A typo in an internal wire name

Wrong:


  wire sel_n;
  not (sel_n, sel);
  and (out_1, b, seln);      // Typo: seln is not declared

Right: Use the declared name sel_n, and add `default_nettype none at the top of the file so that undeclared names are reported as errors

Why? Verilog creates an implicit 1-bit wire for an undeclared name used in a port connection. seln has no driver, so it stays at z, the AND gate outputs x or 0, and the mux silently stops passing b. In gate-level code with many internal wires, this is one of the most common bugs.

Mistake #3: Forgetting the decoder enable

Wrong: Leaving en at 0 or unconnected in the testbench, then debugging why a, b, c and d never change
Right: Drive en to 1, as the testbench does with en <= 1;

Why? The enable is an input to every AND gate, so it masks all four outputs. An unconnected input port is z, which makes the AND outputs x or 0, never a clean 1.

Mistake #4: Using an encoder without a valid output

Wrong: Treating xy = 00 from enc_4x2 as "input a is active"
Right: Add or (valid, a, b, c, d); and only use x and y when valid is 1

Why? The lowest priority input and "no input" share the same code. An interrupt controller without a valid output would service interrupt 0 even when no interrupt is pending.

Mistake #5: A testbench loop that wraps around

Wrong: for (i = 0; i <= 16; i = i+1) #1 {a, b, c, d} <= i;, then wondering why the inputs return to 0000 at the end
Right: Use i < 16 to cover the 16 combinations of a 4-bit value exactly once

Why? Assigning a wider value to a narrower target keeps only the lower bits. 16 is 10000 in binary, so {a, b, c, d} receives 0000. It does no harm here, but in other testbenches the extra wrapped value can trigger unexpected behavior or hide that a case was never tested.

Practice Exercise

Challenge: 2-Bit Equality Comparator

Write a module eq_2bit with inputs a1, a0, b1 and b0, and an output eq that is 1 when the 2-bit numbers {a1, a0} and {b1, b0} are equal. Use only gate primitives, and test all 16 input combinations.

Hint

Two bits are equal when their XNOR is 1. The numbers are equal when both bit pairs are equal, so combine the two XNOR outputs with an AND gate.

Solution

module eq_2bit ( input a1, a0, b1, b0,
                 output eq);

  wire e1, e0;

  xnor (e1, a1, b1);     // 1 when the upper bits match
  xnor (e0, a0, b0);     // 1 when the lower bits match
  and  (eq, e1, e0);     // Equal only if both bits match

endmodule

module tb;
  reg a1, a0, b1, b0;
  wire eq;
  integer i;

  eq_2bit u0 (.a1(a1), .a0(a0), .b1(b1), .b0(b0), .eq(eq));

  initial begin
    $monitor("T=%0t a=%0b%0b b=%0b%0b eq=%0b", $time, a1, a0, b1, b0, eq);
    for (i = 0; i < 16; i = i+1) begin
      {a1, a0, b1, b0} = i;
      #1;
    end
  end
endmodule

eq is 1 for exactly four combinations: 00/00, 01/01, 10/10 and 11/11, which are i = 0, 5, 10 and 15. The loop uses i < 16, so there is no wrapped value at the end. A wider comparator adds one XNOR per bit and a wider AND gate, and synthesis builds the same structure from the RTL expression a == b.

Summary

In this tutorial, you learned:

  • A gate-level module starts from the Boolean equation: one primitive per gate and one wire per internal connection
  • The 2x1 mux uses NOT, two ANDs and an OR; which input sel = 1 picks is set by which AND gate receives sel
  • The full adder shares a ^ b between sum and cout, and the carry is generate OR propagate
  • The 2x4 decoder ANDs the enable with each combination of true and inverted inputs, so exactly one output is 1
  • The 4x2 priority encoder reduces to x = d | c and y = d | (b & ~c), and needs a valid output to tell input a from no input
  • Synthesis keeps the function of gate-level code but is free to remap it to MUX, full adder or complex AND-OR cells
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5