Back to Technology

Embedded Systems Interview Prep: Complete Guide from Fundamentals to System Design

April 1, 2026 Wasil Zafar 60 min read

Ace embedded systems interviews — C/C++ memory management, bit manipulation, volatile and const qualifiers, RTOS scheduling, ISR design, communication protocol debugging, oscilloscope usage, and system design problems with worked solutions.

Table of Contents

  1. The Embedded Interview Landscape
  2. C/C++ Fundamentals
  3. Memory Management
  4. Bit Manipulation
  5. volatile, const & Qualifiers
  6. RTOS Concepts
  7. ISR Design Patterns
  8. Communication Protocols
  9. Debugging & Tools
  10. System Design Problems
  11. Behavioral & Soft Skills
  12. Case Studies
  13. Practice Problems
  14. Interview Prep Sheet Generator
  15. Conclusion & Resources

The Embedded Interview Landscape

Key Insight: Embedded systems interviews differ fundamentally from general software engineering interviews. You are expected to understand hardware-software interaction, memory constraints, real-time guarantees, and low-level C/C++ in ways that web or mobile developers never encounter.

The embedded systems job market continues to grow as every industry from automotive to medical devices to consumer electronics depends on firmware engineers. Companies like Tesla, Qualcomm, Apple, Intel, Bosch, Medtronic, and thousands of startups actively recruit embedded engineers. The Bureau of Labor Statistics projects that embedded-related roles will grow 5% through 2032, but the supply of qualified candidates remains tight because the domain requires a unique blend of hardware and software expertise.

What Companies Look For

Embedded interview panels evaluate candidates across several dimensions that do not appear in typical software interviews. Here is what hiring managers consistently report as the top evaluation criteria:

Skill Category Junior (0-2 yrs) Mid (3-5 yrs) Senior (6+ yrs)
C/C++ Proficiency Pointers, arrays, structs Memory models, linker scripts Compiler internals, optimization
Hardware Knowledge GPIO, basic peripherals DMA, timers, ADC/DAC Custom IP, SoC bring-up
RTOS Task creation, semaphores Priority inversion, mutexes Scheduler design, custom RTOS
Protocols UART, SPI basics I2C, CAN, USB Protocol stack design, PHY tuning
Debugging printf, LED toggling JTAG, logic analyzer Oscilloscope, EMC, root cause
System Design Not usually tested Simple state machines Full system architecture

Typical Interview Stages

Most embedded positions follow a multi-stage process. Understanding what to expect at each stage helps you allocate preparation time effectively.

Interview Pipeline

  • Phone Screen (30-45 min): Basic C questions, volatile/static, simple bit manipulation. Recruiters filter for fundamental knowledge.
  • Technical Phone (60 min): Coding on a shared editor. Implement a ring buffer, parse a UART frame, write an ISR. Expect follow-up questions on your design choices.
  • On-Site / Virtual Loop (4-6 hours): 3-5 sessions covering C deep-dives, RTOS concepts, system design, hardware debugging, and behavioral questions.
  • Take-Home (some companies): Implement a small firmware project on an STM32 or write a driver for a simulated peripheral. Usually 4-8 hour time budget.
  • Hiring Manager (30 min): Culture fit, career goals, team dynamics. Often the final step.

Preparation Timeline

If you are starting from a baseline of knowing C and having some embedded experience, a focused 6-8 week preparation plan works well. Spend weeks 1-2 on C fundamentals and bit manipulation, weeks 3-4 on RTOS and ISR design, weeks 5-6 on protocols and debugging, and the final weeks on system design problems and mock interviews.

C/C++ Fundamentals

C remains the dominant language in embedded systems. A 2024 survey by Embedded.com found that 56% of embedded projects use C as the primary language, with C++ at 23%. Every embedded interview will test your C knowledge, often with trick questions designed to expose shallow understanding.

Pointers: The Cornerstone

If you cannot fluently read, write, and debug pointer code, you will not pass an embedded interview. Period. Here are the patterns that appear most frequently:

/* Interview Question: What does each declaration mean? */
int *p;           /* p is a pointer to int */
int **pp;         /* pp is a pointer to a pointer to int */
int (*fp)(int);   /* fp is a pointer to a function taking int, returning int */
int *arr[10];     /* arr is an array of 10 pointers to int */
int (*arr_p)[10]; /* arr_p is a pointer to an array of 10 ints */

/* Classic trap: What is the output? */
#include <stdio.h>

int main(void) {
    int a[] = {10, 20, 30, 40, 50};
    int *p = a;

    printf("%d\n", *(p + 2));    /* 30 - pointer arithmetic */
    printf("%d\n", *p + 2);      /* 12 - dereference then add */
    printf("%d\n", *(a + 4));    /* 50 - array decays to pointer */
    printf("%d\n", a[3]);        /* 40 - equivalent to *(a+3) */
    printf("%d\n", 2[a]);        /* 30 - a[2] == *(a+2) == *(2+a) == 2[a] */

    return 0;
}
Key Insight: The expression a[i] is defined by the C standard as *(a + i). Since addition is commutative, i[a] is also valid and equals *(i + a). This is a favorite interview trick question. Knowing this demonstrates deep understanding of how C arrays work.

Arrays vs Pointers

A common interview question asks: "Are arrays and pointers the same thing?" The answer is no, but they are closely related. An array name decays to a pointer to its first element in most contexts, but sizeof and the address-of operator & treat them differently.

#include <stdio.h>

void demo(void) {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr;

    /* sizeof difference */
    printf("sizeof(arr) = %zu\n", sizeof(arr));  /* 20 (5 * 4 bytes) */
    printf("sizeof(ptr) = %zu\n", sizeof(ptr));  /* 4 or 8 (pointer size) */

    /* Address-of difference */
    printf("&arr  = %p\n", (void *)&arr);   /* address of entire array */
    printf("&arr+1= %p\n", (void *)(&arr+1)); /* jumps by 20 bytes */
    printf("&ptr  = %p\n", (void *)&ptr);   /* address of the pointer variable */
    printf("&ptr+1= %p\n", (void *)(&ptr+1)); /* jumps by 4 or 8 bytes */
}

Function Pointers

Function pointers appear everywhere in embedded code: callback registrations, state machines, interrupt vector tables, and plugin architectures. Being able to declare, assign, and use function pointers is a must.

#include <stdio.h>

/* Define a type for clarity */
typedef void (*gpio_callback_t)(uint8_t pin, uint8_t state);

/* A simple callback registry */
#define MAX_CALLBACKS 8
static gpio_callback_t callbacks[MAX_CALLBACKS] = {NULL};

