How to Make a Password-Based Lock Using Arduino, Keypad & LCD Display

Password-Based Lock with Arduino, 4x4 Keypad & Servo Motor | Wokwi Tutorial
Expert Project 🔐 Security Arduino C++ Wokwi Simulator EEPROM Storage 4x4 Keypad

Password-Based
Electronic Lock
with Arduino

Build a complete electronic safe using an Arduino Uno, 4×4 membrane keypad, 16×2 LCD, and servo motor. Passwords are stored persistently in EEPROM, survive power cycles, and can be changed at any time — no hardware needed to start.

▶ Open Free Simulation in Wokwi
📅 February 2026 ⏱ ~25 min read 🔗 Wokwi #344891391763022419 👤 Uri Shaked (MIT Licence)

IntroProject Overview

This project turns an Arduino Uno into a fully functional electronic safe controller. It reads a 4-digit PIN from a membrane keypad, compares it against a code stored in EEPROM, and physically drives a servo motor to either the locked (20°) or unlocked (90°) position. The 16×2 LCD gives the user real-time feedback — from the startup animation through to access-denied warnings.

Because the code is saved to EEPROM, the safe remembers its state even after a power cycle. Users can also change their PIN at any time by pressing A on the keypad.

02Components

🔵
Arduino Uno
Brain of the system — reads keypad, drives LCD & servo
🖥️
16×2 LCD Display
Shows instructions, password prompts, and status messages
⌨️
4×4 Membrane Keypad
User input for PIN entry and special commands (A, #)
⚙️
Servo Motor
Physical lock mechanism — rotates between 20° (locked) and 90° (open)
🔁
Potentiometer
Adjusts LCD contrast for clear character visibility
💾
EEPROM (built-in)
Persistent storage for the secret code and lock state

03How It Works

The system has two modes — Locked and Unlocked. On power-up it reads the EEPROM to restore the previous state and syncs the servo accordingly. The main loop then calls either safeLockedLogic() or safeUnlockedLogic() based on the current state.

Power On Read EEPROM state, sync servo, show "Welcome!" startup animation
🔒 Locked LCD shows "Safe Locked!" and waits for 4-digit PIN input
⌨️ PIN Entry Each digit shown as * on LCD. Wait screen plays after 4 digits
Correct PIN Servo rotates to 90° — "Unlocked!" shown with custom icons
Wrong PIN "Access Denied!" displayed, slow wait screen, back to locked
🔑 Change PIN Press A when unlocked → enter new 4-digit code twice to confirm
# Re-Lock Press # when unlocked → servo returns to 20° locked position
💾 EEPROM Save Lock state and code persist to EEPROM on every change

04LCD Display States

The 16×2 LCD uses custom characters for the lock/unlock icons and provides clear visual feedback at every stage of operation.

Startup
Welcome!
ArduinoSafe v1.0
Animated text scrolls across row 2 on boot
Locked State
🔒 Safe Locked! 🔒
[____]
Awaiting PIN; digits appear as * in brackets
Unlocked State
🔓 # to lock 🔓
A = new code
Shows available actions when safe is open
Access Denied
Access Denied!
[==========]
Slow progress bar delays re-entry attempt
Unlocked!
🔓 Unlocked! 🔓
Brief success message before unlocked state screen
Set New Code
Enter new code:
[____]
Then "Confirm new code" — mismatch shows error

05Circuit Wiring

🖥️ LCD Display (Parallel 4-bit mode)
Arduino PinLCD PinWire ColourFunction
5VVDDRedPower
GNDVSS, RW, KBlackGround + R/W low
12RSBlueRegister Select
11EPurpleEnable
10D4GreenData bit 4
9D5BrownData bit 5
8D6GoldData bit 6
7D7GrayData bit 7
5V → 220Ω → AA (backlight)PinkLED backlight
⌨️ 4×4 Membrane Keypad
Arduino PinKeypad PinType
5R1 (Row 1)Row
4R2 (Row 2)Row
3R3 (Row 3)Row
2R4 (Row 4)Row
A3C1 (Col 1)Column
A2C2 (Col 2)Column
A1C3 (Col 3)Column
A0C4 (Col 4)Column
⚙️ Servo Motor
Arduino PinServo WireColour
6 (PWM)SignalOrange
5VV+Red
GNDGNDBlack/Brown

06Build Steps (1–12)

01
Understand the Components

Four main parts: Arduino Uno (controller), 16×2 LCD (output), 4×4 membrane keypad (input), servo motor (physical lock). Each has a distinct role that maps directly to the code architecture.

02
Understand How the System Works

When powered on, the LCD shows a welcome message. The user types a PIN on the keypad. Correct PIN → servo unlocks. Wrong PIN → error message. Pressing A when unlocked sets a new code; pressing # re-locks the safe.

03
Wire the LCD Display

The LCD runs in 4-bit parallel mode using pins 12, 11, 10, 9, 8, 7 on the Arduino. VCC to 5V, GND to ground. A potentiometer connected to the V0 (contrast) pin lets you adjust text visibility.

LiquidCrystal lcd(12, 11, 10, 9, 8, 7)
04
Wire the 4×4 Keypad

The keypad has 8 pins from a ribbon cable — 4 row pins and 4 column pins. Rows connect to digital pins 5–2; columns to analog pins A3–A0. The Keypad library handles matrix scanning automatically.

rowPins[] = {5,4,3,2} · colPins[] = {A3,A2,A1,A0}
05
Wire the Servo Motor

The servo has 3 wires: signal (orange) to pin 6, VCC (red) to 5V, GND (brown/black) to ground. The servo rotates between 20° (locked) and 90° (unlocked) based on lockServo.write() calls.

SERVO_LOCK_POS = 20° · SERVO_UNLOCK_POS = 90°
06
Connect Power and Ground

The Arduino 5V rail powers the LCD, keypad, and servo. All components share a common GND. Red wires carry power; black wires carry ground throughout the circuit.

07
Define the Default Password in Code

The secret code is stored in EEPROM starting at address 2. On first run (EEPROM_EMPTY = 0xFF), any PIN will unlock the safe and prompt the user to set a new one. The default flow is intentionally open until a code is established.

EEPROM address 0 = lock state · 1 = code length · 2+ = digits
08
Write the Keypad Scanning Logic

The inputSecretCode() function blocks until 4 digits are entered. Each valid digit (0–9) is appended to a String and displayed as '*' on the LCD. The Keypad library raises no interrupts — it polls on each iteration.

09
Write the Password Check Logic

safeState.unlock(userCode) reads the stored length and each digit from EEPROM and compares byte-by-byte. Any mismatch returns false immediately. On success it writes the unlocked state to EEPROM and rotates the servo.

10
Write the "Change Password" Logic

Pressing A calls setNewCode(). The user enters a new 4-digit code, then confirms it. If both entries match, safeState.setCode(newCode) writes it to EEPROM digit by digit. A mismatch shows "Code mismatch / Safe not locked!" without locking.

Press A on the keypad when unlocked to trigger code change
11
Write the Lock Function

Pressing # triggers the locking sequence: the LCD shows the transition animation (🔓 → → 🔒), then lock() writes the servo to 20° and sets the EEPROM state to locked. A fast 10-step progress bar plays before the loop restarts.

Press # when unlocked to re-lock the safe
12
Run the Simulation

Open the Wokwi project, click Play. The LCD animates the startup sequence, then displays "🔒 Safe Locked!" and awaits input. Click keypad buttons to enter a PIN. On first run (no EEPROM code) any 4 digits unlock the safe and prompt for a new code.

Wokwi simulates EEPROM — your code persists within the session

Try it in Your Browser

No hardware needed. The full circuit with EEPROM, servo, LCD, and keypad runs at 100% speed in Wokwi.

▶ Open Free Simulation

07Full Source Code

This project spans five files. Click each tab to view the file. All files must be present in the same Arduino sketch folder (or Wokwi project).

sketch.ino
Arduino C++
/**
 * Arduino Electronic Safe
 * Copyright (C) 2020, Uri Shaked. MIT License.
 */

#include <LiquidCrystal.h>
#include <Keypad.h>
#include <Servo.h>
#include "SafeState.h"
#include "icons.h"

/* Locking mechanism */
#define SERVO_PIN        6
#define SERVO_LOCK_POS   20   // degrees — locked
#define SERVO_UNLOCK_POS 90   // degrees — unlocked
Servo lockServo;

/* Display — 4-bit parallel mode */
LiquidCrystal lcd(12, 11, 10, 9, 8, 7);

/* Keypad — 4×4 matrix */
const byte KEYPAD_ROWS = 4;
const byte KEYPAD_COLS = 4;
byte rowPins[KEYPAD_ROWS] = {5, 4, 3, 2};
byte colPins[KEYPAD_COLS] = {A3, A2, A1, A0};
char keys[KEYPAD_ROWS][KEYPAD_COLS] = {
  {'1','2','3','A'},
  {'4','5','6','B'},
  {'7','8','9','C'},
  {'*','0','#','D'}
};
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, KEYPAD_ROWS, KEYPAD_COLS);

