Table of Contents
- What Are Interrupts and Why Do They Matter?
- Hardware Interrupts: External Pin Triggers
- Software Interrupts: Timer-Based Triggers
- ISR Rules: What You Can and Cannot Do
- Practical Examples: Rotary Encoder and RPM Counter
- Pin Change Interrupts: Use Any Pin as an Interrupt
- Debugging Interrupt Problems
- Frequently Asked Questions
- Conclusion
What Are Interrupts and Why Do They Matter?
An arduino interrupts tutorial begins with understanding what interrupts are: signals that tell the processor to immediately pause its current task and execute a special function called an Interrupt Service Routine (ISR). Once the ISR completes, the processor resumes exactly where it left off. Think of it as a doorbell — no matter what you are doing, you stop, answer the door, and then go back to your activity.
Without interrupts, your Arduino sketch runs sequentially from top to bottom in the loop() function. If you need to check a button press, you must poll the button pin repeatedly. If your loop takes 500 ms to complete (reading sensors, updating a display, sending data), you might miss a button press that lasts only 50 ms. Interrupts solve this problem by reacting to events instantly, regardless of what the main loop is doing.
Interrupts are essential for time-critical applications: counting encoder pulses for motor position, detecting zero-crossings in AC power for dimmer circuits, capturing precise timing events, responding to emergency stop buttons, and processing high-speed communication data. Any application where missing an event or responding late causes problems needs interrupts.
Hardware Interrupts: External Pin Triggers
Hardware interrupts (also called external interrupts) are triggered by voltage changes on specific pins. The Arduino Uno supports two hardware interrupts: INT0 on pin 2 and INT1 on pin 3. The Mega 2560 supports six: INT0-INT5 on pins 2, 3, 18, 19, 20, and 21.
Trigger Modes:
- LOW: Triggers whenever the pin is LOW (continuous triggering)
- CHANGE: Triggers on any state change (LOW to HIGH or HIGH to LOW)
- RISING: Triggers only on a LOW-to-HIGH transition
- FALLING: Triggers only on a HIGH-to-LOW transition
// Basic hardware interrupt example - button press counter
volatile unsigned long pressCount = 0;
volatile unsigned long lastPress = 0;
void setup() {
Serial.begin(9600);
pinMode(2, INPUT_PULLUP); // Button on pin 2 with internal pull-up
attachInterrupt(digitalPinToInterrupt(2), buttonISR, FALLING);
}
void loop() {
// Main loop does its work without worrying about the button
Serial.print("Presses: ");
Serial.println(pressCount);
delay(1000);
}
void buttonISR() {
// Simple debounce: ignore presses within 200ms of last press
unsigned long now = millis();
if (now - lastPress > 200) {
pressCount++;
lastPress = now;
}
}
Key Points:
- Always use
digitalPinToInterrupt(pin)to convert the pin number to the interrupt number. Do not hardcode interrupt numbers. - The ISR function takes no parameters and returns nothing (
void). - Variables shared between the ISR and main code must be declared
volatile. This tells the compiler not to optimise away reads of the variable, since it can change at any time. - Use
INPUT_PULLUPfor buttons connected to ground — this eliminates the need for an external pull-up resistor.
Software Interrupts: Timer-Based Triggers
Timer interrupts use the ATMega328P’s internal hardware timers to trigger ISRs at precise intervals. This is useful for tasks that must happen at exact frequencies: ADC sampling at a fixed rate, generating PWM signals with custom frequencies, or updating a display at a fixed refresh rate.
The ATMega328P has three timers: Timer0 (8-bit, used by millis()/delay()), Timer1 (16-bit), and Timer2 (8-bit). Timer1 is the best choice for custom interrupts because it is 16-bit (allows longer intervals) and is not used by critical Arduino functions.
// Timer1 interrupt at 1 Hz (once per second)
void setup() {
Serial.begin(9600);
// Disable interrupts during configuration
noInterrupts();
// Reset Timer1
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;
// Set Compare Match Register for 1 Hz
// 16 MHz / (prescaler 256 * 1 Hz) - 1 = 62499
OCR1A = 62499;
// CTC mode (Clear Timer on Compare Match)
TCCR1B |= (1 << WGM12);
// Prescaler 256
TCCR1B |= (1 << CS12);
// Enable Timer1 Compare A interrupt
TIMSK1 |= (1 << OCIE1A);
interrupts(); // Re-enable interrupts
}
volatile unsigned long seconds = 0;
ISR(TIMER1_COMPA_vect) {
seconds++;
}
void loop() {
Serial.print("Uptime: ");
Serial.print(seconds);
Serial.println(" seconds");
delay(500);
}
Timer Frequency Formula:
OCR1A = (F_CPU / (prescaler * desired_frequency)) - 1
Where F_CPU = 16,000,000 for standard Arduino boards. Available prescaler values are 1, 8, 64, 256, and 1024. Choose the prescaler that keeps OCR1A within the timer’s range (0-65535 for Timer1, 0-255 for Timer0/Timer2).
Common Timer Interrupt Frequencies:
- 1 kHz (1ms): OCR1A = 1999, prescaler 8
- 100 Hz (10ms): OCR1A = 624, prescaler 256
- 10 Hz (100ms): OCR1A = 6249, prescaler 256
- 1 Hz (1s): OCR1A = 62499, prescaler 256
ISR Rules: What You Can and Cannot Do
Interrupt Service Routines have strict rules that beginners often violate, leading to crashes, lockups, and bizarre behaviour.
Rule 1: Keep ISRs short. An ISR should execute in microseconds, not milliseconds. While inside an ISR, other interrupts are disabled by default. If your ISR takes too long, you will miss timer ticks (millis() will drift), miss serial data, and miss other external interrupts.
Rule 2: No Serial.print() inside ISRs. Serial communication relies on interrupts internally. Calling Serial functions inside an ISR causes a deadlock. Instead, set a flag in the ISR and handle Serial output in the main loop.
// WRONG - will cause lockups
void myISR() {
Serial.println("Interrupt!"); // NEVER do this
}
// CORRECT - set a flag
volatile bool eventOccurred = false;
void myISR() {
eventOccurred = true; // Fast - just set a flag
}
void loop() {
if (eventOccurred) {
eventOccurred = false;
Serial.println("Interrupt occurred!"); // Safe in main loop
}
}
Rule 3: No delay() inside ISRs. The delay() function depends on Timer0 interrupts, which are disabled during your ISR. Calling delay() will hang the processor indefinitely.
Rule 4: Use volatile for shared variables. Without volatile, the compiler may cache a variable’s value in a register and never re-read it from memory, even though the ISR has changed it. This causes the main loop to see stale values.
Rule 5: Protect multi-byte reads. On an 8-bit processor, reading a 16-bit or 32-bit variable is not atomic. If an interrupt fires between reading the high and low bytes, you get a corrupted value. Use noInterrupts()/interrupts() around multi-byte reads:
volatile unsigned long counter = 0;
void loop() {
noInterrupts();
unsigned long safeCounter = counter; // Atomic copy
interrupts();
Serial.println(safeCounter);
}
Practical Examples: Rotary Encoder and RPM Counter
Rotary Encoder Reading:
Rotary encoders output two square waves (A and B channels) that are 90 degrees out of phase. By reading one channel in the ISR triggered by the other, you can determine both position and direction.
// Rotary encoder with interrupt-driven reading
#define ENCODER_A 2 // Interrupt pin
#define ENCODER_B 4 // Regular digital pin
volatile int encoderPosition = 0;
void setup() {
Serial.begin(9600);
pinMode(ENCODER_A, INPUT_PULLUP);
pinMode(ENCODER_B, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(ENCODER_A), encoderISR, RISING);
}
void encoderISR() {
if (digitalRead(ENCODER_B) == HIGH) {
encoderPosition++; // Clockwise
} else {
encoderPosition--; // Counter-clockwise
}
}
void loop() {
noInterrupts();
int pos = encoderPosition;
interrupts();
Serial.println(pos);
delay(100);
}
RPM Counter with IR Sensor:
Mount an IR reflective sensor near a rotating shaft with a reflective strip. Each rotation triggers an interrupt. Count pulses over a fixed time interval to calculate RPM.
// RPM counter using external interrupt
volatile unsigned long pulseCount = 0;
unsigned long lastTime = 0;
float rpm = 0;
void setup() {
Serial.begin(9600);
pinMode(2, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(2), countPulse, FALLING);
}
void countPulse() {
pulseCount++;
}
void loop() {
if (millis() - lastTime >= 1000) { // Calculate every second
noInterrupts();
unsigned long count = pulseCount;
pulseCount = 0;
interrupts();
rpm = count * 60.0; // Pulses per second * 60 = RPM
Serial.print("RPM: ");
Serial.println(rpm);
lastTime = millis();
}
}
Pin Change Interrupts: Use Any Pin as an Interrupt
The ATMega328P supports Pin Change Interrupts (PCINT) on every I/O pin, not just pins 2 and 3. However, PCINT triggers on any change (both rising and falling edges) and groups pins into three banks that share interrupt vectors. You must determine which pin triggered the interrupt manually.
// Pin Change Interrupt on pin 8 (PCINT0 bank)
volatile bool pin8Changed = false;
volatile byte lastPIN_B = 0;
void setup() {
Serial.begin(9600);
pinMode(8, INPUT_PULLUP);
// Enable Pin Change Interrupt for pin 8 (PCINT0)
PCICR |= (1 << PCIE0); // Enable PCINT0 bank (pins 8-13)
PCMSK0 |= (1 << PCINT0); // Enable interrupt for pin 8
lastPIN_B = PINB; // Store initial state
}
ISR(PCINT0_vect) {
byte current = PINB;
byte changed = current ^ lastPIN_B;
lastPIN_B = current;
if (changed & (1 << PINB0)) { // Pin 8 changed
pin8Changed = true;
}
}
void loop() {
if (pin8Changed) {
pin8Changed = false;
Serial.println("Pin 8 changed!");
}
}
PCINT Banks:
- Bank 0 (PCINT0-7): Digital pins 8-13 and the crystal pins
- Bank 1 (PCINT8-14): Analog pins A0-A5
- Bank 2 (PCINT16-23): Digital pins 0-7
For simpler implementation, use the EnableInterrupt library, which wraps PCINT configuration in an Arduino-friendly API similar to attachInterrupt().
Debugging Interrupt Problems
Interrupt-related bugs are among the hardest to track down because they involve timing and concurrency. Here are common issues and how to fix them:
Problem: Variables not updating. Cause: Missing volatile keyword. The compiler optimises away reads of non-volatile variables, so your main loop never sees the updated value.
Problem: Random crashes or lockups. Cause: Too much code inside the ISR, or calling functions that use interrupts (Serial, delay, millis inside ISR). Solution: Move all heavy processing to the main loop; use flags in the ISR.
Problem: Missed interrupts. Cause: ISR takes too long, or interrupts are disabled for too long in the main code. Solution: Shorten ISR execution time. Use noInterrupts() for the minimum possible duration.
Problem: Double-counting (bouncing). Cause: Mechanical switches bounce, causing multiple interrupts per press. Solution: Add a 100nF capacitor across the button (hardware debounce) or use a time-based debounce in the ISR (check if 50-200 ms has elapsed since the last interrupt).
Problem: Corrupted multi-byte variables. Cause: Reading a 16-bit or 32-bit variable without disabling interrupts. An interrupt between byte reads gives a value with the high byte from before the update and the low byte from after. Solution: Always wrap multi-byte reads with noInterrupts()/interrupts().
Frequently Asked Questions
How many interrupts can the Arduino Uno handle?
The Uno has 2 external hardware interrupts (pins 2 and 3) and 20 pin change interrupts (all digital and analogue pins). It also has timer interrupts (Timer0, Timer1, Timer2) and communication interrupts (UART, SPI, I2C). In practice, 2-4 active interrupt sources work well; more than that requires careful priority management.
Can I use interrupts with analogRead()?
Do not call analogRead() inside an ISR — it takes approximately 100 microseconds and blocks other interrupts. Instead, set a flag in the ISR and perform the analogRead in the main loop. Alternatively, use the ADC’s own interrupt to handle conversion completion asynchronously.
Do interrupts work during delay()?
Yes, hardware interrupts still fire during delay(). The delay() function uses a polling loop on millis(), and interrupts are enabled throughout. However, your ISR code does not “interrupt” the delay itself — the main code still waits for the full delay period after the ISR returns.
What happens if two interrupts fire at the same time?
The ATMega328P processes interrupts in order of priority (lower interrupt vector number = higher priority). INT0 has the highest priority among external interrupts, followed by INT1, then PCINT0, PCINT1, PCINT2, and so on. The lower-priority interrupt waits until the higher-priority ISR completes.
Conclusion
Interrupts are a fundamental tool for building responsive, reliable Arduino systems. Hardware interrupts respond to external events in microseconds, timer interrupts provide precise periodic execution, and pin change interrupts extend interrupt capability to every I/O pin. By following ISR rules (keep them short, use volatile, avoid Serial/delay), you can build projects that react to real-world events without polling overhead.
Master interrupts, and you unlock the ability to build rotary encoder interfaces, RPM counters, frequency meters, real-time controllers, and any application where timing and responsiveness are critical.
Add comment