Implementing Deterministic Interrupt Service Routines in Embedded C
Learn how to implement deterministic Interrupt Service Routines (ISRs) in embedded C, focusing on volatile variables, critical sections, and avoiding non-reentrant functions.
09 Feb 2026, 20:18 UTC

The Challenge of Asynchronous Event Handling
In embedded systems, hardware events—such as a timer expiring, a button press, or a data packet arriving—occur independently of the main program flow. Handling these via polling wastes CPU cycles and increases latency. Interrupt Service Routines (ISRs) solve this by pausing the main execution to handle the event immediately. However, poorly written ISRs introduce non-deterministic behavior, race conditions, and system crashes.
The goal is to move the system from a state of "waiting for data" to "reacting to data" without blocking the rest of the application or corrupting shared memory.
Prerequisites for ISR Implementation
- Hardware Vector Table: Access to the MCU's interrupt vector table, which maps hardware signals to specific function addresses.
- Compiler Support: A C compiler that supports the
volatilequalifier and allows function definitions without arguments or return values. - Interrupt Controller Configuration: Knowledge of the Nested Vectored Interrupt Controller (NVIC) or Programmable Interrupt Controller (PIC) to set priority levels.
Step-by-Step Implementation
1. Define Shared State with Volatile
When a variable is modified inside an ISR and read in the main loop, the compiler may optimize the main loop by caching the variable in a register, unaware that the hardware can change the value at any time. The volatile keyword forces the compiler to read the value from RAM every time it is accessed.
// Global variable shared between ISR and main loop
volatile uint32_t event_counter = 0;
volatile bool data_ready = false;
2. Write the ISR Handler
The ISR must be lean. Avoid complex logic, floating-point math, or blocking calls. The primary objective is to acknowledge the hardware and signal the main loop to perform the heavy lifting.
void Timer_Handler(void) {
// 1. Clear the hardware interrupt flag to prevent immediate re-triggering
TIMER_REG->STATUS &= ~TIMER_INT_FLAG;
// 2. Perform minimal atomic operation
event_counter++;
data_ready = true;
// 3. Exit quickly to return control to the main loop
}
3. Implement Critical Sections in the Main Loop
On 8-bit or 16-bit architectures, reading a 32-bit integer is not atomic (it takes multiple clock cycles). If an interrupt occurs halfway through the read, the resulting value will be corrupted. You must disable interrupts during the access.
void main_loop(void) {
while(1) {
uint32_t local_count;
// Start Critical Section
disable_global_interrupts();
local_count = event_counter;
data_ready = false;
enable_global_interrupts();
// End Critical Section
if (local_count > 100) {
process_data(local_count);
}
}
}
Engineering Constraints and Risks
Non-Reentrant Functions
Never call functions that maintain internal state or use locks inside an ISR. Examples include printf(), malloc(), or sprintf(). These are non-reentrant; if the main loop is interrupted while inside printf() and the ISR also calls printf(), the internal buffer may be corrupted, leading to a deadlock or hard fault.
Stack Depth and Overflow
ISRs often use a separate interrupt stack or push the current context onto the main stack. Declaring large local arrays inside an ISR can lead to a stack overflow, which typically manifests as random memory corruption or a system reset.
Verification and Diagnostics
Latency Measurement
To verify the responsiveness of your ISR, use a logic analyzer or oscilloscope:
- Set a GPIO pin HIGH at the very beginning of the ISR.
- Set the GPIO pin LOW at the end of the ISR.
- Trigger the oscilloscope on the hardware event signal.
- Measure the time delta between the trigger and the GPIO rising edge (Latency) and the width of the pulse (Execution Time).
Stability Stress Test
Configure the interrupt source to trigger at its maximum rated frequency. If the system hangs or the main loop stops executing, the ISR is likely taking longer to execute than the interval between interrupts, causing "interrupt starvation."
Summary Comparison: Polling vs. ISR
| Metric | Polling | ISR-Based |
|---|---|---|
| CPU Utilization | High (Constant checking) | Low (Event-driven) |
| Response Time | Variable (Depends on loop length) | Deterministic (Low latency) |
| Complexity | Low | Medium (Requires concurrency mgmt) |
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.