cswork / libraryOpen practice workspace

Interview guides · Benchmark Space Systems

Benchmark Space Systems Software Engineer Interview Guide

Benchmark Space Systems Software Engineer interview guide: embedded C, RTOS, bus debugging, state machine design and a 7-day plan for all four rounds.

The 60-second brief

Benchmark Space Systems makes chemical and electric propulsion systems for satellites and spacecraft, a field it calls in-space mobility. Candidates describe the Software Engineer role as the one that writes the code commanding thrusters, valves and sensors on those propulsion units. That means embedded flight software on propulsion controllers, low-level drivers for custom avionics, RTOS applications for thruster operation, and Python tooling for hardware-in-the-loop testing, telemetry dashboards and flight-data analysis.

This guide is for embedded, firmware and flight-software engineers preparing for the Software Engineer loop. Candidates describe it as a hardware-facing role rather than a web or full-stack one, so the guide covers embedded C and architecture, hardware interaction and debugging, fault-tolerant system design and cross-discipline behavioral stories. It also covers the four reported rounds and ends with a seven-day plan.

Candidates report four rounds over about 3 to 5 weeks: a recruiter screen, a technical screening, a practical technical assessment, and an onsite or virtual loop with a cross-functional panel.

Focus areas

  • Software Engineering
  • Embedded Systems
  • Mechatronics Integration

Overview

Candidates describe the day-to-day work as writing and testing embedded flight software for propulsion controllers. Typical tasks are drivers for custom avionics, RTOS applications that manage thruster operation, and Python scripts that automate hardware-in-the-loop (HIL) runs. They also describe telemetry dashboards for vacuum chamber tests and analysis of flight data from units already in orbit. Time in the lab with oscilloscopes, logic analyzers and debuggers is part of the job, and partners include electrical, mechatronics and mechanical design engineers.

The stack candidates list is C and C++, an RTOS such as FreeRTOS or Zephyr, bare-metal work on ARM Cortex-M, and the SPI, I2C, UART and CAN buses. Hands-on debugging with oscilloscopes, logic analyzers and JTAG is also listed. Candidates describe 3 or more years of embedded, flight-software or mechatronics-control experience as typical. Python for test automation, PID and filtering knowledge, and CI/CD for embedded systems are listed as extras. Review your own projects against this list before anything else.

The 18 reported questions fall into four groups. Embedded C and architecture has five: string reversal, memory leaks over years of uptime, volatile, the interrupt path and a bit-clear macro. Hardware interaction and debugging has five: SPI driver prerequisites, an intermittent I2C bus, pull-up sizing, switch debouncing and custom PCB bring-up. System design and fault tolerance has four: a propulsion state machine, an in-orbit bootloader, watchdog timeouts and brownout recovery. Behavioral and collaboration has four. Group one is mostly short answers and small functions, so rehearse writing them without dynamic allocation.

Candidates also report scenarios beyond that list. These include a thread-safe circular buffer for telemetry, priority inversion and two ways to prevent it, and setting, clearing and toggling register bits. Others are an I2C sensor that occasionally hangs the bus, choosing between SPI and I2C for a propulsion sensor, and a timer configured for a 10ms PWM pulse. The last three are a watchdog in a multi-threaded RTOS and a pressure transducer reporting out-of-range values mid-maneuver. Prepare these alongside the 18, because the same fundamentals sit under all of them.

The Benchmark Space Systems interview process

These are the four stages candidates report, in the order they describe them. Depending on the team, such as core flight software or test engineering, the practical stage is reported to lean toward C++ architecture or toward Python automation.

Recruiter Screen

Candidates describe the recruiter screen as a conversation to line up your background, your location preference (Burlington, VT or Pleasanton, CA come up) and your interest in aerospace. No technical questions are reported for it. Treat it as the point where your embedded experience gets summarised and routed, so give a precise summary rather than a general one: microcontroller families, RTOS, buses and the hardware you debugged.

What to demonstrate

  • Fit between your background and an embedded, hardware-facing role
  • Location preference
  • Interest in aerospace

How to prepare

  • Write a short summary of your embedded work: the MCU or board, whether it ran an RTOS or bare metal, which buses you used and what you owned.
  • Decide which hub you prefer and whether you can be in a hardware lab frequently, since candidates describe the role as tied to the labs.
  • Prepare a concrete reason for wanting spacecraft propulsion work, tied to something you have built or studied.

Technical Screening

Candidates report a discussion with a hiring manager or senior engineer about your past embedded projects, your microcontroller experience and high-level system design. Because it centres on your history, the winning move is depth: for each project know the processor, the peripherals, the failure modes you met and what you would change. Be ready to move from a project into fundamentals such as volatile, interrupts and memory use in code that runs for years.

What to demonstrate

  • Depth of your past embedded project work
  • Microcontroller experience
  • High-level system design reasoning

How to prepare

  • Write a one-page summary for your two strongest embedded projects: architecture, task and ISR layout, buses, a bug you fixed and a trade-off you made.
  • Rehearse aloud the reported questions on volatile, the interrupt path from hardware signal to ISR, and avoiding leaks in a system that never reboots.
  • Practise stating a design at block-diagram level: sensors in, controller, actuators out, and what happens on a fault.

Practical Technical Assessment

Candidates describe the practical technical assessment as realistic, domain-specific work instead of algorithm puzzles. Reported examples are designing a state machine for a thruster firing sequence, debugging a snippet of C that talks to a peripheral, and explaining how you would set up a hardware-in-the-loop test. Reported advice for coding here is to use static allocation rather than malloc or new. Work out loud: state assumptions, then the logic, then how you would test it.

