Interrupt
Anand RamaswamiShare
Welcome Back, Maker!
So far, the Arduino has been checking things one step at a time like reading buttons, printing to the Serial Monitor, controlling LEDs ,all in a neat sequence inside loop().
But what happens when something urgent occurs and you can't wait for the loop to finish?
That's where Interrupts come in.
What Is an Interrupt?
An interrupt is a signal that immediately stops whatever the Arduino is currently doing and runs a special function instead.
Think of it like this:
You are reading a book. Suddenly your phone rings. You stop reading, answer the call, and then go back to where you left off.
The phone ring is the interrupt. Your book is the main program.
Without interrupts, the Arduino would have to keep checking "did the button get pressed?" over and over again inside loop(). This wastes time and can cause the Arduino to miss fast events entirely.
With interrupts, the Arduino doesn't need to keep checking. The moment something happens, it reacts instantly.
Why Do We Need Interrupts?
Consider this situation:
Your Arduino is running a motor and also needs to detect a button press to stop it immediately.
If you use digitalRead() inside loop(), there's a chance the button press happens while the Arduino is busy doing something else and it gets missed.
With an interrupt, no matter what the Arduino is doing, the button press is always detected immediately.
Interrupts are essential for:
- Emergency stop buttons
- Counting fast pulses from sensors
- Reacting to events in real time
- Keeping programs responsive
so, if interrupts are what make the Arduino react immediately, we need a command that tells the board which pin to watch and what to do when something happens. That command is attachInterrupt().
The Command: attachInterrupt()
attachInterrupt() tells the Arduino to watch a specific pin and call a function the moment something changes on it.
Syntax:
|
attachInterrupt(digitalPinToInterrupt(pin), ISR, mode); |
Parameters:
- digitalPinToInterrupt(pin) — converts the pin number to the correct interrupt number. Always use this instead of writing the interrupt number directly.
- ISR — the name of the function to run when the interrupt fires. This is called the Interrupt Service Routine.
- mode — what kind of change triggers the interrupt:
|
Mode |
Triggers When |
|
RISING |
Signal goes from LOW to HIGH |
|
FALLING |
Signal goes from HIGH to LOW |
|
CHANGE |
Signal changes in either direction |
|
LOW |
Pin stays at LOW (fires repeatedly) |
For buttons with INPUT_PULLUP, pressing the button causes the pin to go from HIGH to LOW so we use FALLING.
This introduces another important part of interrupts: the special function that runs when the interrupt happens.
The ISR — Interrupt Service Routine
The ISR is a special function that runs when the interrupt fires.
Rules for writing an ISR:
- Keep it as short and fast as possible
- Do NOT use delay() inside an ISR
- Do NOT use Serial.print() inside an ISR
- Variables shared between the ISR and loop() must be declared as volatile
Now that we understand the basic idea, let us look at some projects and see interrupts working in real code.
Project 1: Detect Button Press Using Interrupt
In this project, we will use an interrupt to detect a button press on Pin 2 and print a message on the Serial Monitor instantly, no matter what else the program is doing.
Code:
|
volatile bool buttonPressed = false; // Shared variable void buttonISR() { // ISR — runs when button is pressed buttonPressed = true; } void setup() { pinMode(2, INPUT); // Button on pin 2 Serial.begin(9600); attachInterrupt(digitalPinToInterrupt(2), buttonISR, FALLING); // Attach interrupt } void loop() { if (buttonPressed) { // Check if interrupt fired Serial.println("Button Pressed via Interrupt!"); buttonPressed = false; // Reset the flag } // Imagine other tasks running here — the interrupt still fires instantly delay(500); } |
Before we go further, there is one keyword here that is very important to understand.
What is volatile?
volatile tells the compiler that this variable can change at any time — even outside the normal program flow. Without it, the compiler might optimise the variable away and your code won't work correctly.
|
volatile bool buttonPressed = false; |
Explanation:
- volatile bool buttonPressed = false — a flag variable shared between the ISR and loop(). Declared volatile so the compiler always reads its latest value.
- buttonISR() — the Interrupt Service Routine. It just sets the flag to true. Short and fast.
- attachInterrupt(digitalPinToInterrupt(2), buttonISR, FALLING) — tells the Arduino: when pin 2 goes from HIGH to LOW (button pressed), call buttonISR immediately.
- Inside loop(), we check the flag. If it's true, we print the message and reset the flag.
- Even though loop() has a delay(500), the interrupt will still fire the moment the button is pressed.
Output:

