There are no items in your cart
Add More
Add More
| Item Details | Price | ||
|---|---|---|---|
If you are preparing for an embedded systems interview at companies like Qualcomm, Texas Instruments, Broadcom, or any product company working on microcontrollers, RTOS questions are almost guaranteed. This post covers the top 30 RTOS interview questions with clear answers and code examples — covering FreeRTOS and Zephyr.
An RTOS (Real-Time Operating System) is an operating system designed to execute tasks within guaranteed time bounds. The key difference is determinism — the system must respond to events within a defined deadline, every single time.
| Feature | RTOS | General-Purpose OS |
|---|---|---|
| Response time | Deterministic (microseconds) | Non-deterministic |
| Scheduling | Priority-based, preemptive | Fair-share, throughput-focused |
| Footprint | Small (KB to few MB) | Large (hundreds of MB to GB) |
| Examples | FreeRTOS, Zephyr, VxWorks | Linux, Windows, macOS |
Hard RTOS — missing a deadline is a system failure (pacemaker, airbag).
Soft RTOS — missing a deadline degrades quality but is not catastrophic (video streaming).
Every RTOS provides these core services:
A task in FreeRTOS is an independent thread of execution with its own stack, priority, and state. Each task runs as if it owns the CPU — the scheduler switches between them transparently.
// Task function signature
void vMyTask(void *pvParameters) {
for (;;) {
// task work here
vTaskDelay(pdMS_TO_TICKS(100)); // yield for 100ms
}
}
// Create task
xTaskCreate(
vMyTask, // function
"MyTask", // name (debug only)
128, // stack depth in words
NULL, // parameter passed to task
2, // priority (higher number = higher priority)
NULL // task handle (optional)
);
// Start scheduler — never returns
vTaskStartScheduler();
🚀 Bootcamp 4.0 — Early Bird Opens April 25
Bootcamp 4.0 covers everything you need to crack embedded system interviews at top companies:
Join the Early Bird WhatsApp group — get first access to enrollment and special pricing before anyone else.
💬 Join Bootcamp 4.0 Early Bird Group✅ Free to join · 224+ engineers already inside · Early bird opens April 25
xTaskCreate()
|
▼
READY ◄─────────────────────────────┐
| |
Highest priority selected |
| Task preempted /
▼ time slice ends
RUNNING ──────────────────────────────┘
|
vTaskSuspend() / waiting vTaskDelete()
for event/delay/semaphore |
| ▼
BLOCKED DELETED
|
Event received / delay expired
|
READY
vTaskSuspend(), only resumes via vTaskResume()The tick is a periodic timer interrupt that drives the FreeRTOS scheduler. Configured via configTICK_RATE_HZ in FreeRTOSConfig.h.
// FreeRTOSConfig.h
#define configTICK_RATE_HZ 1000 // 1ms tick
// Convert ms to ticks correctly — never hardcode ticks
vTaskDelay(pdMS_TO_TICKS(50)); // delay 50ms — tick-rate independent
Why it matters: If configTICK_RATE_HZ = 1000, the minimum delay resolution is 1ms. Using pdMS_TO_TICKS() makes your code portable if you change the tick rate.
FreeRTOS uses a fixed-priority preemptive scheduler by default. The rules are:
1. The highest priority ready task always runs
2. If two tasks share the same priority, they share CPU time (round-robin with time slicing)
3. A running task is preempted the moment a higher-priority task becomes ready
4. Tasks yield the CPU by entering a blocked state (delay, waiting for queue/semaphore)
// configUSE_PREEMPTION = 1 → preemptive (default, recommended)
// configUSE_TIME_SLICING = 1 → round-robin among equal-priority tasks
FreeRTOS automatically creates an Idle Task at priority 0 (lowest). It runs whenever no other task is ready.
Critical rule: At least one task must always be able to run. Since the idle task runs at priority 0, no task should block forever at priority 0 — otherwise the CPU has nothing to execute.
The idle task also performs memory cleanup — when a task is deleted, its stack and TCB (Task Control Block) are only freed when the idle task runs.
// Hook function — runs inside idle task (optional)
void vApplicationIdleHook(void) {
// Put CPU to sleep to save power
__WFI(); // Wait For Interrupt — ARM low-power instruction
}
Both delay a task, but they handle timing differently.
// vTaskDelay — relative delay from when it's called
// If task takes variable time, period DRIFTS over time
void vTask_Relative(void *p) {
for (;;) {
do_work(); // takes variable time
vTaskDelay(pdMS_TO_TICKS(100)); // 100ms AFTER do_work finishes
}
}
// vTaskDelayUntil — absolute period, NO drift
// Use this for fixed-frequency control loops
void vTask_Absolute(void *p) {
TickType_t xLastWake = xTaskGetTickCount();
for (;;) {
vTaskDelayUntil(&xLastWake, pdMS_TO_TICKS(100)); // exactly 100ms period
do_work();
}
}
Interview answer: Use vTaskDelayUntil for periodic tasks like sensor sampling or PID control loops where exact timing matters. Use vTaskDelay for simple delays where exact period doesn't matter.
Preemptive: Scheduler can forcibly remove a running task from CPU when a higher-priority task becomes ready. Default in FreeRTOS. Better for real-time responsiveness.
Cooperative: A task runs until it voluntarily yields the CPU (by calling taskYIELD() or a blocking API). No forced preemption. Simpler but dangerous — a misbehaving task starves everything else.
// Force yield in cooperative mode or to give equal-priority tasks a chance:
taskYIELD();
// In FreeRTOSConfig.h:
#define configUSE_PREEMPTION 1 // preemptive (recommended)
// #define configUSE_PREEMPTION 0 // cooperative
| Feature | FreeRTOS | Zephyr |
|---|---|---|
| Scheduling | Fixed-priority preemptive | Fixed-priority preemptive + optional round-robin |
| Thread types | Tasks only | Cooperative threads + preemptive threads |
| Priority range | 0 (lowest) to configMAX_PRIORITIES-1 | -CONFIG_NUM_COOP_PRIORITIES to CONFIG_NUM_PREEMPT_PRIORITIES |
| Tickless idle | Supported | Supported |
| SMP support | FreeRTOS SMP (limited) | Full SMP support |
In Zephyr, cooperative threads (negative priority) run without preemption by other threads of same type — they must explicitly yield. Preemptive threads (non-negative priority) work like FreeRTOS tasks.
// Zephyr thread creation
K_THREAD_DEFINE(my_tid, 1024, // stack size
my_thread_fn, // entry function
NULL, NULL, NULL, // parameters
5, // priority (preemptive)
0, 0); // flags, delay
Each task in FreeRTOS has a fixed-size stack. If a task uses more stack than allocated (deep recursion, large local arrays), it corrupts adjacent memory — a very hard-to-debug bug.
// FreeRTOSConfig.h — enable stack overflow checking
#define configCHECK_FOR_STACK_OVERFLOW 2 // method 2 = most thorough
// Hook called when overflow detected
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) {
// Log the task name, halt system
printf("STACK OVERFLOW in task: %s\n", pcTaskName);
taskDISABLE_INTERRUPTS();
for (;;);
}
// Check how much stack a task has left at runtime:
UBaseType_t uxHighWaterMark = uxTaskGetStackHighWaterMark(NULL);
// Returns minimum free stack words remaining — if close to 0, increase stack size
Priority inversion occurs when a high-priority task is indirectly blocked by a low-priority task holding a resource the high-priority task needs.
Classic example — Mars Pathfinder (1997):
Timeline:
L acquires mutex ──► M preempts L ──► H tries mutex, blocks
H is now waiting on L, L can't run because M is running
= Priority Inversion!
This is one of the most common RTOS interview questions.
| Feature | Mutex | Binary Semaphore |
|---|---|---|
| Purpose | Mutual exclusion (protect shared resource) | Signalling between tasks |
| Ownership | Yes — only the task that takes it can give it | No ownership |
| Priority inheritance | Yes (prevents priority inversion) | No |
| Use from ISR | Not recommended | Yes — xSemaphoreGiveFromISR() |
// Mutex — protect shared resource
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();
void vTask_Writer(void *p) {
if (xSemaphoreTake(xMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
// Critical section — safe to access shared resource
update_shared_buffer();
xSemaphoreGive(xMutex);
}
}
// Binary Semaphore — signal from ISR to task
SemaphoreHandle_t xSem = xSemaphoreCreateBinary();
void UART_IRQHandler(void) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(xSem, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
void vTask_UART(void *p) {
for (;;) {
xSemaphoreTake(xSem, portMAX_DELAY); // wait for ISR signal
process_uart_data();
}
}
Priority inheritance is a mechanism to reduce priority inversion. When a high-priority task blocks waiting for a mutex held by a low-priority task, the RTOS temporarily boosts the low-priority task's priority to match the high-priority task.
Without priority inheritance: With priority inheritance:
H blocked waiting for mutex H blocked waiting for mutex
M preempts L → L starves L boosted to H's priority
H waits indefinitely L finishes, releases mutex
L drops back to original priority
H acquires mutex, runs
FreeRTOS mutexes (xSemaphoreCreateMutex()) automatically implement priority inheritance. Binary semaphores do not.
// Always use Mutex (not binary semaphore) when protecting shared resources
// accessed by tasks of different priorities
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex(); // has priority inheritance
SemaphoreHandle_t xSem = xSemaphoreCreateBinary(); // NO priority inheritance
A counting semaphore maintains a count. Give increments the count, Take decrements it. Blocks when count reaches zero.
Use cases: Track available resources (buffer slots, connection pool), count events from ISR.
// Example: 5 slots in a resource pool
SemaphoreHandle_t xPool = xSemaphoreCreateCounting(5, 5); // max=5, initial=5
void vTask_Consumer(void *p) {
for (;;) {
xSemaphoreTake(xPool, portMAX_DELAY); // wait for a slot
use_resource();
xSemaphoreGive(xPool); // return slot
}
}
// Count events from ISR — initial count = 0
SemaphoreHandle_t xEventCount = xSemaphoreCreateCounting(10, 0);
// ISR calls xSemaphoreGiveFromISR() for each event
// Task calls xSemaphoreTake() to process each event one by one
A regular mutex will deadlock if the same task tries to take it twice. A recursive mutex allows the same task to take it multiple times — it just increments an internal counter and requires an equal number of Give calls to release.
SemaphoreHandle_t xRecMutex = xSemaphoreCreateRecursiveMutex();
void vTask(void *p) {
// Safe to call nested functions that also take the same mutex
xSemaphoreTakeRecursive(xRecMutex, portMAX_DELAY);
xSemaphoreTakeRecursive(xRecMutex, portMAX_DELAY); // would deadlock with normal mutex!
do_work();
xSemaphoreGiveRecursive(xRecMutex);
xSemaphoreGiveRecursive(xRecMutex);
}
When to use: When the same task may re-enter a critical section — for example, a library function that takes a mutex internally, called from code that already holds the same mutex.
A deadlock occurs when two or more tasks each hold a resource and wait for a resource held by the other — neither can proceed.
Task A holds Mutex1, waits for Mutex2
Task B holds Mutex2, waits for Mutex1
→ Both blocked forever = Deadlock
Prevention strategies:
// 1. Always acquire mutexes in the same fixed order across all tasks
// WRONG:
// Task A: take(M1) then take(M2)
// Task B: take(M2) then take(M1) ← different order = deadlock risk
// CORRECT: both tasks always take M1 first, then M2
// 2. Use timeout instead of portMAX_DELAY
if (xSemaphoreTake(xMutex, pdMS_TO_TICKS(200)) == pdFALSE) {
// Could not acquire — handle gracefully, log, retry
log_error("Mutex timeout — possible deadlock");
return ERROR;
}
// 3. Never hold a mutex while waiting for another blocking operation
You cannot use a mutex from an ISR in FreeRTOS. Mutexes have priority inheritance which requires task context. ISRs are not tasks.
From an ISR, you must use:
xSemaphoreGiveFromISR() — for binary/counting semaphores onlyxQueueSendFromISR() — for queuesxTaskNotifyFromISR() — task notifications (fastest, lowest overhead)// WRONG — never do this from an ISR:
// xSemaphoreTake(xMutex, portMAX_DELAY); // CRASH
// CORRECT — use task notification from ISR:
void TIMER_IRQHandler(void) {
BaseType_t xWoken = pdFALSE;
vTaskNotifyGiveFromISR(xTaskHandle, &xWoken);
portYIELD_FROM_ISR(xWoken);
}
void vTask(void *p) {
for (;;) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // wait for ISR signal
process_data();
}
}
A queue is a thread-safe FIFO buffer for passing data between tasks or from ISR to task. It copies data by value — the sender's copy is independent of the receiver's.
// Create queue — holds 10 items of type uint32_t
QueueHandle_t xQueue = xQueueCreate(10, sizeof(uint32_t));
// Sender task
void vSender(void *p) {
uint32_t value = 42;
xQueueSend(xQueue, &value, pdMS_TO_TICKS(100)); // blocks 100ms if full
}
// Receiver task
void vReceiver(void *p) {
uint32_t rxVal;
for (;;) {
if (xQueueReceive(xQueue, &rxVal, portMAX_DELAY) == pdTRUE) {
printf("Received: %lu\n", rxVal);
}
}
}
// From ISR — use ISR-safe version:
void IRQHandler(void) {
uint32_t data = read_hw_register();
BaseType_t xWoken = pdFALSE;
xQueueSendFromISR(xQueue, &data, &xWoken);
portYIELD_FROM_ISR(xWoken);
}
Task notifications are a lightweight mechanism to send a notification value directly to a specific task — without creating a separate synchronization object.
Why faster: No separate semaphore/queue object needed. The notification value is stored directly in the Task Control Block (TCB). Lower RAM usage and faster execution than binary semaphores.
// Notify from ISR (faster than semaphore):
void IRQHandler(void) {
BaseType_t xWoken = pdFALSE;
vTaskNotifyGiveFromISR(xtaskHandle, &xWoken);
portYIELD_FROM_ISR(xWoken);
}
// Wait for notification:
void vTask(void *p) {
for (;;) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // clears on exit
handle_event();
}
}
// Can also send a value:
xTaskNotify(xHandle, 0x05, eSetBits); // set bits in notification value
xTaskNotify(xHandle, 42, eSetValueWithOverwrite); // set value
An event group holds 24 bits (on 32-bit ports), each bit representing an event flag. Multiple tasks can wait on combinations of bits — AND (all bits) or OR (any bit).
EventGroupHandle_t xEvents = xEventGroupCreate();
#define WIFI_CONNECTED (1 << 0)
#define DATA_RECEIVED (1 << 1)
#define SENSOR_READY (1 << 2)
// Task waits until BOTH wifi connected AND data received:
EventBits_t bits = xEventGroupWaitBits(
xEvents,
WIFI_CONNECTED | DATA_RECEIVED, // bits to wait for
pdTRUE, // clear on exit
pdTRUE, // wait for ALL bits (AND)
portMAX_DELAY
);
// Another task sets a bit:
xEventGroupSetBits(xEvents, SENSOR_READY);
FreeRTOS provides 5 memory allocation schemes:
| Scheme | Description | Use Case |
|---|---|---|
| heap_1 | Allocates only, never frees | Simplest, safety-critical systems |
| heap_2 | Frees memory, no coalescence | Legacy, can fragment |
| heap_3 | Wraps standard malloc/free | When OS malloc is available |
| heap_4 | Frees + coalesces adjacent blocks | Most commonly used |
| heap_5 | heap_4 across multiple non-contiguous memory regions | Multiple RAM banks |
// Check free heap at runtime:
size_t freeHeap = xPortGetFreeHeapSize();
size_t minEver = xPortGetMinimumEverFreeHeapSize(); // watermark
// FreeRTOSConfig.h — set heap size:
#define configTOTAL_HEAP_SIZE (16 * 1024) // 16KB heap
Software timers call a callback function after a specified period. They run in the context of the Timer Service Task (a dedicated background task) — not in an ISR, not in the calling task.
// One-shot timer — fires once
TimerHandle_t xTimer = xTimerCreate(
"WatchdogTimer",
pdMS_TO_TICKS(5000), // 5 second timeout
pdFALSE, // one-shot (pdTRUE = periodic)
(void*)0, // timer ID
vTimerCallback // callback function
);
void vTimerCallback(TimerHandle_t xTimer) {
// Called in Timer Service Task context — can use queues/semaphores
// CANNOT block or delay here
printf("Timer expired!\n");
}
xTimerStart(xTimer, 0); // start timer
xTimerReset(xTimer, 0); // restart/pet the watchdog
xTimerStop(xTimer, 0); // stop timer
| Feature | FreeRTOS | Zephyr |
|---|---|---|
| License | MIT | Apache 2.0 |
| Footprint | ~5-10KB ROM | ~8KB+ (more features = more) |
| Build system | Makefile / IDE | CMake + west |
| Device drivers | User-provided | Built-in driver model |
| Networking | FreeRTOS+TCP (separate) | Native (BT, WiFi, Ethernet) |
| SMP support | Limited (v10+) | Full multi-core SMP |
| Community | Very large, Arduino ecosystem | Growing, Linux Foundation backed |
| Best for | Simple MCUs, low footprint | Complex SoCs, Linux-adjacent stack |
#include <zephyr/kernel.h>
// Define stack statically (recommended in Zephyr):
K_THREAD_STACK_DEFINE(my_stack, 1024);
struct k_thread my_thread_data;
void my_thread_fn(void *p1, void *p2, void *p3) {
while (1) {
printk("Thread running\n");
k_msleep(500); // equivalent of vTaskDelay
}
}
// Create thread at runtime:
k_tid_t tid = k_thread_create(&my_thread_data, my_stack,
K_THREAD_STACK_SIZEOF(my_stack),
my_thread_fn, NULL, NULL, NULL,
5, // priority
0, K_NO_WAIT);
// Zephyr Mutex:
K_MUTEX_DEFINE(my_mutex);
k_mutex_lock(&my_mutex, K_FOREVER);
// critical section
k_mutex_unlock(&my_mutex);
// Zephyr Semaphore:
K_SEM_DEFINE(my_sem, 0, 1); // initial=0, max=1
k_sem_give(&my_sem); // from ISR or task
k_sem_take(&my_sem, K_FOREVER);
In Zephyr, ISRs are registered either statically (at compile time) or dynamically. Zephyr provides ISR-safe kernel APIs with the _isr suffix pattern or context detection.
// Static ISR registration in Zephyr:
void my_isr(const struct device *dev, struct gpio_callback *cb, uint32_t pins) {
// Run in ISR context
k_sem_give(&my_sem); // Zephyr detects ISR context automatically
}
// FreeRTOS requires separate FromISR() functions:
// xSemaphoreGiveFromISR() vs xSemaphoreGive()
// Zephyr handles this transparently in most cases
// portMAX_DELAY — blocks forever until event occurs
// Use when: task has nothing else to do, MUST wait for this event
xSemaphoreTake(xMutex, portMAX_DELAY);
// Finite timeout — returns pdFALSE if timeout expires
// Use when: task should handle timeout gracefully, detect deadlocks,
// or retry with different logic
if (xSemaphoreTake(xMutex, pdMS_TO_TICKS(500)) == pdFALSE) {
log_warning("Mutex not acquired in 500ms — possible deadlock");
system_recovery();
}
Best practice for production firmware: Use finite timeouts everywhere. Feed a watchdog only when all expected events arrive within their expected time. portMAX_DELAY can hide bugs.
FreeRTOS on ARM Cortex-M uses this to define the highest interrupt priority from which FreeRTOS API functions can be called. Interrupts with priority numerically lower (higher hardware priority) than this value must never call FreeRTOS APIs.
// FreeRTOSConfig.h
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 5 // (on scale 0-15, 0=highest)
// Interrupt at priority 5, 6, 7... — CAN call xQueueSendFromISR(), etc.
// Interrupt at priority 0, 1, 2, 3, 4 — must NEVER call any FreeRTOS API
// (these are critical hardware interrupts that cannot be masked by FreeRTOS)
Common bug: Calling xQueueSendFromISR() from an interrupt with priority numerically lower than configMAX_SYSCALL_INTERRUPT_PRIORITY causes a hard fault or assertion.
// Pattern: each critical task "pets" a shared flag
// Watchdog task checks all flags periodically
#define NUM_TASKS 3
static volatile uint32_t task_alive_mask = 0;
static const uint32_t EXPECTED_MASK = 0x07; // bits for 3 tasks
// Each task sets its bit:
void vTask1(void *p) {
for (;;) {
do_work_1();
task_alive_mask |= (1 << 0); // I'm alive
vTaskDelay(pdMS_TO_TICKS(100));
}
}
// Watchdog task:
void vWatchdogTask(void *p) {
for (;;) {
vTaskDelay(pdMS_TO_TICKS(500)); // check every 500ms
if ((task_alive_mask & EXPECTED_MASK) != EXPECTED_MASK) {
// One or more tasks missed their deadline
log_error("Task failure detected — resetting");
NVIC_SystemReset(); // hardware reset
}
task_alive_mask = 0; // clear for next window
}
}
1. Stack overflow — Use configCHECK_FOR_STACK_OVERFLOW = 2 and check uxTaskGetStackHighWaterMark() regularly. Size stacks generously.
2. ISR calling non-ISR-safe API — Always use FromISR variants and pass pxHigherPriorityTaskWoken. Enable configASSERT in debug builds.
3. Forgetting to give a mutex — Use goto cleanup pattern or RAII-like wrappers. Finite timeouts catch this at runtime.
4. Missed portYIELD_FROM_ISR — The task woken by ISR won't run until the next tick if you skip this. Always call it after FromISR functions.
5. Deadlock — Always acquire mutexes in the same global order. Use finite timeouts. Log the task name and mutex name on timeout.
// Enable assertions in debug builds — catches most bugs early:
#define configASSERT(x) if ((x) == 0) { taskDISABLE_INTERRUPTS(); \
printf("ASSERT failed: %s:%d\n", __FILE__, __LINE__); \
for(;;); }
// Runtime task stats (debug only — uses significant CPU):
#define configUSE_TRACE_FACILITY 1
#define configGENERATE_RUN_TIME_STATS 1
// Then call: vTaskList(buffer) and vTaskGetRunTimeStats(buffer)
| Topic | Key Points |
|---|---|
| Scheduling | Fixed-priority preemptive, idle task at priority 0 |
| vTaskDelay vs vTaskDelayUntil | Relative delay vs absolute period (no drift) |
| Mutex vs Binary Semaphore | Mutex = ownership + priority inheritance; Semaphore = signalling |
| Priority Inversion | High task blocked by low task via shared mutex |
| Priority Inheritance | Mutex temporarily boosts low task priority |
| Deadlock | Fixed acquisition order + finite timeouts prevent it |
| ISR Safety | Use FromISR() functions + portYIELD_FROM_ISR() |
| Task Notifications | Fastest lightweight signalling — direct to TCB |
| heap_4 | Most common — allocate, free, coalesce adjacent blocks |
| Zephyr vs FreeRTOS | Zephyr = larger ecosystem + SMP; FreeRTOS = simpler + smaller |
These 30 questions cover what gets asked in 90% of embedded interviews. But understanding how to write production-quality RTOS code — structuring tasks, sizing stacks, designing ISR-to-task communication, and debugging in the field — takes hands-on practice with real hardware.
Embedded System Interview Prep Bootcamp 3.0 covers all of this with live sessions, code labs, and a focused interview preparation track.
*Published by EmbeddedShiksha | Follow for weekly embedded systems tips*
🚀 Bootcamp 4.0 — Early Bird
Join our exclusive Early Bird WhatsApp group — get first access to enrollment, special pricing, and curriculum updates for Bootcamp 4.0 before anyone else.
💬 Join Bootcamp 4.0 Early Bird Group✅ Free to join · 224+ engineers already inside · Early bird opens April 25