/* SafeState persists code + lock state in EEPROM */
SafeState safeState;

void lock() {
  lockServo.write(SERVO_LOCK_POS);
  safeState.lock();
}

void unlock() {
  lockServo.write(SERVO_UNLOCK_POS);
}

void showStartupMessage() {
  lcd.setCursor(4, 0); lcd.print("Welcome!"); delay(1000);
  lcd.setCursor(0, 2);
  String msg = "ArduinoSafe v1.0";
  for (byte i = 0; i < msg.length(); i++) {
    lcd.print(msg[i]); delay(100);
  }
  delay(500);
}

String inputSecretCode() {
  lcd.setCursor(5, 1); lcd.print("[____]");
  lcd.setCursor(6, 1);
  String result = "";
  while (result.length() < 4) {
    char key = keypad.getKey();
    if (key >= '0' && key <= '9') {
      lcd.print('*'); result += key;
    }
  }
  return result;
}

void showWaitScreen(int delayMillis) {
  lcd.setCursor(2, 1); lcd.print("[..........]");
  lcd.setCursor(3, 1);
  for (byte i = 0; i < 10; i++) {
    delay(delayMillis); lcd.print("=");
  }
}

bool setNewCode() {
  lcd.clear(); lcd.setCursor(0, 0); lcd.print("Enter new code:");
  String newCode = inputSecretCode();
  lcd.clear(); lcd.setCursor(0, 0); lcd.print("Confirm new code");
  String confirmCode = inputSecretCode();
  if (newCode.equals(confirmCode)) {
    safeState.setCode(newCode); return true;
  }
  lcd.clear(); lcd.setCursor(1, 0); lcd.print("Code mismatch");
  lcd.setCursor(0, 1); lcd.print("Safe not locked!");
  delay(2000); return false;
}

