Calculator on an Arduino Uno
A 5x4 calculator keypad and a 16x2 LCD on an Uno: four functions in hundredths, and Err when you divide by zero. The whole build, an Arduino Uno and 2 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Advanced About 30 minutes to build and run. Runs in your browser. Free, and no account needed.
The parts retail for about $38. On Mokxi they're free.
What you'll learn
- Scanning a keypad matrix of rows and columns
- Using analog pins A0 to A5 as plain digital pins
- Fixed-point math: whole numbers standing in for decimals
- Checking for overflow before a multiply
- A small state machine for typing, operators and errors
- Difficulty: Advanced
- Time: About 30 minutes
- Board: Arduino Uno R3
- Parts: 4
How it works
The keypad has no chip inside. Each key just connects one of its five row pins to one of its four column pins. The Uno scans those nine pins, and a small keypad library hands back one character per press.
Every number is stored as a whole long counting hundredths, so 12.34 is 1234. That keeps adding exact and needs no floating point. Press an operator and the sketch folds the typed number into the running total.
The LCD's top line shows the total and the waiting operator. The bottom line shows what you are typing. Divide by zero, or any answer past 99999.99, shows Err until you press C.
Parts list
4 parts, plus the jumper wires. They retail for about $38; on Mokxi they're free.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Full-size breadboard
- 1 × Keypad, 5x4 calculator
- 1 × Character LCD, 16x2
Wiring table
Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.
| Part and pin | Connects to |
|---|---|
| Keypad, 5x4 calculator pin R1 | Arduino Uno R3 pin 13 |
| Keypad, 5x4 calculator pin R2 | Arduino Uno R3 pin 12 |
| Keypad, 5x4 calculator pin R3 | Arduino Uno R3 pin 11 |
| Keypad, 5x4 calculator pin R4 | Arduino Uno R3 pin 10 |
| Keypad, 5x4 calculator pin R5 | Arduino Uno R3 pin 9 |
| Keypad, 5x4 calculator pin C1 | Arduino Uno R3 pin 8 |
| Keypad, 5x4 calculator pin C2 | Arduino Uno R3 pin 7 |
| Keypad, 5x4 calculator pin C3 | Arduino Uno R3 pin 6 |
| Keypad, 5x4 calculator pin C4 | Arduino Uno R3 pin 5 |
| Character LCD, 16x2 pin VDD | Arduino Uno R3 pin 5V |
| Character LCD, 16x2 pin A | Arduino Uno R3 pin 5V |
| Character LCD, 16x2 pin VSS | Arduino Uno R3 pin GND |
| Character LCD, 16x2 pin V0 | Arduino Uno R3 pin GND |
| Character LCD, 16x2 pin RW | Arduino Uno R3 pin GND |
| Character LCD, 16x2 pin K | Arduino Uno R3 pin GND |
| Character LCD, 16x2 pin RS | Arduino Uno R3 pin A0 |
| Character LCD, 16x2 pin E | Arduino Uno R3 pin A1 |
| Character LCD, 16x2 pin D4 | Arduino Uno R3 pin A2 |
| Character LCD, 16x2 pin D5 | Arduino Uno R3 pin A3 |
| Character LCD, 16x2 pin D6 | Arduino Uno R3 pin A4 |
| Character LCD, 16x2 pin D7 | Arduino Uno R3 pin A5 |
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Calculator: a 5x4 keypad, a 16x2 LCD, and four functions on an Uno.
//
// pins 13, 12, 11, 10, 9 keypad rows R1 to R5
// pins 8, 7, 6, 5 keypad columns C1 to C4
// A0 LCD RS A1 LCD E
// A2, A3, A4, A5 LCD D4 to D7 (RW and V0 to GND)
//
// Fifteen pins, and an Uno has fourteen digital ones, so the display's six
// go on the analog header, which is what A0 to A5 are when nothing is
// reading a voltage on them: ordinary digital pins. This is the pin budget
// that sends most people to a shift register or an I2C backpack.
//
// The pad is printed the way a desk calculator is:
//
// C < % /
// 7 8 9 x
// 4 5 6 -
// 1 2 3 +
// 0 . +/- =
//
// Type a number, press an operator, type another, press =. `C` clears
// everything, `<` rubs out the last digit, `%` divides what you are typing by a
// hundred and `+/-` changes its sign. Divide by zero says `Err` and waits for
// `C`, because that is the honest answer and a calculator that quietly printed
// something would be worse.
//
// # There is no chip in the keypad
//
// Twenty keys on nine pins, and nothing else inside: each key shorts its row to
// its column. mokxi_keypad.h scans it (one row driven low at a time, the four
// columns read with their pull-ups on) and debounces, so `pressed()` gives one
// character per press. Rows are never driven high: two keys held in the same
// column would put one driven output straight across another.
//
// # Why the arithmetic is whole numbers
//
// There is no floating point here, and there does not need to be. Every value
// is a `long` counting hundredths, so 12.34 is 1234, and two decimal places are
// exactly what a two-line display can show. That keeps addition exact, and it
// puts the interesting work in two places:
//
// * multiply is a*b/100, and a*b overflows a 32-bit long long before the
// divide does any good. So it is done in halves, (a/100)*b plus
// (a%100)*b/100, with the overflow checked *before* the multiply that
// would cause it, against MAX_VALUE.
// * divide is a*100/b, and a*100 is why MAX_VALUE is 99999.99 rather than
// something longer: 9999999 * 100 still fits in a long, and one more digit
// would not.
//
// Anything that would go past MAX_VALUE is `Err` as well. A calculator that
// wraps round to a negative number is lying.
#include "mokxi_keypad.h"
#include "mokxi_lcd1602.h"
const uint8_t ROWS[5] = {13, 12, 11, 10, 9};
const uint8_t COLS[4] = {8, 7, 6, 5};
Keypad5x4 pad(ROWS, COLS);
Lcd1602 lcd(A0, A1, A2, A3, A4, A5);
/** The largest number this calculator will hold, in hundredths: 99999.99. */
const long MAX_VALUE = 9999999L;
long acc = 0; // what has been worked out so far, in hundredths
long entry = 0; // the number being typed, in hundredths, never negative
char op = 0; // the operator waiting for its second operand
uint8_t places = 0; // 0 no point yet, 1 just after it, 2 tenths in, 3 full
bool typing = false; // whether `entry` is what the display should show
bool negative = false;
bool error = false;
/** The number being typed, with its sign. */
static long current()
{
return negative ? -entry : entry;
}
static void resetEntry()
{
entry = 0;
places = 0;
negative = false;
typing = false;
}
static void clearAll()
{
acc = 0;
op = 0;
error = false;
resetEntry();
}
/** a + b, or false when the answer is too big to hold. */
static bool addTo(long a, long b, long *out)
{
long r = a + b;
if (r > MAX_VALUE || r < -MAX_VALUE) {
return false;
}
*out = r;
return true;
}
/**
* a * b, both in hundredths, answer in hundredths.
*
* Split so nothing overflows: the whole part of `a` multiplied by `b` is
* checked against MAX_VALUE before it is worked out, and the hundredths left
* over are at most 99, so 99 * b fits with room to spare.
*/
static bool mulTo(long a, long b, long *out)
{
bool minus = (a < 0) != (b < 0);
unsigned long ua = (unsigned long)(a < 0 ? -a : a);
unsigned long ub = (unsigned long)(b < 0 ? -b : b);
unsigned long whole = ua / 100u;
unsigned long rest = ua % 100u;
if (ub != 0u && whole > (unsigned long)MAX_VALUE / ub) {
return false;
}
unsigned long r = whole * ub + (rest * ub) / 100u;
if (r > (unsigned long)MAX_VALUE) {
return false;
}
*out = minus ? -(long)r : (long)r;
return true;
}
/** a / b, both in hundredths, answer in hundredths, truncated. */
static bool divTo(long a, long b, long *out)
{
if (b == 0) {
return false;
}
bool minus = (a < 0) != (b < 0);
unsigned long ua = (unsigned long)(a < 0 ? -a : a);
unsigned long ub = (unsigned long)(b < 0 ? -b : b);
unsigned long r = (ua * 100u) / ub;
if (r > (unsigned long)MAX_VALUE) {
return false;
}
*out = minus ? -(long)r : (long)r;
return true;
}
/** Fold the number just typed into the accumulator with the waiting operator. */
static void apply()
{
long v = current();
if (!typing && op == 0) {
return;
}
if (op == 0) {
acc = v;
return;
}
bool ok = false;
switch (op) {
case '+': ok = addTo(acc, v, &acc); break;
case '-': ok = addTo(acc, -v, &acc); break;
case 'x': ok = mulTo(acc, v, &acc); break;
case '/': ok = divTo(acc, v, &acc); break;
default: ok = true; break;
}
if (!ok) {
error = true;
}
}
/** Take one digit off whatever is being typed. */
static void backspace()
{
if (!typing) {
return;
}
if (places == 3) {
entry = (entry / 10) * 10;
places = 2;
} else if (places == 2) {
entry = (entry / 100) * 100;
places = 1;
} else if (places == 1) {
places = 0;
} else {
entry = (entry / 1000) * 100;
}
}
static void digit(char c)
{
if (!typing) {
resetEntry();
typing = true;
}
long d = (long)(c - '0');
if (places == 0) {
if (entry > (MAX_VALUE - d * 100) / 10) {
return; // already as long as this display can show
}
entry = entry * 10 + d * 100;
} else if (places == 1) {
entry += d * 10;
places = 2;
} else if (places == 2) {
entry += d;
places = 3;
}
}
/** One line of the display: a number in hundredths, left aligned and padded. */
static void showValue(uint8_t row, long value, char suffix)
{
lcd.setCursor(0, row);
lcd.printFixed(value, 2);
if (suffix != 0) {
lcd.write(' ');
lcd.write(suffix);
}
lcd.padLine();
}
static void redraw()
{
lcd.setCursor(0, 0);
if (error) {
lcd.print("Err");
lcd.padLine();
lcd.setCursor(0, 1);
lcd.print("C to clear");
lcd.padLine();
return;
}
showValue(0, acc, op);
showValue(1, typing ? current() : acc, 0);
}
void setup()
{
pad.begin();
lcd.begin();
lcd.print("Calculator");
lcd.setCursor(0, 1);
lcd.print("C clears");
delay(700);
clearAll();
redraw();
}
void loop()
{
char key = pad.pressed();
if (key == 0) {
return;
}
if (key == 'C') {
clearAll();
redraw();
return;
}
if (error) {
return; // nothing but C gets out of an error, the way a real one behaves
}
switch (key) {
case '+':
case '-':
case 'x':
case '/':
apply();
op = key;
resetEntry();
break;
case '=':
apply();
op = 0;
resetEntry();
break;
case '.':
if (!typing) {
resetEntry();
typing = true;
}
if (places == 0) {
places = 1;
}
break;
case '~':
if (!typing) {
resetEntry();
typing = true;
}
negative = !negative;
break;
case '<':
backspace();
break;
case '%':
if (typing) {
entry /= 100;
places = 3;
}
break;
default:
if (key >= '0' && key <= '9') {
digit(key);
}
break;
}
redraw();
}
Code walkthrough
The key functions of the sketch, and what each one does.
mulTo sketch.ino
Multiplying two values in hundredths gives a*b/100, but a*b can overflow a long. So the code splits a into whole and leftover hundredths, and checks the whole part against MAX_VALUE before multiplying. Anything past the limit returns false, which becomes Err.
/**
* a * b, both in hundredths, answer in hundredths.
*
* Split so nothing overflows: the whole part of `a` multiplied by `b` is
* checked against MAX_VALUE before it is worked out, and the hundredths left
* over are at most 99, so 99 * b fits with room to spare.
*/
static bool mulTo(long a, long b, long *out)
{
bool minus = (a < 0) != (b < 0);
unsigned long ua = (unsigned long)(a < 0 ? -a : a);
unsigned long ub = (unsigned long)(b < 0 ? -b : b);
unsigned long whole = ua / 100u;
unsigned long rest = ua % 100u;
if (ub != 0u && whole > (unsigned long)MAX_VALUE / ub) {
return false;
}
unsigned long r = whole * ub + (rest * ub) / 100u;
if (r > (unsigned long)MAX_VALUE) {
return false;
}
*out = minus ? -(long)r : (long)r;
return true;
}apply sketch.ino
This folds the number you just typed into acc using the waiting operator in op. With no operator yet, the typed number simply becomes acc. If addition, multiplication or division reports failure, it sets error.
/** Fold the number just typed into the accumulator with the waiting operator. */
static void apply()
{
long v = current();
if (!typing && op == 0) {
return;
}
if (op == 0) {
acc = v;
return;
}
bool ok = false;
switch (op) {
case '+': ok = addTo(acc, v, &acc); break;
case '-': ok = addTo(acc, -v, &acc); break;
case 'x': ok = mulTo(acc, v, &acc); break;
case '/': ok = divTo(acc, v, &acc); break;
default: ok = true; break;
}
if (!ok) {
error = true;
}
}digit sketch.ino
Adds one typed digit. Before the decimal point it shifts the number left a place. After the point it fills tenths, then hundredths, and ignores a fourth digit. It stops accepting digits when the number would pass MAX_VALUE.
static void digit(char c)
{
if (!typing) {
resetEntry();
typing = true;
}
long d = (long)(c - '0');
if (places == 0) {
if (entry > (MAX_VALUE - d * 100) / 10) {
return; // already as long as this display can show
}
entry = entry * 10 + d * 100;
} else if (places == 1) {
entry += d * 10;
places = 2;
} else if (places == 2) {
entry += d;
places = 3;
}
}loop sketch.ino
Waits for pad.pressed() to return a key. C clears everything. While error is set, every other key is ignored. Operators call apply and store the new op, = finishes the sum, and digits go to digit. Then the display is redrawn.
void loop()
{
char key = pad.pressed();
if (key == 0) {
return;
}
if (key == 'C') {
clearAll();
redraw();
return;
}
if (error) {
return; // nothing but C gets out of an error, the way a real one behaves
}
switch (key) {
case '+':
case '-':
case 'x':
case '/':
apply();
op = key;
resetEntry();
break;
case '=':
apply();
op = 0;
// ... 35 more lines in the editorTroubleshooting
The usual mistakes with this circuit, and the fix for each.
- Keys type the wrong characters or nothing at all. This project
Check that R1 to R5 go to pins 13 to 9 and C1 to C4 go to pins 8 to 5, in that order. - The LCD stays blank. This project
Check the LCD's six control wires: RS on A0, E on A1, and D4 to D7 on A2 to A5, plus power and ground. - The display says Err and keys do nothing. This project
Press C. Err means a divide by zero or an answer past 99999.99, and only C clears it. - Pressing one key reports several keys or the wrong key. Keypad
Check the row and column pin order against your keymap. Scan by driving one column low and reading rows with pull-ups. The Keypad library needs the right pin lists and key layout. - Keys do not register or read false. Keypad
Rows need pull-ups, either INPUT_PULLUP or external resistors. Debounce presses for a few milliseconds, and check all nine wires, since one loose pin kills a whole row or column. - The screen lights up but shows only a row of solid boxes, or nothing at all. Character LCD
Contrast pin V0 needs a voltage. Connect the middle pin of a 10 k pot to V0, with its ends on 5 V and GND. Also confirm that the code calls lcd.begin(16, 2). - Garbled characters or a blank display even with good contrast. Character LCD
In 4-bit mode only D4 to D7 are used, and D0 to D3 stay unconnected. Tie RW to GND. Match the pins in LiquidCrystal(rs, en, d4, d5, d6, d7) exactly to your wiring. - Upload fails or nothing runs after wiring pins 0 and 1 to a module. Arduino Uno R3
Pins 0 and 1 are the USB serial port. Move the module to other pins, or unplug it while uploading. Also check the right board is selected.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.