Skip to main content

Embedded engineer coding interview questions

11 questions on coding for embedded engineer candidates. Expect prompts such as “ISR-safe ring buffer implementation” and “Implement a fixed-size ring buffer for UART receive”, each with a worked answer outline and the follow-ups interviewers push on.

Questions

Showing 1 to 11 of 11 coding questions.

As asked

Implement a lock-free single-producer single-consumer ring buffer in C for passing bytes between a UART ISR (producer) and a main-loop consumer on a single-core Cortex-M. It must not use mutexes or disable interrupts.

Sample answer outline

The candidate should use separate head and tail indices with volatile qualifiers and ensure that the producer only writes head after writing data, and the consumer only writes tail after reading data. The key is that on a single-core system with a single producer and consumer, no locking is needed as long as head and tail each have a single writer. They should use a power-of-two buffer size and modulo-via-mask, and be careful that size checks use copies of the volatile indices to avoid re-reading a changing value mid-check.

Reference implementation (c)

C
#define BUF_SIZE 256  // must be power of two

typedef struct {
    uint8_t  data[BUF_SIZE];
    volatile uint32_t head;  // written only by producer (ISR)
    volatile uint32_t tail;  // written only by consumer (main)
} RingBuf;

void rb_push(RingBuf *rb, uint8_t byte);
int  rb_pop(RingBuf *rb, uint8_t *byte); // returns 1 if data available
uint32_t rb_used(const RingBuf *rb);

Expect these follow-ups

  • Why do you need volatile on head and tail here even though this is single-core?
  • How does this approach break on a multi-core system, and what is the fix?
ring-bufferisrlock-freeuartc-language

As asked

Write a small ring buffer suitable for bytes received from a UART interrupt handler. Explain the overflow policy.

Sample answer outline

Use a fixed-size buffer with head and tail indices so no allocation happens at runtime. The interrupt handler should do the minimum work: attempt to push the byte and record overflow if the buffer is full. Define the overflow policy explicitly, either drop newest, drop oldest, or set an error flag for the consumer. In real firmware, indices shared with an ISR need atomic or critical-section handling depending on the platform width and concurrency model. Candidates often write a dynamic vector or ignore the full versus empty ambiguity.

Reference implementation (rust)

Rust
struct Ring<const N: usize> {
    buf: [u8; N],
    head: usize,
    tail: usize,
    full: bool,
}

impl<const N: usize> Ring<N> {
    fn new() -> Self {
        Self { buf: [0; N], head: 0, tail: 0, full: false }
    }

    fn push(&mut self, byte: u8) -> bool {
        if self.full {
            return false;
        }
        self.buf[self.head] = byte;
        self.head = (self.head + 1) % N;
        self.full = self.head == self.tail;
        true
    }

    fn pop(&mut self) -> Option<u8> {
        if !self.full && self.head == self.tail {
            return None;
        }
        let byte = self.buf[self.tail];
        self.tail = (self.tail + 1) % N;
        self.full = false;
        Some(byte)
    }
}

Expect these follow-ups

  • How would this change for DMA-backed UART receive?
  • What makes the shared indices safe or unsafe on an 8-bit MCU?
  • When would you drop oldest instead of newest?
uartring-bufferno-alloc

As asked

Implement a minimal bit-banged I2C master in C that writes N bytes to a slave at a given 7-bit address. Handle ACK/NAK detection and include a start and stop condition. Assume you have gpio_set(pin, val) and gpio_get(pin) and delay_us(n) available.

Sample answer outline

The candidate should implement start condition (SDA low while SCL high), clock out 8 bits MSB-first with SCL toggling, release SDA for the ACK bit and check it, then repeat for each byte, and finally issue a stop condition (SDA high while SCL high). They should handle NAK by aborting and issuing stop. The key gotchas are correct bit order, SDA stability during SCL high, and open-drain semantics (never drive SDA/SCL high, only release).

Reference implementation (c)

