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.
AdvancedRuns in your browser. Free, and no account needed.
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 analogue 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();
}
Parts list
4 parts, plus the jumper wires. Every one is in the editor's parts bin.
How it is wired
17 connections, pin by pin, read from the circuit itself. Each line is one set of pins joined together, by a jumper wire or a breadboard strip.
- Arduino Uno R3 pin 13; Keypad, 5x4 calculator pin R1
- Arduino Uno R3 pin 12; Keypad, 5x4 calculator pin R2
- Arduino Uno R3 pin 11; Keypad, 5x4 calculator pin R3
- Arduino Uno R3 pin 10; Keypad, 5x4 calculator pin R4
- Arduino Uno R3 pin 9; Keypad, 5x4 calculator pin R5
- Arduino Uno R3 pin 8; Keypad, 5x4 calculator pin C1
- Arduino Uno R3 pin 7; Keypad, 5x4 calculator pin C2
- Arduino Uno R3 pin 6; Keypad, 5x4 calculator pin C3
- Arduino Uno R3 pin 5; Keypad, 5x4 calculator pin C4
- Arduino Uno R3 pin 5V; Character LCD, 16x2 pin VDD; Character LCD, 16x2 pin A
- Ground: Arduino Uno R3 pin GND; Character LCD, 16x2 pin VSS; Character LCD, 16x2 pin V0; Character LCD, 16x2 pin RW; Character LCD, 16x2 pin K
- Arduino Uno R3 pin A0; Character LCD, 16x2 pin RS
- Arduino Uno R3 pin A1; Character LCD, 16x2 pin E
- Arduino Uno R3 pin A2; Character LCD, 16x2 pin D4
- Arduino Uno R3 pin A3; Character LCD, 16x2 pin D5
- Arduino Uno R3 pin A4; Character LCD, 16x2 pin D6
- Arduino Uno R3 pin A5; Character LCD, 16x2 pin D7
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.