Welcome to ESP32 Core Board repository
1.FreeRTOS Core Concepts Theory
FreeRTOS Core Concepts (Theory)
FreeRTOS Core Concepts (Theory)1. What is RTOS1.1 Bare Metal vs RTOS1.2 Why Use RTOS2. Task2.1 What is a Task2.2 Task State Machine2.3 Task Creation2.4 Task Lifecycle Control2.5 Task Diagnostics3. Scheduler3.1 Scheduling Policy3.2 Priority3.3 Preemption Diagram3.4 Round-Robin Time Slice Diagram3.5 When Tasks Yield CPU4. Inter-Task Communication4.1 Five Mechanisms Overview4.2 Queue — FIFO Data Pipeline4.3 Binary Semaphore — ISR → Task Notification4.4 Mutex — Mutual Exclusion Lock4.5 Event Group — 24-bit Bitmap4.6 How to Choose: A Diagram5. Software Timer6. ESP32-S3 FreeRTOS Features7. Common Concurrency Problems7.1 Deadlock7.2 Priority Inversion7.3 Data Race8. FreeRTOS Configuration in ESP-IDF9. Learning Path Recommendations
1. What is RTOS
RTOS (Real-Time Operating System) is an operating system kernel designed specifically for embedded devices. Its core function is to schedule multiple tasks according to priority and time constraints, ensuring that critical tasks respond within specified time limits.
1.1 Bare Metal vs RTOS
| Feature | Bare Metal (Super Loop) | RTOS |
|---|---|---|
| Program Structure | One big loop while(1), executes sequentially | Multiple independent tasks, scheduler allocates CPU |
| Response Latency | Depends on other code execution time in loop | Higher priority task preempts immediately when ready |
| CPU Utilization | One blocking (e.g., delay) → entire program stops | One task blocks → CPU automatically switches to other tasks |
| Complexity | Simple, suitable for single function | Requires understanding tasks, synchronization, priority |
| Code Maintainability | More features → "spaghetti code" | Each task is independent module, clear responsibilities |
1.2 Why Use RTOS
Imagine a smartwatch — the following pseudocode comparison shows execution differences between bare metal and RTOS in the same scenario:
xBare Metal (Bare Metal) — all operations run sequentially in one loop:while(1) {readHeartRateSensor(); // takes 50msrefreshScreen(); // takes 80msWiFiUploadData(); // takes 200ms, blocks touch during uploaddetectTouchButton(); // takes 5ms}RTOS — each function is a separate task, scheduler assigns CPU by priority:Task HeartRate (Prio 4): read sensor every 100msTask Touch (Prio 3): woken by touch, refreshes UI immediatelyTask WiFi (Prio 2): uploads data in idle timeTask Screen (Prio 1): lowest priority, refreshes when CPU is free→ touch always responsive, WiFi never blocks heart-rate reading
In bare metal programs, all code executes serially on a single timeline. RTOS splits the program into multiple independent tasks — blocking operations no longer waste CPU, urgent events no longer wait in queue.
2. Task
2.1 What is a Task
A task is the basic execution unit in FreeRTOS. Each task is a C function that never returns (infinite loop) with its own stack space.
Below is a minimal task skeleton: infinite loop + delay to yield CPU is the fixed pattern for FreeRTOS tasks.
xxxxxxxxxx// Task function, never returnsvoid vMyTask(void *pvParameters){ // Every task must be an infinite loop while (1) { // do work // block for 1 second, yield CPU vTaskDelay(pdMS_TO_TICKS(1000)); }}2.2 Task State Machine
FreeRTOS tasks transition between five states. The following state diagram shows all possible transition paths:
xxxxxxxxxxCreated ──► Ready ──► Running ──► Blocked (waiting for event/delay)▲ ││ ┌────────────┘│ ▼├── Suspended│└── Deleted
| State | Meaning | How to Enter |
|---|---|---|
| Ready | Ready, waiting for scheduler to allocate CPU | After task creation / waiting event arrived |
| Running | Currently executing | Only one per core at any time |
| Blocked | Blocked | vTaskDelay / waiting semaphore / waiting queue |
| Suspended | Suspended, not participating in scheduling | vTaskSuspend() call |
| Deleted | Deleted, memory reclaimed | vTaskDelete() call |
2.3 Task Creation
xTaskCreate is the core API for creating tasks. Below is the complete function signature and parameter descriptions:
xxxxxxxxxxBaseType_t xTaskCreate( TaskFunction_t pvTaskCode, // task function pointer // task name (for debugging, max 16 chars) const char * const pcName, // stack size in WORDS, not bytes uint32_t usStackDepth, // parameter passed to task void *pvParameters, // priority (higher = higher priority) UBaseType_t uxPriority, // task handle (pass NULL if not needed) TaskHandle_t *pxCreatedTask);
// Pinned-to-core versionBaseType_t xTaskCreatePinnedToCore(..., BaseType_t xCoreID);// Core 1 = APP_CPU (user application)Stack size unit is WORDS, not bytes. On ESP32-S3, one word = 4 bytes.
usStackDepth = 2048means 2048 × 4 = 8192 bytes = 8KB.
2.4 Task Lifecycle Control
| API | Function | Notes |
|---|---|---|
vTaskSuspend(handle) | Suspend | Task removed from Ready, no longer scheduled |
vTaskResume(handle) | Resume | Must be called on suspended task |
vTaskDelete(handle) | Delete | NULL = self-delete; cannot delete suspended tasks |
vTaskDelay(ticks) | Relative delay | Calculated from call moment |
2.5 Task Diagnostics
The following three diagnostic APIs are the most commonly used debugging tools during development — check priority, stack remaining, running core:
xxxxxxxxxx// Query current task priorityUBaseType_t prio = uxTaskPriorityGet(NULL);
// Query remaining stack space (Stack High Water Mark)// Stack high water mark (words), smaller = riskierUBaseType_t remaining = uxTaskGetStackHighWaterMark(NULL);
// Query which core the current task is running onint core = xPortGetCoreID();3. Scheduler
3.1 Scheduling Policy
FreeRTOS uses fixed priority preemptive + same priority round-robin time slicing scheduling. The following three rules summarize the core logic of the entire scheduling policy:
xxxxxxxxxxRule 1 (Preemption):Higher priority task ready → immediately preempts lower priority taskRule 2 (Time Slice):Same priority tasks ready → round-robin switch every tickRule 3 (Idle Fallback):No task ready → IDLE task runs (priority 0)
3.2 Priority
Value range: 0 ~ (configMAX_PRIORITIES - 1), default 0 ~ 24
Larger number = higher priority
IDLE task has fixed priority 0
ESP-IDF main task (running
app_main) has default priority 1
3.3 Preemption Diagram
The following timeline shows how high priority tasks immediately preempt low priority tasks:
xxxxxxxxxxPrio 3 (High): ............████████████............Prio 2 (Mid): ....████████............████████....Prio 1 (Low): ████................................↑ ↑ ↑Low runs High preempts High endsLow resumes
3.4 Round-Robin Time Slice Diagram
Multiple tasks of the same priority alternate execution by time slice, switching every tick:
xxxxxxxxxxMid1 (Prio 2): ████............████............Mid2 (Prio 2): ....████............████............├─ 1 tick ─┤ kernel switches every tick
3.5 When Tasks Yield CPU
| Operation | Description |
|---|---|
vTaskDelay() | Active delay, task enters Blocked |
xSemaphoreTake() / xQueueReceive() | Waiting for resource, task enters Blocked |
| Preempted by higher priority task | Passive yield, enters Ready |
| Same priority time slice expired | Passive yield, enters Ready |
A high priority task that doesn't yield CPU will starve all lower priority tasks. High priority tasks must call
vTaskDelayor blocking APIs at appropriate locations, otherwise the system essentially degrades to bare metal.
4. Inter-Task Communication
4.1 Five Mechanisms Overview
| Mechanism | Purpose | Data Carried | Typical Scenario |
|---|---|---|---|
| Queue | Pass data between tasks | Any size data | Sensor data buffer, button events |
| Binary Semaphore | ISR → task event notification | None | GPIO interrupt notifies task to process |
| Mutex | Mutual exclusion for shared resources | None | Protect global variables, peripheral registers |
| Event Group | Multi-event synchronization | 24-bit flags | Wait for multiple conditions simultaneously |
| Task Notification | Lightweight task wakeup | 32-bit value | Single-to-single fast notification |
4.2 Queue — FIFO Data Pipeline
FIFO data pipeline, standard way to pass data between tasks. The following code creates a queue with capacity 10 for unsigned 32-bit integers, and demonstrates basic send/receive usage:
xxxxxxxxxx// 10 slots, 4 bytes eachQueueHandle_t xQueue = xQueueCreate(10, sizeof(uint32_t));
// send, 100ms timeoutxQueueSend(xQueue, &data, pdMS_TO_TICKS(100));// receive, block indefinitelyxQueueReceive(xQueue, &data, portMAX_DELAY);Blocking vs Non-blocking:
xTicksToWait | Behavior when queue full/empty | Applicable Scenario |
|---|---|---|
0 | Return error immediately | Paths that cannot wait |
portMAX_DELAY | Block forever until space/data available | Data must not be lost / event-driven receive |
4.3 Binary Semaphore — ISR → Task Notification
Most commonly used for ISR → task notification. ISR is hardware interrupt service routine, cannot block, must complete quickly. Semaphore lets ISR only do "notification" and delegates actual processing to task.
xxxxxxxxxx// Initial value = 0SemaphoreHandle_t xSem = xSemaphoreCreateBinary();xSemaphoreGiveFromISR(xSem, &xHigherPriorityTaskWoken);
// inside task:// block until ISR notificationxSemaphoreTake(xSem, portMAX_DELAY);Must use
FromISRversion in ISR:xSemaphoreGiveFromISR/xQueueSendFromISRetc. Calling normal API in ISR will trigger assert.
4.4 Mutex — Mutual Exclusion Lock
Protect shared resources, ensuring only one task enters critical section at a time. Key differences from binary semaphore:
| Feature | Mutex | Binary Semaphore |
|---|---|---|
| Purpose | Mutual exclusion — protect data | Synchronization — event notification |
| Priority Inheritance | ✓ | ✗ |
| Who can give lock | Only the task that took it | Any task can give |
| Use in ISR | ✗ | ✓ (GiveFromISR) |
| Initial value | 1 (available) | 0 (blocking, waiting for event) |
The following code uses mutex to protect increment operation of a global variable:
xxxxxxxxxx// Create mutexSemaphoreHandle_t xMutex = xSemaphoreCreateMutex();
// Take lock — block if heldxSemaphoreTake(xMutex, portMAX_DELAY);g_shared_var++; // ← critical sectionxSemaphoreGive(xMutex); // Give lock back4.5 Event Group — 24-bit Bitmap
24-bit bitmap, can wait for multiple events all ready or any ready. The following code creates an event group and waits for all event bits to be ready: