Firmware Developer

Firmware Developer Roadmap and Interview Guide

This page is a complete roadmap for becoming a firmware developer and for passing firmware interviews. It follows the code from the reset vector upwards: C for microcontrollers, start-up code and linker scripts, register-level drivers, interrupts and RTOS design, low power, watchdogs and fault analysis, flash storage, bootloaders, secure boot and safe over-the-air updates, and finally debugging, testing and hardware bring-up.

Arm Cortex-M is the reference throughout because most job adverts ask for it, but each topic notes where other families differ. The role is narrower and deeper than a general Embedded Systems Engineer, who may also work on embedded Linux, and different from a Device Driver Developer, who writes drivers inside an operating system kernel. A firmware developer owns bare-metal and RTOS code that runs directly on the chip.

By the end you will be able to explain what happens between reset and main, write and debug drivers from a reference manual, design an update system that cannot brick a device, decode a HardFault from its registers, and answer the questions firmware interviewers actually ask.

Level
Beginner to job-ready
Time
6–9 months from basic programming at about 10 hours a week; 8–10 weeks of focused interview preparation if you already write microcontroller C
Scope
50 topics in 9 stages
Before you start
Basic programming in any language, ideally some C: variables, loops, functions, arrays and pointers; School-level maths and enough physics to understand voltage, current and resistance; A Cortex-M development board, a USB logic analyser and, later, access to an oscilloscope

What you will be able to do

  • Explain the Cortex-M reset sequence and write the vector table, start-up code and linker script for a project from scratch
  • Write register-level and HAL-based drivers for GPIO, timers, UART, SPI, I2C, CAN, ADC and DMA from the reference manual
  • Design interrupt priorities, protect shared data correctly and choose between a super-loop and an RTOS such as FreeRTOS or Zephyr
  • Build a bootloader with signed images, A/B or swap updates and automatic rollback
  • Reduce current consumption to a measured target, and use watchdogs and fault handlers that leave useful evidence
  • Bring up a new board, debug over SWD with GDB, a logic analyser and a scope, and decode a HardFault
  • Test firmware on the host and on hardware in CI, and follow MISRA C and static analysis practice

What a Firmware Developer does

A firmware developer writes the software that runs directly on microcontrollers inside products: battery chargers, sensors, wearables, motor drives, medical pumps, smart locks, keyboards, power supplies and the control units in vehicles and machines. The code starts at the reset vector and does everything: configures clocks and memory, drives peripherals through their registers, schedules work with a super-loop or an RTOS, saves power, stores settings in flash and updates itself safely in the field.

The job sits between electronics and software. You read schematics and reference manuals, sit with hardware engineers during board bring-up, and debug with a probe, a logic analyser and an oscilloscope as often as with print statements. Firmware ships in devices that may run for ten years without a reboot and cannot be recalled cheaply, so the work values determinism, careful memory use, defensive error handling and a reliable update path over speed of feature delivery.

A typical week

  • Write and review C or C++ for drivers, communication protocols, control logic and device state machines
  • Bring up new board revisions with hardware engineers and isolate faults between the circuit and the code
  • Debug crashes and timing problems with a debug probe, trace tools, a logic analyser and an oscilloscope
  • Measure and reduce current consumption, code size, RAM use and interrupt latency against budgets
  • Maintain the bootloader, image signing and update pipeline, and investigate field reports and crash dumps
  • Write host unit tests and hardware-in-the-loop tests, and keep the CI build and code size checks green

What is expected at each level

Junior

  • Implements a defined feature, such as a sensor driver or a command, in clear C with host tests
  • Configures common peripherals from the reference manual and checks them on a logic analyser
  • Uses the debugger, reads map files and follows the team's coding standard and review process

Mid

  • Owns a subsystem such as communications, power management or storage from design to release
  • Sets interrupt and task priorities deliberately and measures timing, stack and memory headroom
  • Brings up new boards and finds the root cause of faults that cross the hardware and software boundary

Senior

  • Designs the firmware architecture for a product: layers, scheduling model, boot chain, update and fault strategy
  • Influences microcontroller selection and board design with hardware, test and manufacturing teams
  • Leads investigation of field failures from crash data and returns, and changes process to prevent repeats

The interview loop

  1. Recruiter or hiring manager screen20–30 min

    What it tests. Which microcontroller families, RTOSes and toolchains you have used, whether you have worked bare-metal, the products you shipped and whether you have done board bring-up.

    How to prepare. Prepare a two-minute summary of one device: the part, your share of the firmware, one hard problem and how it ended.

  2. Technical screen: C and microcontroller fundamentals45–60 min

    What it tests. Quick questions on volatile, const, static, pointers, integer promotion, struct padding and bit manipulation, plus what happens at reset and how interrupts work, often with a short coding task in a shared editor.

    How to prepare. Revise stages two and three of this page and the C Roadmap. Practise writing bit macros, a ring buffer and declarations without a compiler, and explaining each line aloud.

  3. Firmware coding round60 min

    What it tests. Writing correct, compilable C for a firmware task such as a protocol parser, a memory pool, a debounce routine or a fixed-point filter, with attention to edge cases, overflow and interrupt safety.

    How to prepare. Solve timed problems in the coding practice workspace. State types and assumptions first, test with edge cases by hand and mention what you would change for production.

  4. Low-level and debugging deep dive60 min

    What it tests. Interrupt priorities, critical sections, RTOS behaviour, priority inversion, DMA and caches, HardFault analysis, and scenario questions such as a device that resets once a week in the field or an I2C bus that hangs.

    How to prepare. Practise talking through a debugging method: reproduce, measure, form a hypothesis, test it. Know the Cortex-M fault registers and the watchdog and reset-cause flow well enough to sketch them.

  5. Firmware system design45–60 min

    What it tests. Designing the firmware for a device end to end: requirements, part selection, layers, scheduling, power budget, storage, boot and update with rollback, security, testing and manufacturing.

    How to prepare. Practise three designs aloud with numbers, such as a battery sensor, a motor controller and a connected lock. Use the system design practice workspace and always cover updates, faults and test.

  6. Lab or take-home exercise2–4 hours or a take-home

    What it tests. Some companies give you a board and a task, such as reading a sensor and reporting over UART, or a take-home driver with tests. They watch how you read documentation, structure code and use tools.

    How to prepare. Get fast at a new board: find the clock set-up, pin mapping and peripheral registers in the manual, and keep a template project with a UART console and a test harness ready.

  7. Behavioural and team fit30–45 min

    What it tests. How you work with hardware and test engineers, how you handle a bug found late, a disagreement over a design, or pressure to ship something you believe is unsafe.

    How to prepare. Prepare four or five stories in a situation, action and result structure, including at least two debugging stories with clear evidence and one about quality or safety.

Skill roadmaps for this role

Each skill below has its own complete roadmap. Learn the core ones first.

  • CoreC Roadmap

    C is the main language of firmware and the language of almost every firmware coding interview, so pointers, memory, bit manipulation and undefined behaviour must be second nature.

  • CoreComputer Science Roadmap

    Computer architecture, number representation, data structures and concurrency explain how the core, memory and interrupts behave and why some designs fail.

  • HelpfulModern C++ Roadmap

    Many firmware teams use a restricted C++ for type safety and zero-cost abstractions, and interviews ask what you would allow and what you would forbid.

  • HelpfulLinux Roadmap

    Toolchains, CI runners, debug servers and test rigs run on Linux, and many products pair a microcontroller with a Linux processor.

  • CoreGit and GitHub Roadmap

    Firmware needs exact traceability from a binary in the field to its source, which depends on disciplined tagging, branching and review.

