C Refresher

Embedded C Interview Questions

Whether you are trying to land your first job in Embedded Systems or preparing for a senior role, the technical interview will almost always focus on the same core concepts.

We have compiled 32 of the most frequently asked questions, broken down into Core Fundamentals, Memory, Preprocessor, and a dedicated "Hard Mode" section that separates juniors from seniors.


Part 1: The Core Fundamentals

1. What does the volatile keyword mean, and when should you use it?

Answer: volatile tells the compiler: "Do not optimize this variable. It can change at any time outside the flow of the current code." It forces the compiler to read the variable from physical memory every single time it is used, rather than caching it in a CPU register. You must use it for:

  1. Memory-mapped hardware registers.
  2. Global variables modified inside an Interrupt Service Routine (ISR).
  3. Variables shared between RTOS tasks (though Mutexes are preferred).

2. Write a C macro to SET, CLEAR, and TOGGLE the 4th bit of a register.

Answer: The absolute classic whiteboard question.

c
1#define SET_BIT(REG, BIT) ((REG) |= (1 << (BIT)))
2#define CLEAR_BIT(REG, BIT) ((REG) &= ~(1 << (BIT)))
3#define TOGGLE_BIT(REG, BIT) ((REG) ^= (1 << (BIT)))
4#define CHECK_BIT(REG, BIT) ((REG) & (1 << (BIT)))
5
6// Example usage to set the 4th bit (Bit 3, since we start at 0)
7SET_BIT(register_val, 3);

3. What are the rules for writing a good Interrupt Service Routine (ISR)?

Answer: The golden rule is: Keep it short and fast.

  1. No blocking code (sleep, delay, while loops waiting for hardware).
  2. No printf() (it is slow and not reentrant).
  3. No malloc() or free().
  4. Defer heavy work: The ISR should just clear the hardware flag, grab data, and set a boolean flag (or RTOS Semaphore) to wake up a normal task.

4. Explain the difference between const int *ptr, int const *ptr, and int * const ptr.

Answer: Read pointer syntax from right-to-left!

  • const int *ptr and int const *ptr are the same: ptr is a pointer to an integer that is constant. You can change the pointer, but not the integer.
  • int * const ptr: ptr is a constant pointer to an integer. You cannot change the pointer, but you can change the integer.
  • const int * const ptr: Neither the pointer nor the data can be changed.

5. What does the static keyword mean in C?

Answer: It has two completely different meanings based on scope:

  1. File Scope: When applied to a global variable/function, it means the symbol is private to that .c file and cannot be accessed via extern.
  2. Function Scope: When applied to a local variable inside a function, the variable is created in the .data/.bss segment (not the stack) and retains its value between function calls.

6. What does sizeof() return for an array vs a pointer?

Answer:

c
1int arr[5];
2int *ptr = arr;

sizeof(arr) returns the total size in bytes (5 * 4 = 20 bytes). sizeof(ptr) returns the size of the pointer itself (4 bytes on 32-bit systems). Passing an array into a function causes it to decay into a pointer, so sizeof() inside the function will return the pointer size.

7. What is Struct Padding (or Data Alignment)?

Answer: CPUs read memory faster if data is aligned to word boundaries (e.g., 4-byte boundaries on 32-bit systems). The compiler automatically inserts empty "padding" bytes between members.

c
1struct {
2 char a; // 1 byte
3 int b; // 4 bytes
4} my_struct;

sizeof(my_struct) is 8 bytes, not 5, because the compiler adds 3 padding bytes after char a. Use __attribute__((packed)) to prevent this.

8. What causes a Memory Leak, and what happens when an MCU runs out of Heap?

Answer: A leak occurs when you malloc() memory but never free() it. On a desktop, the OS cleans this up on exit. On an MCU running an infinite loop, the leaked memory is gone forever. Eventually, malloc() returns NULL. If your code doesn't check for NULL, it will trigger a Hard Fault and crash.


Part 2: Pointers and Memory Architecture

9. Can a pointer be volatile? Give an example.

Answer: Yes. There are two types:

  • volatile int *ptr; — The data is volatile (e.g., a pointer to a hardware register).
  • int * volatile ptr; — The pointer itself is volatile (e.g., an ISR modifies where the pointer points).

10. What is a wild pointer vs a dangling pointer?

Answer:

  • Wild Pointer: An uninitialized pointer that points to a random memory address.
  • Dangling Pointer: A pointer that points to memory that has already been free()'d or to a local stack variable that has gone out of scope.

