Real alarm systems don't scream the moment you open the door. They give you a few seconds to leave after arming, and a few seconds to disarm when you come home. In this capstone you build that behaviour yourself. An RFID card arms and disarms the system, a PIR sensor watches for movement, a tilt switch detects when a box, drawer or door is moved, and a buzzer is the siren. The secret is a programming pattern used in almost every machine around you: the state machine.
What you'll learn
- How to plan a program as a state machine
- How to read an RFID card and compare it with a stored card
- How to watch two sensors at the same time
- How to make LEDs blink and a buzzer beep without
delay()
What you need
- UNO R3 board and USB cable (UNO R3 Starter Kit)
- RC522 RFID module and S50 card (UNO R3 Starter Kit)
- PIR motion sensor HC-SR501 (UNO R3 Starter Kit)
- Green LED, red LED and 2 resistors, 220 Ω (UNO R3 Starter Kit)
- Tilt switch module (45-in-1 Sensor Kit)
- Active buzzer module (45-in-1 Sensor Kit)
- Breadboard and jumper wires (UNO R3 Starter Kit)
The five states
At any moment the alarm is in exactly one of five states. Each state has its own lights and sounds, and only a few things can move it to another state.
| State | What you see and hear | What ends it |
|---|---|---|
| Disarmed | Green LED on | Your card starts the exit delay |
| Exit delay (10 s) | Green LED blinks, one tick per second | Time runs out: armed. Your card: disarmed |
| Armed | Red LED blinks slowly | A sensor triggers: entry delay. Your card: disarmed |
| Entry delay (10 s) | Red LED blinks fast, fast ticks | Your card: disarmed. Time runs out: alarm |
| Alarm | Siren and fast red blinking | Your card: disarmed |
Wiring
The RC522 runs on 3.3 V. Connecting its power pin to 5V can damage it.