The Firmware Developer roadmap

  1. Stage 1Foundations: microcontrollers and the firmware jobBeginner · 2 weeks · 5 topics
  2. Stage 2C and C++ for firmwareBeginner · 3 weeks · 7 topics
  3. Stage 3Boot, start-up and linkingIntermediate · 2 weeks · 4 topics
  4. Stage 4Peripherals and driversIntermediate · 3–4 weeks · 9 topics
  5. Stage 5Interrupts, concurrency and RTOSIntermediate · 3 weeks · 6 topics
  6. Stage 6Reliability: power, watchdogs, faults, storage and mathsAdvanced · 3 weeks · 6 topics
  7. Stage 7Bootloaders, secure boot and firmware updatesAdvanced · 2–3 weeks · 5 topics
  8. Stage 8Debugging, bring-up, testing and qualityAdvanced · 3–4 weeks · 5 topics
  9. Stage 9Interview preparationAdvanced · 2 weeks · 3 topics

Stage 1 Foundations: microcontrollers and the firmware job

Beginner2 weeks5 topics

Goal. Describe how a microcontroller is organised, build a program for it with a cross toolchain and find any register in the reference manual.

What Firmware Is and Where It Runs Core

Firmware is the program stored in a device's non-volatile memory that runs from power-on with no operating system underneath it, or with only a small real-time kernel.

A firmware developer owns the code from the reset vector up: clocks, memory, drivers, scheduling, power, updates and fault recovery. The constraints are tight. You may have 32 KB of flash and 8 KB of RAM, a battery that must last two years and a product that cannot be recalled if an update bricks it. That is why firmware interviews focus on determinism, memory, interrupts and failure handling rather than frameworks.

Practice. Pick three products near you and, for each, write down what the firmware must do within a fixed time and what happens if it crashes.

Cortex-M and Other Microcontroller Families Core

Arm Cortex-M is the reference architecture for most firmware roles. The M0 and M0+ are small and cheap with a reduced instruction set and no hardware divide. The M3 adds the full Thumb-2 set and divide. The M4 adds DSP instructions and an optional single-precision FPU. The M7 adds caches and tightly coupled memory. The M23 and M33 are Armv8-M parts with TrustZone for security, and the M55 and M85 add vector extensions.

Vendors wrap these cores with their own peripherals: STM32, NXP LPC and i.MX RT, Nordic nRF, Microchip SAM, Renesas RA, TI MSPM0 and Raspberry Pi RP2040 and RP2350. You will also meet RISC-V cores such as the ESP32-C series, Xtensa in older ESP32 parts, 8-bit AVR and PIC in cost-sensitive designs, and TI C2000 or Renesas RH850 in motor control and automotive.

Practice. Compare an M0+, M4 and M33 part from one vendor in a table: clock, flash, RAM, FPU, DSP, TrustZone and price band.

The Memory Map and Memory-Mapped Registers Core

A Cortex-M has a 4 GB address space split into fixed regions. Code and flash usually start at 0x00000000 or 0x08000000, SRAM at 0x20000000, peripherals at 0x40000000 and the core's own system control space, which holds the NVIC, SysTick and SCB, at 0xE000E000.

Peripherals are controlled through memory-mapped registers: writing to an address configures hardware rather than storing data. Vendor headers describe each block as a struct of volatile fields placed at the base address, so USART2->BRR = 0x0683; is a store to a fixed address. Some registers clear a flag when you read them, some when you write a 1 to a bit, and some bits are reserved and must keep their reset value.

Practice. From your board's reference manual, draw the memory map and list the base address of GPIOA, the first UART and the SysTick block.

Datasheets, Reference Manuals and Errata Core

Three vendor documents decide most firmware decisions. The datasheet covers one part number: pinout, electrical limits, timing and which peripherals exist. The reference manual covers the family: every register, bit field and peripheral state machine. The errata sheet lists silicon bugs and workarounds, such as an I2C peripheral that locks up after a bus error or a flash read that needs extra wait states at low voltage.

Experienced engineers check the errata before they start debugging anything strange.

Practice. Find the errata sheet for your microcontroller and summarise three errata that would affect a design using UART, I2C and the ADC.

Cross Toolchains and the Build Core

Firmware is built on a PC for a different target, so you use a cross compiler such as arm-none-eabi-gcc, Clang with an Arm target, IAR or Arm Compiler. The build compiles each file to an object, links them with a linker script into an ELF file, and converts that to a raw binary or Intel HEX for flashing.

Know the important flags: -mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 select the core and FPU, -Os or -O2 set optimisation, -ffunction-sections -fdata-sections with -Wl,--gc-sections remove unused code, and -Wall -Wextra catch mistakes early. Most teams wrap this in CMake or Make and build in a container so every developer and the CI server produce the same binary. The linker map file and tools such as size and nm --size-sort show where every byte of flash and RAM goes, so track both against a budget in CI.

Practice. Build a blink program from the command line with arm-none-eabi-gcc and a Makefile, then use size and objdump to explain every section in the output.

Stage 2 C and C++ for firmware

Beginner3 weeks7 topics

Goal. Write C that is correct on a microcontroller, explain every keyword and conversion in it, and use the safe subset of C++ that firmware teams allow.

volatile, const, static and extern Core

volatile tells the compiler that a value can change outside the code it can see, so every access must happen as written. Use it for hardware registers and for variables written in an interrupt and read elsewhere. It gives no atomicity and no ordering with other variables, so it does not replace a critical section.

const puts tables and strings in flash rather than RAM when the object is defined at file scope, which matters when RAM is small. static at file scope limits a name to one file, which is how you build private driver state; inside a function it keeps the value between calls and places it in .data or .bss, not on the stack. extern declares something defined in another file. The C Roadmap covers the language in depth.

Practice. Write a small module with a private static counter incremented in a timer ISR and read in main, and explain why each keyword is there.

Bit Manipulation and Register Access Core

Firmware sets, clears and reads bit fields all day. Learn the idioms until they are automatic: set with |=, clear with &= ~, toggle with ^=, test with &, and extract a field with a shift and a mask. Always use unsigned types and 1u << n, because shifting a signed 1 into the sign bit is undefined.

#define FIELD_MSK(pos, w)  (((1u << (w)) - 1u) << (pos))
uint32_t r = TIM2->CR1;
r &= ~FIELD_MSK(5, 2);        /* clear CMS bits */
r |= (1u << 5);               /* set mode 1 */
TIM2->CR1 = r;                /* one write */

A read-modify-write on a register shared with an interrupt can lose an update, so many parts provide set and reset registers such as GPIO BSRR that change bits atomically. Some parts also offer bit-banding or atomic set and clear aliases.

Practice. Write and test functions to count set bits, check for a power of two, reverse bits in a byte and swap the bytes of a 32-bit value.

Integer Types, Promotion and Undefined Behaviour Core

Use fixed-width types from stdint.h such as uint8_t and int32_t, because int is 16 bits on some targets and 32 on others. Integer promotion surprises people: two uint8_t values are added as int, and uint16_t multiplied by uint16_t can overflow a signed int, which is undefined.

Mixing signed and unsigned in a comparison converts the signed value, so -1 < 1u is false. Other undefined behaviour that bites firmware: signed overflow, shifting by the type width or more, reading an uninitialised local, out-of-bounds access and type punning through incompatible pointers. The optimiser assumes undefined behaviour never happens and may delete checks that depend on it. Turn on warnings, run sanitisers in host tests, and write code that is defined on every path.

Practice. Collect five snippets with promotion or signedness traps, predict the result, then check with a compiler on the host and on the target.

Struct Layout, Alignment, Packing and Endianness Core

The compiler adds padding so each member is aligned to its natural boundary, so a struct of uint8_t, uint32_t, uint8_t is usually 12 bytes, not 6. Cortex-M3 and above handle many unaligned accesses in hardware, but M0 parts fault on them, and multiple-load instructions always need alignment.

Packed structs remove padding but generate slower code and can produce unaligned pointers. For wire formats and flash records it is safer to serialise byte by byte with shifts, which also fixes endianness: Cortex-M is little-endian, while many network and sensor protocols are big-endian. Bit-fields are convenient but their layout is implementation-defined, so avoid them for registers and protocols that must be portable.

Practice. Write encode and decode functions for a 12-byte sensor packet in big-endian order and unit test them on the host.

Memory Without a Heap Core

Most firmware avoids malloc after start-up. A general heap fragments over months of running, its timing is not bounded, and a failed allocation deep in a driver has no good recovery. Instead, allocate statically: fixed arrays, ring buffers, object pools with fixed-size blocks, and message buffers owned by one task at a time.

Size stacks by measurement: fill them with a pattern at start-up and check the high-water mark under worst-case load, or use the compiler's stack usage output. When an RTOS needs dynamic objects, use its static creation APIs so all memory is known at link time.

Practice. Implement a fixed-block memory pool with allocate and free in constant time, and add a test that exhausts it.

Ring Buffers and Lock-Free Queues Core

It passes bytes from a UART interrupt to the main loop or from one task to another. A single-producer, single-consumer ring needs no lock if the producer only writes the head index and the consumer only writes the tail index, and each index update is a single aligned store.

Use a power-of-two size so wrapping is a mask, and either keep one slot empty or use free-running counters to tell full from empty. On cores with caches or multiple processors you also need memory barriers so the data is visible before the index. Know the failure policy: when full, drop the new byte, overwrite the oldest, or count an overrun, and make it visible in diagnostics.

Practice. Write an SPSC ring buffer in C with a host test that runs producer and consumer in separate threads for ten million items.

C++ on Microcontrollers Recommended

Many firmware teams use C++ for its stronger types and zero-cost abstractions, with rules. Commonly allowed: classes, namespaces, references, constexpr, templates, enum class, std::array, std::span and RAII objects such as a lock guard that disables interrupts in its constructor and restores them in its destructor.

Commonly restricted: exceptions (code size and unbounded unwinding), RTTI, the heap and containers that allocate, and virtual functions in hot paths. Compile with -fno-exceptions -fno-rtti. Watch for static constructors that run before main and depend on hardware not yet configured. The Modern C++ Roadmap covers the language itself.

Practice. Rewrite a C GPIO driver as a C++ template class with compile-time pin numbers and compare the generated assembly with the C version.

Stage 3 Boot, start-up and linking

Intermediate2 weeks4 topics

Goal. Explain everything that happens between reset and main, write a linker script and start-up file from scratch, and configure the clock tree.

Reset Sequence and the Vector Table Core

On reset a Cortex-M reads two words from the start of the vector table: the initial main stack pointer at offset 0 and the address of the reset handler at offset 4. It loads them and starts executing in Thread mode, privileged, using the main stack. No code runs before this, so the stack pointer must already be valid.

The rest of the table holds the addresses of the system exception handlers, such as NMI, HardFault, SVCall, PendSV and SysTick, followed by one entry per peripheral interrupt. Addresses of Thumb functions have their lowest bit set. The VTOR register in the SCB relocates the table, which is how a bootloader hands over to an application stored at a different address, or how firmware moves the table into RAM to change handlers at run time.

Practice. Dump the first 64 bytes of your firmware binary and identify the stack pointer, reset handler and SysTick handler addresses.

Start-up Code Core

The reset handler prepares the C run-time before calling main. It copies initialised data from its load address in flash to its run address in RAM, zeroes .bss, and on some parts sets up the FPU access bits, the vector table offset and external memory. It then calls SystemInit or similar to configure clocks, runs C++ static constructors through __libc_init_array, and calls main.

void Reset_Handler(void) {
    uint32_t *src = &_sidata, *dst = &_sdata;
    while (dst < &_edata) *dst++ = *src++;
    for (dst = &_sbss; dst < &_ebss; ) *dst++ = 0;
    SystemInit();
    __libc_init_array();
    main();
    for (;;) { }
}

Practice. Replace your vendor start-up file with your own C reset handler and prove that initialised and zeroed globals hold the right values.

Linker Scripts and Sections Core

The linker script tells the linker where memory is and where each section goes. The MEMORY block declares regions such as FLASH and RAM with origins and lengths. The SECTIONS block places .isr_vector first in flash, then .text and .rodata, puts .data in RAM with its load address in flash using AT>, and reserves .bss, heap and stack.

The script also defines symbols such as _sdata and _ebss that start-up code uses. Custom sections let you place a function in RAM for flash programming or speed, keep a no-init area that survives a warm reset for crash logs, put a version header at a fixed offset, or reserve flash pages for configuration. KEEP stops garbage collection from removing the vector table. Add ASSERT checks so the build fails if the image grows past its slot.

Practice. Add a .noinit RAM section and a fixed-address firmware header section to your linker script, and verify both in the map file.

Clock Tree, PLL and Power-on Configuration Core

A microcontroller usually starts on an internal RC oscillator, which is quick to start but inaccurate. Firmware then enables an external crystal if present, waits for it to become stable, configures the PLL to multiply it to the system frequency, sets flash wait states for that frequency, switches the system clock, and sets bus prescalers for the peripheral buses.

Order matters: raise flash wait states before increasing the clock and lower them only after reducing it, or the core reads flash too fast and crashes. Each peripheral also has its own clock enable bit, and forgetting it is the most common reason a new driver does nothing. UART baud rates, timer periods and ADC sampling all derive from these clocks, so a wrong prescaler produces a baud rate error that looks like corrupt data.

Practice. Write clock set-up code from the reference manual for the highest supported frequency, then output the system clock on a pin and measure it with a scope.

Stage 4 Peripherals and drivers

Intermediate3–4 weeks9 topics

Goal. Write and debug register-level and HAL-based drivers for the common peripherals, and design driver interfaces that the rest of the firmware can test against.

GPIO Core

Each pin has a mode (input, output, alternate function or analogue), an output type (push-pull or open-drain), a pull-up or pull-down, and a speed or slew-rate setting. Alternate function selection routes the pin to a peripheral such as a UART or SPI block, and choosing the wrong alternate function number is a classic bring-up bug.

For inputs from buttons and mechanical contacts, debounce in software by sampling at a fixed rate and requiring a stable state for several samples, rather than reacting to each edge interrupt.

Practice. Drive an LED with push-pull and then open-drain output, and write a debounced button driver that reports press, release and long press.

Timers, PWM and Input Capture Core

A general-purpose timer counts clock ticks after a prescaler and resets at an auto-reload value, giving a period of (PSC + 1) × (ARR + 1) ticks. Compare channels produce PWM for LEDs, motors and power stages, with duty cycle set by the compare value. Input capture records the counter value at an edge, which measures frequency, pulse width or encoder position.

Advanced timers add complementary outputs with dead time for half bridges, break inputs that force outputs safe on a fault, and triggers that start ADC conversions in sync with PWM. Know the difference between updating a compare register directly and through a preload register, which changes the value only at the next period and avoids glitches.

Practice. Generate a 20 kHz PWM with duty cycle set over UART, and measure an external signal's frequency and duty with input capture.

UART Core

A UART sends asynchronous frames: a start bit, 5 to 9 data bits, optional parity and stop bits, at an agreed baud rate. Both ends need clocks within about 2% of each other, which is why an inaccurate internal oscillator can corrupt data.

Receive with an interrupt per byte into a ring buffer, or better, with DMA into a circular buffer plus an idle-line interrupt that tells you a message has ended. Handle overrun, framing and noise errors explicitly and clear their flags, or the peripheral can stop receiving. For RS-485 half duplex, control the driver enable pin and switch back to receive only after the transmission-complete flag, not when the transmit register is empty.

Practice. Build a UART command shell that receives with DMA and idle-line detection and survives being sent random bytes at full speed.

SPI Core

SPI is a synchronous full-duplex bus with a clock, data out, data in and one chip select per device. The four modes combine clock polarity (CPOL) and clock phase (CPHA), and the device datasheet states which it needs. A wrong mode often gives data shifted by one bit.

