ISR-to-Task Communication Made Simple: Queues vs Semaphores
In every real-time embedded system, interrupts are where the action begins. Whether it’s a camera sensor signaling a frame ready, a CAN packet arriving, or a GPIO line pulsing high, the first code to run is almost always the Interrupt Service Routine (ISR).
But ISRs should be fast, lightweight, and non-blocking — they exist to acknowledge the event, not to process it. That means most real work must be handed off to background tasks.Concept: What Is ISR-Task Communication?
Here’s where many firmware engineers hit a design fork:
Should you use a flag, a semaphore, or a queue to communicate between the ISR and the task?
Choosing the wrong mechanism can lead to lost events, race conditions, or jitter spikes. This guide breaks down how to make that choice confidently — with clear examples, visual flows, and practical testing advice you can apply right away.
Concept: What Is ISR–Task Communication?
An ISR runs in response to asynchronous hardware events — timers, sensors, DMA transfers, or GPIO triggers. Its purpose is to:
- Capture or acknowledge the event.
- Notify the system that something needs to be handled.
- Exit quickly so other interrupts aren’t blocked.
To hand off work, ISRs typically use one of three communication tools:

Each has its place — but picking the right one depends on how often events occur, and whether data must cross the ISR boundary.
Why This Choice Really Matters
In embedded systems, efficiency and determinism rule everything.
- Use flags for a high-frequency signal? You might miss interrupts.
- Use semaphores for data? You might lose payload context.
- Use queues incorrectly? You might overflow memory or increase latency.
The right communication pattern is not about API familiarity — it’s about architectural fit.
Let’s break down when and why to use each.
Flags — When “Something Happened” Is Enough
Best for: Rare or low-impact events
Typical APIs: Volatile variables, event bits
How It Works:
An ISR sets a volatile flag or RTOS event bit. The main loop or background task periodically checks and clears it.
volatile bool sensor_triggered = false;
void EXTI_IRQHandler(void) {
sensor_triggered = true;
}
void loop(void) {
if (sensor_triggered) {
sensor_triggered = false;
process_sensor();
}
}Pros:
- Fast and simple — no RTOS dependency.
- Great for low-frequency or diagnostic events.
Pitfalls:
- Polling wastes CPU cycles.
- Misses rapid back-to-back interrupts.
- Difficult to scale with multiple ISR sources.
Use flags only when lost events are acceptable. For anything periodic or bursty, move to semaphores or queues.
Semaphores — When You Need to Wake a Task
Best for: Event-driven task activation, no data transfer
Typical API: xSemaphoreGiveFromISR(), xSemaphoreTake()
How It Works:
The ISR “gives” a semaphore to signal the task. The task, blocked on xSemaphoreTake(), wakes up immediately and handles the work.
void TimerISR(void) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(periodicSem, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
void vPeriodicTask(void *arg) {
for (;;) {
if (xSemaphoreTake(periodicSem, portMAX_DELAY)) {
run_periodic_task();
}
}
}Pros:
- Perfect for time-sensitive triggers or edge counters.
- Task wakes immediately — no polling required.
- Supports counting behavior for rapid interrupts.
Pitfalls:
- Not suitable for transferring data.
- Signals can be lost if no task is waiting.
Rule of Thumb: Use semaphores when you care that something happened, not what happened.
Queues — When Data Must Cross Contexts
Best for: Passing event data or messages from ISR → Task
Typical API: xQueueSendFromISR(), xQueueReceive()
How It Works:
The ISR pushes event structures or message pointers into a thread-safe queue. The task dequeues and processes them sequentially.
typedef struct {
uint8_t sensor_id;
uint16_t value;
} SensorEvent;
void ADC_IRQHandler(void) {
SensorEvent evt = { .sensor_id = 2, .value = ADC1->DR };
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendFromISR(sensorQueue, &evt, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
void vSensorTask(void *arg) {
SensorEvent evt;
for (;;) {
if (xQueueReceive(sensorQueue, &evt, portMAX_DELAY)) {
process_sensor_event(&evt);
}
}
}Pros:
- Lossless, ordered data transfer.
- Handles bursts gracefully (if queue sized properly).
- Works across multiple ISR sources.
Pitfalls:
- Needs careful queue size tuning.
- Slightly higher overhead vs semaphores.
Rule of Thumb: Use queues when you must transfer data or preserve event order.

Three standard ISR signaling paths — choose based on event frequency and data need.
Design Patterns: Practical Scenarios
1. Interrupt-Driven Data Capture
A sensor signals data ready via an interrupt.
- ISR pushes event info into a queue.
- Task dequeues and handles bulk transfer.
- Shared bus access protected by a mutex.
2. Timer-Based Task Activation
A hardware timer triggers work every 10 ms.
- ISR gives a binary semaphore.
- Task wakes, executes work, and waits again.
- Ideal for time-deterministic periodic loops.
3. Multi source Event Dispatcher
Multiple peripherals trigger different actions.
- Each ISR pushes a context object into a shared queue.
- A dispatcher task routes commands to target modules.
Visual 2: Decision Matrix
Implementation & Debug Tips
- Use ISR-safe APIs only (
FromISRversions). - Never block inside an ISR. Signal and exit fast.
- Size your queue for at least
event_rate × worst_case_processing_delay. - Stress-test under burst load to catch overflow or jitter early.
- Use diagnostics (
uxQueueMessagesWaiting()) to tune behavior. - Log semaphore counts or queue drops for visibility.
Common Pitfalls and Fixes
| Pitfall | Root Cause | Fix |
|---|---|---|
| Missed events | Polling or flag misuse | Use counting semaphores or queues |
| Data corruption | Shared buffer access in ISR | Move data handling to task, protect with mutex |
| Task starvation | Task too low priority | Adjust task priorities and preemption settings |
| Overflowed queue | Under-sized or unbounded burst | Tune queue depth, add diagnostic monitoring |
Hoomanely Context
At Hoomanely, we build embedded systems that connect sensing, computation, and control at the edge.
Reliable ISR–task communication is the foundation of that performance — ensuring data integrity, responsiveness, and long-term stability across products.
The patterns described here reflect the same engineering discipline used in Hoomanely’s firmware architecture, where predictable ISR handoff enables scalable, resilient, and maintainable real-time systems.
Key Takeaways
- Flags: Simple, fast, but lossy — use only for low-impact signals.
- Semaphores: Ideal for signaling events, not for carrying data.
- Queues: Safest and most flexible for structured, ordered event transfer.
- Always use ISR-safe APIs and keep ISRs minimal.
- Tune queue depth and task priorities under real workloads.
- Protect shared resources only in task context, never inside ISRs.
Get this right once — and you’ll eliminate half your future debugging sessions.
Author’s Note
This post was written from hands-on RTOS experience — across FreeRTOS, Zephyr, and bare-metal systems — to help engineers bridge the invisible gap between interrupts and tasks.
A solid ISR–task design is not just a pattern; it’s a performance enabler.
If this guide helps you architect cleaner, safer firmware, share it with your team — because the best real-time systems start with the right boundaries.