Often in a project you want the ESP8266 to perform its normal program while continuously monitoring for some kind of event. One widely adopted solution for this is the use of interrupts.
Interrupts can be classified into two types.
Hardware interrupts – These occur in response to an external event. For example, a GPIO interrupt (when a key is pressed).
Software Interrupts – These occur in response to a software instruction. For example, a simple timer interrupt or a watchdog timer interrupt (when the timer times out).
ESP8266 GPIO Interrupt
You can configure the ESP8266 to generate an interrupt when a GPIO pin changes its logic level.
All GPIO pins in an ESP8266 board can be configured to act as interrupt request inputs, except GPIO16.
Attaching an Interrupt to a GPIO Pin
In the Arduino IDE, we use a function called attachInterrupt() to set an interrupt on a pin by pin basis. The syntax looks like below.
attachInterrupt(GPIOPin, ISR, Mode);
This function accepts three arguments:
GPIOPin – sets the GPIO pin as the interrupt pin, which tells ESP8266 which pin to monitor.
ISR – is the name of the function that will be called each time the interrupt occurs.
Mode – defines when the interrupt should be triggered. Five constants are predefined as valid values:
LOW | Triggers the interrupt whenever the pin is LOW |
HIGH | Triggers the interrupt whenever the pin is HIGH |
CHANGE | Triggers the interrupt whenever the pin changes value, from HIGH to LOW or LOW to HIGH |
FALLING | Triggers the interrupt when the pin goes from HIGH to LOW |
RISING | Triggers the interrupt when the pin goes from LOW to HIGH |
Interrupt Service Routine
The Interrupt Service Routine (ISR) is a function that is invoked every time an interrupt occurs on the GPIO pin.
Its syntax looks like below.
void ICACHE_RAM_ATTR ISR() {
Statements;
}
ISRs in ESP8266 are special kinds of functions that have some unique rules that most other functions do not have.
- An ISR cannot have any parameters, and they should not return anything.
- ISRs should be as short and fast as possible as they block normal program execution.
- They should have the ICACHE_RAM_ATTR attribute, according to the ESP8266 documentation.
What is ICACHE_RAM_ATTR?
When we flag a piece of code with the ICACHE_RAM_ATTR attribute, the compiled code is placed in the ESP8266’s Internal RAM (IRAM). Otherwise the code is kept in Flash. And Flash on ESP8266 is much slower than internal RAM.
If the code we want to run is an Interrupt Service Routine (ISR), we generally want to execute it as soon as possible. If we had to ‘wait’ for the ISR to load from Flash then things could go horribly wrong.
Hardware Hookup
Enough of the theory! Let’s look at a practical example.
Let’s connect a push button to GPIO#12 (D6) on the ESP8266. You do not need any pullup for this pin as we will be enabling internal pullup.
Example Code: Simple Interrupt
The following sketch demonstrates the use of interrupts and the correct way to write an interrupt service routine.
This program watches GPIO#12 (D6) for the FALLING edge. In other words, it looks for a voltage change going from logic HIGH to logic LOW that occurs when the button is pressed. When this happens the function isr
is called. The code within this function counts the number of times the button has been pressed.
struct Button {
const uint8_t PIN;
uint32_t numberKeyPresses;
bool pressed;
};
Button button1 = {D6, 0, false};
void ICACHE_RAM_ATTR isr() {
button1.numberKeyPresses++;
button1.pressed = true;
}
void setup() {
Serial.begin(115200);
pinMode(button1.PIN, INPUT_PULLUP);
attachInterrupt(button1.PIN, isr, FALLING);
}
void loop() {
if (button1.pressed) {
Serial.printf("Button has been pressed %u times\n", button1.numberKeyPresses);
button1.pressed = false;
}
}
Once you have uploaded the sketch, open the serial monitor at baud rate 115200. On pressing the button you will get the following output.
Code Explanation
At the beginning of the sketch we create a structure called Button
. This structure has three members – the pin number, the number of key presses, and the pressed state. FYI, a structure is a collection of variables of different types (but logically related to each other) under a single name.
struct Button {
const uint8_t PIN;
uint32_t numberKeyPresses;
bool pressed;
};
We then create an instance of the Button
structure and initialize the pin number to D6
, the number of key presses to 0
and the default pressed state to false
.
Button button1 = {D6, 0, false};
The following code is an interrupt service routine. As mentioned earlier, the ISR in ESP8266 must have the ICACHE_RAM_ATTR
attribute.
In the ISR we simply increment the KeyPresses counter by 1 and set the button pressed state to True.
void ICACHE_RAM_ATTR isr() {
button1.numberKeyPresses++;
button1.pressed = true;
}
In the setup section of the code, we first initialize the serial communication with the PC and then we enable the internal pullup for the D6 GPIO pin.
Next we tell ESP8266 to monitor the D6 pin and to call the interrupt service routine isr
when the pin goes from HIGH to LOW i.e. FALLING edge.
void setup() {
Serial.begin(115200);
pinMode(button1.PIN, INPUT_PULLUP);
attachInterrupt(button1.PIN, isr, FALLING);
}
In the loop section of the code, we simply check if the button has been pressed and then print the number of times the key has been pressed so far and set the button pressed state to false so that we can continue to receive interrupts.
void loop() {
if (button1.pressed) {
Serial.printf("Button has been pressed %u times\n", button1.numberKeyPresses);
button1.pressed = false;
}
}
Managing Switch Bounce
A common problem with interrupts is that they often get triggered multiple times for the same event. If you look at the serial output of the above example, you will notice that even if you press the button only once, the counter is incremented several times.
To find out why this happens, you have to take a look at the signal. If you monitor the voltage of the pin on the signal analyzer while you press the button, you will get a signal like this:
You may feel like contact is made immediately, but in fact the mechanical parts within the button come into contact several times before they settle into a particular state. This causes multiple interrupts.
It is purely a mechanical phenomenon known as a switch bounce, like dropping a ball – it bounces several times before finally landing on the ground.
The time for the signal to stabilize is very fast and seems almost instantaneous to us, but for an ESP8266 this is a huge period of time. It can execute multiple instructions in that time period.
The process of eliminating switch bounce is called debouncing. There are two ways to achieve this.
- Through hardware: by adding an appropriate RC filter to smooth the transition.
- Through software: by temporarily ignoring further interrupts for a short period of time after the first interrupt is triggered.
Example Code: Debouncing an Interrupt
Here the above sketch is rewritten to demonstrate how to debounce an interrupt programmatically. In this sketch we allow the ISR to be executed only once on each button press, instead of executing it multiple times.
Changes to the sketch are highlighted in green.
struct Button {
const uint8_t PIN;
uint32_t numberKeyPresses;
bool pressed;
};
Button button1 = {D6, 0, false};
//variables to keep track of the timing of recent interrupts
unsigned long button_time = 0;
unsigned long last_button_time = 0;
void ICACHE_RAM_ATTR isr() {
button_time = millis();
if (button_time - last_button_time > 250)
{
button1.numberKeyPresses++;
button1.pressed = true;
last_button_time = button_time;
}
}
void setup() {
Serial.begin(115200);
pinMode(button1.PIN, INPUT_PULLUP);
attachInterrupt(button1.PIN, isr, FALLING);
}
void loop() {
if (button1.pressed) {
Serial.printf("Button has been pressed %u times\n", button1.numberKeyPresses);
button1.pressed = false;
}
}
Let’s look at the serial output again as you press the button. Note that the ISR is called only once for each button press.
Code Explanation:
This fix works because each time the ISR is executed, it compares the current time returned by the millis()
function to the time the ISR was last called.
If it is within 250ms, ESP8266 ignores the interrupt and immediately goes back to what it was doing. If not, it executes the code within the if
statement incrementing the counter and updating the last_button_time
variable, so the function has a new value to compare against when it’s triggered in the future.