多核与任务管理¶
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, ¬ifyValue, 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);
挂起和恢复任务¶
查看任务状态¶
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==,就会触发看门狗。
解决方法:
- 确保所有循环任务都有
vTaskDelay()或其他阻塞调用 - 在耗时循环中加入
vTaskDelay(1)让出 CPU - 将耗时任务绑定到 CPU 1
ESP32 上 FreeRTOS 和独立安装的 FreeRTOS 有区别吗?
ESP32 的 FreeRTOS 是乐鑫修改的版本(ESP-IDF FreeRTOS),主要区别:
- 支持==对称多处理(SMP)==,可以将任务绑定到特定核心
xTaskCreatePinnedToCore()是 ESP32 特有 API- tick 频率默认 1000 Hz(标准 FreeRTOS 通常 100 Hz)
- 支持浮点上下文保存(每个核心独立 FPU)