Interrupts Basics

Overview

A software application is, at its core, a sequence of machine instructions executed in a defined order. This sequence is dictated by the source code we write—where operation A is followed by operation B, then operation C, and so on. While source code defines a linear and predictable flow of execution, embedded systems often interact with the real world, where events like button presses, sensor readings, or communication signals occur independently of the program’s current state.

An interrupt is a signal to the processor indicating that an event needs immediate attention. It temporarily halts the current code execution, saves the processor state, and jumps to a predefined interrupt service routine (ISR) to handle the event. Interrupts enable a microprocessor to react to events in real time. For example, consider a user pressing a push button. In a typical embedded application, the system might first perform board initialization and then enter an infinite loop that processes data, prints messages, and periodically checks the button’s state. The diagram below illustrates this process, showing two iterations through the main loop.

InterruptIntro1

If the button is pressed while the application is actively polling the button, the press is successfully detected. However, what happens if the button is pressed and released while the microprocessor is busy executing other instructions—such as printing a message? In that case, the application would completely miss the button press.

This is where interrupts become essential. They allow us to change how we monitor the button. Instead of periodically checking its state, we want to detect the button press immediately when it occurs. To achieve this, we need to temporarily pause the main program and execute a special block of code dedicated to handling the button press.

The figure below illustrates this desired behavior—where the system can respond to the button press in real time, regardless of what the main application is doing.

InterruptIntro2

Interrupt Terminology

  • Exception
    An exception is an event that requires immediate attention from software. It can be triggered by hardware or software and typically indicates that something unusual has occurred during program execution.
  • Interrupt
    An interrupt is a type of asynchronous exception, usually generated by a peripheral device (like a timer or I/O port) to signal that it needs attention.
  • Fault
    fault is a high-priority exception that typically indicates a serious error, such as dereferencing a NULL pointer or accessing invalid memory. Faults often result in program termination or require special handling to recover.
  • IRQ (Interrupt Request)
    An IRQ is a signal sent by a device to the interrupt controller, requesting the processor’s attention. It’s the mechanism by which hardware communicates the need for service.
  • Interrupt Handler / Interrupt Service Routine (ISR)
    An ISR is a specialized function that executes in response to an interrupt. It performs tasks such as:

    • Identifying the source of the interrupt
    • Handling the required operation
    • Clearing the interrupt flag
    • Returning control to the application code
  • Application Code
    Any code that is not part of an ISR is referred to as application code. This is the main body of the program that runs when no interrupts are being serviced.

Interrupt Service Routine

When an interrupt occurs, the system executes a special block of code known as an ISR. This routine is designed specifically to handle the interrupt and runs with higher priority than the main application. Once the ISR begins execution, it runs to completion before control is returned to the main program.

The primary responsibilities of an ISR are:

  1. Acknowledge the interrupt – Confirm that the interrupt has been received and clear any flags or signals that triggered it. Clearing the interrupt flag is essential because it tells the processor that the interrupt has been handled. If the ISR fails to clear this bit, the processor will repeatedly re-enter the ISR, preventing the main application from resuming normal execution. This can lead to the system becoming stuck in an endless loop of interrupt handling.
  2. Queue incoming data –When an interrupt is generated by a peripheral device—such as a UART or ADC—the corresponding interrupt service routine (ISR) is responsible for quickly reading the incoming data and storing it in SRAM. This shared memory space allows the ISR to pass information to the main application using global variables. However, rather than processing the data immediately within the ISR, it is considered best practice to buffer the data and defer the processing or analysis to the main application.This design approach ensures that ISRs remain short and efficient, minimizing the time spent in interrupt context. By offloading computationally intensive tasks to the main application, the system remains responsive and capable of handling additional interrupts with less delay
  3. Notify the main application – Optionally, the ISR can set a flag or send a signal to inform the main program that an event has occurred and further processing is needed. The main application is structured to periodically check this flag. If the flag is set to true, it indicates that new data is available. The application then:
    1. Resets the flag to false – This prevents the same data from being processed multiple times.
    2. Processes the queued data – It retrieves the data placed in SRAM by the ISR and performs the necessary operations.

    This approach ensures a clean separation of responsibilities: the ISR handles data acquisition and signaling, while the main application handles data processing.

    When sharing data between interrupt service routines (ISRs) and the main application, it’s essential to declare shared global variables using the volatile keyword. This tells the compiler not to optimize access to the variable and to always re-read its value directly from memory (typically SRAM) each time it is referenced.

    Why is this necessary? The main application has no way of knowing when an ISR might modify a shared variable. Imagine the main application loads a global variable into a general-purpose register and begins making decisions based on that value. If an interrupt occurs and the ISR updates the same variable in memory, the value in the register becomes stale—it no longer reflects the current state of the system. Without the volatile keyword, the compiler might continue using the outdated register value, leading to incorrect behavior. By marking a variable as volatile, you ensure that every access—whether in the main application or in an ISR—reads the most up-to-date value from memory.

Interrupt Service Routine Summary

ISR Do’s

    • Interrupt service routines should as short as possible
    • If the interrupt indicates that data has been received, add the data to a buffer in SRAM
    • Indicate events to the application using volatile global variables.
    • Clear the interrupt prior to returning to the main application

ISR Don’t’s

    • Do not poll on any status bits in an ISR
    • Do not use any computationally intensive loops
    • Do not modify SRAM other than the dedicated queues and global variables that are being used to communicate with the main application

Leave a Reply