PIR Motion Detection
Security System
with Raspberry Pi Pico
Build a professional motion detection security system using a PIR sensor — trigger alarms, activate LED alerts, and display real-time status on a 16×2 LCD. Test everything free in Wokwi before touching hardware.
▶ Open Free SimulationProject Overview
This project implements a complete, professional-grade motion detection security system on a Raspberry Pi Pico. A PIR sensor detects human movement up to 7 metres, triggering a 5-second buzzer alarm, blinking red LED and LCD alert — all controlled through a clean 2-state machine with arm/disarm via a push button.
PIR Detection
HC-SR501 senses movement up to 7m. Output HIGH triggers the alarm sequence.
Arm / Disarm
Single button toggles system state with confirmation beep and green LED.
Red LED Alert
Blinks rapidly during the full 5-second alarm duration.
Green LED Armed
Stays on while system is armed as a constant visual status indicator.
Buzzer Alarm
Pulse pattern for 5 seconds. Single beep on arm, double beep on disarm.
LCD Status
16×2 I2C display shows ARMED / ALERT / detection counter in real time.
Event Counter
Tracks total intrusion detections, resets when system is re-armed.
3s Cooldown
Prevents repeated triggers from a single movement event.
Components Required
No hardware needed: All components above are available in the Wokwi simulator. The diagram.json in Section 4 wires everything automatically — just click ▶ Run.
Circuit Connections
| Component | Pin / Signal | Pico GPIO | Notes |
|---|---|---|---|
| PIR OUT | Motion signal | GP15 | Digital IN, HIGH = motion |
| PIR VCC | Power | VBUS (5V) | HC-SR501 needs 5V |
| PIR GND | Ground | GND.3 | |
| LCD SDA | I2C Data | GP0 | I2C0 |
| LCD SCL | I2C Clock | GP1 | I2C0 @ 400 kHz |
| LCD VCC | Power | VBUS (5V) | Backlight needs 5V |
| LCD GND | Ground | GND.8 | |
| Red LED Anode | Alert output | GP16 → 220Ω | Blinks during alarm |
| Green LED Anode | Armed output | GP17 → 220Ω | ON when armed |
| LED Cathodes | Ground | GND.1 | Both LEDs share |
| Buzzer + | Digital OUT | GP14 | Alarm pulse |
| Buzzer − | Ground | GND.3 | |
| Button pin 1 | ARM/DISARM | GP18 | PULL_DOWN enabled |
| Button pin 2 | Logic HIGH | 3.3V | Reads 1 when pressed |
| Built-in LED | Status blink | GP25 | Slow blink when armed |
┌─────────────────────────────┐ ┌───────────────────┐ │ Raspberry Pi Pico │ │ HC-SR501 PIR │ │ VBUS ─────────────────────────│ VCC │ │ GND.3 ─────────────────────────│ GND │ │ GP15 ─────────────────────────│ OUT │ └─────────────────────────────┘ └───────────────────┘ ┌─────────────────────────────┐ ┌───────────────────┐ │ GP0 (SDA) ──────────────────────│ SDA 16×2 LCD │ │ GP1 (SCL) ──────────────────────│ SCL │ │ VBUS ──────────────────────│ VCC │ │ GND.8 ──────────────────────│ GND │ └─────────────────────────────┘ └───────────────────┘ GP16 ────── 220Ω ──── [RED LED] ──── GND.1 (ALERT) GP17 ────── 220Ω ──── [GRN LED] ──── GND.1 (ARMED) GP14 ────── [BUZZER+] ───────────── GND.3 GP18 ────── [BTN 1.l] │ [BTN 1.r] ────── 3.3V (PULL_DOWN)
diagram.json
Paste the block below into the diagram.json tab in Wokwi to load the complete circuit instantly — Pico, PIR sensor, 16×2 LCD, red & green LEDs, resistors, buzzer and push button, all pre-wired.
{
"version": 1,
"author": "Motion Detection Security System",
"editor": "wokwi",
"parts": [
{ "type": "wokwi-pi-pico", "id": "pico", "top": 0, "left": 0, "attrs": {} },
{ "type": "wokwi-pir-motion-sensor", "id": "pir1", "top": -60, "left": 300, "attrs": {} },
{
"type": "wokwi-lcd1602", "id": "lcd1",
"top": 150, "left": 300,
"attrs": { "pins": "i2c" }
},
{ "type": "wokwi-buzzer", "id": "buzzer1", "top": -100, "left": 450, "attrs": {} },
{
"type": "wokwi-led", "id": "led1",
"top": -100, "left": -150,
"attrs": { "color": "red", "label": "ALERT" }
},
{
"type": "wokwi-led", "id": "led2",
"top": -50, "left": -150,
"attrs": { "color": "green", "label": "ARMED" }
},
{
"type": "wokwi-pushbutton", "id": "btn1",
"top": 50, "left": -150,
"attrs": { "color": "blue", "label": "ARM/DISARM" }
},
{ "type": "wokwi-resistor", "id": "r1", "top": -90, "left": -220, "attrs": { "value": "220" } },
{ "type": "wokwi-resistor", "id": "r2", "top": -40, "left": -220, "attrs": { "value": "220" } }
],
"connections": [
["pico:GP0", "lcd1:SDA", "green", ["v0"]],
["pico:GP1", "lcd1:SCL", "blue", ["v0"]],
["lcd1:VCC", "pico:VBUS", "red", ["v0"]],
["lcd1:GND", "pico:GND.8", "black", ["v0"]],
["pir1:VCC", "pico:VBUS", "red", ["v0"]],
["pir1:GND", "pico:GND.3", "black", ["v0"]],
["pir1:OUT", "pico:GP15", "orange", ["v0"]],
["buzzer1:1", "pico:GP14", "purple", ["v0"]],
["buzzer1:2", "pico:GND.3", "black", ["v0"]],
["r1:1", "pico:GP16", "red", ["v0"]],
["r1:2", "led1:A", "", ["v0"]],
["led1:C", "pico:GND.1", "black", ["v0"]],
["r2:1", "pico:GP17", "green", ["v0"]],
["r2:2", "led2:A", "", ["v0"]],
["led2:C", "pico:GND.1", "black", ["v0"]],
["btn1:1.l", "pico:GP18", "blue", ["v0"]],
["btn1:1.r", "pico:3V3", "red", ["v0"]]
],
"dependencies": {}
}
How the System Works
2-State Machine
⚪ DISARMED
No monitoring.
Green LED off.
🟢 ARMED
PIR monitored.
Green LED on.
🔴 ALARM
5s alarm.
Red LED blinks.
🟢 ARMED
Back to monitoring
after cooldown.
| Event | System Action | Duration |
|---|---|---|
| Button pressed (DISARMED) | Arm system, green LED ON, single beep, reset counter | Instant |
| Button pressed (ARMED) | Disarm system, green LED OFF, red LED OFF, double beep | Instant |
| PIR HIGH (in cooldown) | Ignored — spam protection active | 3s block |
| PIR HIGH (after cooldown) | Alarm: red LED ON, buzzer pulses, LCD shows ALERT, counter++ | 5 seconds |
| Alarm ends | Red LED OFF, buzzer OFF, LCD returns to ARMED, cooldown starts | 3s cooldown |
| Armed + idle | Built-in LED slow blink (0.5 Hz status), LCD shows Alerts count | Continuous |
Button debouncing: The code enforces a 300 ms hardware debounce window. Pressing the button faster than this is ignored, ensuring reliable ARM/DISARM toggling without false triggers.
LCD Display Examples
The 16×2 I2C LCD updates in real time as the system changes state. Each message is centred on its row.
Full MicroPython Code
Paste into main.py in Wokwi. Includes a complete embedded LCD I2C driver — no external library required.
""" Motion Detection Security System Raspberry Pi Pico + PIR Sensor (HC-SR501) Wokwi Simulator Compatible """ from machine import Pin, I2C import time # ── Pin Configuration ──────────────────────────────────────── PIR_PIN = 15 # PIR sensor output BUZZER_PIN = 14 # Buzzer / alarm LED_ALERT = 16 # Red LED – motion detected LED_ARMED = 17 # Green LED – system armed BUTTON_PIN = 18 # Push button – arm/disarm LED_STATUS = 25 # Built-in LED – status blink ALARM_DURATION = 5 # Alarm sounds for 5 seconds COOLDOWN_TIME = 3 # 3 s cooldown between detections # ── Hardware Initialisation ────────────────────────────────── pir = Pin(PIR_PIN, Pin.IN) buzzer = Pin(BUZZER_PIN, Pin.OUT) led_alert = Pin(LED_ALERT, Pin.OUT) led_armed = Pin(LED_ARMED, Pin.OUT) led_status = Pin(LED_STATUS, Pin.OUT) button = Pin(BUTTON_PIN, Pin.IN, Pin.PULL_DOWN) i2c = I2C(0, scl=Pin(1), sda=Pin(0), freq=400000) # ── LCD 16×2 I2C Driver ────────────────────────────────────── class LCD_I2C: def __init__(self, i2c, addr=0x27, rows=2, cols=16): self.i2c = i2c; self.addr = addr self.LCD_CLEARDISPLAY = 0x01; self.LCD_ENTRYMODESET = 0x04 self.LCD_DISPLAYCONTROL= 0x08; self.LCD_FUNCTIONSET = 0x20 self.LCD_SETDDRAMADDR = 0x80 self.LCD_DISPLAYON = 0x04; self.LCD_CURSOROFF = 0x00 self.LCD_BLINKOFF = 0x00; self.LCD_ENTRYLEFT = 0x02 self.LCD_ENTRYSHIFTDEC = 0x00 self.LCD_4BITMODE = 0x00; self.LCD_2LINE = 0x08 self.LCD_5x8DOTS = 0x00 self.LCD_BACKLIGHT = 0x08; self.LCD_NOBACKLIGHT = 0x00 self.backlight_state = self.LCD_BACKLIGHT self.init_display() def write_byte(self, byte, mode): hi = mode|(byte&0xF0)|self.backlight_state lo = mode|((byte<<4)&0xF0)|self.backlight_state self.i2c.writeto(self.addr, bytearray([hi])); self.toggle_enable(hi) self.i2c.writeto(self.addr, bytearray([lo])); self.toggle_enable(lo) def toggle_enable(self, byte): time.sleep_us(1) self.i2c.writeto(self.addr, bytearray([byte|0x04])) time.sleep_us(1) self.i2c.writeto(self.addr, bytearray([byte&~0x04])) time.sleep_us(50) def init_display(self): time.sleep_ms(50) for v in (0x03,0x03,0x03,0x02): self.write_byte(v,0); time.sleep_ms(5) self.write_byte(self.LCD_FUNCTIONSET|self.LCD_4BITMODE|self.LCD_2LINE|self.LCD_5x8DOTS,0) self.write_byte(self.LCD_DISPLAYCONTROL|self.LCD_DISPLAYON|self.LCD_CURSOROFF|self.LCD_BLINKOFF,0) self.clear() self.write_byte(self.LCD_ENTRYMODESET|self.LCD_ENTRYLEFT|self.LCD_ENTRYSHIFTDEC,0) def clear(self): self.write_byte(0x01,0); time.sleep_ms(2) def set_cursor(self, row, col): self.write_byte(self.LCD_SETDDRAMADDR|(col+[0x00,0x40][row]),0) def print(self, text, row=0, col=0): self.set_cursor(row,col) for c in str(text): self.write_byte(ord(c),1) # ── LCD Init ───────────────────────────────────────────────── try: lcd = LCD_I2C(i2c); lcd_ok = True print("LCD initialised") except: lcd_ok = False print("LCD not found – serial monitor only") # ── Global State ───────────────────────────────────────────── system_armed = False detection_count = 0 last_detection = 0 # ── Helper Functions ───────────────────────────────────────── def startup_sequence(): print("="*48) print(" MOTION DETECTION SECURITY SYSTEM") print(" Raspberry Pi Pico + HC-SR501 PIR") print("="*48) if lcd_ok: lcd.clear(); lcd.print("Security System",0,0) lcd.print("Initializing...",1,0) # Test LEDs + buzzer led_alert.on(); led_armed.on(); time.sleep(0.5) led_alert.off(); led_armed.off() buzzer.on(); time.sleep(0.2); buzzer.off() time.sleep(1) if lcd_ok: lcd.clear(); lcd.print("Press Button",0,0) lcd.print("to ARM system",1,0) print("System ready. Press button to ARM/DISARM.\n") def toggle_system(): global system_armed, detection_count system_armed = not system_armed if system_armed: led_armed.on(); detection_count = 0 print("\n🔒 SYSTEM ARMED – Monitoring for motion...") if lcd_ok: lcd.clear(); lcd.print("SYSTEM ARMED",0,2) lcd.print("Monitoring...",1,1) buzzer.on(); time.sleep(0.1); buzzer.off() else: led_armed.off(); led_alert.off(); buzzer.off() print("\n🔓 SYSTEM DISARMED – Standing by...") if lcd_ok: lcd.clear(); lcd.print("DISARMED",0,3) lcd.print("Press to ARM",1,1) for _ in range(2): buzzer.on(); time.sleep(0.1); buzzer.off(); time.sleep(0.1) def check_motion(): return pir.value() == 1 def trigger_alarm(): global detection_count, last_detection detection_count += 1 print(f"\n{'!'*48}\n⚠️ MOTION DETECTED! Detection #{detection_count}\n{'!'*48}") if lcd_ok: lcd.clear(); lcd.print("!! ALERT !!",0,2) lcd.print(f"Motion #{detection_count}",1,2) led_alert.on() alarm_start = time.time() while time.time()-alarm_start < ALARM_DURATION: buzzer.on(); led_status.on(); time.sleep(0.2) buzzer.off(); led_status.off(); time.sleep(0.2) led_alert.off() last_detection = time.time() print(f"Alarm stopped. Total detections: {detection_count}") if lcd_ok: lcd.clear(); lcd.print("SYSTEM ARMED",0,2) lcd.print(f"Detects: {detection_count}",1,2) def print_status(): s = "ARMED" if system_armed else "DISARMED" m = "DETECTED" if check_motion() else "None" print(f"\rStatus: {s} | Motion: {m} | Detections: {detection_count}", end="") # ── Main Loop ──────────────────────────────────────────────── def main(): global last_detection startup_sequence() led_alert.off(); led_armed.off(); buzzer.off(); led_status.off() last_btn_state = 0; last_btn_time = 0; status_time = 0 while True: try: now = time.time() # Button debounce + ARM/DISARM btn = button.value() if btn==1 and last_btn_state==0 and now-last_btn_time>0.3: toggle_system(); last_btn_time=now last_btn_state = btn # Motion detection (armed only) if system_armed: if check_motion() and now-last_detection>COOLDOWN_TIME: trigger_alarm() time.sleep(COOLDOWN_TIME) # Status LED slow blink at 0.5 Hz led_status.value(1 if int(now*2)%2==0 else 0) else: led_status.off() # Serial status every 2 s if now-status_time>2: print_status(); status_time=now time.sleep(0.1) except KeyboardInterrupt: print("\n\nSystem stopped.") led_alert.off(); led_armed.off(); buzzer.off(); led_status.off() if lcd_ok: lcd.clear(); lcd.print("System Stopped",0,0) break except Exception as e: print(f"\nError: {e}"); time.sleep(1) if __name__ == "__main__": main()
Code Architecture
| Function | Role | Triggers |
|---|---|---|
startup_sequence() | Tests LEDs, buzzer, shows boot message on LCD | Once at startup |
toggle_system() | Flips ARMED/DISARMED state, updates LED & LCD | Button press (debounced) |
check_motion() | Reads PIR GPIO — returns True if HIGH | Every 100 ms loop tick |
trigger_alarm() | 5s buzzer pulse, red LED, LCD ALERT, counter++ | PIR HIGH + cooldown passed |
print_status() | Serial monitor status line | Every 2 seconds |
main() | Main event loop — buttons, motion, LED blink, timing | Continuous (100 ms cycle) |
Loop timing: The main loop runs at 10 Hz (100 ms sleep). Button debounce = 300 ms. Alarm = 5 s (blocking). Cooldown = 3 s (enforced via time.time() delta). Status log = every 2 s.
What You'll Learn
time.time() deltas\r
Comments
Post a Comment