Skip to content
RED SAGSkills

Embedded Systems Engineer interview questions and answers

30 questions interviewers really ask, with what a strong answer covers. Read them, then test yourself in a timed mock interview.

Start a Embedded Systems Engineer mock interview

Easy questions easy

1. What is the difference between a microcontroller and a microprocessor?

A strong answer covers:

  • Microcontroller has CPU, flash, RAM and peripherals on one chip
  • Microprocessor needs external memory and peripherals
  • MCUs are low cost, low power, for dedicated tasks
  • MPUs run rich operating systems like Linux
  • Examples of each
Show model answer

A microcontroller, or MCU, puts a CPU core, flash memory for the program, RAM and peripherals such as GPIO, timers, ADC, UART, SPI and I2C on a single chip. It is designed for a dedicated job, like controlling a washing machine, with low cost and very low power, and usually runs bare-metal code or a small RTOS. Examples are STM32 with Cortex-M cores, ESP32 and the RP2040. A microprocessor, or MPU, is a more powerful CPU that needs external RAM, storage and often extra chips for peripherals. It typically has a memory management unit and runs a full operating system like Linux or Android. Examples are the ARM Cortex-A chips in a Raspberry Pi or phones. Choosing between them depends on the need: an MCU for real-time control and battery life, an MPU for rich UIs, networking stacks and heavy processing.

2. What is an interrupt, and how is it different from polling?

A strong answer covers:

  • Hardware signal that pauses normal code to run an ISR
  • Polling repeatedly checks a status in a loop
  • Interrupts are efficient and responsive; polling is simple but wastes CPU
  • Interrupt priorities and nesting
  • ISRs should be short
Show model answer

An interrupt is a signal, usually from hardware like a timer, GPIO pin or UART, that makes the CPU pause its current code, save its state and jump to an interrupt service routine through the vector table. When the ISR finishes, the CPU continues where it left off. Polling is the alternative: the main loop keeps checking a status flag, for example "has a byte arrived?". Polling is simple and predictable, but it wastes CPU time and power and may react late if the loop is busy. Interrupts respond quickly and let the CPU do other work or sleep until something happens. On Cortex-M, the NVIC handles priorities, so a high-priority interrupt can preempt a lower one. ISRs should be short: copy the data, set a flag or notify a task, and let the main code do the heavy processing.

3. Compare UART, SPI and I2C. When would you use each?

A strong answer covers:

  • UART: asynchronous, two wires, point-to-point, baud rate
  • SPI: synchronous, full duplex, fast, chip select per device
  • I2C: synchronous, two wires, multiple devices by address, slower
  • Use cases for each
Show model answer

UART is asynchronous: there is no shared clock, so both sides agree on a baud rate like 115200. It uses TX and RX lines and connects two devices, which makes it great for debug consoles, GPS modules and modems. SPI is synchronous, with a clock from the controller plus MOSI, MISO and one chip-select line per device. It is full duplex and fast, often tens of MHz, so I use it for displays, SD cards, flash memory and high-speed ADCs. I2C uses only two wires, SDA and SCL, with pull-up resistors, and many devices share the bus, each with an address. It is slower, typically 100 kHz or 400 kHz, and half duplex, but very convenient for sensors, EEPROMs and RTC chips when pins are limited. So: UART for simple point-to-point, SPI for speed, I2C for many low-speed devices on few pins.

4. Explain volatile and const in embedded C. Can a variable be both?

A strong answer covers:

  • volatile: value can change outside program control, compiler must not optimise accesses away
  • Used for hardware registers, ISR-shared variables
  • const: program must not modify it; may be placed in flash
  • volatile const for a read-only hardware status register
  • volatile does not guarantee atomicity
Show model answer

volatile tells the compiler that a variable can change in ways it cannot see, so every read and write must really happen in memory and not be cached in a register or removed. I use it for memory-mapped hardware registers, variables shared between an ISR and the main loop, and variables changed by DMA. const means my code is not allowed to modify the value, and constant data can be placed in flash instead of RAM, which is important when RAM is small. Yes, a variable can be both: a read-only hardware status register is a good example. My code must not write it, so it is const, but the hardware changes it, so it is volatile. One important point: volatile does not make operations atomic or thread-safe, so a shared counter still needs a critical section or atomic operations.

