Arduino Interrupt: Find out Exactly how to use INT0 and INT1 - learn all about these interrupts and how to use them, including example code. Did you know, There are more than Two external interrupts on an Uno! It's true; (available on virtually any I/O pin) but how do you use them? Find out here.

Arduino interrupts:
  • Let your program react instantly to events.
  • Are easy to setup when you know how.
  • Let your program work efficiently instead of polling for an event.

  • Find out why you need two functions attachInterrupt() and digitalPinToInterrupt().

An Arduino Interrupt is useful when you need a program to react virtually instantly to an event. If you apply a digital signal to pins 2 or 3 of an Arduino Uno then these can trigger an interrupt (There are more).

There are also many more interrupt sources that are used internally in the chip. For example there are serial port completion interrupts (data received or transmitted), timer match or timer overflow interrupts etc.

These all allow efficient operation of the internal modules, while still allowing the processor to get on with other tasks. But the focus of this page is on external physical event i.e. pin voltage change in the real world that you use to generate an interrupt.

Although this tutorial is about interrupts on the Uno, all external interrupts work in the same way so it applies to any Arduino Board.

How to set up an external interrupt

The two functions you need to activate an interrupt:

  1. attachInterrupt( <interrupt>, my_isr_function, mode ).
  2. digitalPinToInterrupt( <Arduino pin> ).

Other related functions are:

detachInterrupt(digitalPinToInterrupt(pin)) - Detaches a specified interrupt - so use digitalPinToInterrupt() to get the interrupt number from the Arduino pin. This stops the interrupt handler from executing when that pin triggers.

interrupts() - Globally enables or disables hardware interrupts. Used to disable interrupts when sensitive code is executing to prevent conflicts.

noInterrupts() - Disables interrupts globally. Shorthand for interrupts(false).

In general the above three functions are rarely used since:

  • Interrupts are already on, so that millis() and micros() can operate.
  • You also don't often detach an interrupt because you want the code to generate an interrupt at an interrupt pin - unless you don't - maybe you need to ignore the interrupt for a time while you do  a calculation etc.
  • You will only use noInterrupts() in special cases; see pulsein() for instance.

Interrupt pins on some Arduino Boards

Here's a list of interrupts and their Arduino pins, to which you connect up your interrupting signal.

Uno/Nano 2 3

Leonardo 3
2 0

20 19

Note: Physical Arduino Uno/Nano pin 3 has INT1, and physical Arduino pin 2 has INT0.
Why do you need two functions to setup an external interrupt?

In Arduino code we are all used to specifying a pin to which to apply a selected operation for example:

     digitalRead(5); - will read the digital value from Arduino pin 5.

For an external interrupt you can't select the pin as the interrupt source because the attachInterrupt function is wanting the interrupt number (0 or 1 - for an Arduino Uno/Nano).

Therefore a mediating function us used to translate the pin (on which the interrupt must be attached - because it is physically fixed in the microcontroller architecture). The function digitalPinToInterrupt translates the pin on which the interrupt is attached into the interrupt number. This follows the Arduino convention that the pin is specified as the input to the function.

You could say why bother with all this and just make a function that accepts the pin number and translate in the background to the interrupt number. The answer is that there are many different Arduino boards - all have different pins and associated interrupts. So these functions allow other boards to operate.

As long as you select the pin that is associated with the interrupt then these functions return and attach the interrupt without you having to think about the interrupt number. Just select the right pin and the relevant interrupt is attached.

External interrupts are always input to a fixed pin on the microcontroller chip defined by the manufacturer. So you have to know which pin you are going to use as above but the interrupt code uses interrupt numbers from zero upwards; so you need the second function (below).

Interrupt functions

For attaching an interrupt to your code i.e. making your Interrupt Service Routine (ISR) react to an interrupt, you use the function (here <interrupt> is a value indicating the interrupt number):

    attachInterrupt( <interrupt>, my_isr_function, mode ).

The problem is that the <interrupt> argument is the interrupt number and not the pin number, so you need a convenience function that translates the Arduino pin to the required interrupt number; and that function is:

     digitalPinToInterrupt( <Arduino pin> ).