void register_callback(uint8_t slot, gpio_callback_t cb) {
    if (slot < MAX_CALLBACKS) {
        callbacks[slot] = cb;
    }
}

void fire_callbacks(uint8_t pin, uint8_t state) {
    for (int i = 0; i < MAX_CALLBACKS; i++) {
        if (callbacks[i] != NULL) {
            callbacks[i](pin, state);
        }
    }
}

/* Example callback */
void led_handler(uint8_t pin, uint8_t state) {
    printf("LED on pin %u: %s\n", pin, state ? "ON" : "OFF");
}

/* Usage */
int main(void) {
    register_callback(0, led_handler);
    fire_callbacks(13, 1);  /* prints: LED on pin 13: ON */
    return 0;
}

Struct Packing and Alignment

Struct layout is a critical embedded interview topic because it directly impacts memory usage, peripheral register mapping, and communication protocol parsing.

#include <stdio.h>
#include <stddef.h>

/* Without packing - compiler adds padding */
struct sensor_data_padded {
    uint8_t  id;        /* 1 byte + 3 padding */
    uint32_t timestamp; /* 4 bytes */
    uint16_t value;     /* 2 bytes + 2 padding */
    uint8_t  flags;     /* 1 byte + 3 padding */
};
/* Total: 16 bytes (with typical 4-byte alignment) */

/* With packing - no padding */
struct __attribute__((packed)) sensor_data_packed {
    uint8_t  id;        /* 1 byte */
    uint32_t timestamp; /* 4 bytes */
    uint16_t value;     /* 2 bytes */
    uint8_t  flags;     /* 1 byte */
};
/* Total: 8 bytes */

/* Reordered for natural alignment - best of both worlds */
struct sensor_data_optimal {
    uint32_t timestamp; /* 4 bytes, offset 0 */
    uint16_t value;     /* 2 bytes, offset 4 */
    uint8_t  id;        /* 1 byte,  offset 6 */
    uint8_t  flags;     /* 1 byte,  offset 7 */
};
/* Total: 8 bytes, naturally aligned */

void check_sizes(void) {
    printf("Padded:   %zu bytes\n", sizeof(struct sensor_data_padded));
    printf("Packed:   %zu bytes\n", sizeof(struct sensor_data_packed));
    printf("Optimal:  %zu bytes\n", sizeof(struct sensor_data_optimal));
}
Warning: Using __attribute__((packed)) can cause unaligned memory accesses on ARM Cortex-M0/M0+ and many other architectures, leading to hard faults. Always prefer reordering struct members for natural alignment over using packed attributes. Packed structs are acceptable for protocol parsing buffers where you memcpy into them.

Memory Management

Memory management in embedded systems is fundamentally different from desktop or server programming. You typically have kilobytes rather than gigabytes, there is no virtual memory, no swap space, and a memory leak that would be harmless on a server can crash a pacemaker.

Stack vs Heap

Property Stack Heap
Allocation speed Single instruction (SP adjustment) Hundreds of instructions (free list search)
Deallocation Automatic on function return Manual (free) or never
Fragmentation None (LIFO) Severe over time
Typical size (MCU) 1-8 KB 4-64 KB
Determinism Fully deterministic Non-deterministic
Thread safety Each task has own stack Requires locking

malloc Pitfalls in Embedded

/* Common interview question: What are the problems with this code? */
void process_sensor_data(void) {
    /* Problem 1: malloc can return NULL */
    char *buffer = (char *)malloc(256);
    /* Missing NULL check! */

    /* Problem 2: Non-deterministic timing */
    /* malloc searches a free list - unpredictable execution time */

    /* Problem 3: Fragmentation */
    /* After thousands of alloc/free cycles, heap becomes fragmented */
    /* A 256-byte request may fail even with 1024 bytes total free */

    read_sensor(buffer, 256);
    transmit(buffer, 256);

    /* Problem 4: Memory leak if early return */
    if (validate(buffer) != OK) {
        return;  /* LEAK! buffer never freed */
    }

    free(buffer);
}

/* Better approach: Static allocation */
static uint8_t sensor_buffer[256];

void process_sensor_data_safe(void) {
    /* No allocation, no fragmentation, deterministic, no leak */
    read_sensor(sensor_buffer, sizeof(sensor_buffer));
    transmit(sensor_buffer, sizeof(sensor_buffer));
}

Memory-Mapped I/O

In embedded systems, hardware peripherals are accessed through memory-mapped registers. Understanding how to read and write these registers is essential.

/* STM32 GPIO register access - the fundamentals */
#define GPIOA_BASE  0x40020000UL
#define GPIOA_MODER (*(volatile uint32_t *)(GPIOA_BASE + 0x00))
#define GPIOA_ODR   (*(volatile uint32_t *)(GPIOA_BASE + 0x14))
#define GPIOA_IDR   (*(volatile uint32_t *)(GPIOA_BASE + 0x10))
#define GPIOA_BSRR  (*(volatile uint32_t *)(GPIOA_BASE + 0x18))

/* Set pin 5 as output (mode = 01) */
void gpio_init(void) {
    GPIOA_MODER &= ~(3U << (5 * 2));  /* Clear bits 10:11 */
    GPIOA_MODER |=  (1U << (5 * 2));  /* Set bit 10 (output mode) */
}

/* Toggle LED on PA5 */
void gpio_toggle(void) {
    GPIOA_ODR ^= (1U << 5);
}

/* Atomic set/reset using BSRR (no read-modify-write race) */
void gpio_set(void) {
    GPIOA_BSRR = (1U << 5);       /* Set pin 5 */
}

void gpio_reset(void) {
    GPIOA_BSRR = (1U << (5 + 16)); /* Reset pin 5 */
}
Key Insight: The BSRR (Bit Set/Reset Register) is atomic because it is a write-only register that sets or clears individual bits without a read-modify-write cycle. This eliminates race conditions between ISRs and main code accessing the same GPIO port. This is one of the most frequently asked hardware register questions.

The Linker Script and Memory Layout

# Typical ARM Cortex-M memory layout (from linker script)
# Address Range        | Section    | Contents
# 0x0000_0000-0x0007_FFFF | FLASH   | .isr_vector, .text, .rodata
# 0x2000_0000-0x2000_FFFF | SRAM    | .data, .bss, heap, stack
#
# .text   = Code (functions)
# .rodata = Read-only data (const, string literals)
# .data   = Initialized global/static variables (copied from flash at startup)
# .bss    = Uninitialized global/static variables (zeroed at startup)
# heap    = Dynamic allocation (grows upward)
# stack   = Local variables, return addresses (grows downward)

Bit Manipulation

Bit manipulation is the bread and butter of embedded programming. Every peripheral register, every protocol frame, every status flag operates at the bit level. Interviewers use bit manipulation questions to assess whether you truly think at the hardware level.

The Four Essential Operations

/* The four operations you MUST know cold */
#define BIT_SET(reg, bit)    ((reg) |=  (1U << (bit)))
#define BIT_CLEAR(reg, bit)  ((reg) &= ~(1U << (bit)))
#define BIT_TOGGLE(reg, bit) ((reg) ^=  (1U << (bit)))
#define BIT_CHECK(reg, bit)  ((reg) &   (1U << (bit)))

/* Multi-bit field operations */
#define FIELD_SET(reg, mask, shift, val) \
    ((reg) = ((reg) & ~((mask) << (shift))) | (((val) & (mask)) << (shift)))

#define FIELD_GET(reg, mask, shift) \
    (((reg) >> (shift)) & (mask))

/* Example: Configure UART baud rate field (bits 15:4) */
uint32_t uart_cr = 0;
FIELD_SET(uart_cr, 0xFFF, 4, 104);  /* Set 12-bit baud divisor to 104 */
uint32_t baud = FIELD_GET(uart_cr, 0xFFF, 4);  /* Read it back: 104 */

Interview Favorites

/* Q1: Count the number of set bits (Kernighan's algorithm) */
int count_set_bits(uint32_t n) {
    int count = 0;
    while (n) {
        n &= (n - 1);  /* Clears the lowest set bit */
        count++;
    }
    return count;
}
/* Why it works: n-1 flips all bits from the lowest set bit downward.
   ANDing clears that lowest set bit. Runs in O(number of set bits). */

/* Q2: Check if a number is a power of 2 */
int is_power_of_2(uint32_t n) {
    return (n != 0) && ((n & (n - 1)) == 0);
}

/* Q3: Swap two variables without a temporary */
void swap(int *a, int *b) {
    *a ^= *b;
    *b ^= *a;
    *a ^= *b;
}

/* Q4: Reverse bits in a 32-bit integer */
uint32_t reverse_bits(uint32_t n) {
    uint32_t result = 0;
    for (int i = 0; i < 32; i++) {
        result = (result << 1) | (n & 1);
        n >>= 1;
    }
    return result;
}

/* Q5: Find the position of the only set bit */
int find_set_bit_position(uint32_t n) {
    if (!is_power_of_2(n)) return -1;
    int pos = 0;
    while (n > 1) {
        n >>= 1;
        pos++;
    }
    return pos;
}

Endianness

Endianness is a perennial interview topic. ARM Cortex-M is little-endian by default (least significant byte at the lowest address). Network protocols (TCP/IP) and many communication standards use big-endian (most significant byte first).

/* Detect endianness at runtime */
int is_little_endian(void) {
    uint32_t test = 0x01;
    return *((uint8_t *)&test) == 0x01;
}

/* Convert between endianness */
uint16_t swap16(uint16_t val) {
    return (val << 8) | (val >> 8);
}

uint32_t swap32(uint32_t val) {
    return ((val & 0xFF000000) >> 24) |
           ((val & 0x00FF0000) >>  8) |
           ((val & 0x0000FF00) <<  8) |
           ((val & 0x000000FF) << 24);
}

/* On ARM Cortex-M3+, use the REV instruction via intrinsic */
/* uint32_t swapped = __REV(val); */

Bit Fields

/* Bit fields for register mapping - use with caution */
typedef union {
    struct {
        uint32_t mode   : 2;   /* bits 1:0 */
        uint32_t otype  : 1;   /* bit 2 */
        uint32_t ospeed : 2;   /* bits 4:3 */
        uint32_t pupd   : 2;   /* bits 6:5 */
        uint32_t        : 25;  /* reserved */
    } bits;
    uint32_t reg;
} gpio_config_t;

/* Usage */
gpio_config_t config;
config.reg = 0;
config.bits.mode = 1;    /* Output mode */
config.bits.ospeed = 3;  /* Very high speed */
Warning: The C standard does not guarantee the layout of bit fields. Bit ordering, padding, and alignment are implementation-defined. Never use bit fields for portable protocol parsing. Use explicit shift-and-mask operations instead. Bit fields are acceptable for register overlays when you control the compiler and target.

volatile, const & Qualifiers

The volatile keyword is the single most important C qualifier in embedded systems, and the most commonly misunderstood. Every embedded interview asks about it. If you answer incorrectly, the interview is effectively over.

Why volatile Matters

/* Without volatile - the compiler may optimize this into an infinite loop
   or remove it entirely */
uint32_t *status_reg = (uint32_t *)0x40001000;

/* BUG: Compiler reads *status_reg once and caches the value */
while ((*status_reg & 0x01) == 0) {
    /* Wait for ready bit - compiler may optimize to infinite loop */
}

/* CORRECT: volatile forces the compiler to re-read every time */
volatile uint32_t *status_reg = (volatile uint32_t *)0x40001000;

while ((*status_reg & 0x01) == 0) {
    /* Compiler generates a load instruction every iteration */
}

The Three Use Cases of volatile

In an interview, when asked "when do you use volatile?", there are exactly three correct answers:

volatile Use Cases

  • Memory-mapped hardware registers: The hardware can change the register value at any time, independent of the CPU. The compiler must not cache or reorder accesses.
  • Variables shared between ISR and main code: The ISR can modify a global variable at any instruction boundary. Without volatile, the compiler may keep the variable in a register in the main loop and never see updates.
  • Variables shared between threads (with caveats): In a multi-threaded RTOS environment, volatile ensures visibility but does NOT provide atomicity. You still need mutexes or critical sections for correct synchronization.
/* Classic interview example: ISR-shared variable */
volatile uint8_t data_ready = 0;
volatile uint8_t rx_data[64];
volatile uint8_t rx_index = 0;

void USART1_IRQHandler(void) {
    if (USART1->SR & USART_SR_RXNE) {
        rx_data[rx_index++] = USART1->DR;
        if (rx_index >= 64) {
            data_ready = 1;
            rx_index = 0;
        }
    }
}

int main(void) {
    /* ... init code ... */
    while (1) {
        if (data_ready) {
            process_data((uint8_t *)rx_data, 64);
            data_ready = 0;
        }
    }
}

const Correctness

/* Interview question: Read these declarations left to right */
const int *p;        /* Pointer to a const int (can't modify *p) */
int *const p;        /* Const pointer to int (can't modify p) */
const int *const p;  /* Const pointer to const int (can't modify either) */

/* volatile + const = hardware status register */
/* The register can change (hardware writes it) but firmware must not write it */
volatile const uint32_t *adc_result = (volatile const uint32_t *)0x40012440;