| From | To |
|---|---|
| RC522 SDA | UNO pin 10 |
| RC522 SCK | UNO pin 13 |
| RC522 MOSI | UNO pin 11 |
| RC522 MISO | UNO pin 12 |
| RC522 RST | UNO pin 9 |
| RC522 3.3V | UNO 3V3 |
| RC522 GND | GND |
| RC522 IRQ | Not connected |
| PIR OUT | UNO pin 2 |
| PIR VCC and GND | 5V and GND |
| Tilt switch module S | UNO pin 3 |
| Tilt switch module + and − | 5V and GND |
| Active buzzer module S | UNO pin 8 |
| Active buzzer module + and − | 5V and GND |
| UNO pin 5 | 220 Ω resistor, then the green LED's long leg |
| UNO pin 6 | 220 Ω resistor, then the red LED's long leg |
| Both LED short legs | GND |
Sensor modules from different makers can label and order their pins differently. Always follow the labels printed on your module.
Build it in stages
This is a bigger build than the earlier lessons, so don't wire everything at once. Add one part, test it, then add the next. When something stops working, you know it's the part you just added.
-
The card reader. Install the MFRC522 library (by GithubCommunity) in the Arduino IDE. Wire only the RC522 and the two LEDs, upload the sketch and open the Serial Monitor at 9600 baud. Hold your card on the reader. The monitor prints a line such as
Card: 0x3A, 0x7F, 0x12, 0xC4. -
Your card. Copy the four values into the
masterCardline and upload again. Now the card starts the exit delay, and the monitor shows the state changing. - The siren. Add the buzzer module. You now hear the ticks during the delays.
- The sensors. Add the PIR sensor and the tilt switch. Give the PIR sensor about a minute to settle after power-up. Tape the tilt switch to the thing you want to protect, such as a box lid.
- Test it. Arm the system, step out of the sensor's view and wait for the slow red blink. Walk in: fast ticks. Show your card within ten seconds, or the siren starts.
The sketch
#include <SPI.h>
#include <MFRC522.h>
const int SS_PIN = 10;
const int RST_PIN = 9;
const int PIR_PIN = 2;
const int TILT_PIN = 3;
const int GREEN_LED = 5;
const int RED_LED = 6;
const int BUZZER_PIN = 8;
// Replace with the four values of your own card (shown in the Serial Monitor)
byte masterCard[4] = {0x00, 0x00, 0x00, 0x00};
const unsigned long EXIT_DELAY = 10000; // time to leave after arming
const unsigned long ENTRY_DELAY = 10000; // time to show the card after a trigger
const unsigned long SIREN_TIME = 60000; // the siren stops after one minute
const byte DISARMED = 0, EXIT_WAIT = 1, ARMED = 2, ENTRY_WAIT = 3, ALARM = 4;
const char* const STATE_NAMES[] = {"Disarmed", "Exit delay", "Armed", "Entry delay", "ALARM"};
MFRC522 rfid(SS_PIN, RST_PIN);
byte state = DISARMED;
unsigned long stateStart = 0;
int tiltRest = HIGH;
void enter(byte newState) {
state = newState;
stateStart = millis();
Serial.print(F("State: "));
Serial.println(STATE_NAMES[newState]);
}
void beep(int times, int ms) {
for (int i = 0; i < times; i++) {
digitalWrite(BUZZER_PIN, HIGH);
delay(ms);
digitalWrite(BUZZER_PIN, LOW);
delay(ms);
}
}
// Returns 0 = no card, 1 = your card, 2 = an unknown card
int readCard() {
if (!rfid.PICC_IsNewCardPresent() || !rfid.PICC_ReadCardSerial()) return 0;
bool match = true;
Serial.print(F("Card: "));
for (byte i = 0; i < rfid.uid.size; i++) {
Serial.print(F("0x"));
if (rfid.uid.uidByte[i] < 0x10) Serial.print(F("0"));
Serial.print(rfid.uid.uidByte[i], HEX);
if (i < rfid.uid.size - 1) Serial.print(F(", "));
if (i < 4 && rfid.uid.uidByte[i] != masterCard[i]) match = false;
}
Serial.println();
rfid.PICC_HaltA();
return match ? 1 : 2;
}
bool sensorTriggered() {
bool motion = digitalRead(PIR_PIN) == HIGH;
bool moved = digitalRead(TILT_PIN) != tiltRest;
return motion || moved;
}
void lights(bool red, bool green) {
digitalWrite(RED_LED, red);
digitalWrite(GREEN_LED, green);
}
void setup() {
Serial.begin(9600);
SPI.begin();
rfid.PCD_Init();
pinMode(PIR_PIN, INPUT);
pinMode(TILT_PIN, INPUT_PULLUP);
pinMode(GREEN_LED, OUTPUT);
pinMode(RED_LED, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
enter(DISARMED);
}
void loop() {
unsigned long now = millis();
unsigned long inState = now - stateStart; // time spent in this state
bool slowBlink = (now / 500) % 2; // changes twice per second
bool fastBlink = (now / 125) % 2; // changes eight times per second
int card = readCard();
if (card == 2) beep(3, 80); // unknown card
if (card == 1 && state != DISARMED) { // your card always disarms
digitalWrite(BUZZER_PIN, LOW);
beep(2, 100);
enter(DISARMED);
return;
}
switch (state) {
case DISARMED:
lights(false, true);
digitalWrite(BUZZER_PIN, LOW);
if (card == 1) {
beep(1, 150);
enter(EXIT_WAIT);
}
break;
case EXIT_WAIT:
lights(false, slowBlink);
digitalWrite(BUZZER_PIN, now % 1000 < 60); // one short tick per second
if (inState >= EXIT_DELAY) {
tiltRest = digitalRead(TILT_PIN); // remember the resting position
enter(ARMED);
}
break;
case ARMED:
lights(slowBlink, false);
digitalWrite(BUZZER_PIN, LOW);
if (sensorTriggered()) enter(ENTRY_WAIT);
break;
case ENTRY_WAIT:
lights(fastBlink, false);
digitalWrite(BUZZER_PIN, now % 250 < 60); // four ticks per second
if (inState >= ENTRY_DELAY) enter(ALARM);
break;
case ALARM:
lights(fastBlink, false);
digitalWrite(BUZZER_PIN, inState < SIREN_TIME ? fastBlink : LOW);
break;
}
}
How it works
The variable state holds a number from 0 to 4, and the switch statement runs only the block for the current state. Each block does two jobs: it sets the lights and the buzzer for that state, and it checks the few things that can end it. All changes go through enter(), which stores the new state and the time it started, so inState always tells you how long the alarm has been in the current state.
Nothing in loop() waits. Blinking comes from the clock: (now / 500) % 2 is 0 for half a second, then 1 for half a second. That is why the card reader and both sensors are checked hundreds of times per second, even while the lights blink.
The tilt switch can be mounted in any position, so the sketch doesn't assume which reading means "moved". At the moment the system arms, it stores the switch's resting value in tiltRest, and any change from that value counts as a trigger. The siren stops after one minute so that it doesn't annoy the neighbours, but the red light keeps flashing to show that something happened.
Try this next
- Allow a second card by adding another array and checking both.
- Count how many times the alarm was triggered and print it when you disarm.
- Add a "panic" feature: an unknown card three times in a row starts the alarm.
- Swap the tilt switch for a reed switch from the Sensor Kit and a small magnet to make a real door sensor.
Troubleshooting
- No card line appears in the Serial Monitor: check the RC522 wiring, especially 3.3V, GND, SDA and RST. Hold the card flat on the reader.
-
The buzzer beeps three times for your own card: the values in
masterCarddon't match. Copy them again from the Serial Monitor. - The entry delay starts immediately after arming: the PIR sensor still sees you. Leave its view during the exit delay, and wait a minute after power-up before testing.
-
The buzzer is on when it should be off: some buzzer modules switch on with LOW. Swap
HIGHandLOWin the buzzer lines, or use the bare active buzzer from the UNO kit between pin 8 and GND. - The tilt switch triggers by itself: tape it down firmly. It contains a small rolling ball that reacts to vibration.