Google Embedded Software Engineer Interview Preparation Guide - Junior Level
Google's Embedded SWE interview process for junior-level candidates emphasizes practical embedded systems knowledge and low-level programming proficiency. The interview loop includes an initial recruiter screening, technical phone screen rounds focused on C programming and embedded concepts, and multiple onsite rounds covering embedded systems fundamentals, coding under hardware constraints, system-level problem solving, and behavioral assessment. Unlike standard SWE interviews, embedded roles prioritize bit manipulation, memory optimization, hardware interaction understanding, and driver-level concepts over complex data structures and graph algorithms.
Interview Rounds
Recruiter Screening
What to Expect
Initial conversation with Google recruiter to discuss your background, interest in the embedded systems role, and general qualifications. This is a preliminary assessment round designed to verify basic fit and communication skills. The recruiter will also provide information about the role, team, and interview process.
Tips & Advice
Be clear about your embedded systems experience and genuine interest in hardware-software interaction. Prepare 2-3 concrete examples of embedded projects you've worked on (even if academic). Ask thoughtful questions about the team's technology stack, types of devices/hardware they work with, and real-world challenges they solve. Have a professional summary of your background ready that emphasizes any relevant experience with microcontrollers, IoT, or low-level programming.
Focus Topics
Motivation and Questions
Authentic interest in the specific role and team; thoughtful questions about technology, devices, and technical challenges.
Practice Interview
Study Questions
Communication and Clarity
Ability to explain technical concepts clearly without jargon overload; demonstrates capacity to work effectively with both hardware and software engineers.
Practice Interview
Study Questions
Background and Embedded Experience
Clear articulation of your embedded systems experience, academic or professional projects involving microcontrollers, firmware, or IoT systems.
Practice Interview
Study Questions
Technical Phone Screen - Embedded Fundamentals
What to Expect
First technical interview conducted via phone or video where you'll solve an embedded systems problem combining C programming with hardware concepts. This round assesses your ability to write efficient, correct C code while considering hardware constraints like memory limitations, bit-level operations, and performance. You may be asked to write code or pseudo-code on a shared document. The interviewer will probe your understanding of data types, memory management, and how your code maps to actual hardware behavior.
Tips & Advice
Write clean, efficient C code with proper data types for embedded contexts. Think about memory usage and explain your choices. If using bit manipulation, clearly document what each bit represents. Ask clarifying questions about hardware constraints (e.g., 'Are we working with limited RAM?', 'What is the timing constraint?'). Walk the interviewer through your thought process. Be prepared to optimize code for both speed and memory. Avoid using libraries or abstractions; show you can work at the hardware level. If stuck, discuss your approach with the interviewer rather than staying silent.
Focus Topics
Memory and Resource Constraints
Understanding memory hierarchy, stack vs. heap, register constraints, optimizing for minimal memory footprint, and choosing appropriate data structures for limited-resource environments.
Practice Interview
Study Questions
Arrays and String Handling
Working with arrays, index manipulation, string operations in C (without relying on string.h), buffer management, and avoiding overflow conditions.
Practice Interview
Study Questions
Hardware Interaction Concepts
Basic understanding of how software interacts with hardware: memory-mapped I/O, registers, addresses, and relationship between C code and actual device behavior.
Practice Interview
Study Questions
Bit Manipulation and Bit-Level Operations
Competency with bitwise operators (AND, OR, XOR, shifts), bit masking, bit extraction, setting and clearing specific bits, and understanding binary representations.
Practice Interview
Study Questions
C Programming Fundamentals for Embedded
Solid understanding of C syntax, data types (uint8_t, uint16_t, int32_t), pointers, memory management, and function implementation without relying on standard library abstractions where hardware directly accessed.
Practice Interview
Study Questions
Technical Phone Screen - Driver/Protocol Implementation
What to Expect
Second technical phone interview focusing on driver development or hardware protocol implementation. You may be given a practical scenario such as implementing basic driver functionality, handling interrupts, or communicating with a peripheral (e.g., SPI, I2C, UART). The interviewer assesses your understanding of real-time constraints, interrupt handling, state management, and how software bridges microcontroller capabilities with application requirements. This round is more use-case focused than pure coding.
Tips & Advice
Understand the hardware interface or protocol mentioned in the problem (read datasheets if relevant). Ask about real-time constraints, interrupt priorities, and error conditions. Show understanding of state machines and how interrupt handlers interact with main code flow. Write clear, defensive code that handles edge cases. If a specific protocol or hardware interface is mentioned that you've encountered, discuss your prior experience but don't assume; verify details with the interviewer. Discuss timing implications and potential race conditions. Be prepared to explain how your code handles asynchronous events.
Focus Topics
Register Manipulation and Bit-Banging
Direct hardware register access, setting/clearing bits for device configuration, and understanding memory-mapped I/O. Low-level hardware control without abstraction layers.
Practice Interview
Study Questions
Common Communication Protocols (I2C, SPI, UART)
Familiarity with serial communication protocols, timing requirements, data framing, error handling, and how to implement protocol state machines.
Practice Interview
Study Questions
State Machines and Asynchronous Programming
Designing finite state machines for hardware control, managing state transitions, and writing responsive code that handles asynchronous events efficiently.
Practice Interview
Study Questions
Driver Development Basics
Understanding of device driver architecture, initialization sequences, register configuration, interrupt handling, and state management for hardware peripherals.
Practice Interview
Study Questions
Interrupt Handling and Real-Time Constraints
Concepts of interrupt service routines (ISRs), interrupt priorities, interrupt masking, and timing constraints in real-time systems. Understanding how interrupts interact with main program flow.
Practice Interview
Study Questions
Onsite Round 1 - Embedded Systems Coding
What to Expect
First onsite technical interview focused on embedded systems coding problem. You'll solve a problem that combines C programming with embedded systems constraints. This may involve optimizing code for a specific microcontroller with limited resources, implementing a simple algorithm with hardware awareness, or solving a practical embedded challenge. The interviewer assesses code quality, problem-solving approach, resource optimization, and ability to handle real-time constraints in a collaborative setting.
Tips & Advice
Write your solution on a whiteboard or shared document clearly. Explain your approach before coding. Focus on correctness first, then optimization. Discuss trade-offs between speed and memory explicitly. Consider edge cases and error conditions. Explain how your code would behave on actual hardware. Be ready to refactor based on new constraints (e.g., 'the device now has only 2KB of RAM'). Communicate constantly with the interviewer about your reasoning. If you mention specific hardware in your resume, be ready to discuss it in the context of coding problems.
Focus Topics
Error Handling and Robustness
Handling edge cases, error conditions, and designing code that fails gracefully in resource-constrained environments. Defensive programming for embedded contexts.
Practice Interview
Study Questions
Binary Operations and Low-Level Representation
Understanding how data is represented in binary, performing bit-level operations, working with flags and packed data structures, and manipulating bits for efficiency.
Practice Interview
Study Questions
Communication Between Code and Hardware
Understanding how C code translates to hardware behavior, memory-mapped I/O, register access, and the relationship between software logic and physical device state.
Practice Interview
Study Questions
Code Optimization for Embedded Systems
Techniques for optimizing code for speed and memory: avoiding dynamic allocation, using stack efficiently, understanding compiler optimizations, writing cache-friendly code.
Practice Interview
Study Questions
Algorithm Design for Resource-Constrained Environments
Designing algorithms that minimize memory usage and processing time; selecting appropriate algorithms for embedded contexts where standard solutions may be impractical.
Practice Interview
Study Questions
Onsite Round 2 - System Architecture and Integration
What to Expect
Second onsite technical interview assessing your understanding of system-level embedded architecture. You may be asked to design a simple embedded system component, discuss how different subsystems interact (sensor input, processing, output control), or solve a problem requiring understanding of real-time constraints and system integration. The focus is on practical system thinking rather than complex algorithms—how do components work together, what are the timing implications, how do you ensure reliability and synchronization?
Tips & Advice
Think about the complete system picture: inputs, processing, outputs, timing, and synchronization. Draw diagrams to show system flow and data paths. Discuss potential bottlenecks and failure modes. Be specific about timing requirements and how you'd meet them. Consider both hardware capabilities and software limitations. If discussing an actual embedded system you've worked with, draw its architecture and explain design decisions. Ask about non-functional requirements (latency, power consumption, reliability). Use concrete examples from your experience to illustrate points. Avoid over-engineering; solutions should be practical for a junior-level implementation.
Focus Topics
Power and Energy Efficiency
Understanding power consumption in different modes, low-power design techniques, sleep/wake mechanisms, and trade-offs between performance and power.
Practice Interview
Study Questions
Debugging and Verification in Embedded Systems
Techniques for debugging embedded code, using debuggers and emulators, understanding hardware behavior verification, and testing strategies for systems without easy visibility into internals.
Practice Interview
Study Questions
Sensor Input and Output Control
Reading sensor data with appropriate sampling rates, processing noisy inputs, controlling actuators, understanding analog-to-digital and digital-to-analog conversion at system level.
Practice Interview
Study Questions
Embedded System Architecture Design
Understanding system components (sensors, processors, actuators, communication), data flow between components, architectural patterns suitable for embedded systems, and integration considerations.
Practice Interview
Study Questions
Real-Time Systems and Timing Constraints
Meeting hard and soft real-time deadlines, task scheduling, understanding latency and jitter, synchronizing concurrent activities, and designing systems that respond to time-critical events.
Practice Interview
Study Questions
Onsite Round 3 - Behavioral and Cross-Functional Collaboration
What to Expect
Final onsite round focusing on behavioral competencies and demonstrated ability to work effectively in teams. You'll discuss past projects, how you handled technical challenges, experiences collaborating with hardware engineers, learning from mistakes, and alignment with Google's values. The interviewer assesses communication skills, collaborative approach, ability to handle feedback, growth mindset, and how you think about solving real problems in team contexts. This round often includes discussions about your approach to code quality, testing, and communication.
Tips & Advice
Prepare 3-4 concrete project examples (academic or professional) showing: (1) overcoming a technical challenge in embedded systems, (2) collaborating with hardware engineers or team members, (3) debugging a difficult hardware-software issue, (4) iterating on a design based on constraints. Use the STAR method (Situation, Task, Action, Result) for clear storytelling. Be genuine and specific—avoid generic answers. Discuss what you learned from failures. Show curiosity about how embedded systems work. Emphasize collaborative approach rather than solo achievements. Prepare questions about team dynamics, learning opportunities, and how you'll grow at Google. Listen actively and respond thoughtfully to interviewer's comments.
Focus Topics
Code Quality and Communication
Your approach to writing maintainable code, how you document work, communicating technical decisions to teammates, and valuing code reviews.
Practice Interview
Study Questions
Learning from Failure and Iteration
Examples of mistakes you've made in embedded projects, what you learned, how you changed your approach, and how you've grown as an engineer.
Practice Interview
Study Questions
Debugging and Problem Resolution
Approaches to debugging complex issues, handling ambiguous problems, using tools and techniques effectively, and systematic troubleshooting methodology.
Practice Interview
Study Questions
Collaboration with Hardware Engineers
Experience working with hardware teams, understanding hardware constraints that impact software, communicating across disciplines, and resolving hardware-software integration challenges.
Practice Interview
Study Questions
Project Experience and Technical Problem-Solving
Specific examples of embedded projects you've completed, technical challenges you faced, how you approached problem-solving, and measurable outcomes of your work.
Practice Interview
Study Questions
Frequently Asked Embedded Developer Interview Questions
Explain hardware timers and counters in microcontrollers: common timer modes (up, down, up/down), prescalers, auto-reload/period registers, capture/compare channels, overflow/underflow interrupts, and typical uses such as event timing, input capture, PWM, and scheduling. Discuss how prescalers affect resolution and maximum measurable interval.
Sample Answer
Overview
Hardware timers/counters are peripherals that increment/decrement a register at a clock-derived rate to measure time, count edges, or generate timed outputs. They’re essential in embedded firmware for precise timing without CPU polling.
Common timer modes
- Up: counter counts 0 → ARR (auto-reload register) then overflows and optionally triggers interrupt.
- Down: counts ARR → 0 then underflows.
- Up/Down (center-aligned): counts up then down for symmetric PWM.
Key building blocks
- Prescaler (PSC): divides peripheral clock to slow the tick rate.
- Auto-reload / Period register (ARR): top value that defines period.
- Capture/Compare channels (CCx): used to capture timestamp on input edges or compare to generate outputs (PWM, toggles).
- Overflow/underflow interrupts: fired when counter wraps — useful for periodic tasks or extending range.
Typical uses
- Event timing / measuring intervals (input capture stores counter on edge)
- PWM generation (set ARR = period, CCx = duty)
- Edge counting (external pulses as clock source)
- Scheduling periodic tasks without RTOS tick
Prescaler effects
Use formula for timer period:
Timer period (seconds) = (PSC + 1) * (ARR + 1) / F_timer_clock
- Increasing PSC increases maximum measurable interval but reduces time resolution (granularity = (PSC+1)/F_timer_clock).
- To maximize resolution choose smallest PSC that keeps ARR within range; to measure long intervals increase PSC and/or cascade counters.
Practical tip
Combine capture with DMA to log high-rate events; use center-aligned mode to reduce harmonic content in motors.
Write a bounds-checked read function (for example a Go safeSliceRead(buf []byte, offset, length int64) ([]byte, error)) that defensively rejects negative values, integer overflow when computing offset+length, and out-of-bounds access, returning an explicit error instead of panicking. Contrast this with an alternative design that returns an Option/optional type instead of an error for an expected-empty case (for example popping from an empty stack) and discuss when each style is preferable.
Sample Answer
Direct answer
Defend a bounds-checked read by explicitly rejecting negative offsets/lengths, checking for integer overflow when computing offset+length (which can wrap around to a small or negative number and defeat a naive bounds check), and rejecting anything past the buffer's actual length, returning an explicit error rather than panicking on any of these conditions.
Structured elaboration
- Negative values: reject immediately; a negative offset or length is never valid for a byte-buffer read.
- Overflow on
offset+length: if both are large positiveint64values, their sum can wrap around to a smaller (or negative) number, which would then incorrectly pass a naiveend > len(buf)check; detect this by checkingend < offset(the sum wrapping pastint64's max means the result becomes smaller than one of its inputs). - Actual bounds: after the overflow check, confirm
enddoesn't exceed the buffer's real length.
Worked example (executed against Go's actual runtime; all assertions passed)
func safeSliceRead(buf []byte, offset int64, length int64) ([]byte, error) {
if offset < 0 || length < 0 {
return nil, fmt.Errorf("negative offset or length: %w", ErrOutOfBounds)
}
end := offset + length
if end < offset { // overflow wrapped around
return nil, fmt.Errorf("offset=%d length=%d: %w", offset, length, ErrOverflow)
}
if end > int64(len(buf)) {
return nil, fmt.Errorf("offset=%d length=%d exceeds buffer len=%d: %w", offset, length, len(buf), ErrOutOfBounds)
}
return buf[offset:end], nil
}
Verified: a normal in-range read returns the correct slice; a negative offset, an out-of-bounds range, and an offset near math.MaxInt64 (which triggers the overflow branch specifically, not just the plain out-of-bounds branch) all correctly return an error instead of panicking or, worse, silently returning wrong data.
A complementary design for an 'expected-empty' case rather than an out-of-range error: C++'s std::optional<int> safe_pop(std::stack<int>& s) returns an empty optional when popping an empty stack, instead of throwing or returning a sentinel like -1 that could collide with a real value; this is preferable when 'nothing there' is a normal, expected outcome (not a caller bug), whereas safeSliceRead's explicit error is right because an out-of-bounds read usually DOES indicate a caller bug worth surfacing loudly.
Trade-offs and pitfalls
The end < offset overflow check works here specifically because Go's integer overflow wraps (rather than panicking or being undefined behavior), which is exactly the property that makes it possible to detect after the fact by checking if the sum became smaller than an input; in a language with undefined behavior on signed overflow (C, in the strict standard sense), this same check is not safe to rely on and you'd need to check BEFORE the addition (e.g., if length > math.MaxInt64 - offset) instead.
What was the biggest technical challenge in that project, and how did you overcome it?
Sample Answer
Direct answer: Pick one real obstacle, not the project's general level of difficulty, and be honest that something didn't work on the first attempt. State what the failure looked like, what you tried, what actually worked, and why.
What "biggest challenge" means to the interviewer
Distinguish ambient difficulty (the project was generally hard) from a specific moment where you were stuck, wrong, or something broke. The question wants the latter: a real obstacle with a resolution arc, not just "the project was hard."
Framework for the answer
- Name the specific obstacle in one sentence (a bug, a wrong initial approach, a constraint discovered late).
- State what you tried first and why it seemed reasonable at the time.
- State why that didn't work, and what new information surfaced.
- State what you changed and why it worked.
- State what you'd do differently to catch it earlier next time.
Common obstacle types
| Type | Example | Resolution pattern |
|---|---|---|
| Technical / design | An approach that worked in testing broke under real conditions | Instrument to find the actual root cause, then isolate the fix to the affected path only |
| Dependency | A team or system you relied on didn't deliver as expected | Renegotiate scope or build a fallback path instead of waiting |
| Knowledge gap | The domain was unfamiliar and the first design missed a real constraint | Bring in a subject-matter reviewer earlier, before the design is finalized |
Worked example (illustrative, no fabricated precision)
Midway through a service migration, the new system passed all pre-launch load tests but started timing out under real production traffic within the first day. The load tests had used synthetic requests with a flat size distribution. Investigating production logs pointed to a long-tail payload size distribution; illustrative assumption for this example: the largest requests ran roughly 50 times the median size, and those large requests were serialized on a single-threaded parser that the flat synthetic test data never exercised. The fix: moved parsing for large payloads onto a separate worker pool bounded by a queue, instead of the shared request-handling thread pool, isolating the slow path without touching the common case. Verified by replaying a sample of real production traffic against the new code path in staging before rollout, rather than trusting the original synthetic load test again.
Trade-offs and pitfalls
- Picking a challenge that wasn't really yours to solve undermines the whole answer once probed.
- Describing only the technical fix without naming what changed in your process afterward misses half the point of the question.
- Avoiding admitting the first approach failed reads as defensive rather than reflective.
- Choosing an obstacle that resolved mostly by luck doesn't showcase reasoning the way a diagnosed-and-fixed obstacle does.
You suspect heap corruption in a multitasking RTOS where seemingly unrelated tasks crash. Describe a workflow to identify the corruption source: enabling heap guards, per-task pools, stack/heap canaries, selective watchpoints, link-map analysis, binary search tests, and minimizing instrumentation impact to avoid hiding timing-sensitive bugs.
Sample Answer
Overview / goal
I’d follow a methodical, low-intrusion workflow to localize the heap corruption without masking timing-sensitive bugs.
1. Baseline & reproduce
- Reproduce with deterministic inputs; collect core dump, task list, free/used heap stats, and timestamps.
- Run with hardware trace or SWO to avoid printf-induced timing changes.
2. Passive instrumentation
- Enable lightweight heap guards (red zones) and stack canaries globally but keep logging minimal.
- Turn on malloc/free logging with sampling or ring buffer to avoid timing perturbation.
3. Isolate by partitioning
- Convert global heap into per-task or per-subsystem pools one at a time (or use slab allocators). If corruption disappears when a task uses its own pool, that task is suspect.
4. Selective hardening
- Add bigger canaries and guard pages only around allocations from suspected pools.
- Use compiler options to insert stack canaries for individual tasks.
5. Watchpoints & link-map analysis
- Use the map file to find heap/RO/data ranges; set data-breakpoint/watchpoints on canary/footer addresses or frequently-corrupted buffers.
- If hardware breakpoints limited, set conditional watchpoints in the allocator (check pattern on free).
6. Binary-search tests
- Disable half the tasks, or bisect test cases to find minimal reproducer. Use task-enable masks to preserve timing for remaining tasks.
7. Minimize instrumentation impact
- Prefer hardware tracing, JTAG breakpoints, and sampling over heavy logging.
- Use assertion-based checks that only run in a sampled subset of runs or triggered after suspicious events.
- When needed, replicate the scenario on a simulator/emulator where heavy instrumentation won’t affect timing.
8. Confirm & fix
- Once source found, add deterministic unit tests, fix bounds/ownership errors, add durable per-module pools or hardened allocator, and keep regression tests with stress runs.
This approach balances broad detection, focused isolation, and minimal timing interference so the bug isn’t hidden by the debugger itself.
Describe the common memory types used in embedded systems (SRAM, DRAM, flash, EEPROM). For each type explain volatility, typical access speed, endurance, common uses (stack/heap/data/code), and how an IoT gateway design differs from a constrained MCU sensor node in memory usage.
Sample Answer
SRAM
- Volatility: volatile (loses content when power removed).
- Access speed: very fast, single-cycle on many MCUs.
- Endurance: effectively unlimited (read/write cycles not a concern).
- Common uses: stack, CPU registers, heap, runtime variables, small data buffers.
- Notes: used for deterministic access (real-time tasks).
DRAM
- Volatility: volatile, requires refresh.
- Access speed: slower than SRAM (higher latency), but higher density.
- Endurance: not a limiting factor.
- Common uses: system/main memory in gateways or SoCs where large RAM needed (not typical on small MCUs).
- Notes: needs refresh controller and more power.
Flash (NOR/NAND)
- Volatility: non-volatile.
- Access speed: read fast (NOR supports XIP), write slower, block erase required.
- Endurance: limited (typically 10k–100k erase cycles for NOR, varies).
- Common uses: code storage (firmware image), filesystem (NAND + FTL), large non-volatile data.
- Notes: wear-leveling and erase-block management required.
EEPROM
- Volatility: non-volatile.
- Access speed: slower writes than flash, byte- or page-programmable.
- Endurance: typically 100k–1M cycles depending on type.
- Common uses: small configuration parameters, calibration data, persistent settings.
- Notes: easier byte-level updates than flash, used for infrequent writes.
IoT Gateway vs Constrained MCU Sensor Node
- Gateway: has DRAM and larger flash/NAND + storage (SSD/SD). Uses OS, dynamic memory, file systems, complex networking stacks; memory footprint large, can trade latency for capacity.
- Constrained MCU: relies on SRAM + onboard flash (and sometimes EEPROM). Code often runs XIP or is copied to RAM; tight RAM footprint forces static allocation, stack/heap limits, minimal OS or RTOS; wear-management for flash critical.
- Design implications: gateways focus on throughput and concurrency; sensor nodes prioritize deterministic timing, low power, and careful flash/EEPROM write budgeting.
Find the length of the longest substring without repeating characters. Implement lengthOfLongestSubstring(s) in your preferred language and explain the sliding window approach, why it is O(n), and how you maintain character indices. Example: s = "abcabcbb" -> 3.
Sample Answer
Direct answer
Slide a window over the string while tracking, for each character, the index where it was last seen. When you hit a character already inside the current window, jump the window's left edge to just past that character's previous occurrence, then update the best length seen. Each index is visited a bounded number of times, so the whole scan is O(n).
Structured elaboration
The sliding window invariant
Maintain two indices, left and right, that always bound a substring with no repeated characters. right advances one character at a time. last_seen is a hash map from character to the most recent index where it appeared. When the character at right was last seen at an index that is still inside the current window (last_seen[ch] >= left), move left forward to last_seen[ch] + 1, the smallest new left edge that excludes the earlier occurrence. If the last occurrence is outside the window, or the character has never been seen, the window simply grows.
Why it is O(n)
right moves forward exactly once per character, n times total. left only ever moves forward too, and it never exceeds right, so across the whole scan left advances at most n times as well. Neither pointer ever moves backward. That gives a total of at most 2n pointer movements, so the algorithm is O(n) time, with O(min(n, alphabet size)) space for the hash map, bounded by how many distinct characters can appear in a window at once.
Maintaining character indices correctly
The subtle part is the last_seen[ch] >= left check. Without it, a stale entry from far in the past (before the current window even started) could incorrectly shrink the window, because the map keeps the LAST time a character was seen, which might be from before the window's current left edge if that character has not reappeared since.
Worked example
def length_of_longest_substring(s):
last_seen = {}
left = 0
best = 0
for right, ch in enumerate(s):
if ch in last_seen and last_seen[ch] >= left:
left = last_seen[ch] + 1
last_seen[ch] = right
best = max(best, right - left + 1)
return best
print(length_of_longest_substring("abcabcbb"))
Output:
3
Tracing it by hand: at right=3 the window holds "abc" and the next character is a, last seen at index 0, which is still inside the window (0 >= left=0), so left jumps to 1. The window is now "bca", length 3. The same pattern repeats through the rest of the string, and the best length never exceeds 3, matching the printed result and the question's own stated expectation. The same technique ports directly: a Java solution uses a HashMap<Character, Integer> in place of the dict, and a JavaScript solution uses a Map or a plain object, with identical pointer logic.
Trade-offs and pitfalls
A common wrong turn is checking if ch in last_seen alone and moving left on every repeat ever seen, even ones from before the current window, which can move left backward and silently break correctness. Another is recomputing "is this character in the current window" by scanning the window itself on every step, which turns the algorithm back into O(n^2) or O(n * alphabet size). For very large alphabets (full Unicode rather than ASCII), the map can grow larger, but it is still bounded by the number of distinct characters that actually appear, not by the input length.
Walk through the steps of a context switch in a preemptive RTOS triggered by an ISR. Include which registers are saved, where (task stack vs exception stack), how the scheduler decides the next task, and how the ISR requests a context switch (for example using PendSV on Cortex-M).
Sample Answer
Overview — when an interrupt triggers a preemptive context switch
Briefly: the CPU’s exception entry does the first level saving, the ISR requests a context switch (typically by setting PendSV), the scheduler picks the next ready task, and the PendSV handler completes the full context switch by saving/restoring the remaining registers and swapping the PSP.
1) Hardware (automatic) stacking on exception entry
- Cortex‑M hardware automatically pushes this “stack frame” onto the active stack:
- R0, R1, R2, R3, R12, LR (R14), PC (R15), xPSR
- That stack frame is placed on the stack in use by the interrupted thread (usually PSP when threads run); note FPU cores may also push S0–S15 and FPSCR (lazy stacking) if FP in use.
2) ISR context and requesting a switch
- The ISR runs (often on MSP or exception context) and must not perform the full task switch itself.
- To request a switch it sets the PendSV pending bit via the Interrupt Control and State Register (ICSR) — e.g., write to NVIC->ICSR = (1 << PENDSVSET) — or calls the RTOS API that does this.
- Setting PendSV defers the expensive stack manipulation until a lower-priority exception (PendSV) runs after all higher-priority IRQs complete.
3) Scheduler decision
- The ISR or RTOS scheduler computes the next task (priority-based, round‑robin, or RTOS-specific ready lists).
- Scheduler selects the highest-priority READY task; it updates TCBs but does not yet change CPU registers.
4) PendSV handler: full context switch
- PendSV runs in exception context. Typical sequence (assembly/C):
- Read current PSP (process stack pointer) — this points to the interrupted task’s stack frame.
- Push callee-saved registers R4–R11 (and FP callee-saved regs if used) onto the task stack at PSP. These are not saved by hardware and must be preserved.
- Save the new PSP to the current task’s TCB (store task stack pointer).
- Load the next task’s stack pointer from its TCB into PSP.
- Pop R4–R11 from the new task’s stack (restoring its callee-saved registers).
- Execute exception return (BX LR with EXC_RETURN value) which causes the CPU to pop the hardware-stacked frame (R0–R3,R12,LR,PC,xPSR) from the new task’s stack and resume thread mode in that task.
5) Where each register ends up
- Hardware-stacked frame (R0–R3,R12,LR,PC,xPSR): on the active thread stack (PSP) by the CPU on exception entry.
- Software-saved callee-saved (R4–R11): pushed by PendSV onto the task’s stack (PSP).
- Floating-point registers: handled by lazy stacking or explicitly saved/restored by RTOS if FPU used.
6) Notes and pitfalls
- Ensure PendSV priority is lowest so it runs after all IRQs.
- Use atomic operations or disable interrupts briefly when manipulating ready lists/TCBs.
- Pay attention to stack alignment (8‑byte aligned) and EXC_RETURN codes for correct return-to-thread behavior.
- FPU and lazy stacking require explicit handling to avoid corrupting FP state.
This sequence keeps ISR short, defers heavy work to PendSV, and ensures full CPU state is restored for the next task.
Implement an in-place algorithm in C that removes duplicate uint16_t elements from a statically allocated array of length N, using only O(1) additional memory. Assume N is small (e.g., <= 256) and runtime is more important than simplicity. Provide the function signature and explain complexity and worst-case behavior on an MCU with limited cycles.
Sample Answer
Approach (brief)
Sort the array in-place (fast lookup for duplicates), then sweep once to compact unique values. Use an in-place quicksort with median-of-three pivot and insertion sort for small partitions to avoid worst-case degeneration and reduce cycle count on an MCU. Additional memory is O(1) (fixed small stack for iterative quicksort).
#include <stdint.h>
#include <stddef.h>
/* Remove duplicates in-place. Returns new length (<= N). */
size_t dedup_uint16(uint16_t *a, size_t N);
/* Implementation */
static inline void swap_u16(uint16_t *x, uint16_t *y) { uint16_t t=*x; *x=*y; *y=t; }
size_t dedup_uint16(uint16_t *a, size_t N) {
if (N <= 1) return N;
/* Iterative quicksort with median-of-three and insertion sort threshold */
enum { THRESH = 16, STACK_SZ = 32 };
size_t lo_stack[STACK_SZ], hi_stack[STACK_SZ];
int sp = 0;
lo_stack[sp] = 0; hi_stack[sp++] = N-1;
while (sp) {
size_t lo = lo_stack[--sp], hi = hi_stack[sp];
while (hi - lo + 1 > THRESH) {
/* median-of-three */
size_t mid = lo + ((hi - lo) >> 1);
if (a[mid] < a[lo]) swap_u16(&a[mid], &a[lo]);
if (a[hi] < a[lo]) swap_u16(&a[hi], &a[lo]);
if (a[mid] < a[hi]) swap_u16(&a[mid], &a[hi]); /* pivot -> a[hi] */
uint16_t pivot = a[hi];
size_t i = lo, j = hi - 1;
while (1) {
while (i <= j && a[i] < pivot) ++i;
while (j >= i && a[j] >= pivot) --j;
if (i >= j) break;
swap_u16(&a[i++], &a[j--]);
}
swap_u16(&a[i], &a[hi]); /* place pivot */
/* push larger partition, continue with smaller to bound stack */
if (i - lo < hi - i) {
if (i + 1 < hi) { lo_stack[sp] = i + 1; hi_stack[sp++] = hi; }
hi = (i == 0) ? 0 : i - 1;
} else {
if (lo < i) { lo_stack[sp] = lo; hi_stack[sp++] = (i == 0) ? 0 : i - 1; }
lo = i + 1;
}
}
/* insertion sort on small range [lo..hi] */
for (size_t i = lo + 1; i <= hi; ++i) {
uint16_t v = a[i];
size_t j = i;
while (j > lo && a[j-1] > v) { a[j] = a[j-1]; --j; }
a[j] = v;
}
}
/* single pass to compact unique elements */
size_t write = 1;
for (size_t read = 1; read < N; ++read) {
if (a[read] != a[write-1]) a[write++] = a[read];
}
return write;
}
Complexity & MCU worst-case behavior
- Time: average O(N log N) for quicksort + O(N) sweep. Worst-case O(N^2) for quicksort avoided by median-of-three and small-partition insertion sort; highly unlikely for random/typical data. For N <= 256 this runs very fast; insertion sort threshold reduces swap overhead.
- Space: O(1) additional memory (fixed small stacks, constant).
- MCU notes: choose THRESH based on target cycles — smaller threshold favors insertion sort for tiny arrays and reduces recursive overhead. Median-of-three avoids pathological patterns; deterministic behavior helps real-time estimation. For strict worst-case guarantees, use in-place heapsort (O(N log N) worst-case) at cost of slightly more swaps.
Design an instrumented allocator for an embedded target that records allocation sizes, approximate callers, and high-water marks while keeping runtime overhead and RAM impact low. Describe data structures, sampling strategies, how to capture and compress return addresses, and an efficient way to report top-N memory consumers without logging every allocation.
Sample Answer
Approach (short)
I’d implement a low-overhead instrumented allocator that records sampled allocations into a compact in-RAM aggregate table keyed by a compressed caller fingerprint and size-bucket, tracks high-water marks, and uses an approximate top-N algorithm (space-saving) for reporting. Sampling + aggregation avoids logging every allocation.
Data structures
- Fixed-size aggregate table (power-of-two) of entries:
- struct Entry { uint32_t key; uint32_t count; uint32_t total_bytes; uint32_t max_bytes; uint32_t last_seen_epoch; }
- key = 32-bit fingerprint of caller (see compression) + size-bucket folded
- Global high-water mark per heap (uint32_t)
- Small reservoir buffer (N entries) optional to capture occasional full callsite for symbolication
Sampling strategy
- Probabilistic sampling on allocation: sample 1 in S allocations (configurable). Use simple LFSR or counter: if (++sample_counter & (S-1)) != 0 return; — no RNG syscall.
- For large allocations above threshold T always sample.
Capture & compress return addresses
- Capture single-level return address using compiler intrinsics: caller_ra = (uintptr_t)__builtin_return_address(0)
- Compress: compute 32-bit fingerprint = (ROTX32(mix64(caller_ra)) ^ size_bucket)
- Optionally delta-compress against previous fingerprint for reservoir entries. For symbolication, keep a tiny ring-reservoir of raw addresses (e.g., 64 entries) sampled via reservoir sampling to map fingerprints -> strings offline.
Aggregation & collision handling
- Use open-addressing hash with linear probe; when inserting, if key matches update counters; on empty slot insert. If table full, evict using the Space-Saving heuristic: replace lowest-count bucket and set count = evicted_count+1.
Top-N reporting
- Table already holds aggregated counts and total_bytes; run a scan to extract top-N by total_bytes using a small heap (O(M log N), M = table size). No per-allocation logging.
Implementation sketch (C-like)
// compact entry ~20 bytes (pad to 24)
typedef struct { uint32_t key; uint32_t count; uint32_t total; uint32_t max; uint32_t epoch; } entry_t;
entry_t table[TABLE_SZ];
uint32_t sample_mask = S-1; // S power of two
void instrument_malloc(size_t sz, void *ptr) {
high_water = max(high_water, current_heap_used());
if ((++sample_ctr & sample_mask) != 0 && sz < LARGE_T) return;
uintptr_t ra = (uintptr_t)__builtin_return_address(0);
uint32_t key = fingerprint(ra, size_bucket(sz));
table_insert_or_update(key, sz);
maybe_reservoir_add(ra);
}
Why this design
- Single RA capture is cheap (no stack-walk). Sampling + size threshold keeps runtime/RAM low. Fingerprinting gives compact keys; reservoir supports offline symbolication. Space-Saving avoids unlimited growth and gives accurate heavy-hitter detection.
Trade-offs & caveats
- Single-frame RA can conflate inlined wrappers; increase depth if frame-pointer available at cost. Hash collisions reduce precision — mitigate by occasional reservoir-backed resolution. Tune sampling rate and table size to RAM/accuracy needs.
Implement the design for compact nested or hierarchical state machines (HSM) in C to be used on an embedded system with severe RAM constraints. The HSM must support entry/exit actions, shallow history, prioritized events, and dispatching without dynamic allocation. Outline the data structures, table-driven dispatch algorithm, and provide example pseudocode for state transition dispatch.
Sample Answer
Approach (brief)
Design a compact table-driven HSM in C using arrays and enums only, no dynamic allocation. Represent states as small integers, tables for parent, entry/exit handlers, transition targets, guards and prioritized event-action lists. Support shallow history by storing last active child index.
Key data structures
- StateId: uint8_t enum
- EventId: uint8_t enum
- Handler: function pointer type
void (*)(void*) - Tables:
- parent[StateId]
- entry[StateId], exit[StateId]
- history[StateId] (uint8_t, 0 = no history)
- transitions: array of { state, event, guard_idx, action_idx, target_state }
- guards[], actions[] function pointer arrays
Example compact definitions:
typedef uint8_t StateId;
typedef uint8_t EventId;
typedef void (*Action)(void*);
typedef bool (*Guard)(void*);
typedef struct {
StateId src;
EventId evt;
uint8_t guard; // index into guards[], 0xFF = always true
uint8_t action; // index into actions[], 0xFF = none
StateId tgt; // target = same as src means internal
} Transition;
Table-driven dispatch algorithm (steps)
- Build vector of candidate transitions by scanning transitions[] for src = current state or any ancestor (walk parent[]).
- Prioritize by event and transition table ordering (table arranged highest priority first).
- Evaluate guards in ancestor-to-descendant order; pick first true.
- If internal transition (tgt == src) call action only.
- For external transition:
- Compute LCA between current and target using parent[].
- Execute exit handlers from current up to (but not including) LCA; update shallow history for parents.
- Execute transition action.
- Execute entry handlers from LCA down to target (if history exists, descend to recorded child).
- Set current = final entered state.
Pseudocode for dispatch
void dispatch(EventId e, void *ctx){
StateId s = current;
Transition *t = find_transition(s,e,ctx); // scan transitions[] up ancestors
if(!t) return;
if(t->tgt == s){ // internal
if(t->action!=0xFF) actions[t->action](ctx);
return;
}
StateId lca = find_lca(s, t->tgt);
// exit path
for(StateId x = s; x != lca; x = parent[x]){
if(exit[x]) exit[x](ctx);
if(parent[x]!=0xFF) history[parent[x]] = x; // shallow history
}
// action
if(t->action!=0xFF) actions[t->action](ctx);
// entry path stack from lca->tgt
StateId path[DEPTH]; int n=0;
for(StateId y = t->tgt; y != lca; y = parent[y]) path[n++]=y;
while(n--) {
StateId en = path[n];
if(entry[en]) entry[en](ctx);
// follow shallow history
if(history[en]) en = history[en];
}
current = resolve_deepest_active(t->tgt);
}
Complexity & constraints
- Memory: O(#states + #transitions). Tables are packed arrays of uint8_t and small structs.
- CPU: dispatch scans ancestor chain and transitions — acceptable when tables are small; optimize by indexing transitions by event to reduce scan.
Notes / Best practices
- Store tables in flash (const) to save RAM.
- Keep function pointer tables small; use a dispatcher index-to-fn map.
- Precompute parent chains or LCA hints for deep hierarchies if dispatch time critical.
- Use 8-bit types where feasible, align structs to avoid padding.
Want to create your own tailored preparation guide using our deep research?
Get Started for FreeInterview-Ready Courses
Visual-first, interactive, structured learning paths
Browse Embedded Developer jobs
AI-enriched listings across hundreds of company career pages
Explore Jobs