5. When would you use bare-metal firmware versus an RTOS?

A strong answer covers:

  • Bare-metal: super loop with interrupts, simple and small
  • RTOS: tasks, priorities, scheduling, synchronisation
  • RTOS helps with many concurrent activities and timing needs
  • Costs: RAM, complexity, learning curve
  • Connectivity stacks often need an RTOS
Show model answer

Bare-metal firmware usually means a super loop in main that checks flags and calls functions, plus interrupts for time-critical events. It is small, easy to understand and has no extra overhead, so I use it for simple devices like a temperature controller or a remote with a few inputs. An RTOS like FreeRTOS or Zephyr adds tasks with priorities, a preemptive scheduler, queues, semaphores and timers. It becomes worth it when the device does many things at once with different timing needs, such as reading sensors, running a BLE or Wi-Fi stack, updating a display and logging, because each activity can be a separate task and a high-priority task always responds on time. The costs are extra RAM for stacks, more complexity, and new bug types like deadlocks and priority inversion. Many vendor connectivity stacks already require an RTOS, which often makes the decision for you.

6. What GPIO modes does a typical microcontroller pin support, and when would you use each?

A strong answer covers:

  • Input with pull-up, pull-down or floating
  • Push-pull output for driving LEDs and logic
  • Open-drain output for shared lines like I2C
  • Alternate function to connect the pin to a peripheral
  • Analog mode for ADC/DAC and lowest power
Show model answer

Most MCU pins can be digital inputs, digital outputs, alternate function or analog. As an input I choose a pull-up or pull-down so the pin never floats, for example a pull-up for a button to ground. For outputs, push-pull drives the line both high and low, which suits LEDs and chip selects, while open-drain only pulls low and needs an external pull-up, which lets several devices share a line like I2C or an interrupt line. Alternate function hands the pin to a peripheral such as UART, SPI, a timer or PWM output. Analog mode connects the pin to the ADC or DAC and disables the digital input buffer, which is also the lowest-power setting for unused pins. I also set output speed and drive strength, because fast edges cause noise and EMI.

7. What is PWM, how is it generated on a microcontroller, and what is it used for?

A strong answer covers:

  • A square wave with fixed frequency and variable duty cycle
  • Average voltage or power is proportional to duty cycle
  • Generated by a timer in compare mode with a period and compare value
  • Used for LED dimming, motor speed, servos and simple DACs
  • Frequency choice matters: flicker, audible noise, switching loss
Show model answer

PWM, or pulse width modulation, is a square wave at a fixed frequency where we change how long it stays high in each period, the duty cycle. The average power delivered is proportional to the duty cycle, so 25 percent on gives about a quarter of full power. On an MCU a hardware timer generates it: the auto-reload value sets the period, and a compare register sets when the output toggles, so the CPU does no work per cycle. Common uses are LED dimming, motor speed control through an H-bridge, hobby servos, which read the pulse width, and simple DACs with an RC filter. Choosing the frequency matters: LEDs need it high enough to avoid visible flicker, motors usually above 20 kHz to avoid audible whine, but higher frequencies raise switching losses in the drivers.

8. What is a bootloader in an embedded device, and why would a product need one?

A strong answer covers:

  • First code to run after reset, in its own flash region
  • Validates the application, for example with a CRC or signature
  • Can receive and write new firmware over UART, USB, CAN or wireless
  • Jumps to the application by setting the stack pointer and vector table
  • Enables field updates and recovery without a debugger
Show model answer

A bootloader is a small program in its own region of flash that runs first after every reset. Its job is to decide what to run: it checks that the application image is valid, often with a CRC or, for secure products, a cryptographic signature, and then jumps to it by loading the application's stack pointer and reset vector and moving the vector table offset. If an update is requested or the application is broken, the bootloader stays active and receives a new image over UART, USB, CAN or a wireless link, writes it to flash and verifies it. Products need one because firmware will have bugs and new features after shipping, and you cannot attach a debugger to thousands of devices in the field. A good bootloader is small, rarely changed and can always recover the device.

