Welcome to ESP32 Core Board repository
11.Priority Inversion
Priority Inversion
Priority Inversion1. Overview of Priority Inversion1.1 Why Learn This?1.2 Real-World Horror Stories1.3 What This Project Demonstrates2. Core Concepts2.1 Priority Inversion Scenario2.2 Why This Is a Problem2.3 Priority Inheritance for Mutexes2.4 Task Priority API3. Project Architecture and Flow4. Hardware Dependencies5. Source Code Explanation5.1 Header Files and Macro Configuration5.2 vTaskA() — High Priority Task (Prio 3)5.3 vTaskB() — Medium Priority Task (Prio 2)5.4 vTaskC() — Low Priority Task (Prio 1)5.5 app_main() — Main Entry6. Operation Flow6.1 Build and Flash Process6.2 Wiring Overview6.3 Detailed Operation Flow6.4 Complete Log Example6.5 Quantifying Priority Inversion Damage6.6 How Much Can FreeRTOS's Priority Inheritance Help?7. Common Issues
1. Overview of Priority Inversion
1.1 Why Learn This?
Priority Inversion is the most notorious scheduling anomaly in real-time systems. Unlike deadlock—which at least "kills cleanly" and lets you detect it immediately—priority inversion is "half-dead": a high-priority task that should respond immediately gets indefinitely delayed by an unrelated medium-priority task.
Its danger lies in deception—when you set task A to the highest priority (3), you naturally expect A to respond immediately. But priority inversion reveals: A's response time = C's (low priority) lock-holding time + B's (medium priority) execution time. A's priority is effectively meaningless.
1.2 Real-World Horror Stories
| Event | Year | Consequence |
|---|---|---|
| NASA Mars Pathfinder | 1997 | Priority inversion caused the weather data collection task to be indefinitely blocked by the communication task. System bus timeout → watchdog reset → repeated restarts. The lander nearly became scrap metal on Mars due to a single software bug. The NASA JPL team spent three days remotely debugging to identify the priority inversion, and performed a remote hotfix by changing the mutex to priority inheritance mode. This is the most famous priority inversion case in embedded software history. |
| A certain automotive ADAS system | 2018 | Image recognition task (high priority) was preempted by UI rendering task (medium priority) while waiting for a lock. Collision warning delayed by 300ms → at 120km/h, the car traveled an additional 10 meters → equivalent to emergency braking 10 meters too late |
| A certain medical infusion pump | 2015 | Infusion rate monitoring task priority inversion → abnormal flow detected with a 2-second delay → excessive infusion → FDA recall |
NOTE: The Mars Pathfinder story teaches us one thing: Priority inversion doesn't require complex code—just three tasks and one lock. You don't even need to write any logic incorrectly—FreeRTOS's default mutex behavior alone can cause inversion.
1.3 What This Project Demonstrates
This project uses 3 tasks (A/B/C) + 1 mutex + 1 button + 2 GPIO pins, with WS2812 LED colors to visually demonstrate the entire priority inversion process: Green light (C holds lock) → Blue light (B preempts C) → A is now indirectly blocked by B, red light doesn't turn on.
2. Core Concepts
2.1 Priority Inversion Scenario
xPriority: A(3) > B(2) > C(1)Shared Resource: Mutex xMutex (shared by A and C)Expected Behavior (B doesn't exist):C takes lock → executes → releases lock → A takes lock → executesActual Behavior (B preempts midway):C takes lock ──► B preempts C ──► A waits for lock (held by C)(green light) (blue light) (red light off)→ A is indirectly blocked by B → Priority Inversion:High-priority A's actual blocking time = B's execution time (unpredictable)
2.2 Why This Is a Problem
In real-time systems, high-priority tasks have strict deadlines. Priority inversion causes a high-priority task's response time to be determined not by itself, but by when an unrelated medium-priority task releases the CPU. In extreme cases, this can cause system crashes.
2.3 Priority Inheritance for Mutexes
FreeRTOS's mutex has a built-in Priority Inheritance mechanism:
xxxxxxxxxxNormal mutex behavior:C(1) takes lock → B(2) preempts C → A(3) tries to take lock → kernel detects→ temporarily elevate C's priority to 3 → C runs to completion and releases lock→ A takes lock → C restores to priority 1
But priority inheritance cannot eliminate inversion, it can only shorten the duration of inversion. A more fundamental solution is to prevent low-priority tasks from holding locks for extended periods.
2.4 Task Priority API
xxxxxxxxxx// A high priority// B medium priority// C low priority
// Create task with specified priority (higher=higher)xTaskCreatePinnedToCore(vTaskA, "TaskA", 2048, NULL, PRIO_A, NULL, 1);3. Project Architecture and Flow
xxxxxxxxxxOn Core 1, 3 tasks run:Task A (Prio 3) — High priority, Red LED (holds lock)Task B (Prio 2) — Medium priority, Blue LED (button-triggered preemption)Task C (Prio 1) — Low priority, Green LED (holds lock)Mutex (xMutex): Shared by A and CFlow (Priority Inversion):C takes lock (green) → button triggers B to preempt C (blue)→ A waits for lock (held by C, C preempted by B)→ A is indirectly blocked by B → Priority Inversion!→ B releases → C resumes (green) → C releases lock → A takes lock (red)During A's lock-holding, B cannot intervene (g_a_has_mutex flag blocks it)
GPIO Roles:
| Pin | Function | Default Level | Description |
|---|---|---|---|
| GPIO 0 | BOOT button | Pulled up=high | Press=trigger B to preempt C |
| GPIO 2 | A release condition | Pulled up=high | Floating=1=hold lock; GND=0=release |
| GPIO 4 | C release condition | Pulled down=low | Floating=0=release; 3.3V=1=hold lock |
| GPIO 48 | WS2812 | — | Red=A, Blue=B, Green=C |
4. Hardware Dependencies
| Pin | Function | Description |
|---|---|---|
| GPIO 0 | BOOT button | Trigger B preemption |
| GPIO 2 | A release condition | Pulled up — connect to GND to release A's lock |
| GPIO 4 | C release condition | Pulled down — connect to 3.3V to hold C's lock |
| GPIO 48 | WS2812 RGB LED | Red=A holds lock, Blue=B preempts, Green=C holds lock |
Required components: freertos/FreeRTOS, freertos/task, freertos/semphr, driver/gpio, led_strip, esp_log
This project uses a third-party library again: For details, refer to (Detailed third-party library installation process: 2.Basic Peripherals→10.RGB LED Effects)
Note:
led_stripis a third-party component provided by the ESP-IDF Component Registry, not built into IDF. Before use, you must declare the dependency inmain/idf_component.yml, otherwise the compiler won't find the header file:xxxxxxxxxx## IDF Component Manager Manifest Filedependenciesidfversion'>=4.1.0'espressif/led_strip^3.0.3After declaration, executing
idf.py buildwill automatically download the component to themanaged_components/directory.
5. Source Code Explanation
Complete source code: Source Code → ESP-IDF → 4.FreeRTOS → 7. RTOS_Priority_inversion
5.1 Header Files and Macro Configuration
This section defines the priorities of three tasks, mutex handle, LED pin, button pin, conditional release pins, and global state flags—these are the configuration core of the entire demonstration project.
xxxxxxxxxx
/* ==================== Log Tag ==================== */
// Log tagstatic const char *TAG = "PRIO_INV";
/* ==================== Mutex ==================== */
// Mutex handleSemaphoreHandle_t xMutex = NULL;
/* ==================== Priority Definitions ==================== */
/* ==================== Hardware Pin Definitions ==================== */
// Button pin (active-low)// Debounce time (ms)
/* ==================== Global State ==================== */static led_strip_handle_t led_strip;static volatile bool g_b_holding = false;static volatile bool g_a_has_mutex = false;5.2 vTaskA() — High Priority Task (Prio 3)
Task A has the highest priority and shares the same mutex with C. It intentionally delays at the start to let C grab the lock first; afterward, it polls GPIO2 to decide whether to hold/release the lock—red light on when holding.
xxxxxxxxxxvoid vTaskA(void *pv){ // Let C grab the mutex first vTaskDelay(pdMS_TO_TICKS(500));
while (1) { // [A] waiting for mutex ESP_LOGI(TAG, "[A] waiting for mutex..."); xSemaphoreTake(xMutex, portMAX_DELAY);
// Flag A holding mutex g_a_has_mutex = true; // [A] Got mutex, RED on ESP_LOGI(TAG, "[A] Get mutex, RED ON"); set_led(150, 0, 0);
// Poll GPIO2, GND=release while (gpio_get_level(COND_GPIO_A) == 1) { vTaskDelay(1); }
// [A] Release mutex ESP_LOGI(TAG, "[A] GPIO2 == 0, Release mutex"); xSemaphoreGive(xMutex); // Clear flag g_a_has_mutex = false;
// Give C a chance vTaskDelay(pdMS_TO_TICKS(2000)); }}5.3 vTaskB() — Medium Priority Task (Prio 2)
Task B is the "troublemaker" in this demonstration—when the button is pressed, it continuously occupies the CPU, simulating a medium-priority task indirectly blocking a high-priority task on an unrelated lock. When A holds the lock, B skips everything (no LED, no logs), ensuring B only preempts when C holds the lock.
xxxxxxxxxxvoid vTaskB(void *pv){ while (1) { if (!g_a_has_mutex && gpio_get_level(KEY_GPIO) == 0) {
// Debounce vTaskDelay(KEY_DEBOUNCE_MS / portTICK_PERIOD_MS);
// Re-check if (!g_a_has_mutex && gpio_get_level(KEY_GPIO) == 0) {
// [B] Key pressed, BLUE on, preempt C ESP_LOGI(TAG, "[B] Key pressed, BLUE ON (preempt C)"); set_led(0, 0, 150); // Flag B preempting g_b_holding = true;
// Keep running while held; exit if A grabs lock while (!g_a_has_mutex && gpio_get_level(KEY_GPIO) == 0) { // [B] holding... ESP_LOGI(TAG, "[B] holding..."); vTaskDelay(pdMS_TO_TICKS(200)); }
// Clear preempt flag g_b_holding = false;
// Restore green only if A hasn't taken over if (!g_a_has_mutex) { // [B] released, restore green ESP_LOGI(TAG, "[B] Key released, restore GREEN"); set_led(0, 150, 0); } } } vTaskDelay(20 / portTICK_PERIOD_MS); }}5.4 vTaskC() — Low Priority Task (Prio 1)
Task C grabs the lock first and lights green, but intentionally yields during lock-holding—this is the core trigger point for priority inversion. At this point, high-priority A is waiting for the lock, and medium-priority B seizes the opportunity to occupy the CPU without letting go. A's priority becomes meaningless.
xxxxxxxxxxvoid vTaskC(void *pv){ while (1) { // [C] waiting for mutex ESP_LOGI(TAG, "[C] waiting for mutex..."); xSemaphoreTake(xMutex, portMAX_DELAY);
// [C] Got mutex, GREEN on ESP_LOGI(TAG, "[C] Get mutex, GREEN ON"); set_led(0, 150, 0); bool prev_b = false; while (gpio_get_level(COND_GPIO_C) == 1) { if (g_b_holding) { if (!prev_b) { // [C] Preempted! ESP_LOGW(TAG, "[C] Preempted by B, holding mutex but blocked!"); } } else { if (prev_b) { // [C] B released, back running ESP_LOGI(TAG, "[C] B released, back running, still holding mutex"); } } prev_b = g_b_holding; // Intentional yield — let B have a chance to preempt vTaskDelay(100 / portTICK_PERIOD_MS); }
// [C] Release mutex ESP_LOGI(TAG, "[C] GPIO4 != 1, Release mutex"); xSemaphoreGive(xMutex);
// Give A a chance vTaskDelay(pdMS_TO_TICKS(2000)); }}5.5 app_main() — Main Entry
The main function initializes four hardware modules in order (LED, button, two conditional pins), creates the mutex, and finally pins all three tasks to Core 1 to demonstrate single-core scheduling.
xxxxxxxxxxvoid app_main(void){ rgb_init(); // KEY init key_init(); cond_gpio_a_init(); cond_gpio_c_init();
// Create mutex xMutex = xSemaphoreCreateMutex();
// All pinned to Core 1 — single-core demo xTaskCreatePinnedToCore(vTaskA, "TaskA", 2048, NULL, PRIO_A, NULL, 1); xTaskCreatePinnedToCore(vTaskB, "TaskB", 2048, NULL, PRIO_B, NULL, 1); xTaskCreatePinnedToCore(vTaskC, "TaskC", 2048, NULL, PRIO_C, NULL, 1);}6. Operation Flow
6.1 Build and Flash Process
For detailed flash process: 1.Development Basics→3.Build and Flash Process→Build and Flash Process
6.2 Wiring Overview
| Pin | Function | Default Level | Role in Demo |
|---|---|---|---|
| GPIO0 | BOOT button | Pulled up=high, pressed=low | Stage 4 hold to trigger B preemption; Stage 7 hold to verify B is blocked |
| GPIO2 | A release condition | Pulled up=high (floating=1=hold) | Stage 7 floating keeps A holding lock |
| GPIO4 | C release condition | Pulled down=low (floating=0=release) | Before Stage 1 connect to 3.3V → C holds lock; Stage 6 connect to GND → C releases lock |
| GPIO48 | WS2812 LED | — | Red=A holds lock, Blue=B preempts, Green=C holds lock |
6.3 Detailed Operation Flow
The entire demonstration is divided into the following stages in chronological order, each stage annotating the required wiring operations and corresponding code logic.
Stage 1: Startup and Task Creation
Wiring requirement: GPIO4 to 3.3V, GPIO2 floating.


app_main() initializes LED, button, GPIO2, GPIO4 in sequence, then creates the mutex, and finally creates all three tasks pinned to Core 1.
After the three tasks are created, they immediately enter the ready state, and the scheduler selects the highest priority ready task to run:
xxxxxxxxxxReady tasks: A(Prio3) B(Prio2) C(Prio1)──► Scheduler selects A (highest priority) ──► executes vTaskA()
Stage 2: A Voluntarily Yields, C Takes Lock
Although task A has the highest priority, its first line of code is vTaskDelay(pdMS_TO_TICKS(500))—A blocks itself for 500ms. The scheduler selects B(Prio2) from the remaining ready tasks to run.
B enters its main loop, checks the button—the button is not pressed (gpio_get_level(KEY_GPIO) == 1), skips the if block, executes vTaskDelay(20/portTICK_PERIOD_MS) and enters brief blocking.
At this point, C(Prio1) becomes the only ready task and gets the CPU:
xxxxxxxxxxC calls xSemaphoreTake(xMutex, portMAX_DELAY)→ Lock is free → C immediately gets the mutex→ ESP_LOGI prints "[C] Get mutex, GREEN ON"→ set_led(0, 150, 0) → WS2812 lights green
C enters while (gpio_get_level(COND_GPIO_C) == 1) loop—since GPIO4 is connected to 3.3V, the condition is true, and C stays in the loop, yielding once every 100ms via vTaskDelay.
System state at this moment:
A: blocked (waiting for 500ms delay to expire)
B: briefly blocked (20 ticks)
C: running, holding mutex, green LED on, yielding every 100ms in while loop
Stage 3: A Wakes Up, Waits for Lock
After 500ms, A's delay expires and A becomes ready. The scheduler sees A(Prio3) > C(Prio1), A preempts C's CPU:
xxxxxxxxxxA executes: xSemaphoreTake(xMutex, portMAX_DELAY)→ Mutex already held by C → A enters blocked state (waiting for lock)→ Scheduler switches back to C to continue running
Although A has the highest priority, the lock is held by C, so A can only wait. System state at this moment:
xxxxxxxxxxA(Prio3): blocked on xSemaphoreTake — waiting for lockB(Prio2): ready, briefly blockedC(Prio1): running, holding lock, green LED, yielding every 100ms in while loop

Log output: [A] waiting for mutex...

This moment the system runs normally—A waits for C to release the lock, C runs as expected, no anomaly.
Stage 4: Button Pressed—Priority Inversion Occurs
Wiring operation: Hold the BOOT button (GPIO0 to GND), do not release.
The user holds the BOOT button (GPIO0 pulled low). At this moment, B happens to wake up from vTaskDelay(20/portTICK_PERIOD_MS) and detects the button press.
B's detection flow:
xxxxxxxxxxgpio_get_level(KEY_GPIO) == 0 → button pressed ✓g_a_has_mutex == false → A not holding lock ✓ ← conditions met→ Debounce 50ms → re-confirm button pressed→ Conditions still met → enter preemption logic
B executes:
xxxxxxxxxxset_led(0, 0, 150) → blue LED ong_b_holding = true → flag B preemptingESP_LOGI prints "[B] Key pressed, BLUE ON (preempt C)"
B enters while (!g_a_has_mutex && gpio_get_level(KEY_GPIO) == 0) loop—button still held, A hasn't taken lock, condition met, prints [B] holding... every 200ms.

Key point: B(Prio2) > C(Prio1), scheduler lets B run, C is preempted!

When C returns from vTaskDelay(100/portTICK_PERIOD_MS), although it wants to continue the while loop, B has higher priority and the scheduler won't return the CPU to C.
xxxxxxxxxxBlocking chain at this moment:A(Prio3) ──waiting for lock──► C(Prio1) holding lock but CPU ──preempted──► B(Prio2) occupying CPUA can't get lock because C has no CPU to run and release lockC can't release lock because B has higher priority and stole the CPU
C's next loop iteration detects g_b_holding == true:
xxxxxxxxxxESP_LOGW prints "[C] Preempted by B, holding mutex but blocked!"
This is the core scenario of priority inversion: high-priority A is indirectly blocked by unrelated B.
Stage 5: Button Released, B Exits, C Resumes
Wiring operation: Release the BOOT button, GPIO0 returns to high level.
The user releases the BOOT button (GPIO0 returns to high level).
B's while loop condition gpio_get_level(KEY_GPIO) == 0 becomes false, exiting the loop:
xxxxxxxxxxg_b_holding = false → clear preempt flag→ A not holding lock → restore green set_led(0, 150, 0)→ ESP_LOGI prints "[B] Key released, restore GREEN"
B returns to the outer vTaskDelay(20/portTICK_PERIOD_MS) and blocks.
CPU finally returns to C(Prio1):

xxxxxxxxxxC returns from vTaskDelay, g_b_holding == false→ ESP_LOGI prints "[C] B released, back running, still holding mutex"→ continue while (gpio_get_level(COND_GPIO_C) == 1) loop
System state at this moment:
A(Prio3): blocked on xSemaphoreTake — waiting for lock
B(Prio2): ready, briefly blocked
C(Prio1): running, holding lock, green LED on
At this point C has recovered the CPU, but GPIO4 is still connected to 3.3V, so C continues holding lock and running. The blocking chain of priority inversion has been relieved, but A is still waiting for C to release the lock.
Stage 6: GPIO4 to GND, C Releases Lock
Wiring operation: Change GPIO4 from 3.3V to GND.
gpio_get_level(COND_GPIO_C) returns 0, C's while (gpio_get_level(COND_GPIO_C) == 1) condition is false, exiting the lock-holding loop:
xxxxxxxxxxC executes: xSemaphoreGive(xMutex) → release mutex→ ESP_LOGI prints "[C] GPIO4 != 1, Release mutex"→ vTaskDelay(2000ms) → give A a chance to take lock
At the moment C releases the lock, A(Prio3) blocked on xSemaphoreTake is woken up by the kernel. A has highest priority → A immediately preempts CPU:

xxxxxxxxxxA returns from xSemaphoreTake → got lock→ g_a_has_mutex = true → flag A holding lock→ set_led(150, 0, 0) → red LED on→ ESP_LOGI prints "[A] Get mutex, RED ON"

LED changes from green to red—A finally got the lock!
Stage 7: A Holds Lock, Verify Protection Mechanism (Button Press B Doesn't Respond)
Current wiring: GPIO2 floating (pulled up=high), GPIO4 already connected to GND. Operation: Hold the BOOT button, observe if LED changes color.
After A gets the lock, it enters while (gpio_get_level(COND_GPIO_A) == 1) polling—GPIO2 floating, pulled up=high, condition true, A holds lock and keeps red LED on.
At this point g_a_has_mutex = true. After pressing the BOOT button, B's detection logic:
xxxxxxxxxxif (!g_a_has_mutex && gpio_get_level(KEY_GPIO) == 0)// A holding lock, condition not met, B blockedAlthough task B has higher priority than C, it is limited by the g_a_has_mutex flag. The first layer of condition is already false, not even entering debounce—no blue LED, no log output. LED stays red.

This is the protection effect when A holds the lock: during the high-priority task's lock-holding period, low-priority B cannot interfere at all. Compared to Stage 4 (when C holds lock, B preempts easily), the difference is clear: when C holds the lock, the lock's protection is incomplete (CPU was stolen); when A holds the lock, the flag directly blocks B.
6.4 Complete Log Example
xxxxxxxxxxI (1263) PRIO_INV: [C] Get mutex, GREEN ON ← Stage 2: C takes lock, greenI (1263) PRIO_INV: [A] waiting for mutex... ← Stage 3: A wakes up, waitingI (2000) PRIO_INV: [B] Key pressed, BLUE ON (preempt C) ← Stage 4: button triggered, inversion beginsI (2000) PRIO_INV: [C] Preempted by B, holding mutex but blocked!I (2200) PRIO_INV: [B] holding...I (2400) PRIO_INV: [B] holding...I (3000) PRIO_INV: [B] Key released, restore GREEN ← Stage 5: button released, B exitsI (3000) PRIO_INV: [C] B released, back running, still holding mutexI (3500) PRIO_INV: [C] GPIO4 != 1, Release mutex ← Stage 6: GPIO4 to GND, C releases lockI (3500) PRIO_INV: [A] Get mutex, RED ON ← A takes lock, red LED on(at this point pressing BOOT → B has no log output) ← Stage 7: A holds lock, B blocked
The demonstration ends here, with A continuously holding the lock and red LED on.
To replay the demo: reconnect GPIO4 to 3.3V, briefly connect GPIO2 to GND then release (let A release lock and exit), system automatically returns to Stage 2 state.


6.5 Quantifying Priority Inversion Damage
Taking this project as an example, assuming this is a real embedded product:
| Task | Role Analogy | Expected Response |
|---|---|---|
| A (Prio 3) | Emergency brake detection | ≤ 1ms |
| B (Prio 2) | Button/UI refresh | ≤ 50ms |
| C (Prio 1) | Sensor data logging | ≤ 500ms |
Normal case (B doesn't exist): C holds lock 100ms → releases → A takes lock, A delay ≈ 100ms ✓
Priority inversion occurs (B holds button): C holds lock but preempted by B → A waits for lock → A delay = C's lock time + B's preemption duration
If the user holds the button for 10 seconds (child pressing randomly, accidental touch, stuck key, etc.), A is indirectly blocked by B for a full 10 seconds. A priority 3 "emergency brake" task, actual response delay is — 10 seconds.
This is the most lethal aspect of priority inversion: the priority you set for a task is completely useless. A priority 2 UI task, without holding any lock, can block a priority 3 critical task. Because the blocker isn't B itself—it's that B stole CPU from C, and C holds the lock that A needs.
6.6 How Much Can FreeRTOS's Priority Inheritance Help?
FreeRTOS mutex has built-in priority inheritance: when A(3) waits for lock, kernel detects C(1) holds it → temporarily elevates C's priority to 3 → C runs and releases lock → A takes lock → C restores to priority 1.
This shortens the inversion time, but cannot fundamentally eliminate it:
Priority inheritance only elevates C, cannot prevent B from preempting before C takes lock
If B already stole CPU during C's lock-holding, priority inheritance can't help (C is not running)
Fundamental solution: don't let low-priority tasks hold locks for long periods
7. Common Issues
| Phenomenon | Cause | Solution |
|---|---|---|
| Priority inversion didn't occur | C released lock too fast / B didn't preempt | Ensure GPIO4 connected to 3.3V; confirm button held long enough |
| A also lit red during B's run | g_a_has_mutex flag logic error | check if flag is updated before xSemaphoreTake |
| B pressed but LED didn't turn blue | A is holding lock, B is blocked | Normal — B shouldn't interfere during A's lock-holding |
| A never gets lock | GPIO2 floating (pulled up=1) → A's polling doesn't exit | Connect GPIO2 to GND, A releases lock |
| C never releases lock | GPIO4 connected to 3.3V (1) → C's polling doesn't exit | Connect GPIO4 to GND, C releases lock |
| What to do about priority inversion in production | Priority inheritance can only shorten inversion, not eliminate | Reduce lock-holding time; avoid low-priority tasks holding locks for extended periods |