Interview questions & answers
Q1. Why is the volatile keyword necessary in embedded C and when must you use it?
volatile tells the compiler that the value of a variable can change outside the normal program flow, preventing it from being optimized into a register cache that the program never refreshes. You must use it for memory-mapped peripheral registers, variables modified inside ISRs, and shared variables in multi-threaded code — for example, the STM32 HAL defines USART1->SR as volatile so every read actually accesses the hardware register, not a cached value. Omitting volatile on a status register that an ISR sets to 1 can cause an infinite while(!flag) loop that the compiler optimizes into while(1) because it proves flag never changes in the main flow.
Follow-up: Does volatile provide any synchronization guarantee on a multi-core processor?
Q2. What is the difference between const and volatile, and can a variable be both?
const tells the compiler the program will not modify the variable, while volatile tells the compiler the value may change externally; a variable can legitimately be both, declaring that the program won't write it but hardware might. The classic example is a read-only hardware status register: const volatile uint32_t *UART_STATUS = (uint32_t*)0x40011000; — the const prevents accidental writes in software while volatile prevents the compiler from caching the read. Declaring a const volatile pointer to a hardware register is standard practice in CMSIS device headers and is not a contradiction.
Follow-up: What does it mean to have a pointer to volatile versus a volatile pointer?
Q3. How do you set, clear, toggle, and read a specific bit in a register using C?
Use bitwise OR to set (REG |= (1 << n)), AND with NOT to clear (REG &= ~(1 << n)), XOR to toggle (REG ^= (1 << n)), and AND with shift to read ((REG >> n) & 1). For example, to set bit 5 of GPIOA's ODR on STM32: GPIOA->ODR |= (1 << 5); and to clear it: GPIOA->ODR &= ~(1 << 5). Never use read-modify-write on a register where individual bits are write-1-to-clear (like status flags), because reading the register to OR a bit may simultaneously clear an unrelated pending flag.
Follow-up: What is a bit-band region and how does it provide atomic bit access on Cortex-M3/M4?
Q4. What is a function pointer in C and how is it used in embedded systems?
A function pointer is a variable that stores the address of a function and allows calling it indirectly, enabling runtime dispatch without switch-case chains. In embedded systems, interrupt vector tables are arrays of function pointers — the STM32 startup file defines the vector table as __attribute__((section(".isr_vector"))) const pFunc __Vectors[] where pFunc is typedef void(*pFunc)(void). State machines, plugin architectures, and callback registrations (like HAL_UART_RegisterCallback in STM32 HAL) all use function pointers to decouple the caller from the implementation.
Follow-up: How do you declare a pointer to a function that takes an int and returns a float?
Q5. What is the difference between static local and static global variables in C?
A static local variable retains its value across function calls but is only visible within the function, while a static global variable has file scope (not visible in other translation units) but persists for the program lifetime. In a UART driver, a static local buffer index like static uint8_t rxIndex = 0 inside a receive ISR persists the count across each interrupt call without exposing it globally. Using static on a global intentionally limits its linkage scope, which is a good practice to prevent naming conflicts in large firmware projects with multiple .c files.
Follow-up: Where in memory (which section) does a static variable reside?
Q6. What are the .text, .data, .bss, and .rodata sections in an embedded binary?
.text holds compiled machine code instructions, .rodata holds read-only constants like string literals, .data holds initialized global and static variables and is copied from flash to RAM at startup, and .bss holds uninitialized globals that are zero-filled at startup. On STM32, the startup file copies the .data section LMA (in flash) to its VMA (in RAM) and then zeros .bss before calling main(), and the linker script defines the LMA and VMA addresses. Forgetting to initialize .bss can cause sporadic bugs where global variables start with garbage values, which happens if you skip the startup file when porting to a custom toolchain.
Follow-up: What does LMA and VMA mean in the context of a linker script?
Q7. How do you write a structure that maps exactly to a hardware register block in C?
Define a packed struct with uint32_t or uint8_t members matching the register offsets, then cast a pointer to the peripheral's base address to a pointer to that struct. For example, the STM32 GPIO peripheral has typedef struct { volatile uint32_t MODER; volatile uint32_t OTYPER; ... } GPIO_TypeDef; and GPIOA is defined as ((GPIO_TypeDef *)0x40020000). The volatile qualifier on each member is essential, and if the hardware has reserved gaps, use uint32_t RESERVED[N] placeholder members to maintain correct offsets — misalignment by even one register offset causes silently wrong behavior.
Follow-up: What compiler attribute ensures no padding is added to a register-mapped struct?
Q8. What is an endianness issue and how does it affect embedded C code?
Endianness refers to the byte order of multi-byte integers: little-endian (ARM Cortex-M default) stores the least significant byte at the lowest address, big-endian stores the most significant byte first. When a Cortex-M4 receives a 32-bit value over a CAN bus or Ethernet from a big-endian device, the bytes must be swapped using __REV() or ntohl() before interpretation — reading CAN frame data bytes directly into a uint32_t on ARM gives a reversed value. Endianness bugs are common at protocol boundaries between ARM MCUs and DSPs or network stacks, and they produce correct-looking but numerically wrong values.
Follow-up: How do you detect the endianness of a system at runtime in C?
Q9. What is the difference between malloc and a memory pool in embedded C?
malloc uses a general heap and can fragment memory over time until allocation fails unpredictably, while a memory pool pre-allocates a fixed number of fixed-size blocks and allocation is O(1) with no fragmentation risk. In an RTOS-based CAN message handler, a pool of 32 fixed-size CAN_Frame_t blocks allocated at startup lets the ISR grab a frame object in under 10 cycles without risk of malloc failure mid-operation. Embedded firmware for safety-critical systems (ISO 26262, IEC 61508) typically bans dynamic heap allocation entirely and mandates memory pool or static allocation only.
Follow-up: How do you implement a simple memory pool in C without malloc?
Q10. How does the restrict keyword affect pointer behavior in embedded C?
restrict tells the compiler that the memory pointed to by that pointer is not aliased by any other pointer in the same scope, allowing it to generate more aggressive load/store optimizations and avoid redundant memory reads. In a DSP-style memcpy replacement on Cortex-M4, void fastcopy(uint32_t * restrict dst, const uint32_t * restrict src, int n) allows the compiler to use LDRD/STRD double-load instructions without checking for overlap. Omitting restrict when aliasing truly does not occur leaves performance on the table; using it when aliasing is possible causes undefined behavior and incorrect results.
Follow-up: What is pointer aliasing and why does it prevent certain compiler optimizations?
Q11. What is a reentrant function and why does it matter in ISR design?
A reentrant function produces correct results when called simultaneously by two or more contexts — it uses only local variables or parameters, never static locals or global state. An ISR that calls a non-reentrant function like strtok() or rand() is dangerous because if the same function is running in the main loop when the interrupt fires, the shared static internal state gets corrupted. In embedded firmware, ISRs should call only reentrant or interrupt-safe functions; if logging is needed, push to a ring buffer and let the main loop drain it outside interrupt context.
Follow-up: Is printf reentrant? What is the consequence of calling it in an ISR?
Q12. How do you implement a circular (ring) buffer in embedded C?
A ring buffer uses a fixed-size array with two indices — head (write) and tail (read) — where head advances on write and tail advances on read, both wrapping modulo the buffer size. For a UART receive buffer of 256 bytes: uint8_t buf[256]; uint8_t head=0, tail=0; and the ISR does buf[head++] = USART1->DR; (head wraps automatically with uint8_t overflow if size is 256). The buffer is full when (head+1)%SIZE == tail and empty when head == tail; making the buffer size a power of two replaces the modulo with a cheap bitmask.
Follow-up: How do you make a ring buffer safe for use between one ISR writer and one task reader without a mutex?
Q13. What does the __attribute__((packed)) do and when should you use it carefully?
__attribute__((packed)) tells GCC to remove all padding from a struct, making members byte-aligned regardless of their natural alignment. It is used when defining protocol packet structures that must map to a wire format exactly — for example, a CAN frame header struct where a uint16_t message ID must immediately follow a uint8_t with no padding byte. The risk is that accessing a misaligned uint32_t inside a packed struct on an ARM Cortex-M0 (which has no hardware unaligned access support) generates a HardFault; on M4 it works but is slower due to two bus cycles for the unaligned read.
Follow-up: What is the safe alternative to packed structs for protocol serialization on strict-alignment processors?
Q14. What is the difference between #define and const for constants in embedded C?
A #define constant is a preprocessor text substitution with no type, no scope, and no debugger visibility, while a const variable has a type, obeys scope rules, appears in debug symbols, and can be placed in .rodata by the compiler. For MCU register bit definitions like #define USART_CR1_UE (1<<13), #define is acceptable for bit masks; but for physical constants used in calculations like static const float VREF = 3.3f, const gives type-safe usage and appears in the watch window during debugging. Modern embedded C style guides (MISRA-C) prefer const over #define for all non-mask constants to enable type checking.
Follow-up: Can a const variable be used as an array dimension in C (not C++)?
Q15. How do you detect and prevent stack overflow in an embedded C application without an RTOS?
Paint the stack area with a known pattern (0xDEADBEEF) at startup and periodically check whether the pattern near the stack bottom has been overwritten. In a bare-metal STM32 project, set aside the last 64 bytes of the stack in the linker script, fill them with 0xDEADBEEF in the startup code, and poll them in a periodic SysTick handler. A more robust hardware approach uses the Cortex-M MPU to mark the stack guard region as no-access, generating a MemManage fault on overflow before any data is corrupted.
Follow-up: What is the ARM Cortex-M MPU and how can it be used as a stack guard?
Common misconceptions
Misconception: volatile makes a variable thread-safe between two tasks on a single-core MCU.
Correct: volatile only prevents compiler register caching; it does not prevent preemption between a read and a write, so multi-byte accesses still need atomic protection via interrupt disabling.
Misconception: static variables inside a function are stored on the stack.
Correct: static local variables are stored in the .data or .bss section in RAM, not on the stack, so they persist across function calls and survive returns.
Misconception: Using #pragma pack(1) is equivalent to __attribute__((packed)) across all compilers.
Correct: #pragma pack is MSVC syntax and behavior varies between compilers; __attribute__((packed)) is GCC/Clang specific, and portability requires using both or abstracting behind a macro.
Misconception: A const pointer and a pointer to const are the same thing in C.
Correct: const int *p is a pointer to a constant int (the value cannot change), while int * const p is a constant pointer to int (the pointer address cannot change); these are completely different declarations.