🔗Goal
Build a motion-activated alarm system using a PIR sensor, a buzzer, and an LED. When the system is armed and motion is detected, the LED turns on and the buzzer sounds a rapid alarm for 5 seconds. A push button lets you arm and disarm the system, with distinct audio feedback for each state.
Here is how the system works at a high level:
graph LR
A[PIR Sensor] -->|GPIO 13| B[ESP32]
F[Push Button] -->|GPIO 14| B
B -->|GPIO 25| C[LED]
B -->|GPIO 26| D[Buzzer]
B -->|Serial| E[Monitor]The PIR (Passive Infrared) sensor detects changes in infrared radiation caused by a warm body moving through its field of view. When triggered, it outputs a HIGH signal on its data pin. The ESP32 uses an interrupt to catch this signal instantly, without polling.
🔗Prerequisites
You will need the following components:
| Component | Qty | Notes | Buy |
|---|---|---|---|
| ESP32 dev board | 1 | AliExpress | Amazon.de .co.uk .com | |
| PIR sensor (HC-SR501) | 1 | Has adjustable sensitivity and delay potentiometers | AliExpress | Amazon.de .co.uk .com |
| Active buzzer | 1 | 3.3V or 5V compatible | AliExpress | Amazon.de .co.uk .com |
| LED (red) | 1 | Any standard 5mm LED | AliExpress | Amazon.de .co.uk .com |
| 220 ohm resistor | 1 | Current-limiting resistor for the LED | AliExpress | Amazon.de .co.uk .com |
| Push button (tactile) | 1 | Momentary tactile switch | AliExpress | Amazon.de .co.uk .com |
| 10k ohm resistor | 1 | Pull-down resistor for the push button | AliExpress | Amazon.de .co.uk .com |
| Breadboard | 1 | AliExpress | Amazon.de .co.uk .com | |
| Jumper wires | ~12 | Male-to-male and male-to-female | AliExpress | Amazon.de .co.uk .com |
Links marked Amazon/AliExpress are affiliate links. We may earn a small commission at no extra cost to you.
The current through the LED is limited by the resistor. For a red LED with a forward voltage of $V_f = 2.0\,\text{V}$:
$$I = \frac{V_{supply} - V_f}{R} = \frac{3.3 - 2.0}{220} \approx 5.9\,\text{mA}$$
This is well within the ESP32 GPIO limit of $12\,\text{mA}$ per pin.
See our PIR sensor guide for more on how PIR sensors work, including adjusting the sensitivity and trigger delay.
See our buzzer guide for the difference between active and passive buzzers.
🔗Tutorial
🔗Step 1: Wiring
Connect all the components to the ESP32 as shown in the table below:
| Component | Pin | ESP32 Pin | Notes |
|---|---|---|---|
| PIR sensor | VCC | 5V (VIN) | HC-SR501 requires 5V power |
| PIR sensor | GND | GND | |
| PIR sensor | OUT | GPIO 13 | HIGH when motion detected |
| LED (anode, long leg) | -- | GPIO 25 | Through 220 ohm resistor |
| LED (cathode, short leg) | -- | GND | |
| Active buzzer (+) | -- | GPIO 26 | |
| Active buzzer (-) | -- | GND | |
| Push button | Side A | GPIO 14 | |
| Push button | Side B | 3.3V | |
| 10k ohm resistor | GPIO 14 to GND | Pull-down | Keeps pin LOW when button is not pressed |
The HC-SR501 PIR sensor needs 5V to operate, but its output signal is 3.3V, which is safe for ESP32 GPIO pins. Use the VIN (5V) pin on the ESP32 to power it.
Pin labels and GPIO numbers vary between ESP32 boards. Always check your board's pinout diagram and datasheet.
🔗Step 2: Adjust the PIR sensor
The HC-SR501 has two small potentiometers on the back:
| Potentiometer | Function | Recommended setting |
|---|---|---|
| Sensitivity | Detection range (3m to 7m) | Turn clockwise for maximum range |
| Time delay | How long OUT stays HIGH after detection (3s to 300s) | Turn fully counter-clockwise for minimum (about 3 seconds) |
There is also a jumper that selects the trigger mode:
- H (repeatable trigger): OUT stays HIGH as long as motion continues. Use this mode.
- L (single trigger): OUT goes HIGH once, then LOW, even if motion continues.
🔗Step 3: Upload the code
The system uses an interrupt on the PIR pin to detect motion instantly, and another interrupt on the button pin to toggle the armed/disarmed state.
#define PIR_PIN 13
#define LED_PIN 25
#define BUZZER_PIN 26
#define BUTTON_PIN 14
// ----- State -----
volatile bool motionDetected = false;
volatile bool armed = false;
volatile unsigned long lastButtonPress = 0;
// ----- ISR: PIR sensor -----
void IRAM_ATTR onMotion() {
if (armed) {
motionDetected = true;
}
}
// ----- ISR: Button press -----
void IRAM_ATTR onButtonPress() {
unsigned long now = millis();
// Debounce: ignore presses within 300ms of the last one
if (now - lastButtonPress > 300) {
lastButtonPress = now;
armed = !armed;
}
}
// ----- Buzzer patterns -----
void beepShort(int count) {
for (int i = 0; i < count; i++) {
digitalWrite(BUZZER_PIN, HIGH);
delay(100);
digitalWrite(BUZZER_PIN, LOW);
delay(100);
}
}
void beepLong() {
digitalWrite(BUZZER_PIN, HIGH);
delay(500);
digitalWrite(BUZZER_PIN, LOW);
}
void alarmSequence() {
unsigned long start = millis();
// Rapid beeping for 5 seconds
while (millis() - start < 5000) {
digitalWrite(BUZZER_PIN, HIGH);
digitalWrite(LED_PIN, HIGH);
delay(100);
digitalWrite(BUZZER_PIN, LOW);
digitalWrite(LED_PIN, LOW);
delay(100);
}
// Ensure both are off after alarm ends
digitalWrite(BUZZER_PIN, LOW);
digitalWrite(LED_PIN, LOW);
}
void setup() {
Serial.begin(115200);
pinMode(PIR_PIN, INPUT);
pinMode(LED_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(BUTTON_PIN, INPUT); // External pull-down resistor
// Attach interrupts
attachInterrupt(digitalPinToInterrupt(PIR_PIN), onMotion, RISING);
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onButtonPress, RISING);
// Start disarmed
armed = false;
digitalWrite(LED_PIN, LOW);
digitalWrite(BUZZER_PIN, LOW);
Serial.println("Motion Alarm System ready.");
Serial.println("Press the button to arm/disarm.");
Serial.println("Status: DISARMED");
// Give the PIR sensor time to stabilize (20-60 seconds)
Serial.println("Waiting for PIR sensor to stabilize...");
delay(2000); // Minimum warm-up; HC-SR501 may take up to 60 seconds for full accuracy
Serial.println("PIR sensor ready.");
}
// Track previous armed state to detect changes
bool previousArmed = false;
void loop() {
// Handle arm/disarm state change
if (armed != previousArmed) {
previousArmed = armed;
if (armed) {
Serial.println("Status: ARMED");
beepShort(3); // 3 short beeps = armed
} else {
Serial.println("Status: DISARMED");
beepLong(); // 1 long beep = disarmed
motionDetected = false; // Clear any pending detection
digitalWrite(LED_PIN, LOW);
}
}
// Handle motion detection
if (motionDetected) {
motionDetected = false;
unsigned long timestamp = millis() / 1000;
Serial.print("[");
Serial.print(timestamp);
Serial.println("s] MOTION DETECTED! Alarm triggered.");
alarmSequence();
}
delay(50); // Small delay to reduce CPU usage
}🔗Step 4: Test the system
- Upload the sketch and open the Serial Monitor at 115200 baud
- Wait a few seconds for the PIR sensor to stabilize — the Serial Monitor will tell you when it is ready
- Press the push button — you should hear 3 short beeps and see "Status: ARMED" in the Serial Monitor
- Walk in front of the PIR sensor — the LED should flash and the buzzer should beep rapidly for 5 seconds
- Press the button again to disarm — you should hear 1 long beep
The state machine for the system:
stateDiagram-v2
[*] --> Disarmed
Disarmed --> Armed : Button press (3 beeps)
Armed --> Disarmed : Button press (1 long beep)
Armed --> Alarm : Motion detected
Alarm --> Armed : After 5 seconds🔗Step 5: Fine-tune the PIR sensor
If the alarm triggers too easily (false positives) or not at all:
- False triggers: Turn the sensitivity potentiometer counter-clockwise to reduce range. Avoid pointing the sensor at windows (sunlight and moving curtains cause false triggers)
- No detection: Turn the sensitivity clockwise. Make sure the jumper is set to H (repeatable trigger)
- Detection lingers too long: Turn the time delay potentiometer counter-clockwise for shorter output pulse duration
🔗Common Issues and Solutions
| Problem | Cause | Fix |
|---|---|---|
| PIR triggers constantly | Sensor pointed at heat source, window, or air vent | Reposition the sensor. Avoid direct sunlight and moving heat sources |
| PIR never triggers | Sensor not warmed up, or sensitivity too low | Wait 60 seconds after power-on. Turn sensitivity potentiometer clockwise |
| Button does not toggle arm/disarm | Wiring issue or missing pull-down resistor | Ensure the 10k ohm pull-down resistor connects GPIO 14 to GND. Without it, the pin floats |
| Buzzer is quiet or silent | Wrong buzzer type or insufficient voltage | Confirm you have an active buzzer (not passive). Check that it is rated for 3.3V or 5V |
| LED does not light up | LED inserted backwards | The longer leg (anode) goes to GPIO 25 through the resistor. The shorter leg (cathode) goes to GND |
| Alarm triggers immediately on arming | PIR output is still HIGH from warm-up | Add a longer delay after arming, or clear the interrupt flag before arming |
| Serial Monitor shows garbled text | Wrong baud rate | Set the Serial Monitor to 115200 baud |