What to demonstrate

  • Working embedded C and bit-level manipulation
  • State machine design with fault handling
  • Reading and debugging C that touches hardware

How to prepare

  • Practise bit macros and register functions (set, clear, toggle) and say what you assume about bit numbering and width.
  • Read short C snippets for common peripheral bugs: a missing volatile, a wrong mask, an unsafe read-modify-write shared with an ISR, an interrupt flag never cleared.
  • Draw a firing-sequence state machine with Idle, Armed, Firing and Safe states, listing abort conditions and the exit from each state.
  • Outline an HIL setup: what the simulator stands in for, what you inject and what your Python scripts record.

Onsite or Virtual Loop

The final stage is reported as an onsite or virtual loop of several sessions with a cross-functional panel: other software engineers plus electrical and mechatronics engineers. Candidates describe deep dives into past projects, behavioral questions about teamwork, and whiteboarding a solution for a hypothetical spacecraft subsystem. Expect to explain the same project to audiences with different backgrounds, and to move between code, schematics and system behavior in one conversation.

What to demonstrate

  • Depth on past projects across software and hardware
  • Teamwork and communication with other engineering disciplines
  • Architecture of a hypothetical spacecraft subsystem on a whiteboard

How to prepare

  • Write out five stories for the reported behavioral prompts: PCB bring-up, a design disagreement, a code bug that damaged hardware, testing against deadlines and your hardest system-level bug.
  • Practise the I2C intermittent-failure walkthrough and the pull-up sizing explanation, since both sit at the edge of electrical knowledge.
  • Rehearse a whiteboard routine: requirements and constraints, interfaces, states, fault cases, then test strategy.
  • Prepare an honest answer for a mechanical or electrical question outside your range, naming whom you would consult and what you would ask.

Where candidates lose points

Writing code before asking about the hardware

Before any C or any state machine, ask about the processor, the power budget, how the actuator is driven, and what the fail-safe state is. For the propulsion state machine, a single question on valve fail-safe position changes the whole design.

Using malloc, free or new in coding answers for a system that must run for years

Use static buffers and fixed-size pools in the ring buffer, queue and string functions. Candidates report that static allocation is the expected norm for flight-style code, and it also answers the memory-leak question.

Drawing only the happy path of the firing sequence

For every state, list the faults that can occur there, the abort condition and the transition out, including a stuck valve, a pressure reading out of range and a processor reset. Show how each path ends in a safe state.

Describing code without saying how it would be tested

After each answer, add how you would unit test it with mocked hardware and how a HIL run would exercise it, including injected sensor faults. Candidates are advised to raise testing unprompted.

Guessing on electrical or mechanical questions, or telling stories with no hardware evidence

Review voltage dividers, pull-ups, basic filtering and schematic reading, and say who you would consult when you reach your limit. In stories, include a measurement from a scope, analyzer or datasheet, not just a conclusion.

Questions & practice

How do you debounce a physical mechanical switch in software without blocking the main execution thread?

Medium · Hardware Interaction & Debugging

Approach

  1. Never debounce with a blocking delay. Sample the pin from a periodic timer tick or the scheduler tick (every few milliseconds) and change the reported state only after N consecutive samples agree.
  2. A compact version is a shift-register integrator: history = (history << 1) | raw; report pressed when history == 0xFF and released when it is 0x00. It takes O(1) work per tick per button and no blocking.
  3. An interrupt-driven alternative: on the first edge, disable that pin's interrupt, start a one-shot timer for the bounce window, then re-read the pin when it expires and re-enable. Taking an interrupt on every bounce edge can flood the CPU.
  4. Pick the window from a scope trace of the actual switch rather than a guess, and compare timestamps with unsigned subtraction ((now - last) >= window) so tick-counter wraparound is handled.
  5. Hand the result to the application as an event (a flag, queue or task notification) consumed by the main loop or a task, and mention hardware debouncing (RC filter plus Schmitt-trigger input) as the complement.

Follow-up

  • How do you scale this to sixteen buttons without sixteen timers?
  • How would you detect a long press and a double press with the same code?
  • Why is an interrupt on every edge a bad idea for a bouncing switch?

Diff a projection against the primary without per-row point reads

Hard · reconciliation · range hashing · throttling

The listing projection has drifted and some rows show a stale version. The primary holds 40,000,000 resource rows across 12,000 tenants while serving 1,200 writes and 14,000 reads per second. The obvious repair, reading each resource row and comparing its version against the projection, is correct and would eventually finish. Explain precisely why it is unacceptable here, then give a diff that finds the differing rows, state its complexity, and make it safe to run against a live primary. Replication lag is usually under 100 ms and is not bounded.

Approach

  1. Quantify the naive cost rather than calling it slow: 40,000,000 point reads at even 0.5 ms each is over five hours serialised, and the only lever is concurrency, which is exactly what you cannot spend. The primary's pool is sized for the write path, and 40,000,000 random reads evict the buffer cache that sustains the 85 percent cache hit rate, so the audit degrades the system it is auditing.
  2. Replace random access with one ordered pass per side. Both sides can be read in (tenant_id, resource_id) order, which is a sequential scan on each and a merge join in O(n) time and O(1) memory. For a dense diff that is the whole answer, and it reads the primary once instead of 40,000,000 times.
  3. For the expected sparse case, compare range hashes instead of rows: partition the key space, compute per range an order-independent aggregate over hash(resource_id, version), compare aggregates, and descend only into ranges that differ. With d differing rows and branching factor B, at most d ranges mismatch per level, so the drill-down examines O(d log_B(n/d)) ranges and reads full rows only in mismatching leaves.
  4. Aggregate with a sum modulo 2^64 or a multiset hash, never XOR. XOR is order-independent but self-cancelling, so two rows wrong in the same way, or a row duplicated on one side, leave the range aggregate matching and the range is declared clean.