void showUnlockMessage() {
  lcd.clear();
  lcd.setCursor(0, 0); lcd.write(ICON_UNLOCKED_CHAR);
  lcd.setCursor(4, 0); lcd.print("Unlocked!");
  lcd.setCursor(15, 0); lcd.write(ICON_UNLOCKED_CHAR);
  delay(1000);
}

void safeUnlockedLogic() {
  lcd.clear();
  lcd.setCursor(0, 0); lcd.write(ICON_UNLOCKED_CHAR);
  lcd.setCursor(2, 0); lcd.print(" # to lock");
  lcd.setCursor(15, 0); lcd.write(ICON_UNLOCKED_CHAR);
  bool newCodeNeeded = true;
  if (safeState.hasCode()) {
    lcd.setCursor(0, 1); lcd.print("  A = new code");
    newCodeNeeded = false;
  }
  auto key = keypad.getKey();
  while (key != 'A' && key != '#') key = keypad.getKey();
  bool readyToLock = true;
  if (key == 'A' || newCodeNeeded) readyToLock = setNewCode();
  if (readyToLock) {
    lcd.clear(); lcd.setCursor(5, 0);
    lcd.write(ICON_UNLOCKED_CHAR); lcd.print(" ");
    lcd.write(ICON_RIGHT_ARROW); lcd.print(" ");
    lcd.write(ICON_LOCKED_CHAR);
    safeState.lock(); lock(); showWaitScreen(100);
  }
}

