Understanding Interrupts: Definition, Priority Management, Runtime Triggers, and Program Handling

Follow our official account and reply with “Introductory Materials” to get a comprehensive tutorial from beginner to advanced on microcontroller programming.

The development board will guide you in your journey.

Written by: Wu Ji (WeChat: 2777492857)

The full text is approximately 5582 words, and it takes about 10 minutes to read.

Recently, a participant in our training camp mentioned that he was asked about interrupts during an interview.

How are interrupts defined? How do we prioritize them? When triggered at runtime, how does the program handle them?

I thought this question is likely a common interview topic, so I decided to write an article to document it.

To better understand, let me first provide an example of a requirement.

For instance, if you write a running light and a button detection, most beginners would implement it with a while(1) loop, executing sequentially.

Then, the requirement comes. The boss (or your own idea) says: I want the running light to stay on, and when the button is pressed, the light goes off, and when released, it comes back on. Or even more challenging: I want a button to turn on when pressed and turn off when pressed again, without any mispresses or delays.

You start writing code:

C void main(void) { // …initialize LED, button… while (1) { // Running light code LED1_ON(); delay_ms(200); LED1_OFF(); LED2_ON(); delay_ms(200); LED2_OFF(); // …and so on… // Button detection code if (KEY_IS_PRESSED()) { LED_ALL_OFF(); // Might also need to handle button debouncing… while (KEY_IS_PRESSED()); // Wait for button release… Oh no } } }

When you run it, bam! Your face is swollen. The button sometimes works, sometimes it doesn’t. The running light works fine, but when you hold the button down or press it rapidly, the entire program seems to freeze, and the running light stops. You scratch your head: how can a few lines of button code affect the running light above?

Even worse, if now you add another requirement: a timer that toggles an LED state every 100ms. If you stuff the timer logic into the while(1) loop, using software delays or a variable count to implement it, the code quickly becomes a mess… tightly coupled, logically confused, and full of bugs. You fix a bug in the timer, and the button stops working; you fix the button, and you find that serial data reception is lost.

Why does this happen? Because your program follows a linear logic, executing from top to bottom, line by line.

When it is executing the delay for the running light, it is “unaware” of the button being pressed; when it is waiting for the button to be released, it also “doesn’t see” that the timer has overflowed.

You try to string everything together with a while(1), constantly “polling” the status of each peripheral (this is called polling). This might work when there are few tasks, but when the tasks increase and the timing requirements become higher, it immediately becomes unmanageable, leading to missed events. The CPU seems busy, but the efficiency is low, and the response is sluggish.

If these pain points are not resolved, your code will always be just a toy, unable to cope with the complexities and real-time requirements of the real world. The product will be unstable, debugging cycles will be long, and you will spend a lot of time chasing seemingly random bugs, ultimately leading to a deep frustration with microcontroller programming.

To solve these problems and become an engineer who can write stable, efficient, and real-time microcontroller programs, you must master a core skill—interrupts.

This article will guide you through:

1. Thoroughly understanding what interrupts are and how they fundamentally differ from function calls.

2. Clarifying how interrupts are triggered and how the CPU responds.

3. Understanding how microcontrollers handle multiple interrupts arriving simultaneously—interrupt priority.

4. Learning what to do and what not to do in an Interrupt Service Routine (ISR), and how to collaborate with the main program.

By understanding these four points, you will be able to ace most interview questions about interrupts.

1. The True Nature of Interrupts: Not Interrupting, but Efficient Response

Interrupts, as the name suggests, are events that interrupt the normal execution of a program. However, note that this “interruption” is not a violent stop, but a systematic and efficient process switch.

Imagine you are engrossed in reading a book (the main program while(1) loop), and suddenly the doorbell rings (an interrupt event occurs). You wouldn’t just tear the book and rush out, right? You would:

1. Remember which page and line you were on (save the current program context, such as register states and the value of the program counter PC).

2. Close the book and get up to open the door (the CPU jumps to the entry address of the Interrupt Service Routine, ISR).

3. Handle the door knocking (execute the code in the ISR, such as checking who it is or picking up a package).

4. After handling it, close the door (the ISR execution is complete).

5. Return to your desk and flip to the page and line you remembered (restore the previously saved context).

6. Continue reading as if nothing happened (the CPU returns to the point in the main program where it was interrupted and continues executing).

This is the basic process of interrupts. The essence of interrupts is that the CPU pauses the current task to handle a more urgent or higher-priority event, and after handling it, returns to continue executing the original task.

How does it differ from a function call?

Different triggering methods: A function call is explicitly written in your code as FunctionName() and is an active behavior of the program. An interrupt is passively triggered by external events (hardware signals, timer overflows, data received via serial port, etc.) or specific software instructions.

Uncertain occurrence time: You know that a function call will occur at the line you wrote. The occurrence time of an interrupt depends on external events and can happen after any instruction in your program.

Different CPU processing flows: A function call involves pushing the return address onto the stack and then jumping. The interrupt process is more complex, requiring hardware to automatically or software to assist in saving and restoring the entire runtime environment (register sets, status words, etc.), and usually involves looking up the interrupt vector table.

Different purposes: A function call is for code reuse or modular functionality. An interrupt is to quickly respond to asynchronous events at uncertain time points, improving system real-time performance and efficiency.

2. Triggering Interrupts and CPU Response

There are various sources for triggering interrupts; almost all peripherals of a microcontroller can generate interrupt requests:

External interrupts: Changes in the level of a certain pin (rising edge, falling edge, or high/low level). For example, pressing or releasing a button.

Timer interrupts: Timer counts to a set value and overflows, or captures/matches. Used for precise delays, periodic tasks, waveform generation, etc.

Communication interrupts: Data received via serial port, transmission completed, SPI/I2C transmission completed, etc.

ADC interrupts: Completion of analog-to-digital conversion.

Others: Comparator interrupts, DMA transfer completion interrupts, power-down detection interrupts, etc., depending on the specific microcontroller model.

For an interrupt to occur, several conditions usually need to be met:

1. The interrupt source generates a request: For example, pressing a button causes a change in pin level, or a timer overflows. This sets an “interrupt flag” in a status register of the peripheral.

2. The interrupt source is enabled: You need to configure the software to allow this specific interrupt source to issue requests.

3. Global interrupts are enabled: There is a master switch controlling whether the CPU responds to any interrupt requests. This is usually a register bit (like ARM Cortex-M’s PRIMASK or a global enable bit). When global interrupts are disabled, peripheral interrupt requests can still occur and set flags, but the CPU will ignore them until global interrupts are re-enabled.

4. Priority allows: If the CPU is currently handling an interrupt (or global interrupts are disabled), the newly arrived interrupt must have a higher priority than the currently handled interrupt for the CPU to pause the current ISR and handle the new interrupt (this is called interrupt nesting or preemption).

When the conditions are met, the CPU executes the following response process (different architectures may vary slightly, but the core idea is consistent):

1. Complete the current instruction: The CPU will wait for the currently executing instruction to finish.

2. Save context: The CPU will automatically or through hardware mechanisms push the current important register values (including the program counter PC, status register SREG/PSR, etc.) onto the stack for future restoration.

3. Determine the interrupt source and ISR address: The CPU will look up the entry address of the ISR corresponding to the interrupt source in the interrupt vector table. The interrupt vector table is stored in flash memory and consists of a series of addresses. Each interrupt source corresponds to a fixed position in the table, which stores the address of its ISR.

4. Jump to execute ISR: The CPU modifies the value of PC, jumps to the corresponding ISR address, and begins executing the instructions in the interrupt service routine.

5. Clear the interrupt flag: At the beginning or appropriate position of the ISR, you must clear the interrupt flag corresponding to that interrupt source through software. This is a crucial step! If not cleared, after the ISR execution, the CPU will find the flag still set, thinking the interrupt event is still occurring, and will immediately re-enter the same ISR, causing the program to hang.

6. Execute ISR content: Handle the interrupt event.

7. Return: After the ISR execution is complete, a special return instruction (like ARM Cortex-M’s BX LR with specific link register values, or AVR’s RETI) informs the CPU that the interrupt handling is complete.

8. Restore context: The CPU pops the previously saved register values from the stack, restoring the state before the interrupt occurred.

9. Continue the main program: The CPU modifies the value of PC, returning to the point in the main program that was interrupted, and continues executing the next instruction.

The entire process is efficiently completed through hardware and software collaboration. The main program may not even “feel” the interruption process, except for a slight increase in execution time.

Therefore, truly understanding the triggering and response mechanisms of interrupts does not require rote memorization; you don’t have to confront these technical terms directly. Even if you can explain it in layman’s terms, you will likely do well in interviews.

3. Interrupt Priority: Who is More Important, and Who Decides?

If multiple interrupt events occur almost simultaneously, or if one interrupt is being serviced while another occurs, what should the CPU do? This requires an interrupt priority mechanism.

Interrupt priority determines:

1. Service order: If multiple interrupt requests arrive simultaneously, the CPU will prioritize responding to the highest priority interrupt.

2. Whether it can be interrupted: A low-priority ISR that is currently executing can be interrupted by a higher-priority interrupt (interrupt nesting), but a high-priority ISR will not be interrupted by a low-priority interrupt.

How interrupt priority is implemented depends on the microcontroller architecture:

Simple architecture: May only have fixed priorities, such as the 8051 microcontroller, or determine priority based on the order of positions in the interrupt vector table.

Complex architecture (like ARM Cortex-M): Provides flexible priority configurations. Typically, there is a main priority (Preempt Priority) and a sub-priority (Subpriority), such as in STM32.

Main priority: Determines whether an interrupt can preempt (interrupt) another currently executing ISR. A higher main priority can interrupt a lower main priority.

Sub-priority: When multiple interrupt requests have the same main priority, the sub-priority determines their service order. Higher sub-priority is serviced first, but during the execution of an ISR with the same main priority, other interrupt requests of the same main priority will be suspended and will not preempt.

Configuring interrupt priorities usually involves setting registers in the interrupt controller (like ARM’s NVIC).

You need to assign a priority value for each enabled interrupt source. Remember, the direction of priority values and their significance may be reversed (for example, a smaller value indicates a higher priority, or vice versa), depending on the specifications in the chip manual; consulting the manual is essential!

Properly dividing interrupt priorities is crucial:

High priority: Assign to interrupts that require extremely high response times, such as emergency alarms or high-speed data reception (to avoid frame loss). However, be cautious; high-priority ISRs should be as short as possible, as they will block all same-level or lower-priority interrupts and the main program execution.

Low priority: Assign to interrupts that do not have strict timing requirements, such as button presses or periodic but non-urgent tasks.

Same priority: Interrupts with the same main priority cannot preempt each other, which can simplify access control for shared resources, but may lead to higher sub-priority interrupts being blocked for a while by lower sub-priority ones.

A common misconception is that higher priority is always better. Interrupts with excessively high priority and long ISRs may cause low-priority interrupts to go unresponsive for extended periods, even losing events. Therefore, setting priorities is a balancing act.

4. What Can Be Done in an Interrupt Service Routine (ISR)? How Does the Program Handle It at Runtime?

ISRs are the core of the interrupt mechanism; they are the code blocks you write to handle specific interrupt events. However, the execution environment of an ISR is very special, with strict behavioral norms.

Golden Rule of ISRs: Quick In and Out!

In an ISR, you should only do the most urgent, core tasks that can be completed quickly. The reasons are as follows:

1. Blocking effect: Most microcontrollers will disable same-level or lower-priority interrupts while executing an ISR, and higher-priority interrupts can preempt. The longer the ISR execution time, the longer other interrupts are delayed or the main program is paused, which may slow down system response and even lead to the loss of other events.

2. Limited stack space: Interrupts will use the stack to save context; if the ISR calls too many functions or uses a lot of local variables, it may lead to stack overflow.

3. Shared resource issues: ISRs and the main program (or other ISRs) may access the same global variables or hardware resources simultaneously; if not protected, data corruption may occur, leading to race conditions.

So what is typically done in an ISR?

Clear the interrupt flag! (Again, this is very important)

Quickly record events: For example, set a flag indicating that a certain event has occurred.

Access critical data: If it is a communication interrupt, you may need to quickly read the received data into a buffer. If it is an ADC interrupt, read the conversion result.

Perform a few register operations: Directly configure peripheral registers.

What should be avoided in an ISR?

Time-consuming operations: Floating-point calculations, complex mathematical computations, long loops, delay functions (delay_ms()), printing output (printf).

Potentially blocking operations: Waiting for a flag, waiting for a peripheral to complete (unless it is the peripheral corresponding to that ISR).

Dynamic memory allocation (malloc/free).

Calling non-reentrant functions: If a function is interrupted by an ISR and then called again within the ISR, and this function uses non-reentrant resources (like global or static variables without protection), problems will arise. Standard library functions (like some string manipulation functions, dynamic memory functions) are often not reentrant, so use them with caution.

Complex synchronization mechanisms: Using semaphores, mutexes, etc., in an ISR requires extreme caution, as it can easily lead to deadlocks or performance issues.

So where do those time-consuming processing logics go?

Typically, the ISR is responsible for “sensing” and “recording,” while the actual “processing” is done in the main program’s while(1) loop.

After detecting an event, the ISR quickly sets a global flag or stores data in a FIFO queue.

For example, in our microcontroller training camp project, for serial port interrupts like this data stream, it generally exists in a queue.

Understanding Interrupts: Definition, Priority Management, Runtime Triggers, and Program Handling

Then the main program continuously checks these queues in the while(1) loop, and when it detects an event, it processes the data.

Understanding Interrupts: Definition, Priority Management, Runtime Triggers, and Program Handling

This is a classic interrupt handling pattern: interrupt notification + main loop processing, which is adopted by most enterprise-level projects.

Now, let’s look at the earlier example of toggling an LED with a timer, and how to implement it using interrupts:

volatile unsigned char timer_100ms_flag = 0; // volatile tells the compiler that this variable may change at times unknown to the compiler (e.g., in an interrupt) // Timer interrupt service routine (pseudo code)void Timer_IRQHandler(void){    if (TIMER_IS_EXPIRED()) // Check if this timer triggered the interrupt    {        timer_100ms_flag = 1; // Set the flag to notify the main loop        TIMER_CLEAR_FLAG();   // Clear the hardware interrupt flag - must do!    }}int main(void){    // ...initialize LED, timer...    // Configure the timer to generate an interrupt request every 100ms    // Enable timer interrupt    TIMER_INTERRUPT_ENABLE();    // Enable global interrupts    GLOBAL_INTERRUPT_ENABLE(); // Usually __enable_irq() or sei() etc.    while (1)    {        // Main loop does other things...        if (timer_100ms_flag) // Check the timer flag        {            timer_100ms_flag = 0; // Clear the flag            // Perform time-consuming processing in the main loop            LED_TOGGLE(); // Toggle LED state        }        // The main loop can also check other flags and handle other events...        // For example, check the flags set by button interrupts and handle button logic        // Check the flags set by serial port receive interrupts and process received data    }

The timer interrupt occurs every 100ms, and the ISR simply sets a flag and clears the hardware flag. The LED toggling operation is placed in the main loop. This way, even if the LED_TOGGLE() function is a bit slow, it only affects the processing speed of the main loop and does not block the timer interrupt itself (of course, if the main loop processing is too slow, it may cause the flag to remain 1, but the interrupt itself will not be lost, just delayed in subsequent processing).

This pattern separates the quick response of interrupts from the complex processing of the main loop, improving system concurrency and stability.

The volatile keyword is crucial for variables shared between the ISR and the main program, as it forces the compiler to read the variable’s value from memory each time, rather than using a cached value in a register, avoiding errors caused by optimization.

end

Understanding Interrupts: Definition, Priority Management, Runtime Triggers, and Program Handling

Here are more original articles from Wu Ji about personal growth experiences, industry insights, and technical content.

1. What is the growth path of an electronic engineer? A 10-year, 5000-word summary

2. How to quickly understand others’ code and thinking

3. How to manage too many global variables in microcontroller development projects?

4. Why is it that most C language development for microcontrollers uses global variables??

5. How to achieve modular programming in microcontrollers? The practicality is astonishing!

6. Detailed explanation of the use and actual function of callback functions in C language

7. A step-by-step guide to implementing queues in C language, easy to understand and super detailed!

8. Detailed explanation of pointer usage in C language, easy to understand and super detailed!

Leave a Comment