Studio 2 - Interrupts

Interrupts

Motivation: Using interrupts in Arduino (or any microcontroller) is a powerful technique that allows the system to respond immediately to external or internal events without constantly checking for them in the main loop.

I/O Device

"I/O" stands for "Input/Output". The ATmega328 (used in Arduino Uno) has various I/O devices that interact with the CPU through interrupts for efficient processing, like:

  • Digital I/O (GPIO) (External Interrupts, Pin Change Interrupts)

  • Analog-to-Digital Converter (ADC)

  • Timers (Timer/Counter 0, 1, 2)

  • USART (Serial Communication)

  • SPI (Serial Peripheral Interface)

  • I²C (Inter-Integrated Circuit, TWI - Two-Wire Interface)

Three modes of accessing I/O

The three modes are:

  1. Polling

  2. Interrupts

  3. Direct Memory Access (DMA, not needed for CG2111A)

Why is called "Accessing I/O"

They are called modes of accessing I/O because they define how the CPU interacts with I/O devices to transfer data or handle events. The term "accessing I/O" refers to how the CPU monitors, receives, or sends data to peripherals like sensors, buttons, serial ports, etc.

What is being accessed?

  • I/O Device Registers → Status, control, and data registers of peripherals.

  • Memory → Data buffers (especially for DMA).

How they access I/O?

  • Polling → The CPU actively reads I/O registers.

  • Interrupts → The I/O device signals the CPU when it's ready.

  • DMA → The I/O device directly moves data to/from memory without CPU intervention.


Now, let's look at the how the first two modes work in detail!

Polling

Polling is a method where the CPU repeatedly checks the status of an I/O device in a loop to see if an event has occurred.

1

How Polling Works in General

  • The CPU reads a status register of an I/O device.

  • If the device is not ready, the CPU keeps checking (looping).

  • Once the device is ready, the CPU processes the event (e.g., reading sensor data).

  • The cycle repeats indefinitely.

2

Example on Atmega328p

The above is a classic polling code, and basically the reason for its being polling is as follows,

  • The function check_pressed() is called inside loops in flashGreen() and flashRed().

  • The CPU continuously checks digitalRead(SWITCHPIN) to detect a button press.

  • The code does not react immediately to the button press—it only checks at certain times.

  • This is a classic polling behavior where the CPU wastes time checking instead of doing useful work.

3

Issues / Disadvantages of Polling

From the example above, we can summarize the disadvantages of using polling as follows,

  • Inefficient CPU Usage

    • The CPU keeps checking for button presses, even when nothing happens.

    • It cannot perform other tasks efficiently while waiting.

  • Delayed Response

    • The CPU only checks the button at specific points (after LED flashes & delays).

    • If the button is pressed between checks, it might be missed or take time to respond.

  • Unnecessary Power Consumption

    • The CPU stays active even when idle, consuming power.

    • This is a problem for battery-powered devices.

  • Not Scalable

    • If multiple I/O devices (e.g., sensors, serial input) need monitoring, polling becomes slow and inefficient.

Interrupts

An interrupt is a mechanism that allows an I/O device to signal the CPU asynchronously when an event occurs, stopping the CPU’s current task and executing a special function (Interrupt Service Routine, ISR).

1

How Interrupts Work

  • The CPU is executing its normal instructions.

  • An external event (e.g., button press, timer overflow) triggers an interrupt signal.

  • The CPU pauses its current execution and jumps to the ISR (Interrupt Service Routine).

  • The ISR executes to handle the event (e.g., toggling an LED state).

  • Once ISR completes, the CPU resumes its original task.

2

Example on Atmega328p

In the code above,

  • Instead of continuously checking the button state in loop(), the code uses an interrupt to detect the button press immediately.

  • attachInterrupt(digitalPinToInterrupt(buttonPin), switchISR, RISING); registers an interrupt on the rising edge of the button signal.

  • When the button is pressed, the CPU automatically runs switchISR() to toggle the onOff variable.

  • This makes the LED respond instantly without needing a digitalRead() inside loop().

3

Rising Edge vs. Falling Edge

A digital signal is either LOW (0V) or HIGH (e.g., 5V on Arduino Uno). The moment it switches from one state to another is called an edge.

Rising Edge:

  • Definition: The signal changes from LOW (0V) → HIGH (5V).

  • Use Case: Detect when a button is pressed (if using pull-down resistor).

  • Example:

