Introduction: Adding Machine With Logic Gates
Have you ever wondered what really happens when your calculator adds two numbers together? Sure, you probably recall the long-hand arithmetic steps you were taught as a child, but your computer or calculator doesn't have a pen and paper with which to carry out these steps. How, then, do the electrical pulses flow through your calculator to produce the sum of two numbers?
In this project, we will build an adding machine from some of the most basic digital building-blocks: logic gates. We will utilize an Arduino board to help us with the tedious stuff, like reading input from a keypad and displaying the results on an LCD screen, but we will create an ALU (Arithmetic Logic Unit) from a handful of integrated circuits to do the actual meat of the operation.
Step 1: Materials
- 1 Arduino Mega2560 board
- 3-5 Breadboards - The keypad and Arduino don't need to be taped to a board, but I liked keeping things tidy.
- 2 74xx AND chips - I used the 74hc08
- 2 74xx XOR chips - I used the 74hc86
- 1 74xx OR chip - I used the 74hc32
- 1 4x4 membrane keypad
- 5 1K ohm, or greater, resistors
- Lots of solid hook-up wire
Step 2: Connect the LCD Screen
The best place to start and get your bearings is to hook up the LCD screen and write an Arduino sketch to print some text.
Find a good spot on your breadboard where you'd like your screen to live and make the following connections.
Power and Ground
- VSS to GND
- VDD to +5V
- RW to GND
- A to +5V w/ 220 ohm resistor
- K to GND
- V0 to a 10K potentiometer
Digital IO
- RS to digital pin 8
- E to digital pin 9
- D4 to digital pin 10
- D5 to digital pin 11
- D6 to digital pin 12
- D7 to digital pin 13
Now let's power up our board and write a simple sketch to test out the screen!
#include <LiquidCrystal.h> LiquidCrystal lcd(8, 9, 10, 11, 12, 13); void setup(){ lcd.begin(16, 2); // initialize the lcd lcd.setCursor(0,0); lcd.print("This is row 1"); lcd.setCursor(0,1); lcd.print("This is row 2"); } void loop() { }
Step 3: Connect the Keypad
Now let's hookup our keypad and modify our sketch to read from it and display what we type onto the LCD screen.
As shown in the diagram, the pins from the keypad are numbered from 1 to 8, right to left. In my photos, I flipped the ribbon underneath the keypad to keep everything flush with the breadboard.
Pins 1-4 correspond to the columns and 5-8 correspond to the rows.
#include <LiquidCrystal.h> #include <Keypad.h> LiquidCrystal lcd(8, 9, 10, 11, 12, 13); const byte ROWS = 4; //four rows const byte COLS = 4; //four columns char keys[ROWS][COLS] = { {'1','2','3','+'}, {'4','5','6','-'}, {'7','8','9','*'}, {'=','0','C','/'} }; byte rowPins[ROWS] = {53, 52, 51, 50}; //connect to the row pinouts of the keypad byte colPins[COLS] = {49, 48, 47, 46}; //connect to the column pinouts of the keypad Keypad keypad = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS ); void setup() { lcd.begin(16, 2); // initialize the lcd } void loop() { while (1) { char key = keypad.getKey(); if(key) { lcd.print(key); } } }
Step 4: Implement Naive Adding
This step gets everything fully tied together in software so that you can add values using the Arduino board. This is obviously pretty boring and not our end goal, but will lay the groundwork so that we can later swap out the boring adder with our awesome ALU.
#include <LiquidCrystal.h>
#include <Keypad.h> LiquidCrystal lcd(8, 9, 10, 11, 12, 13); const byte ROWS = 4; //four rows const byte COLS = 4; //four columns char keys[ROWS][COLS] = { {'1','2','3','+'}, {'4','5','6','-'}, {'7','8','9','*'}, {'=','0','C','/'} }; byte rowPins[ROWS] = {53, 52, 51, 50}; //connect to the row pinouts of the keypad byte colPins[COLS] = {49, 48, 47, 46}; //connect to the column pinouts of the keypad Keypad keypad = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS ); long num1, num2, total; char operation; char key; byte num1Print; void setup() { lcd.begin(16, 2); // initialize the lcd } void loop() { // Get first number and operator while (1) { key = keypad.getKey(); if (key == 'C') { clearCalc(); } if (key >='0' && key <='9') { num1 = num1*10 + (key -'0'); lcd.setCursor(0,0); num1Print = lcd.print(num1); } if (key == '-' || key == '*' || key == '/') { clearCalc(); lcd.print("Operation not"); lcd.setCursor(0, 1); lcd.print("supported."); } if (key == '+') { operation = key; lcd.setCursor(num1Print+1, 0); lcd.print(operation); break; } } // Get second number and perform operation while (1) { if (key == 'C') { break; } key = keypad.getKey(); if (key == 'C') { clearCalc(); break; } if (key >='0' && key <='9') { num2 = num2*10 + (key -'0'); lcd.setCursor(num1Print+3, 0); lcd.print(num2); } if (key == '=') { doOperation(); break; } } // Wait for the user to clear the results while (1) { if (key == 'C') { break; } key = keypad.getKey(); if (key == 'C') { clearCalc(); break; } } } void doOperation() { switch(operation) { case '+': total = num1 + num2; // BORING!!! break; } lcd.setCursor(0,1); lcd.print('='); lcd.setCursor(1,1); lcd.print(total); } void clearCalc() { num1 = 0; num2 = 0; total = 0; operation = 0; num1Print = 0; lcd.clear(); }
Step 5: Layout the Logic Gates
I found it best to order the 5 logic gates as follows, from left to right:
XOR, AND, OR, AND, XOR
Now would also be a good time to connect the +5V and GND lines to each gate, as shown by the red and black wires.
A nice way to verify your gates are functioning properly is to hookup their first output to an LED and use some jumper cables to switch between +5V and GND; which logically represent 1 and 0, respectively.
Step 6: Create the ALU
Now for the fun part!
It's time to perform the actual arithmetic. To do so, we need to send the two numbers we'd like to add through the series of logic gates in binary format. We will then receive the result in binary and can do with that as we please. For now, we will hookup the output bits to some LEDs for quick and easy verification of our results.
The first thing to wrap your head around is what's called the "Half-Adder" (the simpler of the two block diagrams above). This circuit consists of an XOR gate and an AND gate. With just these two gates, we can add two single-bit numbers! Pretty exhilarating, right?! To understand how this happens, follow what would happen when you send different values for A and B, and create yourself a truth table. You can see that the XOR gate is the one performing the actual summing of the values, and the AND gate is what tells you whether or not the sum resulted in a carry bit.
Of course, we'd like to add more than just single-bit numbers. To do so, we must now raise the complexity a fair bit and create what's called a "Full-Adder" (clever names, huh?). If you look closely at this other diagram, you can see that it's really two half-adders tied together with an OR gate. This new circuit is what allows us to string together single-bit adders so that they each can accept the other's carry-out bit as input to their carry-in bit. Note that for the first bit you are adding, you won't have anything to carry in, so it only applies for subsequent bits down the line.
In the photos above, I've hooked up two sets of full-adders and can sum 2-bit numbers.
Below is our finalized code for sending our values to the ALU and receiving the result. It supports up to 8-bit numbers.
#include <LiquidCrystal.h>
#include <Keypad.h> LiquidCrystal lcd(8, 9, 10, 11, 12, 13); byte numBits = 8; byte num1Bits[] = {23, 25, 27, 29, 31, 33, 35, 37}; byte num2Bits[] = {22, 24, 26, 28, 30, 32, 34, 36}; // WARNING!! Using pins 0 & 1 will interfere with serial communications. // When uploading a sketch, remove any connections to these pins. byte resultBits[] = {0, 1, 2, 3, 4, 5, 6, 7}; const byte ROWS = 4; //four rows const byte COLS = 4; //four columns char keys[ROWS][COLS] = { {'1','2','3','+'}, {'4','5','6','-'}, {'7','8','9','*'}, {'=','0','C','/'} }; byte rowPins[ROWS] = {53, 52, 51, 50}; //connect to the row pinouts of the keypad byte colPins[COLS] = {49, 48, 47, 46}; //connect to the column pinouts of the keypad Keypad keypad = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS ); long num1, num2, total; char operation; char key; byte num1Print; void setup() { lcd.begin(16, 2); for (int i = 0; i < numBits; i++) { pinMode(num1Bits[i], OUTPUT); digitalWrite(num1Bits[i], LOW); pinMode(num2Bits[i], OUTPUT); digitalWrite(num2Bits[i], LOW); pinMode(resultBits[i], INPUT); } } void loop() { // Get first number and operator while (1) { key = keypad.getKey(); if (key == 'C') { clearCalc(); } if (key >='0' && key <='9') { num1 = num1*10 + (key -'0'); lcd.setCursor(0,0); num1Print = lcd.print(num1); } if (key == '-' || key == '*' || key == '/') { clearCalc(); lcd.print("Operation not"); lcd.setCursor(0, 1); lcd.print("supported."); } if (key == '+') { operation = key; lcd.setCursor(num1Print+1, 0); lcd.print(operation); break; } } // Get second number and perform operation while (1) { if (key == 'C') { break; } key = keypad.getKey(); if (key == 'C') { clearCalc(); break; } if (key >='0' && key <='9') { num2 = num2*10 + (key -'0'); lcd.setCursor(num1Print+3, 0); lcd.print(num2); } if (key == '=') { doOperation(); break; } } // Wait for the user to clear the results while (1) { if (key == 'C') { break; } key = keypad.getKey(); if (key == 'C') { clearCalc(); break; } } } void doOperation() { switch(operation) { case '+': for (int i = 0; i < numBits; i++) { byte num1BitRead = bitRead(num1, i); byte num2BitRead = bitRead(num2, i); digitalWrite(num1Bits[i], num1BitRead ? HIGH : LOW); digitalWrite(num2Bits[i], num2BitRead ? HIGH : LOW); } total = 0; for (int i = 0; i < numBits; i++) { if (i < 5) total = total | (digitalRead(i) << i); // AWESOME!!! } break; } lcd.setCursor(0,1); lcd.print('='); lcd.setCursor(1,1); lcd.print(total); } void clearCalc() { num1 = 0; num2 = 0; total = 0; operation = 0; num1Print = 0; lcd.clear(); for (int i = 0; i < numBits; i++) { digitalWrite(num1Bits[i], LOW); digitalWrite(num2Bits[i], LOW); } }
Step 7: Add to Your Heart's Content
In the above photo, I've hooked up 4 full-adders and can sum up to 15+15. You can simply continue to tie together these adders to support more and more bits.
Thanks for checking this out; I hope you enjoyed the project! I plan to add support for multiplication in the coming months, so please check back if you're interested.
Step 8: Implement Multiplication
For bonus points, we can add support for multiplication with relatively low additional complexity.
There are a few methods for multiplication, but the simplest method is the one most (all?) of us learned in grade school: long multiplication. The method where you multiply the multiplicand by each digit of the multiplier and then add up all the properly shifted results (known as partial products). This works exactly the same way in base 2 as it does in base 10, so let's implement this naive approach with logic gates.
This implementation takes a few shortcuts, and some might call it cheating, but it is a quick and elegant way to leverage what we've already created to support multiplication. All you'll need is another AND gate (74hc08).
You'll want to run each multiplicand bit through their own AND gate while running the first bit of the multiplier through all gates at once. This gives you your first partial product. You'll then want to do the same with the second multiplier bit to get your second partial product. To take a shortcut here, we just left shift this second partial product by 1 in our Arduino code, but then we send our first and second partial products through our adder ALU to sum them. We continue to do this for each bit of the multiplier, all the while keeping a running sum of our total by sending the old total and the new partial product through the adder.
The `doOperation()` method needs to be altered to handle this new operation. The code for this is below. You'll notice that I've encapsulated the adder logic into a `doAdd(num1, num2)` function (not shown, but it's the same as before).
void doOperation()
{ switch(operation) { case '+': total = doAdd(num1, num2); break; case '*':
total = 0; for (int i = 0; i < numMultBits; i++) { // Grab the i-th multiplier bit and multiply it against each multiplicand bit byte multiplierBit = bitRead(num2, i); for (int j = 0; j < numMultBits; j++) { byte multiplicandBit = bitRead(num1, j); digitalWrite(multiplicand[j], multiplicandBit ? HIGH : LOW); digitalWrite(multiplier[j], multiplierBit ? HIGH : LOW); } // Read in the partial product and add it to the total long partialProduct = 0; for (int k = 0; k < numMultBits; k++) { partialProduct = partialProduct | (digitalRead(partProdResult[k]) << k); } total = doAdd(total, (partialProduct << i)); } break; }
lcd.setCursor(0,1); lcd.print('='); lcd.setCursor(1,1); lcd.print(total); }
Step 9: Give It a Home!
I decided to make my adding machine a sort of tribute by building a model replica of Blaise Pascal's calculator, commonly referred to as the Pascaline, to house it. My work on this project was inspired by having read the advertisement for Pascal's calculator written in 1649, 7 years after he first designed the device. The engineering genius of the machine which used such simple components in an elegant way got me thinking of what it must have been like to design and build something of the sort in the 17th century. To get a minute taste of what that must have felt like, I decided to build this calculator and experiment with some of the most basic electrical components we have at our disposal today. I felt it would only be right to tie it all together with this replica, a sort of "Electric Pascaline".
I've taken some photos of the major design components of the box to give you an idea of how I put it together, but this can obviously be done to your own taste. I had the luxury of a laser cutter at my disposal; which is how I engraved the details on the top and cut all of the pieces to exact measurements. I used 1/4" plywood boards from the hardware store as that was the thickest material my laser cutter could work with, and it definitely made for a sturdy enough construction. It measures 14"w x 8"d x 4"h, roughly the same size as the original Pascaline. The gears and hands were purchased from a craft store, along with a metallic "new penny" spray paint. While not part of the original design, I thought the angle brackets on the exterior added a nice touch to the box. It was only after installing most of them that it dawned on me that the style of bracket I had purchased was machined for interior installation; the screw cutouts weren't shaved down on the outside and the 90 degree interior angle was rounded instead of squared. This wasn't a huge deal, but some of the pieces have a small amount of give to them because they don't all fit completely flush. Keep that in mind if you decide to use the same sort of piece.
As you can see from the picture of our finalized breadboard, I detached the screen and keypad from the main board so that they could be visible through the top of the box. The shelf for the screen was a bit tricky to get right, and the angle brackets ended up having a bit too much give even installed properly on an interior angle. I ended up driving two small nails through the back side of the box and into the shelf to keep it sturdy.
Small corner pieces were used to keep the breadboard secure on the inside and to keep the lid of the box secure when placed on top.
Thanks for following along. Hopefully this has interested you to go read about Blaise Pascal if you haven't already. Cheers, and happy calculating!