Using ESP32 with PIR Motion Sensor (Interrupts and Timers)

This tutorial shows how to use ESP32 with PIR motion sensor using interrupts and timers. In this example, the ESP32 starts a timer and turns on an LED for a specified number of seconds. The LED is automatically turned off as the timer counts down and finishes.

We'll also explore timers and interrupts, two important ideas, using this example.

The ESP32 add-on should be installed in your Arduino IDE before proceeding with this tutorial. If you haven't previously, use one of the following tutorials to install the ESP32 on the Arduino IDE:

Parts List

You will need the following items to proceed with this project:

You can use the preceding links to find all the parts for your projects at the best price!

Introducing Interrupts

To trigger an event with a PIR motion sensor, you use interrupts. Interrupts are useful for making things happen automatically in microcontroller programs and can help solve timing problems.

With interrupts, you don’t need to constantly check the current value of a pin. With interrupts, when a change is detected, an event is triggered (a function is called).

To set an interrupt in the Arduino IDE, you use the attachInterrupt() function, which accepts as arguments: the GPIO pin, the name of the function to be executed, and the mode:

attachInterrupt(digitalPinToInterrupt(GPIO), function, mode);

GPIO Interrupt

A GPIO number is the first argument. To set the real GPIO as an interrupt pin, you often need to use digitalPinToInterrupt(GPIO). For instance, use the following to use GPIO 27 as an interrupt:

digitalPinToInterrupt(27)

All of the pins highlighted in a red rectangle in the following diagram may be set up as interrupt pins on an ESP32 board. In this example, we'll connect the PIR Motion sensor to GPIO 27 as an interrupt.

pins used as interrupts

Function to be triggered

The function name that will be called each time the interrupt is triggered is the second argument of the attachInterrupt() function.

Mode

The third argument is the mode. There are 5 different modes:

  • LOW: to trigger the interrupt whenever the pin is LOW.
  • HIGH: to trigger the interrupt whenever the pin is HIGH.
  • CHANGE: to trigger the interrupt whenever the pin changes value, for example from HIGH to LOW or LOW to HIGH.
  • FALLING: for when the pin goes from HIGH to LOW.
  • RISING: to trigger when the pin goes from LOW to HIGH.

Because the GPIO connected to the PIR motion sensor goes from LOW to HIGH when motion is detected, we will be using the RISING mode in this example.

Introducing Timers

We'll also incorporate timers in this example. After motion is detected, we want the LED to stay red for a predetermined number of seconds. We should use a timer rather than the delay() function, which blocks your time and prevents you from doing anything else for a second.

alarm clock

The delay() function

The delay() function is widely used; therefore, you should be acquainted with it. It's pretty straightforward to use this function. As an argument, it takes a single integer number. Before going on to the next line of code, this number represents the time in milliseconds the program must wait.

delay(time in milliseconds)

When you do delay(1000) your program stops on that line for 1 second.

A blocking function is delay(). preventing a program from doing anything else until a particular function event is finished. You are unable to use delay() if you need multiple tasks to occur at the same time.

For most projects you should avoid using delays and use timers instead.

The millis() function

Using a function called millis() you can return the number of milliseconds that have passed since the program first started.

millis()

Why is that function useful, exactly? You can easily verify how much time has passed by using basic math without blocking your code.

Blinking an LED with millis()

The following snippet of code shows how to create a blinking LED project using the millis() function. It turns on an LED for 1000 milliseconds before turning it off.

/*********
  LEDEdit PRO
  Complete project details at https://lededitpro.com  
*********/

// constants won't change. Used here to set a pin number :
const int ledPin =  26;      // the number of the LED pin

// Variables will change :
int ledState = LOW;             // ledState used to set the LED

// Generally, you should use "unsigned long" for variables that hold time
// The value will quickly become too large for an int to store
unsigned long previousMillis = 0;        // will store last time LED was updated

