Pointers and Arrays: Stop Saying “Pointers are Just Addresses” This is the “appetizer” for embedded interviews, but 80% of candidates fail to provide a deep answer. For example, when asked about the “difference between pointers and arrays,” simply stating “the array name is a constant pointer” is insufficient; it must be explained in the context of embedded scenarios. High-Frequency Question 1: What is the difference between int a[5] and int *p = a, and what happens with a++ and p++? a is the array name, essentially a “constant pointer to the first element of the array”; its address is determined at compile time and cannot be modified, so a++ will result in a direct error. In embedded systems, arrays are often used to store hardware register addresses or fixed buffers; if addresses could be changed freely, chaos would ensue.
However, p is a variable pointer, and p++ will increment its address by sizeof(int) (for example, 4 bytes on a 32-bit system). In practical projects, it is commonly used to traverse buffers, such as when receiving data via serial communication, using *p++ = data to store data, which is particularly convenient. High-Frequency Question 2: What is a dangling pointer? How can it be avoided? A dangling pointer is a “pointer that points to an uncertain address,” such as an uninitialized pointer or a pointer that is still in use after the memory it points to has been freed. In embedded systems, this can be a “fatal bug”; it can cause the program to crash or even damage hardware (for example, by incorrectly writing to a register address).
To avoid this: initialize pointers directly when defining them, such as int *p = NULL, rather than leaving them uninitialized; after freeing the memory pointed to by a pointer, immediately set the pointer to NULL to prevent it from becoming “dangling”; when passing pointers to functions, always check if (p == NULL), especially in driver functions, as this is a basic operation. Memory Management: In Embedded Systems, “Memory is More Precious than Gold” The memory (RAM/ROM) in embedded devices is usually very limited; for example, a microcontroller may only have a few KB of RAM, and interviewers pay special attention to whether you have “memory awareness.” High-Frequency Question: What is the difference between heap and stack? Why is heap usage minimized in embedded systems? The stack is “managed automatically by the system,” such as local variables and function parameters within functions, which are automatically allocated upon entering the function and released upon exiting, making it fast and preventing fragmentation, but its size is fixed (for example, a microcontroller stack may only be 1KB), and stack overflow will cause a crash; the heap is “manually managed by the programmer,” using malloc to allocate and free memory, which is flexible in size but slower and prone to memory fragmentation. For example, frequently allocating and freeing memory of different sizes can create many “small holes” in memory, leading to situations where memory appears available but cannot be allocated. The core reason for minimizing heap usage in embedded systems is “uncontrollability”: for instance, industrial devices need to run for years without rebooting, and accumulated heap fragmentation will definitely cause problems; also, many compilers for microcontrollers do not support malloc well, which may lead to allocation failures without error reporting, making debugging extremely difficult. In practical projects, we prefer to use “static arrays” (which exist in ROM/RAM and have a size determined at compile time), such as static uint8_t buf[1024], which are stable and worry-free.
Interrupts: The “Emergency Channel” of Embedded Systems High-Frequency Question 1: What is the execution flow of an interrupt? What are the considerations for interrupt service routines (ISRs)? The execution flow is like “you are eating, and the delivery arrives; you put down your chopsticks to get the delivery, and then continue eating”: 1. Hardware triggers an interrupt (for example, a button press or data received via serial communication); 2. The system pauses the currently executing task and saves the context (such as register values); 3. Jumps to the corresponding ISR to execute; 4. After execution, restores the context and continues from where it was paused. Considerations for ISRs: Keep the code short: do not write loops or delays in ISRs; for example, in a serial receive interrupt, just do the task of “storing data in the buffer” and leave data processing to the main function; execution must be fast: ISRs have high priority; if they occupy the CPU, other tasks cannot run; for example, if a timer interrupt triggers every 1ms, the ISR execution time must be less than 1ms; minimize function calls: avoid calling complex functions in ISRs, especially printf (which takes a lot of time) and malloc (which is uncontrollable); minimize the use of global variables: if necessary, always add the volatile keyword to prevent compiler optimization. For example, if you modify flag = 1 in the ISR and check flag in the main function without adding volatile, the compiler may assume flag has not changed and optimize it into an infinite loop. High-Frequency Question: What is interrupt priority? How is it configured? Interrupt priority is the rule of “who cuts in line first”; for example, if both a serial interrupt and a timer interrupt arrive simultaneously, the one with higher priority executes first. Configuration involves two steps (using STM32 as an example): 1. First configure “preemption priority” and “response priority”: a higher preemption priority can interrupt a lower preemption priority interrupt; if the preemption priorities are the same, the response priority determines which executes first; 2. Assign priorities to specific interrupts, such as HAL_NVIC_SetPriority(UART1_IRQn, 1, 0), indicating that the serial 1 interrupt has a preemption priority of 1 and a response priority of 0. In practical projects, follow the principle that “urgent tasks have higher priority”; for example, a motor control interrupt has a higher priority than a button interrupt, as failure to control the motor could lead to safety issues.
RTOS: The Soul of “Multitasking” in Embedded Systems Nowadays, in embedded interviews, whenever “multitasking” is involved, RTOS (such as FreeRTOS, uCOS) is a must-ask topic. Interviewers do not test you on “how to port RTOS” but rather on “how to use RTOS correctly.” High-Frequency Question: What are the task scheduling algorithms in FreeRTOS? What is time-slice scheduling? The most commonly used algorithm in FreeRTOS is “priority-based preemptive scheduling”; the core logic is that “when a high-priority task becomes ready, it immediately preempts the CPU from a low-priority task.” However, there is also “time-slice scheduling,” specifically for “tasks of the same priority”: each task is allocated a “time slice” (for example, 10ms), and after executing for one time slice, it voluntarily yields the CPU to allow the next same-priority task to execute. For example, if two tasks of the same priority, A is “displaying data” and B is “reading sensors,” are given a time slice of 10ms, A runs for 10ms, then switches to B for 10ms, allowing both tasks to execute without blocking each other. However, be careful not to set the time slice too short (for example, 1ms), as it would lead to excessive task switching and high CPU overhead; nor too long (for example, 100ms), as it would slow down task responsiveness. High-Frequency Question: What are semaphores, queues, and mutexes? In what scenarios are they used? These three are the “tools” in RTOS, used to solve the problem of “how tasks cooperate”; do not confuse them: Semaphores: like a “pass”, used for “synchronization” or “mutual exclusion”. Synchronization scenario: for example, task A needs to wait for sensor data to be ready before executing; task B (in an interrupt) collects the data and gives a semaphore “a pass”; task A can only continue running after obtaining the pass;
Mutual exclusion scenario: for example, if two tasks need to access the serial port, a “binary semaphore” (with only one pass) is added to the serial port; one task can access it while the other must wait, preventing data corruption. Queues: like a “delivery box”, used to “pass data”. For example, if task A collects temperature and humidity data to pass to task B for display, a queue is used to “package” the data and send it—queues can store multiple data items and transmit them in a first-in-first-out (FIFO) order, making them particularly suitable for passing structures and arrays between tasks, such as xQueueSend(queue_handle, &data, 0). Mutexes: a “magic tool” specifically designed to solve the problem of “priority inversion”. For example, if a high-priority task A needs to access a resource that a low-priority task B is currently using, and a medium-priority task C preempts B’s CPU, A will be stuck waiting for B to release the resource. This is priority inversion.
Using a mutex, when B acquires the lock, it temporarily raises its priority to match A’s, preventing C from preempting the CPU. After B finishes using the resource and releases the lock, A can execute immediately. In practical projects, always use mutexes when accessing shared memory or peripherals; otherwise, debugging can become extremely difficult. High-Frequency Question: What common mistakes occur when designing RTOS tasks?
Using “infinite loops without delays” in tasks: for example, task A while(1){ process data; }, without adding vTaskDelay(), will occupy the CPU continuously, preventing other tasks from running—correct practice is to add a delay after processing data, such as vTaskDelay(10), to yield the CPU to others. Setting the task stack too small: for example, if a task is allocated only 128 bytes of stack, but the task contains a large array, it will directly cause stack overflow and crash the program. In practical projects, use FreeRTOS’s uxTaskGetStackHighWaterMark() function to check the “remaining stack space” and ensure at least 20% is free. Using blocking APIs in interrupts: for example, calling xQueueSend() in an ISR with a timeout set to portMAX_DELAY (infinite wait) will cause a crash, as ISRs cannot block—correctly, use “interrupt-safe APIs,” such as xQueueSendFromISR(), with a timeout set to 0. Hardware Interfaces: The Ability to “Communicate with Hardware” in Embedded Systems 1. UART: The Most Common “Serial Port” High-Frequency Question: What do baud rate, data bits, stop bits, and parity bits mean? How do you troubleshoot serial communication failures in practical projects? Baud rate: how many bits are transmitted per second; for example, 9600bps means 9600 bits per second, with 115200bps being commonly used in practical projects for speed and stability; data bits: how many bits are transmitted per byte, typically 8 bits (corresponding to one byte); stop bits: the “end marker” added after each byte, commonly 1 bit; parity bits: used to check if data has been transmitted incorrectly, such as odd parity (the number of 1s in data bits + parity bit is odd), commonly used in industrial scenarios, while sometimes disabled in civilian scenarios. To troubleshoot serial issues, follow the “three-step approach”: 1. First check the hardware: use a multimeter to check if the TX/RX pins are reversed (for example, if A’s TX is connected to B’s RX, it will definitely not work) and check for cold solder joints; 2. Then check the parameters: confirm that the baud rates, data bits, etc., on both sides are completely consistent; for example, if one side is 9600 and the other is 115200, communication will fail; 3. Finally, check the software: use an oscilloscope to see if there is a waveform on the TX pin; if there is a waveform, it indicates that the sending end is fine; if there is no waveform, check the initialization code (such as GPIO configuration, UART clock); if there is a waveform but it cannot be received, check if the receiving end’s interrupt is enabled and if the buffer has overflowed. High-Frequency Question: What are the differences between I2C and SPI? In what scenarios are they used?
First, look at the “hardware differences”: I2C: two lines (SDA data line, SCL clock line), supports multiple masters and slaves, each slave device has a unique address (for example, 0x48), hardware is simple but slow (for example, standard mode 100kbps, fast mode 400kbps); SPI: four lines (SCLK clock, MOSI master out slave in, MISO master in slave out, CS chip select), can only have one master and multiple slaves, using the CS line to select devices, faster (for example, from several Mbps to tens of Mbps), but hardware is a bit more complex. Then consider “scenario selection”: Use I2C: for sensors (such as temperature and humidity sensor SHT30), EEPROM, as these devices do not have high-speed requirements and I2C uses fewer lines, saving PCB space; Use SPI: for displays (such as OLED screens), Flash (such as W25Q64), as these devices require high-speed data transmission, such as OLED screens needing to refresh quickly, which SPI can meet. In practical projects, the most common issue with I2C is “bus arbitration failure”; for example, if two master devices send data simultaneously, software must add “bus busy detection” to check if SDA/SCL are free before sending data.