Follow-up

  • The diff reports 900 stale rows. How do you decide between patching those rows and rebuilding the projection from resource_revision?
  • Same job, but the projection lives in a search index that cannot be scanned in key order. What changes?

Merge partitioned event streams into one ordered feed with bounded lateness

Hard · k-way merge · watermarks · out-of-order streams

The read-model service consumes 64 log partitions carrying about 4,000 events per second in total. Each partition is ordered within itself, but partitions drift by up to 30 seconds, and the activity feed must present a tenant's events in occurred_at order. Produce the merge. State its complexity, the buffer it requires in events and in bytes, what happens when one partition is idle, and what you do with an event that arrives after you have already emitted its position. Payloads average 1 KB.

Approach

  1. Merge with a min-heap over the 64 partition heads keyed on (occurred_at, event_id): O(log P) per event and O(n log P) overall. The tie-break on event_id is what makes the output deterministic when two partitions carry the same millisecond, which matters because the feed is paginated and a non-deterministic order reorders pages under the reader.
  2. Emitting the heap head is only correct once every partition has produced everything up to that timestamp, so the emit condition is a watermark: the minimum across partitions of the highest occurred_at seen, less the allowed lateness. Events are held until the watermark passes them, which is what turns individually ordered streams into a jointly ordered one.
  3. Size the buffer from the lateness rather than guessing: 4,000 events per second times 30 seconds is 120,000 buffered events, and at 1 KB each about 120 MB of heap. That number is the real price of the ordering guarantee and belongs in front of whoever asked for it.
  4. Handle the idle partition explicitly, because it fails the feed rather than corrupting it: a partition with no traffic never advances its own maximum, so the watermark freezes and output stops entirely. Either every partition emits a periodic idle marker carrying the broker's current time, or the watermark falls back to wall clock for a partition silent beyond a threshold.

Follow-up

  • The lateness budget is raised to five minutes. What is the new buffer, and what besides memory changes?
  • The consumer restarts. Where does it resume from, and what does the feed look like for the first 30 seconds?

Write the update path that detects a concurrent edit

Medium · optimistic concurrency · transactional outbox · audit history

resource carries version INT NOT NULL DEFAULT 1. resource_revision holds revision_id, resource_id, version, actor_user_id, change_kind, patch JSONB, request_id, created_at with UNIQUE (resource_id, version). outbox_event holds aggregate_type, aggregate_id, aggregate_version, event_type, payload, status. A PUT carries the version the client read. Write the exact statements for the single transaction that applies the edit, records the revision and enqueues 'resource.updated', and give the handler's branch on zero affected rows. Then say what PostgreSQL 16 does under READ COMMITTED when two of these updates hit one row at once.

Approach

  1. One transaction, three writes, no network call inside it: UPDATE resource SET title = $3, version = version + 1, updated_at = now() WHERE resource_id = $1 AND tenant_id = $4 AND version = $2; then INSERT the resource_revision row at version $2 + 1; then INSERT the outbox_event row at the same aggregate_version. The event goes to a table rather than a broker because no transaction spans both.
  2. Branch on the affected-row count before doing anything else. Zero has three causes — stale version, wrong tenant, row gone — so re-read once and map to 409 carrying the current version, or 404 for an id outside the caller's tenant, which also stops the endpoint confirming that another tenant's id exists.
  3. State the engine behaviour instead of assuming it. Under READ COMMITTED the second UPDATE blocks on the row lock, and when the first commits PostgreSQL re-evaluates the WHERE clause against the newly committed row, so the version predicate now fails and the statement reports zero rows. Under REPEATABLE READ the identical collision raises SQLSTATE 40001 instead, so the handler must fold both shapes into one conflict response.
  4. Keep UNIQUE (resource_id, version) even though the predicate already serialises writers. It is what makes a lost update unwritable if any other path ever reaches the revision table, and it converts a logic bug into 23505 rather than into a silently missing history row.

Follow-up

  • A client sends the version it read ten minutes ago and the resource has moved three versions. What is in your 409 so it can resolve the conflict without a full re-fetch?
  • Two editors, two disjoint fields, no overlap. Does your answer still refuse the second write, and should it?

Denormalise tenant onto revisions and backfill it live

Hard · denormalisation · online migration · backfill

resource_revision (revision_id, resource_id, version, actor_user_id, change_kind, patch, request_id, created_at) has 400M rows and no tenant column; tenant_id lives only on resource. Two reads need it: a tenant-scoped audit feed ordered by created_at DESC, and an offboarding purge. Both join back to resource today. Justify adding tenant_id to resource_revision against those two reads, name the anomaly the copy introduces and the constraint that prevents it, then give the ordered migration for a live table taking 1.2k writes/second — the lock each step takes, how the backfill is batched, and where each step stops being reversible. PostgreSQL 16.