// constants won't change :
const long interval = 1000;           // interval at which to blink (milliseconds)

void setup() {
  // set the digital pin as output:
  pinMode(ledPin, OUTPUT);
}

void loop() {
  // here is where you'd put code that needs to be running all the time.

  // check to see if it's time to blink the LED; that is, if the
  // difference between the current time and last time you blinked
  // the LED is bigger than the interval at which you want to
  // blink the LED.
  unsigned long currentMillis = millis();

  if (currentMillis - previousMillis >= interval) {
    // save the last time you blinked the LED
    previousMillis = currentMillis;

    // if the LED is off turn it on and vice-versa:
    if (ledState == LOW) {
      ledState = HIGH;
    } else {
      ledState = LOW;
    }

    // set the LED with the ledState of the variable:
    digitalWrite(ledPin, ledState);
  }
}

Let’s take a closer look at this blink sketch that works without a delay() function (it uses the millis() function instead).

In essence, this code subtracts the current time (currentMillis) from the time that was previously recorded (previousMillis). The program updates the previousMillis variable to the current time and either turn the LED on or off if the remainder is greater than the interval, in this instance 1000 milliseconds.

if (currentMillis - previousMillis >= interval) {
  // save the last time you blinked the LED
  previousMillis = currentMillis;
  (...)

Any code located outside of that first if statement should work properly since this snippet is non-blocking.

You should now be able to understand that you may add more tasks to your loop() function, and your task will still blink the LED once per second.

To test it and see how it works, you may upload this code to your ESP32 and put together the following schematic diagram, You can also modify the second number of milliseconds.

blink without delay bb

Note: If you’ve experienced any issues uploading code to your ESP32, take a look at the ESP32 Troubleshooting Guide.

ESP32 with PIR Motion Sensor

Let's continue with the project now that you have a basic understanding of interrupts and timers.

Schematic

We'll be using an LED and a resistor to create an easy-to-assemble circuit. GPIO 26 is where the LED is connected. The Mini AM312 PIR Motion Sensor, which runs on 3.3 volts, is what we'll be using. On GPIO 27, it will be connected. Simply follow to the following schematic diagram:

pir esp32 interrupts

Important: The Mini AM312 PIR Motion Sensor used in this project operates at 3.3 volts. However, if you’re using another PIR motion sensor like the HC-SR501, it operates at 5 volts. You can either modify it to operate at 3.3 volts or simply power it using the Vin pin.

The following figure shows the AM312 PIR motion sensor pinout.

mini pir pinout

Uploading the Code

Copy the code provided into your Arduino IDE after wiring the circuit as shown in the schematic diagram.

You may upload the code exactly as is or modify the number of seconds for which the LED is lit once motion is detected. To get the desired number of seconds, just change the timeSeconds variable.

/*********
  LEDEdit PRO
  Complete project details at https://lededitpro.com  
*********/

#define timeSeconds 10

// Set GPIOs for LED and PIR Motion Sensor
const int led = 26;
const int motionSensor = 27;

// Timer: Auxiliary variables
unsigned long now = millis();
unsigned long lastTrigger = 0;
boolean startTimer = false;
boolean motion = false;

// Checks if motion was detected, sets LED HIGH and starts a timer
void IRAM_ATTR detectsMovement() {
  digitalWrite(led, HIGH);
  startTimer = true;
  lastTrigger = millis();
}

void setup() {
  // Serial port for debugging purposes
  Serial.begin(115200);
  
  // PIR Motion Sensor mode INPUT_PULLUP
  pinMode(motionSensor, INPUT_PULLUP);
  // Set motionSensor pin as interrupt, assign interrupt function and set RISING mode
  attachInterrupt(digitalPinToInterrupt(motionSensor), detectsMovement, RISING);

  // Set LED to LOW
  pinMode(led, OUTPUT);
  digitalWrite(led, LOW);
}

void loop() {
  // Current time
  now = millis();
  if((digitalRead(led) == HIGH) && (motion == false)) {
    Serial.println("MOTION DETECTED!!!");
    motion = true;
  }
  // Turn off the LED after the number of seconds defined in the timeSeconds variable
  if(startTimer && (now - lastTrigger > (timeSeconds*1000))) {
    Serial.println("Motion stopped...");
    digitalWrite(led, LOW);
    startTimer = false;
    motion = false;
  }
}

How the Code Works

Take a look at the source code. Assigning two GPIO pins to the motionSensor and led variables is the first step.

// Set GPIOs for LED and PIR Motion Sensor
const int led = 26;
const int motionSensor = 27;

Create variables that allow you to set a timer to turn off the LED when motion is detected.

// Timer: Auxiliar variables
long now = millis();
long lastTrigger = 0;
boolean startTimer = false;

The variable now holds the current time. The lastTrigger variable holds the time when the PIR sensor detects motion. When motion is detected, the startTimer boolean variable starts the timer.

setup()

In the setup(), start by initializing the Serial port at 115200 baud rate.

Serial.begin(115200);

Set the PIR Motion sensor as an INPUT PULLUP.

pinMode(motionSensor, INPUT_PULLUP);

To set the PIR sensor pin as an interrupt, use the attachInterrupt() function as described earlier.

attachInterrupt(digitalPinToInterrupt(motionSensor), detectsMovement, RISING);

The pin that will detect motion is GPIO 27, and it will call the function detectsMovement() on RISING mode.

The LED is an OUTPUT whose state starts at LOW.

pinMode(led, OUTPUT);
digitalWrite(led, LOW);

loop()

The loop() function is constantly running over and over again. In every loop, the now variable is updated with the current time.

now = millis();

Nothing else is done in the loop().

But, when motion is detected, the detectsMovement() function is called because we’ve set an interrupt previously on the setup().

The detectsMovement() function prints a message in the Serial Monitor, turns the LED on, sets the startTimer boolean variable to true and updates the lastTrigger variable with the current time.

void IRAM_ATTR detectsMovement() {
  Serial.println("MOTION DETECTED!!!");
  digitalWrite(led, HIGH);
  startTimer = true;
  lastTrigger = millis();
}

Note: IRAM_ATTR is used to run the interrupt code in RAM; otherwise, the code is stored in Flash, which is slower.

After this step, the code goes back to the loop().

This time, the startTimer variable is true. So, when the time defined in seconds has passed (since motion was detected), the following if statement will be true:

if(startTimer && (now - lastTrigger > (timeSeconds*1000))) {
  Serial.println("Motion stopped...");
  digitalWrite(led, LOW);
  startTimer = false;
}

The “Motion stopped…” message will be printed in the Serial Monitor, the LED is turned off, and the startTimer variable is set to false.

Demonstration

Upload the code to your ESP32 board. Make sure you have the right board and COM port selected.

Open the Serial Monitor at a baud rate of 115200.

serial monitor

Move your hand in front of the PIR sensor. In the Serial Monitor, a message saying “MOTION DETECTED!!!” should be printed, and the LED should turn on. The LED should turn off after 10 seconds.

pir ESP32 demo

Conclusion

To wrap things up, interrupts are used to detect a change in the GPIO state without continually reading the current GPIO value. When a change is recognized via interrupts, a function is activated. You've also learned how to set a simple timer, which enables you to check whether a predetermined number of seconds have elapsed without having to stop your code.

If you like ESP32, you may also like:

We hope you find this tutorial useful. Thanks for reading.

Oh hi there It’s nice to meet you.

Sign up to receive awesome content in your inbox, every month.

We don’t spam! Read our privacy policy for more info.

Leave a Reply

Your email address will not be published. Required fields are marked *

Developing IoT Projects with ESP32

Automate your home or business with inexpensive Wi-Fi devices