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 WokwiIntroProject 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
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.
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.
ArduinoSafe v1.0
[____]
A = new code
[==========]
[____]
05Circuit Wiring
| Arduino Pin | LCD Pin | Wire Colour | Function |
|---|---|---|---|
| 5V | VDD | Red | Power |
| GND | VSS, RW, K | Black | Ground + R/W low |
| 12 | RS | Blue | Register Select |
| 11 | E | Purple | Enable |
| 10 | D4 | Green | Data bit 4 |
| 9 | D5 | Brown | Data bit 5 |
| 8 | D6 | Gold | Data bit 6 |
| 7 | D7 | Gray | Data bit 7 |
| 5V → 220Ω → A | A (backlight) | Pink | LED backlight |
| Arduino Pin | Keypad Pin | Type |
|---|---|---|
| 5 | R1 (Row 1) | Row |
| 4 | R2 (Row 2) | Row |
| 3 | R3 (Row 3) | Row |
| 2 | R4 (Row 4) | Row |
| A3 | C1 (Col 1) | Column |
| A2 | C2 (Col 2) | Column |
| A1 | C3 (Col 3) | Column |
| A0 | C4 (Col 4) | Column |
| Arduino Pin | Servo Wire | Colour |
|---|---|---|
| 6 (PWM) | Signal | Orange |
| 5V | V+ | Red |
| GND | GND | Black/Brown |
06Build Steps (1–12)
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.
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.
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)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}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.
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.
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+ = digitsThe 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.
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.
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.
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.
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 sessionTry it in Your Browser
No hardware needed. The full circuit with EEPROM, servo, LCD, and keypad runs at 100% speed in Wokwi.
▶ Open Free Simulation07Full 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).
/** * 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(); }
/** 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 */
/** 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); }
/** 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 */
/** 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.
{
"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"]]
]
}
Comments
Post a Comment