void safeLockedLogic() {
  lcd.clear();
  lcd.setCursor(0, 0); lcd.write(ICON_LOCKED_CHAR);
  lcd.print(" Safe Locked! "); lcd.write(ICON_LOCKED_CHAR);
  String userCode = inputSecretCode();
  bool ok = safeState.unlock(userCode);
  showWaitScreen(200);
  if (ok) { showUnlockMessage(); unlock(); }
  else {
    lcd.clear(); lcd.setCursor(0, 0); lcd.print("Access Denied!");
    showWaitScreen(1000);
  }
}

void setup() {
  lcd.begin(16, 2); init_icons(lcd);
  lockServo.attach(SERVO_PIN);
  Serial.begin(115200);
  if (safeState.locked()) lock(); else unlock();
  showStartupMessage();
}

void loop() {
  if (safeState.locked()) safeLockedLogic();
  else safeUnlockedLogic();
}
SafeState.h
C++ Header
/** Arduino Electronic Safe — Copyright (C) 2020, Uri Shaked. MIT License. */

#ifndef SAFESTATE_H
#define SAFESTATE_H

class SafeState {
public:
  SafeState();
  void lock();
  bool unlock(String code);
  bool locked();
  bool hasCode();
  void setCode(String newCode);

private:
  void setLock(bool locked);
  bool _locked;
};

#endif /* SAFESTATE_H */
SafeState.cpp
C++
/** Arduino Electronic Safe — Copyright (C) 2020, Uri Shaked. MIT License. */

#include <Arduino.h>
#include <EEPROM.h>
#include "SafeState.h"

#define EEPROM_ADDR_LOCKED   0
#define EEPROM_ADDR_CODE_LEN 1
#define EEPROM_ADDR_CODE     2
#define EEPROM_EMPTY         0xff
#define SAFE_STATE_OPEN   (char)0
#define SAFE_STATE_LOCKED (char)1

SafeState::SafeState() {
  this->_locked = EEPROM.read(EEPROM_ADDR_LOCKED) == SAFE_STATE_LOCKED;
}

void SafeState::lock() { this->setLock(true); }

bool SafeState::locked() { return this->_locked; }

bool SafeState::hasCode() {
  auto codeLength = EEPROM.read(EEPROM_ADDR_CODE_LEN);
  return codeLength != EEPROM_EMPTY;
}

void SafeState::setCode(String newCode) {
  EEPROM.write(EEPROM_ADDR_CODE_LEN, newCode.length());
  for (byte i = 0; i < newCode.length(); i++) {
    EEPROM.write(EEPROM_ADDR_CODE + i, newCode[i]);
  }
}

bool SafeState::unlock(String code) {
  auto codeLength = EEPROM.read(EEPROM_ADDR_CODE_LEN);
  if (codeLength == EEPROM_EMPTY) { this->setLock(false); return true; }
  if (code.length() != codeLength) return false;
  for (byte i = 0; i < code.length(); i++) {
    auto digit = EEPROM.read(EEPROM_ADDR_CODE + i);
    if (digit != code[i]) return false;
  }
  this->setLock(false); return true;
}

void SafeState::setLock(bool locked) {
  this->_locked = locked;
  EEPROM.write(EEPROM_ADDR_LOCKED, locked ? SAFE_STATE_LOCKED : SAFE_STATE_OPEN);
}
icons.h
C++ Header
/** Arduino Electronic Safe — Copyright (C) 2020, Uri Shaked. MIT License. */

#ifndef ICONS_H
#define ICONS_H

#include <LiquidCrystal.h>

// Custom character slot numbers (0–7 on HD44780)
#define ICON_LOCKED_CHAR   (byte)0
#define ICON_UNLOCKED_CHAR (byte)1

// Standard right-arrow in HD44780 character set
#define ICON_RIGHT_ARROW   (byte)126

void init_icons(LiquidCrystal &lcd);

#endif /* ICONS_H */
icons.cpp
C++
/** Arduino Electronic Safe — Copyright (C) 2020, Uri Shaked. MIT License. */

#include <Arduino.h>
#include "icons.h"

// 5×8 pixel bitmap — closed padlock
const byte iconLocked[8] PROGMEM = {
  0b01110,   // ▄█▄  (shackle top)
  0b10001,   // █   (shackle sides)
  0b10001,
  0b11111,   // █████ (lock body top)
  0b11011,   // key hole
  0b11011,
  0b11111,   // █████ (lock body bottom)
};

