← All projects
Automation Beginner

Code Lock: a 4×4 Keypad, an LCD and a PIN

A 4×4 membrane keypad needs only 8 pins thanks to matrix scanning. We build a code lock: PIN entry shown as asterisks on an LCD, "#" to confirm, "*" to clear, and an LED standing in for the electric lock.

Difficulty
Beginner
Time
45 min
Category
Automation
Updated

About the project

16 buttons, 16 pins? No, eight are enough: the keys of a membrane keypad are wired as a matrix of 4 rows × 4 columns. We build a code lock with an LCD 1602 and a "lock" that opens for 2 seconds after the right PIN.

What you need

  • ESP32 DevKit
  • 4×4 membrane keypad
  • LCD 1602 with an I2C backpack
  • LED + 220 Ω resistor (in a real build, a relay or MOSFET and an electric lock)
  • The Keypad and LiquidCrystal_I2C libraries

Wiring

Keypad R1 R2 R3 R4 C1 C2 C3 C4
ESP32 GPIO19 GPIO18 GPIO5 GPIO17 GPIO16 GPIO4 GPIO13 GPIO14
  • LCD: VCC → 3V3, GND → GND, SDA → GPIO21, SCL → GPIO22.
  • "Lock": GPIO26 → 220 Ω → LED → GND.

The keypad ribbon has 8 contacts: left to right (keys facing you), the four rows come first, then the four columns.

How the matrix is scanned

The library pulls one column LOW at a time and reads the rows with INPUT_PULLUP. If row R2 reads LOW while column C3 is active, the key at that crossing, "6", is pressed. A full scan takes microseconds, so the user never notices.

keypad.getKey() returns a character once per press (debounced inside), or 0 when nothing is pressed.

Code

// Code lock: 4×4 keypad + LCD 1602, PIN 1234
#include <Keypad.h>
#include <LiquidCrystal_I2C.h>

const byte ROWS = 4, COLS = 4;
char keys[ROWS][COLS] = {
  {'1', '2', '3', 'A'},
  {'4', '5', '6', 'B'},
  {'7', '8', '9', 'C'},
  {'*', '0', '#', 'D'},
};
byte rowPins[ROWS] = {19, 18, 5, 17};
byte colPins[COLS] = {16, 4, 13, 14};
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);
LiquidCrystal_I2C lcd(0x27, 16, 2);

const int LOCK = 26;
const String PIN_CODE = "1234";
String entered;

void show() {
  lcd.clear();
  lcd.print("PIN: ");
  for (unsigned int i = 0; i < entered.length(); i++) lcd.print('*');   // never show the digits
}

void setup() {
  lcd.init();
  lcd.backlight();
  pinMode(LOCK, OUTPUT);
  show();
}

void loop() {
  char k = keypad.getKey();
  if (!k) return;
  if (k == '#') {             // confirm
    lcd.clear();
    if (entered == PIN_CODE) {
      lcd.print("Access granted");
      digitalWrite(LOCK, HIGH);   // open the lock for 2 s
      delay(2000);
      digitalWrite(LOCK, LOW);
    } else {
      lcd.print("Wrong PIN");
      delay(1500);
    }
    entered = "";
    show();
  } else if (k == '*') {      // clear
    entered = "";
    show();
  } else if (entered.length() < 8) {
    entered += k;
    show();
  }
}

Doing it for real

  • The lock. A 12 V electric strike draws 0.3–1 A: switch it through a MOSFET or relay, with a diode across the coil.
  • Brute-force protection. After three wrong PINs, block input for a minute, then double the pause each time.
  • Keep the PIN out of the code. Store it in Preferences (the ESP32 flash) and allow changing it, e.g. "A" + old PIN + new PIN.
  • Power. A lock that opens on any glitch is a bad idea. Think about what happens if the ESP32 resets while the output is HIGH.