0x6A Logbook

0x6A Logbook
Shi6a的筆記本
  1. 首頁
  2. 程式開發
  3. 正文

ESP32 雙核心工作分配完整教學:FreeRTOS xTaskCreatePinnedToCore、IPC 與 Task Pinning 最佳實踐

2026 年 6 月 20 日 17點熱度 0人點贊 0條評論

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 + 控制迴圈

硬體架構

ESP32 雙核心架構與任務分配

核心分工

特性 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():

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)

ISR → Task Notification 時序

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); }

效能測試

單核心 vs 雙核心 CPU 使用率比較

除錯與監控

// 查看目前哪個核心在執行
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
標籤: 教學
最後更新:2026 年 6 月 20 日

shi6a

這個人很懶,什麼都沒留下

點贊
< 上一篇
下一篇 >

文章評論

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回覆

COPYRIGHT © 2026 0x6A Logbook. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang