ESP32-S3 双核任务分配实战:把 WiFi 协议栈和实时控制任务隔核运行的配置与踩坑记录
一、为什么需要隔核运行?
ESP32-S3 内置双核 Xtensa LX7,默认情况下 FreeRTOS 调度器允许任务在任意核心上运行。但 WiFi 协议栈任务(如 wifi、esp_timer)通常优先级较高且执行时间不确定,若与电机控制、PID 调节等实时任务共享同一核心,会导致:
- 实时任务被抢占,控制周期抖动增大
- 中断响应延迟不可预测
- 严重时触发看门狗复位
隔核运行的核心思想:将 WiFi 协议栈绑定到 Core 0,将实时控制任务绑定到 Core 1,利用硬件双核实现物理隔离。
二、FreeRTOS 任务亲和力原理
ESP-IDF 基于 FreeRTOS,提供 xTaskCreatePinnedToCore() API 将任务固定到指定核心。关键参数:
BaseType_t xTaskCreatePinnedToCore(
TaskFunction_t pvTaskCode, // 任务函数
const char *pcName, // 任务名
uint32_t usStackDepth, // 栈深度(字)
void *pvParameters, // 参数
UBaseType_t uxPriority, // 优先级
TaskHandle_t *pxCreatedTask, // 句柄
const BaseType_t xCoreID // 核心ID:0或1
);
-
xCoreID = 0:绑定到 Core 0(协议核) -
xCoreID = 1:绑定到 Core 1(应用核) -
tskNO_AFFINITY:不限制核心
注意:WiFi 协议栈任务由 ESP-IDF 自动创建,默认运行在 Core 0。我们只需确保实时任务不跑到 Core 0 即可。
三、配置步骤
3.1 确认 WiFi 任务核心
在 menuconfig 中检查:
Component config → FreeRTOS → Run FreeRTOS only on first core
不要勾选此选项,否则所有任务强制运行在 Core 0。
3.2 创建实时控制任务
#define CONTROL_TASK_PRIO 10
#define CONTROL_TASK_STACK 4096
void control_task(void *arg)
{
TickType_t last_wake = xTaskGetTickCount();
const TickType_t period = pdMS_TO_TICKS(1); // 1ms 控制周期
while (1) {
// 读取传感器
float adc_val = adc1_get_raw(ADC1_CHANNEL_0);
// PID 计算
float output = pid_calculate(&pid, adc_val);
// 输出 PWM
ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, (uint32_t)output);
ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);
vTaskDelayUntil(&last_wake, period);
}
}
void app_main(void)
{
// 初始化 WiFi(协议栈任务自动运行在 Core 0)
wifi_init_sta();
// 将控制任务固定到 Core 1
xTaskCreatePinnedToCore(
control_task, // 任务函数
"control_task", // 任务名
CONTROL_TASK_STACK, // 栈大小
NULL, // 参数
CONTROL_TASK_PRIO, // 优先级
NULL, // 句柄
1 // 核心ID = 1
);
}
3.3 验证任务核心
void print_task_core(void)
{
printf("Current core: %d\n", xPortGetCoreID());
}
在 control_task 中调用,应输出 1。
四、完整代码示例
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "nvs_flash.h"
#include "driver/adc.h"
#include "driver/ledc.h"
#define CONTROL_TASK_PRIO 10
#define CONTROL_TASK_STACK 4096
static void wifi_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
}
}
void wifi_init_sta(void)
{
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
&wifi_event_handler, NULL));
wifi_config_t wifi_config = {
.sta = {
.ssid = "YourSSID",
.password = "YourPassword",
},
};
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
}
void control_task(void *arg)
{
// 配置 ADC
adc1_config_width(ADC_WIDTH_BIT_12);
adc1_config_channel_atten(ADC1_CHANNEL_0, ADC_ATTEN_DB_11);
// 配置 PWM
ledc_timer_config_t ledc_timer = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.timer_num = LEDC_TIMER_0,
.duty_resolution = LEDC_TIMER_10_BIT,
.freq_hz = 5000,
.clk_cfg = LEDC_AUTO_CLK,
};
ledc_timer_config(&ledc_timer);
ledc_channel_config_t ledc_channel = {
.gpio_num = 4,
.speed_mode = LEDC_LOW_SPEED_MODE,
.channel = LEDC_CHANNEL_0,
.timer_sel = LEDC_TIMER_0,
.duty = 0,
.hpoint = 0,
};
ledc_channel_config(&ledc_channel);
TickType_t last_wake = xTaskGetTickCount();
const TickType_t period = pdMS_TO_TICKS(1);
while (1) {
int raw = adc1_get_raw(ADC1_CHANNEL_0);
uint32_t duty = (raw * 1023) / 4095;
ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, duty);
ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);
vTaskDelayUntil(&last_wake, period);
}
}
void app_main(void)
{
wifi_init_sta();
xTaskCreatePinnedToCore(
control_task,
"control_task",
CONTROL_TASK_STACK,
NULL,
CONTROL_TASK_PRIO,
NULL,
1 // Core 1
);
}
五、踩坑记录与注意事项
5.1 优先级反转
- 现象:控制任务优先级设为 10,WiFi 任务优先级为 23,若控制任务在 Core 0 上运行,会被 WiFi 任务频繁抢占。
-
解决:确保控制任务固定在 Core 1,且优先级不要超过
configMAX_PRIORITIES - 1。
5.2 缓存争用
- 现象:双核同时访问 PSRAM 或 Flash 时,性能下降。
- 解决:将实时任务的数据放在内部 SRAM,避免跨核缓存同步。
5.3 看门狗超时
- 现象:控制任务长时间阻塞,触发 Task Watchdog。
-
解决:在控制任务中定期调用
vTaskDelay或esp_task_wdt_reset(),或调整看门狗超时时间。
5.4 中断分配
- 现象:WiFi 中断默认在 Core 0,但某些外设中断可能分配到 Core 1。
-
解决:使用
esp_intr_alloc()时指定核心,或通过esp_ipc_call()跨核调用。
5.5 栈大小
- 现象:控制任务栈溢出,导致崩溃。
-
解决:使用
uxTaskGetStackHighWaterMark()监控栈使用,建议至少 4096 字。
六、总结
通过 xTaskCreatePinnedToCore() 将实时控制任务固定到 Core 1,WiFi 协议栈自动运行在 Core 0,可显著降低控制周期抖动。实际项目中还需注意优先级设置、缓存一致性、看门狗配置等细节。建议在开发初期就使用 xPortGetCoreID() 验证任务核心,避免后期调试困难。