Approach

  1. Justify from the access path rather than from taste. Without the column, the audit feed either scans resource_revision by created_at and discards other tenants' rows, or resolves the tenant's resource_ids first and probes with them — both proportional to the tenant's whole history rather than to one page. With (tenant_id, created_at DESC, revision_id DESC) it is a seek that stops at 50 rows, and the purge becomes a ranged delete instead of a join.
  2. Name the cost exactly: a second copy of a fact can disagree with the first. Make the disagreement unwritable rather than documented — add UNIQUE (resource_id, tenant_id) on resource so it can serve as a foreign-key target, then FOREIGN KEY (resource_id, tenant_id) REFERENCES resource (resource_id, tenant_id) on the revision table. A revision can then only ever carry its parent's tenant.
  3. Step one, expand: ALTER TABLE resource_revision ADD COLUMN tenant_id BIGINT NULL, with no default, so it is a catalogue change and no rewrite. It still needs ACCESS EXCLUSIVE for an instant, and that instant queues behind the longest open transaction on the table while every later query queues behind it — set lock_timeout to 2s and retry rather than wait.
  4. Step two, dual-write: deploy the writer that populates tenant_id on every new revision while reads still use the join. Reversible by redeploying the previous build, because nothing reads the column yet.

Follow-up

  • The backfill is half finished and a rollback is required. What state is the table in, and what does the previous build do with a half-populated column?
  • How do you verify the backfill actually finished, given rows are still being inserted while it runs?

Write a function to reverse a string in place without using any standard library functions.

Medium · Embedded C and Architecture

Approach

  1. Find the length by walking to the '\0' terminator yourself (no strlen), then swap characters with two indices: i from 0 and j from len-1, moving inward while i < j. That is O(n) time and O(1) extra space.
  2. Swap through a temporary char, not the XOR trick. XOR swap zeroes the value when both operands are the same location, and on modern compilers it is no faster.
  3. Cover the edge cases out loud: a NULL pointer (return early), an empty string (len 0, so j must not underflow; use a signed index or check len first), a single character, and odd lengths where the middle character stays put.
  4. Point out that the caller must pass writable memory. Modifying a string literal (char *s = "abc") is undefined behaviour and on many MCUs it lives in flash, so pass a char array instead.
  5. Say what 'reverse' means for multi-byte UTF-8 text: a byte-wise reverse corrupts multi-byte sequences, so ask whether the input is plain ASCII.

Follow-up

  • How would you reverse the order of words in a sentence in place, still with no library calls?
  • Would you write this recursively on a microcontroller? What does recursion cost in stack?
  • How would you unit test this function, and which inputs would you pick?

Explain the volatile keyword in C. When and why must you use it when interacting with hardware registers?

Medium · Embedded C and Architecture

Approach

  1. Define it precisely: volatile tells the compiler that every read and write of the object is an observable side effect, so it may not cache the value in a register, drop a 'redundant' access, or merge accesses, and it keeps volatile accesses in program order relative to each other.
  2. Give the canonical bug: polling a status register in while (!(STATUS & READY)); without volatile, the optimiser can hoist the single read out of the loop and spin forever. Registers are declared as #define REG (*(volatile uint32_t *)0x40021000u).
  3. List the three standard uses: memory-mapped peripheral registers, variables shared between an ISR and the main loop or a task, and local variables modified between setjmp and longjmp.
  4. State what volatile does NOT give you: atomicity (a 64-bit counter on a 32-bit core can tear), mutual exclusion for read-modify-write, or ordering against non-volatile accesses and CPU or bus reordering. For those you need critical sections, atomics or barriers such as DMB on ARM.
  5. Distinguish volatile uint32_t *p (pointer to a volatile register) from uint32_t * volatile p (the pointer itself is volatile), and explain const volatile for a read-only status register.

Follow-up

  • Is volatile enough to share a flag between two RTOS tasks? Why or why not?
  • An ISR increments a volatile uint32_t and main reads and clears it. Where is the race?
  • What does const volatile mean, and where would you use it?

Describe how an interrupt is handled by a processor from the moment the hardware signal is asserted to the exe

Medium · Embedded C and Architecture

Describe how an interrupt is handled by a processor from the moment the hardware signal is asserted to the execution of the ISR.

Approach

  1. Walk the chain: the peripheral asserts its IRQ line, and the interrupt controller (the NVIC on ARM Cortex-M) latches the pending bit. The interrupt is taken only if it is enabled, not masked (PRIMASK/BASEPRI), and of higher priority than the code currently running.
  2. Describe the Cortex-M entry in hardware: the core stacks R0-R3, R12, LR, PC and xPSR (plus FP registers if FP context is active), fetches the handler address from the vector table at VTOR, loads LR with an EXC_RETURN value and switches to Handler mode on the main stack.
  3. Inside the ISR: clear the peripheral's interrupt flag, or the interrupt re-pends and fires again on exit. Do the minimum, then hand the work to a task (a FreeRTOS ...FromISR call or a task notification), and never block.
  4. Exit: loading EXC_RETURN into PC unstacks the context. Mention tail-chaining (a pending interrupt runs next without unstacking and restacking) and late arrival (a higher-priority interrupt takes over during stacking).
  5. Name what adds latency: a long critical section with interrupts disabled, flash wait states, a higher-priority ISR running, and edge-triggered versus level-triggered behaviour if a flag is missed.

Follow-up

  • What should and should not happen inside an ISR in an RTOS application?
  • What happens if you forget to clear the peripheral's interrupt flag?
  • How would you measure worst-case interrupt latency on real hardware?

Implement a macro to clear the 5th bit of a 32-bit integer.

Medium · Embedded C and Architecture