/* Practical use: function that promises not to modify the buffer */
void uart_transmit(const uint8_t *data, size_t len) {
    for (size_t i = 0; i < len; i++) {
        while (!(UART->SR & TX_EMPTY));
        UART->DR = data[i];
    }
}

The restrict Qualifier

The restrict qualifier (C99) tells the compiler that a pointer is the only way to access the data it points to. This enables aggressive optimizations, particularly in DSP-heavy code.

/* Without restrict, compiler must assume dst and src may overlap */
void vector_add(int *dst, const int *src, size_t n) {
    for (size_t i = 0; i < n; i++)
        dst[i] += src[i];
}

/* With restrict, compiler can vectorize and pipeline */
void vector_add_fast(int *restrict dst, const int *restrict src, size_t n) {
    for (size_t i = 0; i < n; i++)
        dst[i] += src[i];
}
/* On ARM Cortex-M4/M7, the restrict version may use SIMD instructions */
Key Insight: A favorite interview trap is the declaration volatile const uint32_t *reg. Candidates often say "that's a contradiction." It is not. const means the program cannot write through this pointer. volatile means the hardware can still change the value. This pattern is used for read-only hardware status registers.

RTOS Concepts

Real-Time Operating System questions appear in virtually every mid-level and senior embedded interview. FreeRTOS dominates the market with over 40% of RTOS deployments according to the 2024 Embedded Market Study. Zephyr RTOS is growing rapidly in the IoT space.

Tasks, Priorities, and Preemption

/* FreeRTOS task creation example */
#include "FreeRTOS.h"
#include "task.h"

void sensor_task(void *params) {
    TickType_t last_wake = xTaskGetTickCount();

    while (1) {
        read_sensors();
        process_data();

        /* vTaskDelayUntil provides precise periodic execution */
        vTaskDelayUntil(&last_wake, pdMS_TO_TICKS(100));
    }
}

void comms_task(void *params) {
    while (1) {
        if (xQueueReceive(tx_queue, &packet, portMAX_DELAY) == pdTRUE) {
            uart_transmit(packet.data, packet.len);
        }
    }
}

int main(void) {
    /* Higher number = higher priority in FreeRTOS */
    xTaskCreate(sensor_task, "Sensor", 256, NULL, 3, NULL);
    xTaskCreate(comms_task,  "Comms",  512, NULL, 2, NULL);
    vTaskStartScheduler();

    /* Should never reach here */
    while (1);
}

Mutexes vs Semaphores

Property Mutex Binary Semaphore Counting Semaphore
Ownership Yes (only owner can release) No (any task can give) No
Priority inheritance Yes (prevents inversion) No No
Use case Mutual exclusion ISR-to-task signaling Resource counting
Can be given from ISR? No Yes Yes
Recursive locking Yes (recursive mutex) No (deadlock) N/A

Priority Inversion

Priority inversion is one of the most famous problems in embedded systems, brought to global attention by the Mars Pathfinder incident in 1997. The Sojourner rover experienced system resets caused by priority inversion in its VxWorks RTOS. A low-priority meteorological task held a mutex needed by the high-priority bus management task, while a medium-priority communications task preempted the low-priority task, preventing it from releasing the mutex.

Key Insight: The Mars Pathfinder fix was to enable priority inheritance on the mutex, which was already supported by VxWorks but had been left disabled. The fix was uploaded to Mars via a 10-bit-per-second radio link. This real-world incident appears in interviews at aerospace and automotive companies constantly.

Deadlock

/* Classic deadlock scenario - two tasks, two mutexes */

/* Task A */
void task_a(void *params) {
    while (1) {
        xSemaphoreTake(mutex_1, portMAX_DELAY);
        vTaskDelay(1);  /* Context switch happens here */
        xSemaphoreTake(mutex_2, portMAX_DELAY);  /* DEADLOCK! */

        /* ... critical section ... */

        xSemaphoreGive(mutex_2);
        xSemaphoreGive(mutex_1);
    }
}

/* Task B */
void task_b(void *params) {
    while (1) {
        xSemaphoreTake(mutex_2, portMAX_DELAY);
        vTaskDelay(1);  /* Context switch happens here */
        xSemaphoreTake(mutex_1, portMAX_DELAY);  /* DEADLOCK! */

        /* ... critical section ... */

        xSemaphoreGive(mutex_1);
        xSemaphoreGive(mutex_2);
    }
}

/* Fix: Always acquire mutexes in the same order */
/* Both tasks: take mutex_1 first, then mutex_2 */

ISR Design Patterns

Interrupt Service Routine design is where embedded engineers separate themselves from application developers. ISR questions test your understanding of real-time constraints, hardware interaction, and concurrent programming.

ISR Rules

The Golden Rules of ISR Design

  • Keep it short: ISRs should execute in microseconds, not milliseconds. The longer an ISR runs, the more latency it adds to all lower-priority interrupts.
  • No blocking calls: Never call malloc, printf, mutex take, queue receive with blocking, or any function that may block. These can cause deadlock or priority inversion.
  • No floating point: On many architectures, the FPU context is not saved/restored in ISRs. Using float in an ISR can corrupt FPU registers used by main code. ARM Cortex-M4/M7 can optionally save FPU context but at a latency cost.
  • Use volatile for shared variables: Any variable read in main code and written in an ISR (or vice versa) must be declared volatile.
  • Clear the interrupt flag: Forgetting to clear the peripheral's interrupt flag causes the ISR to fire immediately again, creating an infinite loop.
  • Use ISR-safe RTOS APIs: In FreeRTOS, use xQueueSendFromISR, not xQueueSend. Use xSemaphoreGiveFromISR, not xSemaphoreGive.

Deferred Processing Pattern

/* The deferred processing pattern: ISR does minimum work,
   defers heavy processing to a task */

/* Shared queue */
static QueueHandle_t adc_queue;

/* ISR: Read ADC result, send to queue, return immediately */
void ADC_IRQHandler(void) {
    BaseType_t higher_priority_woken = pdFALSE;
    uint16_t reading = ADC1->DR;  /* Read clears interrupt flag */

    xQueueSendFromISR(adc_queue, &reading, &higher_priority_woken);

    /* Request context switch if a higher-priority task was unblocked */
    portYIELD_FROM_ISR(higher_priority_woken);
}

/* Task: Heavy processing happens here, not in ISR */
void adc_processing_task(void *params) {
    uint16_t reading;

    while (1) {
        if (xQueueReceive(adc_queue, &reading, portMAX_DELAY) == pdTRUE) {
            float voltage = (reading / 4095.0f) * 3.3f;
            apply_filter(voltage);
            update_display(voltage);
            log_to_flash(voltage);
        }
    }
}

