DoorDash Entry-Level Embedded Developer Interview Preparation Guide
DoorDash's entry-level embedded developer interview process (based on industry standards for this role and level) typically consists of an initial recruiter screening followed by a technical phone screen and multiple onsite rounds. The process evaluates foundational embedded systems knowledge, C/C++ coding ability, understanding of hardware constraints, and cultural fit. Interviews focus on fundamentals rather than advanced topics; candidates are expected to demonstrate learning ability and problem-solving approach rather than mastery.
Interview Rounds
Recruiter Screening
What to Expect
Initial conversation with a recruiter to assess basic fit, background, and interest in the embedded developer role. This round focuses on your educational background, any internships or relevant projects, familiarity with embedded systems concepts, and motivation for joining DoorDash. Expect questions about your experience with C/C++, microcontroller programming, hardware platforms you've worked with, and why you're interested in this specific role. The recruiter will also outline the interview process and answer logistical questions.
Tips & Advice
Be specific about your embedded systems experience; don't just say 'I know C++,' explain a project where you used it for embedded work. Show genuine interest in DoorDash's delivery logistics and how embedded systems might be involved in IoT or hardware solutions. Frame your motivation around learning and growing as an embedded engineer, not just job-seeking. Ask thoughtful questions about the team and the types of embedded systems you'd work on.
Focus Topics
Hardware Platforms and Development Boards
Describing experience with microcontroller boards (Arduino, STM32, ARM Cortex-M, etc.), development environments, and tools you've used.
Practice Interview
Study Questions
Motivation for DoorDash and the Role
Articulating why you're interested in embedded development at DoorDash, what aspects of the role appeal to you, and how it fits your career goals.
Practice Interview
Study Questions
Background and Embedded Systems Experience
Communicating your educational background, internships, coursework, or personal projects involving embedded systems, microcontrollers, or firmware development.
Practice Interview
Study Questions
C/C++ and Programming Language Familiarity
Discussing your hands-on experience with C and/or C++ in embedded contexts, including relevant projects and what you've built.
Practice Interview
Study Questions
Technical Phone Screen
What to Expect
A technical engineer (likely a mid-level or senior embedded engineer at DoorDash) conducts a 45-60 minute video call to assess your coding fundamentals, embedded systems understanding, and problem-solving approach. You'll be asked to write C/C++ code in a shared editor to solve embedded systems problems such as bit manipulation, memory-efficient algorithms, or simple hardware interface implementations. Expect one or two coding problems with increasing complexity. The focus is on your ability to think through constraints (memory, power, real-time requirements) and write efficient, correct code—not on solving obscure algorithmic challenges. The interviewer will likely ask follow-up questions about how you'd optimize, debug, or adapt your solution.
Tips & Advice
Start by asking clarifying questions about hardware constraints, performance requirements, and any assumptions you should make. Write code that is readable and includes comments; explain your approach before coding. When discussing embedded-specific challenges, mention constraints like RAM limitations, CPU cycles, or power draw. Use the shared editor effectively—show your thinking step-by-step. If you get stuck, say so and ask for hints; interviewers appreciate candidates who can work through problems collaboratively. Practice bit operations, memory management, and writing tight loops. Avoid over-engineering; a simple, correct solution is better than an elegant but incorrect one.
Focus Topics
Memory Management and Constraints
Understanding stack vs. heap, memory layout, data type sizes, pointer usage, and optimizing code for limited RAM environments typical of microcontrollers.
Practice Interview
Study Questions
Basic Hardware Interfaces and Register Access
Understanding GPIO, UART, SPI, I2C at a conceptual level; reading and writing to hardware registers; basic knowledge of interrupt handling.
Practice Interview
Study Questions
Problem-Solving Under Constraints
Identifying and reasoning about embedded-specific constraints (CPU speed, memory, power, real-time deadlines) and designing solutions that respect these limits.
Practice Interview
Study Questions
C/C++ Coding Fundamentals for Embedded Systems
Writing efficient, correct C/C++ code with emphasis on memory management, pointer arithmetic, bit operations, and avoiding common pitfalls like buffer overflows or undefined behavior.
Practice Interview
Study Questions
Bit Manipulation and Bitwise Operations
Understanding and manipulating individual bits, bit shifting, masking, and using bitwise operations to control hardware registers or optimize memory usage.
Practice Interview
Study Questions
Onsite Round 1: Embedded Systems Fundamentals
What to Expect
First onsite interview conducted by an embedded systems engineer focused on assessing your understanding of embedded systems core concepts. Expect a mix of conceptual questions and coding exercises. You may be asked about microcontroller architecture basics, the role of bootloaders and firmware, interrupt handling, memory organization, or how embedded systems differ from general-purpose computing. You might also solve a coding problem that reinforces these concepts, such as writing a simple state machine, handling interrupts safely, or implementing a circular buffer. The goal is to confirm you have solid foundational knowledge and can apply it correctly.
Tips & Advice
Be prepared to sketch or draw diagrams (e.g., memory layout, interrupt flow, peripheral interaction) to clarify your thinking. If you're unsure about a concept, say so and ask the interviewer to explain; entry-level candidates are expected to learn. Use correct terminology (e.g., distinguish between bootloader, firmware, and application code). When discussing interrupts, acknowledge the importance of interrupt safety and re-entrancy without over-complicating your answer. Show that you understand trade-offs: for example, why might you use polling vs. interrupts for a given scenario? Demonstrate curiosity by asking why certain design patterns are used in embedded systems.
Focus Topics
Memory Organization and Data Types
Understanding embedded memory layout (flash for program, RAM for runtime data), data type sizes on embedded platforms, and implications for portability and efficiency.
Practice Interview
Study Questions
Firmware, Bootloaders, and Startup Code
Understanding what firmware is, the role of bootloaders in initializing hardware, and how embedded programs start execution (reset vector, initialization before main).
Practice Interview
Study Questions
Polling vs. Interrupt-Driven Design
Understanding trade-offs between polling (checking status repeatedly) and interrupt-driven (responding to events) approaches, and when to use each.
Practice Interview
Study Questions
Interrupt Handling and Real-Time Concepts
Understanding interrupts, interrupt vectors, interrupt priority, and basic concepts like interrupt safety, re-entrancy, and avoiding blocking operations in ISRs.
Practice Interview
Study Questions
Microcontroller Architecture Basics
Understanding CPU architecture (ARM Cortex-M series common in DoorDash IoT contexts), memory hierarchy (flash, RAM, registers), and how processors fetch, decode, and execute instructions.
Practice Interview
Study Questions
Onsite Round 2: Low-Level Programming and Firmware
What to Expect
Interview with a senior embedded engineer focusing on low-level coding, firmware development, and your ability to work with hardware registers, peripheral drivers, and optimized code. You may be asked to implement a simple driver or peripheral interface (e.g., UART communication, GPIO control, timer setup), write assembly code snippets, or optimize an existing code snippet for speed or memory. Expect questions about compiler behavior, code generation, memory efficiency, and debugging techniques. This round tests your practical coding skills and understanding of how C/C++ maps to actual machine code.
Tips & Advice
Write clear, well-commented code that shows you understand what the hardware is doing. When writing driver-like code, define register addresses clearly using macros or structs. Explain your optimization rationale; for example, 'I'm using a static buffer to avoid stack overhead since RAM is limited.' Show familiarity with embedded development tools: debuggers (GDB), hardware analysis (oscilloscopes conceptually), and compiler flags. If asked about assembly, don't panic; you need only basic understanding of calling conventions and instruction flow. Discuss memory-mapped I/O, volatile keyword, and the importance of reading hardware documentation. Be honest about using reference materials in real development; show you know where to find information and how to apply it.
Focus Topics
Assembly Language Basics
Understanding basic ARM Cortex-M or x86 assembly syntax, calling conventions, registers, and how C code maps to assembly; reading and writing simple assembly snippets.
Practice Interview
Study Questions
Debugging Embedded Systems
Using debuggers (GDB, JTAG), setting breakpoints, inspecting registers and memory, understanding printf debugging in embedded contexts, and using hardware analysis tools conceptually.
Practice Interview
Study Questions
Code Optimization for Embedded Constraints
Optimizing code for low memory footprint, fast execution, or low power consumption; understanding compiler optimization flags, reducing function call overhead, inlining, and data structure choices.
Practice Interview
Study Questions
Hardware Register Access and Peripheral Control
Reading and writing memory-mapped registers to control peripherals, using bit masks and shifts, understanding volatile and const qualifiers in hardware contexts.
Practice Interview
Study Questions
Basic Driver Implementation and Peripheral Initialization
Writing simple drivers or initialization code for common peripherals (GPIO, UART, timers), understanding peripheral configuration, clock setup, and pin multiplexing basics.
Practice Interview
Study Questions
Onsite Round 3: Hardware-Software Integration and Problem-Solving
What to Expect
Interview with a hardware or systems engineer assessing your ability to understand hardware-software interactions, debug real-world embedded challenges, and think holistically about IoT or hardware solutions. You may receive a scenario describing a hardware problem (e.g., 'a sensor reading is noisy,' 'a device isn't responding,' 'power consumption is too high') and asked how you'd diagnose and solve it. Alternatively, you might design a simple embedded system solution given requirements. This round evaluates your ability to work cross-functionally with hardware engineers, understand signal integrity, timing, power constraints, and your problem-solving approach to ambiguous, real-world embedded challenges.
Tips & Advice
Ask clarifying questions about the hardware setup, power budget, performance requirements, and constraints before proposing solutions. Draw diagrams to visualize the system (e.g., block diagram, timing diagram). Break down problems methodically: for a sensor issue, consider hardware (connection, power, signal quality), firmware (configuration, calibration, filtering), or software (algorithm, timing). Discuss trade-offs openly: e.g., 'We could add a filter for noise but that introduces latency.' Show awareness of topics like signal integrity, impedance matching, pull-up/pull-down resistors, and debouncing without going too deep. Mention collaboration with hardware engineers as essential; you don't need all the answers. Demonstrate curiosity about the 'why' behind design decisions.
Focus Topics
Power Management and Energy Efficiency
Understanding power consumption sources (CPU, peripherals, communication), sleep modes, power optimization strategies, and trade-offs between performance and power.
Practice Interview
Study Questions
Troubleshooting Embedded System Problems
Systematic approach to diagnosing embedded issues: understanding where problems originate (hardware, firmware, integration), using tools for diagnosis, and proposing targeted solutions.
Practice Interview
Study Questions
Timing, Synchronization, and Real-Time Constraints
Understanding timing requirements, synchronization between hardware and software, real-time deadlines, and how to ensure predictable behavior.
Practice Interview
Study Questions
Sensor Integration and Data Acquisition
Understanding sensor types, interfacing with sensors via I2C, SPI, or analog inputs, calibration, filtering noisy sensor data, and interpreting sensor specifications.
Practice Interview
Study Questions
Signal Integrity and Hardware-Software Interaction Basics
Understanding how software interfaces with hardware signals, concepts like pull-up/pull-down resistors, debouncing, noise, and signal quality issues, and how firmware responds to hardware state.
Practice Interview
Study Questions
Onsite Round 4: Behavioral and Cultural Fit
What to Expect
Final onsite interview typically conducted by a hiring manager or team lead, focused on behavioral competencies, cultural alignment with DoorDash, and team fit. Expect STAR-format questions about handling challenges, learning from mistakes, collaborating with teammates, dealing with ambiguity, and your approach to quality and deadlines. This round assesses communication skills, teamwork, growth mindset, and whether you share DoorDash's values of execution, ownership, and bias toward action. You'll likely discuss your past projects, how you've contributed to a team, and your motivation for joining DoorDash.
Tips & Advice
Use the STAR method (Situation, Task, Action, Result) for behavioral questions; be specific and quantifiable where possible. Emphasize learning from failures and growing from feedback. Show humility as an entry-level candidate; highlight eagerness to learn from senior engineers. Discuss collaboration positively; mention times you worked well with teammates or how you incorporated feedback. Ask thoughtful questions about the team's culture, how they support junior engineers, and what success looks like. Be authentic; cultural fit is mutual—ensure the team and role align with your values and growth goals. Avoid generic answers; tie your experiences directly to DoorDash's mission around delivery logistics and IoT/hardware solutions.
Focus Topics
Passion for Embedded Systems and DoorDash's Mission
Articulating genuine interest in embedded development, IoT, or hardware-software interaction, and connecting that to DoorDash's delivery logistics and innovation.
Practice Interview
Study Questions
Handling Ambiguity and Executing with Incomplete Information
Sharing examples of working on projects with unclear requirements or constraints, how you clarified goals, and how you moved forward.
Practice Interview
Study Questions
Ownership and Attention to Quality
Discussing times you took ownership of a problem, ensured quality in your work (testing, code review), and followed through on commitments.
Practice Interview
Study Questions
Collaboration and Teamwork in Technical Environments
Sharing examples of working effectively with teammates, incorporating feedback, asking for help when needed, and contributing to a positive team dynamic.
Practice Interview
Study Questions
Learning from Failures and Technical Challenges
Discussing a past technical problem you faced, how you debugged it, what you learned, and how that improved your engineering approach.
Practice Interview
Study Questions
Frequently Asked Embedded Developer Interview Questions
What is endianness and how does it affect serialising a 32-bit integer to bytes or reading it from a network packet? Write code that writes and reads a 32-bit value in a fixed byte order independent of the host.
Sample Answer
What endianness is
Memory and network packets hold bytes, but a 32-bit integer has four of them. Endianness is the order in which those bytes are laid out. The most significant byte (MSB) carries the biggest digits of the number; the least significant byte (LSB) the smallest.
| Order | First byte stored | 0x0A0B0C0D in memory |
|---|---|---|
| big-endian | MSB | 0A 0B 0C 0D |
| little-endian | LSB | 0D 0C 0B 0A |
Two words used below: the host is the machine running your program and the wire is the byte sequence in the packet or file. Most desktop, phone and server CPUs run little-endian. Internet protocol headers carry multi-byte fields in big-endian order, called network byte order (POSIX provides htonl and ntohl to convert). A byte array that arrives from a network or a file has a defined order that does not care what CPU reads it.
How it affects serialising and parsing
If you copy the in-memory bytes of an integer into a packet (memcpy(buf, &v, 4), or *(uint32_t *)buf = v), the packet's byte order silently becomes the host's. A little-endian sender and a big-endian receiver then disagree on every field. Casting a packet buffer to uint32_t * to read it is worse: it reads the host's order, can fault on a misaligned address on some CPUs, and breaks C's aliasing rules (the compiler may assume a uint32_t * never points at uint8_t data), which is undefined behaviour (the C standard places no requirement on what the program then does, so the compiler may assume it never happens, and the result can be a crash, a wrong value, or a value that looks right). A misaligned address is one that is not a multiple of the type's size: a uint32_t is expected to start at an address divisible by 4, and a packet buffer may start anywhere.
The host-independent fix: shifts
Shifts and masks act on the value of a number, not on how the CPU stores it. v >> 24 is always the most significant byte of v, on any machine. For the value 0x0A0B0C0D: v >> 24 is 0x0A; v >> 16 is 0x0A0B, and & 0xFF (keep only the low byte) leaves 0x0B; v >> 8 is 0x0A0B0C, giving 0x0C after the mask; v itself ends in 0x0D. The cast (uint8_t) keeps only the low 8 bits, so it does that masking for you. Those four bytes in that order are the big-endian form on any host. So write each byte out by value, and rebuild the value from bytes by value:
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void put_u32_be(uint8_t out[4], uint32_t v) {
out[0] = (uint8_t)(v >> 24);
out[1] = (uint8_t)(v >> 16);
out[2] = (uint8_t)(v >> 8);
out[3] = (uint8_t)v;
}
uint32_t get_u32_be(const uint8_t in[4]) {
return ((uint32_t)in[0] << 24) | ((uint32_t)in[1] << 16) |
((uint32_t)in[2] << 8) | (uint32_t)in[3];
}
void put_u32_le(uint8_t out[4], uint32_t v) {
out[0] = (uint8_t)v;
out[1] = (uint8_t)(v >> 8);
out[2] = (uint8_t)(v >> 16);
out[3] = (uint8_t)(v >> 24);
}
uint32_t get_u32_le(const uint8_t in[4]) {
return (uint32_t)in[0] | ((uint32_t)in[1] << 8) |
((uint32_t)in[2] << 16) | ((uint32_t)in[3] << 24);
}
/* The tempting version: the uint8_t is promoted to a signed int before << 24. */
uint32_t get_u32_be_buggy(const uint8_t in[4]) {
return (uint32_t)(in[0] << 24 | in[1] << 16 | in[2] << 8 | in[3]);
}
static void show(const char *label, const uint8_t b[4]) {
printf("%-22s %02X %02X %02X %02X\n", label, b[0], b[1], b[2], b[3]);
}
int main(void) {
uint32_t v = 0x0A0B0C0Du;
uint8_t be[4], le[4], host[4];
put_u32_be(be, v);
put_u32_le(le, v);
memcpy(host, &v, 4);
show("big-endian (network)", be);
show("little-endian", le);
show("raw host memory", host);
printf("read back BE %08X, LE %08X\n", get_u32_be(be), get_u32_le(le));
printf("BE bytes read as LE: %08X\n", get_u32_le(be));
srand(7);
for (int i = 0; i < 100000; i++) {
uint32_t r = ((uint32_t)rand() << 16) ^ (uint32_t)rand() ^ ((uint32_t)rand() << 31);
uint8_t a[4], b[4];
put_u32_be(a, r);
put_u32_le(b, r);
if (get_u32_be(a) != r || get_u32_le(b) != r || a[0] != b[3] || a[3] != b[0]) {
printf("FAIL %08X\n", r);
return 1;
}
}
uint32_t edges[] = {0u, 0xFFFFFFFFu, 0x80000000u, 1u};
for (int i = 0; i < 4; i++) {
uint8_t a[4];
put_u32_be(a, edges[i]);
if (get_u32_be(a) != edges[i]) { printf("FAIL edge\n"); return 1; }
}
printf("round trips ok\n");
uint8_t high[4] = {0x80, 0x00, 0x00, 0x01};
printf("buggy read of 80 00 00 01: %08X\n", get_u32_be_buggy(high));
return 0;
}
Output
Compiled with gcc -std=c11 -Wall -Wextra -fsanitize=undefined (GCC 14.2, aarch64 Linux, a little-endian host), stdout is:
big-endian (network) 0A 0B 0C 0D
little-endian 0D 0C 0B 0A
raw host memory 0D 0C 0B 0A
read back BE 0A0B0C0D, LE 0A0B0C0D
BE bytes read as LE: 0D0C0B0A
round trips ok
buggy read of 80 00 00 01: 80000001
The raw host memory line is the only host-dependent line: a big-endian machine would print 0A 0B 0C 0D there. The last line, buggy read, comes from undefined behaviour: on this build the bytes 80 00 00 01 happen to come back as 80000001, but the C standard does not promise that, and another compiler, optimisation level or CPU may print a different value or compile the function differently. The two serialisers print the same bytes on every host. "BE bytes read as LE" shows the symptom of an order mismatch: 0A0B0C0D arrives as 0D0C0B0A.
UBSan (the undefined-behaviour sanitizer, a set of run-time checks that -fsanitize=undefined adds) also writes this line to stderr, from the deliberately wrong get_u32_be_buggy:
endian_io.c:32:29: runtime error: left shift of 128 by 24 places cannot be represented in type 'int'
The common mistake the sanitizer caught
in[0] << 24 looks fine, but C first promotes uint8_t (widens any type narrower than int) to a signed int before the shift, and 0x80 << 24 is 2^31, which does not fit in a 32-bit int, so it is undefined behaviour in C. The correct-looking printed value is the trap: the program does not fail visibly, so only the sanitizer message (or a different compiler) reveals the bug, and code like this can break after a compiler upgrade. Cast to uint32_t first, as get_u32_be does. Other mistakes: using htonl on a buffer of bytes instead of a value, reading a signed field by shifting into an int (read as uint32_t, then convert), and assuming sizeof(int) is 4 (use uint32_t).
Complexity and edge cases
Each function does a fixed handful of shifts and ORs: O(1) time, O(1) space. The run checks 100,000 random values plus 0, 1, 0x80000000 and 0xFFFFFFFF for a round trip through both orders, and that the big- and little-endian outputs are byte reversals of each other. No host-specific code is involved, so the same source runs unchanged on a big-endian target.
Running the code
gcc -std=c11 -Wall -Wextra -fsanitize=undefined -o endian_io endian_io.c && ./endian_io
Your control loop meets its deadline on the bench but a reviewer says you cannot trust that on this processor. Explain how caches, branch prediction and pipelines make the worst case hard to pin down, and what you can do about each.
Sample Answer
Why the reviewer is right. A bench run shows the time of the paths and hardware states that the bench happened to produce. On a processor with caches, a branch predictor and a pipeline, the time of the same code depends on state left behind by earlier code: what is in the cache, what the predictor has learned, what is still in flight. The worst state may never occur on the bench, so a passing bench shows only that the exercised states met the deadline. Each effect has a mechanism, a way to bound it and a way to remove it.
A model of the three effects. The program below is a software model with assumed costs (a load costs 1 cycle on a cache hit and 20 on a miss; a mispredicted branch costs 10 extra cycles), not a measurement of any chip. It is deterministic. Run in a python:3.12-slim container (CPython 3.12), it prints:
import random
# Model parameters (assumptions for illustration, not a specific chip):
HIT_CYCLES, MISS_CYCLES = 1, 20 # one load: cache hit vs miss to slow memory
LINE, SETS = 32, 128 # 32-byte lines, 128 sets, direct-mapped = 4 KiB cache
class DirectMappedCache:
def __init__(self):
self.tags = [None] * SETS
self.misses = self.hits = 0
def access(self, addr):
line = addr // LINE
s, tag = line % SETS, line // SETS
if self.tags[s] == tag:
self.hits += 1
else:
self.tags[s] = tag
self.misses += 1
def cycles(self):
return self.hits * HIT_CYCLES + self.misses * MISS_CYCLES
def dot_product(cache, base_a, base_b, n=256):
for i in range(n): # reads a[i] and b[i], 4-byte elements
cache.access(base_a + 4 * i)
cache.access(base_b + 4 * i)
def report(label, cache):
print(f"{label:46} hits={cache.hits:4} misses={cache.misses:4} cycles={cache.cycles():6}")
print("1) Same code, same data, different placement of b (4 KiB cache):")
for label, base_b in (("b placed 4096 bytes after a (same cache sets)", 4096),
("b placed 4096+1024 bytes after a", 4096 + 1024)):
c = DirectMappedCache()
dot_product(c, 0, base_b)
report(label + ", cold", c)
print("\n2) Cold versus warm versus evicted by another task (non-conflicting placement):")
c = DirectMappedCache()
dot_product(c, 0, 5120); report("first run (cold)", c)
c.hits = c.misses = 0
dot_product(c, 0, 5120); report("second run (warm, what a bench loop times)", c)
for addr in range(0x10000, 0x10000 + 4096, LINE): # a higher-priority task sweeps 4 KiB
c.access(addr)
c.hits = c.misses = 0
dot_product(c, 0, 5120); report("after a task sweeps 4 KiB between runs", c)
# Branch prediction: one branch site, 2-bit saturating counter (0,1 predict not taken; 2,3 taken)
BRANCH_PENALTY = 10 # cycles lost per misprediction (assumed)
def mispredicts(outcomes):
state, wrong = 2, 0
for taken in outcomes:
if (state >= 2) != taken:
wrong += 1
state = min(3, state + 1) if taken else max(0, state - 1)
return wrong
print("\n3) Branch `if (x >= 128)` over 10,000 elements (2-bit predictor):")
rng = random.Random(7)
data = [rng.randrange(256) for _ in range(10_000)]
for label, xs in (("unsorted data", data), ("sorted data", sorted(data))):
m = mispredicts([x >= 128 for x in xs])
print(f"{label:14} mispredictions={m:5} extra cycles={m * BRANCH_PENALTY}")
1) Same code, same data, different placement of b (4 KiB cache):
b placed 4096 bytes after a (same cache sets), cold hits= 0 misses= 512 cycles= 10240
b placed 4096+1024 bytes after a, cold hits= 448 misses= 64 cycles= 1728
2) Cold versus warm versus evicted by another task (non-conflicting placement):
first run (cold) hits= 448 misses= 64 cycles= 1728
second run (warm, what a bench loop times) hits= 512 misses= 0 cycles= 512
after a task sweeps 4 KiB between runs hits= 448 misses= 64 cycles= 1728
3) Branch `if (x >= 128)` over 10,000 elements (2-bit predictor):
unsorted data mispredictions= 5012 extra cycles=50120
sorted data mispredictions= 3 extra cycles=30
1. Caches. A cache is a small fast memory that holds recently used lines of main memory (here 32 bytes each). Access time depends on whether the line is present. Three things make the worst case hard to pin down:
- Placement. In a direct-mapped cache each memory line can live in exactly one slot (a set), chosen by its address, and a stored tag (the remaining address bits) says which line currently occupies it; two lines with the same set number evict each other. The dot product reads a[i] and b[i] together; if b sits 4096 bytes after a, both map to the same sets and evict each other on every access: 512 misses and 10,240 cycles, against 64 misses and 1,728 cycles when b is shifted by 1 KiB. The code is identical; only the linker's placement (the choice of addresses the linker gives the arrays) changed.
- History. The second run of a loop is faster than the first (512 cycles against 1,728) because the data is already cached. A bench that loops the code and reports the average, or the best, is timing the warm case.
- Preemption. When a higher-priority task runs between two runs of this one and touches 4 KiB, the lines are gone and the task pays the cold cost again (1,728 cycles). That extra reload time is the cache-related preemption delay (the time to refill the lines the preempting task evicted), and a response-time analysis that uses the warm figure ignores it.
What to do: analyse the code with a static cache analysis (it classifies each access as a guaranteed hit, a possible miss or a guaranteed miss), or take the cache out of the question for the critical code by locking its lines, placing code and data in tightly coupled memory (Arm's name for on-chip RAM wired directly to the core) or scratchpad (the general term for software-managed fast on-chip RAM), neither of which is a cache and both of which have fixed access timing as long as no other bus master (DMA, a second core) can contend for the same memory, which you must check against that memory's arbitration, partitioning the cache between tasks, or disabling it. Add the reload cost per preemption to the response-time analysis. A cold-cache run is a useful sanity figure but not a proof.
2. Branch prediction. A pipelined core guesses which way a branch goes and fetches ahead; a wrong guess throws that work away. The model's 2-bit predictor keeps one counter per branch with values 0 to 3: values 0 and 1 predict "not taken", 2 and 3 predict "taken", each taken outcome adds 1 (up to 3) and each not-taken outcome subtracts 1 (down to 0), starting at 2. On sorted data the branch is not taken for the first half and taken for the second: the start is wrong once (counter 2 predicts taken, falls to 1), the later switch is wrong twice (counter 0 up to 1, then 1 up to 2, both still predicting not taken, and then right), so 3 mispredictions in all. Random data flips the outcome about half the time, which no counter can follow. The predictor sees a branch on x >= 128 over 10,000 values: 5,012 mispredictions on unsorted data against 3 on sorted data, which is 50,120 extra cycles against 30 for identical code. The cost therefore depends on the data. What to do: make the slowest path data-independent where you can (branch-free or constant-time code, for example adding the value times the 0-or-1 comparison result instead of using an if; check the compiler output, because it may turn that back into a branch); choose a core whose branch timing is fixed or simple; or let the analysis charge every uncertain branch as mispredicted, which is safe and pessimistic.
3. Pipelines. Overlapping instructions means an instruction's time depends on its neighbours (data hazards, stalls waiting for memory). The sharper problem is the timing anomaly (Lundqvist and Stenstrom, 1999): a locally faster event, such as a cache hit instead of a miss, can make the whole sequence slower. A small illustrative case, deterministic and with assumed latencies: a core has two units, a load unit that runs A and E and an ALU that runs B, C and D. Each cycle a free unit starts the earliest instruction, in program order A, B, C, D, E, that it can run and whose inputs are ready. C needs A's result and E needs D's result; B and D need nothing. A is a load that takes 1 cycle on a cache hit or 4 on a miss; B takes 2, C takes 1, D takes 4 and E takes 3. Run in a python:3.12-slim container (CPython 3.12), this program prints the schedule for both cases:
# Two units: the load unit runs A and E, the ALU runs B, C and D.
# Each cycle a free unit starts the earliest instruction (in program order) that it can run and whose inputs are ready.
UNIT = {"A": "load", "B": "alu", "C": "alu", "D": "alu", "E": "load"}
NEEDS = {"A": [], "B": [], "C": ["A"], "D": [], "E": ["D"]}
LAT = {"B": 2, "C": 1, "D": 4, "E": 3}
def run(a_cycles):
lat = dict(LAT, A=a_cycles)
done, start, free, pending, t = {}, {}, {"load": 0, "alu": 0}, list("ABCDE"), 0
while pending:
for unit in ("load", "alu"):
if free[unit] <= t:
for ins in pending:
if UNIT[ins] == unit and all(done.get(d, 10**9) <= t for d in NEEDS[ins]):
start[ins], done[ins] = t, t + lat[ins]
free[unit] = done[ins]
pending.remove(ins)
break
t += 1
return start, done
for label, a in (("A hits (1 cycle)", 1), ("A misses (4 cycles)", 4)):
start, done = run(a)
order = sorted(start, key=start.get)
print(f"{label:20} total {max(done.values())} cycles; " + ", ".join(f"{i} {start[i]}-{done[i]}" for i in order))
A hits (1 cycle) total 10 cycles; A 0-1, B 0-2, C 2-3, D 3-7, E 7-10
A misses (4 cycles) total 9 cycles; A 0-4, B 0-2, D 2-6, E 6-9, C 6-7
| Case | Load unit | ALU | Total |
|---|---|---|---|
| A hits | A (0-1), idle, E (7-10) | B (0-2), C (2-3), D (3-7) | 10 cycles |
| A misses | A (0-4), idle, E (6-9) | B (0-2), D (2-6), C (6-7) | 9 cycles |
On the hit, A's result arrives at cycle 1, so C is ready when the ALU frees at cycle 2 and, being earlier in program order than D, it runs first; D, which E is waiting for, starts at cycle 3 and ends at 7, so E starts at 7. On the miss, C is not ready at cycle 2 (A is still loading), so the ALU starts D at once, D ends at 6 and E runs 6-9. The faster load makes the run one cycle slower overall. Because of this, an analysis cannot simply assume the local worst case everywhere and trust the total; it has to follow every alternative. A core where that cannot happen (so that adding up the per-event worst cases is safe) is called timing-compositional; simple in-order cores are the usual route to one, though being in-order does not by itself guarantee it. Memory wait states and a bus shared with DMA (hardware that moves data without the CPU) add stalls of the same kind.
What to give the reviewer. Do not present the bench figure as the WCET. Give a bound for the exact binary and hardware configuration, from a static analysis (or hybrid analysis) that models the cache, predictor and pipeline, or reduce the hardware's variability first (cache locked or off for the critical code, critical code and data in scratchpad, simple core) so the analysis is tractable. Keep the bench as a cross-check under the nastiest conditions you can build: cold caches, DMA running, interrupts firing. The measured maximum must stay below the bound; if it does not, the model is wrong.
Design an end-to-end low-noise measurement system for a small-amplitude analog sensor (for example, a 10 mV signal) targeting effective 16-bit ADC resolution. Discuss ADC selection (SAR vs delta-sigma), analog front-end (amplification, filtering, input impedance), PCB layout best practices (grounding, star returns, separation of analog/digital sections), supply decoupling, reference selection and buffering, and firmware techniques (oversampling, calibration and offset/gain correction) needed to reach the target noise floor.
Sample Answer
Clarify goal & constraints
- Sensor amplitude = 10 mV. Target effective 16‑bit resolution referenced to a 2.048 V FS gives LSB ≈ 2.048 V / 2^16 = 31.25 µV. So input-referred RMS noise must be significantly below ~30 µV (target ~10–15 µV RMS to allow margin).
High-level choice: ADC
- Prefer delta‑sigma ADC for best input noise and integrated digital filtering (e.g., ADS1220 family for low-rate, AD7768/AD712 for higher throughput). SAR can work if front-end settles and you implement high-performance filters; SARs typically need more analog anti-alias filtering and precision PGA.
- Recommendation: delta‑sigma with internal PGA (gain selectable to avoid saturating) and >18–24 bit nominal resolution, ENOB at target sample rate ≥16 bits.
Analog front-end
- Gain: amplify 10 mV to near FS. For 2.048 V FS, use gain ≈ 205. Implement as two stages: low-noise instrumentation preamp (CMRR for differential sensor) then precision buffer/PGA.
- Op-amps: select low input‑noise (en < 1 nV/√Hz), low offset, low bias (examples: AD8610/ADA4528/OPA188 family depending on frequency). Use chopper/opamp only if offset drift is an issue.
- Filtering: anti‑alias RC or active low-pass. For delta‑sigma less aggressive, but include input RC and second‑order filter to limit bandwidth to reduce integrated noise. Target bandwidth so that noise integrated < ~10–15 µV RMS.
- Input impedance: high relative to sensor; ensure source drive can handle input bias currents. Use input buffer if sensor source impedance is high.
- Layout: keep gain stage as close as possible to sensor, short traces, guarding for high-impedance nodes.
Reference & buffering
- Use low-noise, low‑drift reference (ADR4525/REF5030 or similar) with PSRR. Buffer reference with low-noise op amp if ADC requires it or use internal ref if high-quality.
- Place reference decoupling and backup caps close to ADC ref pins.
PCB layout & grounding
- Separate analog and digital domains with single star point near ADC/reference return.
- Short, direct analog traces; avoid digital signals crossing analog areas.
- Stitch grounds with vias; use plane splits only if necessary and return digital currents on digital plane.
- Place analog components (op amps, ADC, ref) close together; route sensitive traces away from clock lines.
- Use guard rings around high‑impedance inputs and thermally stable placement for precision resistors.
Supply decoupling & power
- Local decoupling: 0.1 µF + 10 µF near pins; add 100 nF + 1 µF for LDOs. Use low-noise LDOs for analog rails.
- Sequencing: ensure ADC/reference stable before sampling.
Firmware techniques
- Oversampling and decimation: increase SNR by OSR; e.g., 4× oversampling improves SNR by 6 dB (1 bit). Use digital filter taps from ADC or implement custom CIC+FIR if needed.
- Averaging with care (avoid correlated noise): use power-spectral analysis to set sample windows and filters.
- Calibration: two-point calibration (offset & gain) at start-up and periodic. Store coefficients and apply in fixed-point math. Temperature compensation: measure local temp and apply polynomial correction.
- Offset/gain correction: collect known reference measurements (short to ground, apply known voltage) and compute linear correction: V_corrected = (V_raw - offset) * gain_scale.
- Monitor ADC status, CRC, and perform self-test sequences; use synchronous sampling if multiple channels.
Example numbers
- ADC: AD7768 at 2 kSPS, ENOB ~18 at chosen bandwidth OR ADS1220 (24‑bit, low‑rate) for simpler MCU integration.
- Front-end noise target: keep Input‑referred noise < 10–15 µV RMS. With amplifier en = 1 nV/√Hz and bandwidth 1 kHz, integrated noise ≈ en * sqrt(BW) ≈ 1 nV * sqrt(1000) ≈ 31.6 nV — after gain 205 gives ~6.5 µV — meets target.
Trade-offs
- Delta‑sigma: simpler analog F/E, high resolution, limited high-rate throughput and higher latency.
- SAR: lower latency and power, needs cleaner analog anti‑alias filtering and precision PGA.
Implement incremental validation: measure noise at each node (sensor out, post‑amp, ADC input) with spectrum analyzer, iterate filter bandwidth and gain, then tune firmware oversampling and calibration.
Profiling shows 40% of CPU time going to a naive byte-by-byte memcpy in C on an ARM Cortex-M4 with a 32-bit bus. How would you speed this up? Provide an optimized C implementation that handles both aligned and unaligned buffers, and explain the cache and alignment trade-offs behind your choices.
Sample Answer
Approach
A byte-by-byte memcpy on a microcontroller unit (MCU) with a 32-bit bus issues one bus transaction per byte. Each transaction pays the same fixed overhead (address setup plus wait states) whether it moves 1 byte or 4, so a byte loop is using at most a quarter of the bus's real capacity per transfer. The fix is to move data 4 bytes (one word) at a time whenever alignment allows, and fall back to byte copies only for the unaligned edges.
Cortex-M4 cores generally have no data cache (that shows up on Cortex-M7 and above), so this optimization is not about cache locality at all. It is purely about cutting the number of bus transactions. Some silicon (e.g. ST's ART Accelerator) adds a flash prefetch buffer, but that speeds up instruction fetch from flash, not this data copy.
Alignment matters, though not for the reason it's easy to assume. Cortex-M0/M0+ (ARMv6-M) parts always fault on an unaligned word or halfword access, there's no way to enable support for it. Cortex-M3/M4/M7 (ARMv7-M), the family named in this question, are different: by default (the CCR.UNALIGN_TRP bit is 0 at reset) a single-register LDR/STR to an unaligned address is handled transparently in hardware, it does not fault, it just costs extra bus cycles for the split access. A project can set UNALIGN_TRP to make the M4 trap on unaligned accesses instead, and multi-register transfers (LDM/STM) and exclusive accesses (LDREX/STREX) fault on misalignment regardless of that bit's setting. So on the Cortex-M4 in this question, the naive byte loop wasn't faulting, it was just slow, one bus transaction per byte; the word-aligned implementation below is still the right call, because it avoids the extra per-access bus cycles unaligned hardware accesses cost, and it stays correct if this code is ever reused on an M0/M0+ part or built with UNALIGN_TRP set. It word-aligns the destination first with a byte-wise prefix, does the bulk of the copy as words, then mops up a trailing partial word byte-by-byte. If the source and destination have different alignment offsets relative to each other, no single shift makes both word-aligned at once, so it has to fall back to a safe byte copy for that case.
#include <stdint.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
void *fast_memcpy(void *dst, const void *src, size_t n) {
uint8_t *d = (uint8_t *)dst;
const uint8_t *s = (const uint8_t *)src;
/* Different alignment offsets: can't word-align both, use safe byte copy. */
if (((uintptr_t)d & 3) != ((uintptr_t)s & 3)) {
while (n--) *d++ = *s++;
return dst;
}
/* Byte-copy the unaligned prefix until dst is 4-byte aligned. */
while (((uintptr_t)d & 3) != 0 && n > 0) {
*d++ = *s++;
n--;
}
/* Bulk-copy full words: 1 bus transaction per 4 bytes instead of 1 per byte. */
uint32_t *d32 = (uint32_t *)d;
const uint32_t *s32 = (const uint32_t *)s;
size_t words = n / 4;
while (words--) *d32++ = *s32++;
/* Byte-copy the trailing remainder. */
d = (uint8_t *)d32;
s = (const uint8_t *)s32;
n = n % 4;
while (n--) *d++ = *s++;
return dst;
}
#define BUF_LEN 64
static int run_case(const char *name, size_t src_off, size_t dst_off, size_t n) {
uint8_t backing_src[BUF_LEN + 8];
uint8_t ref[BUF_LEN + 8];
uint8_t got[BUF_LEN + 8];
for (size_t i = 0; i < sizeof(backing_src); i++) backing_src[i] = (uint8_t)(i * 37 + 11);
memset(ref, 0xAA, sizeof(ref));
memset(got, 0xAA, sizeof(got));
memcpy(ref + dst_off, backing_src + src_off, n);
fast_memcpy(got + dst_off, backing_src + src_off, n);
int match = memcmp(ref, got, sizeof(ref)) == 0;
printf("%-28s src_off=%zu dst_off=%zu n=%2zu -> %s\n",
name, src_off, dst_off, n, match ? "MATCH" : "MISMATCH");
return match;
}
int main(void) {
int all_ok = 1;
all_ok &= run_case("aligned/aligned", 0, 0, 23);
all_ok &= run_case("matching-offset unaligned a", 1, 1, 23);
all_ok &= run_case("matching-offset unaligned b", 2, 2, 23);
all_ok &= run_case("mismatched offsets", 1, 2, 23);
all_ok &= run_case("zero length", 0, 0, 0);
all_ok &= run_case("shorter than one word", 0, 0, 3);
printf("all cases matched standard memcpy: %s\n", all_ok ? "yes" : "no");
return 0;
}
Output
aligned/aligned src_off=0 dst_off=0 n=23 -> MATCH
matching-offset unaligned a src_off=1 dst_off=1 n=23 -> MATCH
matching-offset unaligned b src_off=2 dst_off=2 n=23 -> MATCH
mismatched offsets src_off=1 dst_off=2 n=23 -> MATCH
zero length src_off=0 dst_off=0 n= 0 -> MATCH
shorter than one word src_off=0 dst_off=0 n= 3 -> MATCH
all cases matched standard memcpy: yes
Compiled with clang -Wall -Wextra -O2 and run unchanged: these are exactly the six cases described (aligned/aligned, matching-offset unaligned at two different offsets, mismatched offsets, zero length, and a length shorter than one word), each diffed byte-for-byte against the standard library memcpy on the same buffers, and every case matched.
Key points
- The win is bus-transaction count, not cache behavior: 4x fewer transactions for the aligned bulk region.
- A secondary lever on top of this is unrolling the word loop (copy 2 or 4 words per iteration) to amortize loop-branch overhead further, worth it once the byte-by-byte cost is already fixed.
- For large, infrequent copies, offloading to a DMA (direct memory access) controller is the next step up: it moves the copy off the CPU entirely, but has its own setup latency and interrupt/polling overhead, so it only pays off above some size threshold, not for small copies.
Complexity
Same O(n) as the naive version, this is a constant-factor optimization: roughly 4x fewer bus transactions for the aligned middle section, at the cost of a small fixed amount of extra branching logic for the prefix/suffix handling.
Edge cases
- Source and destination overlapping (this implementation assumes non-overlapping regions, like standard
memcpy; overlapping regions needmemmovesemantics with direction-aware copying). - Very short copies (fewer than 4 bytes) where the word loop never runs and the whole thing degenerates to the safe byte path, which is correct but gets none of the speedup.
- Mismatched alignment between source and destination, which forces the byte-by-byte fallback for the entire copy, not just the edges.
What boundaries do you keep in mind as a mentor, things like confidentiality, favoritism, or mentoring someone you might later have to evaluate? Has a boundary ever gotten blurry in practice?
Sample Answer
Direct answer
Three boundaries matter most: confidentiality (what's said in a mentoring conversation doesn't automatically become input to a performance review), favoritism (visible time and opportunity should be fair and roughly trackable, not just felt fair), and the evaluator conflict (mentoring someone you also formally evaluate creates a structural incentive problem, since they cannot be fully candid with someone who scores them). The practical fix is not avoiding all overlap, since it is often unavoidable, it is naming the boundary explicitly up front and having a plan for when it blurs.
Structured elaboration
| Boundary | Why it's a real risk | Practical mitigation |
|---|---|---|
| Confidentiality | A mentee shares something (a mistake, a personal issue, career doubt) expecting it stays private | Agree upfront on what's confidential versus what you're obligated to escalate (safety, harassment), and say that out loud at the start, not after something sensitive comes up |
| Favoritism and access | Whoever you mentor gets disproportionate visibility, opportunities, and informal coaching | Keep mentoring reasonably visible, share or rotate access rather than hoarding one high-potential mentee, and disclose the relationship when it's relevant to a decision |
| Evaluator conflict | The mentee can't be candid about struggles with someone who also writes their review | Separate the conversations explicitly ("this 1:1 is mentoring, not eval input"), and when something material later needs to inform an evaluation, go back to them first instead of silently absorbing it into a review they didn't know was informed by it |
Worked example
Situation: I was mentoring someone I would later be asked to give calibration input on for a promotion cycle.
Task: keep the mentoring relationship honest without compromising the integrity of the eventual evaluation.
Action: I told them directly, early, that promotion calibration might eventually involve my input, so mentoring conversations and evaluation conversations were separate. When something they shared in a mentoring context, a specific technical mistake, later became relevant to an honest performance discussion, I went back to them first rather than using it silently, and asked how they wanted it handled.
Result: the relationship stayed trustworthy because the boundary was named rather than assumed, even though the overlap between mentor and evaluator could not be fully removed.
Trade-offs & pitfalls
Pretending you can be a purely neutral mentor when you structurally have some evaluator influence is dishonest and erodes trust once it surfaces. Over-formalizing boundaries, a written contract for every casual pairing, feels bureaucratic when the stakes don't call for it, so match the formality to the actual overlap: an evaluator relationship needs an explicit conversation, a casual peer pairing usually doesn't. The senior distinction is naming the boundary before it's tested, not scrambling to explain it after a mentee feels burned.
When would you mask a single interrupt source and when would you disable all interrupts? How is each done on a Cortex-M, and what does each cost in latency?
Sample Answer
Three levels of masking on a Cortex-M
An interrupt can be held off at three granularities. All three delay the interrupt; none discards it, because the NVIC (the Cortex-M interrupt controller) keeps the request pending (remembered in a pending bit, so it is held rather than lost) and runs it once it is allowed.
- Mask one source (per-IRQ enable). The NVIC has ISER and ICER registers (set-enable and clear-enable); writing a 1 to a bit enables or disables just that IRQ, so no read-modify-write is needed (CMSIS wraps it as
NVIC_DisableIRQ/NVIC_EnableIRQ). - Mask by priority with BASEPRI. Writing a value N to this register blocks every exception whose priority value is N or numerically higher (less urgent); more urgent ones still run. Zero turns the mask off.
- Mask everything configurable with PRIMASK (
cpsid ito set,cpsie ito clear). The reset, NMI (non-maskable interrupt) and HardFault exceptions are not affected.
Run it
Three handlers at priorities 0x80, 0x40 and 0x20 record their IRQ number. In each mode all three are pended, then the mask is released; | marks the release.
#include <stdint.h>
#define REG(a) (*(volatile uint32_t *)(a))
#define NVIC_ISER REG(0xE000E100)
#define NVIC_ICER REG(0xE000E180)
#define NVIC_ISPR REG(0xE000E200)
#define NVIC_IPR ((volatile uint8_t *)0xE000E400)
static void puts_(const char *s) { while (*s) REG(0x4000C000) = *s++; }
static void done(void) {
register uint32_t r0 __asm("r0") = 0x18, r1 __asm("r1") = 0x20026;
__asm volatile("bkpt 0xAB" :: "r"(r0), "r"(r1));
for (;;) {}
}
static volatile char trace[16];
static volatile int n;
static void mark(char c) { trace[n++] = c; }
void irq0(void) { mark('0'); } /* priority 0x80, least urgent */
void irq1(void) { mark('1'); } /* priority 0x40 */
void irq2(void) { mark('2'); } /* priority 0x20, most urgent */
static void sync(void) { __asm volatile("dsb\n isb" ::: "memory"); }
static void pend_all(void) { NVIC_ISPR = 1; sync(); NVIC_ISPR = 2; sync(); NVIC_ISPR = 4; sync(); }
static void report(const char *name) {
mark(0); puts_(name); puts_(" order run: "); puts_((const char *)trace); puts_("\n");
n = 0; for (int i = 0; i < 16; i++) trace[i] = 0;
}
int main(void) {
NVIC_IPR[0] = 0x80; NVIC_IPR[1] = 0x40; NVIC_IPR[2] = 0x20; NVIC_ISER = 7;
NVIC_ICER = 2; /* A: mask source IRQ1 only */
pend_all(); mark('|'); NVIC_ISER = 2; sync();
report("A ICER IRQ1 only :");
__asm volatile("cpsid i" ::: "memory"); /* B: PRIMASK, everything configurable */
pend_all(); mark('|');
__asm volatile("cpsie i" ::: "memory"); sync();
report("B PRIMASK :");
__asm volatile("msr basepri, %0" :: "r"(0x40) : "memory"); /* C: BASEPRI = 0x40 */
pend_all(); mark('|');
__asm volatile("msr basepri, %0" :: "r"(0) : "memory"); sync();
report("C BASEPRI = 0x40 :");
done();
return 0;
}
void reset(void) { main(); }
__attribute__((section(".vectors"), used)) void (*const vectors[19])(void) = {
[0] = (void *)0x20010000, [1] = reset, [16] = irq0, [17] = irq1, [18] = irq2 };
Scaffolding in the listing: REG(a) turns a fixed address into a volatile 32-bit register access, so NVIC_ISER and the others are just named memory-mapped registers (ISER enables, ICER disables, ISPR sets the pending bit, IPR holds one priority byte per interrupt). puts_ writes characters to the board's UART data register at 0x4000C000, and done makes the semihosting exit call so QEMU stops. The vectors array is the vector table: entry 0 is the initial stack pointer, entry 1 the reset handler, and entries 16 to 18 are the handlers for IRQ0 to IRQ2 (external interrupts start at exception number 16). -Wl,--section-start=.vectors=0x0 places that table at address 0, where the core looks for it at reset.
Built with arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -O2 -nostartfiles -ffreestanding -Wl,--section-start=.vectors=0x0 -Wl,-Ttext=0x400 -Wl,-e,reset -Wl,-Tbss=0x20000000 mask_modes.c -o mask_modes.elf (GCC 14.2.1) and run under QEMU 10.0 (qemu-system-arm -M lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel mask_modes.elf), it prints:
A ICER IRQ1 only : order run: 02|1
B PRIMASK : order run: |210
C BASEPRI = 0x40 : order run: 2|10
Reading it: in A, IRQ0 and IRQ2 ran as soon as they were pended, and only the masked IRQ1 waited until the enable. In B nothing ran until cpsie, then all three ran, most urgent first (2, 1, 0). In C only the most urgent (IRQ2, priority 0x20, above the BASEPRI value) ran during the masked window; the other two ran after the release. The dsb and isb barriers sit after each write to an NVIC pending or enable register and after each release of a mask: dsb makes sure the write has completed and isb flushes the pipeline, so a pending interrupt that is now allowed is taken before the next line runs and the trace reads the same on every run.
When to use which
- Mask a single source when only that interrupt's handler shares data with the code you are protecting, or when the code must not be interrupted by that peripheral but the rest of the system must stay live (for example, quieting a UART receive interrupt while main code drains its buffer). The catch: the source still becomes pending, and it does nothing for any other interrupt that touches the same data.
- BASEPRI when several ISRs at different priorities touch the data. Set it to the most urgent priority among them (a priority ceiling: the highest priority of any code that uses the data, so everything that could touch it is blocked); anything more urgent keeps its latency. This is my default for a driver's shared state on Armv7-M parts (Cortex-M3 and up).
- PRIMASK for a handful of instructions that must be indivisible against everything, or on a core without BASEPRI (small cores such as the Cortex-M0/M0+ are generally without it; check your core's reference manual). The same section on a Cortex-M3 or later could often be an
ldrex/strexatomic instead: compiled with-mcpu=cortex-m3,__atomic_fetch_addon a 32-bit counter becomes anldrex/strexretry loop, while for-mcpu=cortex-m0the compiler emits a call to a library routine,__atomic_fetch_add_4.
What each costs in latency
Latency here means the time from the interrupt request to the first instruction of its handler; jitter is how much that time varies. While a mask is set, any interrupt it covers waits for the end of the masked section, so the worst-case latency of a covered interrupt grows by the longest masked section anywhere in the firmware, not by the average. With PRIMASK, that covers every configurable interrupt, so the longest section sets the latency floor of the most urgent interrupt as well. With BASEPRI only the interrupts at or below the mask are delayed. With a single-source mask only that source is delayed, and the cost of setting it is a single register write.
Worked figure: a 40-cycle masked section on a 48 MHz core delays covered interrupts by up to 40 / 48 MHz = 0.83 microseconds, on top of the core's own entry time. If the most urgent interrupt has a 2-microsecond latency budget, that one section already takes a large share of it, so it needs BASEPRI or an atomic, not PRIMASK. The cycle count of a section is measured on hardware with the DWT cycle counter (the Data Watchpoint and Trace unit's free-running counter of core clock cycles, read as DWT->CYCCNT, present on cores that implement it); QEMU does not model cycle counts and is not evidence for latency.
Rules that keep a critical section short
- A few instructions: copy the data out, then process it outside the section.
- No function calls whose length you do not control (no
printf, nomalloc, no callbacks). - No waiting or blocking inside it: never wait for a flag an interrupt sets while that interrupt is masked, since it will not arrive.
- No loops with a data-dependent bound.
- Save and restore the previous mask instead of unconditionally enabling (
mrs r0, primaskbeforecpsid i,msr primask, r0after, so a section entered with interrupts already off leaves them off). - Add a compile-time or review check on the longest section, and measure it, because determinism means the worst case, not the typical case.
Explain the startup steps a firmware must perform to copy initialized .data from flash to RAM and zero the .bss section. Provide typical linker symbols (e.g., _sidata, _sdata, _edata, _sbss, _ebss) and a concise C startup routine that performs the copy and zeroing. Why would you place time-critical functions in RAM and how does this affect the startup sequence?
Sample Answer
Brief approach / steps
- Copy initialized data (.data) from its load address in flash to its run address in RAM.
- Zero the .bss region in RAM.
- Optionally call constructors (C++), init hardware, then jump to main.
- If time-critical functions live in RAM, ensure their flash-load image is copied to RAM before they execute.
Typical linker symbols
- _sidata : start of initialized data in flash (load address)
- _sdata : start of .data in RAM (run address)
- _edata : end of .data in RAM
- _sbss : start of .bss in RAM
- _ebss : end of .bss in RAM
Concise C startup routine
extern uint32_t _sidata; /* in flash */
extern uint32_t _sdata; /* in RAM */
extern uint32_t _edata;
extern uint32_t _sbss;
extern uint32_t _ebss;
void Reset_Handler(void) {
uint32_t *src = &_sidata;
uint32_t *dst = &_sdata;
/* copy .data from flash to RAM */
while (dst < &_edata) {
*dst++ = *src++;
}
/* zero .bss */
dst = &_sbss;
while (dst < &_ebss) {
*dst++ = 0;
}
/* If functions are executed from RAM, ensure their image is copied here
before calling them. Then call system init and main(). */
SystemInit();
main();
}
Why place time-critical functions in RAM
- RAM execution removes flash wait states and flash controller contention, reducing latency and improving determinism (critical for ISRs, DMA-sensitive code, or flash-inaccessible modes like during erase/write).
- Impact on startup: you must include an extra copy step in startup to relocate those functions' code from flash to their RAM addresses (with corresponding linker symbols, e.g., _siram_text/_eiram_text and _siram_text_lma). That copy must occur before any code calls those routines (including interrupts). Also ensure vector table points to RAM if ISRs run there.
Metastability and input jitter cause occasional corrupted reads from asynchronous digital inputs. Describe software-level mitigations (synchronizers, double-flop, oversampling, majority voting, hysteresis), their cost in latency and CPU usage, and outline how to design tests to estimate the MTBF (mean time between failures) for metastability on your target.
Sample Answer
High-level summary
Metastability and input jitter require trade-offs between reliability, latency and CPU. Below I list common software-level mitigations, their costs, and a testing plan to estimate MTBF for your target.
Mitigations (what, why, cost)
- Double-flop synchronizer (2-stage FF): simple, industry standard for single-bit crossing. Latency = 2 clock cycles; CPU cost = none (hardware flip-flops). Reduces metastability escape probability by giving extra resolve time.
- Multi-stage synchronizer (3+ FFs): further reduces failure probability exponentially; latency = N cycles; CPU cost = none.
- Oversampling (sample input multiple times per bit using software/GPIO interrupts or ADC): increases robustness to narrow glitches and jitter. Latency = depends on sample window (often tens–hundreds µs); CPU cost = high if polling; moderate if using DMA + timer.
- Majority voting (N samples, take majority): works with oversampling; extra memory/CPU to collect and compute majority; latency = sampling window length + compute (very small).
- Hysteresis / Schmitt-trigger inputs: analog front-end reduces noise-induced transitions; latency = negligible; CPU cost = none if hardware; software equivalent = require stable run-length before accept (debounce) — increases latency and CPU if polled.
Trade-offs summary
- Hardware FFs: minimal CPU, fixed latency (clock cycles).
- Software oversampling/majority: flexible but high CPU and higher latency; good for low-rate signals.
- Hysteresis/debounce: low CPU (if implemented in hardware); increases detection time.
MTBF estimation (design tests + extrapolation)
- Controlled stress: use FPGA or signal generator to create asynchronous transitions at controlled rates and adjustable timing relative to destination clock; inject worst-case pulses and jitter.
- Measure failure rate: run long-duration tests counting corrupted captures. Vary synchronizer resolve time (extra wait cycles) or sampling window.
- Fit exponential model to observed failure rate to extract device/time constant and constant K:
MTBF = e^(T_res / tau) / (f_input * f_clock * K)
Plain-English: increasing resolve time T_res exponentially increases MTBF; f_input and f_clock scale collision opportunities.
4. Extrapolate to field conditions using measured f_input, f_clock and fitted tau, K. Include confidence intervals and run time sufficient to observe >10 failures for statistical validity or use accelerated stressing to get failures faster.
5. Practical checks: use oscilloscope to capture ambiguous transitions, validate with multiple input patterns, and combine hardware (Schmitt) + 2-stage sync + optional software debounce for high-assurance.
Notes: document assumptions (clock domains, measured jitter), include safety margins (design for MTBF >> expected device lifetime).
Given a hypothetical microcontroller with the following register addresses: GPIO_DIR at 0x40000000, GPIO_PULLUP at 0x40000004, GPIO_IN at 0x40000008, write C code (register defines and a function) to configure pin 3 as input with pull-up enabled and return its current logical value. Use volatile and explain your choices.
Sample Answer
Approach
- Use volatile pointers to memory-mapped registers.
- Use bit masks for pin 3 (bit 3).
- Do a read-modify-write for DIR and PULLUP to avoid altering other pins.
- Return logical 0/1 from GPIO_IN masked and shifted.
#include <stdint.h>
#define GPIO_DIR_ADDR ((volatile uint32_t *)0x40000000)
#define GPIO_PULLUP_ADDR ((volatile uint32_t *)0x40000004)
#define GPIO_IN_ADDR ((volatile uint32_t *)0x40000008)
#define PIN3_MASK (1U << 3)
/* Configure pin 3 as input with pull-up enabled and return its value (0 or 1) */
static inline int read_pin3_with_pullup(void)
{
/* Set as input: clear bit in DIR (1 = output, 0 = input) */
uint32_t dir = *GPIO_DIR_ADDR;
dir &= ~PIN3_MASK;
*GPIO_DIR_ADDR = dir;
/* Enable pull-up: set bit in PULLUP register */
uint32_t pu = *GPIO_PULLUP_ADDR;
pu |= PIN3_MASK;
*GPIO_PULLUP_ADDR = pu;
/* Read pin value, return 0 or 1 */
return ( ( *GPIO_IN_ADDR & PIN3_MASK ) ? 1 : 0 );
}
Why volatile and RMW
- volatile prevents compiler optimizations that could cache register values or reorder accesses; required for hardware registers.
- Read-modify-write preserves other pins' configuration.
- Using explicit masks keeps code clear and safe for concurrent peripheral usage.
Describe Pulse-Width Modulation (PWM) and how it is used to generate a variable analog-like voltage or to drive motors and LEDs. Explain key parameters: frequency (period), duty cycle, timer resolution (bits), and trade-offs between low-frequency and high-frequency PWM. Explain how to filter PWM to create a smooth DC (RC/LC filters), and how filter cutoff should relate to PWM frequency and the desired control bandwidth.
Sample Answer
Definition & use
Pulse‑Width Modulation (PWM) outputs a fixed‑amplitude square wave whose pulse width varies. By changing the duty cycle (fraction of time the signal is high) you create an average voltage Vavg ≈ Vcc * duty — useful for “analog‑like” control of LEDs and motors without a DAC. Typical uses: LED dimming (human eye integrates), brushed DC motor torque control, and motor driver gate signals.
Key parameters
- Frequency (f) / Period (T = 1/f): how fast pulses repeat.
- Duty cycle (D): percent time high (0–100%). Vavg = Vcc * D.
- Timer resolution (bits): N bits → 2^N steps for duty. Step size = 1/2^N of full scale; affects granularity and quantization error.
Trade-offs: low vs high frequency
- Low f: simpler filtering, lower switching losses, but visible LED flicker or audible motor whine and higher current ripple.
- High f: smoother motor torque and flicker‑free LEDs, smaller filter, but more switching losses, EMI, and need for faster timers and MOSFET switching.
Filtering PWM to DC
Use RC (low‑pass) or LC filters to average PWM into smooth DC prior to sensitive circuits.
f_c = 1 / (2 * pi * R * C)
Choose cutoff f_c well below PWM frequency to attenuate carrier (e.g., f_c ≤ f_pwm / 10–20) but above the desired control bandwidth so you don’t slow response. For motor velocity control pick f_c ~ 1–5× control bandwidth; for LED dimming you can use much lower fc.
Practical notes
- For power stages prefer LC to reduce losses; account for load impedance and damping.
- Ensure timer resolution gives enough steps across your control range (increase timer clock or bits).
- Consider dead‑time, rise/fall times, and driver limits for high‑f PWM.
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