CS61CL Fall 2008 Lab 17: Combinational Circuit Optimization

Combinational Circuit Optimization

In the previous lab you saw the deep relationship between truth tables, boolean expressions, and combinational circuit. The truth table specifies the mapping from inputs to outputs, and it is unique (up to permutations of the rows). For any truth table, there is an infinite set of combinational circuits that implement the mapping, and each combinational circuit has an identical boolean expression. However, there are canonical circuits that are 1-1 with the truth table, such as the sum-of-products form. Often there are other circuits that have identical functional behavior, but they are better by some metric, for example, smaller, faster, cheaper, or more robust. An elegant body of theory allows us to automate combinational circuit optimization. This is the essence of Computer Aided Design tools, many of which were pioneered here at Berkeley. Upper division courses, such as EECS150, go into that in detail. In cs61cl we'll get an apprecitiation for it and use some of those tools while we put togehter useful designs.

Boolean algebra

The system of Boolean algebra (invented by the mathematician George Boole in the 19th century) allows us to manipulate Boolean expressions to make the circuits that implement them faster, smaller, or cheaper. (Remember, in logic &bull is AND and + is OR. In C, the bitwise versions of these are & and |.)

Listed below are useful laws of Boolean algebra.

x • !x = 0
x • 0 = 0
x • 1 = x
x • x = x
x • y = y • x
(x • y) • z = x • (y • z)
x • (y + z) = x•y + x•z
x•y + x = x
!(x•y) = !x + !y
x + !x = 1
x + 1 = 1
x + 0 = x
x + x = x
x + y = y + x
(x + y) + z = x + (y + z)
x + y•z = (x + y) • (x + z)
(x + y) • x = x
!(x + y) = !x • !y

complementarity
laws of 0's and 1's
identities
idempotent law
commutativity
associativity
distribution
uniting theorem
DeMorgan's law

Notice that all these laws come in two versions, one being called the dual of the other. Practically speakinng, this means you if you know one version of the law and the other one can be easily derived.

Here is an example of using these laws to simplify the expression y = a•b + a + c.

y = a•b + a + c
= a•(b+1) + cdistribution, identity
= a•1 + claw of 1's
= a + cidentity

Brainstorm - Another XOR

An alternative expression for a xor b is (a + b)•(!a + !b). Show that this expression is equivalent to the one previously presented, a xor b = a•!b + !a•b.

Brainstorm - A useful derived law

Using the laws of Boolean algebra, show that a + !a•x = a + x. (This means we can eliminate two of the three gates without changing the behavior of the circuit!)

Brainstorm - Simplification practice

Supply the steps that simplify

    y = !a • !b • !c + !a • !b • c + !a • b • c + a • !b • !c +a • b • !c

to

    !a•!b + !a•c + a•!c

Using Boolean Algebra for Optimization

The laws of boolean algebra are used to optimize designs while preserving correctness in much the same way that compilers optimize the machine code produced in the translation from a high level programming language. The logic designer can work at a higher level and produce a design that is easy to understand and to verify. The tools seek to eliminate redundancies, exploit special cases, or transform the circuit improve an important metric, say delay, at the expense of a less important one, say number of gates.

These transformations can be done algebraicly or graphically. Many of them seem obvious, but when you have 100s of millions of gates, having tools that perform these zillions of little optimizations for you, without making any mistakes, is a huge benefit. Here's some simple examples that eliminate unnecessary gates.

Indeed, all of these are also peephole optimizations used in compilers as well, but in that setting it is used to optimize predicates, loops, and branches. Here we are eliminating gates and wires. Some optimizations improve the performance of a circuit, but don't necessarily reduce its gate count, some improve both.
((X & Y) & A) & B => (X & Y) & (A & B) 
(X & A) | (A & B) => (X | Y) & A)

Brainstorm: Invent an optimization

Propose an optimization as a rule that converts a pattern (i.e., boolean expression) to an equivalent but more efficient one.

