Volume 18 Beginner 5 sub-modules ~25 min read

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.

You will learn
  • 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
You need
  • 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

Quick check

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
Puzzle 43 is a warning, not a fact

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.

Quick check

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.

Common mistake

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.

Quick check

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
Remember

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.

Quick check

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.

Quick check

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

Practice 1

"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)*

Practice 2

"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)*

Practice 3

"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)*

Practice 4

"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)*

Practice 5

"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)*

Practice 6

"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)*

Practice 7

"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)*

Practice 8

"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

Interview question 1

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

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.