Learn

Arduino interrupts with attachInterrupt()

Start building free

Or see every lesson.

  • 455parts on the bench
  • 43boards running now
  • 1.00xreal time, on every board
Arduino Uno: counting button presses with an interruptlive0.000 s 0.00x
Press Run, then press and release the button
Pin 2’s interrupt counts every falling edge, bounces included. Press and release the button.

An interrupt runs a function of yours the instant a pin changes, whatever loop() is doing at the time. On an Uno only pins 2 and 3 can do it. Write attachInterrupt(digitalPinToInterrupt(2), onPress, FALLING); in setup() and onPress() runs every time pin 2 falls from HIGH to LOW. Mark any variable the function changes as volatile.

The circuit above is a button on pin 2 and an LED on pin 13, and the sketch counts presses with an interrupt. Press Run and press the button once. The serial monitor prints "presses: 2", and when you let go, "presses: 4". The interrupt is so fast it catches every bounce of the button’s contacts, on the press and again on the release.

Below is how attachInterrupt() works, the four modes, why the counter needs volatile, how to debounce a button inside an interrupt, and what interrupts are really for: counting pulses too fast to poll.

Want the step-by-step version? The lesson "Reading a button, and the bounce" walks through this with checkpoints.

Open the lesson

attachInterrupt(), piece by piece

attachInterrupt() takes three things. The interrupt number, which on an Uno is 0 for pin 2 and 1 for pin 3; always get it with digitalPinToInterrupt(pin) rather than writing the number. The function to run, called the handler or ISR, which takes nothing and returns nothing. And the mode, which says which change of the pin to react to.

The sketch below is the smallest useful one. It counts falling edges on pin 2 and prints the count once a second. With an ideal button that does not bounce, one press printed 0 and then 1. With the simulator’s ordinary bouncing button, the same press and release printed 0 and then 4.

Count falling edges on pin 2
const int BUTTON = 2;

volatile int presses = 0;

void onPress() {
  presses++;
}

void setup() {
  Serial.begin(9600);
  pinMode(BUTTON, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(BUTTON), onPress, FALLING);
}

void loop() {
  Serial.println(presses);
  delay(1000);
}

FALLING, RISING, CHANGE and LOW

FALLING runs the handler when the pin goes from HIGH to LOW, which for a button on INPUT_PULLUP is the press. RISING is LOW to HIGH, the release. CHANGE is both. LOW runs it over and over for as long as the pin is low.

One press and release of the bouncing button counted 4 with FALLING, 4 with RISING and 8 with CHANGE, since every bounce is a fall and a rise. LOW ran the handler 16043 times during a tenth of a second hold. LOW is for chips that hold an alarm line low until you answer them, not for buttons.

Why the variable must be volatile

The compiler does not know that a handler can run between any two lines of loop(). In the sketch below, setup() waits in a loop for pressed to turn true. Without volatile, the compiler reads pressed once, sees nothing in the loop change it, and loops forever. We pressed the button: it printed "waiting for the button" and nothing more, ever.

Change the first line to volatile bool pressed = false; and the same press printed "pressed!". volatile tells the compiler to read the variable fresh every time. Every variable a handler shares with the rest of the sketch needs it.

A variable bigger than one byte, such as an int or an unsigned long, needs one more step. The Uno moves one byte at a time, so the handler can change it halfway through loop() reading it. Copy it with interrupts off, as the pulse counter further down does: noInterrupts(); copy = count; interrupts();.

A flag without volatile
const int BUTTON = 2;

bool pressed = false;  // needs to be volatile

void onPress() {
  pressed = true;
}

void setup() {
  Serial.begin(9600);
  pinMode(BUTTON, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(BUTTON), onPress, FALLING);
  Serial.println("waiting for the button");
  while (!pressed) {
  }
  Serial.println("pressed!");
}

void loop() {
}

Debouncing a button with an interrupt

A handler cannot wait for a bounce to settle, because nothing else runs while it waits. So let it note the time, and let loop() decide. The handler below only records that an edge happened and when. loop() waits for 50 ms of quiet, then checks whether the button is still down.

Three presses printed "press 1", "press 2" and "press 3" and nothing else, even with the button set to its worst bounce. The plain counter above counted 14 for the same three presses.

An interrupt button, debounced
const int BUTTON = 2;
const unsigned long QUIET_MS = 50;

volatile bool edge = false;
volatile unsigned long lastEdge = 0;
int presses = 0;

void onEdge() {
  edge = true;
  lastEdge = millis();
}

