ESP32 雙核心架構與 FreeRTOS 工作分配
ESP32 搭載 兩個 Xtensa LX6 核心(PRO_CPU 與 APP_CPU),各以 240 MHz 獨立運作,共享同一記憶體空間和周邊匯流排。正確使用雙核心工作分配(Task Pinning)可顯著提升 IoT 設備的整體效能和回應速度。
本文將深入探討:
- ESP32 雙核心硬體架構
- xTaskCreatePinnedToCore API
- 核心間通訊(IPC):Queue、Semaphore、Mutex
- Task Notification(最快 IPC)
- IRAM 與 Cache 衝突
- 實戰案例:感測器 + Wi-Fi + 控制迴圈
硬體架構

核心分工
| 特性 | PRO_CPU (Core 0) | APP_CPU (Core 1) |
|---|---|---|
| 預設用途 | WiFi/BT 協定棧、Arduino loop() | 使用者任務(xTaskCreate) |
| 快取 | 16 KB I-Cache + 16 KB D-Cache | 16 KB I-Cache + 16 KB D-Cache |
| IRAM 區段 | 0x40080000 - 0x400A0000 | 0x400A0000 - 0x400C0000 |
| 中斷 | 所有中斷預設路由至此 | 可設定中斷路由 |
| 典型任務 | TCP/IP、MQTT、HTTP Server | 感測器輪詢、PID 控制、LCD 顯示 |
重要限制:兩個核心共享同一條 SPI Flash 匯流排和快取控制器。如果兩個核心同時觸發快取未命中(Cache Miss),Flash 讀取會序列化,造成效能下降。這就是為什麼 CPU 使用率不會單純翻倍。
雙核心排程範例
Task Pinning:xTaskCreatePinnedToCore
FreeRTOS 的 xTaskCreate() 在 ESP32 上預設將任務分配給 APP_CPU(Core 1)。如果需要指定特定核心,使用 xTaskCreatePinnedToCore():
基礎 API
// xTaskCreatePinnedToCore 原型
BaseType_t xTaskCreatePinnedToCore(
TaskFunction_t pvTaskCode, // 任務函數
const char* pcName, // 任務名稱(除錯用)
uint32_t usStackDepth, // 堆疊深度(words)
void* pvParameters, // 參數指標
UBaseType_t uxPriority, // 優先權 (0~24)
TaskHandle_t* pvCreatedTask, // 任務 Handle
BaseType_t xCoreID // 核心 ID (0=PRO, 1=APP, tskNO_AFFINITY)
);
範例:WiFi 在 Core 0,感測器在 Core 1
// ESP32 雙核心工作分配範例
#include <WiFi.h>
#include <freertos/task.h>
// 任務宣告
void sensorTask(void *pvParameters);
void wifiTask(void *pvParameters);
void controlTask(void *pvParameters);
// 共享資料(全域變數,需用 volatile 避免編譯器最佳化)
volatile float sensorData = 0;
volatile bool dataReady = false;
void setup() {
Serial.begin(115200);
// 建立感測器任務 → 綁定到 Core 1 (APP_CPU)
xTaskCreatePinnedToCore(
sensorTask, // 任務函數
"SensorPoll", // 名稱
4096, // 堆疊大小
NULL, // 參數
3, // 優先權
NULL, // Task Handle
1 // Core 1
);
// 建立控制任務 → 綁定到 Core 1
xTaskCreatePinnedToCore(
controlTask,
"ControlLoop",
2048,
NULL,
2,
NULL,
1
);
// 建立 WiFi 任務 → 綁定到 Core 0
xTaskCreatePinnedToCore(
wifiTask,
"WiFiMgr",
8192,
NULL,
2,
NULL,
0
);
Serial.println("所有任務已建立,排程器執行中...");
}
void loop() {
// Arduino loop() 預設在 Core 1 執行
// 這裡可以放一些低優先權的工作
vTaskDelay(pdMS_TO_TICKS(1000));
}
// === 感測器任務 (Core 1) ===
void sensorTask(void *pvParameters) {
TickType_t xLastWakeTime = xTaskGetTickCount();
while (1) {
// 模擬讀取感測器
sensorData = analogRead(34) * 3.3 / 4095.0;
dataReady = true;
Serial.printf("[Sensor Task] Core=%d, Data=%.2f
",
xPortGetCoreID(), sensorData);
// 精確定時 100 ms (10 Hz)
vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(100));
}
}
// === 控制任務 (Core 1) ===
void controlTask(void *pvParameters) {
while (1) {
if (dataReady) {
dataReady = false;
// 根據感測器資料執行控制邏輯
float output = sensorData * 2.5 + 1.0;
Serial.printf("[Control Task] Core=%d, Output=%.2f
",
xPortGetCoreID(), output);
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
// === WiFi 任務 (Core 0) ===
void wifiTask(void *pvParameters) {
WiFi.begin("SSID", "PASSWORD");
while (WiFi.status() != WL_CONNECTED) {
vTaskDelay(pdMS_TO_TICKS(500));
}
Serial.printf("[WiFi] 已連線, Core=%d
", xPortGetCoreID());
while (1) {
// 定時上傳資料
vTaskDelay(pdMS_TO_TICKS(30000));
Serial.printf("[WiFi] Uploading... Core=%d
", xPortGetCoreID());
}
}
核心間通訊(IPC)
雙核心程式設計的核心挑戰是資料同步與資源互斥。FreeRTOS 提供多種 IPC 機制:
1. Queue — 資料傳遞
// Queue:Core 0 → Core 1 傳遞感測器資料
QueueHandle_t sensorQueue;
void senderTask(void *pv) { // Core 0
float data = 25.5;
while (1) {
xQueueSend(sensorQueue, &data, portMAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(100));
}
}
void receiverTask(void *pv) { // Core 1
float received;
while (1) {
if (xQueueReceive(sensorQueue, &received, portMAX_DELAY)) {
Serial.printf("[Core %d] Received: %.1f
",
xPortGetCoreID(), received);
}
}
}
void setup() {
sensorQueue = xQueueCreate(10, sizeof(float));
xTaskCreatePinnedToCore(senderTask, "Sender", 2048, NULL, 1, NULL, 0);
xTaskCreatePinnedToCore(receiverTask, "Receiver", 2048, NULL, 1, NULL, 1);
}
2. Semaphore — 事件通知
// Binary Semaphore:Core 1 通知 Core 0 資料就緒
SemaphoreHandle_t dataSemaphore;
void producerTask(void *pv) { // Core 1 (感測器)
while (1) {
vTaskDelay(pdMS_TO_TICKS(50)); // 讀取感測器
xSemaphoreGive(dataSemaphore); // 通知消費者
}
}
void consumerTask(void *pv) { // Core 0 (WiFi)
while (1) {
xSemaphoreTake(dataSemaphore, portMAX_DELAY); // 等待通知
Serial.printf("[Core %d] Data ready, sending...
",
xPortGetCoreID());
// 上傳資料到雲端
}
}
3. Mutex — 保護共享資源(如 SPI 匯流排)
// Mutex:保護 SPI Flash / 周邊匯流排
SemaphoreHandle_t spiMutex;
void setup() {
spiMutex = xSemaphoreCreateMutex();
}
void readSensorSPI() {
if (xSemaphoreTake(spiMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
// 安全操作 SPI 匯流排
// SPI.beginTransaction(SPISettings(...));
// digitalWrite(CS, LOW);
// SPI.transfer(...);
// digitalWrite(CS, HIGH);
// SPI.endTransaction();
xSemaphoreGive(spiMutex);
} else {
Serial.println("SPI Busy!");
}
}
Task Notification(最快 IPC)
Task Notification 是 FreeRTOS 專有的輕量級 IPC,比 Semaphore 快約 3~5 倍,因為不需要獨立的 queue 或 semaphore 物件——每個 Task 內建一個 32-bit 通知值。
// Task Notification:ISR 通知另一核心的任務
static TaskHandle_t workerTaskHandle = NULL;
void IRAM_ATTR gpio_isr_handler(void* arg) {
// 從 ISR 通知另一個核心上的任務
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
vTaskNotifyGiveFromISR(workerTaskHandle, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
void workerTask(void *pvParameters) { // Core 1
uint32_t notificationCount;
while (1) {
// 等待通知(阻塞,不佔 CPU)
notificationCount = ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// 被喚醒後執行工作
Serial.printf("[Core %d] Notified! Count=%u
",
xPortGetCoreID(), notificationCount);
}
}
void setup() {
// 建立工作者任務(Core 1)
xTaskCreatePinnedToCore(
workerTask, "Worker", 2048, NULL, 3,
&workerTaskHandle, 1
);
// 設定 GPIO 中斷
pinMode(4, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(4),
gpio_isr_handler, FALLING);
}
IPC 效能比較
| IPC 機制 | 速度(CPU 週期) | 資料承載 | 適用場景 |
|---|---|---|---|
| Task Notification | ~120 週期(最快) | 32-bit 值 | ISR → Task 通知 |
| Semaphore (Binary) | ~300 週期 | 無 | 事件同步 |
| Queue | ~400 週期 | 任意大小(複製) | 資料傳遞 |
| Mutex | ~250 週期 | 無 | 資源互斥 |
| Event Group | ~350 週期 | 24-bit 事件旗標 | 多重條件同步 |
| Spinlock | ~50 週期(但忙等) | 無 | 極短臨界區段 |
IRAM 與快取衝突
// 將關鍵 ISR 放入 IRAM(避免 Cache Miss 延遲)
// 在函數前加上 IRAM_ATTR 即可
#include "esp_attr.h"
// 這個函數會被編譯到 IRAM (0x40080000+)
void IRAM_ATTR fastISR() {
// 極速執行,不需從 Flash 讀取指令
// 但只能呼叫也在 IRAM 中的函數
}
// 也可以在 ESP-IDF 的 CMakeLists.txt 或 component.mk 中設定
// IDF 自動將中斷處理函數放入 IRAM
IRAM 使用建議
- WiFi/BT 中斷:預設已使用 IRAM,不需修改
- 自訂 ISR:若 ISR 執行時間 < 10 us,不需要 IRAM
- 高速 ISR(> 10 us 或 > 10 kHz):使用 IRAM_ATTR
- IRAM 大小:總共約 192 KB,但 Arduino 框架已用掉大部分
- 檢查 IRAM 用量:透過 Serial 輸出或 ESP-IDF Monitor
實戰案例:WiFi 感測器站台
// 實戰:雙核心 WiFi 感測器站台
// Core 0: WiFi + MQTT + HTTP Server
// Core 1: 感測器輪詢 + 控制演算法 + OLED 顯示
// === 共用資料結構 ===
typedef struct {
float temperature;
float humidity;
float pressure;
uint32_t timestamp;
} SensorData;
QueueHandle_t sensorToNetwork; // Core 1 → Core 0
SemaphoreHandle_t i2cMutex; // 保護 I2C 匯流排
// === Core 1:感測器任務 ===
void sensorTask(void *pv) {
SensorData data;
TickType_t lastWake = xTaskGetTickCount();
while (1) {
xSemaphoreTake(i2cMutex, portMAX_DELAY);
// 讀取 BME280
data.temperature = readBME280_Temp();
data.humidity = readBME280_Hum();
data.pressure = readBME280_Press();
xSemaphoreGive(i2cMutex);
data.timestamp = millis() / 1000;
// 送給網路任務
xQueueSend(sensorToNetwork, &data, 0);
// 更新 OLED(同 Core 1,不需要同步)
updateOLED(data.temperature, data.humidity);
vTaskDelayUntil(&lastWake, pdMS_TO_TICKS(2000)); // 2 Hz
}
}
// === Core 0:網路任務 ===
void networkTask(void *pv) {
SensorData data;
WiFi.begin("SSID", "PASSWORD");
while (WiFi.status() != WL_CONNECTED) vTaskDelay(100);
while (1) {
if (xQueueReceive(sensorToNetwork, &data, portMAX_DELAY)) {
// 透過 WiFi 上傳(Core 0 處理,不影響 Core 1 的感測器)
uploadToCloud(data.temperature, data.humidity, data.pressure);
}
}
}
void setup() {
sensorToNetwork = xQueueCreate(5, sizeof(SensorData));
i2cMutex = xSemaphoreCreateMutex();
xTaskCreatePinnedToCore(sensorTask, "Sensor", 4096, NULL, 3, NULL, 1);
xTaskCreatePinnedToCore(networkTask, "Network", 8192, NULL, 2, NULL, 0);
}
void loop() { vTaskDelay(1000); }
效能測試

除錯與監控
// 查看目前哪個核心在執行
Serial.printf("Running on Core: %d
", xPortGetCoreID());
// 列出所有任務與狀態(ESP-IDF)
// 在 terminal 輸入: monitor
// 或呼叫: vTaskList / vTaskGetRunTimeStats
// FreeRTOS 除錯選項需在 menuconfig 中啟用:
// Component config → FreeRTOS → Enable FreeRTOS trace facility
// 檢查任務堆疊使用量
UBaseType_t stackHighWaterMark;
TaskHandle_t xHandle = xTaskGetHandle("SensorPoll");
stackHighWaterMark = uxTaskGetStackHighWaterMark(xHandle);
Serial.printf("Sensor task free stack: %d words
", stackHighWaterMark);
常見問題
| 問題 | 原因 | 解法 |
|---|---|---|
| WiFi 斷線 + 任務卡死 | 兩個核心同時存取 Flash(Cache Thrashing) | 降低任務頻率,或將 WiFi 任務優先權提高 |
| 重啟 (Guru Meditation) | Cache 衝突 + 中斷未正確路由 | 將 ISR 放入 IRAM,設定中斷 affinity |
| 感測器讀取錯誤 | I2C 匯流排同時被兩個核心存取 | 使用 Mutex 保護 I2C/SPI |
| Queue 滿了丟失資料 | 生產者速度 > 消費者速度 | 增大 Queue 長度,或降低生產者頻率 |
| Stack Overflow | 任務堆疊太小 | 使用 uxTaskGetStackHighWaterMark() 檢查 |
| 效能不如預期 | Amdahl's Law:序列化瓶頸 | 減少共享資源存取,增加 IPC 粒度 |
總結
ESP32 的雙核心架構是真實的對稱多處理(SMP)系統,正確使用 xTaskCreatePinnedToCore 搭配 IPC 機制,可以讓 Wi-Fi 通訊與即時控制完全並行,達到最佳效能。
建議分配策略:
- Core 0(PRO_CPU):WiFi/BT 協定棧、TCP/IP、MQTT、HTTP Server、檔案系統
- Core 1(APP_CPU):感測器輪詢、控制演算法(PID)、LCD/OLED 顯示、音頻處理
- IPC 選擇:資料傳遞用 Queue、事件同步用 Semaphore/Notification、資源互斥用 Mutex
- CORE 0 專用:所有 Arduino 的 yield()/delay() 相關函數預設綁定 Core 1
文章評論