C
// GPIO abstractions available:
// void gpio_set(int pin, int val);  // 0 = low/drive, 1 = release (open-drain)
// int  gpio_get(int pin);           // read pin state
// void delay_us(unsigned int us);

#define SCL_PIN 0
#define SDA_PIN 1
#define I2C_DELAY 5  // half-period in us

void i2c_start(void);
void i2c_stop(void);
int  i2c_write_byte(uint8_t byte); // returns 0 on ACK

// TODO: implement these functions

Expect these follow-ups

  • How would you add clock stretching support to your bit-bang master?
  • What changes when the slave is 10-bit addressed?
i2cbit-banggpioc-languageprotocols

As asked

Implement a table-driven CRC-32 (polynomial 0x04C11DB7, reflected) in C, as you would use it to validate a firmware image. The table must be pre-computed. Show both table generation and the CRC calculation over a byte array.

Sample answer outline

The candidate should generate a 256-entry lookup table by shifting each value 8 times and XORing with the reflected polynomial 0xEDB88320, then implement crc32_update that XORs each input byte with the low byte of the running CRC, uses the result as a table index, and XORs the table value with the upper 24 bits. The initial CRC value is 0xFFFFFFFF and the final value is XORed with 0xFFFFFFFF. They should note that STM32 has a hardware CRC peripheral but the polynomial must match.

Reference implementation (c)

C
#include <stdint.h>
#include <stddef.h>

static uint32_t crc32_table[256];

void crc32_init_table(void) {
    /* TODO: populate crc32_table using reflected poly 0xEDB88320 */
}

uint32_t crc32_compute(const uint8_t *data, size_t len) {
    uint32_t crc = 0xFFFFFFFFUL;
    /* TODO: process each byte */
    return crc ^ 0xFFFFFFFFUL;
}

Expect these follow-ups

  • How does the reflected CRC differ from the non-reflected form?
  • If you need to compute a CRC over a very large flash region at boot, how do you avoid blocking the watchdog?
crcc-languagefirmware-integrityalgorithmsflash

As asked

Implement a state machine in C that handles a single push-button. It must distinguish between a short press (< 500ms), a long press (>= 500ms), and a repeat-while-held event every 200ms after the long press. The tick function is called every 10ms.

Sample answer outline

The candidate should define states IDLE, PRESSED, LONG_PRESSED with a tick counter. On button-down transition to PRESSED and start counting. At 50 ticks (500ms) transition to LONG_PRESSED and fire the long-press event. Every 20 ticks thereafter fire repeat. On button-up from PRESSED fire short-press and reset; from LONG_PRESSED just reset. The key quality indicator is debounce handling (require N consecutive same readings before acting) and clear separation of event generation from state transitions.

Reference implementation (c)

C
typedef enum { BTN_IDLE, BTN_PRESSED, BTN_LONG_PRESSED } BtnState;

typedef struct {
    BtnState state;
    uint32_t ticks;       // incremented each 10ms call
    uint32_t repeat_ticks;
} BtnCtx;

typedef enum { EVT_NONE, EVT_SHORT, EVT_LONG, EVT_REPEAT } BtnEvent;

// Called every 10ms. raw_pressed: 1 if button is currently held.
BtnEvent btn_tick(BtnCtx *ctx, int raw_pressed);

Expect these follow-ups

  • How would you extend this to handle a double-click event?
  • Where does debounce fit into this state machine and why is it separate from the long-press logic?
state-machinec-languagegpiotiminghmi

As asked

Write portable C functions to byte-swap a uint16_t and a uint32_t without using compiler builtins. Then show how you would replace them with the ARM __REV and __REV16 intrinsics conditionally at compile time.

Sample answer outline

The candidate should write pure-C versions using shifts and masks, then wrap them in a preprocessor check for __arm__ or defined(__GNUC__) to substitute __builtin_bswap16 and __builtin_bswap32 or the CMSIS __REV/__REV16 intrinsics. They should note that __builtin_bswap32 compiles to a single REV instruction on ARM with -O1 or higher anyway, so the intrinsic is mainly needed to force the optimization at -O0.

