Menu
CoddyTech

Embedded C Interview Questions

Where firmware interviews start: volatile, register bit operations, fixed-width types and endianness.

10 questions
  1. What does the volatile keyword do?

    volatile tells the compiler a variable can change outside the visible control flow, so every read and write must happen, in order, and cannot be cached or removed. Use it for hardware registers, variables changed by interrupt or signal handlers (volatile sig_atomic_t), and locals that must survive longjmp.

    volatile int data_ready = 0;          /* set by an ISR */
    
    void wait_for_data(void) {
        while (!data_ready) { }           /* without volatile this can become an infinite loop */
    }
    

    It does not make operations atomic or synchronize threads; use C11 _Atomic or a mutex. MSVC's /volatile:ms, the default on x86 and x64, adds acquire and release ordering as a Microsoft extension; see the /volatile docs.

  2. Can a variable be both const and volatile?

    Yes. const means your code may not write it; volatile means something else may change it. A read-only hardware status register is the textbook case.

    #define STATUS_REG (*(const volatile uint32_t *) 0x40021000u)
    
    while ((STATUS_REG & 0x1u) == 0) {
        /* wait until the device sets bit 0 */
    }
    
  3. How do you set, clear, toggle and check a bit in C?

    Build a mask with 1u << n, then OR to set, AND with the inverted mask to clear, XOR to toggle and AND to test.

    C
    #include <stdio.h>
    
    #define BIT(n)            (1u << (n))
    #define SET_BIT(x, n)     ((x) |= BIT(n))
    #define CLEAR_BIT(x, n)   ((x) &= ~BIT(n))
    #define TOGGLE_BIT(x, n)  ((x) ^= BIT(n))
    #define CHECK_BIT(x, n)   (((x) >> (n)) & 1u)
    
    int main(void) {
        unsigned int reg = 0;
        SET_BIT(reg, 3);
        SET_BIT(reg, 0);
        printf("after set:    0x%02X\n", reg);
        CLEAR_BIT(reg, 0);
        printf("after clear:  0x%02X\n", reg);
        TOGGLE_BIT(reg, 7);
        printf("after toggle: 0x%02X\n", reg);
        printf("bit 3 is %u, bit 1 is %u\n", CHECK_BIT(reg, 3), CHECK_BIT(reg, 1));
        return 0;
    }

    It prints 0x09, 0x08, 0x88 and bit 3 is 1, bit 1 is 0. Use 1u: 1 << 31 on a 32-bit int is undefined behavior.

    Practice it: Power of Two

  4. What is 0x0F after setting bit 6 and clearing bit 0?

    C
    #include <stdio.h>
    
    int main(void) {
        unsigned int x = 0x0F;
        x |= 1u << 6;
        x &= ~(1u << 0);
        printf("0x%X\n", x);
        return 0;
    }

    It prints 0x4E. 0x0F is 0000 1111; setting bit 6 gives 0100 1111, and clearing bit 0 gives 0100 1110. Bits count from 0.

  5. How do you count the set bits in an integer?

    Brian Kernighan's method: n & (n - 1) clears the lowest set bit, so count iterations until n is 0, once per set bit.

    C
    #include <stdio.h>
    
    int popcount(unsigned int n) {
        int count = 0;
        while (n) {
            n &= n - 1;     /* drop the lowest set bit */
            count++;
        }
        return count;
    }
    
    int main(void) {
        unsigned int values[] = {0, 1, 7, 8, 255, 0xF0F0};
        for (int i = 0; i < 6; i++)
            printf("%u has %d set bits (builtin says %d)\n",
                   values[i], popcount(values[i]), __builtin_popcount(values[i]));
        return 0;
    }

    In production use __builtin_popcount, one instruction where the target has it (on x86-64 only with -mpopcnt or a matching -march), or C23's stdc_count_ones.

    Practice it: Counting Bits

  6. How can a union tell you your machine's byte order?

    C
    #include <stdio.h>
    
    int main(void) {
        union {
            unsigned int i;
            unsigned char c[sizeof(unsigned int)];
        } u;
        u.i = 1;
        printf("%d\n", u.c[0]);
        return 0;
    }

    It prints 1. On a little-endian machine (x86, and ARM as Linux normally runs it) the least significant byte comes first; big-endian would print 0. Byte order is implementation-defined. Reading another union member is allowed in C but undefined in C++.

  7. How do you swap two numbers without a temporary variable?

    With XOR: a ^= b; b ^= a; a ^= b;, because x ^ x is 0 and x ^ 0 is x.

    C
    #include <stdio.h>
    
    int main(void) {
        int a = 12, b = 25;
        a ^= b;
        b ^= a;
        a ^= b;
        printf("a = %d, b = %d\n", a, b);
        return 0;
    }

    It prints a = 25, b = 12. If both names refer to the same object, the first XOR zeroes it. A temporary variable is clearer and as fast.

    Practice it: Single Number

  8. Why do embedded programmers use uint8_t and uint32_t instead of int?

    Because int and long vary in width (an int is 16 bits on some microcontrollers), while registers and protocols have exact sizes. <stdint.h> (C99) provides uint8_t, uint32_t and the rest. They are optional: a platform with no 8-bit type has no uint8_t, while int_least16_t and int_fast16_t always exist.

  9. What should you avoid inside an interrupt service routine?

    Keep it short: record the event and let the main loop do the work. Avoid blocking, non-reentrant calls such as printf and malloc, and floating point where the ISR does not save the FPU registers. Shared variables must be volatile, and multi-byte ones need interrupts disabled (or atomic access) around the main loop's read.

    volatile uint8_t rx_byte;
    volatile bool rx_ready;
    
    void UART_IRQHandler(void) {
        rx_byte = UART->DR;   /* on many UARTs, reading the data register clears the interrupt */
        rx_ready = true;
    }
    
  10. What happens when you shift a negative number right or left in C?

    Right-shifting a negative signed value is implementation-defined; GCC and most compilers copy the sign bit, so -8 >> 1 is -4. Left-shifting a negative value is undefined behavior in C17.

    C
    #include <stdio.h>
    
    int main(void) {
        int n = -8;
        unsigned int u = 0xF0000000u;
        printf("%d\n", n >> 1);        /* implementation-defined, GCC: -4 */
        printf("0x%X\n", u >> 4);      /* unsigned: always fills with zeros */
        return 0;
    }

    On this GCC build it prints -4 and 0xF000000. Shifting by a negative count or by at least the type's width is undefined. Do bit work on unsigned types.

Coddy programming languages illustration

Learn to code with Coddy

Get started