Every byte sent clocks a byte back, so reading means sending dummy bytes. Chip select timing matters: some devices need a delay after select or a minimum high time between commands. Use DMA for long transfers and protect the bus with a mutex when several tasks share it.

Practice. Read the JEDEC ID of an SPI flash chip, then write, read back and erase a sector using DMA.

I2C Core

I2C uses two open-drain lines, clock and data, with pull-up resistors, and addresses up to 127 devices with 7-bit addresses. A transaction is a start condition, the address with a read or write bit, an acknowledge from the device, data bytes each acknowledged, and a stop. A repeated start lets you write a register address and then read from it without releasing the bus.

Common faults: missing or too weak pull-ups give slow edges, two devices share an address, and a reset in the middle of a read leaves a slave holding data low. The recovery is to clock SCL manually up to nine times until SDA is released, then send a stop. Always use timeouts so a stuck bus cannot hang the firmware, and return clear error codes for a missing acknowledge.

Practice. Write an I2C driver for a temperature sensor with timeouts and the nine-clock bus recovery, and test it by unplugging the sensor mid-transfer.

CAN and CAN FD Recommended

CAN is a differential multi-drop bus used in vehicles, industrial machines and robots. Frames carry an identifier rather than a destination address, and the identifier also sets priority: arbitration is bitwise, so the lowest identifier wins without destroying the frame. Classic CAN carries up to 8 data bytes at up to 1 Mbit/s. CAN FD allows 64 bytes and a faster data phase.

Firmware configures bit timing (prescaler, segments and sample point), acceptance filters so the CPU only sees relevant identifiers, and transmit mailboxes or FIFOs. Know the error counters and states, error active, error passive and bus off, and how your firmware recovers from bus off.

Practice. Connect two boards with CAN transceivers, send prioritised messages between them and trigger bus off by shorting the lines, then recover.

ADC, DAC and Analogue Inputs Core

An ADC converts a voltage to a number: a 12-bit converter with a 3.3 V reference gives about 0.8 mV per step. Know sampling time, which must be long enough for the input to charge the sampling capacitor through the source impedance, and conversion modes: single, continuous, scan over several channels, and triggered by a timer for a steady sample rate.

Accuracy depends on the reference voltage, noise and offset errors, so calibrate where the part allows it, average or filter readings, and read the internal reference channel to correct for supply changes. Sample at least twice the highest frequency of interest and add an analogue anti-aliasing filter. A DAC does the reverse, producing a voltage for audio, references or test signals.

Practice. Sample an analogue input at 1 kHz using a timer trigger and DMA, then apply a moving average and report minimum, maximum and mean.

DMA Core

A DMA controller moves data between peripherals and memory without the CPU. You configure source, destination, transfer size, increment modes and a request line, then start it. Circular mode with half-transfer and transfer-complete interrupts gives double buffering: the CPU processes one half while DMA fills the other.

Pitfalls come up in interviews. On cores with a data cache, such as Cortex-M7, you must clean the cache before DMA reads a buffer and invalidate it after DMA writes one, and buffers should be cache-line aligned. Some DMA controllers cannot reach every RAM region. A buffer on the stack can go out of scope while DMA still writes to it.

Practice. Stream ADC samples into a circular DMA buffer and process each half in the half and full interrupts without losing samples.

HAL Versus Register-Level Drivers Core

Vendor HALs such as STM32Cube HAL, the nRF SDK drivers or MCUXpresso SDK get a board working quickly and cover many parts, but they are large, sometimes slow, and can hide bugs or blocking behaviour. Register-level code is smaller and fully understood but costs time and ties you to one part. Most teams mix them: HAL or low-level libraries for set-up, direct registers in timing-critical paths.

Whatever is underneath, design your own driver interface: a small set of functions with clear ownership, non-blocking operations with callbacks or events, error codes rather than silent failure, and no hardware access outside the driver. This lets application code run in host tests against a fake driver, and lets you change microcontroller without rewriting the product.

Practice. Write the same UART transmit function with the vendor HAL and with registers, then compare code size and execution time.

Stage 5 Interrupts, concurrency and RTOS

Intermediate3 weeks6 topics

Goal. Design interrupt priorities and shared-data protection correctly, choose between a super-loop and an RTOS, and build a multi-task application on FreeRTOS or Zephyr.

Exceptions, the NVIC and Interrupt Priorities Core

The NVIC handles peripheral interrupts with programmable priority, where a lower number means higher urgency. A higher-priority interrupt preempts a lower one; equal priorities wait. Priority grouping splits the bits into preemption priority and sub-priority. Only the top bits are implemented, so a part with 4 bits has 16 levels and the values sit in the high bits of the byte.

On entry the core stacks R0 to R3, R12, LR, PC and xPSR automatically, so handlers can be plain C functions. PRIMASK masks all configurable interrupts, while BASEPRI masks only those at or below a level, which lets an RTOS keep critical sections short and leave the most urgent interrupts running. Keep handlers short: clear the flag, move data to a buffer and defer processing to a task or the main loop. Set every priority deliberately: the default of zero for everything is rarely right.

Practice. Configure three interrupts at different priorities, toggle a pin in each, and capture preemption on a logic analyser.

Shared Data, Critical Sections and Atomics Core

When main code and an interrupt share data, the main code can be interrupted half-way through a read-modify-write or a multi-word read. A 64-bit counter read on a 32-bit core can return the low word from before an increment and the high word from after. Solutions, from lightest to heaviest: a single writer per variable, aligned 32-bit accesses that are naturally atomic, disabling interrupts briefly around the access, raising BASEPRI, or using exclusive load and store instructions.

Cortex-M3 and above provide LDREX and STREX, which C11 stdatomic.h uses for lock-free compare-and-swap. M0 has none, so atomics fall back to disabling interrupts. Always save and restore the previous interrupt state rather than unconditionally enabling, so nested critical sections work. Keep critical sections to a few instructions and measure their longest duration.

Practice. Reproduce a torn read of a 64-bit tick counter on hardware, then fix it two ways and explain the trade-off.

Super-Loop, Cooperative Schedulers and State Machines Core

Many products need no RTOS. A super-loop calls each module's poll function forever, with interrupts filling buffers and setting flags. It is simple, small and easy to reason about, but each module must return quickly, and worst-case response time is the sum of all the work in one pass.

A cooperative scheduler runs short jobs at fixed periods from a timer tick, and an event-driven design sleeps until an interrupt posts an event. Write module logic as explicit state machines with a state enum, events and a transition table or switch. Choose an RTOS when you have several independent activities with different deadlines, blocking protocol stacks, or a team large enough that isolation between tasks pays for itself.

Practice. Build a super-loop with a 1 ms tick that runs a sensor job, a display job and a command parser at different rates, written as state machines.

RTOS Fundamentals with FreeRTOS Core

An RTOS runs several tasks, each with its own stack and priority, and its scheduler always runs the highest-priority ready task. FreeRTOS is the most common kernel in interviews. Know task states (running, ready, blocked, suspended), the tick interrupt, time slicing between equal priorities, and how a context switch works on Cortex-M: the scheduler pends PendSV at the lowest priority, and its handler saves R4 to R11 on the current task's process stack and restores the next task's.

Configure it through FreeRTOSConfig.h: tick rate, heap scheme or static allocation, stack overflow checking, and configMAX_SYSCALL_INTERRUPT_PRIORITY, above which interrupts must not call the kernel. Size each task's stack from measurements using the high-water mark, and turn on overflow hooks during development.

Zephyr is the main larger alternative: it adds a device model, networking and Bluetooth, describes hardware in devicetree and selects features with Kconfig. ThreadX, embOS and RTX use the same concepts.

Practice. Port a super-loop project to FreeRTOS with three tasks, static allocation and stack high-water reporting over UART.

Queues, Semaphores, Mutexes and Priority Inversion Core