9. What is endianness, and how do you deal with it when exchanging data between devices?

A strong answer covers:

  • Order in which bytes of a multi-byte value are stored
  • Little-endian stores least significant byte first; big-endian most significant first
  • Most MCUs are little-endian; network byte order is big-endian
  • Define the byte order in the protocol and convert explicitly with shifts
  • Avoid sending raw structs because of padding and byte order
Show model answer

Endianness is the order in which a multi-byte value is stored in memory. Little-endian puts the least significant byte at the lowest address, and big-endian puts the most significant byte first. Most Cortex-M and RISC-V MCUs are little-endian, while many network protocols and some sensors use big-endian, often called network byte order. When devices exchange data, I make the byte order part of the protocol specification and convert explicitly, building values with shifts, such as (b[0] << 8) | b[1], instead of casting a byte pointer to a wider type. That code works on any CPU and also avoids alignment problems. I avoid sending raw C structs because padding and byte order differ between compilers and processors, and I write unit tests with known byte sequences.

10. How do you approach reading a datasheet for a new sensor or microcontroller?

A strong answer covers:

  • Start with overview, block diagram and pinout
  • Check electrical characteristics and absolute maximum ratings
  • Study the interface: protocol, timing diagrams, register map
  • Look for power-up sequence, reset and initialisation steps
  • Read errata and application notes
Show model answer

I start with the overview and block diagram to understand what the part does, then the pinout and package. Next I check the electrical characteristics: supply range, current in each mode, logic levels, and the absolute maximum ratings so the design stays safe. For the interface I read the protocol section closely, such as I2C address, SPI mode or maximum clock, along with the timing diagrams and the register map with reset values. I look for the power-up and initialisation sequence, for example a startup delay or a required configuration write before measuring. I always read the errata sheet, because silicon bugs can waste days, and the application notes for recommended circuits and example code. While reading, I note key numbers and open questions for the hardware engineer, and later compare the logic analyser trace with the timing diagrams.

Medium questions medium

1. How would you write a driver for a new I2C temperature sensor?

A strong answer covers:

  • Read the datasheet: address, registers, timing, conversion time
  • Layered design: sensor driver over an I2C bus abstraction
  • Clear API: init, read, configure, with error codes
  • Handle timeouts, NACKs and retries
  • Test with a logic analyser and unit tests with a mocked bus
Show model answer

I start with the datasheet: the I2C address, register map, power-on timing, conversion time, data format and any CRC. Then I design layers. The sensor driver does not call the MCU's HAL directly; it uses a small I2C interface, like function pointers for write and read, passed in at init. This makes it portable across MCUs and easy to unit test with a fake bus. The API is simple: sensor_init, sensor_read_celsius, sensor_set_mode, each returning an error code instead of failing silently. Inside, I check the chip ID at init, respect conversion delays, convert raw values with the datasheet formula using fixed point if there is no FPU, and handle NACKs and timeouts with a limited number of retries, plus bus recovery if SDA gets stuck low. I verify the real traffic with a logic analyser, compare readings with a reference thermometer, and write unit tests for the conversion and error paths.

2. Explain the difference between a mutex and a semaphore in an RTOS, with examples.

A strong answer covers:

  • Mutex protects a shared resource; has an owner and must be released by the same task
  • Mutex supports priority inheritance
  • Binary semaphore is used for signalling, for example from ISR to task
  • Counting semaphore tracks multiple resources or events
  • Do not take mutexes in ISRs
Show model answer

A mutex is for mutual exclusion around a shared resource, like an SPI bus or a UART used by several tasks. It has an owner: the task that takes it must give it back. In FreeRTOS, mutexes support priority inheritance, which prevents priority inversion when a low-priority task holds the lock. A semaphore is mainly for signalling and counting. A binary semaphore is like a flag: an ISR gives it when data is ready, and a task blocked on it wakes up and processes the data. In FreeRTOS, direct task notifications are a faster option for this. A counting semaphore tracks several identical resources, for example five free buffers, or counts events that arrive faster than they are processed. Mutexes should not be used from ISRs, because an ISR cannot block or own a lock. Using a semaphore where a mutex is needed loses priority inheritance and ownership checks.

