跳转至

多核与任务管理

ESP32 内置 FreeRTOS 操作系统和双核处理器,可以真正并行执行多个任务。理解任务管理和双核分配是发挥 ESP32 全部性能的关键。


一、ESP32 双核架构

ESP32 有两个 Xtensa LX6 CPU 核心:

核心 名称 默认用途 核心 ID
CPU 0 PRO_CPU(Protocol) WiFi/BLE 协议栈 0
CPU 1 APP_CPU(Application) Arduino setup() / loop() 1
graph TB
    subgraph "CPU 0 (PRO_CPU)"
        WiFi[WiFi 协议栈]
        BLE_T[BLE 协议栈]
        SYS[系统任务]
    end

    subgraph "CPU 1 (APP_CPU)"
        LOOP[Arduino loop 任务]
        USER1[用户任务 1]
        USER2[用户任务 2]
    end

    subgraph "FreeRTOS 调度器"
        SCHED[任务调度<br>时间片轮转<br>优先级抢占]
    end

    SCHED --> WiFi
    SCHED --> BLE_T
    SCHED --> LOOP
    SCHED --> USER1
    SCHED --> USER2

Arduino 框架下的任务模型

Arduino 的 setup()loop() 实际上运行在一个 FreeRTOS 任务中:

  • 任务名:loopTask
  • 任务优先级:1
  • 栈大小:8192 字节
  • 绑定核心:CPU 1

你可以在此基础上创建更多任务。


二、创建 FreeRTOS 任务

基本任务创建

void task1(void *parameter) {
    // 任务初始化(相当于该任务的 setup)
    Serial.println("任务 1 启动");

    for (;;) {  // 任务必须是无限循环
        Serial.println("任务 1 运行中...");
        vTaskDelay(pdMS_TO_TICKS(1000));  // 延时 1 秒(让出 CPU)
    }
    // 如果需要退出,调用 vTaskDelete(NULL);
}