Tasks communicate through queues that copy messages, signal through semaphores or lighter task notifications, and protect shared resources with mutexes. Binary semaphores are for signalling, often from an interrupt to a task. Mutexes are for ownership and must be released by the task that took them.

Priority inversion happens when a high-priority task waits for a mutex held by a low-priority task while a medium-priority task keeps the low one from running. Priority inheritance, built into FreeRTOS mutexes, raises the holder's priority temporarily. Also know deadlock (two tasks taking two locks in opposite orders), and prevent it with a fixed lock order and timeouts.

Practice. Create a priority inversion with three tasks and a binary semaphore, show it in a trace, then fix it with a mutex.

Real-Time Timing, Latency and Worst Case Core

Real-time means meeting deadlines, not being fast on average. A hard real-time function, such as a motor current loop, fails if it is late once. Measure interrupt latency, task response time and jitter with GPIO toggles and a scope or logic analyser, or with the cycle counter DWT->CYCCNT on M3 and above.

Know the sources of delay: higher-priority interrupts, critical sections that mask interrupts, flash wait states, cache misses, bus contention with DMA and long library calls. For hard deadlines, put the work in a high-priority interrupt or hardware, keep it out of the RTOS's masked range, and test under worst-case load rather than the typical case.

Practice. Measure the worst-case jitter of a 10 kHz control interrupt while UART, DMA and flash writes run, and reduce it by changing priorities.

Stage 6 Reliability: power, watchdogs, faults, storage and maths

Advanced3 weeks6 topics

Goal. Make firmware run for years: low power with measured current, watchdogs that catch real hangs, fault handlers that leave evidence, safe flash storage and fast fixed-point maths.

Low-Power Modes and Current Measurement Core

Microcontrollers offer several power modes. In sleep the core stops but peripherals run and any interrupt wakes it within a few cycles. In stop or deep sleep most clocks stop and RAM is kept, and wake-up takes longer and may need the clocks reconfigured. In standby or shutdown almost everything is off, RAM is usually lost and wake-up looks like a reset.

Firmware enters a mode with WFI or WFE after setting the sleep-deep bit and peripheral options. An RTOS can use tickless idle, stopping the periodic tick and programming a low-power timer for the next deadline. Battery life is set by average current, so measure it with a current profiler or a shunt and scope over a full duty cycle. Common leaks: floating pins, peripherals left clocked, debug connected, and pull-ups fighting outputs.

Practice. Put a board into stop mode between one-second wake-ups, measure average current, then halve it by fixing pins and peripheral clocks.

Watchdogs and Health Monitoring Core

A watchdog resets the device if firmware stops refreshing it in time. An independent watchdog runs from its own oscillator so it works even if the main clock fails. A window watchdog also resets if refreshed too early, which catches code running in a tight wrong loop. Once enabled, many watchdogs cannot be stopped.

Never refresh from a timer interrupt, because interrupts often keep running when the main code is stuck. In an RTOS, have each important task check in with a monitor, and refresh only when every task has reported within its period. Record the reset cause from the reset status register at start-up and count watchdog resets in persistent storage, so field data shows which builds hang.

Practice. Build a task-level health monitor that refreshes the watchdog only when all three tasks report, and log the reset cause on every boot.

HardFault Analysis Core

When a Cortex-M faults, it enters a fault handler with the exception frame on the stack. Bit 2 of the LR value tells you whether it was the main or process stack. From the frame read the stacked PC, which points at or near the faulting instruction, and the LR, which shows the caller.

On M3 and above read the fault status registers: CFSR holds memory management, bus and usage fault bits, such as precise or imprecise bus error, undefined instruction, invalid state, unaligned access and divide by zero; HFSR shows a forced escalation; MMFAR and BFAR hold the bad address when marked valid. An invalid state fault with a PC whose lowest bit is clear usually means a call through a corrupted function pointer. Imprecise bus faults point a few instructions after the real cause. An MPU guard region below each stack turns a silent overflow into an immediate fault. Save this data to a no-init RAM area, reset, and report it on the next boot.

Practice. Trigger a null pointer write, a divide by zero and a bad function pointer, then decode each fault from the registers and stacked frame.

Flash Programming and Wear Core

Internal flash is erased in sectors or pages, typically 1 KB to 128 KB, and an erase sets all bits to 1. Programming can only change 1 bits to 0, in units of a word, double word or more, and many parts forbid writing the same unit twice without an erase. Erase takes milliseconds and, on single-bank parts, stalls code running from the same flash, so time-critical code may need to run from RAM during it.

Each sector survives a limited number of erase cycles, often around 10,000 for internal flash and around 100,000 for external NOR, so data written often must be spread across sectors. Check voltage before programming, because a brown-out during a write leaves a partly written sector.

Practice. Write a driver that erases and programs internal flash, verifies by read-back and refuses to start when the supply voltage is low.

EEPROM, EEPROM Emulation and Power-Loss Safe Storage Core

Configuration, calibration and counters must survive power loss. Real EEPROM, internal or on I2C, allows byte writes with a high endurance but has write delays and page-boundary rules. Most microcontrollers lack it, so firmware emulates it in flash: records with a key, value, sequence number and CRC are appended to an active page, and when the page fills, the latest values are copied to a fresh page and the old one is erased.

The design must survive power loss at any instruction. Write the data first and a valid marker last, use CRCs to reject torn records, keep two copies of critical settings, and on start-up pick the newest valid copy. Wear levelling spreads erases across pages. For larger data, use a power-safe file system such as littlefs, which is designed for small flash devices.

Practice. Build a key-value store on two flash pages and test it by cutting power at random points thousands of times with a relay or simulated in host tests.

Fixed-Point Maths and DSP Core

Parts without an FPU, and hot loops on parts with one, use fixed-point numbers: integers with an implied binary point. In Q15 a 16-bit value represents −1 to just under 1, and in Q16.16 a 32-bit value has 16 integer and 16 fractional bits. Multiplying two Q15 numbers gives a Q30 result in 32 bits, which is shifted right by 15, with rounding and saturation to avoid wrap-around.

static inline int16_t q15_mul(int16_t a, int16_t b) {
    int32_t p = (int32_t)a * b + (1 << 14); /* round */
    p >>= 15;
    if (p > INT16_MAX) p = INT16_MAX;      /* saturate */
    return (int16_t)p;
}

Know how to choose a format from the value's range and needed resolution, how to implement moving averages and first-order IIR filters, and when to use the CMSIS-DSP library. On M4F parts, single-precision float is fast, but double is done in software.

Practice. Implement a Q15 low-pass filter, compare its output with a floating-point version on the host and report the maximum error.

Stage 7 Bootloaders, secure boot and firmware updates

Advanced2–3 weeks5 topics

Goal. Design and build a bootloader with signed images, A/B or swap updates and automatic rollback, and explain the threat model behind each choice.

Bootloader Design Core

A bootloader is a small program at the reset address that decides which application to run, validates it and jumps to it. It may also accept a new image over a serial link, USB or radio. Keep it small, simple and rarely changed, because a bootloader bug can make every device unrecoverable, and many products never update the bootloader in the field. Many parts also have a ROM bootloader, selected by boot pins or option bytes, that accepts images over UART or USB and is useful for recovery.

The jump must be clean: disable interrupts, stop the SysTick and peripherals the bootloader used, clear pending interrupts, set VTOR to the application's vector table, load the main stack pointer from its first word, and branch to its reset handler. Leftover state, such as a running DMA channel or an enabled interrupt with no handler in the application, causes faults that appear only on some boots.

__disable_irq();
SysTick->CTRL = 0;
SCB->VTOR = APP_BASE;
__set_MSP(*(uint32_t *)APP_BASE);
((void (*)(void))(*(uint32_t *)(APP_BASE + 4)))();

Practice. Write a bootloader that checks an application's CRC and jumps to it, and an application linked at the matching offset.

Image Format, Versioning and Integrity Core