void setup() {
  Serial.begin(9600);
  pinMode(BUTTON, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(BUTTON), onEdge, FALLING);
}

void loop() {
  noInterrupts();
  bool seen = edge;
  unsigned long when = lastEdge;
  interrupts();
  if (seen && millis() - when > QUIET_MS) {
    edge = false;
    if (digitalRead(BUTTON) == LOW) {
      presses++;
      Serial.print("press ");
      Serial.println(presses);
    }
  }
}

What interrupts are for: fast pulses

A button is slow enough to poll. Interrupts earn their place on signals too fast to watch from loop(): a flow sensor, a rotary encoder, a fan’s speed output. Here pin 9 makes a 490 Hz test signal with analogWrite(), wired to pin 2. The interrupt caught every edge: it printed "490 pulses in the last second", then 490 again, then 491, as the edges fell either side of each second.

A loop that checks pin 2 with digitalRead() and then calls delay(5) counted 92 pulses a second from the same wire, because the pin went up and down several times during each delay. The interrupt does not care what loop() is doing.

Count a 490 Hz signal
volatile unsigned long pulses = 0;

void onPulse() {
  pulses++;
}

void setup() {
  Serial.begin(9600);
  pinMode(9, OUTPUT);
  analogWrite(9, 128);  // a 490 Hz test signal, wired to pin 2
  attachInterrupt(digitalPinToInterrupt(2), onPulse, RISING);
}

void loop() {
  delay(1000);
  noInterrupts();
  unsigned long counted = pulses;
  pulses = 0;
  interrupts();
  Serial.print(counted);
  Serial.println(" pulses in the last second");
}

Keep the handler short

While a handler runs, other interrupts wait, including the timer that moves millis() on. This handler reads millis(), busy-waits for 5 ms by watching micros(), and reads millis() again. It printed "millis() before: 299" and "millis() 5 ms later: 299": the clock stood still the whole time.

So inside a handler, do not call delay(), do not wait for anything, and keep Serial.print() out of it. A print in a handler works for one short line, but with a bouncing button one press printed "pressed" four times. Set a flag or add to a count, and do the rest in loop().

millis() inside a handler
volatile unsigned long before = 0;
volatile unsigned long after = 0;
volatile bool done = false;

void onPress() {
  before = millis();
  unsigned long start = micros();
  while (micros() - start < 5000) {
  }
  after = millis();
  done = true;
}

void setup() {
  Serial.begin(9600);
  pinMode(2, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(2), onPress, FALLING);
  while (!done) {
  }
  Serial.print("millis() before: ");
  Serial.println(before);
  Serial.print("millis() 5 ms later: ");
  Serial.println(after);
}

void loop() {
}

Common mistakes

Writing the pin number as the interrupt number. attachInterrupt(2, onPress, FALLING) asks for interrupt 2, which an Uno does not have, so it does nothing: ours counted 0. Write digitalPinToInterrupt(2).

Using a pin that has no interrupt. On an Uno only pins 2 and 3 work; digitalPinToInterrupt(4) is -1, and a button on pin 4 counted 0. Pin 3 counted the same 4 as pin 2.

Leaving out volatile. The loop that waits for the flag never sees it change.

Counting a button with no debounce. Every bounce is an edge, and one press can count 4.

Doing too much in the handler. No delay(), no long prints, no waiting.

Questions

Which pins can use interrupts on an Arduino Uno?

Only pins 2 and 3, which are interrupt 0 and interrupt 1. Use digitalPinToInterrupt(pin) to get the number. A Mega has six (2, 3, 18, 19, 20 and 21), and most ESP32 pins can use one.

Why use volatile with interrupts?

Without it the compiler may keep the variable in a register and never read the new value the handler wrote. Here a loop waiting for a flag without volatile never ended.

Can I use delay() inside an interrupt?

No. While the handler runs, the timer interrupt that moves millis() is held off: millis() stood still for 5 ms inside one here. Set a flag in the handler and do the waiting in loop().

Why does my interrupt count one button press many times?

The button’s contacts bounce, and the interrupt is fast enough to see every bounce: one press and release counted 4 here. Let the handler record the time and debounce in loop().

What is the difference between RISING, FALLING and CHANGE?

FALLING reacts when the pin goes from HIGH to LOW, RISING from LOW to HIGH, and CHANGE to both. For a button on INPUT_PULLUP, FALLING is the press.

Sources

Where the facts on this page come from

Build this for real

Open the editor, change a value and watch the number move with it. Nothing to install, and no account needed.