Sitemap

How Interrupts Work in Embedded Systems? (With Practical Examples)

12 min readApr 22, 2026

--

Press enter or click to view image in full size
How Interrupts Work in Embedded Systems

Let’s start with something you already understand.

You’re sitting at your desk, deeply focused on your work. Suddenly, your phone rings.

You don’t keep checking your phone every few seconds just to see if someone might call. You don’t interrupt yourself unnecessarily. Instead, you continue your work peacefully and only when the phone actually rings do you stop, respond, and then return exactly to where you left off.

That natural behaviour is exactly how interrupts work in embedded systems.

Now imagine if your brain worked differently, if every 2 seconds you had to stop and check your phone manually, even when no one was calling. That would be exhausting, inefficient, and distracting.

That inefficient behaviour is what we call polling in embedded systems.

Understanding this difference is the key to understanding interrupts.

Before Interrupts: What is an Embedded System?

Before understanding interrupts, you need to understand where they are used.

An embedded system is a small computer designed to perform a specific task.

Unlike laptops or phones, it does not run multiple random applications. It is built for one purpose.

Examples:

  • Washing machine → controls wash cycles
  • Car system → controls braking, sensors
  • Smartwatch → tracks health data

Every embedded system has two main parts:

1. Hardware

  • Microcontroller (CPU)
  • Sensors
  • Buttons
  • Communication modules

2. Software

  • Code written to control hardware
  • Logic that defines behaviour

The hardware collects information from the real world, and the software decides what to do with it.

Interrupts act as a bridge between real-world events and software response.

What even is a microcontroller doing all day?

Before we talk about interrupts, let’s be clear about what a microcontroller normally does.

It runs code. Line by line. One instruction at a time. Like this:

  • Read input (sensor, button, etc.)
  • Process data
  • Update output (display, motor, LED)
  • Repeat

This loop runs thousands, sometimes millions, of times per second. The CPU is always busy doing something in sequence.

Now here’s the problem. What if, in the middle of step 3, a button gets pressed? Or a byte arrives over the wire from another device? Or a motor encoder pulses?

The CPU is busy. I don’t know. It misses it.

You could solve this the brute-force way: just check everything, constantly.

cwhile (1) {
check_button();
check_uart();
check_sensor();
check_timer();
check_encoder();
// ... check 10 more things
}

This is called polling. And it works, kind of. But it has a real problem.

Every time around the loop, you’re spending time checking things that probably haven’t changed. You’re wasting effort. And if the loop is slow (say it takes 50 milliseconds to go around once), you might miss a button press that lasted only 20 milliseconds. It’s already gone by the time you check.

Polling is like having a security guard who walks the entire building every 10 minutes to check if any door was opened. Slow, inefficient, and you’ll miss things.

Interrupts are the alarm system. The moment a door opens, a siren goes off instantly.

What is an interrupt?

An interrupt is a:

signal sent by 2. Hardware 3. that tells the CPU

“Stop what you’re doing. Right now. Come handle this. Then go back.”

That’s the whole idea. The CPU doesn’t need to check. The hardware tells it when something happens.

When that signal arrives, here is what happens inside the chip, in order:

Step 1: The CPU finishes its current instruction.

It never abandons mid-thought. It completes whatever single operation it’s on.

Step 2: It saves where it was.

The CPU takes a snapshot of its current state, which line of code it was on, what values were in its registers, and stores all of it on the stack. Think of it as placing a bookmark.

Step 3: It jumps to the ISR.

ISR stands for Interrupt Service Routine. It’s just a function you write that says, “when this happens, do this.” The CPU jumps straight to it.

Step 4: The ISR runs.

Your handler executes. Maybe it reads a byte from the UART. Maybe it logs a button press. Whatever needs doing.

Step 5: It comes back.

The CPU picks up its bookmark, restores its saved state, and resumes the main code. Your main program has absolutely no idea that anything happened. It picks up mid-sentence.

The one story we’ll use throughout this article

To make all of this stick, let’s follow one example from beginning to end. We’ll keep coming back to it as we go deeper.

The scenario: You’re building a small device that reads temperature from a sensor and displays it on a screen. A button lets the user toggle between Celsius and Fahrenheit.

Simple. Beginner-friendly. But it has everything: a main loop, a peripheral (the button), and a need for instant response.

