I2C keypad door lock with an LCD on an Arduino Uno
A 4x4 keypad on a PCF8574 and an I2C LCD share A4 and A5: the right code and # open a servo bolt, and three wrong codes lock the keypad for 30 s. The whole build, an Arduino Uno and 4 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.
Intermediate About 30 minutes to build and run. Runs in your browser. Free, and no account needed.
The parts retail for about $40. On Mokxi they're free.
What you'll learn
- Sharing two I2C wires between several devices
- Using an I/O expander to scan a keypad
- A small state machine: locked, open and lockout
- Timing with
millisinstead of waiting - Moving a servo as a door bolt
- Difficulty: Intermediate
- Time: About 30 minutes
- Board: Arduino Uno R3
- Parts: 6
How it works
A 4x4 keypad normally needs eight pins. Here a PCF8574 expander at address 0x20 scans it, and the LCD's backpack at 0x27 shares the same two I2C wires, A4 and A5. Only the servo needs its own pin, 9.
Type digits and the LCD shows a star for each one. Press # to check the code against CODE, which is 1234. If it matches, the servo moves to 90 degrees for 5 seconds, then goes back to 0. Press * to clear what you typed.
Three wrong codes in a row lock the keypad for 30 seconds, with a countdown on the screen.
Parts list
6 parts, plus the jumper wires. They retail for about $40; on Mokxi they're free.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Half breadboard
- 1 × I/O expander, PCF8574
- 1 × Keypad, 4x4In Vending Machine Kit, Escape Box Kit.
- 1 × Character LCD with I2C backpackIn Claw Machine Kit, Vending Machine Kit, Rhythm Game Kit.
- 1 × Hobby servoIn Halloween Prop Kit, Christmas Lights Kit, Starter Projects Kit.
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 |
|---|---|
| Character LCD with I2C backpack pin SCL | Arduino Uno R3 pin SCL |
| Character LCD with I2C backpack pin SDA | Arduino Uno R3 pin SDA |
| Hobby servo pin PWM | Arduino Uno R3 pin 9 |
| I/O expander, PCF8574 pin VCC | Arduino Uno R3 pin 5V |
| Character LCD with I2C backpack pin VCC | Arduino Uno R3 pin 5V |
| Hobby servo pin VCC | Arduino Uno R3 pin 5V |
| I/O expander, PCF8574 pin GND | Arduino Uno R3 pin GND |
| Character LCD with I2C backpack pin GND | Arduino Uno R3 pin GND |
| Hobby servo pin GND | Arduino Uno R3 pin GND |
| I/O expander, PCF8574 pin SDA | Arduino Uno R3 pin A4 |
| I/O expander, PCF8574 pin SCL | Arduino Uno R3 pin A5 |
| Keypad, 4x4 pin R1 | I/O expander, PCF8574 pin P0 |
| Keypad, 4x4 pin R2 | I/O expander, PCF8574 pin P1 |
| Keypad, 4x4 pin R3 | I/O expander, PCF8574 pin P2 |
| Keypad, 4x4 pin R4 | I/O expander, PCF8574 pin P3 |
| Keypad, 4x4 pin C1 | I/O expander, PCF8574 pin P4 |
| Keypad, 4x4 pin C2 | I/O expander, PCF8574 pin P5 |
| Keypad, 4x4 pin C3 | I/O expander, PCF8574 pin P6 |
| Keypad, 4x4 pin C4 | I/O expander, PCF8574 pin P7 |
The note on the canvas
Word for word, as it sits beside the circuit in the editor.
Try it
1. Press Run. The LCD says Enter code:
2. Press 1, 2, 3, 4 on the keypad, then #. The servo pulls the bolt back for five seconds, then locks again.
3. * clears what you typed.
4. Enter a wrong code three times: the keypad is locked for 30 seconds, with a countdown.
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// I2C keypad door lock: a 4x4 keypad, a 16x2 LCD and a servo bolt, and the
// keypad and the screen need only two wires between them.
//
// A 4x4 keypad usually takes eight pins. Here a PCF8574 I2C expander (at
// 0x20) scans it, and the LCD's I2C backpack (at 0x27) sits on the same two
// wires, so the whole lock uses A4, A5 and pin 9.
//
// PCF8574 SDA -> A4 SCL -> A5 P0..P3 -> keypad R1..R4
// P4..P7 -> keypad C1..C4
// LCD I2C SDA -> A4 SCL -> A5 (address 0x27)
// servo PWM -> 9 VCC -> 5V GND -> GND
//
// Type the code and press # to open: the servo pulls the bolt back for five
// seconds, then locks again. * clears what you typed. Three wrong codes in
// a row lock the keypad for 30 seconds, with a countdown on the screen.
//
// Things to try. Change CODE to your own, up to eight digits. Change
// OPEN_MS to keep the door open longer.
#include <Wire.h>
#include <I2CKeyPad.h>
#include <LiquidCrystal_I2C.h>
#include <Servo.h>
const char CODE[] = "1234";
const unsigned long OPEN_MS = 5000;
const unsigned long LOCKOUT_MS = 30000;
const uint8_t MAX_TRIES = 3;
const uint8_t LOCKED_ANGLE = 0;
const uint8_t OPEN_ANGLE = 90;
const uint8_t SERVO_PIN = 9;
I2CKeyPad keyPad(0x20);
LiquidCrystal_I2C lcd(0x27, 16, 2);
Servo bolt;
// The keypad's layout, then N for no key and F for a failed read.
char keymap[19] = "123A456B789C*0#DNF";
char entered[9];
uint8_t length = 0;
uint8_t wrong = 0;
bool keyWasDown = false;
enum State { LOCKED, OPEN, LOCKOUT } state = LOCKED;
unsigned long since = 0;
unsigned long lastSecond = 0;
void prompt() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(F("Enter code:"));
lcd.setCursor(0, 1);
for (uint8_t i = 0; i < length; i++) lcd.print('*');
}
void lockDoor() {
bolt.write(LOCKED_ANGLE);
state = LOCKED;
length = 0;
prompt();
Serial.println(F("locked"));
}
void check() {
entered[length] = '\0';
if (strcmp(entered, CODE) == 0) {
wrong = 0;
bolt.write(OPEN_ANGLE);
state = OPEN;
since = millis();
lcd.clear();
lcd.print(F("Access granted"));
lcd.setCursor(0, 1);
lcd.print(F("Door open"));
Serial.println(F("open"));
return;
}
wrong++;
length = 0;
Serial.print(F("wrong code "));
Serial.println(wrong);
if (wrong >= MAX_TRIES) {
wrong = 0;
state = LOCKOUT;
since = millis();
lastSecond = 0;
lcd.clear();
lcd.print(F("Too many tries"));
Serial.println(F("keypad locked out"));
return;
}
lcd.clear();
lcd.print(F("Wrong code"));
lcd.setCursor(0, 1);
lcd.print(MAX_TRIES - wrong);
lcd.print(F(" tries left"));
delay(1500);
prompt();
}
void onKey(char key) {
Serial.print(F("key "));
Serial.println(key);
if (key == '*') {
length = 0;
prompt();
} else if (key == '#') {
if (length > 0) check();
} else if (key >= '0' && key <= '9' && length < sizeof(entered) - 1) {
entered[length++] = key;
lcd.setCursor(length - 1, 1);
lcd.print('*');
}
}
void setup() {
Serial.begin(9600);
Wire.begin();
lcd.init();
lcd.backlight();
bolt.attach(SERVO_PIN);
if (!keyPad.begin()) {
lcd.print(F("No keypad found"));
Serial.println(F("No PCF8574 at 0x20: check SDA, SCL and its power"));
while (true);
}
keyPad.loadKeyMap(keymap);
lockDoor();
Serial.println(F("keypad lock ready"));
}
void loop() {
// One press is one key: act when a key goes down, then wait for it to come up.
bool down = keyPad.isPressed();
if (down && !keyWasDown) {
char key = keyPad.getChar();
if (key != 'N' && key != 'F' && state == LOCKED) onKey(key);
}
keyWasDown = down;
if (state == OPEN && millis() - since >= OPEN_MS) {
lockDoor();
}
if (state == LOCKOUT) {
unsigned long left = (LOCKOUT_MS - (millis() - since) + 999) / 1000;
if (millis() - since >= LOCKOUT_MS) {
Serial.println(F("keypad unlocked"));
lockDoor();
} else if (left != lastSecond) {
lastSecond = left;
lcd.setCursor(0, 1);
lcd.print(F("Wait "));
lcd.print(left);
lcd.print(F(" s "));
}
}
delay(20);
}
Code walkthrough
The key functions of the sketch, and what each one does.
check sketch.ino
Compares what was typed with CODE. A match resets the wrong count, moves the servo to OPEN_ANGLE and starts the open timer. A miss adds to wrong. At MAX_TRIES it starts the lockout. Otherwise it shows how many tries are left.
void check() {
entered[length] = '\0';
if (strcmp(entered, CODE) == 0) {
wrong = 0;
bolt.write(OPEN_ANGLE);
state = OPEN;
since = millis();
lcd.clear();
lcd.print(F("Access granted"));
lcd.setCursor(0, 1);
lcd.print(F("Door open"));
Serial.println(F("open"));
return;
}
wrong++;
length = 0;
Serial.print(F("wrong code "));
Serial.println(wrong);
if (wrong >= MAX_TRIES) {
wrong = 0;
state = LOCKOUT;
since = millis();
lastSecond = 0;
lcd.clear();
lcd.print(F("Too many tries"));
Serial.println(F("keypad locked out"));
return;
}
// ... 8 more lines in the editoronKey sketch.ino
Handles one key press. * clears the entry, # calls check if something was typed, and digits are stored, up to eight, with a star drawn on the screen.
void onKey(char key) {
Serial.print(F("key "));
Serial.println(key);
if (key == '*') {
length = 0;
prompt();
} else if (key == '#') {
if (length > 0) check();
} else if (key >= '0' && key <= '9' && length < sizeof(entered) - 1) {
entered[length++] = key;
lcd.setCursor(length - 1, 1);
lcd.print('*');
}
}loop sketch.ino
Acts once when a key goes down, and ignores keys unless the lock is in the locked state. It also closes the door after OPEN_MS, and during lockout it updates the countdown until LOCKOUT_MS has passed.
void loop() {
// One press is one key: act when a key goes down, then wait for it to come up.
bool down = keyPad.isPressed();
if (down && !keyWasDown) {
char key = keyPad.getChar();
if (key != 'N' && key != 'F' && state == LOCKED) onKey(key);
}
keyWasDown = down;
if (state == OPEN && millis() - since >= OPEN_MS) {
lockDoor();
}
if (state == LOCKOUT) {
unsigned long left = (LOCKOUT_MS - (millis() - since) + 999) / 1000;
if (millis() - since >= LOCKOUT_MS) {
Serial.println(F("keypad unlocked"));
lockDoor();
} else if (left != lastSecond) {
lastSecond = left;
lcd.setCursor(0, 1);
lcd.print(F("Wait "));
lcd.print(left);
lcd.print(F(" s "));
}
}
delay(20);
}setup sketch.ino
Starts I2C, the LCD and the servo. If the keypad does not answer at 0x20, it prints a message and stops. Otherwise it loads the key layout and calls lockDoor.
void setup() {
Serial.begin(9600);
Wire.begin();
lcd.init();
lcd.backlight();
bolt.attach(SERVO_PIN);
if (!keyPad.begin()) {
lcd.print(F("No keypad found"));
Serial.println(F("No PCF8574 at 0x20: check SDA, SCL and its power"));
while (true);
}
keyPad.loadKeyMap(keymap);
lockDoor();
Serial.println(F("keypad lock ready"));
}Troubleshooting
The usual mistakes with this circuit, and the fix for each.
- The LCD says No keypad found This project
Check that the PCF8574's SDA goes to A4 and SCL to A5, and that it has power and ground. - The right code does not open the door This project
You must press # after the digits. Also checkCODE, which is 1234. - Keys do nothing for a while This project
After three wrong codes the keypad is locked out for 30 seconds. Wait for the countdown. - Inputs read wrong, or LEDs stay dim. I/O expander
The pins are quasi-bidirectional. Write 1 to a pin before reading it as an input. Outputs sink better than they source, so wire LEDs to the positive supply. - The expander is not found on I2C. I/O expander
The address is 0x20 to 0x27 (PCF8574) or 0x38 to 0x3F (PCF8574A), set by A0 to A2. Add pull-ups on SDA and SCL if none exist. - Pressing one key reports several keys, or the wrong key. Keypad
Match the keymap and the row and column pin order to your keypad's pinout. Without diodes, pressing three keys at once can ghost a fourth. - No keys are detected. Keypad
Rows need pull-ups, or use INPUT_PULLUP, while one column is driven low at a time. The Keypad library does this for you if you pass the pins in the right order. - The screen stays blank or shows only a row of blocks. Character LCD with I2C backpack
The I2C address is usually 0x27 or 0x3F. Run an I2C scanner to find yours. Then turn the contrast trimmer on the backpack slowly until text appears.
Questions and answers
Why put the keypad on a PCF8574?
A 4x4 keypad wired directly needs eight pins. Through a PCF8574 it needs only SDA and SCL, which the LCD's I2C backpack already uses, so the whole lock takes A4, A5 and pin 9.
Which PCF8574 pins go to the keypad?
Rows R1 to R4 on P0 to P3 and columns C1 to C4 on P4 to P7, the order I2CKeyPad scans. Swapped rows and columns give the wrong keys.
What if my expander or LCD has another address?
A PCF8574 is 0x20 to 0x27, set by A0 to A2, and a PCF8574A is 0x38 to 0x3F. LCD backpacks are usually 0x27 or 0x3F. Change the numbers in I2CKeyPad keyPad(0x20) and LiquidCrystal_I2C lcd(0x27, 16, 2).
How do I change the code or the lockout?
Change CODE (up to eight digits), MAX_TRIES and LOCKOUT_MS at the top of the sketch. The bolt stays open for OPEN_MS, 5 seconds.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.