Simplifying Sum of Products

In practice, one of the most important rules is the following.
(x & A) | (~x & A) => A
This allows you to take the AND gates in the SOP coming from two ON rows in a truth table that differ by complementing one input and smash them together. It eliminates two AND gates and one input to the OR gate. In EECS150 you learn a clever trick for this called Karnaugh maps. In CAD courses you learn even more powerful algorithms for this.

The majority function

Given below is the truth table for the majority function. It takes three inputs, and returns whatever value two or more of the inputs share.

a b c  m
0 0 0  0
0 0 1  0
0 1 0  0
0 1 1  1
1 0 0  0
1 0 1  1
1 1 0  1
1 1 1  1

Devise an implementation with as few gates as possible (our version has three AND's and two OR's), and implement your solution in Logisim.

Converting Gate Types - Fan-in

A simple example automatic curcuit transformation is to convert between gate types. For example, the circuit family you are working with may have only two-input gates are these may be much faster that wider gates. The associative law allow us to form a tree of AND gates to make a wide AND (or a tree of OR gates to make a wide OR). In fact, there are lots of trees that work and they have different performance but the same logical behavior. Let's let the tools do it for us.

Copy ~cs61cl/code/adder.circ to a local directory and open it up with logisim. Double click FA to focus on the FA subcircuit. Do Project->Analyze Circuit. (You will see that the inputs and truth table and expressions all reflect the input and output labels.) This time when you Build Circuit make the name FA2 and click the "Use Two-Input Gates Only" box. Take a look at the new circuit that you have, called FA2.

The behavior of FA and FA2 is identical. Poke it and try it out. Workng with subcircuits