3. A device freezes randomly once every few days in the field. How do you debug it?

A strong answer covers:

  • Gather data: logs, reset reason, conditions, units affected
  • Add fault handlers and save crash info in non-volatile or no-init memory
  • Watchdog to recover while you investigate
  • Check for stack overflow, memory corruption, deadlocks and race conditions
  • Reproduce with stress testing, environment changes and hardware tools
Show model answer

First I collect data: which units, firmware versions and conditions, like temperature, power events or network loss. If possible I read the reset reason register and any logs. Next I make the device tell me more when it fails: a HardFault handler that saves the stacked PC, LR and fault registers to no-init RAM or flash, a task-level watchdog that records which task stopped checking in, and a small ring buffer of recent events. The watchdog also gets the device back online while I investigate. Then I look at the usual suspects: stack overflow, so I check high-water marks; heap fragmentation; deadlock between two mutexes; race conditions on shared data; counter overflows after 49 days with a 32-bit millisecond tick; and peripherals like I2C buses getting stuck. To reproduce, I run stress tests, speed up timers, and vary power and temperature, using a debugger, logic analyser or oscilloscope to catch it.

4. Explain the memory layout of a typical microcontroller program: .text, .data, .bss, stack and heap.

A strong answer covers:

  • .text holds code and constants in flash
  • .data holds initialised globals, copied from flash to RAM at startup
  • .bss holds zero-initialised globals, cleared at startup
  • Stack for local variables and calls; heap for dynamic allocation
  • Linker script defines regions; map file shows usage
Show model answer

The .text section holds the program code and usually read-only constants (.rodata), and lives in flash. The .data section holds global and static variables with non-zero initial values; their initial values are stored in flash, and the startup code copies them into RAM before main runs. The .bss section holds globals that start at zero; it takes no flash space, and the startup code just clears that RAM region. The stack holds local variables, function return addresses and saved registers, and on Cortex-M it grows down from the top of RAM, its start address being the first entry in the vector table. The heap, if used, sits between .bss and the stack and serves malloc. In an RTOS, each task also has its own stack. The linker script defines these regions and their addresses, and the map file shows how much each uses, which I check to avoid running out of RAM or flash.

5. When would you use fixed-point arithmetic instead of floating point on a microcontroller?

A strong answer covers:

  • MCUs without an FPU emulate float slowly in software
  • Fixed point uses integers with an implied scale, such as Q15 or milli-units
  • Faster, deterministic and smaller code on such MCUs
  • Need care with overflow, rounding and scaling
  • With a hardware FPU, single-precision float is often fine; avoid accidental double
Show model answer

Many small MCUs, like Cortex-M0 or M3, have no floating-point unit, so every float operation is emulated in software, which is slow and adds code size. In those cases, or in tight control loops, I use fixed-point arithmetic: integers with an agreed scale. For example, I store temperature in hundredths of a degree, so 23.45 degrees is 2345, or use Q15 and Q31 formats for signal processing, where CMSIS-DSP provides optimised functions. Fixed point is fast and deterministic, but I must manage scaling, use wider intermediates like int64 for multiplication to avoid overflow, and choose rounding carefully. On chips with a single-precision FPU, such as Cortex-M4F, float is fast and easier to read, so I use it, but I make sure constants have an f suffix, like 0.5f, so the compiler does not silently use slow double precision.

6. What is DMA, when would you use it, and what pitfalls do you watch for?

A strong answer covers:

  • DMA moves data between peripherals and memory without the CPU
  • Good for ADC streams, UART/SPI bulk transfers and audio
  • Circular and double-buffer (half/full transfer) modes
  • Cache coherency on cores with data cache
  • Buffer lifetime, alignment and memory regions DMA can reach
Show model answer