Falling Edge:

  • Definition: The signal changes from HIGH (5V) → LOW (0V).

  • Use Case: Detect when a button is released (if using pull-up resistor).

  • Example:

  • Why Use RISING in the Code?

    • The button press is detected when the voltage jumps from LOW to HIGH, triggering the ISR.

4

Advantages of Using Interrupts (Compared to Polling)

  • Instant Response

    • The CPU immediately detects button presses without waiting.

    • Unlike polling, which might miss a quick press between checks, interrupts never miss an event.

  • CPU Efficiency

    • The CPU can perform other tasks instead of constantly checking the button state.

    • Saves processing power, making the system faster and more efficient.

  • Power Saving

    • Microcontrollers can enter low-power sleep modes and wake up only on interrupts.

    • Ideal for battery-powered applications.

  • Scalability

    • Works well even with multiple input devices.

    • Polling multiple I/O devices would be inefficient, but interrupts allow handling multiple events without wasting CPU cycles.

Bare Metal Programming

Vector Table

In the ATmega328p, when an interrupt occurs,

  1. the microcontroller uses the vector table to find the address of the corresponding Interrupt Service Routine (ISR).

  2. The CPU then jumps to the ISR address, executes it, and returns to the main program after completion.

The vector table is located at the beginning of flash memory and stores addresses for each interrupt source.

Reset and Interrupt Vectors in ATmega328/P (P82)

External Interrupts for Digital I/O

  • INT0 and INT1 are external interrupt pins (pins D2 and D3 on the ATmega328p).

  • They are typically used for detecting changes in external signals, such as button presses or sensor readings.

  • INT0 and INT1 can be triggered on specific edge transitions (rising or falling) or low level signals.

1

Configuring EICRA for INT0 and INT1

EICRA (External Interrupt Control Register A) is used to configure how the external interrupts are triggered.

EICRA (P89)

The configuration for INT0 and INT1 is done by setting specific bits in EICRA.

  • ISC01 and ISC00: Control the trigger condition for INT0 (bits for rising/falling edge).

  • ISC11 and ISC10: Control the trigger condition for INT1 (bits for rising/falling edge).

This is the table summarizing the ISCx[1:0] bits' behavior. (x means either 0 or 1)

Value
Description

00

The low level of INT1/INT0 generates an interrupt request

01

Any logical change on INT1/INT0 generates an interrupt request

10

The falling edge of INT1/INT0 generates an interrupt request.

11

The rising edge of INT1/INT0 generates an interrupt request.

Example:

  • INT0 triggers on rising edge: EICRA |= (1 << ISC01) | (1 << ISC00);

  • INT1 triggers on falling edge: EICRA |= (1 << ISC11);

2

Enabling INT0 and INT1 with EIMSK

EIMSK (External Interrupt Mask Register) enables or disables external interrupts for INT0 and INT1.

EIMSK (P90)

Example:

  • To enable INT0, set EIMSK |= (1 << INT0);

  • To enable INT1, set EIMSK |= (1 << INT1);

3

Interrupt Mask: cli() and sei()

  • cli() (Clear Interrupts):

    • Disables global interrupts.

    • It clears the I-bit in the status register, stopping the processor from accepting interrupts.

  • sei() (Set Interrupts):

    • Enables global interrupts.

    • It sets the I-bit in the status register, allowing interrupts to be processed by the CPU.

These functions are used to globally enable or disable interrupts in the system.

Example:

volatile keyword

TLDR;

Always declare GLOBAL variables that are changed by ISRs to be volatile

Why use volatile?

  • When an ISR changes a global variable, the value of the variable can be updated at any time, and the main program or other code might not expect it.

  • Without volatile, the compiler may optimize accesses to the variable by assuming its value doesn't change unexpectedly, potentially causing incorrect behavior.

Why "optimize" here? Because ld instruction consumes lots of CPU cycles. (2 iirc in ARM assembly)

Explanation with Assembly

In this example, if counter is not declared volatile, the compiler might optimize the read (lds r16, counter), assuming that the variable doesn't change outside the main program's control. This could lead to reading stale data and incorrect behavior.

Basically, just think of moving Line 11 outside the main loop.

By declaring counter as volatile, the compiler ensures that each read/write operation accesses the memory location directly, preserving the value updated by the ISR.

Basically, just think of moving Line 11 back into the main loop so that during each iteration, it will load the counter variable.

Last updated