Reference implementation (c)

C
#include <stdint.h>

static inline uint16_t swap16(uint16_t x) {
    // TODO: portable version
}

static inline uint32_t swap32(uint32_t x) {
    // TODO: portable version
}

// Then: conditional ARM intrinsic version
#if defined(__arm__) && defined(__GNUC__)
#include "cmsis_gcc.h"
// override with __REV / __REV16
#endif

Expect these follow-ups

  • Why is casting through a union to swap bytes potentially undefined behavior in C?
  • How does the compiler's strict aliasing rule affect a byte-swap implemented via a uint8_t* pointer cast?
endiannessc-languagebit-manipulationcortex-mportability

As asked

Write a C function that computes a CRC-32 over a firmware image stored in Flash and compares it against a stored expected value. The function runs in the bootloader before jumping to the application. Assume the expected CRC is stored at a fixed address just before the vector table.

Sample answer outline

The candidate should cast the flash start address to a const uint8_t pointer, call a CRC function over the image length (excluding the stored CRC word itself), compare against the value at the known fixed address, and return a boolean. They should note that the image length can be stored alongside the CRC, or be a fixed compile-time constant. They should also mention that this function must not corrupt any registers that the application startup code depends on and should be kept simple to minimize bootloader code size.

Reference implementation (c)

C
#include <stdint.h>
#include <stdbool.h>

#define APP_START_ADDR   0x08004000UL
#define APP_IMAGE_SIZE   0x00010000UL
// Expected CRC is stored at (APP_START_ADDR - 4)
#define APP_CRC_ADDR     (APP_START_ADDR - 4)

extern uint32_t crc32_compute(const uint8_t *data, size_t len);

bool bootloader_verify_image(void) {
    const uint8_t *image = (const uint8_t *)APP_START_ADDR;
    uint32_t expected = *(const uint32_t *)APP_CRC_ADDR;
    // TODO: compute and compare
}

Expect these follow-ups

  • How do you embed the expected CRC into the firmware image at link time?
  • What prevents an attacker from modifying both the firmware and the stored CRC?
crcbootloaderflashsecurityc-language

As asked

Implement a fixed-size max-priority queue in C for scheduling deferred work items on a bare-metal system. Each item has a uint8_t priority. The queue must support enqueue and dequeue operations and must not use dynamic memory allocation.

Sample answer outline

The candidate should implement a binary heap stored in a fixed-size array, with enqueue performing sift-up and dequeue removing the root and performing sift-down. They should define a compile-time MAX_ITEMS constant, use a simple struct for work items, and ensure the code handles full and empty conditions. They should note that this gives O(log n) enqueue and dequeue, which is acceptable for small queues common in embedded systems.

Reference implementation (c)

C
#define MAX_WORK_ITEMS 16

typedef struct {
    uint8_t priority;
    void (*handler)(void *arg);
    void *arg;
} WorkItem;

typedef struct {
    WorkItem items[MAX_WORK_ITEMS];
    int size;
} WorkQueue;

void wq_init(WorkQueue *q);
int  wq_push(WorkQueue *q, WorkItem item);  // 0 on success, -1 if full
int  wq_pop(WorkQueue *q, WorkItem *out);   // 0 on success, -1 if empty

Expect these follow-ups

  • How would you make enqueue safe to call from an ISR?
  • What is the maximum queue depth you would tolerate before the O(log n) cost becomes a concern at your interrupt rate?
priority-queueheapc-languagealgorithmsbare-metal

As asked

Implement a discrete PID controller in C using fixed-point arithmetic (no floating point) suitable for a Cortex-M0 that lacks an FPU. The controller runs at 1kHz. Show the accumulator type choices and anti-windup logic.

Sample answer outline