DMA is a hardware engine that copies data between peripherals and memory, or memory to memory, without the CPU moving each byte. I use it for continuous or bulk data, such as ADC sampling at high rates, receiving UART streams, SPI display updates or audio. A common pattern is circular mode with half-transfer and transfer-complete interrupts, so the CPU processes one half of the buffer while DMA fills the other. Pitfalls: on cores with a data cache, like Cortex-M7, I must clean the cache before a transmit and invalidate it after a receive, or place buffers in non-cacheable memory, with buffers aligned to cache lines. The buffer must stay valid for the whole transfer, so never a local stack variable that goes out of scope. Some DMA controllers cannot reach every RAM region, so I check the bus matrix in the reference manual.

7. Design firmware that receives text commands over UART, like "SET LED 1", and responds reliably.

A strong answer covers:

  • ISR or DMA pushes bytes into a ring buffer
  • A task or main loop assembles lines and parses them
  • Bounded line length and input validation
  • Command table mapping names to handler functions
  • Error responses, timeouts and overflow handling
Show model answer

I keep the interrupt side minimal: the UART receive ISR, or DMA with idle-line detection, pushes bytes into a ring buffer and maybe signals a task. A parser task or the main loop pulls bytes and builds a line in a fixed-size buffer until it sees a newline. If the line gets too long I discard it and report an error, so a bad sender cannot overflow memory. I then split the line into tokens and look up the first token in a constant table of command names and handler functions, which keeps parsing clean and easy to extend. Each handler validates its arguments, like range-checking the LED number, and returns OK or a clear error code. I also handle ring buffer overflow by counting dropped bytes, add a timeout for partial lines, and write host unit tests for the parser with good and malformed input.

8. Explain how the CAN bus works and why it is popular in automotive and industrial systems.

A strong answer covers:

  • Differential two-wire bus with 120 ohm termination at each end
  • Message IDs, not node addresses; ID also sets priority
  • Non-destructive bitwise arbitration with dominant and recessive bits
  • Built-in CRC, ACK, error counters and fault confinement
  • CAN FD for larger payloads and faster data phase
Show model answer

CAN is a multi-master serial bus on a twisted differential pair, CAN_H and CAN_L, terminated with 120 ohms at each end. That makes it resistant to the electrical noise in cars and factories. Frames carry a message identifier rather than a destination address, and any node can listen to any message. The ID also sets priority: when nodes transmit together, dominant zero bits override recessive ones, and a node that loses backs off without destroying the winning frame. CAN has strong built-in error handling with CRC, acknowledgement bits, bit stuffing checks, and error counters that move a faulty node to error-passive and finally bus-off, so one broken node cannot block the bus. Classic CAN carries up to 8 bytes at up to 1 Mbit/s; CAN FD allows 64 bytes and a faster data phase. Higher layers like CANopen or J1939 add structure.

9. Your firmware is close to the flash and RAM limits of the MCU. How do you reduce memory usage?

A strong answer covers:

  • Measure first with the map file, size and nm
  • Compiler flags: -Os, LTO, function/data sections with gc-sections
  • Remove heavy library features like float printf
  • Make tables const so they stay in flash
  • Right-size stacks, buffers and heap using measured usage
Show model answer

I start by measuring, not guessing: the linker map file and arm-none-eabi-nm --size-sort show which functions and data are largest, and tools like puncover make it visual. For flash, I build with -Os, enable link-time optimisation, and use -ffunction-sections and -fdata-sections with --gc-sections so unused code is dropped. Library choices often matter most: a printf with float support or a big HAL driver can cost tens of kilobytes, so I use newlib-nano or a small printf, and register-level code for simple peripherals. For RAM, I make lookup tables and strings const so they stay in flash, measure task stacks with high-water marks and shrink them, size buffers from real data rates, and remove the heap if nothing needs it. I track memory size in CI so growth is caught in code review.

10. How would you design a watchdog strategy for firmware running several RTOS tasks?

A strong answer covers:

  • Hardware independent watchdog with its own clock
  • Monitor task collects check-ins from each critical task
  • Refresh only when all tasks are healthy
  • Never kick from a timer ISR
  • Record reset cause and context to diagnose watchdog resets
