External Arduino Interrupts Explained

Often when interfacing with real world inputs for switches or analog inputs there is often the need to detect these immediately, instead of when the code next reaches that segment of the loop() code.


The best way to achieve this is to harness Interrupts, which allow code to be run when a change is detected on a defined pin.

They are so called as they will intterupt the thread of execution after the current instruction completes, and run their code, returning to the next instruction from where it left off when it has finished.


Interrupt Considerations

These routines

  • need to be extremely fast to execute, and it is often best to simply set a number of flags or states within the Interrupt Service Routine, then evaluate them when required in your normal thread code in loop().
  • only fire on RISING, FALLING and CHANGE events, so if any specific thresholds are required these have to be implemented manually
  • are restricted to specific pins on some hardware, depending on the architecture of the MCU (e.g ATMega328 only allows hardware interrupts on pins 2/3, ESP32 boards allow them on all I/O pins)
  • may introduce false positives with switches for example due to the "bouncing" seen when a contact is made or broken.  This can be mitigated with some debouncing software techniques, or use an R/C circuit to clean it up in hardware
  • can only set values to variables declared with VOLATILE, which ensures they arent optimized away, and can be used reliably in the ISR and in the main loop() code

Quick Videos

Example Software

The software example below will simply show the count of times it has fired, in the Serial Monitor from a button attached to pin 2.

NOTE - INPUT_PULLUP is used, with the switch going to GND to reduce the number of external parts needed. If your board does not support this add the 10K pull up resistor between the I/O pin and VCC.


Interrupt Button Example Setup

Debouncing with ISRs

As you can now see when running the example it can fire the ISR 10's or 100's of times depending on the physical switch itself, due to how the contacts are physically made.  If we were detecting a hall effect sensor for magnetism or and LDR as a light sensor, there is no need to debounce the input.

Often debouncing is done with a simple delay, and then checking the state again, which is less than ideal in this sceanrio with the interrupt firing, and works reliably for momentary presses.

If we record the last time the ISR fired, we can simulate the delay by ignoring all requests after it fires for e.g 2/10th second. 

To handle longer presses (as the bouncing occurrs when you release the switch), you would need to actually read the pin state in the ISR to check it is HIGH/LOW as you expect.


const int buttonPin = 2;
volatile long pressedCount = 0;
volatile long rawPressedCount = 0;
volatile long lastButtonPressMS = 0;

void setup()
	pinMode(buttonPin, INPUT_PULLUP); // Saves having an external resistor on our switch
	attachInterrupt(buttonPin, ISR, FALLING); // Use FALLING as we have the pin pulled HIGH with pull up resistor when not pressed

// Ensure you include the ICACHE_RAM_ATTR attribute to force the ISR code into memory on ESP32
	if ((millis() - lastButtonPressMS) > 200) { // Ignore all requests for 2/10th second after press 
		lastButtonPressMS = millis();

void loop()
	// Loop is free to run other code as the input handling is done via the ISR
	Serial.printf("Debounce Presses:%d\n", pressedCount);
	Serial.printf("RAW Presses:%d\n\n", rawPressedCount);
	delay(5000); // Only report data every 5s, however the ISR will fire even while we are delayed (or in other code)

Further Uses

Due to the speed of execution interrupts can also be used to track some of the faster inputs such as the rotation of a motor shaft with an optical encoder, or the sound made by tapping a microphone.


Other Types of Interrupt

Interrupts are also used to create Timer Interrupts, so the ISR code can fire on a very specific time interval without an external trigger.