void task2(void *parameter) {
    for (;;) {
        Serial.println("任务 2 运行中...");
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

void setup() {
    Serial.begin(115200);

    // 创建任务
    xTaskCreate(
        task1,       // 任务函数
        "Task1",     // 任务名(调试用)
        4096,        // 栈大小(字节)
        NULL,        // 传入参数
        1,           // 优先级(0 最低,configMAX_PRIORITIES-1 最高)
        NULL         // 任务句柄(可用于后续控制)
    );

    xTaskCreate(task2, "Task2", 4096, NULL, 1, NULL);
}

void loop() {
    // loop 本身也是一个任务(loopTask)
    Serial.println("主循环运行中...");
    delay(2000);
}

绑定到指定核心

TaskHandle_t sensorTaskHandle;
TaskHandle_t displayTaskHandle;

void sensorTask(void *parameter) {
    for (;;) {
        // 传感器读取(绑定到 CPU 1)
        int val = analogRead(34);
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

void displayTask(void *parameter) {
    for (;;) {
        // 显示更新(绑定到 CPU 0)
        // updateDisplay();
        vTaskDelay(pdMS_TO_TICKS(50));
    }
}

void setup() {
    Serial.begin(115200);

    // xTaskCreatePinnedToCore — 指定运行核心
    xTaskCreatePinnedToCore(
        sensorTask,          // 任务函数
        "SensorTask",        // 名称
        4096,                // 栈大小
        NULL,                // 参数
        2,                   // 优先级
        &sensorTaskHandle,   // 句柄
        1                    // 核心 ID(0 或 1)
    );

    xTaskCreatePinnedToCore(
        displayTask, "DisplayTask", 4096, NULL, 1,
        &displayTaskHandle,
        0  // 绑定到 CPU 0
    );
}

核心分配策略

任务类型 建议核心 理由
WiFi/BLE 通信 CPU 0 与系统协议栈同核,减少上下文切换
传感器读取 CPU 1 避免被 WiFi 任务阻塞
电机/PID 控制 CPU 1 实时性要求高
UI/显示更新 两者均可 优先级较低
使用 tskNO_AFFINITY 不绑定 由调度器自动分配

三、任务优先级

FreeRTOS 使用==优先级抢占式调度==:

优先级 用途建议 说明
0 空闲任务(Idle) 系统保留
1 Arduino loop / 低优先级任务 默认用户任务
2~4 传感器采集、数据处理 中等优先级
5~9 电机控制、实时通信 高优先级
10+ WiFi/BLE 内部任务 系统使用
// 动态修改优先级
vTaskPrioritySet(sensorTaskHandle, 3);  // 提高优先级

// 获取当前优先级
UBaseType_t prio = uxTaskPriorityGet(NULL);  // NULL = 当前任务

优先级注意事项

  • 高优先级任务会==抢占==低优先级任务
  • 如果高优先级任务不让出 CPU(没有 vTaskDelay 或等待信号量),低优先级任务将永远无法运行
  • 同优先级任务之间使用==时间片轮转==

四、任务间通信 — 队列(Queue)

队列是 FreeRTOS 中==最常用的任务间通信方式==:

QueueHandle_t sensorQueue;

struct SensorData {
    float temperature;
    float humidity;
    unsigned long timestamp;
};

void sensorTask(void *parameter) {
    for (;;) {
        SensorData data;
        data.temperature = 25.0 + random(0, 100) / 10.0;
        data.humidity = 60.0 + random(0, 200) / 10.0;
        data.timestamp = millis();

        // 发送到队列(等待最多 100ms)
        if (xQueueSend(sensorQueue, &data, pdMS_TO_TICKS(100)) == pdTRUE) {
            Serial.println("数据已发送到队列");
        } else {
            Serial.println("队列已满,发送失败");
        }

        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void displayTask(void *parameter) {
    SensorData receivedData;

    for (;;) {
        // 从队列接收(阻塞等待)
        if (xQueueReceive(sensorQueue, &receivedData, portMAX_DELAY) == pdTRUE) {
            Serial.printf("温度: %.1f°C, 湿度: %.1f%%, 时间: %lu ms\n",
                receivedData.temperature,
                receivedData.humidity,
                receivedData.timestamp);
        }
    }
}

void setup() {
    Serial.begin(115200);

    // 创建队列(容量 10 个 SensorData)
    sensorQueue = xQueueCreate(10, sizeof(SensorData));

    if (sensorQueue == NULL) {
        Serial.println("队列创建失败!");
        return;
    }

    xTaskCreatePinnedToCore(sensorTask, "Sensor", 4096, NULL, 2, NULL, 1);
    xTaskCreatePinnedToCore(displayTask, "Display", 4096, NULL, 1, NULL, 0);
}

void loop() {
    // 查看队列状态
    Serial.printf("队列中待处理: %d\n", uxQueueMessagesWaiting(sensorQueue));
    delay(5000);
}

五、任务同步 — 信号量与互斥量

二值信号量(Binary Semaphore)

用于任务间的==事件通知==:

SemaphoreHandle_t xSemaphore;

void IRAM_ATTR buttonISR() {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

void buttonTask(void *parameter) {
    for (;;) {
        // 阻塞等待信号量
        if (xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE) {
            Serial.println("按键按下!执行处理...");
            // 处理按键事件
        }
    }
}

void setup() {
    Serial.begin(115200);

    xSemaphore = xSemaphoreCreateBinary();

    pinMode(0, INPUT_PULLUP);
    attachInterrupt(digitalPinToInterrupt(0), buttonISR, FALLING);

    xTaskCreate(buttonTask, "ButtonTask", 4096, NULL, 2, NULL);
}

ISR 中使用信号量

ISR 中必须使用 FromISR 后缀的 API:

  • xSemaphoreGiveFromISR() 而不是 xSemaphoreGive()
  • xQueueSendFromISR() 而不是 xQueueSend()
  • 调用后使用 portYIELD_FROM_ISR() 触发任务切换

互斥量(Mutex)

用于==保护共享资源==,防止数据竞争:

SemaphoreHandle_t xMutex;
float sharedTemperature = 0;

void sensorTask(void *parameter) {
    for (;;) {
        float newTemp = readTemperature();

        // 获取互斥量(加锁)
        if (xSemaphoreTake(xMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
            sharedTemperature = newTemp;  // 安全写入
            xSemaphoreGive(xMutex);       // 释放互斥量(解锁)
        }

        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

void displayTask(void *parameter) {
    for (;;) {
        // 获取互斥量(加锁)
        if (xSemaphoreTake(xMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
            float temp = sharedTemperature;  // 安全读取
            xSemaphoreGive(xMutex);           // 释放互斥量(解锁)

            Serial.printf("显示温度: %.1f°C\n", temp);
        }

        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

void setup() {
    Serial.begin(115200);

    xMutex = xSemaphoreCreateMutex();

    xTaskCreatePinnedToCore(sensorTask, "Sensor", 4096, NULL, 2, NULL, 1);
    xTaskCreatePinnedToCore(displayTask, "Display", 4096, NULL, 1, NULL, 0);
}

互斥量 vs 信号量

特性 互斥量 (Mutex) 二值信号量
用途 保护共享资源 事件通知
所有权 谁获取谁释放 任何任务可释放
优先级继承 ✅ 支持 ❌ 不支持
ISR 中使用 ❌ 不可以 ✅ 可以

六、任务通知(Task Notification)

任务通知是比队列和信号量==更轻量级==的通信方式:

TaskHandle_t receiverTaskHandle;

void senderTask(void *parameter) {
    for (;;) {
        // 发送通知值
        xTaskNotify(receiverTaskHandle, 42, eSetValueWithOverwrite);
        Serial.println("通知已发送");
        vTaskDelay(pdMS_TO_TICKS(2000));
    }
}

void receiverTask(void *parameter) {
    uint32_t notifyValue;

    for (;;) {
        // 等待通知
        if (xTaskNotifyWait(0, ULONG_MAX, &notifyValue, portMAX_DELAY) == pdTRUE) {
            Serial.printf("收到通知,值: %u\n", notifyValue);
        }
    }
}

void setup() {
    Serial.begin(115200);

    xTaskCreate(receiverTask, "Receiver", 4096, NULL, 1, &receiverTaskHandle);
    xTaskCreate(senderTask, "Sender", 4096, NULL, 1, NULL);
}

七、任务管理实用操作

删除任务

TaskHandle_t taskHandle;

void temporaryTask(void *parameter) {
    Serial.println("临时任务执行中...");
    delay(5000);
    Serial.println("临时任务完成,自我删除");
    vTaskDelete(NULL);  // NULL = 删除自身
}

void setup() {
    xTaskCreate(temporaryTask, "TempTask", 4096, NULL, 1, &taskHandle);
}

// 也可以从外部删除
// vTaskDelete(taskHandle);

挂起和恢复任务

// 挂起任务(暂停)
vTaskSuspend(taskHandle);

// 恢复任务(继续)
vTaskResume(taskHandle);

查看任务状态

void printTaskInfo() {
    // 获取空闲内存
    Serial.printf("剩余堆内存: %d bytes\n", esp_get_free_heap_size());
    Serial.printf("最小堆内存: %d bytes\n", esp_get_minimum_free_heap_size());

    // 获取当前任务栈剩余
    Serial.printf("当前任务栈剩余: %d words\n", uxTaskGetStackHighWaterMark(NULL));

    // 获取运行中的任务数
    Serial.printf("运行中任务数: %d\n", uxTaskGetNumberOfTasks());

    // 打印所有任务信息
    char taskList[512];
    vTaskList(taskList);
    Serial.println("任务列表:");
    Serial.println("名称\t\t状态\t优先级\t剩余栈\t任务号");
    Serial.println(taskList);
}

八、实际应用示例

IoT 传感器节点(多任务架构)

#include <WiFi.h>
#include <HTTPClient.h>

QueueHandle_t dataQueue;
SemaphoreHandle_t wifiMutex;

struct SensorData {
    float temp;
    float hum;
    uint32_t timestamp;
};

// 任务 1: 传感器采集(CPU 1,高优先级)
void sensorTask(void *param) {
    for (;;) {
        SensorData data = {
            .temp = 25.0 + random(-50, 50) / 10.0,
            .hum = 60.0 + random(-100, 100) / 10.0,
            .timestamp = millis()
        };
        xQueueSend(dataQueue, &data, pdMS_TO_TICKS(100));
        vTaskDelay(pdMS_TO_TICKS(5000));  // 5 秒采集一次
    }
}

// 任务 2: WiFi 上传(CPU 0,中优先级)
void uploadTask(void *param) {
    SensorData data;
    for (;;) {
        if (xQueueReceive(dataQueue, &data, portMAX_DELAY) == pdTRUE) {
            if (WiFi.status() == WL_CONNECTED) {
                xSemaphoreTake(wifiMutex, portMAX_DELAY);

                HTTPClient http;
                http.begin("http://api.example.com/data");
                http.addHeader("Content-Type", "application/json");

                char json[128];
                snprintf(json, sizeof(json),
                    "{\"temp\":%.1f,\"hum\":%.1f,\"ts\":%u}",
                    data.temp, data.hum, data.timestamp);

                int code = http.POST(json);
                Serial.printf("上传结果: %d\n", code);
                http.end();

                xSemaphoreGive(wifiMutex);
            }
        }
    }
}

// 任务 3: 本地显示(CPU 0,低优先级)
void displayTask(void *param) {
    for (;;) {
        Serial.printf("[%lu] 堆: %d bytes, 任务数: %d\n",
            millis(), esp_get_free_heap_size(), uxTaskGetNumberOfTasks());
        vTaskDelay(pdMS_TO_TICKS(10000));
    }
}

void setup() {
    Serial.begin(115200);
    WiFi.begin("SSID", "password");
    while (WiFi.status() != WL_CONNECTED) delay(500);

    dataQueue = xQueueCreate(20, sizeof(SensorData));
    wifiMutex = xSemaphoreCreateMutex();

    xTaskCreatePinnedToCore(sensorTask, "Sensor", 4096, NULL, 3, NULL, 1);
    xTaskCreatePinnedToCore(uploadTask, "Upload", 8192, NULL, 2, NULL, 0);
    xTaskCreatePinnedToCore(displayTask, "Display", 4096, NULL, 1, NULL, 0);
}

void loop() {
    vTaskDelay(pdMS_TO_TICKS(1000));
}

九、常见问题

vTaskDelay 和 delay 有什么区别?

函数 行为
delay(ms) Arduino 封装,内部调用 vTaskDelay,让出 CPU
vTaskDelay(ticks) FreeRTOS 原生,参数是 tick 数
vTaskDelayUntil() 精确的周期性延时(补偿执行时间)

在 ESP32 Arduino 中,delay() 是==安全的==(会让出 CPU),但参数是毫秒。vTaskDelay 参数是 tick,用 pdMS_TO_TICKS(ms) 转换。

栈大小设多少合适?

  • 简单任务(GPIO 读写、简单计算):2048~4096 字节
  • 带 Serial.printf 的任务:4096 字节
  • WiFi/HTTP 网络任务:8192~16384 字节
  • 可以用 uxTaskGetStackHighWaterMark() 查看实际使用量,栈剩余应 > 500 字节

看门狗超时 (Task watchdog got triggered) 怎么解决?

ESP32 的 Task WDT 默认监控 CPU 0 上的 Idle 任务。如果某个高优先级任务在 CPU 0 上==长时间不让出 CPU==,就会触发看门狗。

解决方法:

  1. 确保所有循环任务都有 vTaskDelay() 或其他阻塞调用
  2. 在耗时循环中加入 vTaskDelay(1) 让出 CPU
  3. 将耗时任务绑定到 CPU 1

ESP32 上 FreeRTOS 和独立安装的 FreeRTOS 有区别吗?

ESP32 的 FreeRTOS 是乐鑫修改的版本(ESP-IDF FreeRTOS),主要区别:

  • 支持==对称多处理(SMP)==,可以将任务绑定到特定核心
  • xTaskCreatePinnedToCore() 是 ESP32 特有 API
  • tick 频率默认 1000 Hz(标准 FreeRTOS 通常 100 Hz)
  • 支持浮点上下文保存(每个核心独立 FPU)