Show model answer

I use the independent hardware watchdog, which runs from its own clock so it still works if the main clock fails, and I enable it early in boot. Each critical task sets a check-in bit or increments a counter every time it completes its loop. A monitor task, or a low-priority supervisor, checks that every task has checked in within its expected period, and only then refreshes the watchdog. If one task hangs or deadlocks, the refresh stops and the device resets. I never kick from a timer ISR, because interrupts often keep running when tasks are stuck. A window watchdog can also catch code that runs too fast or loops wrongly. After reset I read the reset cause register and save information such as which task missed its check-in to a no-init RAM area, so field watchdog resets can be diagnosed.

Hard questions hard

1. Design the firmware for a battery-powered sensor that must run for 5 years on a coin cell and report data every 15 minutes.

A strong answer covers:

  • Power budget calculation from battery capacity and duty cycle
  • Deep sleep most of the time, wake by RTC
  • Minimise radio on-time: batch data, efficient protocol
  • Turn off peripherals and configure pins correctly
  • Measure real current with a power profiler
  • Handle battery voltage drop, brownout and robust recovery
Show model answer

I start with a power budget. A CR2032 has about 220 mAh, and 5 years is roughly 43,800 hours, so the average current must be under about 5 uA, minus margin for self-discharge and temperature. That means the device sleeps almost always. I pick an MCU and radio with sub-microamp sleep and an RTC wake-up. Every 15 minutes it wakes, powers the sensor, takes a reading, and goes back to sleep. The radio is the biggest consumer, so I batch several readings and transmit less often if allowed, use a low-power protocol like BLE advertising, LoRaWAN or Zigbee, and keep packets short. Before sleep, I turn off peripheral clocks, put unused GPIOs in analog mode and cut sensor power with a load switch. I measure the real current profile with a power analyser like a Nordic PPK2, check the coin cell can handle radio current peaks, often with a capacitor, and handle brownout and low-battery reporting gracefully.

2. How would you secure an IoT device's firmware against tampering and cloning?

A strong answer covers:

  • Secure boot chain with signature verification from an immutable root of trust
  • Signed and encrypted OTA updates with anti-rollback
  • Disable or lock debug ports in production
  • Unique per-device keys stored in secure hardware
  • Secure communication with TLS and certificate-based identity
  • Follow regulations such as the EU Cyber Resilience Act
Show model answer

I start with secure boot: an immutable bootloader in ROM or locked flash holds a public key, or its hash in one-time-programmable memory, and verifies the signature of the next stage before running it. This forms a chain of trust, so only firmware signed with our private key, kept in an HSM, can run. OTA updates are signed, optionally encrypted, and include a version counter in OTP memory for anti-rollback, so attackers cannot install an old vulnerable version. In production, I lock or disable JTAG/SWD and enable flash read-out protection so firmware cannot be dumped. Each device gets a unique key and certificate, ideally generated inside a secure element or TrustZone, so cloning one device does not compromise others. Communication uses TLS with mutual authentication. I also follow standards like PSA Certified and ETSI EN 303 645, and prepare for rules like the EU Cyber Resilience Act, which require vulnerability handling and security updates.

3. How do you make sure a real-time system always meets its deadlines?

A strong answer covers:

  • Identify tasks with period, deadline and worst-case execution time
  • Priority assignment such as rate-monotonic
  • Schedulability analysis: utilisation bounds and response time analysis
  • Account for interrupts, blocking and jitter
  • Measure WCET and timing on real hardware with margins
Show model answer

First I list every task and interrupt with its period or minimum inter-arrival time, deadline and worst-case execution time. WCET comes from measurement on the real target with caches and worst-case inputs, plus margin, or from static analysis tools for safety-critical work. Then I assign priorities, typically rate-monotonic, where shorter periods get higher priority, or deadline-monotonic if deadlines are shorter than periods. For a quick check, the Liu and Layland bound says that n tasks are schedulable under rate-monotonic if total utilisation is below n times (2 to the power 1/n minus 1), about 69% for many tasks. A more exact method is response time analysis, which adds each task's execution time, interference from higher-priority tasks and interrupts, and blocking time from shared resources. I keep critical sections short and use priority inheritance. Finally I verify on hardware by toggling GPIOs and measuring with an oscilloscope or tracing tools like SEGGER SystemView.

