Are you preparing for an Embedded C Interview? As someone who has been on both ends of the interview process; as a candidate who has been asked several questions and as an interviewer who has had to sit through many candidate interviews, I have created this blog that covers everything from the basics through advanced concepts.
This blog will help new graduates through their first job as well as experienced candidates preparing for an Embedded Systems, Firmware and/or IoT positions. Let’s start!
Read Also: What is C# Programming Language?
If you're just starting out, interviewers usually focus on core language fundamentals and basic hardware-interaction concepts before moving into anything advanced. The questions below cover the essentials every Embedded C beginner should be comfortable explaining.
A segmentation fault (Segmentation Violation or SIGSEGV) is a runtime error that occurs when a program tries to access memory that it is not allowed to access. This usually happens due to invalid memory operations such as dereferencing a NULL pointer, accessing freed memory, writing to read-only memory, or accessing memory outside an array's bounds.
In desktop operating systems like Linux, the operating system detects this illegal access and terminates the program by generating a segmentation fault.
However, in many embedded systems, there is no operating system or memory protection unit (MPU). Instead of showing a segmentation fault, the system may simply crash, reset, or enter a Hard Fault or Bus Fault exception.
For example:
|
#include <stdio.h> int main() { int *ptr = NULL; printf("Trying to access NULL pointer...\n"); *ptr = 10; // Invalid memory access return 0; } |
Startup code is the first piece of code executed immediately after a microcontroller resets, even before the main() function runs.
Its purpose is to prepare the hardware and software environment so that the application can execute correctly.
Typical startup code performs tasks such as:
Initializing the stack pointer
Copying initialized global variables from Flash to RAM
Initializing uninitialized global variables (BSS section) to zero
Setting up the interrupt vector table
Configuring the system clock (sometimes)
Calling constructors for C++ programs
Finally calling main()
Without startup code, global variables and the runtime environment would not be initialized correctly.
Example:
|
#include <stdio.h> int main() { printf("Application Started\n"); return 0; } |
ISR stands for Interrupt Service Routine.
It is a special function that automatically executes whenever a specific interrupt occurs.
Interrupts allow the CPU to respond immediately to important hardware events without continuously checking them in a loop.
Examples include:
Timer overflow
UART data received
Button press
ADC conversion complete
A good ISR should:
Execute quickly
Avoid blocking operations
Avoid large computations
Avoid infinite loops
Return control to the interrupted program as soon as possible
Example:
|
#include <stdio.h> void Timer_ISR() { printf("Timer Interrupt Executed\n"); } int main() { printf("Program Running...\n"); // Simulating interrupt Timer_ISR(); return 0; } |
Also Read: What is Bash?
A void pointer (void *) is a generic pointer that can store the address of any data type. Since it has no associated data type, it must be typecast before dereferencing. Void pointers improve flexibility because the same function can work with different data types.
Embedded Applications:
Generic drivers
Dynamic memory handling
Device-independent APIs
Callback functions
Communication libraries
The volatile keyword tells the compiler that the value of a variable may change unexpectedly outside the current program flow.
Therefore, the compiler must always read the value directly from memory instead of optimizing it into a CPU register.
It is commonly used for:
Hardware registers
Variables modified inside ISRs
Shared variables
Memory-mapped I/O
Although both are type qualifiers, they serve different purposes.
| Feature | const | volatile |
| Purpose | Prevents modification | Prevents compiler optimization |
| Value | Should not change through the program | May change unexpectedly |
| Compiler behavior | Protects against accidental writes | Always reloads value from memory |
| Common use | Lookup tables, configuration values | Hardware registers, ISR variables |
| Memory access | Can be optimized | Cannot be optimized |
Storage classes define the scope, lifetime and visibility of variables and functions.
The main storage classes are:
Default for local variables
Stored on the stack
Exists only inside the function
Lifetime lasts throughout the program
Local static variables retain their value between function calls
Static global variables are visible only within the source file
Declares a variable defined in another source file.
Suggests storing the variable in a CPU register for faster access. Modern compilers generally decide register allocation automatically.
|
#include <stdio.h> void counter() { static int count = 0; count++; printf("%d\n", count); } int main() { counter(); counter(); counter(); return 0; } |
Related Article: How to Install R on Windows, Mac OS X, and Ubuntu?
Interrupt latency is the time between the occurrence of an interrupt and the execution of its corresponding ISR.
It includes:
Interrupt detection
Completing the current instruction
Saving CPU context
Fetching the ISR address
Jumping to the ISR
Lower interrupt latency is important for real-time embedded systems.
Factors affecting interrupt latency include:
Interrupt priority
Long critical sections
Disabled interrupts
CPU architecture
Operating system overhead
The variable should be defined in one source file and declared as extern in another.
file1.c
| int counter = 0; |
file2.c
| extern int counter; |
Now both files can access the same global variable.
Typically, the extern declaration is placed inside a header file.
Embedded C is an extension of the C programming language used to develop software for embedded systems such as microcontrollers, automotive ECUs, IoT devices, medical equipment and consumer electronics.
It includes hardware-specific programming concepts like:
GPIO control
Interrupt handling
Timers
UART, SPI and I2C communication
Memory-mapped registers
Real-time execution
Unlike general-purpose C programs, Embedded C interacts directly with hardware and often runs without a full operating system.
| Feature | C Language | Embedded C |
| Purpose | General-purpose programming | Embedded system programming |
| Platform | PCs, servers, operating systems | Microcontrollers and embedded devices |
| Hardware Access | Limited | Direct register and peripheral access |
| Memory | Relatively abundant | Limited and carefully managed |
| Execution | Usually OS-based | Often bare-metal or RTOS-based |
| Real-Time Constraints | Usually not critical | Frequently critical |
Once the basics are solid, interviewers dig into how you handle memory, timing and concurrency in real hardware-facing code. These questions test whether you understand why Embedded C behaves differently from application-level C, not just the syntax.
Memory-mapped registers are hardware control and status registers that are assigned specific addresses in the microcontroller's memory map. Instead of using special instructions, the CPU reads and writes to these registers using normal load/store operations, just like accessing regular memory. In Embedded C, they are typically accessed using pointers cast to the register's address and the pointer is declared volatile so the compiler never caches or optimizes away the access.
Stack memory is used for local variables, function parameters and return addresses. It grows and shrinks automatically as functions are called and return and allocation/deallocation is extremely fast since it just involves moving a pointer. Heap memory is used for dynamic allocation (malloc/free) and must be managed manually. It can grow unpredictably and become fragmented over time. In embedded systems, stack usage is preferred and dynamic heap allocation is generally avoided or used very sparingly, because:
RAM is limited, so fragmentation can quickly exhaust memory.
malloc/free have non-deterministic execution time, which is unacceptable for real-time systems.
A failed allocation deep in the code is much harder to detect and recover from in a bare-metal system. Most embedded projects allocate memory statically or use fixed-size memory pools instead of relying on the heap.
Local variables are declared inside a function, stored on the stack and exist only for the duration of that function call. Use them for temporary, function-scoped data.
Global variables are declared outside all functions, stored in a fixed data/BSS memory region and are visible throughout the program (or file, if declared static). Use them for state that must be shared across functions or ISRs, but sparingly, since excessive globals make code harder to maintain and debug.
Static variables retain their value between function calls (static locals) or restrict visibility to the file they're declared in (static globals). Use them for counters, flags, or module-private data that shouldn't be exposed elsewhere.
Register variables are a hint to the compiler to keep a variable in a CPU register for faster access, typically used for variables accessed very frequently inside tight loops. Modern compilers usually make this decision automatically, so explicit use is rare today.
Also Read: What is Command Prompt? Windows CMD Guide for Beginners
A race condition occurs when two or more execution contexts (main loop and an ISR, or two ISRs, or two RTOS tasks) access and modify a shared resource concurrently and the final outcome depends on the unpredictable timing of that access. In embedded systems this often shows up as corrupted shared variables or inconsistent peripheral states. It can be prevented by:
Disabling interrupts briefly around critical sections that touch shared data.
Using atomic operations where the hardware/compiler supports them.
Using mutexes or semaphores when working with an RTOS.
Declaring shared variables as volatile so they're always read fresh from memory.
Keeping critical sections as short as possible to minimize the time interrupts are blocked.
A watchdog timer (WDT) is a hardware timer that resets the microcontroller if the software fails to "feed" or "kick" it within a specified time interval.
The application periodically resets the watchdog counter during normal operation; if the firmware hangs, crashes, or gets stuck in an infinite loop, it stops feeding the watchdog and the WDT triggers a system reset. It's important because embedded devices often run unattended for long periods (industrial controllers, IoT sensors, automotive ECUs) and a watchdog provides a last line of defense that automatically recovers the system from software faults without needing manual intervention.
Bitwise operators (&, |, ^, ~, <<, >>) operate directly on the individual bits of a variable. They are heavily used in Embedded C because most hardware registers are controlled at the bit level. Common uses include:
Setting a bit: reg |= (1 << n);
Clearing a bit: reg &= ~(1 << n);
Toggling a bit: reg ^= (1 << n);
Checking a bit: if (reg & (1 << n))
Masking specific bit fields within a register, for example extracting a 4-bit field with (reg >> offset) & 0xF These operations are efficient (single machine instructions) and allow precise, safe manipulation of individual configuration bits without disturbing the rest of the register.
Optimizing Embedded C code means reducing execution time while using as little RAM and Flash memory as possible without compromising reliability.
To improve speed, I would:
Choose efficient algorithms and data structures instead of relying only on compiler optimizations.
Minimize unnecessary loops and repeated calculations.
Use interrupts instead of continuously polling hardware when appropriate.
Avoid expensive operations like floating-point arithmetic on MCUs without an FPU.
Use compiler optimization flags such as -O2 or -Os depending on whether speed or code size is more important.
To reduce memory usage, I would:
Use the smallest suitable data types, such as uint8_t instead of int when the value range allows.
Avoid dynamic memory allocation (malloc() and free()) because they can cause memory fragmentation.
Store constant data in Flash using the const keyword.
Reuse buffers wherever possible instead of allocating multiple copies.
Remove unused variables and unnecessary library functions.
Finally, I would profile the code using debugging and profiling tools instead of optimizing blindly. Optimization should always be based on measurements.
Polling and interrupt-driven programming are two methods of detecting and handling hardware events.
In polling, the processor repeatedly checks the status of a device or peripheral to determine whether an event has occurred. Since the CPU continuously performs these checks, it may waste processing time when no event is present.
In interrupt-driven programming, the processor continues executing other tasks until the hardware generates an interrupt. The CPU temporarily pauses its current task, executes the Interrupt Service Routine (ISR) to handle the event and then resumes normal execution.
I would choose polling when:
The application is simple and easy to manage.
Events occur very frequently and are expected continuously.
Precise timing is predictable.
The hardware does not support interrupts.
I would choose interrupt-driven programming when:
Fast response to external events is required.
Multiple peripherals need to operate simultaneously.
CPU efficiency is important.
The system needs to reduce power consumption by avoiding continuous monitoring.
Why this answer stands out: It explains both concepts, compares their advantages and disadvantages and shows the ability to choose the appropriate approach based on the application's requirements.
Also Read: What is Selenium? Components, Uses and Limitations
Endianness refers to the order in which the bytes of a multi-byte data value are stored in memory.
In a little-endian system, the least significant byte is stored at the lowest memory address.
In a big-endian system, the most significant byte is stored at the lowest memory address.
This difference becomes important whenever data is exchanged between systems that use different byte orders.
In embedded systems, endianness matters because:
Devices communicating over interfaces such as UART, SPI, I²C, CAN, or Ethernet may use different byte orders.
Binary files and communication protocols often define a specific byte order that must be followed.
Data exchanged between different processors or microcontrollers may require byte conversion.
Incorrect handling of endianness can result in corrupted data or communication errors.
A good embedded developer always checks the byte order expected by the hardware or communication protocol before transmitting or processing data.
Why this answer stands out: It not only defines endianness but also explains its practical importance in real embedded applications.
Pointer arithmetic refers to performing arithmetic operations on pointers to move through memory locations. When a pointer is incremented or decremented, it automatically moves by the size of the data type it points to rather than by a single byte.
Pointer arithmetic is widely used in Embedded C because embedded applications frequently access memory directly.
Some common use cases include:
Traversing arrays efficiently.
Reading or writing communication buffers used by UART, SPI, CAN, or I²C.
Accessing memory-mapped peripheral registers.
Developing hardware drivers.
Processing sensor data stored in continuous memory locations.
Managing DMA buffers and packet data.
One important point is that pointer arithmetic depends on the pointer's data type. For example, incrementing a pointer to an 8-bit data type advances it by one byte, while incrementing a pointer to a 32-bit data type advances it by four bytes.
Why this answer stands out: It explains the concept clearly, highlights practical embedded applications and demonstrates an understanding of how pointers interact with memory, which is a fundamental skill for Embedded C development.
At the experienced level, interviewers expect you to reason about subtle compiler behavior, hardware-software interaction and edge cases that only show up in production systems. These questions probe deeper judgment rather than textbook definitions.
Yes. This can be achieved by making the pointer itself constant using the const qualifier. A constant pointer always points to the same memory location, preventing accidental reassignment. If required, the data being pointed to can also be declared as constant to prevent modification.
An experienced developer should understand the difference between:
A constant pointer
A pointer to constant data
A constant pointer to constant data
They should also mention that const provides compile-time protection and does not prevent memory corruption caused by invalid writes or hardware faults.
A segmentation fault occurs when a program attempts to access memory that it is not allowed to access. While segmentation faults are common in embedded Linux systems, most bare-metal microcontrollers do not generate segmentation faults. Instead, they typically trigger exceptions such as HardFault or BusFault.
Common causes include:
Dereferencing a NULL pointer
Accessing an uninitialized or invalid pointer
Buffer overflow
Array index out of bounds
Stack overflow
Accessing freed memory
Writing to read-only memory
Calling an invalid function pointer
Corrupted stack due to memory corruption
A strong candidate should explain that the exact behavior depends on whether the system has memory protection hardware such as an MPU or MMU.
Also Read: Factorial Program in Python
No. Using printf() inside an Interrupt Service Routine (ISR) is generally discouraged.
printf() is a relatively slow function and often consumes significant stack space. It may also use internal locks, perform blocking operations, or access shared resources, all of which increase interrupt latency. Longer ISRs delay the servicing of other interrupts and can negatively affect the real-time behavior of the system.
A well-designed ISR should perform only the minimum work required, such as reading hardware data or setting a flag, while more time-consuming tasks like logging or printing should be handled by the main program or a background task.
No. An ISR is invoked directly by the processor in response to a hardware interrupt, not by another function. Because of this, it does not follow the normal C function calling mechanism.
Instead of accepting parameters or returning values, ISRs typically communicate with the rest of the application through:
Global variables
Volatile flags
Ring buffers
Queues
Semaphores or event flags in RTOS-based systems
An experienced candidate should mention that ISRs follow a hardware-defined interrupt entry and exit sequence rather than the standard function calling convention.
Virtual memory is a memory management technique where applications use virtual addresses instead of physical addresses. The processor's Memory Management Unit (MMU) translates virtual addresses into physical memory addresses.
Most small embedded microcontrollers, such as ARM Cortex-M, AVR, PIC and MSP430, do not support virtual memory because they do not contain an MMU. Instead, they use fixed memory mapping.
Virtual memory is commonly available in embedded systems running operating systems like embedded Linux on processors such as ARM Cortex-A.
The major advantages include:
Process isolation
Memory protection
Efficient memory utilization
Shared libraries
Demand paging
On systems without an MMU, memory protection is usually achieved using a Memory Protection Unit (MPU) or carefully designed linker scripts.
A strong candidate should clearly distinguish between an MMU and an MPU.
|
int square (volatile int *p){ return (*p) * (*p) ; } |
The issue is that the volatile variable is read twice. Since the value is declared as volatile, the compiler must fetch it from memory every time it is accessed rather than storing it in a register.
If the value changes between the two reads—for example, due to an interrupt or hardware update—the multiplication may use two different values, resulting in an incorrect or inconsistent result.
A better approach is to read the volatile value once into a local variable and then perform all calculations using that local copy.
An experienced candidate should also point out that volatile prevents compiler optimization but does not guarantee atomicity or thread safety
|
__interrupt double calculate_circle_area (double radius){ double circle_area = PI ∗ radius ∗ radius; printf ( 'Area = %f ' , circle_area); return circle_area; } |
The code is incorrect for several reasons:
An ISR cannot accept function parameters because it is invoked by hardware.
An ISR cannot return a value because execution resumes at the interrupted instruction rather than returning to a caller.
Performing floating-point calculations inside an ISR increases execution time and may require saving additional processor registers.
Calling printf() inside an ISR is poor practice because it is slow, may not be reentrant and significantly increases interrupt latency.
A well-designed ISR should execute quickly, avoid blocking operations and defer complex processing to the main application or a lower-priority task.
Read Also: What is Django?
Memory alignment refers to storing data at memory addresses that match the processor's preferred alignment requirements. Proper alignment allows the CPU to access data more efficiently.
Incorrect or unaligned memory access can lead to:
Reduced performance
Additional memory access cycles
Alignment exceptions on some processors
Hardware faults on architectures that do not support unaligned access
Compilers often insert padding bytes inside structures to satisfy alignment requirements. Although this increases memory usage, it improves execution speed and ensures compatibility with the processor's memory access rules.
A strong candidate should also mention that packed structures reduce memory usage but may reduce performance or even cause faults on certain architectures.
nterrupt latency is the time between the occurrence of an interrupt and the execution of the first instruction of its ISR.
Common causes of interrupt latency include:
Higher-priority interrupts already being serviced
Interrupts temporarily disabled
Long-running ISRs
Context saving and restoring
Cache misses in high-performance processors
Pipeline flushing
RTOS scheduling overhead
Slow memory access
Interrupt latency can be reduced by:
Keeping ISRs as short as possible
Minimizing critical sections where interrupts are disabled
Assigning appropriate interrupt priorities
Avoiding blocking or lengthy operations inside ISRs
Using DMA to reduce processor workload
Deferring non-critical processing to background tasks or threads
An experienced engineer should also distinguish between interrupt latency and interrupt service time, as they are different performance metrics.
In modern Embedded C, this statement is generally a misconception. With optimizing compilers, both expressions usually generate identical machine code, so there is typically no measurable performance difference.
The real difference lies in their behavior:
++i increments the variable itself.
i + 1 simply computes a new value without modifying the original variable.
The belief that ++i is faster comes from older compilers that generated less optimized code. Modern compilers optimize both expressions effectively.
A strong candidate should explain that writing clear, maintainable code is more important than relying on outdated optimization myths. They may also mention that the distinction between pre-increment (++i) and post-increment (i++) is more relevant in C++, where post-increment can involve creating temporary objects, whereas in Embedded C, the difference is generally negligible for primitive data types.
Read Also: What is Servicenow?
Beyond syntax and definitions, senior interviews often present real-world firmware problems to see how you approach debugging, trade-offs and system design under constraints like power, memory and timing. The scenarios below reflect the kind of open-ended questions you can expect.
In developing an OTA update process, preventing a device from being rendered unusable by an incomplete upgrade would be the first priority of my OTA upgrade design.
Rather than overwriting the firmware currently in use, I would be using an A/B (dual bank) firmware configuration for upgrade.
This keeps the "old version" in one part of memory and the "new version" is downloaded into the opposite part of memory until after the download is completed and verified to not being corrupt or altered via CRC or digital signature, I would mark the new image as ready to use via a bootable flag so that the Boot Loader would boot the new firmware on the next power cycle.
If there was a power loss at any point during the upgrade process, the Boot Loader would detect that the new image is invalid or incomplete and boot the old version of the firmware, continuing to run the last known working version of the firmware until the new version could be tried again.
Additionally, I would include a roll back procedure so that if the upgraded firmware was booted and continues to fail, after 3 failed boots the Boot Loader would automatically restore the previous working version of the firmware.
By that stated design, the device always have at least one valid version of firmware to boot from even in loss of power situations.
I would start by measuring the device's current usage in other operating modes instead of just guessing. Initially, I want to check how much power is used when active, idle, sleeping, reading a sensor, sending/receiving data via wireless methods and waking up from sleep mode to determine the best operating mode.
Next, I want to see if the microcontroller is properly entering its low-power states. I also want to check if the UART, ADC, GPIO or timer is stopping it from going into sleep. I want to confirm that there aren't any unnecessary interrupts or frequent wake-ups, which keep the CPU awake.
If a wireless connection is used, I want to analyze how often the data is sent, as Bluetooth or Wi-Fi connections usually draw a great deal of power. I would want to either batch the sending of data or send it only periodically. I would look at how the sensor sampling occurred to ensure that the sensors are only powered when needed and disable any peripherals that are not needed before entering sleep mode.
Finally, I would compare the new power consumption measurements against the original measurements to verify that changes made to the firmware result in meeting expected battery life.
Once I have identified an intermittent, reproducible issue, I would investigate what interrupts are active at the time of an application crash using some combination of debug tools (such as a logic analyzer), logging and trace. Once I’ve determined which interrupts are most likely involved, I would check for the following common causes of problems with interrupts:
Race conditions in interrupt handling.
Global variables accessed by multiple ISRs without appropriate protection.
Conflicts between interrupt priorities for resources.
Stack overflow when an ISR tries to push too much data to the stack; or the ISR does so because it runs longer than the time slice allowed by the scheduler.
Lengthy ISRs.
I believe that ISRs should be designed to run in the shortest possible time; therefore, I would capture the event, clear the interrupt flag and notify either the main application or an RTOS task of the event for further processing instead of performing any significant processing within the ISR.
If multiple interrupts are attempting to share resources, I would use appropriate synchronisation mechanisms (for example, mutexes) to protect critical sections, or I would temporarily disable interrupts for those ISR code sections requiring it, if that can sufficiently resolve the issue.
I would also verify that higher-priority (real-time) interrupts will not be blocked by lower-priority (non-real-time) interrupts based on the current priority scheme.
Lastly, I would perform stress testing (applying as many interrupts as possible) to verify that the firmware continues to operate correctly under the worst-case scenario.
Read Also: UiPath Tutorial For Beginners
To begin with, I would categorize tasks as time-critical or time-tolerant. The processing of sensor data being time-critical (has an absolute deadline of two milliseconds), I will allocate it the highest possible priority by triggering it via a timer (hardware) or a high-priority Real-Time Operating System (RTOS) task.
Sending and receiving data via the Universal Asynchronous Receiver/Transmitter (UART) will be done using interrupt-driven and/or Direct Memory Access (DMA) approaches to prevent the CPU from being blocked while exchanging data.
Writing to flash memory will introduce long delays, thus will be implemented as a lower-priority background task. Rather than writing data directly into flash memory, data will first be stored within a ring buffer and then written to flash memory at a later time; this process is asynchronous.
I will avoid using blocking functions and utilize queues and/or message buffers for inter-task communication.
If an RTOS is used the real-time tasks will be assigned the highest priority (sensor processing) enabling the remaining tasks (UART communication and flash memory logging) to execute whenever the CPU is available.
Finally, I will monitor the CPU utilization and analyze worst-case execution time to ensure all tasks meet their timing constraints at the maximum system load.
Security should be considered from the beginning of the firmware design rather than added later. I would store sensitive information such as Wi-Fi credentials in encrypted form using secure storage or hardware security features available on the microcontroller. Communication with cloud services or mobile applications would always use encrypted protocols such as TLS so that credentials and commands cannot be intercepted.
To prevent unauthorized firmware modifications, I would implement Secure Boot, where the bootloader verifies the firmware's digital signature before execution. For OTA updates, I would only accept firmware images that are digitally signed by the manufacturer and reject any unauthorized or modified firmware.
I would also implement authentication and authorization for remote commands so that only trusted users or servers can control the device. Additionally, I would disable unnecessary debug interfaces such as JTAG or SWD in production devices or protect them with appropriate security mechanisms.
Finally, I would follow secure coding practices, validate all external inputs, protect against buffer overflows and regularly update the firmware to fix newly discovered vulnerabilities. These measures help protect both sensitive user data and the overall integrity of the device.
An embedded C interview will evaluate your knowledge of embedded C programming basics, hardware familiarity and common sense in debugging. To prepare yourself for these types of interviews, don't simply memorize definitions. Learn the rationale for each item so you can defend your answers (e.g., why volatile is important, why interrupt service routines (ISRs) should have minimal execution time and why memory management is critical). Your interviewers will follow this path as they probe deeper with more complex questions.
Also Read: Why Businesses Need Robotic Process Automation (RPA)?
The one key area many embedded C interviewers look at is whether candidates have real-world experience and knowledge of hardware/software interaction with the volatile keyword, memory-mapped registers, and ISR (Interrupt Service Routine) behavior (which helps measure actual/on-the-job experience, etc.) as opposed to just having knowledge of the syntax rules of C.
Yes, Embedded C and C share the same basic syntax; however, Embedded C is designed for direct access to hardware, handle interrupts, perform memory-mapped I/O, and operate in real time—none of which, most PC-based C programming does in their normal use case.
Should focus on understanding tradeoffs that occur with firmware design in the real world (e.g. power optimization, safety of OTA updates, and reliability of interrupts) instead of just memorizing answers—your problem-solving method will be more important than listing a textbook definition or repetitive answers.