11. Write a function to detect Little Endian vs Big Endian architecture.

Answer:

c
1bool is_little_endian() {
2 uint16_t num = 0x1234;
3 uint8_t *ptr = (uint8_t *)&num;
4 return (*ptr == 0x34); // If the lowest byte is stored first, it's Little Endian.
5}

12. What is the .bss segment? What happens to uninitialized global variables?

Answer: Uninitialized global and static variables are stored in the .bss (Block Started by Symbol) segment. Unlike .data (which takes up ROM space for initial values), .bss takes up zero space in ROM. The C startup code automatically clears the entire .bss segment to zero before main() is called.

13. Why does reading an unaligned 32-bit integer crash some ARM processors?

Answer: Many ARM Cortex-M processors (like Cortex-M0) lack hardware support for unaligned memory access. If you try to read a 32-bit uint32_t from an address that is not a multiple of 4 (e.g., address 0x20000001), the CPU will throw a Usage Fault and crash.

14. What does the restrict keyword do?

Answer: It is a promise to the compiler that for the lifetime of the pointer, no other pointer will access the same memory. This allows the compiler to aggressively optimize loops (e.g., using vector instructions) because it doesn't have to worry about memory aliasing.

15. Write a macro to swap two variables without using a temporary variable.

Answer: The XOR Swap trick.

c
1#define SWAP(a, b) do { (a) ^= (b); (b) ^= (a); (a) ^= (b); } while(0)

16. What is a Memory Leak vs Stack Overflow vs Heap Fragmentation?

Answer:

  • Leak: Forgetting to free() heap memory.
  • Stack Overflow: Calling too many nested functions or declaring massive arrays inside a function, exceeding the fixed stack limit.
  • Fragmentation: Allocating and freeing various sized blocks until the heap looks like Swiss cheese. Even if there is 10KB of total free memory, if the largest contiguous block is 1KB, a malloc(2KB) will fail.

Part 3: Compilation and Preprocessor

17. What is the difference between #define and inline functions?

Answer:

  • #define macros are dumb text replacements done before compilation. They have no type checking and can cause side-effect bugs (e.g., MAX(a++, b) increments a twice).
  • inline functions are real functions that the compiler tries to copy-paste into the call site to save function-call overhead. They provide strict type checking.

18. What is the difference between #include <file.h> and #include "file.h"?

Answer: Angle brackets < > tell the compiler to search the system/compiler include paths first. Quotes " " tell the compiler to search the current project directory first, and then fallback to system paths.

19. What is "Name Mangling" and why do we use extern "C"?

Answer: C++ allows function overloading (multiple functions with the same name). To differentiate them, the C++ compiler "mangles" the names by appending parameter types. C does not do this. If C++ code needs to call a C library, you wrap the C headers in extern "C" to tell the C++ compiler: "Don't mangle these names, they are standard C."

20. What are the 4 stages of C Compilation?

Answer:

  1. Preprocessing: Handles #include, #define, strips comments.
  2. Compilation: Translates C code into Assembly code.
  3. Assembly: Translates Assembly into Machine Code (Object files .o).
  4. Linking: Combines object files and libraries into the final executable binary.

21. What is a Linker Script (.ld file)?

Answer: A script that tells the Linker exactly where physical memory exists on the microcontroller. It maps the .text section to Flash memory, and the .data/.bss sections to RAM addresses.

22. Explain the do { ... } while(0) pattern in C macros.

Answer: It forces a multi-line macro to behave as a single safe statement, preventing catastrophic bugs when the macro is used inside an if statement that lacks curly braces.

c
1#define MACRO() do { func1(); func2(); } while(0)

Part 4: HARD MODE (Architecture & RTOS)

These questions test your deep architectural understanding. If you can answer these, you are ready for senior-level systems engineering.

23. Explain Reentrancy vs Thread-Safety. Can a function be thread-safe but not reentrant?

Answer:

  • Thread-Safe: Multiple RTOS tasks can call the function simultaneously without corrupting data (usually achieved by using a Mutex to lock shared resources).
  • Reentrant: A function can be interrupted in the middle of execution (by an ISR), and that ISR can safely call the same function without issue.
  • Yes: A function using a Mutex is thread-safe, but it is not reentrant because if an ISR interrupts it and tries to take the same Mutex, the system will deadlock! True reentrancy requires using only local stack variables and no locks.

24. What is Priority Inversion in an RTOS, and how does a Mutex solve it?

