Gates & Logic

Our goal over the next couple of lectures is to build a computer.

Let’s take it back to the beginning: computers are made out of logical switches. In the modern era, these switches are implemented using transistors. But let’s start with relays instead, because they’re easier to think about.

We won’t build a computer in one step. We’re going to use relays to build bigger components, and then think abstractly about what those components do. Then we can forget about the internals, i.e., how we built the thing, and we can build something even bigger out of that. Step by step, we will climb up the latter of abstraction and build a computer.

Truth Tables

To climb the abstraction latter, we need an abstract way to write down the behavior of a circuit element. Our tool for this is a truth table, which exhaustively describes how the circuit’s input and output signals behave in terms of bits.

Relays have three “ports”: two input ports, which we’ll call c for “control,” in, and out. The c and in ports are both inputs, and the out port is the only output.

Recall how relays have a “default on” and a “default off” variant. (The electromagnet repulses or attracts the bendy piece of metal, respectively.) Truth tables are a good way to write down the difference between the variants.

Here is the truth table for a “default off” relay:

cinout
000
010
100
111

Truth tables have one column per port, and they have one row for every combination of input-port values.

Here’s the truth table for the “default on” kind of relay:

cinout
000
011
100
110

Building Nand

Let’s build a different circuit using relays as the “raw materials.” We will build a nand (“not and”) function.

It’s important to write down the specification for the function we want. Our specifications will be truth tables. Here’s the truth table for nand:

about
001
011
101
110

There are two inputs, a and b, and one output, out.

How can you write up relays to make a circuit with this truth table? Hint: You can do it two relays, one of each kind.

Level Up: Building Not

Here’s the philosophy of this kind of work: now that we’ve built a nand circuit, we earn the right to use it in larger, more interesting circuits. We have leveled up, and we can build something else using nands—and we can forget how nand works internally.

Let’s build a not function next. Here’s the truth table:

inout
01
10

This circuit is also called an inverter.

Can you use nands to make not?

Keep Leveling Up

We’re going to keep building larger and more interesting circuits out of smaller ones. This “leveling up” sort of feels like a video game. In fact, people have made video games out of this process! A cool one is Nandgame.

Try using Nandgame to build the circuits we already made. Then, try going farther and making and and or circuits.

Logic Notation

It’s going to be helpful to have a notation to write down these logic circuits as we make them more complicated. Here is some common mathy notation that people use to write these operators.

nameC bitwise opmathy
not~a\( \overline{a} \) or \( \neg a \) or \(a’\)
anda & b\( a \wedge b \) or \(a \cdot b\) or just \(ab\)
ora | b\( a \vee b \) or \(a + b\)
xora ^ b\(a \oplus b\)

Each of these operators has a visual representation for wiring schematics, but they are too hard to include here. You can see them all on the Wikipedia page for logic gate.

Universal Gates, and a Recipe for Building Anything

Nandgame encourages you to be creative: to think carefully about how to use your “inventory” efficiently to build a new circuit. But there is an easier, more mechanical way that works to build anything: that is, given an arbitrary truth table, this method can give you a circuit.

Here are the steps:

  1. Start with a truth table.
  2. For every row where the output is 1, write out the minterms. The minterm is the logical expression that is an “and” of all the input variables, either with or without negation, according to the truth value of the given input. For example, if the row in the truth table has \(a = 1\) and \(b = 0\), then the minterm is \(a\overline{b}\). The idea is that the minterm completely describes the input condition where that row is active.
  3. Join all the minterms for those output-1 rows with “ors.” This is the sum-of-products expression.

That gives you a logical expression consisting only of not, and, and or that is 1 when the output in the truth table is 1 and 0 otherwise. You can construct a circuit out of these three gates to match the expression.

Because this sum-of-products process works for any truth table, and it only uses those three gates, you can conclude that the combination of and, not and or is all you really need: if you just have those three functions, you can build any other function.

It gets better: you can each all of and, or, and not through a clever combination of only nand gates. You can also build any of them out of just nor gates. (Try it in Nandgame if you want!) That means that, transitively, you can build any circuit out of just nand or just nor. People call these gates universal for that reason.

Practicing Sum-of-Products Constructions

Here are two functions you can build to try out your newfound skills in building arbitrary circuits out of and, or, and not:

  1. Try building xnor, i.e., “not xor,” using this technique.
  2. A multiplexer (aka a mux or a selector) has three inputs: s for “select,” in₀, and in₁. It has one output, out. When s is 0, out is equal to in₀. When s is 1, out is equal to in₁.

Because the multiplexer has 3 inputs, you will want to use 3-input and and or gates. You can, of course, implement these with a cascade of 2-input gates.

Arithmetic

If this technique really works to build “everything,” let’s try using it build math. Starting with addition.

Half Adder

To keep the circuit small, let’s add two 1-bit numbers.

Let’s start by writing out all the possible combinations, and the sum as a binary value. This is not quite a truth table, because the output is a 2-bit number and not a truth value, but it’s close:

aba+b
000
011
101
1110

To make this into a truth table, let’s separate the two bits of the output sum—and fill in the implicit 0 in the most significant bit. The normal way to do this is to label the two bits c, for the carry bit, and s, for the sum. The truth table looks like this:

abcs
0000
0101
1001
1110

Remember that a and b are the input columns, and c and s are the output columns.

This truth table is a little different from the other ones on this page because it has two outputs. But we can still use the same approach, just one output at a time. That is, we can write the logical formulas for the two outputs separately \( c = ab \) and \( s = \overline{a}b \vee a\overline{b} \).

It is “fun” to notice that there is another truth table that already matches the behavior of the sum value: namely, \( s = a \oplus b \). So we can use two of the gates we built above to make this one-bit adder: an and gate for c and an xor gate for s.

This circuit is usually called a half adder. Why “half”? It’s missing an important feature that we’ll add next.

Full Adder

Adding one-bit numbers is nice, but we would like to add bigger numbers. The insight that will get us there is that, when we do “long addition” of binary numbers, we add up one bit at a time—and possibly “carry the one” to the next column. At each step in this process, we actually need to add three one-bit numbers together: each of the two input bits and—for every bit except the first—the carried bit from the previous column (which may be zero).

So the key to implementing a circuit that does “long addition” is to extend our one-bit adder above to take three inputs instead of two. This thing will be called a full adder. It has three one-bit inputs: \(a\), \(b\), and \(c_{\mathrm{in}}\) for the carry-in bit. Just like the half adder, it has two one-bit outputs: the sum \(s\) and the carry-out bit \(c_{\mathrm{out}}\).

Try writing out a truth table for this circuit. One useful thing to remember is that, despite \(c_{\mathrm{in}}\) having a different-looking name, the three inputs are really indistinguishable: we’re just adding up 3 one-bit numbers here.

We could absolutely use the sum-of-products approach to build the circuit for the full adder. But it turns out that there is a much simpler way to do it by using two half adders and some other logic. Can you build this circuit? You can try skipping to the “full adder” level in Nandgame to try it out.

n-Bit Adder

The full adder is the building block we need to construct an \(n\)-bit adder, for any \(n\): a circuit that takes two \(n\)-bit numbers and adds them together, producing an \((n+1)\)-bit result. You can make this circuit by chaining together a series of \(n\) full adders, hooking the \(c_{\mathrm{out}}\) of one to the \(c_{\mathrm{in}}\) of the next.

By climbing the abstraction ladder, we have gradually gotten from relays, something we can physically understand, all the way to a binary calculator. We don’t have a computer yet, exactly, but we do have something pretty cool.