Each application image carries a header: magic number, header version, image size, firmware version, a hash or CRC, hardware compatibility, and optionally a signature and dependencies on other images.

A CRC detects accidental corruption, such as a failed flash write. A cryptographic hash such as SHA-256 detects any change, and a signature proves who produced it. Use semantic versions plus a monotonic security counter. Keep the build reproducible and record the git commit and build identifier in the image, so a crash report from the field maps to exact source code. The Git and GitHub Roadmap covers tagging and release practice.

Practice. Add a post-build script that appends a header with size, version and SHA-256 to your binary, and make the bootloader reject a modified image.

OTA and DFU Transports Core

Firmware reaches a device in many ways: USB DFU class, a UART or CAN protocol from a service tool, Bluetooth Low Energy from a phone app, Wi-Fi or cellular from a server, or a host processor writing to a co-processor over SPI. Each needs a protocol with framing, chunk numbering, acknowledgements, resumption after interruption and a final integrity check.

Downloads on battery devices must handle loss of power and link at any point. Write chunks to a staging slot, never over the running image, and verify the complete image before marking it ready. Over the air, check that the battery has enough charge for the download and the install.

Practice. Implement a resumable chunked update protocol over UART with a Python host script, and test it by unplugging the cable mid-transfer.

A/B Slots, Swap Updates and Rollback Core

With A/B slots the device has two application slots. It runs from one and writes the new image into the other, then marks the new slot as pending and reboots. If the new image fails to start, the old slot is still intact. This needs twice the flash but makes updates almost impossible to brick.

When the application must run from one fixed address, a swap scheme exchanges the primary and secondary slots sector by sector using a scratch area, recording progress so a power cut mid-swap resumes. In both designs the new image boots in a trial state. It must confirm itself after a self-test, for example after it has connected to the server and survived a few minutes. If it resets before confirming, the bootloader reverts to the previous image. MCUboot, used with Zephyr and other systems, implements these schemes and is worth studying.

Practice. Add a trial flag and confirm call to your bootloader, then show that an image which crashes on start-up is rolled back automatically.

Secure Boot and the Chain of Trust Core

Secure boot means each stage runs the next only after verifying its signature with a public key it trusts. The first key or its hash lives in immutable storage, such as ROM, one-time-programmable fuses or protected flash, and forms the root of trust. Signatures use ECDSA P-256 or Ed25519, with the private key kept in a hardware security module or offline signing service, never in the repository.

Signing alone is not enough. Add anti-rollback with a monotonic counter so an attacker cannot install an older signed image with a known vulnerability. Lock or authenticate the SWD port in production, enable read-out protection, and verify the image you will run, not a copy that can change after checking. Plan key revocation and rotation before you ship. On Armv8-M parts such as the M33, TrustZone can keep keys and update logic in a secure world the application cannot read. Encrypt images if they contain secrets or intellectual property, but remember that encryption gives confidentiality, not authenticity.

Practice. Sign images with an ECDSA key on the host, verify the signature in your bootloader with a small crypto library, and reject a downgraded version.

Stage 8 Debugging, bring-up, testing and quality

Advanced3–4 weeks5 topics

Goal. Bring up a new board methodically, debug with a probe and lab instruments, and ship firmware that is unit tested on the host, tested on real hardware in CI and checked against a coding standard.

JTAG, SWD and Debug Probes Core

Cortex-M parts expose a debug port through JTAG or the two-wire Serial Wire Debug (SWDIO and SWCLK). A probe such as a J-Link, ST-LINK or CMSIS-DAP device connects it to your PC, and a GDB server such as OpenOCD, pyOCD or the probe vendor's server lets GDB or an IDE halt the core, read memory and registers, flash images and set breakpoints.

Know the hardware limits: Cortex-M3 and M4 typically have six hardware breakpoints and four watchpoints. A data watchpoint on an address that gets corrupted finds the culprit in minutes. If the probe cannot connect, the cause is often a firmware that disables the debug pins, enters deep sleep, or locks the part, and connecting under reset is the fix. Learn GDB commands: bt, info registers, x/16wx, watch and monitor reset halt.

Halting the core changes timing, so also learn tools that watch it run: SWO with the ITM or Segger RTT for fast logging, and RTOS trace viewers that show task switches and interrupts on a timeline.

Practice. Debug a deliberately corrupted global with a GDB data watchpoint over SWD, using only the command line.

Hardware Bring-Up Core

Bring-up is getting firmware running on a new board for the first time. Work in order. Inspect the board, then power it from a current-limited supply and check each rail. Check the reset line and the oscillator. Connect the debugger and read the device ID. Run a minimal program that toggles a pin. Then add clocks, a UART console, and each peripheral one at a time, comparing each with the schematic.

You need enough electronics to read the schematic: pull-ups on open-drain lines, logic levels between 1.8, 3.3 and 5 V parts, decoupling, crystals and brown-out. A logic analyser with protocol decoders shows what crossed a bus, and an oscilloscope shows slow edges, ringing and supply dips. Keep a bring-up log with every measurement. When something fails, separate hardware from software: check the pin on a scope, compare with a known-good development board, and re-read the schematic and the part's errata. Common causes are swapped TX and RX lines, missing pull-ups, a wrong crystal value, a sensor at a different address from the datasheet's default, and a chip select on the wrong pin.

Practice. Write a bring-up checklist for a board with a microcontroller, an SPI flash, an I2C sensor and a CAN transceiver, and follow it on any board you have.

Unit Testing on the Host Core

Most firmware logic, such as protocols, state machines, filters, parsers and storage formats, does not need hardware and can be tested on a PC in seconds. Unity, CppUTest and GoogleTest are common frameworks, with CMock or fakes to replace drivers. Design for this: keep hardware access in thin drivers behind interfaces, and pass dependencies in rather than calling registers from business logic.

Build host tests with sanitisers to catch overflows and undefined behaviour. Be aware of differences between host and target: int size, endianness, alignment and floating-point behaviour, and run at least some tests on the target too.

Practice. Put a command parser and a ring buffer under host unit tests with Unity or GoogleTest, including fakes for the UART driver, and run them in CI.

Hardware-in-the-Loop Testing and CI Recommended

Hardware-in-the-loop (HIL) tests run the real firmware on real boards connected to a test rig that drives inputs and checks outputs: a Raspberry Pi or test PC with GPIO, relays to cut power, a CAN interface, programmable supplies and simulated sensors. Tests flash the build, reset the board and run scripted scenarios, usually in Python with pytest.

A good firmware pipeline builds every commit for every target, runs static analysis and host tests, checks code size budgets, then flashes a board farm for HIL smoke tests, and stores signed artefacts for release. Test power-loss during updates, brown-outs, long soak runs and temperature extremes before shipping. Build servers, rigs and host tools run on Linux and Python, which the Linux Roadmap covers.

Practice. Write a pytest script that flashes your board, resets it, sends commands over UART and checks the responses, and run it from a CI job.

MISRA C, Static Analysis and Coding Standards Core

MISRA C is a set of guidelines for C in critical systems, widely used in automotive, medical and industrial firmware and increasingly elsewhere. Its rules avoid undefined and implementation-defined behaviour, restrict dynamic memory and recursion, require explicit conversions and a single point of exit in some styles, and forbid confusing constructs. Rules are mandatory, required or advisory, and deviations must be documented and justified.

Static analysers such as PC-lint Plus, Polyspace, Coverity, Cppcheck, clang-tidy and the analysers in commercial compilers check these rules and find bugs such as null dereferences and buffer overruns. In interviews, show that you understand why a rule exists rather than reciting numbers, and that you can explain a sensible deviation. In cars, medical devices and industrial machines, safety standards such as ISO 26262, IEC 62304 and IEC 61508 add traceable requirements, coverage targets and run-time self-tests on top.

Practice. Run Cppcheck with a MISRA add-on on your project, fix the findings that indicate real risk and write one justified deviation.

Stage 9 Interview preparation

Advanced2 weeks3 topics