The candidate should use a Q-format fixed-point representation (e.g., Q16.16 or Q24.8 depending on the value range), compute the proportional, integral (accumulated error), and derivative (difference of last two errors) terms, and sum them. Anti-windup should clamp the integral accumulator when the output saturates, not just clamp the final output. They should choose int32_t or int64_t accumulators carefully to avoid overflow and show where they divide (shift) to stay in range.

Reference implementation (c)

C
// Fixed-point format: Q16 (values in units of 1/65536)
typedef int32_t q16_t;
#define Q16(x)  ((q16_t)((x) * 65536))

typedef struct {
    q16_t kp, ki, kd;
    q16_t integral;       // accumulated
    q16_t prev_error;
    q16_t output_min, output_max;
} PidCtx;

q16_t pid_update(PidCtx *ctx, q16_t setpoint, q16_t measured);

Expect these follow-ups

  • How do you tune Kp, Ki, Kd in fixed-point without losing precision?
  • What happens to your derivative term when a large setpoint step occurs and how do you mitigate derivative kick?
pidfixed-pointcontrol-systemscortex-mc-language

As asked

Implement a fast memset for a Cortex-M4 that writes word-aligned blocks using 32-bit stores instead of byte stores, handles unaligned head and tail bytes, and works correctly for any size and any alignment of dst.

Sample answer outline

The candidate should align the destination pointer to a 4-byte boundary by writing leading bytes one at a time, then write 4-byte words in a loop, then write any trailing bytes. They should replicate the fill byte into a uint32_t fill word (e.g., val | val<<8 | val<<16 | val<<24). They should avoid casting a uint8_t* to uint32_t* directly because of strict aliasing; instead use memcpy semantics or __attribute__((may_alias)). They should note that for large regions the STM32 DMA is faster.

Reference implementation (c)

C
#include <stdint.h>
#include <stddef.h>

void *fast_memset(void *dst, int val, size_t n) {
    uint8_t *p = (uint8_t *)dst;
    // Phase 1: align to 4 bytes
    // Phase 2: write 32-bit words
    // Phase 3: write trailing bytes
    return dst;
}

Expect these follow-ups

  • What pragma or attribute lets GCC perform a uint8_t-pointer to uint32_t-pointer alias safely?
  • When is it worth switching to DMA-based memset instead of this CPU loop?
memsetalignmentcortex-mc-languageoptimization

As asked

Implement a driver in C that serializes an RGB framebuffer to a WS2812B LED strip using STM32 TIM + DMA to generate the 800kHz pulse-width-encoded signal. Describe your buffer layout and how you handle the reset pulse at the end of a frame.

Sample answer outline

The candidate should describe encoding each bit as a DMA value for the timer compare register: a short pulse for 0 (~0.4us high, 0.85us low) and a long pulse for 1 (~0.85us high, 0.4us low) at 800kHz. The DMA buffer holds 24 compare values per LED (GRB order) plus at least 50 zero values for the reset interval. They should use a circular or one-shot DMA with the DMA complete interrupt to clear the output after the reset period. They should note that GRB byte order is correct for WS2812B, not RGB.

Reference implementation (c)

C
// WS2812B timing at 800kHz (1.25us period)
// T0H = 0.4us, T0L = 0.85us
// T1H = 0.85us, T1L = 0.4us
// Reset: SDA low >= 50us

#define NUM_LEDS     60
#define BITS_PER_LED 24
#define RESET_BITS   50
#define DMA_BUF_LEN  (NUM_LEDS * BITS_PER_LED + RESET_BITS)

static uint16_t dma_buf[DMA_BUF_LEN];  // TIM compare values

typedef struct { uint8_t r, g, b; } RGB;

void ws2812_write_frame(const RGB *leds, uint16_t count);

Expect these follow-ups

  • How does the DMA transfer complete interrupt interact with the timer output pin to generate the reset pulse?
  • What is the minimum DMA buffer size in bytes for a 60-LED strip?
ws2812bdmatimerstm32led-driver