Here’s what the main loop looks like:

cwhile (1) {
float temp = read_temperature();
update_display(temp, current_unit);
}

It reads the temperature and updates the display. Over and over.

Now the question: how does the button press get detected?

Without interrupts: polling the button

The naïve approach:

cwhile (1) {
float temp = read_temperature();
update_display(temp, current_unit);

if (button_is_pressed()) {
toggle_unit();
}
}

This seems fine. But what if read_temperature() talks to a slow sensor over I2C and takes 80 milliseconds? What if update_display() refreshes a screen and takes another 40 milliseconds?

Your loop takes 120 milliseconds per cycle. If the user presses the button for 50 milliseconds and lets go, you might never catch it. The button press happened entirely between two checks.

Your device feels broken. The user presses the button, and nothing happens.

With interrupts: the button calls you

Instead of checking the button inside the loop, you configure it as an interrupt source. Now the button “calls” the CPU the moment it’s pressed, no matter what else is happening.

You write one small function:

cvoid EXTI0_IRQHandler(void) {
toggle_unit();
clear_interrupt_flag();
}

That’s your ISR. It runs the instant the button is pressed. Your main loop doesn’t change at all:

cwhile (1) {
float temp = read_temperature();
update_display(temp, current_unit);
// no button check needed here, ever
}

The button press could happen at any point, in the middle of reading the sensor, halfway through drawing the screen, and the CPU will stop, toggle the unit, and come straight back. The user gets an instant response every single time.

This is the magic. The main loop stays clean. The response is guaranteed. No polling needed.

The vector table, how does the CPU know where to jump?

Great question. When the button fires its interrupt, how does the CPU know to run EXTI0_IRQHandler and not some random function?

The answer is the vector table.

The vector table is a list of addresses stored at the very beginning of flash memory. Each entry is a pointer to an ISR function. Entry 0 points to the reset handler.

Entry 1 points to a fault handler. And so on. Entry 22 (on an STM32F4) points to EXTI0_IRQHandler, the external pin 0 interrupt.

Address 0x00000000 → Reset_Handler
Address 0x00000008 → NMI_Handler
Address 0x0000000C → HardFault_Handler
...
Address 0x00000058 → EXTI0_IRQHandler <-- button interrupt lands here
Address 0x000000D4 → USART1_IRQHandler
Address 0x000000DC → TIM2_IRQHandler

When the button fires, the hardware looks up the correct entry in this table and jumps there. No searching, no decision-making. It’s a direct lookupwhich is why interrupts respond in nanoseconds.

You never built this table by hand. Your startup file (provided by the chip vendor) builds it automatically based on the ISR function names you define in your code.

The NVIC, the traffic controller

Now, what if two things happen at once? The button gets pressed at the same moment a UART byte arrives. Who goes first?

This is managed by the NVIC, Nested Vectored Interrupt Controller. It’s a piece of hardware built into every ARM Cortex-M chip. Think of it as an air traffic controller for interrupts.

You assign every interrupt a priority number.

Lower number = higher priority.

If two interrupts fire simultaneously, the NVIC lets the higher-priority one run first. And if a high-priority interrupt fires while you’re already inside a lower-priority ISR, it preempts it, cuts in line, and runs immediately.

Get Theiotacademy’s stories in your inbox

Join Medium for free to get updates from this writer.

For our temperature device, this might look like:

c// Button response, user-facing, should feel instant
HAL_NVIC_SetPriority(EXTI0_IRQn, 3, 0);

// UART — receiving commands from a PC
HAL_NVIC_SetPriority(USART1_IRQn, 5, 0);

The button (priority 3) beats the UART (priority 5) if they collide. The user always gets a snappy response.

Context saving: what exactly gets bookmarked?

Let’s go one layer deeper, because this is the part beginners often wonder about: when the CPU “saves its state,” what does that actually mean?

When an interrupt fires, the ARM Cortex-M hardware automatically pushes these values onto the stack:

PC, Program Counter. The address of the next instruction in the main code. This is how the CPU knows where to return.

xPSR, Status flags (zero flag, carry flag, etc.)
R0, R1, R2, R3, General-purpose registers
R12, LR, More registers used by function calls