Ring Buffer for ISR Communication

/* Lock-free single-producer single-consumer ring buffer */
/* ISR writes (producer), main loop reads (consumer) */

#define RING_SIZE 256  /* Must be power of 2 */
#define RING_MASK (RING_SIZE - 1)

typedef struct {
    volatile uint8_t buffer[RING_SIZE];
    volatile uint32_t head;  /* Written by producer (ISR) */
    volatile uint32_t tail;  /* Written by consumer (main) */
} ring_buffer_t;

static ring_buffer_t uart_rx_ring = {0};

/* Called from UART ISR */
static inline void ring_put(ring_buffer_t *rb, uint8_t byte) {
    uint32_t next = (rb->head + 1) & RING_MASK;
    if (next != rb->tail) {  /* Not full */
        rb->buffer[rb->head] = byte;
        rb->head = next;
    }
    /* If full, byte is silently dropped */
}

/* Called from main loop or task */
static inline int ring_get(ring_buffer_t *rb, uint8_t *byte) {
    if (rb->head == rb->tail) return 0;  /* Empty */
    *byte = rb->buffer[rb->tail];
    rb->tail = (rb->tail + 1) & RING_MASK;
    return 1;
}

static inline uint32_t ring_count(ring_buffer_t *rb) {
    return (rb->head - rb->tail) & RING_MASK;
}

/* UART ISR */
void USART1_IRQHandler(void) {
    if (USART1->SR & USART_SR_RXNE) {
        uint8_t byte = USART1->DR;
        ring_put(&uart_rx_ring, byte);
    }
}
Key Insight: The ring buffer above is lock-free because there is exactly one writer (ISR) and one reader (main loop). The head index is only modified by the producer, and the tail index only by the consumer. This pattern requires the buffer size to be a power of 2 so that the mask operation works correctly for wrapping.

DMA: The ISR Alternative

/* DMA transfers data without CPU intervention.
   The CPU sets up the transfer, then does other work.
   An interrupt fires only when the transfer is complete. */

void uart_dma_init(void) {
    /* Configure DMA channel for UART TX */
    DMA1_Channel4->CPAR = (uint32_t)&USART1->DR;  /* Peripheral address */
    DMA1_Channel4->CMAR = (uint32_t)tx_buffer;      /* Memory address */
    DMA1_Channel4->CNDTR = tx_length;                /* Number of bytes */
    DMA1_Channel4->CCR = DMA_CCR_MINC    /* Memory increment mode */
                       | DMA_CCR_DIR     /* Memory to peripheral */
                       | DMA_CCR_TCIE;   /* Transfer complete interrupt */
    DMA1_Channel4->CCR |= DMA_CCR_EN;   /* Start transfer */
}

/* DMA complete ISR - fires once when ALL bytes are sent */
void DMA1_Channel4_IRQHandler(void) {
    if (DMA1->ISR & DMA_ISR_TCIF4) {
        DMA1->IFCR = DMA_IFCR_CTCIF4;  /* Clear flag */
        DMA1_Channel4->CCR &= ~DMA_CCR_EN;  /* Disable channel */
        tx_complete = 1;  /* Signal main code */
    }
}

Communication Protocols

Every embedded interview asks about at least one communication protocol. You need to know the physical layer, timing, frame format, and common debugging techniques for UART, SPI, I2C, and CAN.

UART (Universal Asynchronous Receiver/Transmitter)

Parameter Typical Value Notes
Baud rate 9600, 115200, 921600 Both sides must match within 2-3%
Data bits 8 7 for ASCII-only, 9 for multi-drop
Parity None, Even, Odd Single-bit error detection only
Stop bits 1 2 stop bits for slower receivers
Flow control None, RTS/CTS, XON/XOFF Hardware flow control preferred
Wires TX, RX, GND Cross TX/RX between devices

SPI (Serial Peripheral Interface)

/* SPI master transmit/receive - bit-banged for interview understanding */
uint8_t spi_transfer(uint8_t tx_byte) {
    uint8_t rx_byte = 0;

    for (int i = 7; i >= 0; i--) {
        /* Set MOSI based on tx bit */
        if (tx_byte & (1 << i))
            GPIO_SET(MOSI_PIN);
        else
            GPIO_CLEAR(MOSI_PIN);

        /* Clock high - slave samples MOSI (CPOL=0, CPHA=0) */
        GPIO_SET(SCK_PIN);

        /* Read MISO */
        if (GPIO_READ(MISO_PIN))
            rx_byte |= (1 << i);

        /* Clock low */
        GPIO_CLEAR(SCK_PIN);
    }

    return rx_byte;
}

/* SPI transaction with chip select */
void spi_write_register(uint8_t reg, uint8_t value) {
    GPIO_CLEAR(CS_PIN);         /* Assert chip select (active low) */
    spi_transfer(reg & 0x7F);  /* Write command (bit 7 = 0) */
    spi_transfer(value);
    GPIO_SET(CS_PIN);           /* Deassert chip select */
}

I2C (Inter-Integrated Circuit)

/* I2C master write - reading a sensor register */
/* Address format: 7-bit device address + R/W bit */
int i2c_read_register(uint8_t dev_addr, uint8_t reg_addr, uint8_t *data) {
    /* Step 1: Send START + device address + WRITE */
    i2c_start();
    if (i2c_send_byte((dev_addr << 1) | 0) != ACK)
        return -1;  /* NACK - device not responding */

    /* Step 2: Send register address */
    if (i2c_send_byte(reg_addr) != ACK)
        return -1;

    /* Step 3: Repeated START + device address + READ */
    i2c_start();  /* Repeated start, no stop in between */
    if (i2c_send_byte((dev_addr << 1) | 1) != ACK)
        return -1;

    /* Step 4: Read data byte, send NACK (last byte) */
    *data = i2c_read_byte(NACK);

    /* Step 5: STOP */
    i2c_stop();

    return 0;
}

CAN (Controller Area Network)

CAN is the backbone of automotive and industrial communication. Developed by Bosch in 1986 and first deployed in the 1991 Mercedes W140 S-Class, CAN uses differential signaling on a two-wire bus (CAN_H and CAN_L) with bitwise arbitration that provides non-destructive priority-based access.

