Skip to content

Embedded Systems interview questions

Microcontrollers, peripherals, and the hardware-software boundary.

Practice all 10, gradedOne at a time. Free, no account.

What interviewers probe

  • MCU Architecture & Peripherals
  • Interrupts & ISR Design
  • DMA
  • RTOS & Scheduling
  • Memory & Stack Constraints
  • Low Power Modes

10 questions, easiest first

  1. 1You've just enabled the clock for a UART peripheral and immediately write a value to its baud rate register, but when you read it back you get zero (or the MCU appears to hang). Walk me through what's likely happening and how you'd confirm it on the bench.intro

    This is a common first bring-up mistake when a candidate is new to memory-mapped peripherals and clock trees.

    Answer this one, graded
    Or just show what a strong answer covers

    This smells like a clock-gating issue: many MCUs keep peripherals clock-gated off by default to save power, and you have to explicitly enable that peripheral's clock in the RCC/PCC block before its registers behave. If I wrote to the baud rate register before the clock enable took effect, the bus could return zero, a stale value, or in some architectures even fault. I'd check the clock enable bit for that peripheral first, confirm it's set, and check the datasheet's exact power-up sequence — clock enable, then reset release, then configuration, sometimes with a required delay or dummy read in between. To confirm, I'd step through in a debugger watching the raw register address before and after enabling the clock, or scope the peripheral's clock line to see when it actually starts toggling.

    • Identifies clock-gating/enable timing as likely cause — States that the peripheral's clock must be enabled (and possibly stable for a few cycles) before its registers behave normally, and that writing too soon can fail silently.
    • Distinguishes read-as-zero vs bus fault behavior — Recognizes that different architectures respond differently to accessing an unclocked peripheral (silent zero read vs hard fault) and that this depends on the bus/MCU.
    • Describes concrete debug steps — Mentions checking the clock-enable register bit, using a debugger's memory/register view, or reading a peripheral ID/status register to confirm the clock is active.
    • Proposes a verification and fix — Suggests adding a short delay or dummy read after enabling the clock and re-checking against the datasheet's power-up/enable sequence.

    Where answers go wrong: Assuming the hardware is broken without checking the clock-enable sequence first · Not knowing that unclocked peripherals can return stale or zero values instead of erroring · Skipping the datasheet's specified enable order (clock, then reset release, then configure)

  2. 2You write firmware that polls a peripheral status register in a tight loop, something like `while (STATUS_REG & BUSY_BIT);`, waiting for a hardware operation to finish. It works fine in a debug build, but the same code compiled with optimizations turned on for release hangs forever on that line. What is actually happening here, and what's the structural fix so this class of bug never comes back?intro
    Answer this one, graded
    Or just show what a strong answer covers

    The compiler doesn't know that STATUS_REG is a hardware register that changes outside the program's own writes, so with optimizations on it reads it once, keeps the value in a CPU register, and loops forever on that stale copy since nothing in the visible C code ever changes it. In the unoptimized debug build the compiler happens to re-read memory every time anyway, which is why the bug is invisible there — that's a coincidence of code generation, not a guarantee. The fix is to declare the register access as volatile, which tells the compiler this memory location can change asynchronously and must be re-read from the actual address on every access, never cached in a register or hoisted out of a loop. In practice this should be handled once, in the peripheral register struct or macro definition in the header, not patched at each call site — most vendor HAL headers already do this, so this bug usually shows up when someone hand-rolls a register pointer without volatile.

    • Root cause: optimizer caches the read — States that with optimization enabled the compiler hoists the register read out of the loop or keeps it in a register because it sees no reason the value would change.
    • Names volatile as the fix — Explicitly says the register (or pointer to it) must be declared volatile to force a fresh memory read each iteration.
    • Explains why debug vs release differs — Connects the symptom appearing only at higher optimization levels to the compiler being more aggressive about eliminating 'redundant' loads without optimization off.
    • Generalizes the fix — Notes this should be handled once in the register/peripheral definition (e.g., vendor header) rather than patched ad hoc at each call site.

    Where answers go wrong: Blaming the hardware or the peripheral itself instead of the compiler's optimization behavior. · Adding volatile to the wrong variable (e.g., a local loop counter) instead of the register access itself. · Not connecting the debug/release difference to optimization level.

  3. 3Your MCU runs at 48 MHz. On an interrupt request, the core needs 12 clock cycles to push context onto the stack, the interrupt controller adds 6 cycles of arbitration overhead when multiple sources are pending, and the vector fetch from flash takes 2 wait-state cycles. What is the minimum interrupt latency, in microseconds, from the interrupt assertion to the first instruction of the ISR executing?easy

    This checks whether the candidate can translate a cycle budget into a real time figure, a routine task when validating real-time deadlines.

    Answer this one, graded
    Or just show what a strong answer covers

    I'd add the cycle counts: 12 for context save, 6 for arbitration, 2 for vector fetch, giving 20 cycles total. At 48MHz the clock period is about 20.8ns, so latency is roughly 20 times 20.8ns, which is about 0.417 microseconds. That's a best-case number assuming nothing else is contending — in practice I'd want to measure it by toggling a GPIO at ISR entry and triggering a scope off the actual interrupt source to see the real-world latency, which could be higher if a lower-priority interrupt is currently masking it.

    • Sums the cycle counts correctly — Adds 12 + 6 + 2 = 20 total cycles before the ISR's first instruction.
    • Converts cycles to time using the clock period — Uses period = 1/48MHz ≈ 20.8ns per cycle to convert the cycle count into a time value.
    • Arrives at approximately the correct latency — Computes latency ≈ 0.417us (within a reasonable rounding tolerance).
    • Notes this is a best-case/minimum bound — States that real latency could be worse due to a currently-masked interrupt, higher priority servicing, or pipeline effects not captured in the given numbers.

    Where answers go wrong: Leaving the answer in cycles instead of converting to time · Inverting the clock frequency calculation (multiplying by 48MHz instead of dividing) · Treating this number as the typical latency rather than a theoretical minimum · Unit errors between ns and us

  4. 4Your MCU has 1KB of stack allocated for a task. Baseline stack usage before any function calls is 200 bytes (locals in main plus saved registers). Each additional nested function call in your worst-case call chain adds 40 bytes of frame overhead, and a worst-case interrupt preemption at any point in that chain needs an extra 150 bytes for its own context and locals. What is the maximum call depth you can nest before you risk overflowing the stack?easy

    Stack sizing on small MCUs with no MMU is a routine sanity check before shipping firmware.

    Answer this one, graded
    Or just show what a strong answer covers

    Available headroom for call frames, after reserving the worst-case interrupt hit, is 1024 minus 200 minus 150, which is 674 bytes. Dividing by 40 bytes per frame gives about 16.85, and since a partial frame can't happen, the safe limit is 16 nested calls, rounding down. In practice I wouldn't trust that number exactly, since real frame sizes vary with compiler optimization and register spilling, so I'd add margin and verify empirically — either with a compiler's stack-usage report, by painting the stack with a known pattern and checking the high-water mark after a stress test, or by placing an MPU guard region just past the stack to catch an overflow immediately.

    • Subtracts baseline and interrupt reserve before dividing — Computes (1024 - 200 - 150) as the space available for nested call frames before dividing by the per-call cost.
    • Rounds down to a safe integer — Arrives at 16 calls, explicitly rounding down from the fractional result rather than up.
    • Frames this as a theoretical worst case — Notes that real margin should be added since compiler frame sizes and additional interrupt nesting aren't perfectly predictable.
    • Names a real verification technique — Mentions at least one practical method to confirm stack usage empirically, such as stack painting/watermarking, compiler stack-usage reports, or a linker map/MPU guard region.

    Where answers go wrong: Forgetting to subtract the interrupt reserve before dividing · Rounding the fractional result up instead of down · Treating the calculated number as exact without acknowledging the need for margin · Not knowing any real-world method to verify stack usage on hardware

  5. 5You've written an ISR for a UART receive-not-empty interrupt. On every byte received it: converts the byte into a display string, runs a floating-point checksum update, and pushes a status message out over a second UART for debug logging. Under light traffic it works fine, but under sustained heavy traffic you start silently dropping received bytes. Walk through why, and how you'd redesign the ISR.easy
    Answer this one, graded
    Or just show what a strong answer covers

    The likely failure is a receive overrun: at a given baud rate, bytes arrive at a fixed interval, and if the ISR takes longer than that interval to finish, the hardware's single-byte receive register gets overwritten by the next byte before the ISR gets back to read it, so bytes are silently lost. All three things in this ISR make it slow — string formatting and floating-point checksum work are just computationally heavy for interrupt context, especially on an MCU without a hardware FPU where every float op costs a software emulation call, and worst of all the debug UART write, if it blocks until there's room in the transmit buffer, adds a completely unbounded delay since it depends on how fast that second UART is draining. The fix is to strip the ISR down to the minimum: read the received byte and push it into a ring buffer, increment an index, and return — nothing else. All the formatting, checksum computation, and debug logging get deferred to the main loop or a background task that drains the ring buffer at its own pace, using volatile head/tail indices to hand off data safely between the ISR and the deferred processing.

    • Identifies overrun as the failure mode — States that bytes are lost because the ISR takes longer than the inter-byte arrival interval, causing the hardware receive buffer/register to be overwritten before it's read.
    • Flags floating-point/string work as too slow for an ISR — Explicitly calls out the checksum and string conversion as computationally expensive operations that shouldn't run in interrupt context.
    • Flags the blocking debug UART write as the worst offender — Recognizes that a busy-wait or blocking transmit inside the ISR is the largest and least predictable source of added latency.
    • Proposes a concrete redesign — Describes moving the ISR down to just copying the received byte into a ring buffer (plus incrementing an index/flag) and deferring formatting, checksum, and logging to the main loop or a lower-priority task.

    Where answers go wrong: Suggesting the fix is simply to raise the interrupt's priority without addressing the actual work being done. · Proposing a bigger receive buffer without shortening the ISR itself. · Not recognizing that a blocking transmit inside an ISR is fundamentally different, and worse, than the other two operations.

  6. 6You set up a DMA channel to fill a 256-byte buffer from an ADC in circular mode, with an interrupt firing on transfer-complete to process the data. Occasionally the processed data looks like a mix of old and new samples, as if you read the buffer mid-transfer. Walk me through the likely causes and how you'd fix it.medium

    This is a classic hardware/software boundary bug that appears as soon as DMA is combined with any kind of double-use buffer.

    Answer this one, graded
    Or just show what a strong answer covers

    This looks like a buffer ownership race: in circular DMA mode, the channel may already be refilling the buffer from the start before I've finished processing the previous contents, so I end up reading a mix of old and new samples. First I'd check whether my interrupt is actually tied to full-transfer-complete or is mistakenly firing on half-transfer. Then I'd switch to a ping-pong double-buffer scheme, where DMA fills buffer A while I process buffer B from the last completed transfer, flipping the DMA target address in the ISR. If this core has a data cache, I'd also add a cache-invalidate on the buffer's address range before reading it, since DMA writes to RAM aren't automatically visible to a cached CPU. To confirm the actual overlap, I'd toggle a GPIO right when processing starts and scope it against the DMA transfer-complete signal to see the real timing relationship.

    • Identifies the CPU/DMA buffer race — States that the DMA (in circular mode) may already be refilling the buffer before the CPU finishes processing it, causing a mix of old and new data.
    • Proposes double buffering as the fix — Suggests a ping-pong scheme where DMA fills one buffer while the CPU processes the other, swapping roles on each transfer-complete.
    • Considers cache coherency — Mentions that on a cached core, a cache invalidate on the buffer's address range is needed before the CPU reads DMA-written memory.
    • Checks for a wiring/config mistake — Considers that the interrupt might be firing on half-transfer instead of full-transfer completion.
    • Proposes a concrete confirmation method — Suggests toggling a GPIO at the start of processing and scoping it against the DMA transfer-complete signal to see actual timing overlap.

    Where answers go wrong: Blaming ADC noise instead of a buffer race condition · Assuming a transfer-complete interrupt guarantees full visibility of DMA-written memory on a cached core without cache maintenance · Not knowing the double-buffering/ping-pong pattern exists · Overlooking that the interrupt could be tied to half-transfer rather than full-transfer completion

  7. 7Your board is supposed to draw under 10uA in deep sleep, but your bench measurement shows a steady 2mA instead. Walk me through how you'd track down where the current is going.medium

    Low-power validation is one of the first hands-on measurement exercises an intern is likely to run.

    Answer this one, graded
    Or just show what a strong answer covers

    First I'd confirm the part is really in the deep sleep mode I think it's in, by reading back the power-mode status register or comparing measured current against the datasheet's expected value for that exact mode, since sleep modes vary a lot in what they keep alive. Then I'd isolate systematically: use a current probe or precision source-meter in series with the supply, and do something like a binary search — disconnecting or depopulating board sections such as sensors, LEDs, or unused regulators to see when the 2mA drops. Common culprits are floating GPIO inputs drawing through internal pull networks, an external LED or pull-up still tied to an always-on rail, a peripheral clock or regulator I forgot to gate before sleeping, or a wake-source like a UART RX line forcing a subsystem to stay active. I'd also double-check my measurement setup itself — whether a debugger is still attached, or whether periodic wake events are skewing an averaged multimeter reading that isn't fine-grained enough to resolve microamps.

    • Proposes systematic isolation — Suggests disconnecting or depopulating subsystems one at a time, or a binary-search approach on the schematic, to localize the excess current.
    • Verifies the actual sleep mode entered — Checks a power-mode status register or debugger readout to confirm the part is truly in the intended deep-sleep state, not just idling.
    • Considers GPIO and external-component leakage — Names floating/misconfigured GPIO pins, unswitched pull-ups, LEDs, or peripheral ICs as common current sinks left active during sleep.
    • Uses appropriate measurement technique — Mentions using a precision current probe/source-meter capable of resolving microamp currents rather than a standard multimeter, and accounts for wake-event spikes affecting the average reading.
    • Considers wake-source misconfiguration — Notes that a peripheral kept active to service a wake source (e.g., a UART RX line or timer) could be preventing full power-down of that subsystem.

    Where answers go wrong: Trusting the datasheet's typical current figure without verifying the actual configuration used · Not confirming the part truly entered deep sleep versus just idling · Overlooking external hardware like LEDs or pull-ups as the dominant leakage source · Measuring with an instrument that can't resolve microamp-scale current, especially with periodic wake spikes present

  8. 8You're using DMA to fill a 512-byte SRAM buffer from an ADC on an MCU that has a data cache enabled. After the DMA transfer-complete interrupt fires, your processing code reads the buffer and sometimes gets stale, old sample values even though you can confirm on a debugger that the DMA definitely wrote fresh data to that memory address. What's the likely cause, and what are two different ways you could fix it?medium
    Answer this one, graded
    Or just show what a strong answer covers

    This is a classic cache coherency problem: the DMA controller is a separate bus master that writes directly into SRAM, and it has no way to tell the CPU's data cache that the underlying memory changed. If the CPU had previously read from that address range — say, on a prior pass through the loop — it may still be serving reads out of a stale cache line even though the physical RAM now holds fresh ADC samples. The key fix is to invalidate, not flush, the CPU's cache lines covering that buffer address range right after the DMA-complete interrupt fires and before the processing code reads it — invalidate discards the stale cached copy so the next read goes to RAM, whereas flush is for the opposite direction, pushing CPU-modified data out to RAM. A second, simpler fix is to mark that buffer's memory region as non-cacheable in the MPU, so the CPU always reads it directly from RAM and there's never a stale copy to worry about, at the cost of slightly slower CPU access whenever you do read from it. I'd pick cache maintenance calls if performance on that buffer matters and non-cacheable if the buffer is read infrequently and I want to eliminate an entire class of bugs.

    • Identifies cache coherency as root cause — States that the CPU's data cache holds a stale copy of the buffer because DMA writes to RAM bypass the cache and don't trigger any automatic invalidation.
    • Correctly distinguishes invalidate from flush — Uses or implies the correct direction: the CPU needs to invalidate (discard) its cached copy before reading DMA-written data, not flush it.
    • Gives a working fix #1: explicit cache maintenance — Describes calling a cache-invalidate operation on the buffer's address range after the DMA-complete interrupt fires and before the CPU reads it.
    • Gives a working fix #2: non-cacheable memory region — Describes configuring the buffer's address range as non-cacheable (e.g., via MPU attributes) so the CPU always reads directly from RAM.

    Where answers go wrong: Blaming a DMA controller malfunction or a race in the transfer-complete flag rather than the cache. · Confusing cache flush and cache invalidate, or saying 'flush the cache' when reading fresh DMA data actually requires invalidate. · Forgetting that cache invalidate operations typically require the buffer be cache-line aligned and sized to avoid corrupting adjacent unrelated data.

  9. 9You have three tasks in an RTOS: a low-priority task that periodically takes a mutex to write to a shared log buffer, a high-priority task that also needs that same mutex briefly, and a medium-priority task that runs a CPU-heavy computation with no locking at all. In testing, you notice the high-priority task occasionally misses its deadline by tens of milliseconds even though its own work takes only microseconds. Explain what's happening and how you'd fix it.hard

    This is the canonical priority inversion scenario used to test whether a candidate understands scheduling interactions beyond simple priority ordering.

    Answer this one, graded
    Or just show what a strong answer covers

    This is classic unbounded priority inversion. The low-priority task grabs the mutex, and before it can release it, the medium-priority task preempts it — since medium beats low in priority, it keeps running even though it never touches the mutex, and it holds off the low-priority task from ever finishing its critical section. Meanwhile the high-priority task is stuck blocked on that same mutex, so its tiny amount of actual work gets delayed by tens of milliseconds through no fault of its own code. The fix is to use a mutex implementation that supports priority inheritance, so while the low-priority task holds the lock it gets temporarily boosted to the priority of whoever is waiting on it, preventing the medium-priority task from preempting it during that window. Priority ceiling protocol is a stricter variant that boosts to the highest priority that could ever contend for that resource. I'd also keep the critical section as short as possible and avoid having high-priority tasks block on any resource shared with lower-priority code where avoidable. To confirm this diagnosis rather than guess, I'd use the RTOS's tracing tools to look at actual task state transitions and mutex hold times during a failing run.

    • Correctly diagnoses priority inversion — Explains that the low-priority task holds the mutex, gets preempted by the medium-priority task, and the high-priority task is left blocked waiting on that mutex.
    • Explains the medium-priority task's indirect role — Notes that the medium-priority task never touches the mutex but still causes the stall simply by preempting the mutex holder.
    • Proposes priority inheritance or ceiling protocol — Names a mechanism where the low-priority holder is temporarily boosted to a higher priority while holding the mutex so it can't be preempted by unrelated tasks.
    • Suggests a complementary mitigation — Recommends shortening critical sections and/or avoiding blocking calls on shared resources from high-priority tasks.
    • Mentions verification via tracing — Suggests using RTOS trace/profiling tools to confirm task state transitions and mutex hold durations rather than guessing.

    Where answers go wrong: Assuming the high-priority task's own code is slow, without looking at what's blocking it · Simply raising the high-priority task's priority further, which doesn't address the underlying inversion · Failing to notice the medium-priority task's role since it never touches the mutex at all · Not realizing many RTOS mutex implementations already support priority inheritance and just need to be enabled

  10. 10You're scoping firmware architecture for a battery-powered sensor node: it samples three sensors on different schedules (10Hz, 1Hz, and on-demand triggered by a button press), does light filtering on the results, and transmits over a low-power radio. You have about 32KB of flash and 8KB of RAM to work with. Walk through how you'd decide between a bare-metal super loop with a simple scheduler versus a small RTOS, and what specifically would change your answer.hard
    Answer this one, graded
    Or just show what a strong answer covers

    With only three tasks — two periodic and one asynchronous button event — and an 8KB RAM budget, I'd lean toward a bare-metal super loop built around a timer tick and a simple state machine: each task gets serviced when its schedule comes due, checked with flags set by a timer interrupt, and the button just sets its own flag from a GPIO interrupt. That avoids the RTOS kernel's flash footprint and, more importantly, avoids needing a separate stack for every task, which adds up fast against 8KB when each task might need a few hundred bytes of headroom. An RTOS would genuinely help if the button needed to preempt an in-progress, longer-running task deterministically, or if I wanted the scheduler's tickless idle support to automatically drop into the lowest sleep state between the 10Hz and 1Hz windows without hand-rolling that logic myself — that's a real advantage for battery life. What would flip my answer: if the radio's vendor driver only exposes an RTOS-based API with blocking send/receive calls, I'd effectively be forced onto an RTOS regardless of the RAM cost; similarly, if the task count grows past four or five with real priority differences between them, hand-rolling the scheduling logic in a super loop starts to become its own maintenance burden and an RTOS starts paying for itself.

    • Weighs RTOS memory cost against the actual budget — Explicitly connects RTOS kernel and per-task stack overhead to the 8KB RAM constraint given, not just as an abstract concern.
    • Recognizes a super loop can handle this workload — Argues that a timer-driven state machine handling three periodic/on-demand tasks doesn't necessarily require a scheduler, given how few and how simple the tasks are.
    • Identifies where an RTOS would genuinely help here — Names a specific advantage relevant to this scenario, such as cleanly prioritizing the asynchronous button event over periodic sampling, or using tickless idle to simplify sleep entry across mixed schedules.
    • States a concrete condition that changes the answer — Gives at least one specific scenario change (e.g., the radio driver requiring RTOS blocking APIs, or the task count/complexity growing) that would flip the decision either direction.

    Where answers go wrong: Treating an RTOS as a strictly superior default choice without weighing its memory cost. · Ignoring that entering low-power sleep gets more complex under a scheduler unless tickless idle is explicitly used. · Giving a generic definition of RTOS benefits without tying any of it back to the specific 32KB/8KB budget and the three described tasks.

Next topicFirmware Fundamentals

Interviewing for a specific role?

Paste the job description and we weight embedded systems against everything else the role needs.