Using the Arduino IDE, you will learn how to use interrupts and timers with the ESP8266 NodeMCU. Interrupts allow you to detect changes in the GPIO state without having to check the current value all of the time. When a change is recognised, an event is triggered via interrupts (a function is called).

Introducing ESP8266 Interrupts

Interrupts are useful in microcontroller applications for making things happen automatically and for resolving timing issues.

You don’t have to keep checking the current pin value with interrupts. An event is triggered — a function is called – when a change is noticed. The interrupt service procedure is the name of this function (ISR).

When an interrupt occurs, the CPU interrupts the main programme to do a task before returning to the main programme, as indicated in the diagram below.

Introducing to ESP8266 NodeMCU Interrupts: how it works

This is especially useful to trigger an action whenever motion is detected or whenever a pushbutton is pressed without the need to constantly check its state.

attachInterrupt() Function

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

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

GPIO interrupt pin

The first argument is a GPIO interrupt. You should use digitalPinToInterrupt(GPIO) to set the actual GPIO as an interrupt pin. For example, if you want to use GPIO 14 as an interrupt, use:

digitalPinToInterrupt(14)

The ESP8266 supports interrupts in any GPIO, except GPIO16.

ISR

The name of the function that will be called every time the interrupt is caused – the interrupt service routine – is the second argument of the attachInterrupt() function (ISR).

The ISR function should be as minimal as feasible so that the processor can swiftly return to the main program’s execution.

The best way is to use a global variable to notify the main code that an interrupt has occurred, then verify and clear that flag within the loop() and execute instructions.

ISRs need to have ICACHE_RAM_ATTR before the function definition to run the interrupt code in RAM.

Interrupt modes

The third argument is the mode and there are 3 different modes:

  • 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.

For our example, will be using the RISING mode, because when the PIR motion sensor detects motion, the GPIO it is connected to goes from LOW to HIGH.

Introducing ESP8266 Timers

Introducing to ESP8266 NodeMCU Timers: how it works

We’ll use timers in this tutorial. After motion is detected, we want the LED to stay on for a predetermined amount of seconds. We’ll use a timer instead of the delay() function, which pauses your code and prevents you from doing anything else for a set number of seconds.

delay() vs millis()

As an argument, the delay() method takes a single int integer. This value indicates how long the programme must wait in milliseconds before going on to the next line of code.

delay(time in milliseconds);

When you call delay(1000), your programme will pause for one second on that line. A blocking function is delay(). Blocking functions prevent a programme from performing any other tasks until the work at hand is accomplished. You can’t utilise delay if you need numerous tasks to happen at the same time . You should avoid utilising delays in most projects and instead utilise timers.

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? Because by using some math, you can easily verify how much time has passed without blocking your code.

Blinking an LED using millis() (without delay)

If you’re not familiar with millis() function, we recommend reading this section. If you’re already familiar with timers, you can skip to the PIR motion sensor project.

The following snippet of code shows how you can use the millis() function to create a blink project. It turns an LED on for 1000 milliseconds, and then turns it off.

Code

// 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);
  }
}
#define timeSeconds 10

// Set GPIOs for LED and PIR Motion Sensor
const int led = 12;
const int motionSensor = 14;

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

// Checks if motion was detected, sets LED HIGH and starts a timer
ICACHE_RAM_ATTR void detectsMovement() {
  Serial.println("MOTION DETECTED!!!");
  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();
  // 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;
  }
}

How does the code works

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

Basically, this code subtracts the previous recorded time (previousMillis) from the current time (currentMillis). If the remainder is greater than the interval (in this case, 1000 milliseconds), the program updates the previousMillis variable to the current time, and either turns the LED on or off.

if (currentMillis – previousMillis >= interval) {

  // save the last time you blinked the LED

  previousMillis = currentMillis;

  (…)

Because this snippet is non-blocking, any code that’s located outside of that first if statement should work normally.

You should now be able to understand that you can add other tasks to your loop() function and your code will still be blinking the LED every one second.

You can upload this code to your ESP8266 to test it. The on-board LED should be blinking every second.

ESP8266 NodeMCU with PIR Motion Sensor

Parts Required

Here’s a list of the parts required to complete this tutorial:

  • ESP8266 
  • PIR motion sensor (AM312)
  • 5mm LED
  • 330 Ohm resistor
  • Breadboard
  • Jumper wires

Schematic Diagram

Assemble the PIR motion sensor and an LED to your ESP8266. We’ll connect the LED to GPIO 12 (D6) and the PIR motion sensor data pin to GPIO 14 (D5).

ESP8266 NodeMCU Interrupts and Timers with PIR Motion Sensor Schematic Circuit Diagram

Code

After wiring the circuit as shown in the schematic diagram, copy the code provided to your Arduino IDE.

You can upload the code as it is, or you can modify the number of seconds the LED is lit after detecting motion. Simply change the timeSeconds variable with the number of seconds you want.

// 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);
  }
}
#define timeSeconds 10

// Set GPIOs for LED and PIR Motion Sensor
const int led = 12;
const int motionSensor = 14;

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

// Checks if motion was detected, sets LED HIGH and starts a timer
ICACHE_RAM_ATTR void detectsMovement() {
  Serial.println("MOTION DETECTED!!!");
  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();
  // 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;
  }
}

How the Code Works

Let’s take a look at the code.

Start by assigning two GPIO pins to the led and motionSensor variables.

const int led = 12;

const int motionSensor = 14;

Then, create variables that will allow you set a timer to turn the LED off after motion is detected.

unsigned long now = millis();

unsigned long lastTrigger = 0;

boolean startTimer = false;

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

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 14 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 in 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.

ICACHE_RAM_ATTR void detectsMovement() {

  Serial.println(“MOTION DETECTED!!!”);

  digitalWrite(led, HIGH);

  startTimer = true;

  lastTrigger = millis();

}

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 ESP8266. Make sure you have the right board and COM port selected.

Open the Serial Monitor at a baud rate of 115200.

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

Conclusion

To sum up, interrupts are useful to detect a change in a GPIO state and instantly trigger a function. You’ve also learned that you should use timers to write non-blocking code.

author avatar
Aravind S S