Interview guides · Ambimat Electronics
Ambimat Electronics Software Engineer Interview Guide
Ambimat Electronics Software Engineer interview guide: 4 reported rounds, embedded C and C++ questions, ISR and I2C/SPI debugging, and a 7-day plan.
The 60-second brief
Ambimat Electronics is described by candidates as an electronics company whose Software Engineer role sits between application-level logic and low-level hardware. The reported work is writing C and C++ for embedded systems and firmware, optimizing existing C++ code, and making software line up with the hardware it runs on. The reported location for the role is Gujarat. In practice that means working with hardware engineers, reading schematics and datasheets, and testing on real boards rather than only on a host machine.
This guide is for a candidate applying as a Software Engineer at Ambimat Electronics, with embedded C and C++ as the centre of gravity. It covers the four reported rounds, the firmware, C++ and problem-solving questions candidates report, how to prepare for each stage, and a seven-day schedule. It makes no claims about pay, internal teams or projects beyond what candidates report.
Candidates report four rounds over roughly 3-5 weeks: Initial Screening, Hands-on Technical Discussions, Team Interaction and Final Technical Assessments. The sequence runs from a background check to practical technical discussion, then conversations with the engineering team, then a closing technical evaluation.
Focus areas
- C++
- Embedded Software Development
- Embedded Hardware Engineering
Overview
The reported requirements read like an embedded firmware job description. Must-have items are C and C++, microcontroller architectures, the I2C, SPI and UART protocols, and the ability to read hardware schematics and datasheets. RTOS experience, Git and exposure to PCB design or hardware test equipment are listed as nice-to-haves. Prepare as someone who has to make a physical board behave, not only as someone who solves algorithm puzzles.
The reported questions fall into three groups. Embedded systems and firmware covers volatile versus non-volatile memory, interrupt service routines, debugging I2C or SPI on a target, resource-constrained code and power consumption. C++ and programming foundations covers pointers and memory management, stack versus heap, concurrency in firmware, classes and inheritance, and portability across architectures. Problem-solving and logic covers optimizing slow code, designing under ambiguous requirements and isolating a hard bug. Candidates also report example scenarios such as designing a driver for a new sensor, handling a race condition in firmware, avoiding leaks in a long-running device, and weighing templates against inheritance.
Candidates also describe tooling in the conversation: logic analyzers, oscilloscopes and debuggers for diagnosing software and hardware failures, and trade-offs in memory footprint, speed and power. Have a concrete method for each tool, not just its name. Candidates are also advised to be ready to go through every project on their resume in detail, and that practical embedded C++ matters more than theoretical algorithms. Keep enough coding fluency to write or debug a short routine on request, since candidates report assessments that involve writing or debugging code.
The Ambimat Electronics interview process
Candidates describe four stages over roughly 3-5 weeks. The descriptions below combine what is reported for each stage with preparation tied to the questions and stack candidates report.
Initial Screening
Candidates describe this stage as a screening of your technical background to judge fit for the role. Treat it as the point where your resume has to line up with the reported requirements: C and C++, microcontrollers, I2C, SPI and UART, and reading datasheets. Be ready to name the boards, peripherals and tools in each project and say which part you personally built.
What to demonstrate
- Whether your technical background matches the reported requirements for an embedded Software Engineer
- How clearly you describe your own contribution to past projects
How to prepare
- Build a one-page project inventory: microcontroller, peripherals and protocols used, language, tools, what you owned and one problem you solved per project.
- Be ready to discuss every project on your resume in detail, since candidates are advised to expect that.
- Refresh basics you may have used only through vendor libraries: how a timer is configured, how a GPIO is set up and what a UART frame contains.
Hands-on Technical Discussions
Candidates describe detailed discussions focused on practical engineering ability and problem-solving. The reported embedded topics, such as register-level programming, peripheral configuration, memory in constrained devices, ISRs, protocol debugging and power, are the natural material for this format. Answer by stating the mechanism first, then the trade-off, then how you would verify it on hardware.
What to demonstrate
- Practical engineering ability and problem-solving, as the stage is described
- Working knowledge of embedded firmware: peripherals, memory and interrupts
- How clearly you explain your reasoning aloud
How to prepare
- Practise the firmware questions aloud: volatile versus non-volatile memory, ISR handling, debugging an I2C or SPI link, and power optimization.
- Outline a driver for a new sensor end to end: read the datasheet, configure the bus, define register access, add error handling and test.
- Prepare a firmware race condition example, covering the shared data, the window where it breaks and the fix.
Team Interaction
Candidates describe interaction with several members of the engineering team, aimed at understanding culture and project goals. The reported role involves close work with hardware engineers and explaining technical issues to both technical and non-technical stakeholders, so expect collaboration and communication to matter alongside technical talk.
What to demonstrate
- How you collaborate with hardware engineers and other software engineers
- How you explain technical problems to different audiences
- Whether your interests fit the project goals discussed
How to prepare
- Prepare two collaboration stories with hardware people, such as a schematic or datasheet mismatch you found, and how you raised it.
- Practise explaining one bug to a non-technical listener in a few sentences, then to an engineer in full detail.
- Write questions about the team's projects, testing setup and how firmware and hardware changes are coordinated.
Final Technical Assessments
Candidates describe a closing evaluation of technical skills and project experiences, and report assessments that involve writing or debugging code. The C++ and programming-foundations questions are the natural preparation here: pointers and ownership, stack versus heap, concurrency in firmware, classes and inheritance, and portability. Expect to defend design choices with numbers on memory, speed and power.
What to demonstrate
- Hands-on C and C++ ability, shown by writing or debugging code
- Depth in memory management and language features
- The technical reasoning behind decisions in your past projects
How to prepare
- Write by hand: a fixed-size ring buffer, a bit-field set/clear routine, and an RAII wrapper that avoids heap use.
- Review smart pointers, templates versus inheritance, and avoiding leaks in a device that runs for a long time.
- Practise reading a short buggy C++ snippet and naming the defect: dangling pointer, missing volatile, buffer overrun or double free.
Where candidates lose points
Treating volatile as the fix for every shared-variable problem between an ISR and main code
Say what volatile does: it stops the compiler caching a value. Then add what it does not do, which is atomic access to multi-word data or ordering across several variables. Be ready to show a ring buffer with a single writer per index, or a short critical section.
Answering the I2C or SPI debugging question with 'check the code' and no instrument sequence
Use a fixed order. Check wiring and pull-ups, then the bus on a logic analyzer or oscilloscope. For I2C check the address, ACK/NACK and clock stretching. For SPI check mode (CPOL/CPHA), clock speed and chip select. Only then read the driver code.
Answering C++ memory questions as if the target were a desktop, with new/delete, exceptions and containers used freely
Say what each choice costs on a small microcontroller: heap fragmentation, nondeterministic allocation time, code size. Offer static allocation, fixed-size pools, RAII wrappers and smart pointers with a defined owner where they fit.
Being unable to explain a project listed on your resume down to the board, bus, tool and your own contribution
Before the screening, write a project inventory covering the microcontroller, peripherals, protocols, toolchain, your role and one bug you fixed. Candidates are advised to expect detailed project discussion in the loop.
Giving an ambiguous-requirements or optimization answer as a list of virtues, with no sequence and no measurement
Name the questions you would ask first, the constraint that would change the design (memory, latency, power), and how you would measure before and after. For a slow routine, say what you profiled and what the number was.
Questions & practice
Explain the difference between volatile and non-volatile memory.
Medium · Embedded Systems & Firmware
Approach
- Define the split by what happens at power loss. Volatile memory (SRAM, DRAM, CPU registers) loses its contents. Non-volatile memory (flash, EEPROM, FRAM, ROM) keeps them. On a microcontroller, SRAM holds the stack, heap and variables, and on-chip flash holds code and constants.
- Link it to startup. The linker puts code and const data in flash. The initial values of .data are stored in flash and copied to RAM by the startup code, and .bss is zeroed. That is why an initialized global costs both flash and RAM.
- Cover the write behaviour that shapes firmware. Flash is erased in pages or sectors: erasing sets bits to 1, and programming can only clear bits. Erase and write are far slower than reads, and endurance is a finite, datasheet-rated number of erase cycles. EEPROM is usually byte-erasable, and FRAM writes fast with very high endurance.
- Explain how to store settings safely. Keep a working copy in RAM and write to flash only on change. Use two slots, each with a sequence number and a CRC, so a power cut mid-write leaves the previous valid copy. Spread frequently updated data across pages (wear levelling).
- Name the naming trap: the C/C++ volatile keyword is a different thing. It tells the compiler a value can change outside normal program flow (hardware registers, ISR-shared flags). Say which meaning you are answering and cover the other briefly.
Follow-up
- A counter must survive power loss and changes every second. How do you store it without wearing out the flash?
- What happens if power drops halfway through a flash page erase, and how does your boot code detect it?
- Why can code on some MCUs not keep running from a flash bank while that same bank is being programmed?
Explain the role of pointers and memory management in C++.
Medium · C++ & Programming Foundations
Approach
- Separate the two jobs a pointer does: referring to an object (non-owning) and owning it (responsible for freeing it). Modern C++ puts ownership in the type: std::unique_ptr for single, move-only ownership with no reference count; std::shared_ptr for shared ownership through a reference-counted control block; raw pointers or references for non-owning access.
- Present RAII as the default strategy: the destructor releases what the constructor acquired, so early returns and exceptions cannot leak. Prefer std::make_unique and std::make_shared. make_shared puts the object and its control block in one allocation.
- List the bugs manual management causes and how to catch them: leaks, double delete, dangling pointers after the owner is freed, delete vs delete[] mismatch, and use-after-move. On the host, AddressSanitizer and Valgrind find these. shared_ptr cycles leak unless one link is a std::weak_ptr.
- Bring it to embedded: pointers to volatile registers at fixed addresses, and pointer arithmetic over DMA buffers. Avoid runtime heap use by creating objects statically, from pools, or with placement new into a reserved static buffer, then call the destructor explicitly.
Follow-up
- How would you keep a long-running device from leaking memory over months of uptime?
- What does shared_ptr cost compared with unique_ptr, in memory and in time?
- When is a reference a better function parameter than a pointer?
How do you implement multithreading or handle concurrency in a firmware context?
Medium · C++ & Programming Foundations
Approach
- Name the concurrency model first: a bare-metal super-loop where ISRs preempt the main loop, or an RTOS with preemptive tasks plus ISRs. Then give every piece of shared state an owner and a rule for who may write it.
- ISR-to-main sharing: volatile only stops the compiler from caching a value. It does not make a read-modify-write atomic. Protect multi-step updates with a short critical section (mask interrupts or raise BASEPRI), or use std::atomic where the core supports it. Cortex-M0 (ARMv6-M) has no LDREX/STREX, so atomic RMW there means masking interrupts.
- Prefer designs that need no locks. In a single-producer, single-consumer ring buffer, the ISR only advances the head and the consumer only advances the tail. Keep ISRs short (capture, signal, return) and defer processing to a task or the main loop.
- With an RTOS: signal from ISR to task through queues or semaphores using the ISR-safe API variants. Never block or take a mutex inside an ISR. Use mutexes with priority inheritance to bound priority inversion, and take locks in one global order to avoid deadlock.
- To find a race, look for a symptom that changes with timing or optimization level. Audit every shared variable, instrument with low-overhead GPIO toggles captured on a logic analyzer, and stress the system with higher interrupt rates.
Follow-up
- How would you handle a race condition between an ISR and the main loop?
- What is priority inversion, and how does priority inheritance limit it?
- Why can a 64-bit tick counter updated in an ISR read back wrong on a 32-bit core, and how do you read it safely?
How do you approach the design of a system when requirements are ambiguous?
Medium · Problem-Solving & Logic
Approach
- Turn the ambiguity into questions grouped by constraint: required behaviour, inputs and outputs, timing and latency, memory and power budget, operating environment, interfaces to other boards or software, and what failure looks like. Ask first the ones that would change the architecture.
- Write down assumptions for anything nobody can answer yet, and design so they are cheap to change: a hardware abstraction layer, configuration in one place, and a state machine for device behaviour so states can be added later.
- Prototype the riskiest part first, such as an unfamiliar sensor, the radio link or the timing-critical loop, and measure it so later decisions rest on numbers.
- Work a concrete case such as a driver for a new sensor. Read the datasheet, separate what the hardware fixes (bus, timing, registers) from what is open (sample rate, buffering, error handling), sketch the interface, and review it with the people who will call it.
- State each trade-off aloud as you decide, in terms that matter on hardware: memory, CPU time and power.
Follow-up
- Which question would you ask first, and why that one?
- A requirement changes after the boards are built. How does your design absorb it?
- How do you record decisions so the team knows which assumptions are still open?
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
- 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.
- 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.
- 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.
- 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
- 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.
- 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.
- 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.
- 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?
These questions test your understanding of how software interacts with hardware components.
Medium · Embedded Systems & Firmware
Approach
- Frame the answer around the path from a line of code to a pin. Peripherals are controlled through memory-mapped registers, so a write to an address in the peripheral region changes hardware state. A read returns live status bits, not stored data.
- Show a correct register access: a pointer to volatile uint32_t at the address from the reference manual, then a masked read-modify-write, reg = (reg & ~MASK) | (val << SHIFT). That sequence is not atomic: an ISR writing the same register in between can lose an update. Set/reset registers such as STM32 GPIO BSRR avoid this.
- Compare the three ways software learns about hardware events. Polling is simple but burns CPU and power. Interrupts give low latency but need care with shared state. DMA moves data without the CPU but needs buffer-ownership rules, plus cache maintenance on cores that have a data cache.
- Walk through bringing up a peripheral: enable its clock, configure the pins (mode, alternate function, pulls), set the peripheral registers from the datasheet, then enable the interrupt in both the peripheral and the interrupt controller. On many MCUs a peripheral without its clock ignores writes and reads back zero.
Follow-up
- How would you design a driver for a new sensor?
- Why does volatile not make a register update safe when an ISR also writes that register?
- When would you pick DMA over one interrupt per byte for UART receive, and what changes in your buffer handling?
What are the common challenges when writing code for resource-constrained environments?
Medium · Embedded Systems & Firmware
Approach
- Name the constraints concretely: kilobytes of RAM and flash, no MMU (so a stack overflow silently corrupts neighbouring memory), often no FPU, real-time deadlines and a power budget. Then give the practice that answers each one.
- Memory: allocate statically, or from fixed-size pools at init, instead of general heap use at runtime. Fragmentation and malloc timing are hard to bound. Measure usage with the linker map file and the size tool, and size task stacks by stack painting: fill with a pattern, then read the high-water mark.
- Code size and C++ cost: build with -Os, consider -fno-exceptions and -fno-rtti, and watch template instantiation and inlining growth. Avoid pulling in printf-family or iostream code just for logging. The map file shows what each library adds.
- Compute: use integer or fixed-point maths where there is no FPU, since software floating point is slow and large. Use lookup tables when flash is cheaper than cycles, and DMA for bulk transfers.
- Reliability: enable a watchdog, put a timeout on every loop that waits for hardware, and define what the device does on error. Test the logic on the host with the hardware abstracted, then on the target.
Follow-up
- How do you find out how much stack each task actually uses?
- The firmware no longer fits in flash after a new feature. What do you look at first?
- When, if ever, is dynamic allocation acceptable in firmware?
How do you optimize power consumption in an embedded application?
Medium · Embedded Systems & Firmware
Approach
- Measure before changing anything. Profile current over time with a power analyser, or a shunt resistor and an oscilloscope, to see which state (active, radio, sleep) dominates. Average current is the time-weighted sum of each state's current. Battery life is roughly capacity divided by that average.
- Spend most of the time asleep. Replace polling loops with interrupt-driven wakeups and use the deepest sleep mode that still keeps the wake source and RAM you need. Trade-off: deeper modes save more but wake more slowly and may lose RAM or peripheral state.
- Cut active power. Gate the clocks of unused peripherals and lower core clock or voltage where the workload allows: dynamic CMOS power scales with switched capacitance, voltage squared and frequency. Weigh race-to-sleep (finish fast at a high clock, then sleep) against running slowly for longer.
- Find the leaks the measurement shows: floating input pins, pull-ups fighting a driven line, sensors and peripherals left powered, LEDs, and a debug connection that keeps clocks running in sleep. Batch radio transmissions and flash writes so the expensive parts wake less often.
Follow-up
- The device draws far more current in sleep than the datasheet figure. How do you track down the cause?
- When does running at a lower clock frequency use more energy, not less?
- How would you estimate battery life for a device that wakes every 10 seconds to sample a sensor?
These questions assess your core language proficiency and your ability to write maintainable, efficient code.
Medium · C++ & Programming Foundations
Approach
- Treat this as a prompt to show sound C++ habits on a small piece of code. Use RAII for every resource (a lock guard, a file, a claimed peripheral released in the destructor), keep const correctness, and pass large objects by const reference.
- Know the rule of zero/three/five. If a class manages a resource, define or delete the copy constructor, copy assignment, move constructor, move assignment and destructor as a set. Otherwise let members with value semantics handle it and write none of them.
- Explain move semantics precisely. std::move is only a cast to an rvalue reference. The move itself happens in whichever move constructor or move assignment overload resolution picks. A moved-from object must stay valid, in an unspecified state.
- Show efficiency without obscurity: constexpr for compile-time values, enum class instead of bare macros, std::array for fixed buffers, and reserve() when a vector's size is known. Avoid undefined behaviour: signed overflow, out-of-bounds access, reading uninitialized values.
- Back the maintainability claims with specifics: small single-purpose functions, names that carry units (timeout_ms), and unit tests that run on the host.
Follow-up
- Why does a base class with virtual functions need a virtual destructor?
- What is the difference between const, constexpr and a #define for a constant?
- When would you mark a function noexcept, and what does that change for std::vector reallocation?
What are the differences between stack and heap allocation?
Medium · C++ & Programming Foundations
Approach
- Stack: automatic storage for locals and call frames. Allocating and freeing just moves the stack pointer, so it is fast and deterministic, and lifetime ends with the scope. Its size is fixed per thread or RTOS task, so deep recursion or large local arrays overflow it.
- Heap: dynamic storage from new or malloc, sized at runtime, with a lifetime you control. The costs are allocator bookkeeping, timing that is hard to bound, fragmentation over a long run, and leaks when ownership is unclear.
- Name the classic errors: returning a pointer or reference to a local (it dangles once the frame is gone), and freeing heap memory twice or never. Note that std::vector and std::string keep their elements on the heap even when the object itself is a local.
- Embedded angle: without an MMU, a stack overflow silently overwrites adjacent memory. Size stacks from measured high-water marks plus a margin, use the MPU or a guard region where available, and make large buffers static. Many firmware projects use the heap only during init, or use fixed-block pools with constant-time allocation.
Follow-up
- How do you choose the stack size for an RTOS task?
- Why is calling malloc inside an interrupt handler a problem?
- Where do static and global variables live, if not on the stack or the heap?
Describe the use of classes and inheritance in embedded software design.
Medium · C++ & Programming Foundations
Approach
- Use classes for encapsulation first. A driver class owns a peripheral's configuration and state, keeps register details private, and exposes a small interface (init, read, write, error status). That gives most of the maintainability benefit at no runtime cost.
- Use inheritance for interfaces, not code reuse. An abstract bus or sensor interface lets application logic depend on the abstraction, so the same code runs against real hardware or a mock in host unit tests. Keep hierarchies shallow.
- State the cost of virtual dispatch accurately. Each polymorphic object carries a vtable pointer, each class has a vtable (usually in flash), every call is indirect, and the compiler can rarely inline it. That is negligible for most drivers but can matter on a tight ISR path.
- Give the compile-time alternative. Templates or CRTP provide static polymorphism with no vtable and full inlining. The price is code size per instantiation, harder error messages, and types fixed at build time.
- State the embedded rules: create objects statically rather than with runtime new; give a polymorphic base a virtual destructor, or a protected non-virtual one if objects are never deleted through it; and know whether the build disables exceptions and RTTI.
Follow-up
- When would you choose templates over inheritance for a driver interface, and what does each cost?
- How would you unit-test a driver class without the hardware?
- What happens when a constructor calls a virtual function?
How do you ensure code portability across different hardware architectures?
Medium · C++ & Programming Foundations
Approach
- Isolate hardware behind an abstraction layer. Application and protocol logic call an interface (GPIO, timer, bus), and only the per-board implementation touches registers. Porting then means writing one layer, not editing the logic.
- Make type sizes explicit. Use <cstdint> fixed-width types for any data whose size matters: int is 16 bits on AVR and MSP430 but 32 bits on ARM. Whether plain char is signed is implementation-defined (usually unsigned on ARM, signed on x86). Put a static_assert on every size or layout assumption.
- Never rely on in-memory layout for data that leaves the chip. Endianness differs between architectures and struct padding between compilers. Serialize each field into a byte buffer with shifts and masks instead of memcpy-ing a struct or casting a byte pointer. This also avoids unaligned accesses, which fault on some cores.
- Avoid undefined or implementation-defined behaviour that one compiler happens to tolerate: signed overflow, shifting by the type's width or more, strict-aliasing violations, and compiler extensions used without a wrapper macro.
- Prove portability by building it. Compile for at least two targets plus the host in CI with warnings as errors, and run the unit tests on the host.
Follow-up
- How would you send a 32-bit sensor value over UART so a receiver with the other byte order reads it correctly?
- What breaks when code moves from a 16-bit MCU to a 32-bit ARM core?
- Where do compiler-specific attributes, such as struct packing or interrupt-handler declarations, belong in a portable codebase?
These questions focus on your analytical skills and your approach to technical roadblocks.
Medium · Problem-Solving & Logic
Approach
- Treat this as a request for your method on a stuck problem, and answer with a concrete sequence. State the symptom precisely (what, when, how often), make it reproducible, then shrink the search space.
- Narrow by halving: git bisect between a good and a bad build, swap one board, cable or firmware version at a time, or disable half the features. Choose tests that rule out a group of causes whichever way they come out.
- Work one written hypothesis at a time, each paired with the observation that would confirm or kill it. Check the instruments before trusting a reading: probe ground, analyzer sample rate, and logging that itself changes timing.
- Say when you change approach. Timebox the investigation, then read the chip errata, take a capture to a hardware colleague, or build a minimal reproduction. Close with how you stopped it recurring: a test, an assertion or a checklist item.
- Have one embedded example ready with numbers: how long it took, what you tried that failed, and what the root cause turned out to be.
Follow-up
- What did you try that turned out to be wrong, and how did you find out?
- How do you decide whether a fault is in hardware or in software?
- When did you ask for help, and what evidence did you bring?
Walk us through a time you had to optimize a piece of code that was performing poorly.
Medium · Problem-Solving & Logic
Approach
- Build the story around measurement: the missed requirement (a deadline, throughput, current draw), the baseline number, the tool that found the hotspot, the change, and the after number. Without before and after figures the story is a guess.
- Name a real profiling method: a sampling profiler on a host build, a cycle counter such as DWT CYCCNT on Cortex-M3/M4/M7, or a GPIO set at function entry and cleared at exit, timed on a logic analyzer or oscilloscope.
- Show that you removed the biggest cost first: an algorithm or data-structure change (O(n^2) to O(n log n), a lookup table), skipping repeated work, moving data transfers to DMA, or replacing software floating point with fixed-point. Micro-tuning and compiler flags come after that.
- State the trade-off you accepted (flash for a table, RAM for a buffer, less readable code) and how you showed the output had not changed, for example by comparing results on recorded inputs before and after.
Follow-up
- How did you confirm the hotspot you fixed was the real bottleneck, not just the first one you found?
- What would you have done if the faster version had been much harder to maintain?
- Did the compiler optimization level change the result, and how did you check?
Describe a challenging bug you encountered and the steps you took to isolate and fix it.
Medium · Problem-Solving & Logic
Approach
- Pick a bug with a real isolation path, ideally across hardware and software: an intermittent fault, a crash only in release builds, or data corruption under load. Open with the symptom and how often it appeared.
- Walk through the isolation steps in order. Reproduce it reliably (stress, temperature, a specific input). Gather evidence: debugger, fault status registers after a hard fault, logic analyzer or oscilloscope captures, logs. Form a hypothesis, then run the test that rules it in or out.
- Name the root cause precisely and explain why it produced that symptom. Examples: a flag shared with an ISR, missing volatile and optimized away at -O2; a stack overflow overwriting a global; a race on a shared buffer; an uninitialized variable.
- Close with the fix and the prevention: a regression test, a static-analysis rule, a stack-watermark check or a review checklist item. Avoid stories where the fix was a timing tweak nobody could explain.
Follow-up
- How did you know the fix addressed the root cause rather than hiding the symptom?
- What evidence did you have before blaming hardware or software?
- What would you instrument differently next time?
Describe the process of debugging a communication protocol like I2C or SPI on an embedded target.
Medium · Embedded Systems & Firmware
Approach
- Start below the protocol. Confirm power, ground and the device supply voltage against the MCU's I/O levels, and check the wiring against the schematic. For I2C, confirm SDA and SCL have pull-up resistors: both lines are open-drain and cannot go high without them.
- Put a logic analyzer with a protocol decoder on the bus before changing code. For I2C, check the address byte and whether the device ACKs it. A NACK usually means a wrong address (7-bit vs shifted 8-bit form), no power, or a wrong address-select pin. For SPI, check chip-select framing, mode (CPOL/CPHA) and bit order against the timing diagram.
- Read a fixed register first, such as a device ID or WHO_AM_I register. That separates 'the bus works' from 'my configuration is wrong'. Then check the transaction format the device expects: a register-address write followed by a repeated start for I2C reads, or a read/write bit in the first SPI byte.
- Move to an oscilloscope when the decoder shows garbage. Look for slow I2C rise times from weak pull-ups or long wires, ringing on fast SPI clocks, or a clock faster than the device supports. Lowering the clock is a quick test.
- Handle the stuck bus. If a slave holds SDA low after a reset mid-transfer, toggle SCL (up to nine pulses) until SDA is released, then send a STOP. Put a timeout on every bus wait so the firmware reports an error instead of hanging.
Follow-up
- The sensor works on the bench but fails now and then on the production board. What do you check?
- How do you tell from a capture whether the slave is clock stretching or the bus is stuck?
- Every SPI byte you read comes back shifted by one bit. What is the likely cause?
Suggested practice checklist
The plan follows the reported stack: first project stories and the loop map, then memory and C++, interrupts and concurrency, protocols and debugging, constrained and portable design, problem-solving stories, and a mixed rehearsal. Each day produces something you can reuse in an answer.
Day 1 — Map the loop and inventory your projects
- Write the four reported rounds in order and note the likely question types for each: background fit, practical discussion, team conversation, final technical assessment.
- Build a project inventory for every resume entry: microcontroller, peripherals, protocols, tools, your role and one problem solved.
- List the must-have skills reported for the role and rate yourself on each: C, C++, microcontrollers, I2C, SPI, UART, schematics and datasheets.
Deliverable: A round map, a project inventory sheet and a skill self-rating with the two weakest items marked.
Day 2 — Memory and C++ foundations
- Write a short note on volatile and non-volatile memory in a microcontroller: RAM versus flash or EEPROM, what survives power loss, and write-endurance limits.
- Write a table of stack versus heap: lifetime, allocation cost, fragmentation, and what the linker places in each memory region on a microcontroller.
- Explain pointers and ownership aloud, covering raw pointers, references, smart pointers and how you would prevent leaks and dangling pointers in a long-running device.
Deliverable: Three written notes (memory types, stack versus heap, pointer ownership) plus a recorded spoken answer for each.
Day 3 — Interrupts, concurrency and shared data
- Write an ISR and main-loop pair that moves UART bytes through a single-producer, single-consumer ring buffer, and explain why it needs no lock.
- Write a short answer on how to keep ISR latency low: minimal ISR, deferred work, DMA, priorities and critical sections.
- Describe a firmware race condition scenario: the shared variable, the failing interleaving and two fixes.
Deliverable: A working ring buffer sketch, a written ISR answer and a race condition write-up.
Day 4 — Peripherals and protocol debugging
- Write a checklist for debugging an I2C link: wiring and pull-ups, address, ACK/NACK, clock stretching, logic analyzer decode.
- Write the equivalent for SPI (mode, speed, chip select, byte order) and for UART (baud, framing, parity).
- Outline the design of a driver for a new sensor from datasheet to tested code: register map, bus access layer, init sequence, error handling.
Deliverable: A protocol debugging checklist covering I2C, SPI and UART, and a one-page sensor driver outline.
Day 5 — Constrained, low-power and portable design
- List the challenges of resource-constrained code (RAM and flash size, no heap, stack depth, timing) and one mitigation for each.
- Write a power optimization answer: sleep modes, clock gating, interrupt-driven wake-ups, polling avoided, and how to measure current.
- Design a hardware abstraction layer so the same application code builds for two architectures, and decide where classes and inheritance help or cost you.
Deliverable: A constraint-and-mitigation table, a power answer and a hardware abstraction layer sketch with the inheritance trade-off stated.
Day 6 — Problem-solving stories
- Write a story about optimizing slow code: what you profiled, the number before, the change and the number after.
- Write a story about a hard bug, covering how you isolated it, which tool you used and what the cause turned out to be.
- Write a step-by-step approach for a design with ambiguous requirements: questions to ask, assumptions to state, first version to build.
Deliverable: Three written stories or frameworks, each with a measurable detail, rehearsed aloud once.
Day 7 — Mixed rehearsal
- Answer aloud one question from each group: an ISR or concurrency question, a protocol debugging question and a C++ memory question.
- Write a short C++ routine by hand, such as a bit manipulation helper or an RAII wrapper, and review it for ownership and overflow defects.
- Review your project inventory against the weakest skills from day 1 and write the questions you would ask the team.
Deliverable: Three spoken answers, one handwritten routine reviewed, and a list of questions for the team.
Project stories and engineering judgement
Candidates report project deep-dives and conversations with several engineers, so these prompts test whether you can tell a precise, measurable engineering story. For this role, favour stories involving hardware, debugging tools and trade-offs in memory, speed or power. State what you decided yourself and what the result was.
- Describe a time you had to optimize a piece of code that was performing poorly.
- Describe a challenging bug you encountered and the steps you took to isolate and fix it.
- How do you approach the design of a system when requirements are ambiguous?
- Tell me about a time you worked with a hardware engineer to resolve a mismatch between a schematic or datasheet and your software.
- Explain a technical failure from one of your projects to someone who is not an engineer.
How do you handle interrupt service routines (ISRs) to prevent system latency?
Medium · Embedded Systems & Firmware
Approach
- Keep each ISR minimal. On entry, clear or acknowledge the peripheral flag, read the data register, record a timestamp if needed, push the data into a buffer or set a flag, and return. Parsing, logging and heavy computation run in the main loop or an RTOS task. System latency is driven by the longest ISR and the longest time interrupts stay masked.
- Defer work with a top-half/bottom-half split. On a bare-metal loop, set a flag or write to a single-producer, single-consumer ring buffer. On an RTOS, signal a task with the ISR-safe variant of the semaphore or notification call. Use DMA for bulk transfers so the CPU takes one interrupt per block instead of one per byte.
- Handle shared data carefully. Declare variables shared with an ISR volatile so the compiler re-reads them, but remember volatile does not give atomicity. A multi-word value on a small core needs a short critical section, or a design where the ISR is the only writer of one index and the main loop the only writer of the other.
- Never block in an ISR. No malloc, no printf, no mutexes, no long loops, no waiting on another peripheral. Assign priorities by deadline, enable nesting only when a high-priority source truly cannot wait, and keep critical sections short.
- Back the answer with a measurement from a real project: toggle a GPIO at ISR entry and exit and read it on an oscilloscope or logic analyzer to get worst-case latency and jitter, then state the before and after. The mistake that sinks this answer is reciting rules with no number from a device you worked on.
Follow-up
- How do you pass data between an ISR and the main loop without a lock, and what ordering problems can appear?
- What does volatile guarantee, and what does it not?
- Interrupts arrive faster than you can service them. What do you check and change?
Turn a code review disagreement into a decision
Easy · code review · optimistic concurrency · communication
A colleague's change updates a row with UPDATE resource SET version = version + 1 WHERE resource_id = $1 AND version = $2 and treats an affected-row count of zero as a successful no-op. You read that as a silently lost update; they think returning 200 is friendlier to clients than returning a conflict. Describe how you have handled a review disagreement of this shape: what goes in the comment, when you leave the thread, and who decides. Then write the comment you would leave here, in under 80 words.
Approach
- Sort the disagreement before writing anything. A silently discarded write is a correctness claim about data; the choice between 409 and 412 is taste. Only the first justifies blocking a merge, and saying which one you are doing is most of the value of the comment.
- Make the claim reproducible in the comment itself with an interleaving rather than a principle: A reads version 7, B reads version 7, B commits version 8, A's predicate matches zero rows, A is told it succeeded and A's edit is gone.
- Offer the alternative with its cost attached: return 409 carrying the current version and the revision that won, so the client can re-read and re-apply. Note that automatic retry is not the fix, because a retry re-reads the winner's state and reapplies an intent formed against data that no longer exists.
- Apply an escalation rule you can state: two round trips on the thread, then a call, and the service's owner decides rather than the reviewer. A reviewer who cannot be overruled is a bottleneck with extra steps.
Follow-up
- Where would you put the test that fails if someone reintroduces the swallowed zero rowcount?
- The author says clients cannot handle a 409. How do you check whether that is true?
Estimate work you have never done and defend the range
Hard · estimation · backfills · expand-contract
You are asked to estimate a change you have never attempted: add a column to a 100-million-row table, populate it, move reads across, and drop the old shape. Give a range with the assumptions that generate it, including batch size, the signal your backfill throttles on, and wall-clock hours, and name the three unknowns that would move the number most. Then describe a real estimate you gave under comparable ignorance: how you expressed its uncertainty, what you committed to, and how wrong you turned out to be.
Approach
- Decompose into independently deployable steps before estimating anything: add the column nullable, write both shapes, backfill in batches, verify, move reads, stop writing the old shape, drop it. That is four deploys spread over days, and the calendar estimate is dominated by them rather than by the loop's runtime.
- Do the arithmetic aloud for the part that has arithmetic in it: batch size times number of batches times per-batch duration, at a write rate the primary can absorb alongside roughly 1.2k writes per second of production traffic. The loop is throttled by replication lag and lock waits, not by how fast it can issue statements.
- Price the schema step by its lock rather than its statement duration. In PostgreSQL an ALTER TABLE taking ACCESS EXCLUSIVE waits for every open transaction on that table while later queries queue behind it, so a millisecond change issued during a thirty-second analytics query stalls that table for thirty seconds. Adding a nullable column with a non-volatile default avoids a rewrite from version 11; a new index wants CREATE INDEX CONCURRENTLY, which cannot run inside a transaction block and leaves an invalid index behind if it fails.
- Express the answer as a range whose endpoints each trace to a stated assumption, then name the cheapest experiment that collapses it, which is almost always running one real batch against the real table and multiplying.
Follow-up
- How do you verify the backfill genuinely finished, given rows written by production traffic while it ran?
- Where does the backfill resume from after a worker is killed mid-batch, and what makes that resume point trustworthy?