Embedded C Interview Vault and Revision
This is the volume to come back to. The first module is the entire course as a set of tables you can read in twenty minutes. The rest is practice: ninety-six problems of the kind interviews use, each one run through a compiler so the answer printed here is the answer the machine gave.
- The whole course as a revision sheet, volume by volume
- What 45 tricky expressions actually evaluate to, and why
- The bit-manipulation idioms worth knowing by heart
- How to read any C declaration, and the pointer traps interviews use
- The concept questions asked in embedded interviews, answered properly
- Volumes 00 to 17, or the parts of them you want to revise
18.1 One-page revision sheet
Eighteen volumes, condensed. If you can hold this page in your head, you can hold the course in your head.
The language
| The thing to remember | |
|---|---|
| Types | Use uint8_t, int16_t, uint32_t. Plain int is 16 bits on some chips and 32 on others |
| Promotion | Anything smaller than int is promoted to int before arithmetic, so (uint8_t)200 + (uint8_t)100 is 300 |
| Signed and unsigned | Mixing them converts the signed value, so -1 < 1u is false |
| Truncation | Assigning a wider value to a narrower type keeps the low bits and discards the rest |
| Division | Truncates towards zero, so -7 / 2 is -3 and -7 % 2 is -1 |
static at file scope |
Private to this file |
static in a function |
One copy, surviving between calls |
const |
Goes in flash, costs no RAM |
volatile |
Read and write it every time, in order. Not atomic |
Bits
| Want | Write |
|---|---|
| Set bit n | v \|= (1u << n) |
| Clear bit n | v &= ~(1u << n) |
| Toggle bit n | v ^= (1u << n) |
| Test bit n | if (v & (1u << n)) |
| Clear the lowest set bit | v & (v - 1) |
| Isolate the lowest set bit | v & -v |
| Is it a power of two | v && !(v & (v - 1)) |
| Extract a field | (v >> shift) & mask |
| Insert a field | (v & ~(mask << shift)) \| (field << shift) |
| Round up to a power of two | (v + to - 1) & ~(to - 1) |
Memory
| Section | Holds | Flash | RAM |
|---|---|---|---|
.text |
code | yes | no |
.rodata |
const data, string literals |
yes | no |
.data |
globals starting non-zero | yes | yes |
.bss |
globals starting at zero | no | yes |
| stack | locals, return addresses | no | yes |
| heap | malloc |
no | yes |
Start-up copies .data from flash to RAM, zeroes .bss, then calls main. A global stuck at a
repeating pattern such as 0xA5A5A5A5 means that did not happen.
Hardware
| A register | memory at a fixed address; describe a peripheral with one volatile struct |
| Reserved gaps | write them into the struct, and _Static_assert the last offset |
reg \|= BIT |
three steps; an interrupt between them loses data |
| Safe alternatives | a set/reset register (one store), or a short critical section |
| Interrupt handler | check why, clear the flag, move the data, leave. No waiting, no printf, no malloc |
| Priorities | lower number is more urgent on Arm; more urgent preempts |
| Sharing with an ISR | volatile, single writer, and a size the chip writes in one instruction |
| A stream from an ISR | a ring buffer: one index per side, publish the data before moving the index |
Time
/* has the interval passed? Subtract, never add. */
if ((uint32_t)(now - last) >= interval) {
last = now;
do_the_thing();
}
/* a state machine: one decision per pass, and never a delay */
switch (state) {
case IDLE: if (start) { enter(RUNNING, now); } break;
case RUNNING: if (held_for(now) >= RUN_MS) { enter(DONE, now); } break;
default: enter(SAFE, now); break;
}
now >= last + interval fails the first time the tick counter wraps, which at 1 kHz is after 49.7
days.
The habits
- Build with
-Wall -Wextra -Wconversion -Wsign-conversion -Wundef -Werror, and-Os - Guard every header; declare variables
externthere and define them once - Size buffers at compile time; use a pool if things must come and go; avoid
malloc - Check every return value, or cast it to
(void)on purpose - Assert what cannot happen; handle what the outside world decides
- Kick the watchdog only when the work has been proved done
- Record faults where a reset cannot erase them
- Measure the stack and the buffers with high-water marks
Which of these costs flash but no RAM?
Show the answer
Answer: B. const data lives in .rodata and is read from flash in place. The zero-filled array is .bss, so
RAM only. The initialised one is .data, so both. A local is on the stack, so RAM while it runs.
18.2 45 output-prediction puzzles
Forty-five expressions that do not do what they look like. Cover the right-hand column, work each one out, then check.
Every answer below was printed by tests/c/vol18_puzzles.c on a 64-bit desktop, where int is 4
bytes and a pointer is 8. The notes say which answers would differ on a chip.
1. sizeof(char) -> 1
2. sizeof('a') -> 4
a character constant is an int in C, not a char
3. (uint8_t)200 + (uint8_t)100 -> 300
both are promoted to int before the addition
4. uint8_t small = 200 + 100 -> 44
the int result is truncated on the way into a uint8_t
5. -1 < 1u -> 0
-1 converts to a huge unsigned, so this is false
6. sizeof(int) > -1 -> 0
sizeof is unsigned, so -1 converts up and wins
7. (unsigned char)-1 == 255 -> 1
8. 7 / 2 -> 3
9. -7 / 2 -> -3
10. -7 % 2 -> -1
division truncates towards zero, and the remainder follows it
11. 5 / 2.0 -> 2.5
12. (int)2.9 -> 2
13. (int)-2.9 -> -2
Puzzles 2 and 6 are the ones that catch experienced people. sizeof returns an unsigned type, so
comparing it against anything negative converts the negative value into something enormous.
14. 0x0F ^ 0xFF -> 0xF0
15. ~0u -> 4294967295
16. 12 & (12 - 1) -> 8
x & (x-1) clears the lowest set bit
17. 12 & -12 -> 4
x & -x isolates the lowest set bit
18. 1u << 31 -> 2147483648
19. 0xB7 & 0x0F -> 7
20. set bits in 0xB7 -> 6
21. 5 & 3 == 3 -> 1
== binds tighter than &, so this is 5 & 1
22. 2 + 3 * 4 % 5 -> 4
23. !5 -> 0
24. !0 -> 1
25. int a = 5; int b = a++; -> b 5, a 6
26. int a = 5; int b = ++a; -> b 6, a 6
27. returns_zero() && returns_one() -> 0, calls 1
&& stops as soon as the answer is known
28. returns_one() || returns_zero() -> 1, calls 1
Puzzle 21 is worth committing to memory, because it is a real bug rather than a curiosity. gcc warns about it, and the warning is the useful part.
29. sizeof("hello") -> 6
five letters and the terminator
30. strlen("hello") -> 5
31. char s[] = "abc"; sizeof s -> 4
32. const char *p = "abc"; sizeof p -> 8
one is an array of four, the other is a pointer
33. sizeof arr, with int arr[10] -> 40
34. sizeof arr inside a function -> 8
an array parameter is a pointer, so the length is lost
Puzzles 32 and 34 give 4 rather than 8 on a 32-bit chip, because a pointer is four bytes there.
Puzzle 33 gives 40 on both, and 20 on a chip where int is 16 bits.
35. sizeof(struct padded) -> 12
36. sizeof(struct ordered) -> 8
the same three members, ordered differently
37. offsetof(struct padded, b) -> 4
38. offsetof(struct ordered, a) -> 4
39. (char *)(p + 1) - (char *)p -> 4 bytes
40. got = *p++ -> value 0, p now at index 1
41. (*p)++ -> words[0] 1, p still at index 0
42. 0x11223344 read as four bytes -> 44 33 22 11
little-endian: the least significant byte comes first
43. counter_static() three times -> 3 2 1
the order of those three calls is unspecified, so only the set matters
44. SQUARE_BAD(2 + 3) -> 11
expands to 2 + 3 * 2 + 3
45. 2 * N_BAD -> 11
expands to 2 * 5 + 1
Three calls in one printf printed 3, 2, 1 here. The order in which arguments are evaluated is
unspecified in C, so another compiler may print 1, 2, 3, and both are correct. Never write code
whose answer depends on it.
On a 32-bit microcontroller, what does sizeof arr give inside void f(int arr[10])?
Show the answer
Answer: C. An array parameter is adjusted to a pointer, so sizeof measures the pointer. It gave 8 on the
64-bit machine above and gives 4 on a 32-bit chip. Pass the length separately.
18.3 23 bit-manipulation problems
Twenty-three bit problems. These are the ones interviews ask for on a whiteboard, and the ones you write for real in every driver.
1. set bit 3 of 0x00 -> 0x08
2. clear bit 2 of 0xFF -> 0xFB
3. toggle bit 7 of 0x0F -> 0x8F
4. is bit 4 of 0x10 set -> yes
5. set bits in 0xB7 -> 6
6. set bits in 0xFFFFFFFF -> 32
7. is 64 a power of two -> yes
8. is 0 a power of two -> no
9. lowest set bit of 0x2C -> 0x04
10. xor-swap 0xAAAA and 0x5555 -> a 0x5555, b 0xAAAA
11. reverse the bits of 0xB2 -> 0x4D
12. byte-swap 0x11223344 -> 0x44332211
13. bits 7:4 of 0xAB -> 0xA
14. put 0x5 into bits 7:4 of 0xAB -> 0x5B
15. round 37 up to a multiple of 16 -> 48
16. round 32 up to a multiple of 16 -> 32
17. parity of 0xB7 -> 0
18. bits differing 0xB7 vs 0xA5 -> 2
19. rotate 0x80000001 left by 1 -> 0x00000003
20. sign-extend 0x1F from 5 bits -> -1
21. sign-extend 0x0F from 5 bits -> 15
22. highest set bit of 0x2C -> 5
23. highest set bit of 0 -> 32 (none set)
The five worth writing from memory
static unsigned popcount(uint32_t v)
{
unsigned c = 0u;
while (v != 0u) { v &= v - 1u; c++; } /* each pass clears one bit */
return c;
}
It loops once per set bit rather than once per bit, which is the point of the trick.
static bool is_power_of_two(uint32_t v)
{
return v != 0u && (v & (v - 1u)) == 0u;
}
The v != 0u is the half everybody forgets. Zero has no bits set, so v & (v - 1) is zero for it
too, and without the guard zero is reported as a power of two.
static uint32_t extract(uint32_t v, unsigned shift, unsigned width)
{
return (v >> shift) & ((1u << width) - 1u);
}
static uint32_t insert(uint32_t v, unsigned shift, unsigned width, uint32_t field)
{
uint32_t mask = (1u << width) - 1u;
return (v & ~(mask << shift)) | ((field & mask) << shift);
}
The & mask on the incoming field matters: without it, a value too large for the field spills into
the bits above and corrupts a neighbour.
static uint32_t rotate_left(uint32_t v, unsigned by)
{
by &= 31u; /* a shift of 32 would be undefined */
return (by == 0u) ? v : ((v << by) | (v >> (32u - by)));
}
static int32_t sign_extend(uint32_t v, unsigned bits)
{
uint32_t m = 1u << (bits - 1u);
v &= (1u << bits) - 1u;
return (int32_t)((v ^ m) - m);
}
This is the one that comes up with sensors. A 12-bit signed reading arrives as 12 bits in a 16-bit register, and without sign extension every negative reading becomes a large positive one.
Offering the xor swap as a clever answer. It works, and on any real processor it is slower than using a temporary, and it silently zeroes the value if both pointers are the same. Knowing it is fine; preferring it is not.
Why does is_power_of_two need the v != 0 test?
Show the answer
Answer: A. For zero, v - 1 wraps to all ones and the AND gives zero, which passes the test. Zero has no bits
set and is not a power of two, so it has to be excluded explicitly.
18.4 28 pointer problems
Twenty-eight pointer problems. Everything else in C is reachable from pointers, which is why interviews concentrate here.
1. int *p -> pointer to int
2. const int *p -> pointer to a const int
3. int * const p -> a const pointer to an int
4. int *a[5] -> array of 5 pointers to int, 40 bytes
5. int (*a)[5] -> pointer to an array of 5 ints, 8 bytes
6. int (*f)(int, int) -> pointer to a function taking two ints
The rule for reading any declaration: start at the name, go right when you can, left when you
cannot, and brackets group. Take int (*a)[5]. Start at a. The brackets stop you going right, so go left to *, giving a
pointer. Then out of the brackets and right to [5], so a pointer to an array of five. Then left
to int.
7. swap(&a, &b) with a=3 b=7 -> a 7, b 3
8. set_to_null_by_value(p) -> p is unchanged
9. set_to_null_by_address(&p) -> p is NULL
10. const int *p: what may change -> the pointer, not the value
11. int * const p: what may change -> the value, not the pointer (now 42)
Problems 8 and 9 are the ones that reveal whether somebody has really understood. A pointer passed to a function is a copy, so the function can change what it points at but not where the caller's pointer points. Changing that needs a pointer to the pointer.
12. p + 2 - p, with uint32_t *p -> 2 elements
13. (char *)(p + 2) - (char *)p -> 8 bytes
14. *(words + 2) -> 30
15. 2[words] -> 30
a[i] means *(a + i), so i[a] is the same thing
16. &words[4] is legal to hold -> yes, but never to read
17. sizeof "embedded" as an array -> 9
18. my_strlen("embedded") -> 8
19. reversed in place -> deddebme
Problem 15 is a party trick with a real lesson behind it: a[i] is defined as *(a + i), and
addition does not care which way round it is written.
20. call through a function pointer -> 42
21. (*function_pointer)(1, 2) -> 3
both forms are the same; the plain one is usual
22. has_loop on a straight list -> no
23. has_loop once it loops -> yes
24. offsetof(struct reading, value) -> 4
25. sizeof(struct reading) -> 12
three members totalling 6 bytes, padded out
static bool has_loop(node_t *head)
{
node_t *slow = head, *fast = head;
while (fast != NULL && fast->next != NULL) {
slow = slow->next; /* one step */
fast = fast->next->next; /* two steps */
if (slow == fast) {
return true; /* they met, so there is a loop */
}
}
return false; /* the fast one reached the end */
}
Two walkers, one twice as fast. If there is a loop the fast one laps the slow one and they meet. If there is not, the fast one reaches the end. It uses no extra memory, which is why it is the expected answer rather than "keep a set of visited nodes".
26. free(NULL) and p == NULL checks -> both are legal and do nothing
27. values + 3, one past the end -> legal to hold and compare
28. how many elements it spans -> 3
One past the end of an array is a legal address to form and compare, which is what makes
for (p = a; p < a + n; p++) correct. Two past the end is not, and neither is reading the one past
the end.
A function takes int *p and sets p = NULL. What does the caller see?
Show the answer
Answer: C. Arguments are passed by value, including pointers. To change the caller's pointer the function needs
its address, which means a parameter of type int **.
18.5 Embedded concept questions
The concept questions. These come up in nearly every embedded interview, and each has a volume behind it.
Which four topics come up in almost every embedded C interview?
Show the answer
Answer: B. Those four are the ones that separate somebody who has written firmware from somebody who has written C. Each has a volume of this course behind it, and each has a concrete example you can give.
Practice
"What does volatile do, and give me an example where leaving it out breaks something."
Show the solution
"It tells the compiler the value can change outside the program's control, so every read and write in the source must appear in the object code, in order.
The example I would give is a pulse. Writing reg = 1; reg = 0; on a plain variable compiled at
-O2 to a single instruction. The write of 1 was deleted, because nothing read it before it was
overwritten, and the pin never moved. With volatile both writes survive.
The second example is reading a register twice to see how much a counter moved. Without volatile
the compiler folded it into a constant zero and never read the register at all.
I would add what it does not do: it does not make anything atomic. A read-modify-write on a
volatile register is still three steps and still needs protecting."
*(Volume 10)*
"Why is my struct bigger than the sum of its members?"
Show the solution
"Padding. Each member has to start at an address its type allows, so the compiler inserts unused bytes to line them up. It also adds tail padding, so that an array of the struct keeps every element aligned.
A uint8_t, a uint32_t and a uint16_t came to 12 bytes in one order and 8 in another, with no
other change. Ordering the largest members first usually gives the smallest struct.
If the layout has to match something outside the program, such as a protocol message or a flash record, I would pack it and accept slower access. Better still, build the bytes explicitly with shifts, because a packed struct's members can be misaligned and taking their address is undefined."
*(Volume 08)*
"Walk me through what happens between power-on and the first line of main."
Show the solution
"The core reads the first two words of flash: the initial stack pointer and the address of the reset handler. It sets the stack pointer and jumps there.
The reset handler usually sets up the clock, then runs two loops. The first copies .data from its
load address in flash to its run address in RAM. The second writes zeros from _sbss to _ebss.
On a C++ project it then runs global constructors, and if the C library is used it initialises that.
Then it calls main.
The linker script supplies all the boundary symbols, so those loops need no sizes at compile time.
And if the .bss loop is missing, a global that should be zero starts at whatever the RAM held.
That is often a recognisable pattern like 0xA5A5A5A5, which is a strong hint that start-up is the
problem."
*(Volume 09)*
"How do you safely share a variable between an interrupt handler and the main loop?"
Show the solution
"Mark it volatile, so both sides really read and write it. Then make sure every update is a single
indivisible step.
A count++ is not: it is a read, a change and a write, and two increments can end up as one. So either give the variable a single writer, with the handler incrementing and the main loop only
reading. Or wrap the update in a short critical section that saves and restores the interrupt
state.
Size matters too. A uint32_t on a 32-bit chip is written in one instruction and cannot be torn. A
uint64_t is written in two, so the main loop can catch it half updated. I saw a 64-bit microsecond clock read back four billion too low for exactly that reason. The fix is
to read the high half, the low half, then the high half again, and retry if it changed.
For a stream of data rather than a single value, a ring buffer. The handler writes one index and the main loop writes the other, so neither ever read-modify-writes something the other writes, and no locking is needed at all."
*(Volumes 11 and 12)*
"Why is dynamic allocation discouraged in firmware?"
Show the solution
"Three reasons that compound.
It can fail, and on a device with no user there is usually nowhere sensible to report that. Handling it correctly at every call site is hard and most code does not.
It has no bounded worst-case time, because it searches for a block. That breaks real-time guarantees and rules it out of an interrupt handler.
And it fragments. I built a small allocator to show this. With 40 bytes free, a request for 40 bytes failed, because the free space was in two pieces of 20 with a live block between them. The total free figure stays healthy while the largest block shrinks, so the failure arrives after months in a device that has already shipped.
Instead: static allocation wherever the lifetime is the whole program, so the linker proves it fits before the program runs. Where things genuinely come and go, a pool of fixed-size blocks, which cannot fragment and allocates in constant time."
*(Volume 14)*
"What is undefined behaviour, and why does it matter more with optimisation on?"
Show the solution
"It is a construct the standard deliberately gives no meaning to - signed overflow, shifting past the width of a type, reading out of bounds. What matters is not that the result is unpredictable, but that the compiler may assume it never happens.
The example is an overflow check. a + b >= a looks like a test for overflow. At -O2 gcc compiled
it to a test of b >= 0 and dropped the addition entirely, because the sum can only be smaller if
it overflowed, and that cannot happen. The check no longer checks anything, and the source still
looks correct.
At -O0 the compiler does less of this, which is why such code often works until optimisation is
turned on. So 'it only works at -O0' is a bug in my code, not in the compiler, and usually a missing
volatile or undefined behaviour. The way to find it is warnings, a sanitiser on the parts that
run on a desktop, and reading the generated assembly when in doubt."
*(Volume 16)*
"Your board hits a HardFault. What do you do?"
Show the solution
"Read the fault registers rather than guessing. CFSR says what kind: a MemManage bit, a BusFault bit, or a UsageFault bit. HFSR usually just says FORCED, meaning a configurable fault escalated because its handler was not enabled, so the real information is in CFSR. If MMARVALID or BFARVALID is set, the matching address register holds the address that was refused.
Then the stacked frame. The core pushes eight registers, with the faulting instruction address at
offset 0x18 from the stack pointer, and addr2line turns that into a file and line.
The patterns tell you a lot before you look anything up. A program counter in RAM means a corrupted function pointer or return address. A BusFault with BFAR pointing at a peripheral usually means its clock was never enabled. A MemManage on address zero is a null pointer.
I would also enable the divide-by-zero and unaligned traps, because they are off by default."
*(Volume 17)*
"How do you decide between polling and interrupts?"
Show the solution
"By what it costs to be late, and whether the event can be missed.
Poll when the event is slow or frequent, or something you would check every pass anyway. The timing is easy to reason about, and worst-case behaviour can be worked out on paper.
Use an interrupt when the event is rare, urgent, or short enough to be missed. A pulse 50 microseconds long against a 5 millisecond loop is missed almost every time, because it begins and ends between two checks.
Most systems use both: interrupts catch the events and push data into queues, and the main loop does the slow work. What I avoid is doing real work in the handler, because every microsecond there is latency for everything less urgent."
*(Volume 11)*
Rapid fire
Twelve one-line answers
The short questions that get asked to fill gaps.
Show the solution
What is the difference between const int *p and int * const p? The first is a pointer to a
constant integer: the pointer may move, the value may not be changed through it. The second is a
constant pointer: the value may change, the pointer may not.
What does static mean on a function? Private to this translation unit, so no other file can
call it or clash with the name.
Why do headers need include guards? So a header pasted in twice does not define things twice.
Most things cannot be defined twice, though C23 now permits an identical struct definition.
What is the difference between .data and .bss? Both are RAM. .data starts non-zero so its
values are stored in flash and copied at start-up; .bss starts at zero so nothing is stored.
Why is sizeof on an array parameter wrong? It is not an array; array parameters are adjusted
to pointers, so you measure the pointer.
What does extern do? Declares that a name exists and is defined elsewhere, so a header can
mention a variable defined in one .c file.
When is goto acceptable? Jumping forward to cleanup labels in one function, releasing what was
acquired in reverse order. Nothing else.
Why not use float on a small chip? Many have no floating-point unit, so every operation is a
library call costing hundreds of cycles. Fixed-point integers are usually faster and precise enough.
What is a memory-mapped register? Ordinary memory at a fixed address, wired to hardware, reached
with the same loads and stores as anything else - which is why volatile is required.
What does the linker do? Resolves every undefined symbol against a definition, lays the sections out according to the linker script, and writes the final image.
Why is -Os usual for firmware? Flash is usually the tight resource, and -Os is -O2 without
the transformations that trade size for speed. The same file measured 370 bytes at -Os and 1835 at
-O3.
What is the first thing you would add to a new project? A fault handler that records CFSR, the fault addresses and the stacked frame into RAM that a reset does not clear, plus a small trace buffer. Both cost almost nothing and turn a dead board into a diagnosable one.
The end of the course
- Volumes 00 to 03 gave you the language: types, sizes, operators, and what the compiler does with them
- Volumes 04 to 08 gave you the tools: bits, scope, arrays, pointers and structs
- Volumes 09 to 13 opened the chip: the memory map, registers, interrupts, timers and the buses
- Volumes 14 to 17 made it survive: memory discipline, the build, reliability, and debugging
- Every example in this course was compiled with warnings as errors and run before it was published
- The numbers quoted were measured, not remembered, and the harnesses that produce them are re-run whenever anything changes
Where to go next depends on what you want to build. The State Machines from Zero course takes the switch statement from Volume 12 and turns it into a discipline, in hardware and in C. The Academy has the rest, and the learning paths suggest an order.
Whatever you pick, the habit that matters is the one this course was built on: do not believe a number you have not checked. Write the program, run it, and read what it actually said.