This all happens in hardware, automatically, before your ISR even starts. When the ISR finishes and returns, the CPU pops all of it back. Your main code resumes as if nothing happened, same variables, same position, same register values.

In our temperature example

If the CPU was halfway through calculating temp when the button fires, it saves that half-calculated state, runs the ISR, then comes back and finishes the calculation with all values intact.

Writing the ISR: What you must never do?

Now, let’s talk about how to write a good ISR. Because there are rules, and breaking them causes bugs that are very hard to find.

Rule 1: Keep it short.

The ISR runs while the main code is paused. The longer you stay in there, the longer everything else is blocked. A good ISR is 5–10 lines. It grabs what it needs and leaves.

Rule 2: Never block or wait.

No HAL_Delay(500). No waiting for a sensor to respond. No printf. These either take time or aren't safe to call from inside an ISR. They will hang your system.

Rule 3: Always clear the interrupt flag.

Every peripheral has a status flag that says “my interrupt is pending.”

If you don’t clear it before leaving the ISR, the CPU returns to the main code, immediately sees the flag still set, and fires the ISR again. Infinite loop. System frozen.

cvoid EXTI0_IRQHandler(void) {
toggle_unit();
__HAL_GPIO_EXTI_CLEAR_IT(BUTTON_PIN); // MUST do this — always
}

Rule 4: Use volatile for shared variables.

If a variable is written inside an ISR and read in the main loop (or vice versa), mark it volatile. Without it, the compiler might cache it in a register, and your main loop will never see the update.

cvolatile uint8_t unit_changed = 0;

void EXTI0_IRQHandler(void) {
unit_changed = 1; // set flag in ISR
__HAL_GPIO_EXTI_CLEAR_IT(BUTTON_PIN);
}

while (1) {
float temp = read_temperature();

if (unit_changed) { // check in main loop
unit_changed = 0;
toggle_unit();
}

update_display(temp, current_unit);
}

Notice the pattern here: the ISR only sets a flag. The actual work (toggle_unit) happens in the main loop. This is the correct design for anything more than trivial work.

Putting it all together: the full picture

Let’s now connect every concept through our temperature device, end to end.

c// --- Setup ---

// Configure button pin as falling-edge interrupt
GPIO_InitStruct.Pin = BUTTON_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

// Tell NVIC: enable this interrupt, priority 3
HAL_NVIC_SetPriority(EXTI0_IRQn, 3, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);

// --- Shared state ---
volatile uint8_t unit_changed = 0;
uint8_t current_unit = CELSIUS;

// --- ISR: runs the instant button is pressed ---
void EXTI0_IRQHandler(void) {
unit_changed = 1;
__HAL_GPIO_EXTI_CLEAR_IT(BUTTON_PIN);
}

// --- Main loop: free to focus on its job ---
while (1) {
float temp = read_temperature();

if (unit_changed) {
unit_changed = 0;
current_unit = (current_unit == CELSIUS) ? FAHRENHEIT : CELSIUS;
}

update_display(temp, current_unit);
}

Read through this slowly. Every piece connects:

The GPIO is configured to fire on a falling edge (button pressed down)

The NVIC is enabled with priority 3

The ISR is tiny, sets a flag, clears the source, and exits

The main loop does real work, checks the flag, and responds cleanly

volatile makes sure the main loop always reads the latest flag value

This is production-quality interrupt-driven firmware. Not because it’s complicated, but because every piece is doing exactly its job and nothing more.

Quick summary of what had learned

  1. Interrupts are not complex once you understand the core idea clearly.
  2. The main concept is simple: Don’t check. Get notified.
  3. Instead of constantly checking for events, the system waits for a signal.
  4. The hardware sends a signal when an event occurs.
  5. The CPU pauses its current task and saves its state.
  6. It then executes a handler function (ISR) to deal with the event.
  7. After handling, the CPU restores its previous state.
  8. Execution continues as if nothing happened.
  9. This approach keeps your main program clean and organised.
  10. It allows the system to respond instantly to events.
  11. It also improves overall efficiency by avoiding unnecessary checks.
  12. A helpful way to understand this is the phone analogy:
  13. Instead of repeatedly checking your phone, you wait for it to ring.

Once you grasp this mental model, advanced concepts become easier:

  • Vector table
  • NVIC (Nested Vectored Interrupt Controller)
  • Context saving
  • ISR design rules

