Built-in project · Arduino Uno R3

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.

The circuit runs here once the page has loaded.

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.

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 pinConnects to
Keypad, 5x4 calculator pin R1Arduino Uno R3 pin 13
Keypad, 5x4 calculator pin R2Arduino Uno R3 pin 12
Keypad, 5x4 calculator pin R3Arduino Uno R3 pin 11
Keypad, 5x4 calculator pin R4Arduino Uno R3 pin 10
Keypad, 5x4 calculator pin R5Arduino Uno R3 pin 9
Keypad, 5x4 calculator pin C1Arduino Uno R3 pin 8
Keypad, 5x4 calculator pin C2Arduino Uno R3 pin 7
Keypad, 5x4 calculator pin C3Arduino Uno R3 pin 6
Keypad, 5x4 calculator pin C4Arduino Uno R3 pin 5
Character LCD, 16x2 pin VDDArduino Uno R3 pin 5V
Character LCD, 16x2 pin AArduino Uno R3 pin 5V
Character LCD, 16x2 pin VSSArduino Uno R3 pin GND
Character LCD, 16x2 pin V0Arduino Uno R3 pin GND
Character LCD, 16x2 pin RWArduino Uno R3 pin GND
Character LCD, 16x2 pin KArduino Uno R3 pin GND
Character LCD, 16x2 pin RSArduino Uno R3 pin A0
Character LCD, 16x2 pin EArduino Uno R3 pin A1
Character LCD, 16x2 pin D4Arduino Uno R3 pin A2
Character LCD, 16x2 pin D5Arduino Uno R3 pin A3
Character LCD, 16x2 pin D6Arduino Uno R3 pin A4
Character LCD, 16x2 pin D7Arduino 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.

sketch.ino · Arduino Uno R3
// 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 editor

Troubleshooting

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.