This first project shows the basic idea very clearly: the interrupt catches the event instantly, and the main loop handles the printing later.
Once you understand that, you can use interrupts not just to detect something, but also to control outputs immediately.
Project 2: LED Toggle Using Interrupt
Each time the button is pressed, the LED switches state — ON becomes OFF, OFF becomes ON. No holding, no complicated logic. Just press and it flips.
The best part? The main program does not need to do anything at all.
The LED responds the instant the button is pressed no matter what else the Arduino is doing.
Code:
|
volatile bool ledState = false; void toggleLED() { ledState = !ledState; digitalWrite(5, ledState); } void setup() { pinMode(2, INPUT); pinMode(5, OUTPUT); attachInterrupt(digitalPinToInterrupt(2), toggleLED, RISING); } void loop() { // Nothing here — interrupt handles everything } |
Explanation:
volatile bool ledState = false creates a true/false variable that tracks whether the LED is ON or OFF. It starts as false — LED is off. The volatile keyword tells the Arduino to always read the real value of this variable from memory, never from a shortcut cache. This is important because the ISR can change it at any moment and the main program needs to always see the latest value.
void toggleLED() is the ISR — the function that runs the instant the button is pressed. ledState = !ledState flips the value. True becomes false, false becomes true. Every press switches it to whatever it was not before. digitalWrite(5, ledState) immediately updates the LED to match.
attachInterrupt(digitalPinToInterrupt(2), toggleLED, RISING) tells the Arduino to watch pin 2 and call toggleLED the moment it goes from LOW to HIGH. RISING is used because the NanoMake Pro buttons have external pull-down resistors — pressing the button takes the pin HIGH.
loop() is completely empty. The LED is toggling perfectly and the main program is doing nothing. That is the whole point — the interrupt works completely on its own.
Press the button. LED ON. Press again. LED OFF. Instant every time.
This project shows how interrupts can control devices instantly, without needing the main loop to keep checking all the time.
Output:

After that, we can take the same idea one step further and use interrupts to count events.
Project 3: Interrupt Counter
Every button press increases a counter by one. The Serial Monitor shows the running total. Simple but this project proves something really useful about interrupts.
The loop() has a delay(500) in it. During that half second the Arduino is frozen waiting, doing nothing. If you used digitalRead() inside loop() to detect the button, any press during that delay would be completely missed. With an interrupt, every single press is counted even the ones that happen while loop() is sleeping.
Code:
|
volatile int pressCount = 0; void countPress() { pressCount++; } void setup() { pinMode(2, INPUT); Serial.begin(9600); attachInterrupt(digitalPinToInterrupt(2), countPress, RISING); } void loop() { Serial.print("Button pressed: "); Serial.print(pressCount); Serial.println(" times"); delay(500); } |
Explanation:
volatile int pressCount = 0 is the counter. It starts at zero and increases every time the button is pressed. volatile is needed here for the same reason as before — the ISR writes to this variable and loop() reads it, so the Arduino must always fetch the real current value.
void countPress() is the ISR. It does one thing — pressCount++ — adding 1 to the counter. Short, fast, done. The ISR does not print anything. It just records that the press happened. loop() handles the printing.
attachInterrupt(digitalPinToInterrupt(2), countPress, RISING) watches pin 2 and calls countPress the moment the button is pressed.
Inside loop(), the three Serial print calls work together to print a clean line like "Button pressed: 7 times" every half second.
delay(500) is the interesting part. While the Arduino is frozen here, the interrupt is still fully active. Press the button three times during the delay — pressCount jumps by 3. When the delay ends and loop() prints again, it shows every press that happened — none were missed.
Try it. Press the button rapidly and watch the count jump by more than 1 between prints. That is the interrupt catching every single press, even the ones loop() never directly saw.
Output:
After seeing these examples, it becomes much easier to understand why interrupts are such a powerful feature in Arduino.
Why This Matters
Interrupts make your programs genuinely responsive. Without them, a busy loop can miss button presses, sensor triggers, or fast signals.
Once you understand interrupts, you can build systems like:
- Emergency stop for a robot
- Pulse counters for speed sensors
- Real-time event detection
- Responsive user interfaces
Final Thoughts
Interrupts are one of the most powerful features of microcontrollers. They let your Arduino do multiple things at once running a main program while always staying ready to react.
Keep the ISR short, use volatile for shared variables, and let the loop() handle the rest.
Keep experimenting, keep building, and keep making.