4. How do you build a strong automated testing strategy for embedded firmware?

A strong answer covers:

  • Separate hardware-dependent code behind abstractions
  • Unit tests on the host with mocks and frameworks such as Unity or GoogleTest
  • Static analysis and MISRA checks
  • Hardware-in-the-loop tests on real boards in CI
  • Simulation or emulation such as QEMU or Renode
  • Fault injection and long-run soak tests
Show model answer

The key is design for testability. I keep business logic, like control algorithms, protocol parsers and state machines, separate from hardware access, which sits behind a thin hardware abstraction layer. Then most code can be unit tested on a PC with Unity, Ceedling or GoogleTest, using mocks for the HAL, and it runs in seconds in CI with sanitisers that catch undefined behaviour. I add static analysis, such as cppcheck, clang-tidy or a MISRA checker, and compiler warnings as errors. For integration, I use emulators like Renode or QEMU to run the real firmware binary with simulated peripherals. Then hardware-in-the-loop: real boards connected to a CI runner, with a programmer, power control and test equipment, run automated tests on every merge, for example flashing, booting, exercising sensors and checking UART output. Finally, fault injection, like power cuts during flash writes, and multi-day soak tests catch leaks and rare timing issues.

5. How do you choose a microcontroller for a new product?

A strong answer covers:

  • Requirements: performance, memory, peripherals, I/O count
  • Power consumption and low-power modes
  • Connectivity and security features
  • Cost at volume, supply chain and long-term availability
  • Ecosystem: toolchain, SDK, RTOS support, community
  • Certifications and headroom for future features
Show model answer

I start from the requirements: what processing is needed, such as control loops, DSP or a small ML model; how much flash and RAM the firmware and stacks will need, with 30 to 50% headroom for future features and updates; which peripherals and how many pins, such as ADC resolution, CAN, USB, timers for motor control or a display interface. Power matters for battery devices, so I compare sleep current and wake-up time. If the product is connected, I look at integrated radios like BLE or Wi-Fi and security features: secure boot, crypto accelerators, TrustZone and a secure key store. Then business factors: unit cost at our volume, supply chain risk, which we learned from the 2021 to 2023 chip shortage, long-term availability guarantees, and pin-compatible alternatives. Finally, ecosystem: quality of the SDK, Zephyr or FreeRTOS support, debugger tools, documentation and community, plus any needed certifications like automotive AEC-Q100 or functional safety.

6. Design a secure, fail-safe over-the-air firmware update system for a fleet of IoT devices.

A strong answer covers:

  • A/B slots or bootloader with a separate download slot
  • Signature verification with keys anchored in immutable storage
  • Anti-rollback version checks
  • Trial boot with health check and automatic rollback
  • Resumable, power-loss-safe download
  • Staged rollout and fleet monitoring
Show model answer

I use a small, immutable bootloader such as MCUboot with two image slots. The running image downloads the new one into the inactive slot in resumable chunks, so power loss or a dropped connection only means continuing later. Images are signed, and the bootloader verifies the signature with a public key stored in protected flash or OTP, plus a security version counter to block rollback to vulnerable releases. After switching, the new image boots in trial mode and must pass a health check, such as connecting to the server and running self-tests, before confirming itself. If it crashes or the watchdog fires before confirmation, the bootloader reverts to the previous slot. On the server side I roll out in stages, starting with a small percentage of the fleet, monitor crash and reconnect rates, and pause automatically if metrics get worse. Delta updates can reduce bandwidth.

7. You receive the first prototype of a new custom board. How do you bring it up?

A strong answer covers:

  • Visual inspection and check power rails before connecting firmware
  • Confirm debugger connection and clock configuration
  • Blink an LED and get a UART console working first
  • Bring up peripherals one at a time with test firmware
  • Use scope and logic analyser; log issues for the hardware team