Approach

  1. Settle the convention first: '5th bit' is bit index 4 if you count from bit 0, or bit 5 if the asker numbers from zero. Say which you chose, and write a general macro so the choice is a single argument.
  2. Write it with an unsigned mask and every argument in parentheses: #define BIT_CLR(x, n) ((x) &= ~(UINT32_C(1) << (n))), so BIT_CLR(reg, 4) clears bit 4. A signed 1 << 31 is undefined behaviour, which is why the constant is unsigned.
  3. Show the matching set, toggle and test forms (|= mask, ^= mask, ((x) >> (n)) & 1U), and offer a static inline function as the type-checked alternative to a macro.
  4. Flag the hardware traps: on a register, the read-modify-write is not atomic against an ISR touching the same register, and on a write-1-to-clear status register, x &= ~mask writes back 1s and clears every other pending flag. Use dedicated set/clear registers (such as STM32 GPIO BSRR), bit-banding where the core has it, or a critical section.

Follow-up

  • Write macros to set, clear and toggle a field several bits wide, for example bits 7:4.
  • An ISR also writes this register. How do you make the clear safe?
  • Why is this macro wrong for a write-1-to-clear interrupt status register?

Design a state machine for a satellite propulsion system that must heat a catalyst bed, open a valve, and moni

Medium · System Design & Fault Tolerance

Design a state machine for a satellite propulsion system that must heat a catalyst bed, open a valve, and monitor pressure.

Approach

  1. List the states and make the safe one the default: SAFE/IDLE (heater off, valve closed), ARMED, PREHEAT (catalyst-bed heater on), READY, FIRING (valve open), SHUTDOWN, and FAULT, which every state can reach.
  2. Guard every transition: PREHEAT to READY only when bed temperature passes a threshold before a heat-up timeout; READY to FIRING only on an explicit fire command after arming; FIRING ends on commanded duration, a maximum burn-time limit, or an abort.
  3. Spell out the abort conditions: chamber or feed pressure out of bounds, temperature sensor out of range or two sensors disagreeing, a heater timeout, valve position feedback not matching the command, or a stale command or sensor reading. Each sends the machine to a state that closes the valve and turns off the heater.
  4. Implement it table-driven ((state, event) -> guard, action, next state) with entry and exit actions, run it in one fixed-rate task that samples all inputs once per tick, and boot into SAFE after any reset rather than resuming a firing.
  5. Test it by enumerating every state and event pair in unit tests with mocked sensors and actuators, then repeat the abort cases on a HIL rig with injected faults.

Follow-up

  • The pressure reading goes out of range halfway through a burn. Walk through what happens.
  • The processor resets during FIRING. What state does it come back in, and why?
  • How would you prove that every transition has been tested?

How would you design a software update mechanism (bootloader) for a spacecraft that is already in orbit?

Medium · System Design & Fault Tolerance

Approach

  1. Keep a small immutable first-stage bootloader in write-protected flash or ROM that is never updated in flight, plus two application slots (A/B): the running image and a staging slot for the upload.
  2. Upload in numbered chunks with a CRC per chunk, so a transfer can resume across contact windows and only missing chunks are resent. Then verify the whole image with a hash and a cryptographic signature before marking it bootable.
  3. Use a trial boot: switch to the new slot with a boot-attempt counter. The new image confirms itself only after its self-tests pass. If it crashes, watchdogs or fails to confirm within N attempts, the bootloader reverts to the last known-good slot.
  4. Make every step survive power loss: the running slot is never touched while staging, and the slot-selection metadata is written as a double-buffered record with a CRC and a sequence number, so a half-written record is detected and ignored.
  5. Name the trade-offs and extras: two slots double the flash cost (delta updates save uplink time but add complexity), version and compatibility checks against configuration data, and ECC or periodic CRC scrubbing of stored images against radiation upsets.

Follow-up

  • Power is lost exactly while the slot pointer is being written. What boots next?
  • How would you update the first-stage bootloader itself, if ever?
  • How would you test the rollback path on the ground?

What is a watchdog timer, and how do you determine the appropriate timeout period for a specific application?

Medium · System Design & Fault Tolerance

Approach

  1. Define it: an independent hardware counter that resets the processor unless software refreshes it before it expires. An independent watchdog runs from its own clock; a window watchdog also resets if refreshed too early, which catches a runaway loop that refreshes too often.
  2. Set the timeout from both ends: longer than the worst-case legitimate gap between refreshes (measure it, including slow paths such as a flash sector erase) plus margin, and shorter than the time the hardware can safely be left uncontrolled, such as an open valve or a heater left on.
  3. In an RTOS, never refresh from a timer ISR or the idle task alone, since that keeps refreshing while a task is deadlocked. A monitor task refreshes only when every registered task has checked in within its own deadline.
  4. After a reset, read the reset-cause register, log and count watchdog resets in telemetry or persistent memory, and switch to a safe or degraded mode after repeated resets instead of looping.
  5. Note the practical details: freeze the watchdog during a debugger halt if the part allows it, and remember that on many parts it cannot be disabled once started.

Follow-up

  • A flash erase takes longer than your watchdog timeout. How do you handle that?
  • How do you implement the watchdog check-in for five tasks running at different rates?
  • What would you record so the ground team can diagnose a watchdog reset?

If your system experiences a sudden dip in power (brownout), how should your software handle the recovery?

Medium · System Design & Fault Tolerance