It is a bit cart-before-the-horse since you have to know which pin uses an interrupt before you can decide to use that pin as an interrupt pin! So really you know the interrupt number because you had to look up the pin functionality anyway. This function is really just a convenience function.

If you wanted to use INT1 on an Arduino Uno triggering it to execute myIsr() - on the rising edge of the input signal - then you would write:

    attachInterrupt( digitalPinToInterrupt(3) , myIsr, RISING);

You could also write

    attachInterrupt( 1 , myIsr, RISING);

So the above means Interrupt 1. It looks like there was confusion over that parameter being interpreted as a pin number hence the function  digitalPinToInterrupt(3) which stops that mistake so it's a good idea to use it.

Arduino Interrupt MODEs

There are three modes for an interrupt (the last argument above):

The interrupt modes that can be specified when using attachInterrupt() on Arduino are:

- CHANGE - This will trigger the interrupt on any change of state on the input pin, either from LOW to HIGH or HIGH to LOW.

- RISING - The interrupt will trigger on a rising (LOW to HIGH) edge event on the input pin.

- FALLING - The interrupt will trigger on a falling (HIGH to LOW) edge event on the input pin.

So in summary:

- CHANGE - Triggers on any state change
- RISING   - Triggers on rising (LOW to HIGH) edge 
- FALLING - Triggers on falling (HIGH to LOW) edge

The interrupt mode determines exactly what type of transition or edge on the input pin will cause the interrupt handler function to execute. This allows triggering interrupts on specific events.


The two modes RISING and FALLING are self explanatory.

The CHANGE mode

The CHANGE mode in attachInterrupt() is useful for triggering an interrupt handler function on any transition (change of state) occurring on the input pin.

How it works:

- The Arduino keeps track of the previous state of the input pin - whether it was last seen as HIGH or LOW.

- During each pass of the Arduino main loop, it checks the current state of the pin and compares it to the previous state.

- If the current and previous states are different, that signifies a change or transition has occurred on the pin.

- For example, if the pin was previously LOW but is now reading HIGH, that's a LOW to HIGH transition (rising edge).

- Or if it was previously HIGH but is now LOW, that's a HIGH to LOW transition (falling edge).

- Whenever a change of state is detected, it will trigger the interrupt handler function attached using attachInterrupt().

- The previous state is then updated to match the current pin state for the next comparison.

So in summary, the CHANGE mode essentially checks for either a rising or falling edge. It will trigger the interrupt routine on any transition between the HIGH and LOW states.

This is useful for situations where you need to detect either type of transition, without distinguishing between rising and falling specifically. The handler will execute anytime the pin changes value.

Stopping an external interrupt

You won't often need it but if you want to stop the interrupt action then use the function:

    detachInterrupt ( digitalPinToInterrupt( INTPIN ) )

Again there are two functions detachInterrupt() and digitalPinToInterrupt() used as shown above.

Note: The opposite to attachInterrupt is: detachInterrupt(<INTn>).

This will remove the interrupt function from the related interrupt pin "INTPIN".

How Many Arduino Interrupts are there?

...On the Arduino Uno there only two external interrupt pins:

  • pin 2 - interrupt 0.
  • pin 3 - interrupt 1.

Do you think this is a true statement?: "There only two external interrupts"

See if you know the right answer:
How many external Interrupts are available on an Arduino Uno?

  • 2
  • 5
  • 26
  • 32
2, 5, 26, or 32? (click to find out)

Surprisingly there are 26!

There are the 2 external interrupts (INT0, INT1) plus three sets of 8 pin change interrupts.

Arduino Interrupt Overview

Microcontrollers are very good at repeatedly doing a task by executing one  instruction after another. They do this by incrementing the program counter and fetching the next instruction from memory. The problem comes when the processor has to deal with unexpected events.

For an external event you need the program to branch off and do something without waiting around for the current task to complete. The mechanism to do this is the interrupt.

Consider a program waiting for serial port data. it is in a polling loop constantly reading the serial port register waiting for data. However, you want the program to react to a button press to start another activity.

While polling the serial port, your program does nothing else and this is where interrupts come in. [Yes I know the serial port has its own interrupt - give me a break - its just an example for illustration ok - lets assume it does not have an interrupt].