Show model answer

First I inspect the board for assembly problems, then power it from a bench supply with a current limit and measure every rail and its ripple before anything else. Next I connect the SWD debugger, confirm I can read the chip ID, and run a minimal program on the internal oscillator. Then I blink an LED and get a UART console, which gives me a debugging channel. After that I switch to the external crystal and the full clock tree, and check clock output on a scope. I bring up peripherals one at a time with small test firmware: I2C and SPI devices by reading ID registers, checking signals with a logic analyser against the schematic and datasheets, then ADC, memory, radio and power modes, measuring sleep current. I record each problem with evidence and share it quickly with the hardware engineers, because board fixes take time.

8. What changes when you write firmware for a safety-critical product, such as automotive or medical?

A strong answer covers:

  • Follow a standard like ISO 26262, IEC 61508 or IEC 62304
  • Requirements traceability to design, code and tests
  • Coding standards like MISRA C and static analysis
  • High test coverage, including MC/DC where required
  • Defensive design: watchdogs, memory checks, safe states
  • Change control and documented reviews
Show model answer

The process becomes as important as the code. Work follows a standard such as ISO 26262 for automotive, IEC 62304 for medical software or IEC 61508 for industrial, starting from a hazard analysis that assigns a safety level. Every requirement is traced to design, code and tests, so reviewers can prove nothing is missing. Code follows MISRA C, with static analysis tools and documented deviations, and avoids dynamic memory, recursion and unbounded loops. Testing is much deeper, with unit and integration tests, coverage targets such as MC/DC at the highest levels, and fault injection. The design itself is defensive: watchdogs, RAM and flash integrity checks, range checks on inputs, redundant sensors, and a defined safe state the system enters on any fault. Tools may need qualification, and every change goes through formal review and impact analysis.

9. How do you safely share data between interrupt handlers and tasks while keeping interrupt latency low?

A strong answer covers:

  • Keep ISRs short and defer work to tasks
  • Use RTOS FromISR queues or task notifications
  • Lock-free single-producer single-consumer ring buffers
  • Short critical sections only when needed, measured
  • Understand atomicity, volatile and memory barriers
  • Respect the RTOS max syscall interrupt priority
Show model answer

My rule is that ISRs capture data and signal, and tasks do the processing. For low rates I use RTOS queues or task notifications with the FromISR APIs and yield if a higher-priority task woke. For streams like UART or ADC data, I use a single-producer single-consumer ring buffer: only the ISR writes the head, only the task writes the tail, and indices are aligned 32-bit values, so no locks are needed on a single core. Data is written before the index is published, with volatile and barriers where reordering is possible. When a multi-word structure must be updated together, I use a very short critical section, or BASEPRI masking that leaves the highest-priority interrupts running, and I measure how long interrupts are disabled. I also make sure ISRs that call the kernel are at or below the RTOS max syscall priority.

10. A variable is randomly overwritten in devices running in the field, but you cannot reproduce it on your desk. How do you find the cause?

A strong answer covers:

  • Collect evidence: fault registers, crash dumps, logs in no-init RAM
  • Check stack overflow and buffer overruns near the variable using the map file
  • Use the MPU or data watchpoints to trap the bad write
  • Look for DMA, ISR races and dangling pointers
  • Reproduce field conditions: stress, temperature, power, timing
Show model answer

I start by getting evidence from the field: a fault handler that saves registers, stack trace and fault status to no-init RAM or flash, plus logs that are uploaded after reboot. Then I look at the map file to see what is next to the corrupted variable, because overwrites usually come from a neighbour: an array overrun, a task stack that grew into it, or a DMA buffer with the wrong length. I add canaries around suspect buffers and enable stack overflow checks. To catch the write in the act, I use the Cortex-M data watchpoint unit, which can trigger on writes to that address even without a debugger, or configure the MPU to make the region read-only except in known code. I review ISR races, dangling pointers and DMA to freed buffers, and reproduce field conditions with stress tests, high traffic, temperature and power dips.