// 5×8 pixel bitmap — open padlock
const byte iconUnlocked[8] PROGMEM = {
  0b01110,
  0b10000,   // shackle open on right
  0b10000,
  0b11111,
  0b11011,
  0b11011,
  0b11111,
};

void init_icons(LiquidCrystal &lcd) {
  byte icon[8];
  memcpy_P(icon, iconLocked,   sizeof(icon));
  lcd.createChar(ICON_LOCKED_CHAR, icon);
  memcpy_P(icon, iconUnlocked, sizeof(icon));
  lcd.createChar(ICON_UNLOCKED_CHAR, icon);
}

08diagram.json — Wokwi Circuit File

Paste this into your Wokwi project's diagram.json. It places the Arduino Uno, 4×4 keypad, servo, LCD, and resistor, then wires all connections with colour-coded routes.

📄 diagram.json
{
  "version" 1,
  "author" "Uri Shaked",
  "editor" "wokwi",
  "parts" [
    { "id" "uno",    "type" "wokwi-arduino-uno",     "top" 200, "left" 20 },
    { "id" "keypad", "type" "wokwi-membrane-keypad",  "left" 360, "top" 140 },
    {
      "id" "servo", "type" "wokwi-servo",
      "left" 400, "top" 20,
      "attrs" { "hornColor" "black" }
    },
    { "id" "lcd", "type" "wokwi-lcd1602", "top" 8, "left" 20 },
    {
      "id" "r1", "type" "wokwi-resistor",
      "top" 140, "left" 220,
      "attrs" { "value" "220" }
    }
  ],
  "connections" [
    ["uno:GND.1", "lcd:VSS", "black",  ["v-51", "*", "h0", "v18"]],
    ["uno:GND.1", "lcd:K",   "black",  ["v-51", "*", "h0", "v18"]],
    ["uno:GND.1", "lcd:RW",  "black",  ["v-51", "*", "h0", "v18"]],
    ["uno:5V",   "lcd:VDD", "red",    ["v16",  "h-16"]],
    ["uno:5V",   "r1:2",    "red",    ["v16",  "h-118", "v-244", "h50"]],
    ["r1:1",     "lcd:A",   "pink",   []],
    ["uno:12",   "lcd:RS",  "blue",   ["v-16", "*", "h0", "v20"]],
    ["uno:11",   "lcd:E",   "purple", ["v-20", "*", "h0", "v20"]],
    ["uno:10",   "lcd:D4",  "green",  ["v-24", "*", "h0", "v20"]],
    ["uno:9",    "lcd:D5",  "brown",  ["v-28", "*", "h0", "v20"]],
    ["uno:8",    "lcd:D6",  "gold",   ["v-32", "*", "h0", "v20"]],
    ["uno:7",    "lcd:D7",  "gray",   ["v-36", "*", "h0", "v20"]],
    ["uno:6",    "servo:PWM", "orange", ["v-40", "*", "h0",  "h-52"]],
    ["uno:5V",   "servo:V+",  "red",    ["v16",  "h-118", "v-244", "*", "h-56"]],
    ["uno:GND.1","servo:GND", "black",  ["v-46", "*", "v88",   "h-60"]],
    ["uno:A3",   "keypad:C1", "brown",  ["v116", "*", "h0", "v0"]],
    ["uno:A2",   "keypad:C2", "gray",   ["v120", "*", "h0", "v0"]],
    ["uno:A1",   "keypad:C3", "orange", ["v124", "*", "h0", "v0"]],
    ["uno:A0",   "keypad:C4", "pink",   ["v128", "*", "h0", "v0"]],
    ["uno:5",    "keypad:R1", "blue",   ["v-34", "h96", "*", "v12"]],
    ["uno:4",    "keypad:R2", "green",  ["v-30", "h80", "*", "v16"]],
    ["uno:3",    "keypad:R3", "purple", ["v-26", "h64", "*", "v20"]],
    ["uno:2",    "keypad:R4", "gold",   ["v-22", "h48", "*", "v24"]]
  ]
}

09More Arduino Projects

© 2026 MakeMindz · Arduino & Wokwi Project Tutorials

Original ArduinoSafe project by Uri Shaked — released under the MIT Licence.

Comments

try for free