Answer: Priority Inversion happens when a Low-Priority Task acquires a lock on a resource. A High-Priority Task wakes up and wants the resource, but is blocked. Then, a Medium-Priority Task wakes up, preempts the Low task, and runs forever. The High task is now permanently blocked by a Medium task! Solution: Mutexes use Priority Inheritance. The RTOS temporarily boosts the Low task's priority to match the High task, allowing it to finish quickly, release the lock, and give control to the High task.

25. Write an Array of Function Pointers to implement a State Machine.

Answer: Switch statements are slow for massive state machines. Function pointer arrays execute in O(1) time.

c
1typedef void (*state_func_t)(void);
2
3void state_idle(void) { /* ... */ }
4void state_run(void) { /* ... */ }
5void state_error(void) { /* ... */ }
6
7// Array of function pointers
8state_func_t state_table[] = {
9 [0] = state_idle,
10 [1] = state_run,
11 [2] = state_error
12};
13
14// Execute current state
15int current_state = 1;
16state_table[current_state](); // Instantly calls state_run()

26. You are writing a DMA driver. The DMA buffer is updated by hardware, but the CPU reads old data. Why?

Answer: Cache Coherency. Modern MCUs (like STM32H7 or ESP32-S3) have CPU Data Caches. The CPU reads the buffer into its ultra-fast L1 cache. Meanwhile, the DMA hardware writes new data directly to the physical RAM. The CPU doesn't know the RAM changed, so it keeps reading the old data from its cache. You must manually invalidate the cache before reading the DMA buffer.

27. What is the "Volatile Struct Pattern" for mapping hardware peripheral registers?

Answer: Instead of using dozens of #define addresses, embedded engineers define a struct that perfectly maps to the hardware register layout, and cast a volatile pointer to the base address.

c
1typedef struct {
2 volatile uint32_t CR1; // Control Reg 1 (offset 0x00)
3 volatile uint32_t CR2; // Control Reg 2 (offset 0x04)
4 volatile uint32_t SR; // Status Reg (offset 0x08)
5} uart_hw_t;
6
7// Cast the base memory address to the struct pointer
8#define UART1 ((uart_hw_t *) 0x40011000)
9
10// Usage:
11UART1->CR1 |= 0x01; // Enable UART

28. Can you pass a local array as a parameter to a FreeRTOS task during creation? What happens?

Answer: No! When you call xTaskCreate(), the task is scheduled but doesn't necessarily run immediately. If the function that created the task returns, the local array is popped off the stack and destroyed. When the task finally runs and tries to read the array pointer, it reads corrupted stack memory. You must use global memory, malloc, or wait for the task to confirm it copied the data.

29. You have an ISR that increments a uint32_t counter. A main loop reads this counter. On an 8-bit MCU, the main loop occasionally reads a wildly incorrect value. Why?

Answer: Non-Atomic Reads. On an 8-bit MCU, reading a 32-bit variable takes 4 separate assembly instructions. If the main loop reads the first 2 bytes, and then the ISR fires and modifies all 4 bytes, the main loop will resume and read the last 2 bytes of the new value. The resulting 32-bit number is a corrupted mashup of old and new data. You must temporarily disable interrupts before reading the variable in the main loop.

30. What is a Watchdog Timer (WDT) and why is it dangerous to "kick" the dog inside an ISR?

Answer: A WDT is a hardware timer that resets the MCU if the software crashes (hangs in an infinite loop). To prevent a reset, the software must periodically "kick" (reset) the timer. If you put the kick function inside a hardware timer ISR, the ISR will continue to fire and kick the dog even if the main program crashes and freezes! The watchdog becomes useless. Kicking must be done in the main loop/task.

31. Explain the concept of a "Function Frame" (Stack Frame) on the call stack. What gets pushed?

Answer: When a function is called, a block of memory is pushed to the stack called a Stack Frame. It contains:

  1. The Return Address (where the CPU should go back to when the function finishes).
  2. The function's parameters/arguments.
  3. The function's local variables.
  4. Saved CPU registers. When the function returns, the Stack Pointer is simply decremented, instantly "freeing" the frame.

32. Why shouldn't you return a pointer to a local variable?

Answer: Local variables are allocated on the Stack Frame. When the function returns, its Stack Frame is invalidated and the memory is considered free to be overwritten by the next function call. The returned pointer becomes a dangling pointer, pointing to garbage memory.

Previous
ESP-IDF Patterns