Goal. Solve firmware coding problems on a whiteboard, design a device's firmware architecture aloud and tell clear stories about bugs you have found.

Firmware Coding Rounds Core

Firmware coding questions are usually in C and test precision more than algorithms. Expect bit manipulation, a ring buffer, a fixed-size memory pool, parsing a framed protocol with checksums, a debounce routine, a moving average or fixed-point maths, implementing memcpy or atoi safely, and finding the bug in a short interrupt or driver snippet. Some companies also give standard data structure problems, so revise arrays, linked lists, hashing and complexity from the Data Structures and Algorithms Roadmap.

Talk while you write: state the types and their widths, mention overflow and alignment, handle errors, and say what happens if an interrupt runs in the middle. Write compilable code with no hidden assumptions, then test it by hand with edge cases such as empty, full, wrap-around and maximum values. Practise in the coding practice workspace under time limits.

Practice. Solve ten firmware-style C problems with a 25-minute limit each, writing the tests before the code.

Firmware System Design Rounds Core

A design round asks you to sketch firmware for a device: a battery-powered sensor that reports over LoRa or BLE, a motor controller, a smart lock, a USB peripheral or a data logger. Start with requirements and constraints: power budget, timing, memory, connectivity, safety and update needs. Then choose the microcontroller class, draw the hardware blocks, and lay out the software layers: drivers, middleware, application state machines and the scheduling model.

Cover the parts many candidates forget: boot and update with rollback, fault handling and watchdogs, storage of configuration, manufacturing test and provisioning, logging and field diagnostics, and how you would test it. Give numbers, such as average current for a target battery life or a buffer size from data rate and latency. Use the system design practice workspace to practise explaining a design out loud.

Practice. Design the firmware for a coin-cell BLE temperature tag that must last two years, including a current budget and an update plan, in 45 minutes.

Debugging Stories and Behavioural Questions Core

Every firmware interview asks about a hard bug. Prepare three stories in a clear structure: the situation and symptom, how you narrowed it down, the root cause, the fix and what you changed so it would not happen again. Good stories show method and evidence: you reproduced it, measured it with a scope or trace, formed a hypothesis and tested it, and worked with hardware colleagues without blame.

Also prepare examples of a design trade-off you made, a time you missed a deadline or shipped a bug, a disagreement with a hardware engineer, and how you raised a safety or quality concern. Use the study plan workspace to schedule your preparation and progress page to track it.

Practice. Write and rehearse three debugging stories aloud, each under two minutes, and record yourself once to check clarity.

Projects to build

Build these in order. Each one should end up in a public repository with a short README that explains your decisions.

  1. Bare-metal start-up and UART consoleBeginner

    Without the vendor library, write the vector table, reset handler and linker script for your board, configure the clocks and build a UART console with a ring buffer and a few commands. Document each register you set and why. This project alone answers half the questions in a firmware screen.

    Start-up code · Linker scripts · Clock tree · UART · Ring buffers

  2. Low-power sensor nodeIntermediate

    Read an I2C or SPI sensor every 30 seconds, filter readings in fixed point, store them in a power-loss safe log in flash and send them over UART or BLE. Sleep in stop mode between readings, measure the average current and calculate battery life for a real cell.

    I2C or SPI · Low-power modes · Fixed-point maths · Flash storage

  3. RTOS motor or LED controller with health monitoringIntermediate

    On FreeRTOS or Zephyr, run a 10 kHz PWM control interrupt, a command task, a telemetry task and a health monitor that feeds the watchdog only when all tasks check in. Measure control-loop jitter under load and include a timeline trace in your write-up.

    Timers and PWM · RTOS · Interrupt priorities · Watchdogs

  4. Secure bootloader with A/B updates and rollbackAdvanced

    Write a bootloader that verifies an ECDSA signature and version counter, supports two slots with a trial boot and confirmation, and rolls back automatically if the new image fails. Provide a host tool that signs and sends images in chunks, and a test that cuts power at random points during updates.

    Bootloaders · Secure boot · OTA and DFU · Flash programming

Preparation plan

  1. Weeks 1–2C precision and a bare-metal start
    • Revise the C Roadmap and stage two of this page; solve two bit manipulation or pointer problems every day.

    • Blink an LED and read a button with direct register access, then replace the vendor start-up file with your own.

    • Write a linker script with a no-init section and read the map file line by line.

  2. Weeks 3–4Drivers and buses
    • Write UART with DMA and idle-line detection, SPI flash and I2C sensor drivers with timeouts.

    • Verify every driver on a logic analyser and keep screenshots for your portfolio.

    • Configure the clock tree from the reference manual and measure the result on a pin.

  3. Weeks 5–6Interrupts and RTOS
    • Port a super-loop project to FreeRTOS with static allocation, queues and a mutex, and record a trace.

    • Reproduce and fix a torn read and a priority inversion, and explain both aloud.

    • Add a task-level watchdog monitor and log reset causes across reboots.

  4. Weeks 7–8Reliability, storage and updates
    • Build a bootloader with image headers, signature checks, A/B slots and rollback.

    • Write a power-loss safe key-value store and test it with random power cuts.

    • Decode three deliberate HardFaults from the registers and put the decoder in your fault handler.

  5. Weeks 9–10Interview practice
    • Do timed firmware coding problems in the coding practice workspace and two mock design rounds a week.

    • Rehearse your debugging stories and answer the questions on this page aloud.

    • Track gaps in the progress page view and go back to the weakest stage.

Firmware Developer interview questions and answers61 questions by topic, 10 coding exercises with solutions, design and debugging scenarios, a mock interview and a cheat sheet.
Open the question bank

Interview questions

Try to answer each question out loud before you open the answer.

What happens on a Cortex-M from reset to the first line of main?Junior

The core reads the initial stack pointer from address offset 0 of the vector table and the reset handler address from offset 4, then runs the reset handler in privileged Thread mode. The handler copies .data from flash to RAM, zeroes .bss, calls the system initialisation that sets clocks and flash wait states, runs C++ constructors if any, and calls main. A bootloader may run first and relocate the vector table with VTOR before jumping to the application.

When do you need volatile, and what does it not do?Junior

Use it for memory-mapped registers and for variables shared between an interrupt and other code, so the compiler performs every access instead of caching a value or removing a loop. It does not make an access atomic, so a 64-bit read or a read-modify-write can still be torn by an interrupt, and it does not order accesses to other variables. Shared data still needs a critical section, a single-writer design or atomics.

Write a macro or function to set, clear and read a 3-bit field at bit 4 of a 32-bit register.Junior

Build a mask 0x7u << 4. To write value v, read the register, clear the field with &= ~mask, OR in (v << 4) & mask, and write once. To read, shift right by 4 and AND with 0x7u. Use unsigned constants, mask the input so a bad value cannot spill into other bits, and note that the read-modify-write is not atomic if an interrupt also writes that register.

How do NVIC priorities work, and how would you assign them in a motor controller?Mid

Lower numbers are more urgent, a higher-priority interrupt preempts a lower one, and grouping splits preemption from sub-priority. I would give the current-loop or PWM fault interrupt the highest priority and keep it outside the range the RTOS masks, then encoder capture, then communications and DMA completion, then the RTOS tick and PendSV at the lowest. Critical sections would use BASEPRI so the control interrupt is never delayed by kernel calls, and I would measure latency on a scope to confirm.

Explain priority inversion and how to prevent it.Mid

A high-priority task blocks on a mutex held by a low-priority task, and a medium-priority task that needs neither runs instead of the low one, so the high task waits for an unbounded time. Fixes are priority inheritance, where the holder temporarily gets the waiter's priority, which FreeRTOS mutexes provide, or a priority ceiling. Also keep locked sections short, avoid sharing resources across very different priorities, and use a binary semaphore only for signalling, never for ownership.

When would you choose a super-loop over an RTOS?Mid