An external interrupt will temporarily divert the processor from its current operation to a so-called ISR (Interrupt Service Routine) which will perform the operation you need. Not only that, it will execute the ISR virtually instantly providing an easy way to respond quickly to events.

Arduino interrupt vectors

When an interrupt is triggered your program has to do something. You tell the Arduino what to do by providing a function that services the interrupt (an ISR or Interrupt Service Routine).

The address of the ISR is placed into a specific location in a table of interrupt vectors. Each location in that table is specific to each interrupt. When the interrupt triggers, your ISR is called.

There are 6 dedicated external interrupts. These are the first six entries in the interrupt vector table:

Vector Name
Two, you already know about: INT0 and INT1 but there is also the reset vector which sits at address zero, so when the program counter is set to its power-up state (0x0000) the code at the reset vector is executed - you place a jump to your reset handing routine at this vector address. PCINT0 ~ 2 are the pin interrupt change vectors.

The Arduino Uno has a total of 26 interrupt vectors, and most of these are internal interrupts that are triggered by internal peripherals such as the ADC, timers or UARTs etc.

The total number of interrupt sources (any single external signal or internal module that can cause an interrupt to trigger) is:

26-3+3*8  =  47

[ This includes internal module interrupts and 3x8 pin change signals. ]

Arduino Interrupt Call Stack

For normal function calls, a stack is used to save and restore the Program Counter (PC). The PC contains the address of the current instruction.

When a subroutine is called, the value of the PC is pushed onto the stack, and when the subroutine is finished, the value is popped back from the stack. Thus returning the PC to its original address before the function call. The same process is used for an ISR subroutine.

For a subroutine call, the assembler instruction CALL pushes the PC onto the stack. To return from the call, RET pops the PC from the stack.

The only difference between a subroutine return and an ISR return is that the ISR return uses the RETI instruction, which re-enables interrupts.

ISR operation

So an ISR is virtually identical to a normal subroutine call. The problem is that an ISR can be executed at any time corrupting any existing register values.

To allow the ISR to work current register values must be stored at the beginning and restored at the end. Fortunately this is taken care of for you within the compiler. All relevant flags and registers are saved before, and then restored after an interrupt.

In this way the current program is interrupted in the middle of its operation and does not know anything about the interrupt.

Hardware connection

Connect a push button on one side to Arduino pin D2 and on the other side to Ground.

Connect a 100nF capacitor from Arduino pin D2 to Ground. To explore interrupts and switch debouncing and why this works see the page on switch debouncing.

Examples Showing Arduino Interrupt Operation


Arduino Ide: 1.8.9+


None used.

Example Sketch 1

Arduino Interrupt Hardware setup

Connect a push button from pin D2 to ground.

Arduino Interrupt Code Description

To illustrate the concept, consider the simple program below, all it does is flash an led on and off at 500ms intervals and then repeats. If the button is pressed causing an interrupt, then the delay time is toggled to 50ms.

The delay function shows that the interrupt is operating - even while most of the time the processor is sitting within the delay function doing nothing.

Adding interrupt code allows the program to react instantly and change the delay time for the LED flash (delayTime is updated immediately in the interrupt) however the flash rate of the LED will not update immediately since the delay operation must finish first. The second example below, shows how to avoid this using arduino millis.

// By John Main ©
// Simple LED flash and button interrupt using delay().
#define interruptPin 2  // Can only be pin 2 or 3 on the Uno.

volatile uint16_t delayTime= 500;