/* CAN frame transmission on STM32 */
void can_send_message(uint32_t id, uint8_t *data, uint8_t len) {
    CAN_TxHeaderTypeDef header;
    uint32_t mailbox;

    header.StdId = id;        /* 11-bit standard identifier */
    header.IDE = CAN_ID_STD;  /* Standard frame (not extended) */
    header.RTR = CAN_RTR_DATA; /* Data frame (not remote) */
    header.DLC = len;          /* Data Length Code: 0-8 bytes */

    /* HAL_CAN_AddTxMessage queues the frame for transmission.
       Arbitration happens automatically on the bus. */
    if (HAL_CAN_AddTxMessage(&hcan, &header, data, &mailbox) != HAL_OK) {
        error_handler();
    }
}

/* CAN receive callback */
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) {
    CAN_RxHeaderTypeDef header;
    uint8_t data[8];

    HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &header, data);

    switch (header.StdId) {
        case 0x201:  /* Engine RPM */
            engine_rpm = (data[0] << 8) | data[1];
            break;
        case 0x202:  /* Vehicle speed */
            vehicle_speed = data[0];
            break;
    }
}

Protocol Debugging Scenarios

Common Interview Debugging Scenarios

  • UART garbage characters: Baud rate mismatch, wrong voltage levels (3.3V vs 5V), TX/RX not crossed, ground not connected.
  • SPI always reads 0xFF: MISO not connected, wrong SPI mode (CPOL/CPHA), chip select not asserted, device not powered.
  • I2C NACK on address: Wrong 7-bit address (check datasheet vs shifted value), pull-ups missing, bus stuck (SDA held low).
  • CAN no communication: Termination resistors missing (120 ohm at each end), CAN_H/CAN_L swapped, baud rate mismatch, transceiver not enabled.

Debugging & Tools

Senior embedded interviews often include a debugging round where you are given a scenario and asked to walk through your diagnostic process. The interviewer is assessing your systematic approach, not just your tool knowledge.

Oscilloscope Fundamentals

An oscilloscope shows voltage over time. For embedded debugging, you need to understand triggering, time base, protocol decoding, and how to measure signal integrity.

Oscilloscope Checklist for Embedded Debugging

  • Probe ground: Always connect the probe ground clip to the circuit ground. A floating ground reference causes phantom signals.
  • Bandwidth: Use a scope with bandwidth at least 5x the signal frequency. For 100 MHz SPI, you need a 500 MHz scope.
  • Trigger: Set trigger to the chip select (SPI) or start condition (I2C) for clean capture of protocol transactions.
  • Measure rise time: If rise time is too slow, increase drive strength or reduce capacitive load.
  • Check voltage levels: I2C pull-up voltage should be close to VCC. If it only reaches 2.8V on a 3.3V bus, pull-ups are too weak.

Logic Analyzer

A logic analyzer captures digital signals across many channels simultaneously and can decode protocol frames. Tools like Saleae Logic are common in embedded labs and cost $150-500 for the hardware.

JTAG/SWD Debugging

# GDB commands for embedded debugging via OpenOCD

# Connect to target
arm-none-eabi-gdb firmware.elf
(gdb) target remote localhost:3333

# Set breakpoint at a function
(gdb) break main
(gdb) break uart_init
(gdb) break HardFault_Handler

# Examine registers
(gdb) info registers
(gdb) print/x $pc        # Program counter
(gdb) print/x $sp        # Stack pointer
(gdb) print/x $lr        # Link register

# Examine memory
(gdb) x/16xw 0x20000000  # 16 words at SRAM start
(gdb) x/8xb 0x40020014   # 8 bytes at GPIOA_ODR

# Watchpoint: break when variable changes
(gdb) watch sensor_value
(gdb) rwatch *(uint32_t *)0x40001000  # Break on hardware register read

# Step through code
(gdb) step     # Step into functions
(gdb) next     # Step over functions
(gdb) continue # Run until next breakpoint

Common Debugging Scenarios

Hard Fault Debugging Checklist

  • Step 1: Check the stacked PC value in the exception frame. This tells you which instruction caused the fault.
  • Step 2: Read the Configurable Fault Status Register (CFSR) at 0xE000ED28. Bits indicate the fault type: bus fault, usage fault, memory fault.
  • Step 3: Common causes: null pointer dereference (bus fault), unaligned access on M0 (usage fault), stack overflow (memory fault), executing from invalid address (bus fault).
  • Step 4: If stack overflow, increase the task stack size or reduce local variable usage. Use a stack canary pattern to detect overflow early.
/* Hard Fault handler that extracts useful debug info */
void HardFault_Handler(void) {
    __asm volatile (
        "tst lr, #4          \n"  /* Check which stack was in use */
        "ite eq              \n"
        "mrseq r0, msp       \n"  /* Main stack pointer */
        "mrsne r0, psp       \n"  /* Process stack pointer */
        "b hard_fault_handler\n"
    );
}

void hard_fault_handler(uint32_t *stack_frame) {
    volatile uint32_t r0  = stack_frame[0];
    volatile uint32_t r1  = stack_frame[1];
    volatile uint32_t r2  = stack_frame[2];
    volatile uint32_t r3  = stack_frame[3];
    volatile uint32_t r12 = stack_frame[4];
    volatile uint32_t lr  = stack_frame[5];
    volatile uint32_t pc  = stack_frame[6];  /* Address of faulting instruction */
    volatile uint32_t psr = stack_frame[7];

    volatile uint32_t cfsr = *(volatile uint32_t *)0xE000ED28;

    /* Breakpoint here - inspect variables in debugger */
    __BKPT(0);
    while (1);
}

System Design Problems

System design questions are the hallmark of senior embedded interviews. You are expected to design a complete embedded system: choose components, define the software architecture, draw state machines, identify edge cases, and discuss trade-offs.

Traffic Light Controller

This is perhaps the most classic embedded system design problem. Here is a production-quality approach:

/* State machine for a 4-way traffic light intersection */

typedef enum {
    STATE_NS_GREEN,        /* North-South green, East-West red */
    STATE_NS_YELLOW,       /* North-South yellow */
    STATE_ALL_RED_1,       /* All red (safety clearance) */
    STATE_EW_GREEN,        /* East-West green, North-South red */
    STATE_EW_YELLOW,       /* East-West yellow */
    STATE_ALL_RED_2,       /* All red (safety clearance) */
    STATE_PEDESTRIAN_NS,   /* Pedestrian crossing NS */
    STATE_PEDESTRIAN_EW,   /* Pedestrian crossing EW */
    STATE_EMERGENCY,       /* All red, emergency vehicle override */
    NUM_STATES
} traffic_state_t;

typedef struct {
    traffic_state_t state;
    uint32_t duration_ms;
    traffic_state_t next_state;
    uint8_t ns_light;   /* 0=Red, 1=Yellow, 2=Green */
    uint8_t ew_light;   /* 0=Red, 1=Yellow, 2=Green */
} state_config_t;

