一、为什么需要双核任务分配?
ESP32-S3 内置双核 Xtensa LX7,主频高达 240MHz。默认情况下,FreeRTOS 调度器会将任务动态分配到两个核心上,WiFi 协议栈、蓝牙、TCP/IP 等系统任务与用户任务共享 CPU 时间。
对于电机控制、数字电源、传感器融合等硬实时场景,这种动态调度会带来两个致命问题:
- 抖动大:WiFi 中断和协议栈任务随时可能抢占 CPU,导致控制周期从 100us 抖动到 500us 以上。
- 优先级反转:即使控制任务优先级设为最高,WiFi 任务仍可能在其他核心上运行并争抢总线、Cache 和内存带宽。
解决方案:利用 FreeRTOS 的任务亲和力(Core Affinity),把 WiFi 协议栈钉在 Core0,把实时控制任务钉在 Core1,实现物理隔离。
二、原理:ESP32-S3 双核与 FreeRTOS 亲和力
2.1 核心编号与默认行为
-
PRO_CPU_NUM(Core0):协议栈 CPU,默认运行 WiFi、蓝牙、TCP/IP 等任务。 -
APP_CPU_NUM(Core1):应用 CPU,默认运行app_main及用户任务。
但“默认”并不等于“固定”。FreeRTOS 的 xTaskCreatePinnedToCore() 才是真正的绑定手段。
2.2 任务亲和力 API
BaseType_t xTaskCreatePinnedToCore(
TaskFunction_t pvTaskCode,
const char *pcName,
uint32_t usStackDepth,
void *pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *pxCreatedTask,
BaseType_t xCoreID // 0 或 1,或 tskNO_AFFINITY
);
-
xCoreID = 0:任务只在 Core0 运行。 -
xCoreID = 1:任务只在 Core1 运行。 -
tskNO_AFFINITY:允许在两个核心间迁移。
三、配置步骤:把 WiFi 钉在 Core0,控制钉在 Core1
3.1 步骤 1:确认 WiFi 任务的核心归属
ESP-IDF 默认将 WiFi 任务创建在 Core0。可通过 menuconfig 确认:
Component config → Wi-Fi → [*] WiFi Task Core ID (Core 0)
若使用蓝牙,同样在 Component config → Bluetooth → Bluedroid Options → Bluetooth task core 中选择 Core0。
3.2 步骤 2:创建实时控制任务并绑定 Core1
#define CONTROL_TASK_PRIO 10
#define CONTROL_TASK_STACK 4096
void control_task(void *arg)
{
const TickType_t period = pdMS_TO_TICKS(1); // 1ms 控制周期
TickType_t last_wake = xTaskGetTickCount();
while (1) {
// 1. 读取传感器
int adc = adc1_get_raw(ADC1_CHANNEL_0);
// 2. 执行控制算法(PID 等)
float duty = pid_calc(adc);
// 3. 更新 PWM
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)
{
// 初始化外设...
xTaskCreatePinnedToCore(
control_task,
"ctrl_task",
CONTROL_TASK_STACK,
NULL,
CONTROL_TASK_PRIO,
NULL,
1 // 绑定到 Core1
);
}
3.3 步骤 3:将 WiFi 初始化和事件处理也绑定到 Core0
WiFi 事件回调默认在 Core0 的 wifi 任务中执行。若在回调中做耗时操作,建议再创建一个专用任务并绑定 Core0:
void wifi_event_task(void *arg)
{
// 处理 WiFi 事件,如重连、MQTT 发布
while (1) {
// 等待事件组或队列
vTaskDelay(pdMS_TO_TICKS(100));
}
}
xTaskCreatePinnedToCore(wifi_event_task, "wifi_evt", 4096, NULL, 5, NULL, 0);
3.4 步骤 4:关闭不必要的核心迁移
在 menuconfig 中关闭 FreeRTOS → Enable task snapshot 等调试功能,减少调度开销。同时确保 CONFIG_FREERTOS_UNICORE 为 n,即双核模式。
四、完整代码示例:双核隔离的 WiFi + 控制工程
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/adc.h"
#include "driver/ledc.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "nvs_flash.h"
#define CONTROL_PERIOD_MS 1
#define CONTROL_TASK_PRIO 10
static void control_task(void *arg)
{
TickType_t last = xTaskGetTickCount();
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, pdMS_TO_TICKS(CONTROL_PERIOD_MS));
}
}
static void wifi_event_handler(void *arg, esp_event_base_t base,
int32_t id, void *data)
{
if (base == WIFI_EVENT && id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
}
}
void app_main(void)
{
// 1. NVS 初始化
nvs_flash_init();
// 2. 外设初始化
adc1_config_width(ADC_WIDTH_BIT_12);
adc1_config_channel_atten(ADC1_CHANNEL_0, ADC_ATTEN_DB_11);
ledc_timer_config_t timer = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.duty_resolution = LEDC_TIMER_10_BIT,
.timer_num = LEDC_TIMER_0,
.freq_hz = 20000,
.clk_cfg = LEDC_AUTO_CLK
};
ledc_timer_config(&timer);
ledc_channel_config_t ch = {
.gpio_num = 4,
.speed_mode = LEDC_LOW_SPEED_MODE,
.channel = LEDC_CHANNEL_0,
.timer_sel = LEDC_TIMER_0,
.duty = 0
};
ledc_channel_config(&ch);
// 3. WiFi 初始化(默认在 Core0)
esp_netif_init();
esp_event_loop_create_default();
esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL);
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&cfg);
esp_wifi_set_mode(WIFI_MODE_STA);
esp_wifi_start();
// 4. 创建控制任务并绑定 Core1
xTaskCreatePinnedToCore(control_task, "ctrl", 4096, NULL,
CONTROL_TASK_PRIO, NULL, 1);
}
五、注意事项与调优建议
- 栈大小:控制任务建议 4KB 以上,WiFi 事件任务 4KB 以上,避免栈溢出。
-
优先级:控制任务优先级应高于 WiFi 任务(通常 WiFi 任务优先级为 23),但不要超过
configMAX_PRIORITIES - 1。 -
中断分配:ESP32-S3 的外设中断可绑定到指定核心,使用
esp_intr_alloc()时指定ESP_INTR_FLAG_IRAM和核心编号。 -
Cache 争抢:双核共享 Cache,若控制任务对时间敏感,可将关键代码放入 IRAM(
IRAM_ATTR)。 -
看门狗:Core1 上的控制任务若长时间不喂狗,会触发任务看门狗,需在循环中调用
vTaskDelay或喂狗。 -
调试:使用
xTaskGetAffinity()确认任务实际运行的核心,使用esp_timer测量控制周期抖动。
通过以上配置,WiFi 协议栈固定在 Core0,实时控制任务固定在 Core1,两者物理隔离,控制周期抖动可控制在 10us 以内,满足大多数嵌入式实时控制需求。