void setup(void) {

  attachInterrupt(digitalPinToInterrupt(interruptPin), myisr, RISING);

// Arduino Delay LED flash.
void loop(){


// Interrupt Service Routine
// Do not use this ISR in 'real' code because
// of switch bouncing see this page for useful
// code:
void myisr() {
static byte state = 1;
state = !state; if (state) delayTime = 500; else delayTime = 50; }


Example Sketch 2

Arduino Interrupt Hardware setup

Connect a push button from pin D2 to ground (the same as sketch 1).

Arduino Interrupt Code Description

This is a more realistic example, where the main loop reacts more quickly to the information sent from the interrupt. Instead of using the delay function (which is a do nothing operation) the millis() function is used. This allows the processor to do other useful work; In this case executing the loop function more quickly.

Note: Because we use millis() in this code it is also used in the ISR to filter out too much bouncing i.e. if we get 2 or more interrupts within 300ms the routine only acts on one interrupt.

TIP: See the page on switch debouncing to see how much the capacitor filters out switch bounces.

// By John Main ©
// Simple LED flash and button interrupt using millis().
#define interruptPin 2  // Can only be pin 2 or 3 on the Uno.

volatile uint16_t delayTime= 500;

uint32_t timeWas = 0;

void setup(void) {
  attachInterrupt(digitalPinToInterrupt(interruptPin), myisr, RISING);

// Arduino millis delay LED flash.
void loop(){
  static byte LEDstate=0;

  if (millis()-timeWas>delayTime) {
    timeWas = millis();
    LEDstate = LEDstate ? 0 : 1;
    if (LEDstate) digitalWrite(LED,HIGH); else digitalWrite(LED,LOW);

// Interrupt Service Routine
void myisr() {
static byte state = 1;
static uint32_t lastButtonTime=0;

  // Filter out too quick buttons = errors.
  if (millis()-lastButtonTime > 300) {
    state = !state;
    if (state) delayTime = 500; else delayTime = 50;
    lastButtonTime = millis();


Arduino - When To Use Volatile

The Arduino volatile command is only required if a variable can be changed outside normal program flow.

You can see examples of volatile in the Sketches above.

In a normal function, variables are input to a function as parameters. These are then used within the function and can be returned as a value from the function.

In this case the compiler knows all the operations that can happen on that variable and when they happen. It is totally defined by the code written within the function. Therefore the compiler can make assumptions about the variable to optimise operations.

For a function that receives information from an interrupt routine, communicated via a global variable, the interrupt can change the value of that variable at any time even while the function is operating!

Compilers do fancy optimisations behind the scenes and the Arduino volatile keyword tells the compiler to "leave this variable alone" and not optimise it. The variable must be left available for both the function and the ISR to read and update.  It tells the compiler to re-read the value and not read the cached version, since the variable may have been updated by the interrupt.

PCINT Arduino Interrupt

These are the pin change interrupts associated with the each port on the Arduino. So there are 3 ports each with 8 pins that can be detected for interrupt use. These are labelled PCINT0..23.

TIP: PCINT0..23 can be used to wake the part from sleep mode (other than idle).

Interrupts are PCINT2..0 registers. To use them you have to use hardware register names ( read-the-datasheet ) i.e. its not quite as easy as the normal Arduino interrupt method.

Warning: Warning: PCINT0..23 will trigger an interrupt even if pins are outputs.

How to use PCINT

Hardware setup

  • Connect a push button from pin A0 to ground.
  • Connect a push button from pin A4 to ground.
  • Connect a sounder from A3 to ground (piezo disc : optional).

How to Setup Pin Change Interrupts

Since these interrupts are simpler than INT0 and INT1, they only trigger on a pin change. They also trigger only for blocks of 8 bits and there is no indication on which pin caused the interrupt. The three problems are:

  1. A single interrupt triggers for any of 8 pins.
  2. No indication of which pin triggered the interrupt.
  3. These interrupts only trigger on a pin change.

The first thing to do is use a mask register to enable or disable individual pins in an 8 bit block. Then only these will cause an interrupt to trigger. You don't want outputs triggering the interrupt - they can do that and could be used as a software interrupt mechanism.

The second thing to do is to record the port value before (and after) interrupt operations. That way you can use an XOR expression to find out the pin changes.

Lets get some interrupts from port C specifically PC4 and PC0 which are on PCINT8 and PCINT12 respectively. The mask register for PORT C is PCMSK1.

Code Description

The serial port shows some information as the buttons are pushed. Pushing the A0 button will set the LED flash rate to 500ms and pushing the A4 button will set the flash rate to 50ms.

Loop code

In the loop() code the LEDs are flashed at the rate set in delayTime (changed in the ISR). if an interrupt is detected (flgTone is set) then the number of interrupts is printed to the serial port. If there were more than one interrupt (intsFound ) a high frequency sound is made otherwise a low frequency sound is made.

Both flgTone and intsFound are reset to zero ready for the next button push.

Warning: Since any change on the pin is detected as an interrupt both falling and rising edges will cause an interrupt but switch bounce also causes many more interrupts.

Due to switch bounce there can be many more than one bounce for each button press. See here for how to eliminate switch bounce in interrupt routines.

ISR code

In the ISR the old port value is XORed with the new value (any different bits show up as ones). v1 and v2 are given the old values of the port. 

For the following code; If a change in A0 was detected, and v1 was zero then the delay time is set to 500ms.

if (v1==0 && change & (1<<PCINT8) ) delayTime = 500;

A similar process is done for A4.

If any rising edge is found then flgTone is incremented.

if (change && (v1==0 || v2==0) ) flgTone++; // rising edge only

The oldPorta value is updated and the number of interrupt detection's (intsFound) is incremented.

// By John Main ©
// Using Pin Change interrupts.
const byte LED = LED_BUILTIN;
const byte TONE_PIN = A3;
const byte PC0_pin = A0;
const byte PC4_pin = A4;
volatile byte state = LOW;

volatile static byte oldPorta;
volatile static byte PCMask;
volatile uint16_t delayTime= 500;
volatile byte flgTone = 0,intsFound = 0;

void setup() {

  pinMode(LED, OUTPUT);
  pinMode(PC0_pin, INPUT_PULLUP);
  pinMode(PC4_pin, INPUT_PULLUP);

  oldPorta = PINC;
  PCMSK1 |= (1<<PCINT8);
  PCMSK1 |= (1<<PCINT12);
  Serial.println(PCMSK1,HEX); // What is the total mask.
  PCMask = PCMSK1;

  PCICR |= (1<<PCIE1); // Enable PCINT on portc.

// Arduino Delay LED flash.
void loop(){


  if(flgTone) {
    Serial.print("Found ");Serial.print(intsFound);
    Serial.print(" interrupts, using: ");Serial.println(flgTone);


    intsFound = flgTone = 0;

ISR( PCINT1_vect ) {
  byte change,v1,v2;
  change = oldPorta ^ PINC;

  v1 = oldPorta & (1<<PCINT8);
  v2 = oldPorta & (1<<PCINT12);

  // v1==0 previously so detecting rising edge.
  if (v1==0 && change & (1<<PCINT8) ) delayTime = 500;
  if (v2==0 && change & (1<<PCINT12)) delayTime = 50;
  if (change && (v1==0 || v2==0) ) {
    intsFound++; // Only count rising edge interrupts (as this interrupt reacts to both).
    flgTone = 1; // rising edge only

  oldPorta = PINC;


Warning: Button presses will cause multiple bounces. To properly debounce the button inputs on interrupt pins see this page.

Pin Change Registers

PCICR Interrupt Control Register

Control which bank of 8 interrupts is active (enabled).

Bits: PCIE[2..0] : Interrupt Enable for PORT B(0), C(1), D(2).

PCMSK Interrupt Mask Register[2..0]

Enable individual pins to interrupt with the mask register.

PCMSK2 : Contains PCINT[23..16] - PORTD[7..0].
PCMSK1 : Contains PCINT[14..8] - PORTC[6..0].
PCMSK1 : Contains PCINT[7..0] - PORTB[7..0].

Note: PORTC bit 7 (PCINT15) is not available [ATMega328p datasheet]. Also PC6 is the reset pin - unless programmed by fuses as an I/O pin.

PCINT Pin Change Interrupts

PCINT0 for PORTB (Input using PINB) - compiler vector is PCINT0_vect.
PCINT1 for PORTC (Input using PINC) - compiler vector is PCINT1_vect.
PCINT2 for PORTD (Input using PIND) - compiler vector is PCINT2_vect.

PCIFR Interrupt Flag Register

Flag bit is set when an interrupt is triggered (useful for polling).

Bits: PCIF[2..0] : Interrupt Flag.

Written by John Main who has a degree in Electronic Engineering.

Note: Parts of this page were written using claude-instant as a research assistant.


Have your say about what you just read! Leave me a comment in the box below.

Don’t see the comments box? Log in to your Facebook account, give Facebook consent, then return to this page and refresh it.

Privacy Policy | Contact | About Me

Site Map | Terms of Use