static const state_config_t state_table[NUM_STATES] = {
    /* State               Duration  Next            NS  EW */
    { STATE_NS_GREEN,      30000,    STATE_NS_YELLOW,     2,  0 },
    { STATE_NS_YELLOW,      3000,    STATE_ALL_RED_1,     1,  0 },
    { STATE_ALL_RED_1,      1000,    STATE_EW_GREEN,      0,  0 },
    { STATE_EW_GREEN,      30000,    STATE_EW_YELLOW,     0,  2 },
    { STATE_EW_YELLOW,      3000,    STATE_ALL_RED_2,     0,  1 },
    { STATE_ALL_RED_2,      1000,    STATE_NS_GREEN,      0,  0 },
    { STATE_PEDESTRIAN_NS, 15000,    STATE_ALL_RED_1,     0,  0 },
    { STATE_PEDESTRIAN_EW, 15000,    STATE_ALL_RED_2,     0,  0 },
    { STATE_EMERGENCY,         0,    STATE_ALL_RED_1,     0,  0 },
};

static traffic_state_t current_state = STATE_NS_GREEN;
static uint32_t state_timer = 0;
static volatile uint8_t emergency_active = 0;
static volatile uint8_t ped_request_ns = 0;
static volatile uint8_t ped_request_ew = 0;

void traffic_controller_tick(uint32_t elapsed_ms) {
    /* Emergency override has highest priority */
    if (emergency_active) {
        current_state = STATE_EMERGENCY;
        apply_lights(0, 0);  /* All red */
        return;
    }

    state_timer += elapsed_ms;

    if (state_timer >= state_table[current_state].duration_ms) {
        state_timer = 0;

        /* Check pedestrian requests at transition points */
        traffic_state_t next = state_table[current_state].next_state;

        if (next == STATE_EW_GREEN && ped_request_ns) {
            next = STATE_PEDESTRIAN_NS;
            ped_request_ns = 0;
        } else if (next == STATE_NS_GREEN && ped_request_ew) {
            next = STATE_PEDESTRIAN_EW;
            ped_request_ew = 0;
        }

        current_state = next;
        apply_lights(state_table[current_state].ns_light,
                     state_table[current_state].ew_light);
    }
}

Elevator Controller

/* Elevator controller with SCAN algorithm */

#define NUM_FLOORS 10

typedef enum {
    DIR_UP,
    DIR_DOWN,
    DIR_IDLE
} direction_t;

typedef struct {
    uint8_t current_floor;
    direction_t direction;
    uint8_t requests_up[NUM_FLOORS];    /* Floor buttons going up */
    uint8_t requests_down[NUM_FLOORS];  /* Floor buttons going down */
    uint8_t requests_cab[NUM_FLOORS];   /* Cabin buttons */
    uint8_t door_open;
    uint32_t door_timer;
} elevator_t;

/* SCAN algorithm: service all requests in current direction,
   then reverse. Like a disk head scheduler. */
int get_next_floor(elevator_t *e) {
    if (e->direction == DIR_UP) {
        /* Look for requests above current floor */
        for (int f = e->current_floor + 1; f < NUM_FLOORS; f++) {
            if (e->requests_up[f] || e->requests_cab[f] || e->requests_down[f])
                return f;
        }
        /* Nothing above - reverse direction */
        for (int f = e->current_floor - 1; f >= 0; f--) {
            if (e->requests_down[f] || e->requests_cab[f] || e->requests_up[f])
                return f;
        }
    } else if (e->direction == DIR_DOWN) {
        for (int f = e->current_floor - 1; f >= 0; f--) {
            if (e->requests_down[f] || e->requests_cab[f] || e->requests_up[f])
                return f;
        }
        for (int f = e->current_floor + 1; f < NUM_FLOORS; f++) {
            if (e->requests_up[f] || e->requests_cab[f] || e->requests_down[f])
                return f;
        }
    }
    return -1;  /* No pending requests */
}

void elevator_tick(elevator_t *e) {
    if (e->door_open) {
        e->door_timer += TICK_MS;
        if (e->door_timer >= 3000) {  /* Door open for 3 seconds */
            e->door_open = 0;
            e->door_timer = 0;
        }
        return;
    }

    int target = get_next_floor(e);
    if (target < 0) {
        e->direction = DIR_IDLE;
        return;
    }

    if (target > e->current_floor) {
        e->direction = DIR_UP;
        e->current_floor++;
    } else if (target < e->current_floor) {
        e->direction = DIR_DOWN;
        e->current_floor--;
    }

    /* Arrived at target floor */
    if (e->current_floor == target) {
        e->door_open = 1;
        e->door_timer = 0;
        e->requests_up[target] = 0;
        e->requests_down[target] = 0;
        e->requests_cab[target] = 0;
    }
}

Thermostat with PID Control

/* Simple PID thermostat controller */
typedef struct {
    float kp;           /* Proportional gain */
    float ki;           /* Integral gain */
    float kd;           /* Derivative gain */
    float setpoint;     /* Target temperature */
    float integral;     /* Accumulated error */
    float prev_error;   /* Previous error for derivative */
    float output_min;   /* Output clamp minimum (e.g., 0%) */
    float output_max;   /* Output clamp maximum (e.g., 100%) */
} pid_controller_t;

float pid_update(pid_controller_t *pid, float measurement, float dt) {
    float error = pid->setpoint - measurement;

    /* Proportional */
    float p_term = pid->kp * error;

    /* Integral with anti-windup */
    pid->integral += error * dt;
    float i_term = pid->ki * pid->integral;

    /* Clamp integral to prevent windup */
    if (i_term > pid->output_max) {
        i_term = pid->output_max;
        pid->integral = pid->output_max / pid->ki;
    } else if (i_term < pid->output_min) {
        i_term = pid->output_min;
        pid->integral = pid->output_min / pid->ki;
    }

    /* Derivative (on measurement to avoid derivative kick) */
    float d_term = pid->kd * (pid->prev_error - error) / dt;
    pid->prev_error = error;

    /* Sum and clamp output */
    float output = p_term + i_term + d_term;
    if (output > pid->output_max) output = pid->output_max;
    if (output < pid->output_min) output = pid->output_min;

    return output;
}

/* Usage */
pid_controller_t heater_pid = {
    .kp = 2.0f, .ki = 0.5f, .kd = 1.0f,
    .setpoint = 22.0f,  /* 22 degrees C */
    .output_min = 0.0f, .output_max = 100.0f
};