All these concepts are simply ways of implementing that one core idea in hardware.

After mastering basic interrupts, the next step is learning interrupts in RTOS environments like FreeRTOS.

In such systems, ISRs communicate with tasks using mechanisms like queues and semaphores.

That deeper layer builds on the same fundamental interrupt concept.

How Interrupts Work (Step-by-Step Flow)?

Now, let’s understand the internal working of interrupts in a clear and connected way.

When an event occurs, like pressing a button or receiving data, a signal is sent to the processor. This signal is the interrupt request.

The processor does not immediately stop mid-instruction. It completes the current instruction and then pauses its execution. Before switching tasks, it saves its current state (like where it was in the program). This is called saving the context.

After saving the context, the processor jumps to a predefined function known as the Interrupt Service Routine (ISR). This ISR is responsible for handling the event.

For example, if a sensor sends data, the ISR may read and process that data.

Once the ISR finishes its job, the processor restores its previous state and resumes the original task as if nothing happened.

All of this happens extremely fast, often in microseconds, making the system feel instant and responsive.

Interrupt Service Routine (ISR): The Core Handler

The ISR is the heart of interrupt handling.

It is a small, specially written function that executes whenever an interrupt occurs. The main purpose of an ISR is to handle urgent events quickly and efficiently.

Because ISRs interrupt the normal flow of the program, they must be designed carefully. They should be short, precise, and fast. If an ISR takes too long, it can delay other important tasks.

You can think of the ISR as an emergency responder, it comes in, handles the situation, and leaves quickly so normal operations can continue.

Types of Interrupts in Embedded Systems

Interrupts are generally categorised based on their source, and understanding this helps in designing better systems.

Hardware Interrupts

Hardware interrupts are triggered by external devices. These could include sensors, timers, buttons, or communication modules.

For instance, when you press a button on a remote, a hardware interrupt is generated. The processor immediately reacts to that input.

These interrupts are unpredictable and can occur at any time, which makes them asynchronous.

Software Interrupts

Software interrupts are generated internally by the program itself. These are used for tasks like system calls, error handling, or requesting services from the operating system.

Unlike hardware interrupts, software interrupts occur in a controlled manner during program execution. To understand the difference between these two with real-life examples, you can explore a detailed guide on the Key Difference Between Hardware and Software Interrupts

Interrupts vs Polling: A Practical Perspective

The difference between interrupts and polling becomes clearer when you look at real-world behaviour.

Polling is like repeatedly checking your phone for messages every few seconds.

Interrupts are like receiving a notification the moment a message arrives.

With polling, the system is constantly busy, even when nothing happens. With interrupts, the system is efficient and reacts only when required.

This is why interrupts are widely used in embedded systems, especially where performance and power efficiency matter.

Triggering Mechanisms of Interrupts

Interrupts can also be classified based on how they are triggered.

In some cases, an interrupt is triggered when a signal changes, for example, when a button goes from unpressed to pressed. This is known as an edge-triggered interrupt.

In other cases, the interrupt remains active as long as the signal stays at a certain level. This is called a level-triggered interrupt.

Edge-triggered interrupts are often preferred because they reduce the chances of repeated or unwanted triggering.

Interrupt Latency: The Speed Factor

Interrupt latency refers to the time it takes for the system to respond to an interrupt.

This includes the time taken to stop the current task, save the context, and start executing the ISR.

In systems like medical devices or automotive safety systems, even a tiny delay can have serious consequences. That’s why minimising latency is extremely important in embedded design.

Conclusion

Interrupts are a fundamental concept that makes embedded systems responsive, efficient, and intelligent.

They allow systems to react instantly to events without wasting time or resources. By pausing ongoing tasks, handling urgent events, and then resuming normal execution, interrupts ensure smooth operation.

From simple devices like remote controls to complex systems like autonomous vehicles, interrupts play a crucial role in making everything work seamlessly.

If you truly understand interrupts, you are not just learning a concept; you are understanding how modern technology responds to the world in real time.

--

--

Theiotacademy
Theiotacademy

Written by Theiotacademy

The IoT Academy is a trusted platform for learning emerging tech like AI, ML, IoT, and Data Science with hands-on training for career growth