Approach

  1. Rely on hardware first: enable the brownout reset so the core is held in reset below a safe voltage instead of running with corrupted flash reads or registers. Where the part has one, use an early-warning voltage detector interrupt to put actuators in a safe state and stop flash writes.
  2. On restart, read the reset-cause register to tell power-on, brownout and watchdog resets apart, boot with every actuator in its safe state, and never resume an operation from leftover RAM state.
  3. Validate everything persisted: keep non-volatile state in double-buffered records with CRC and sequence numbers so a write interrupted by the dip is detected and the previous good copy is used.
  4. Re-initialise external devices explicitly. Sensors on another rail may not have reset while the MCU did, so an I2C slave can be stuck mid-transaction or a sensor left in an unexpected mode; run bus recovery and re-read identity and configuration.
  5. Stop a reset loop: bring loads back in stages so heaters do not cause the next dip, count brownouts in telemetry, and enter a safe mode after repeated events. Test on the bench with a programmable supply that sweeps and dips the rail.

Follow-up

  • A brownout hits in the middle of a flash write. What does the next boot see, and how do you recover?
  • Why not just let the watchdog handle brownouts?
  • How would you reproduce a brownout reliably on a HIL rig?

How do you prevent memory leaks in an embedded system that must run continuously for years?

Medium · Embedded C and Architecture

Approach

  1. Remove the cause: allocate everything statically or once at initialisation and never free afterwards. MISRA C:2012 Rule 21.3 bans the stdlib malloc/free for this reason, and a FreeRTOS heap_1 configuration never frees at all.
  2. When objects really come and go (messages, buffers), use fixed-size block pools with O(1) allocate and free. They cannot fragment, and a pool that runs empty is a bounded, detectable error rather than a slow leak.
  3. Separate a leak from fragmentation: free memory can stay roughly constant while the largest free block shrinks until an allocation fails. Pools and init-only allocation avoid both problems.
  4. Instrument for the leaks that remain: log minimum-ever free heap (xPortGetMinimumEverFreeHeapSize in FreeRTOS), per-task stack high-water marks and pool low-water counts in telemetry, and fill stacks with a canary pattern.
  5. Count other resources as well: semaphores, queue slots, file handles and DMA descriptors leak in the same way. Run a long soak test on the HIL rig with accelerated cycling and watch every counter trend.

Follow-up

  • How would you size a block pool, and what should the code do when it runs out?
  • Free heap is flat but an allocation fails after six months. What is going on?
  • Which numbers would you put in telemetry to spot a slow leak on a unit already in orbit?

You are writing a driver for a new SPI sensor. What parameters do you need to know from the datasheet before y

Medium · Hardware Interaction & Debugging

You are writing a driver for a new SPI sensor. What parameters do you need to know from the datasheet before you write the first line of code?