A super-loop with interrupts and state machines suits small parts, a few activities with relaxed deadlines, and products where certification or code size favour simplicity. Its worst-case response is the length of one loop pass, which is easy to analyse. I would choose an RTOS when there are several independent activities with different deadlines, blocking protocol stacks such as TCP/IP or Bluetooth, or a team that benefits from isolated tasks. The cost is stacks per task, more concurrency bugs and more configuration.

How do you safely share a 64-bit timestamp between an interrupt and main code on a 32-bit core?Mid

The read takes two loads, so an interrupt between them can give a mixed value. Options: disable interrupts briefly around the read while saving and restoring the previous mask; read high, low, high again and retry if the high word changed; or keep a 32-bit counter that is naturally atomic and extend it in the reader. On Cortex-M3 and above, exclusive load and store can protect read-modify-writes, but a simple masked read is often clearest.

A device in the field reboots about once a week. How do you investigate?Mid

First collect evidence: read the reset cause register on every boot and store it with a count, and save fault registers and the stacked PC in no-init RAM before resetting. That tells me whether it is a watchdog, a HardFault, a brown-out or a software reset. Then I look for slow leaks such as a 32-bit counter wrapping, heap fragmentation, a queue that fills, or stack growth, and run a soak test at accelerated rates. I check whether failures correlate with temperature, battery level or a radio event.

How do you analyse a HardFault?Mid

In the handler, use bit 2 of LR to pick MSP or PSP and read the stacked frame: R0 to R3, R12, LR, PC and xPSR. The stacked PC points at or near the faulting instruction, and LR shows the caller. Then read CFSR for the fault type, HFSR for escalation, and MMFAR or BFAR when their valid bits are set. Imprecise bus faults point after the real cause, so I temporarily disable write buffering to make them precise. Look up the PC in the map file or with addr2line.

Your I2C bus hangs after a reset during a transfer. What is happening and how do you fix it?Mid

The slave was part-way through sending a byte when the master reset, so it is still holding SDA low and waiting for clocks. The master sees a busy bus and cannot send a start. The fix is bus recovery at start-up and on timeout: configure SCL as a GPIO, clock it up to nine times until SDA goes high, generate a stop, then re-initialise the peripheral. All I2C operations should have timeouts and return errors, and I would check the part's errata for known lock-up issues.

How would you design a firmware update that cannot brick the device?Senior

Keep a small, rarely changed bootloader. Download the new image into a separate slot, never over the running one, in chunks that can resume, and verify the full image hash and signature before marking it pending. On reboot the bootloader boots it in trial mode, by swapping or by selecting the slot. The application confirms only after a self-test, such as connecting to the server. If it resets before confirming, the bootloader reverts. Add anti-rollback counters, check battery before starting, and test power cuts at every step.

What does secure boot protect against, and what does it not?Senior

It ensures that only code signed by the holder of the private key runs, rooted in an immutable key or key hash in ROM or fuses. It stops modified or malicious images being installed through the update path or the debug port, if that is locked. It does not stop a signed but vulnerable old image unless you add anti-rollback, does not hide the firmware unless images are encrypted, and does not stop glitching or side-channel attacks without hardware countermeasures. Key handling and revocation matter as much as the code.

How do you store configuration in flash so that power loss never corrupts it?Mid

Flash erases in sectors and has limited endurance, so I append records to a log rather than rewrite in place. Each record has a key, value, sequence number and CRC, and its valid marker is written last. When a page fills, current values are copied to a fresh page before the old one is erased. On boot, the newest record with a valid CRC wins. Critical values have two copies, and I test by cutting power at random points thousands of times.

Tell me about the hardest firmware bug you have found.Mid

A strong answer is specific and evidence-led: the symptom and its impact, how the bug was reproduced, what was measured with which tool, the hypotheses tested and rejected, the root cause, the fix, and the lasting change, such as a new test, an assertion or a review rule. Mention who you worked with, for example a hardware engineer who confirmed a supply dip on a scope. Keep it to about two minutes and avoid blaming anyone.

A hardware engineer says a fault is in your firmware and you think it is the board. What do you do?Senior

I would move from opinion to evidence together. Agree on a reproduction, then measure at the boundary: probe the pins, supply rails and timing on a scope while the fault happens, and try the firmware on a known-good board and the same board with minimal test firmware. Whatever the result, share the data and fix it jointly. In my experience the cause is often an interaction, such as firmware enabling a load that the supply cannot handle, so both sides usually change something.

Common mistakes

  • Forgetting to enable a peripheral's clock before writing its registers; check the clock enable bit first whenever a new driver does nothing.
  • Treating volatile as a lock; protect multi-word or read-modify-write shared data with a critical section, atomics or a single-writer design.
  • Doing slow work inside interrupt handlers; keep handlers short and move processing to a task or the main loop.
  • Refreshing the watchdog from a timer interrupt; refresh only when every important task has proved it is alive.
  • Overwriting the running image during an update; always write to a separate slot and boot it in a trial state with rollback.
  • Guessing stack sizes; fill stacks with a pattern, measure the high-water mark under worst-case load and add a guard region.
  • Leaving every interrupt at the default priority; assign priorities deliberately and keep the most time-critical ones outside the RTOS masked range.
  • Testing only on hardware; move logic into host-testable modules so most bugs are caught in seconds rather than at the bench.

Ready-for-interview checklist

  • I can explain the reset sequence and write a vector table, reset handler and linker script from memory.
  • I can explain volatile, const, static, integer promotion and struct padding with examples of the bugs they cause.
  • I can write bit field macros, a ring buffer and a memory pool in C without looking anything up.
  • I can configure a clock tree and write UART, SPI, I2C, timer, ADC and DMA drivers from the reference manual.
  • I can assign NVIC priorities, explain BASEPRI and protect shared data between interrupts and tasks.
  • I can explain FreeRTOS scheduling, context switching with PendSV, queues, mutexes and priority inversion.
  • I can decode a HardFault from the stacked frame and the fault status registers.
  • I can design a watchdog strategy and use reset cause registers to diagnose field resets.
  • I can describe power modes and estimate battery life from a current budget.
  • I can design a bootloader with signed images, A/B or swap slots, trial boot and rollback.
  • I can implement a power-loss safe settings store in flash and explain wear.
  • I can bring up a new board methodically with a debugger, logic analyser and oscilloscope.
  • I can write host unit tests with fakes and explain how hardware-in-the-loop tests fit into CI.
  • I can tell three clear debugging stories with evidence, root cause and lasting fix.

Where this role leads

Related roadmaps

FAQ

What is the difference between a firmware developer and an embedded systems engineer?

The titles overlap and many companies use them interchangeably. Where they differ, a firmware developer works on bare-metal and RTOS code running directly on microcontrollers, from the reset vector up, while an embedded systems engineer may also cover embedded Linux, larger processors and system integration. A device driver developer usually means drivers inside an operating system kernel such as Linux.

Do I need an electronics degree to become a firmware developer?

No. Many firmware developers studied computer science or taught themselves. You do need to read schematics, understand pull-ups, logic levels, power rails and decoupling, and use a scope and logic analyser, all of which you can learn with a development board. Roles close to power electronics or analogue design may prefer an electrical engineering background.

Which microcontroller should I learn on?

Start with an Arm Cortex-M development board that has an on-board debug probe, such as an STM32 Nucleo or a Nordic nRF52 board. These have good documentation, free toolchains and match many job adverts. After that, learning a second family, such as an ESP32 or a RISC-V part, shows that your skills transfer.

Is Rust worth learning for firmware?

It is growing in firmware, especially for new products and security-sensitive code, because its ownership rules prevent many memory and concurrency bugs at compile time. Most existing firmware and most interviews are still in C, so learn C first and treat Rust as a strong second skill. The Rust Roadmap covers the language.

Practise while you learn.Solve interview problems in the browser, with hints and a timer.
CodingSQLSystem design
No Comment
Add Comment
comment url