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

 

Step 1 — Understand the Components

There are four main parts in this project. The Arduino Uno (blue board in the center) is the brain that controls everything. The 16x2 LCD display (green screen at the top) shows messages to the user. The 4x4 membrane keypad (dark panel on the right) is used to input the password. The servo motor (top right) acts as the physical lock mechanism.


Step 2 — Understand How the System Works

The system works like a simple door lock. When you power it on, the LCD shows instructions to the user. You type a password on the keypad. If the password is correct, the servo rotates to unlock. If it's wrong, the LCD displays an error. Pressing A on the keypad allows you to set a new password, which is why the LCD currently shows "A = new code".

code:


/**
   Arduino Electronic Safe

   Copyright (C) 2020, Uri Shaked.
   Released under the MIT License.
*/

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

/* Locking mechanism definitions */
#define SERVO_PIN 6
#define SERVO_LOCK_POS   20
#define SERVO_UNLOCK_POS 90
Servo lockServo;

/* Display */
LiquidCrystal lcd(12, 11, 10, 9, 8, 7);

/* Keypad setup */
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 stores the secret code 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 message = "ArduinoSafe v1.0";
  for (byte i = 0; i < message.length(); i++) {
    lcd.print(message[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;
  } else {
    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 unlockedSuccessfully = safeState.unlock(userCode);
  showWaitScreen(200);

  if (unlockedSuccessfully) {
    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);

  /* Make sure the physical lock is sync with the EEPROM state */
  Serial.begin(115200);
  if (safeState.locked()) {
    lock();
  } else {
    unlock();
  }

  showStartupMessage();
}

void loop() {
  if (safeState.locked()) {
    safeLockedLogic();
  } else {
    safeUnlockedLogic();
  }
}





Step 3 — Wire the LCD Display

The LCD has multiple pins along its bottom edge. The colored wires connect it to the Arduino Uno. The key connections are VCC to 5V, GND to GND, and the data pins (D4–D7) to digital pins on the Arduino. A potentiometer (the small component between the LCD and Arduino) is connected to the LCD's contrast pin to adjust the screen brightness/contrast.


Step 4 — Wire the 4x4 Keypad

The keypad has 8 pins (4 row pins and 4 column pins) coming out from the bottom through a ribbon cable. These connect to digital pins on the Arduino. The keypad uses the same row/column multiplexing technique as the LED matrix — it scans rows and columns to detect which key is pressed.

/**
   Arduino Electronic Safe

   Copyright (C) 2020, Uri Shaked.
   Released under the MIT License.
*/

#ifndef ICONS_H
#define ICONS_H

#include <LiquidCrystal.h>

// Our custom icon numbers
#define ICON_LOCKED_CHAR   (byte)0
#define ICON_UNLOCKED_CHAR (byte)1

// This is a standard icon on the LCD1602 character set
#define ICON_RIGHT_ARROW   (byte)126

void init_icons(LiquidCrystal &lcd);

#endif /* ICONS_H */




Step 5 — Wire the Servo Motor

The servo motor has 3 wires (the orange/yellow/brown bundle at the top). The signal wire connects to a PWM-capable digital pin on the Arduino (to control the rotation angle). VCC connects to 5V and GND connects to ground. The servo physically rotates between two positions — one for locked and one for unlocked.

/**
   Arduino Electronic Safe

   Copyright (C) 2020, Uri Shaked.
   Released under the 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 */


Step 6 — Connect Power and Ground

The red and black wires running around the board handle the power and ground connections. The Arduino Uno supplies 5V to power the LCD, keypad, and servo. All components share a common ground connection.

/**
   Arduino Electronic Safe

   Copyright (C) 2020, Uri Shaked.
   Released under the MIT License.
*/

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

const byte iconLocked[8] PROGMEM = {
  0b01110,
  0b10001,
  0b10001,
  0b11111,
  0b11011,
  0b11011,
  0b11111,
};

const byte iconUnlocked[8] PROGMEM = {
  0b01110,
  0b10000,
  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);
}


Step 7 — Define the Default Password in Code

In the Arduino sketch, a default password is stored as a string variable (for example, "1234"). This is what the system checks against when the user types on the keypad. The user can later change this by pressing A and entering a new code.

/**
   Arduino Electronic Safe

   Copyright (C) 2020, Uri Shaked.
   Released under the MIT License.
*/

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

/* Safe state */
#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) {
    // There was no code, so unlock always succeeds
    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);
}



Step 8 — Write the Keypad Scanning Logic

The code uses a Keypad library to handle scanning the 4x4 matrix. It continuously checks if a key is being pressed. When a key is detected, the character is added to an input string that builds up the password the user is typing.

{
  "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"]]
  ]
}


Step 9 — Write the Password Check Logic

Once the user finishes entering their code (usually by pressing #), the code compares what was typed against the stored password. If they match, the servo is commanded to rotate to the unlocked position. If they don't match, the LCD displays an error message like "Wrong Code" and resets the input.




Step 10 — Write the "Change Password" Logic

When the user presses A, the system enters a special mode. The LCD displays "A = new code" as shown in the screenshot. It then waits for the user to type a new password and confirm it. Once confirmed, the stored password variable is updated in memory for future attempts.


Step 11 — Write the Lock Function

Pressing # (as shown on the LCD — "# to lock") triggers the servo to rotate back to the locked position. This is a simple servo angle command in the code, switching between two preset angles (e.g., 0° for locked, 90° for unlocked).


Step 12 — Run the Simulation

The project is running in Wokwi at 100% speed as shown in the top right. The LCD is actively displaying instructions, and the servo is ready to respond to keypad input in real time.

Comments