In fact, you could replace FAs with FA2s in main. But one thing you need to be careful about is the layout of pins on the subcircuit symbol. Try dropping an FA2 in main somewhere. See, it has three pins on the left (the inputs) and two on the right (the outputs). You need to be extremely careful if you change the pins in a subcircuit in any way. (In case we didn't say it strong enough. BEWARE. Modularity through subcircuits is incredibly powerful. But, you must establish the interface to the module and keep it.)

Rearrange the pins on your FA2 module so that they conform to the orientation in FA. Note, when you mouse over the pins on the symbol you can see their names. When you've got it right, rip out an FA and replace it with an FA2. Poke at it to verify that it is still a 4-bit adder.

NAND gates and De Morgan's Theorem

So far we have been using familiar logic operations: AND, OR, and NOT. NAND seems kind of funny. It is like an AND followed by a NOT. Its symbols is the AND with a complementing circle on the output. In logisim, click on the Gates entry in the explorer panel on the left to see the NAND. Its truth table is
A B nand(A,B)
0 0 1
0 1 1
1 0 1
1 1 0
In the last lab, you saw that this funny gate is actually more powerful than the three more familiar ones - it is complete all by itself. Any logic function can be constructed using only NANDs. Perhaps even more surprising is that it is a lot faster and smaller than an AND gate or and OR gate. It seems that transitors just really like this idea of complementing the output. NOR is complete, fast, and small as well.

Well, if that hasn't got you excited about NAND, there's more. You may have run into De Morgan's law. It has two forms.

A + B = ~(~A • ~B)
A &bull B = ~(~A + ~B)
To prove the first one, construct the truth table for the to side and check if they are the same.
A  B  A+B   ~A  ~B  (~A & ~B)  ~(~A & ~B)
0  0   0     1   1      1          0
0  1   1     1   0      0          1
1  0   1     0   1      0          1
1  1   1     0   0      0          1
Look at what this does with a Sum of Product circuit. The OR is replaced with a NAND and a bubble (complement) is place on each of its inputs. Slide the bubble back to the AND gate. Sum of products is a NAND of NANDS!

Go back to your FA subcircuit and run Project->Analyze Circuit. This time when you run build circuit click the "use only nand gates" and give it a new name.

Letting tools work for you

Start a new project, drop in three input pins and one out put. Click on the pin to bring up the properties in the the lower left portion. Update the labels to reflect the names used in the columns of the truth table for the majority function. Enter in the truth table. Take a look at the expression. Did it find the same optimizations that you did? Build the circuit. Build it again using 2-input gates.

Cranking up the CAD tools

Your ripple carry adder is pretty easy to understand and verify. It simply mimics adding pairs of bits and the carry-in form right to left. To see where the power of modularity comes in and so see how optimization tools can do things that humans are really not very good at, lets make logisim do a bit of heavy lifting. Open up adder.circ once again. This time run Project->Analyze Circuit on your 4 bit adder in main, rather than the bit slice in FA. It generate a huge truth table. Click build circuit and put it in a new subcircuit. Can you figure out what is going on in the result?

Building a decoder

One of the important concepts in machine structures in number systems. While we were learning about integers in C we had to make the shift from decimal representation to binary representation. Both are place value with the digits (bit) multiplied by increasing powers of the base. Your fingers may seem a lot like bits, but for some reason we use a completely different number system when we count on our hands. There we use a kind of unary representation. The number is the count of the fingers that we are holding up. Just as we converted between numbers in different bases, we can convert between binary and unary. In fact, your FA converted three unary bits into two binary bits - the sum is the 1's place and the carry is the 2's place. When we strung them together, we add three digits of the same place value to produce one of the same place value and one of the next larger place.

A decoder does the reverse. It converts a binary input into a unary output. However, instead of the number of fingers it is the position of the finger that gives the value. (Yes, counting on your fingers is a very inefficient number system!) The truth value for a 2-bit decoder is giveb below. Create a new project can implement this decoder using logic gates.

S1 S0 | D3 D2 D1 D0
0  0  | 0  0  0  1
0  1  | 0  0  1  0
1  0  | 0  1  0  0
1  1  | 1  0  0  0
It should have four data outputs, d0, d1, d2, d3 on the right side, and two control inputs, s1 and s0. When you are done, you can compare your solution to the one in ~cs61cl/code/decoder.circ. One new bit of logisim that you will want. Click on gates in the explorer panel on the left. In there you will see a constant.

In the homework for the last lab you constructed a 2-1 Multiplexor (MUX) and then built a 4-1 multiplexor using 2-1 MUXes. The other name for a multiplexor is a selector. The control inputs select one of the data inputs to appear on the output. We will used these a lot. If you haven't done that exercise already, you will probably want to do it now.

A demultiplexor is the inverse of a multiplexor. It steers the input to the one of 2^n outputs that is specified by the n control inputs. Modify your decoder to that it becomes a demultiplexor. The truth table for this demultiplexor is the following. But in this case, you will find it easier to think structurally and just steer the input to the correct output, rather than cranking through the truth table.

S1 S0 IN | D3 D2 D1 D0
0  0   0 | 0  0  0  0
0  0   1 | 0  0  0  1
0  1   0 | 0  0  0  0
0  1   1 | 0  0  1  0
1  0   0 | 0  0  0  0
1  0   1 | 0  1  0  0
1  1   0 | 0  0  0  0
1  1   1 | 1  0  0  0

Bundles of bits

In learning C and MIPS machine language, we have come to see that variables are not really numbers and strings, they are collection of bits organized as words that we manipulate in various ways according to what those collections of bits represent. At the lowest level of the machine we have signals, individual wires that carry a voltage representing the logical value of a bit at any point in time, and busses collection of signals that carry a closely related collection of bits, e.g., a word. All CAD tools provide some means of going between the signal view of individual wires and the bus view of wider logical units.

Logisim handles this duality with two mechanisms. First, the splitter tool can either take several individual signals and group them together into a bundle or split a bundle into its individual signals. Second, each input to a gate can have a bit width, as well as a number of inputs. So far, all our inputs were one bit wide. The logical symbols can be used on every signal in a bundle. This is a little like the bit-wise operators in C (&, |, and ^), as they do a bit-wise logical operation on each bit in the (scalar) object, i.e., char or int.

Let's first use the concept of a data bus on your four bit adder. Open it up under logisim and remove the individual input pins and output pins. (Grab the select tool, right click on the object and select delete or select a region and cut.) Under the Base menu, grab the splitter tool. Change its properties so that it faces south and has a bit width of 4 and a fanout of four. Drop this above you 4-bit adder built out of FAs. Attach a wire to the top, i.e, the 4-bit wide part. Grab the input pin tool, set it to be south facing and four bits wide. Attach it to your wire and label it A. Attach the individual signals to the A input of your FAs.

Do the same for the B side.

Grab the splitter again, but make it North facing with a bit width of 5 and an fanout of 5. (This is a bit of a misnomer, because it will really be a fan-in).

Why does it need to be five bits wide? What is the fifth bit?

Connect the sum output and the carry out to you unsplitter and put a 5-wide output pin on it facing north. It should look something like what is below. Poke the inputs. Now this adds binary numbers, rather than a bunch of bits.

While you've got the Base menu open, notice the Probe tool. This is your debugging friend. Grab it and drop it on each of the internal carrys. Poke the inputs to see that it is showing you whats going on. Here you get the same information as the highlighting on the signal. But you can make the probe multiple bits wide and drop it on busses. Try it.

Logic unit

Let's get some experience with gates that are multiple bits wide. Copy over ~cs61cl/code/and4.circ and open it up. This contains a 4 bit wide bit-wise & circuit using spliters and 2-input 1-bit wide gates. Rip out the spliters and the gates and the wires connecting them Replace all of that with a single 2-input 4-bit wide and gate and wire it to the input and output pins. Poke it to convince yourself that it is the same circuit.

Now we can move up one level of design. The Logism gates are really a library of gates of different sizes and widths. You know how to build multiplexors and complex boolean functions out of individual logic gates. Logisim provides a library of the larger devices too. Go to Project->Load Library->Built-in Library and select Plexers. Open that up. See some familiar parts.

Let's build a simple 16-bit logic unit. It has four functions: AND, OR, XOR, EQ. It has two 16-bit inputs: A and B, and one 16-bit output, R. It has a 2-bit control input OP that selects which function is performed, according to the following table.

OP  Function
00  AND
01  OR
10  XOR
11  EQ

Build this using four 16-bit wide logic gates and one 16-bit wide multiplexor.

Shifter

All sorts of useful circuits can be built with multiplers and splitters. Build a 4-bit wide arithmetic right shifter. It should have a 4-bit data input D, a 4-bit data output R, and a 2-bit control input S that is the shift amount. Recall, arithmetic right shift means that you fill with the left most bit (the sign bit) rather than zero when you shift to the right. The right most bits just dissappear. You can build all of this with a single 4-way 4-bit wide MUX and some splitters. However, as soon as you have a bundle of bits you need to keep track of the endians. Hover your select tool over the individual bits of the split. See what they say. Pay careful attention to the marking on the MUX. See which is the 0 input?

Homework and Reading

Using the same method that we did for the adder, implement a 4-bit subtractor. First, construct a truth table for a bit slice. It has three inputs: A, B and BRin (borrow in). It produces two outputs: R and BRout. The way it works is just like when you learned to subtract. A minus the Borrow minus B products on bit of the result and a Borrow (which may be zero) from the next higher position. Do the following example by hand first.

A:   1 1 0 1    (13)
B:  -0 0 1 1     (3)
    --------
c:   1 0 1 0    (10)

Using seven FAs, construct a circuit that takes 8 input bits and produces a 4-bit binary output that is the number of 1s in the input. This is a different kind of unary-to-binary conversion.


culler