What you will learn
- Define alarm states and the events that move between them.
- Distinguish PIR warm-up from the user exit delay.
- Drive an active buzzer through an additional transistor output stage.
- Keep an alarm latched until manual disarming regardless of later PIR output.
- Process the button, sensor and different LED and sound patterns together.
Before you start
Courses 09 and 13: PIR sensing, a transistor-driven active buzzer, button debouncing and elapsed-time control with millis().
Reference setup
- Board in Arduino IDE
- ESP32 Dev Module
- Arduino-ESP32
- 3.3.12
- Serial Monitor
- 115200 baud
This is a reference profile, not an identification of the pictured board. Adapt the GPIO mapping and verify the circuit before using ESP32-C3/S2/S3 or a differently labelled board.
What we will build
We will build an alarm using a PIR sensor, button, LED and active buzzer. After settling, a button press starts a ten-second exit delay. The sensor can then trigger continuous light and repeated beeps. The alarm remains active until another press, even when the sensor output returns to LOW.
This educational state-machine model is not a certified security system.
Learning objectives
- Organise a program into five clearly defined states.
- Monitor a button, a sensor and several time intervals together.
- Latch an alarm event until it is manually cleared.
- Distinguish an exit delay from sensor settling time.
- Drive an active buzzer through a transistor.
Prerequisites
Complete courses 09 and 13 on PIR sensing and the active buzzer. You should understand INPUT_PULLUP, debouncing and millis() timing. Check the sensor and transistor output in those simpler examples first.
Equipment
| Component | Quantity | Specification and notes |
|---|---|---|
| Development board with a classic ESP32-WROOM-32 module | 1 | Match the reference GPIO labels; check physical header positions on the actual board. |
| USB data and power cable | 1 | Use the connector fitted to your board; the pictured kit lists Micro-USB. |
| Solderless breadboard | 1 | The kit lists 830 tie points. Check whether the power rails are split. |
| Active buzzer — 5 V, up to 20 mA reference | 1 | The photo lists an active buzzer but does not establish its voltage or current. For this circuit use a documented 5 V model with operating current no higher than 20 mA. |
| Additional PN2222A NPN transistor | 1Required extra — not in kit photo | ADDITIONAL, not listed in the kit. Identify B/C/E for the exact manufacturer; PN2222A and P2N2222A may have different lead orders. ADDITIONAL: the transistor is not listed in the kit photograph. |
| Additional 1N4148 protective diode | 1Required extra — not in kit photo | ADDITIONAL, not listed in the kit. The band marks cathode K. In the buzzer circuit the cathode faces +5 V. ADDITIONAL: the diode is not listed in the kit photograph. |
| 1 kΩ resistor | 1 | Base resistor; course 14 also uses one across the piezo. The kit quantity at this resistance is unconfirmed. |
| 10 kΩ resistor | 1 | R2 pulls Q1 base down to GND, keeping the base low while the GPIO is not driving. |
| HC-SR501 PIR motion sensor | 1 | Module supplied from the board USB 5 V rail; OUT is 3.3 V. Verify pin order, H/L jumper and output pulse duration. |
| Momentary pushbutton | 1 | Use two terminals connected only when pressed; two legs on the same side may already be joined. |
| Red LED | 1 | Anode A and cathode K; identify polarity on the actual part. |
| 220 Ω resistor | 1 | One current-limiting resistor per LED branch; use the kit’s 220 Ω resistors. |
| Jumper wires | 15 | Use male-to-male or female-to-male leads to suit the board headers. Approximate quantity; depends on the physical layout. |
The PN2222A transistor and 1N4148 protection diode are additional components not shown in the photographed kit. Use a verified active buzzer rated for 5 V and at most 20 mA. Check its markings and documentation; appearance does not establish these specifications.
The reference PIR is an HC-SR501 powered from 5 V, with a 3.3 V compatible output. Verify your actual module's labels and connections. We use a classic ESP32-WROOM-32 board powered through USB only.
States and transition rules
| State | LED and sound | Transition event |
|---|---|---|
WARMUP | Both outputs off. | After 60 s, enter DISARMED. |
DISARMED | Both outputs off. | A new press starts EXIT_DELAY. |
EXIT_DELAY | LED on for 250 ms, off for 250 ms. | After 10 s, enter ARMED; a press cancels. |
ARMED | LED on for 50 ms, off for 1950 ms. | PIR HIGH triggers ALARM; a press disarms. |
ALARM | LED continuously on; sound for 200 ms, silence for 800 ms. | A press returns to DISARMED. |
PIR activity during the exit delay does not sound the alarm. If its output is already HIGH when ten seconds expire, the alarm triggers immediately. The program checks the current level rather than waiting for another rising edge.
Wiring
Wiring diagram
PIR OUT now connects to GPIO33, the button to GPIO27, and the buzzer driver to GPIO26. PIR and active buzzer use the verified 5 V supply; PIR OUT is 3.3 V logic. D1 is the protective diode; D2 is the LED. Q1 and D1 are not listed in the kit.
| From | To | Connection |
|---|---|---|
ESP32.GPIO26 | R1.1 | R1, 1 kΩ, limits base current. |
R1.2 | Q1.B | Base of the additional PN2222A transistor. |
Q1.B | R2.1 | R2, 10 kΩ, pulls the base low while the GPIO is not driving. |
R2.2 | ESP32.GND | Other end of the pull-down resistor. |
Q1.E | ESP32.GND | Emitter to common ground; verify actual B/C/E pin order. |
ESP32.USB5V | BZ1.PLUS | Verified USB 5 V rail to the buzzer positive terminal. |
D1.K | ESP32.USB5V | Cathode of the protective 1N4148, marked with a band, to +5 V. |
BZ1.MINUS | Q1.C | Collector switches the active buzzer current. |
D1.A | BZ1.MINUS | Diode anode to buzzer negative and collector. |
ESP32.USB5V | U1.VCC | HC-SR501 supply from the verified USB 5 V rail. |
ESP32.GND | U1.GND | Common ground. |
U1.OUT | ESP32.GPIO33 | PIR 3.3 V output; course 15 uses GPIO33. |
ESP32.GPIO27 | SW1.CONTACT_A | Button input configured with INPUT_PULLUP. |
SW1.CONTACT_B | ESP32.GND | Other terminal of the selected contact pair, connected when pressed. |
ESP32.GPIO25 | R3.1 | Digital output to a series 220 Ω resistor. |
R3.2 | D2.A | Resistor to the LED anode. |
D2.K | ESP32.GND | LED cathode to ground. |
- Disconnect USB. Identify B, C and E from the exact transistor's datasheet; do not infer its lead order from a photograph.
- Connect a common GND for the ESP32, Q1 emitter, PIR and other return connections.
- Connect GPIO26 through R1, 1 kΩ, to Q1 base. Connect R2, 10 kΩ, between the base and GND.
- Connect Q1 collector to BZ1 minus, and the buzzer plus to the board's documented USB-derived 5 V connection.
- Place the 1N4148 across the buzzer: anode to its minus, cathode with the identifying band to its 5 V plus.
- Connect PIR VCC to the same USB 5 V rail, GND to common GND and OUT to GPIO33. Course 09 used GPIO27; this course reserves it for the button.
- Connect GPIO27 through the button to GND. With a four-leg button, use terminals that are connected by pressing it.
- Connect GPIO25 through R3, 220 Ω, to LED D2 anode; connect the cathode to GND.
Confirm that the board and USB source can supply all connected loads. Do not parallel another independent 5 V source with USB. Never connect the buzzer directly to a GPIO, and ensure PIR OUT remains compatible with 3.3 V logic.
How the output stage works
When GPIO26 is HIGH, base current flows through R1 and Q1 provides a current path through the buzzer to GND. A LOW turns the transistor off. R2 holds the base near GND while the board output is temporarily high-impedance. The protection diode is reverse-biased during normal operation; its orientation must match the schematic.
An active buzzer generates its own tone. Our program controls the on and off envelope, so changing the 200 ms interval changes the alarm rhythm rather than setting the tone frequency.
Arduino setup
In Arduino IDE 2.x, select esp32 by Espressif Systems 3.3.12, ESP32 Dev Module and the board's port. No extra libraries are required. Open pir_alarm/pir_alarm.ino and check GPIO33 for PIR, GPIO27 for the button, GPIO26 for the buzzer and GPIO25 for the LED. Use 115200 baud in Serial Monitor.
Complete Arduino program
#include <Arduino.h>
constexpr uint8_t BUZZER_PIN = 26;
constexpr uint8_t PIR_PIN = 33;
constexpr uint8_t LED_PIN = 25;
constexpr uint8_t BUTTON_PIN = 27;
constexpr uint32_t DEBOUNCE_MS = 30;
constexpr uint32_t WARMUP_MS = 60000;
constexpr uint32_t EXIT_DELAY_MS = 10000;
// Each event is accepted once after 30 ms of stable input.
// If held during reset, a stable release is required before the first press.
struct DebouncedButton {
bool rawHigh = true;
bool stableHigh = true;
bool armed = true;
bool pressed = false;
bool released = false;
uint32_t rawChangedAt = 0;
uint32_t pressStartedAt = 0;
void begin(uint32_t now) {
rawHigh = stableHigh = digitalRead(BUTTON_PIN) == HIGH;
armed = rawHigh;
rawChangedAt = now;
pressed = released = false;
}
void update(uint32_t now) {
pressed = released = false;
const bool readingHigh = digitalRead(BUTTON_PIN) == HIGH;
if (readingHigh != rawHigh) {
rawHigh = readingHigh;
rawChangedAt = now;
}
if (rawHigh != stableHigh && static_cast<uint32_t>(now - rawChangedAt) >= DEBOUNCE_MS) {
stableHigh = rawHigh;
if (stableHigh) {
armed = true;
released = true;
} else if (armed) {
pressed = true;
pressStartedAt = rawChangedAt;
}
}
// A short LOW pulse inhibited at the warmup boundary may never become
// stable LOW. Still recognize a complete stable-HIGH release afterward.
if (!armed && rawHigh && stableHigh && static_cast<uint32_t>(now - rawChangedAt) >= DEBOUNCE_MS) {
armed = true;
}
}
};
DebouncedButton button;
enum AlarmState { WARMUP, DISARMED, EXIT_DELAY, ARMED, ALARM };
AlarmState state = WARMUP;
uint32_t stateSince = 0;
void enterState(AlarmState next, uint32_t now);
void enterState(AlarmState next, uint32_t now) {
state = next;
stateSince = now;
Serial.print("State=");
switch (state) {
case WARMUP: Serial.println("WARMUP"); break;
case DISARMED: Serial.println("DISARMED"); break;
case EXIT_DELAY: Serial.println("EXIT_DELAY"); break;
case ARMED: Serial.println("ARMED"); break;
case ALARM: Serial.println("ALARM"); break;
}
}
void updateOutputs(uint32_t now) {
const uint32_t elapsed = static_cast<uint32_t>(now - stateSince);
bool ledOn = false;
bool buzzerOn = false;
if (state == EXIT_DELAY) ledOn = (elapsed % 500UL) < 250UL;
else if (state == ARMED) ledOn = (elapsed % 2000UL) < 50UL;
else if (state == ALARM) {
ledOn = true;
buzzerOn = (elapsed % 1000UL) < 200UL;
}
digitalWrite(LED_PIN, ledOn ? HIGH : LOW);
digitalWrite(BUZZER_PIN, buzzerOn ? HIGH : LOW);
}
void setup() {
Serial.begin(115200);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(LED_PIN, OUTPUT);
digitalWrite(BUZZER_PIN, LOW);
digitalWrite(LED_PIN, LOW);
pinMode(PIR_PIN, INPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP);
button.begin(millis());
enterState(WARMUP, millis());
}
void loop() {
const uint32_t now = millis();
button.update(now);
if (state == WARMUP) {
if (static_cast<uint32_t>(now - stateSince) >= WARMUP_MS) {
// Discard an unfinished press that began before warmup ended.
// A button still LOW (raw or stable) must be released before arming.
button.armed = button.rawHigh && button.stableHigh;
enterState(DISARMED, now);
}
} else if (button.pressed) {
if (state == DISARMED) enterState(EXIT_DELAY, now);
else enterState(DISARMED, now);
} else {
if (state == EXIT_DELAY && static_cast<uint32_t>(now - stateSince) >= EXIT_DELAY_MS) enterState(ARMED, now);
if (state == ARMED && digitalRead(PIR_PIN) == HIGH) enterState(ALARM, now);
}
updateOutputs(now);
}
Start with the original timing: 60 s for initial settling, 10 s to exit and a 200/800 ms sound cycle.
Understanding the program
state stores the current state and stateSince its start time. enterState() updates both and prints the new name to Serial Monitor. Elapsed time determines transitions. updateOutputs() derives the LED and sound patterns from it, without long delay() calls that would stop button polling.
The button must maintain a new level for at least 30 ms. A press event occurs once per accepted press. During WARMUP, the debouncer still updates, but press events do not arm the system: holding the button through the end of warm-up must not start arming. Release it and press again.
Entering ALARM remembers the trigger through the program's state. A later PIR LOW does not clear it. A button press takes priority when manually disarming. Subtracting timestamps of type uint32_t accommodates counter rollover for these short intervals.
Running the experiment
Click Verify, then Upload. Reset and wait 60 s without moving the module. This lesson setting does not guarantee every module has settled. Follow its documentation and choose a short output hold time to simplify observation.
Press and release the button. Leave the detection area while the LED flashes quickly. When it changes to brief, infrequent flashes, walk in front of the sensor. Wait for the sensor to stop reporting motion: the alarm should remain active. Clear it with a new button press.
Experiment and observations
| Test | Expected result |
|---|---|
| Hold the button during warm-up and through its completion. | The system remains disarmed until release and a new press. |
| Press during the exit delay. | The delay is cancelled and the LED turns off. |
Keep PIR HIGH at the end of the delay. | The alarm triggers immediately on arming. |
Allow PIR to return LOW after triggering. | Beeping and the continuous LED remain active. |
| Hold the button after disarming. | Holding alone does not rearm the system. |
Record actual results on your hardware; the table states expectations, not completed physical testing.
Troubleshooting
| Symptom | Check |
|---|---|
| Motion has no effect. | Check the current state and PIR OUT on GPIO33, rather than its previous GPIO27 connection. |
| The alarm starts exactly as the delay expires. | PIR is still HIGH; check its hold time and position. |
| The LED works but there is no sound. | Check the active buzzer type, BZ1 polarity, transistor B/C/E and supply. |
| Walking away does not stop the alarm. | Latching is intentional; press the button to clear it. |
| The board resets when the buzzer sounds. | Disconnect power and check load current, wiring and the USB supply. |
Challenge and worked solution
Set EXIT_DELAY_MS to 5000. In the sound condition inside updateOutputs(), change < 200UL to < 100UL while retaining % 1000UL: this gives 100 ms of sound and 900 ms of silence. Preserve warm-up. If a press is accepted 72 seconds after startup, when does monitoring begin? How many beep starts occur in five full seconds of alarm operation?
Monitoring begins at approximately 77 seconds, on the first loop iteration after the interval expires. The sound cycle still lasts one second, giving starts at 0, 1, 2, 3 and 4 seconds of alarm operation. The on-time fraction falls from 20% to 10%. If PIR is already HIGH, the alarm triggers without another waiting period.
Why does PIR LOW not stop the sound? The stored ALARM state controls sound, rather than directly following the sensor output. Why avoid a ten-second delay()? It would prevent regular processing of the cancellation button during that time.