void thermostat_task(void *params) {
    while (1) {
        float temp = read_temperature_sensor();
        float duty = pid_update(&heater_pid, temp, 0.1f);
        set_heater_pwm(duty);  /* 0-100% duty cycle */
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

Behavioral & Soft Skills

Many candidates spend all their preparation time on technical questions and neglect behavioral preparation. This is a mistake. At the senior level, behavioral rounds carry equal weight to technical rounds in the hiring decision.

The STAR Method

STAR Framework

  • Situation: Set the context. "On the medical device project at Company X, we were six weeks from FDA submission..."
  • Task: What was your specific responsibility? "I was responsible for the firmware for the pulse oximeter sensor subsystem..."
  • Action: What did YOU do? (Use "I", not "we") "I designed a watchdog-based recovery mechanism and wrote a test harness that simulated sensor failures..."
  • Result: Quantify the outcome. "We achieved zero field failures in the first 12 months of deployment across 5,000 units, and the approach was adopted as a standard pattern for all new products."

Common Behavioral Questions for Embedded Roles

Prepare specific stories for each of these themes: a time you debugged a difficult hardware-software interaction, a time you disagreed with a team member on a technical approach, a time you had to learn a new technology quickly, a time you made a mistake that affected production, and a time you improved a process or tool.

Technical Communication

Embedded engineers frequently interface with hardware teams, test engineers, and management. Being able to explain complex technical concepts to non-technical stakeholders is a differentiator. Practice explaining concepts like interrupt latency, stack overflow, and race conditions using analogies that non-engineers can understand.

Key Insight: When an interviewer asks "tell me about yourself," they want a 2-minute technical narrative, not your life story. Structure it as: current role and focus, one or two accomplishments with impact, why you are interested in this specific role. End with something that invites a follow-up question.

Case Studies

Tesla Firmware Interview Process

Tesla's embedded firmware interviews are among the most challenging in the industry. The process typically consists of a recruiter screen, a 60-minute technical phone interview focused on C and real-time systems, and a 5-hour on-site loop. The on-site includes a live coding session on an STM32 board where you implement a CAN bus driver, a system design round where you design a battery management system, a code review round where you critique production firmware, and a behavioral round with the hiring manager.

Tesla expects candidates to be comfortable with bare-metal programming, RTOS (FreeRTOS), CAN/LIN protocols, and power management. The company uses C for most firmware (not C++), and interview questions heavily emphasize ISR design, DMA, and peripheral driver development. Salary ranges for firmware engineers at Tesla in 2025 were $130K-$200K base plus stock, depending on level.

Qualcomm Embedded Roles

Qualcomm interviews for embedded software roles focus more on C++ than pure C, especially for their modem firmware and camera subsystem teams. The process includes an online assessment with timed coding problems, two technical phone screens (one on data structures and algorithms, one on embedded-specific topics), and a virtual on-site with 4-5 rounds. Qualcomm places heavy emphasis on concurrency, cache coherence, memory ordering (due to ARM's weakly-ordered memory model), and low-level optimization. Their Snapdragon platform teams expect familiarity with ARM TrustZone, device tree, and Linux kernel driver development.

Startup vs FAANG Comparison

Startup vs FAANG Embedded Roles

  • Scope: Startups expect full-stack embedded (schematic review to cloud API). FAANG roles are narrower and deeper (you may own one driver for 2 years).
  • Interview focus: Startups test breadth and adaptability. FAANG tests depth and algorithmic thinking.
  • Tools: Startups often use cheaper tools (STM32, Saleae, open-source RTOS). FAANG uses custom silicon, proprietary tools, and large internal codebases.
  • Compensation: FAANG pays $150K-$300K+ total comp. Well-funded startups pay $120K-$180K base plus significant equity.
  • Impact: At a startup, your code ships in weeks. At FAANG, your code ships in millions of devices but takes months to reach production.

Practice Problems

Exercise 1 Beginner

Implement a Circular Buffer

Write a complete circular (ring) buffer implementation in C with the following API: ring_init(), ring_put(), ring_get(), ring_full(), ring_empty(), ring_count(). The buffer should be safe for single-producer single-consumer use (ISR writes, main loop reads). Test with a simulated UART receive scenario where bytes arrive at 115200 baud and the main loop processes them in batches of 32.

C Programming Data Structures ISR Safety
Exercise 2 Intermediate

RTOS Task Synchronization

Design a FreeRTOS application with three tasks: a sensor task that reads an ADC every 100ms, a processing task that applies a moving average filter, and a display task that updates an LCD every 500ms. Use queues for inter-task communication and a mutex to protect the shared display resource. Implement priority inversion prevention. Draw the task timing diagram and calculate worst-case CPU utilization using Rate Monotonic Analysis.

FreeRTOS Synchronization Real-Time Analysis
Exercise 3 Advanced

Design a Battery Management System

Design the complete firmware architecture for a lithium-ion battery management system (BMS) for an electric bicycle. Requirements: monitor 10 cells in series via SPI-connected AFE (Analog Front End), implement cell balancing (passive), calculate State of Charge using Coulomb counting with OCV correction, communicate battery status over CAN bus at 10 Hz, implement safety shutoffs for over-voltage (4.25V/cell), under-voltage (2.7V/cell), over-temperature (60C), and over-current (30A). Draw the state machine, define the CAN message format, and write the cell balancing algorithm.

System Design CAN Protocol Safety-Critical State Machines

Interview Prep Sheet Generator

Use this tool to organize your interview preparation topics and generate a study plan document. Download as Word, Excel, PDF, or PowerPoint for structured review.

Interview Prep Sheet Generator

Organize your embedded interview preparation into a structured document. All data stays in your browser.

Draft auto-saved

All data stays in your browser. Nothing is sent to or stored on any server.

Conclusion & Resources

Embedded systems interviews are challenging because they span hardware and software, theory and practice, algorithms and real-time constraints. The key to success is systematic preparation across all the domains covered in this guide.

Key Takeaways:
  • Master C pointers, memory layout, and struct alignment before anything else
  • Know the three use cases of volatile and be able to explain them with examples
  • Understand RTOS primitives, priority inversion, and the Mars Pathfinder story
  • Practice bit manipulation problems until the operations are automatic
  • Be ready to design state machines for system design questions
  • Prepare STAR-format stories for behavioral rounds

Recommended Resources

Books and References

  • "Making Embedded Systems" by Elecia White (O'Reilly, 2011) - Best overall embedded systems book
  • "The C Programming Language" by Kernighan and Ritchie - The definitive C reference
  • "Mastering the FreeRTOS Real Time Kernel" by Richard Barry - Free PDF from freertos.org
  • "Test Driven Development for Embedded C" by James Grenning - Essential for modern firmware development
  • Barr Group Embedded C Coding Standard - Industry-standard coding guidelines
Technology