Introduction
Embedded systems engineering is one of the most technically demanding—and most specialized—disciplines in software. It powers automotive ECUs, medical devices, industrial controllers, and consumer IoT products. Companies like Tesla, SpaceX, Qualcomm, Texas Instruments, and ARM are always hiring embedded engineers, and the interview process tests a depth of low-level knowledge that most software engineers never acquire.
This guide covers the questions that appear in embedded systems interviews at top hardware and automotive companies.
C Programming for Embedded Systems
Q: What is the volatile keyword in C and why is it critical in embedded programming?
volatile tells the compiler that a variable's value can change at any time without any action taken by the code the compiler knows about. This prevents the compiler from optimizing away reads/writes to that variable.
Critical uses in embedded systems:
- Memory-mapped hardware registers: The hardware can change register values independently of software.
// Without volatile, the compiler might cache the value in a register
// and never re-read it from the actual hardware address
volatile uint32_t *const UART_STATUS = (volatile uint32_t *)0x40001000;
while (!(*UART_STATUS & 0x01)) {
// Wait for TX ready bit-must re-read each iteration
}
Variables modified in ISRs: An Interrupt Service Routine can modify a variable between two instructions in the main loop. Without
volatile, the compiler may cache the old value.Shared variables in multi-threaded/RTOS contexts: Always use
volatilefor variables shared between tasks (in addition to proper synchronization primitives).
Q: What is the difference between const and volatile? Can a variable be both?
consttells the compiler the program cannot modify this variable.volatiletells the compiler the variable can be modified by external factors—don't optimize reads.
Yes, a variable can be const volatile — a hardware status register that the software reads but never writes:
const volatile uint32_t *const STATUS_REG = (const volatile uint32_t *)0x40001000;
// const: this code won't write to it
// volatile: the hardware changes it; don't cache reads
Q: Explain bit manipulation for setting, clearing, toggling, and checking a bit.
#define BIT(n) (1U << (n))
// Set bit 3
register |= BIT(3);
// Clear bit 3
register &= ~BIT(3);
// Toggle bit 3
register ^= BIT(3);
// Check bit 3
if (register & BIT(3)) {
// bit is set
}
// Set bits 4-7 (a nibble) while preserving others
register = (register & ~(0xF << 4)) | (value & 0xF) << 4;
These patterns appear constantly in embedded code—in GPIO configuration, peripheral control registers, and status flag polling.
Q: What is the difference between little-endian and big-endian? How do you detect it programmatically?
- Little-endian: Least significant byte stored at the lowest address. (Most modern CPUs: x86, ARM Cortex-M)
- Big-endian: Most significant byte stored at the lowest address. (Network byte order, some DSPs, SPARC)
Detection:
int is_little_endian(void) {
uint16_t test = 0x0001;
return *(uint8_t *)&test == 0x01; // 1 if little-endian
}
Endianness matters when: sending data over a network (use htonl/ntohl), reading binary file formats, interfacing with external hardware peripherals, or parsing multi-byte registers from sensors.
Interrupts & ISRs
Q: What are the key rules for writing a safe Interrupt Service Routine?
Keep it short: ISRs should do the minimum possible—set a flag, copy data into a buffer—and return immediately. Long ISRs prevent other interrupts from being serviced (priority inversion, missed interrupts).
Do not call blocking functions: Never call
printf, dynamic memory allocation, or any RTOS blocking call (mutex lock, semaphore wait) from an ISR.Use volatile for shared variables: Variables shared between ISR and main loop must be
volatile.Disable interrupts around non-atomic operations: A multi-byte variable update is not atomic. Protect read-modify-write sequences:
volatile uint32_t counter = 0;
// In main loop - protect against ISR corruption
uint32_t saved_primask = __get_PRIMASK();
__disable_irq();
uint32_t snapshot = counter;
__set_PRIMASK(saved_primask);
- Clear the interrupt flag: Most peripherals require you to explicitly clear the pending interrupt flag before returning, or the ISR will fire again immediately.
Q: What is interrupt latency and what factors affect it?
Interrupt latency is the time between an interrupt event occurring and the first instruction of the ISR executing.
Factors:
- CPU pipeline: Time to complete the current instruction and save context.
- Interrupt priority: Higher-priority interrupts preempt lower-priority ones (nested interrupts).
- Critical sections: Code with interrupts disabled increases worst-case latency.
- Cache misses: On Cortex-M processors with cache, fetching ISR code from flash takes longer than SRAM.
On ARM Cortex-M (no cache, zero-wait-state SRAM): typical ISR entry latency is 12 clock cycles. On complex processors with cache and pipelines: hundreds of cycles, making RTOS systems with real-time guarantees much harder.
Real-Time Operating Systems (RTOS)
Q: What is an RTOS and how does it differ from a general-purpose OS?
An RTOS (Real-Time Operating System) provides deterministic scheduling guarantees. Tasks must complete within defined time windows (deadlines).
Key differences from a general-purpose OS:
- Preemptive priority scheduling: The highest-priority ready task always runs. Not fair scheduling.
- Deterministic context switch time: Guaranteed, typically microseconds.
- Minimal footprint: FreeRTOS kernel can fit in 6KB of flash.
- No virtual memory, no file system by default: Direct hardware access.
Common RTOSes: FreeRTOS (open source, most widely used), Zephyr, VxWorks (aerospace/defense), QNX (automotive), ThreadX (now Azure RTOS).
Q: What is priority inversion and how does FreeRTOS solve it?
Priority inversion occurs when a high-priority task is blocked waiting for a resource held by a low-priority task—and a medium-priority task preempts the low-priority task, starving the high-priority task indefinitely.
Example: Task H (high) blocks waiting for mutex held by Task L (low). Task M (medium) preempts Task L. Task H is now stuck behind Task M, which has lower priority.
Solution: Priority inheritance. When Task L holds a mutex that Task H is waiting for, Task L's priority is temporarily elevated to Task H's priority. Task L finishes quickly and releases the mutex, restoring its original priority.
FreeRTOS implements priority inheritance in its mutex implementation (not binary semaphores—an important distinction for interviews).
Memory Management
Q: Why is dynamic memory allocation (malloc/free) often avoided in embedded systems?
Fragmentation: After many alloc/free cycles, the heap becomes fragmented—many small free blocks, but no contiguous block large enough for a new allocation. In a long-running system, this eventually causes allocation failure.
Non-deterministic timing:
mallocexecution time varies depending on heap state. Real-time systems need deterministic behavior.No memory protection: If code writes beyond an allocated buffer, it corrupts other allocations. Bare-metal systems have no MMU protection.
Alternatives:
- Static allocation: Allocate all memory at startup, never free. Maximum determinism.
- Memory pools: Pre-allocate a pool of fixed-size blocks. O(1) allocation and deallocation, no fragmentation.
- Stack allocation: Local variables on the stack are automatically reclaimed. Fast and deterministic.
Q: What is a stack overflow and how do you detect it in an embedded system?
A stack overflow occurs when function call depth exceeds the allocated stack size—overwriting adjacent memory (often critical data or code). This causes unpredictable behavior, not a clean crash.
Detection strategies:
- Stack canaries: Fill the stack region with a known pattern at startup. Periodically check if the pattern at the stack bottom has been overwritten.
- MPU (Memory Protection Unit): Configure a guard region at the stack bottom. Any write triggers a fault exception.
- FreeRTOS stack high watermark:
uxTaskGetStackHighWaterMark()returns the minimum remaining stack at any point in the task's history. Monitor and alarm if below a safety threshold.
Communication Protocols
Q: Compare SPI, I2C, and UART for embedded peripherals.
| UART | SPI | I2C | |
|---|---|---|---|
| Wires | 2 (TX, RX) | 4+ (MOSI, MISO, SCK, CS×N) | 2 (SDA, SCL) |
| Speed | ~1 Mbps | Up to hundreds of MHz | Up to 5 MHz (HS mode) |
| Multi-device | No (point-to-point) | Yes (chip select per device) | Yes (7-bit address, 127 devices) |
| Full duplex | Yes | Yes | No (half duplex) |
| Complexity | Lowest | Low | Medium (ACK/NACK, arbitration) |
| Common use | Debug, GPS, BLE modules | Flash memory, displays, ADCs | Sensors, RTC, EEPROMs |
Summary
Embedded systems interviews test a depth of knowledge that spans hardware, operating systems, and low-level C—requiring years of hands-on experience to develop truly. If you can articulate volatile semantics, ISR rules, priority inversion, and stack overflow detection clearly and confidently, you will stand out from the majority of candidates.
Practice technical interview questions with Interview Masters →