Approach

  1. Bus timing: SPI mode (CPOL and CPHA), maximum SCLK frequency, bit order (MSB or LSB first), word length (8 or 16 bits), and whether the sensor talks full duplex or needs dummy bytes clocked out to read.
  2. Chip select: polarity, setup and hold times around the first and last clock edges, the minimum CS-high time between transactions, and whether CS must stay low for a whole multi-byte transfer. That last one decides whether hardware NSS management works.
  3. Protocol and register map: the command format (for example the read/write bit in the address byte), auto-increment for burst reads, the identity register to read back at start-up as a first sanity check, and the default register values.
  4. Electrical and timing: I/O voltage level against the MCU, power-up and reset sequence, start-up time before the first valid read, conversion time, and whether a data-ready pin exists, which decides whether you poll or interrupt.
  5. Data meaning: output format (two's complement, endianness, resolution), scale factor and offset to convert counts into physical units, and the fault or status bits the driver should surface.

Follow-up

  • Every read returns 0xFF or 0x00. What do you check first?
  • Two devices with different SPI modes share one bus. How does the driver handle that?
  • When would you move this driver from polling to DMA?

Walk me through how you would use a logic analyzer to debug an I2C bus that is intermittently failing.

Medium · Hardware Interaction & Debugging

Approach

  1. Capture SDA, SCL and a spare GPIO that firmware toggles at the start of each transaction and on errors, so bus traffic lines up with code events. Sample several times faster than the bus clock and turn on the I2C protocol decoder.
  2. For an intermittent fault, trigger instead of watching: trigger on a NACK, on SCL held low longer than any legal clock stretch, or on the error GPIO, with a deep pre-trigger buffer so you see the transactions leading up to the failure.
  3. Read the decoded failure: an address NACK points to the wrong address or a busy or unpowered device; SDA held low with no clock means a slave was left mid-byte (often after a master reset); a long SCL low means clock stretching the master does not support.
  4. Know the tool's limit: a logic analyzer shows thresholded 1s and 0s, so for slow rise times from weak pull-ups or high capacitance, ringing, or noise coupled in when a valve or heater switches, move to an oscilloscope.
  5. Close with recovery in software: driver timeouts on every wait, bus recovery by clocking SCL up to nine times with SDA released and then sending a STOP, then re-initialising the peripheral and counting the events in telemetry.

Follow-up

  • The failures line up with an actuator switching on. What do you measure next?
  • How exactly do you recover a bus where a slave holds SDA low?
  • How would you tell a firmware timing bug from an electrical problem from these captures?

Explain the purpose of a pull-up resistor. What happens if it is sized incorrectly?

Medium · Hardware Interaction & Debugging

Approach

  1. Purpose: a pull-up gives a line a defined high level when nothing drives it low. Open-drain buses such as I2C need one because devices can only pull low; it also keeps button inputs, reset and chip-enable lines from floating.
  2. Too large a value: the rise time is set by R times the bus capacitance (the I2C specification gives tr = 0.8473 x Rp x Cb from 30% to 70%), so edges get slow, can break the rise-time limit (1000 ns in standard mode, 300 ns in fast mode) and pick up more noise.
  3. Too small a value: when a device pulls the line low, current is (VDD - VOL)/Rp. If that exceeds what the driver can sink (3 mA at VOL 0.4 V for standard and fast-mode I2C), the low level is not low enough and static power rises.
  4. Work the bounds as numbers: Rp(min) = (VDD - VOL(max)) / IOL, which at 3.3 V gives about 967 ohms, and Rp(max) = tr / (0.8473 x Cb). Pick a value between them.
  5. Note that internal MCU pull-ups are weak (high resistance) and usually too weak for fast I2C over a real harness, and that pull-up versus pull-down on an actuator enable line decides its state while the MCU is in reset.

Follow-up

  • Pick a pull-up for 400 kHz I2C at 3.3 V with 200 pF of bus capacitance.
  • How would you see on a scope that the pull-up is too weak?
  • Should a valve-driver enable line be pulled up or down, and why?

Suggested practice checklist

The plan follows the four reported rounds and the 18 reported questions in their four groups. It spends the early days on embedded C and buses, then fault-tolerant design, then stories and a combined mock.

Day 1 — Map the loop and inventory your projects

  • Write out the four reported rounds and list what each covers, marking the one you are least ready for.
  • List your three strongest embedded projects with the MCU, RTOS or bare-metal setup, buses used, your role, a bug fixed and a trade-off made.
  • Pick the project you would lead with in a technical screening and write its architecture on one page.

Deliverable: A loop map with the weakest round marked, and a project inventory with one page on the lead project.

Day 2 — Embedded C: bits, volatile and interrupts

  • Write the in-place string reversal and the bit-clear macro with no library calls, stating your bit-numbering assumption, then add set and toggle versions.
  • Explain volatile aloud with a register polling loop and an ISR-shared flag as examples.
  • Trace an interrupt from the hardware signal through to the ISR and back, naming each step.

Deliverable: A tested C file with the string reversal and the bit macros, plus a spoken answer each on volatile and the interrupt path.

Day 3 — Memory, RTOS and recovery

  • Write a thread-safe ring buffer for telemetry with static storage only, and note where a mutex, a critical section or a lock-free design fits.
  • Write an answer on avoiding leaks in a system that runs for years, covering static allocation, fixed pools and fragmentation.
  • Work out a watchdog timeout from a task's worst-case period, then write how the system should restart cleanly after a brownout.

Deliverable: A ring buffer implementation plus a written answer each on long-run memory use, watchdog timeout choice and brownout recovery.

Day 4 — Buses, sensors and debugging

  • Take a real sensor datasheet and write the checklist of parameters you need before coding an SPI driver, such as mode, clock limit, bit order and register map.
  • Write the logic analyzer procedure for an intermittent I2C failure: trigger setup, what to decode, and what to compare between a good and a bad transaction.
  • Calculate pull-up values for a bus from a given capacitance and rise time, and describe what too large or too small a value does.
  • Write a non-blocking switch debounce using a sampled state machine.

Deliverable: A datasheet checklist, an I2C debugging walkthrough, a pull-up calculation and a debounce routine.

Day 5 — Fault-tolerant design on the whiteboard

  • Draw the propulsion state machine for heating a catalyst bed, opening a valve and monitoring pressure, with a transition table, abort conditions and a safe state.
  • Outline an in-orbit update mechanism: image storage, integrity checks, a way to fall back, and what the running system keeps if an update fails.
  • Write how you would test the state machine in unit tests and in an HIL setup, including which faults you would inject.

Deliverable: A state diagram with transition table, a one-page update design and a test plan for the state machine.

Day 6 — Write the five behavioral stories

  • Write one story each for the PCB bring-up, design disagreement, hardware-damaging bug, testing versus deadlines and hardest system-level bug.
  • For each story, add a concrete measurement or piece of evidence and the lasting change in how you work.
  • Read each aloud and cut it until the decision and the reasoning take most of the time.

Deliverable: Five story outlines, each with situation, action, evidence and lesson.

Day 7 — Mixed mock and cross-discipline practice

  • Whiteboard a hypothetical subsystem from requirements through states, interfaces and faults, then add the test strategy.
  • Answer aloud the hardest-system-bug story, then the I2C intermittent failure walkthrough, back to back.
  • Write an answer for a deep electrical or mechanical question you cannot fully answer, saying whom you would consult.

Deliverable: A whiteboard photo of the subsystem design and two recorded spoken answers.

Stories for the cross-functional panel

Candidates report behavioral questions centred on working with electrical, mechanical and mechatronics engineers, plus hardware failures, test rigor against schedule pressure and hard system-level bugs. Choose stories where software met physical behavior and bring measurements: a scope capture, a datasheet limit, a soak run. In each story, state your own decision and what you now do differently.

  1. Tell me about a time you had to explain a software constraint to an engineer from another discipline.

Describe your experience bringing up a brand new, custom PCB. What are your first steps?

Medium · Hardware Interaction & Debugging

Approach

  1. Open with the order of operations, which is what the question asks for. Before power, inspect for solder bridges and measure resistance from each supply rail to ground to catch shorts. Then power from a current-limited bench supply, raise the voltage gradually and watch the current draw.
  2. Verify each rail with a scope for voltage and ripple, then confirm that reset and the clock source behave, such as a crystal that actually oscillates. Next connect the debugger over JTAG or SWD and read the device ID, because a readable ID shows the core is alive.
  3. Load the smallest possible firmware: toggle a GPIO or LED, then bring up a UART console. Add peripherals one at a time (memories, SPI and I2C sensors, the CAN transceiver), checking each against the schematic and with a logic analyzer.
  4. Close with the collaboration: log every deviation from the schematic, hand findings to the electrical engineer, and say what you asked for on the next revision, such as test points or a debug header. If you have not done a full bring-up, say so and describe the nearest real experience.

Follow-up

  • The debugger cannot connect to the core. What do you check, in order?
  • The board draws more current than expected at power-up. How do you find the cause without damaging it?
  • What would you ask the board designer to add so the next revision is easier to bring up?

Tell me about a time you disagreed with an electrical or mechanical engineer about a system design. How did yo

Medium · Behavioral & Cross-Functional Collaborat

Tell me about a time you disagreed with an electrical or mechanical engineer about a system design. How did you resolve it?

Approach

  1. Choose a real design disagreement where each side had a legitimate constraint, for example a pin assignment, a sensor sampling rate, a valve drive timing or where a protection function should live. Say in two sentences what each engineer needed and why it conflicted with your view.
  2. Show how you moved the conversation from opinion to evidence: a datasheet limit, a scope capture, a bench test, a timing calculation or a small prototype. This part of the story carries the answer, so give the actual numbers or measurements you used.
  3. State the outcome plainly, including any compromise or any point where you changed your mind, and how you recorded the decision so it did not reopen. Name your own contribution, not the team's.
  4. Avoid presenting the other engineer as simply wrong, and avoid a story with no resolution. A strong version ends with a better design and a working relationship intact.

Follow-up

  • What if the electrical engineer had been right? How would you have found out?
  • How did you explain the software constraint to someone who does not write code?
  • What did you change in how you review interface decisions afterward?

Describe a situation where a bug in your code caused a physical hardware failure. What did you learn?

Medium · Behavioral & Cross-Functional Collaborat

Approach

  1. Pick a story that is true and specific. Hardware that was damaged or put at risk counts: a driver stage that overheated, an actuator left energised, a motor stalled or a board that browned out. If you have no actual damage, use the closest near miss and say so honestly.
  2. Structure it as what happened, what you did first, and the root cause. First actions are making the system safe, preserving evidence and telling the hardware owner immediately. A typical root cause is an output with no safe default at reset, a missing limit check, or a timing assumption about a valve or heater.
  3. Show the fix at two levels: the code change, and the guard that makes the class of bug harder to repeat. Examples are a safe-state output configuration at boot, a software interlock, a watchdog behavior, a review checklist item, or a HIL test that replays the failing sequence.
  4. Do not blame the hardware or shrink the incident. The lesson should be concrete and visible in how you work now, not a general statement about being careful.

Follow-up

  • Which test would have caught this before it reached real hardware?
  • How quickly did you tell the people who owned the hardware, and what did you give them?
  • Which of your habits changed as a result?

How do you balance the need for rigorous, aerospace-grade testing with aggressive project deadlines?

Medium · Behavioral & Cross-Functional Collaborat

Approach

  1. Answer with a risk-based method rather than a slogan. Classify the code by consequence: valve control, abort logic, fault detection and recovery, and anything that drives actuators gets full verification. Convenience tooling and logging can carry lighter checks.
  2. Cut scope before you cut verification. When a deadline is tight, propose dropping or deferring features, not the tests on the safety-relevant paths, and make the trade-off explicit to the decision maker with the residual risk written down.
  3. Reduce the cost of rigor so it fits the schedule: unit tests with mocked hardware interfaces, automated HIL runs triggered by a CI job, and Python scripts that replay known fault cases so regression checks run without manual effort.
  4. Back this with a short real example. Describe a time a schedule forced a choice, what you deferred, what you refused to skip and how it turned out. Saying you never compromise, or that you ship and patch later, both fail as answers.

Follow-up

  • Tell me about a time the deadline won. What did you skip, and what did you do about the risk?
  • How do you decide which tests are mandatory for a given change?
  • What would you do if a manager asked you to skip a HIL run before a release?

Tell me about the most complex system-level bug you have ever tracked down.

Medium · Behavioral & Cross-Functional Collaborat

Approach

  1. Choose a bug that crossed a boundary: firmware and hardware, software and timing, or two tasks sharing state. A single-file logic error does not show system-level skill. Open with the symptom and why it resisted the obvious explanations, such as being intermittent, load-dependent or visible only on one board.
  2. Walk through your method in order: how you made it reproducible, what you instrumented (timestamped logs, GPIO toggles captured on a logic analyzer, a scope on a rail or signal, a debugger trace), and how you split the system to eliminate hypotheses, for example swapping a board or removing a task.
  3. State the root cause precisely and which layers interacted. An example is an ISR racing a task over shared data, a bus line with a weak pull-up, or a brownout resetting the part mid-write. Then give the fix and the evidence that it worked, such as an overnight soak run with no recurrence.
  4. End with prevention: the test, assertion, timing margin or monitoring that now exists. Spend most of your time on the investigation, not the cleanup.

Follow-up

  • How did you decide whether the fault was in hardware or in software?
  • How did you reproduce something that only failed occasionally?
  • What would you instrument from day one if you were building that system again?