ESP32双核实操:用FreeRTOS把任务分配到两个核

2026-11-11 20:26:29 16阅读 所属项目:ESP32 开发板

ESP32 芯片里塞的是两颗 Xtensa 32 位 LX6 处理器,也就是 core 0 和 core 1。但很多人用 Arduino IDE 写了很久 ESP32,都不知道自己的代码默认只跑在 core 1 上,另一颗核基本闲着。这篇把 xTaskCreatePinnedToCore 的用法理一遍,让两颗核真正同时干活。

ESP32 FreeRTOS 双核

先把结论放在前面:Arduino 代码默认跑在 core 1,想用上 core 0,就得用 FreeRTOS 建任务,把某段代码作为任务钉到指定的核上。另外提醒一句,单核上 FreeRTOS 本来就能多任务轮转,不建任务也能多线程-ish,双核的价值在于两段代码真并行。平时不折腾双核也没关系,用 Arduino IDE 上传代码后它自己就跑起来了,不用操心哪颗核在执行。

先确认你的板子是不是双核

不是所有 ESP32 系列芯片都有两颗核。基于 ESP32-S2、ESP32-C3、ESP32-C6、ESP32-H2 的板子是单核的,比如 ESP32-S2-Saola-1、Seeed Studio XIAO ESP32-C3、ESP32-C6-DevKitC-1 这类常见型号,动手前先查一下自己手上板子用的哪颗芯片。

ESP32 芯片框图,核心区标着 2 (or 1) x Xtensa 32-bit LX6

想知道当前代码跑在哪个核,有一个现成的函数:xPortGetCoreID()。把下面这段烧进去,串口监视器设 115200 波特率,你会看到 setup() 和 loop() 都自报家门。

/*********
  Rui Santos
  Complete project details at https://randomnerdtutorials.com  
*********/

void setup() {
  Serial.begin(115200);
  Serial.print("setup() running on core ");
  Serial.println(xPortGetCoreID());
}

void loop() {
  Serial.print("loop() running on core ");
  Serial.println(xPortGetCoreID());
}

串口监视器显示 setup() 和 loop() 都运行在 core 1

实测输出就是两个 core 1,连 loop() 也在 core 1 上转。这印证了默认行为:不特意指定,你写的所有 Arduino 代码都交给 core 1。

建任务:xTaskCreatePinnedToCore 的七个参数

Arduino IDE 给 ESP32 配好了 FreeRTOS,这是个实时操作系统,能让我们把几段代码拆成独立并行跑的任务。任务可以是闪一个 LED、发一次网络请求、读一组传感器数据,任何独立的事情都行。想把它一次吃透,可以先看 FreeRTOS 的任务入门篇,再回来看这篇的钉核操作,顺序反了容易懵。

建任务分三步。第一步,声明一个任务句柄:

TaskHandle_t Task1;

第二步,在 setup() 里用 xTaskCreatePinnedToCore 创建任务并指定核。它的最后一个参数就是要钉的核,优先级也在参数里一起给:

xTaskCreatePinnedToCore(
      Task1code, /* Function to implement the task */
      "Task1", /* Name of the task */
      10000,  /* Stack size in words */
      NULL,  /* Task input parameter */
      0,  /* Priority of the task */
      &Task1,  /* Task handle. */
      0); /* Core where the task should run */

七个参数逐个拆开看,对着表读一遍基本就明白了:

参数含义示例值
Task1code任务要执行的函数名Task1code
"Task1"任务名字符串"Task1"
10000栈大小,单位是 words10000
NULL传给任务的输入参数NULL
0任务优先级,0 为最低0 或 1
&Task1任务句柄的地址&Task1
0钉到哪个核,最后一个参数0 或 1

优先级从 0 开始起算,数值越大越先被处理器照顾。第三步,把任务函数写出来,里面放一个 for(;;) 无限循环,它就像第二个 loop()。底下这段是教程里的示意写法,原样贴出来看结构就够:

Void Task1code( void * parameter) {
  for(;;) {
    Code for task 1 - infinite loop
    (...)
  }
}

如果运行到某个阶段想把这个任务撤掉,用 vTaskDelete(),参数就是任务句柄:

vTaskDelete(Task1);

完整实例:两个核各闪一个 LED

来做一个能看见效果的最小实验。需要的材料:一块双核的 ESP32 开发板(比如 ESP32 DOIT DEVKIT V1)、两个 5mm LED、两个 330 欧电阻、一块面包板和几根杜邦线。

两个 LED 接到 GPIO 2 和 GPIO 4 的面包板接线图

两个 LED 分别接 GPIO 2 和 GPIO 4,如上图。接下来建两个任务:Task1 钉到 core 0,每 1000 毫秒翻转一次 LED;Task2 钉到 core 1,每 700 毫秒翻转一次。完整代码如下:

/*********
  Rui Santos
  Complete project details at https://randomnerdtutorials.com  
*********/

TaskHandle_t Task1;
TaskHandle_t Task2;

// LED pins
const int led1 = 2;
const int led2 = 4;

void setup() {
  Serial.begin(115200); 
  pinMode(led1, OUTPUT);
  pinMode(led2, OUTPUT);

  //create a task that will be executed in the Task1code() function, with priority 1 and executed on core 0
  xTaskCreatePinnedToCore(
                    Task1code,   /* Task function. */
                    "Task1",     /* name of task. */
                    10000,       /* Stack size of task */
                    NULL,        /* parameter of the task */
                    1,           /* priority of the task */
                    &Task1,      /* Task handle to keep track of created task */
                    0);          /* pin task to core 0 */                  
  delay(500); 

  //create a task that will be executed in the Task2code() function, with priority 1 and executed on core 1
  xTaskCreatePinnedToCore(
                    Task2code,   /* Task function. */
                    "Task2",     /* name of task. */
                    10000,       /* Stack size of task */
                    NULL,        /* parameter of the task */
                    1,           /* priority of the task */
                    &Task2,      /* Task handle to keep track of created task */
                    1);          /* pin task to core 1 */
    delay(500); 
}

//Task1code: blinks an LED every 1000 ms
void Task1code( void * pvParameters ){
  Serial.print("Task1 running on core ");
  Serial.println(xPortGetCoreID());

  for(;;){
    digitalWrite(led1, HIGH);
    delay(1000);
    digitalWrite(led1, LOW);
    delay(1000);
  } 
}

//Task2code: blinks an LED every 700 ms
void Task2code( void * pvParameters ){
  Serial.print("Task2 running on core ");
  Serial.println(xPortGetCoreID());

  for(;;){
    digitalWrite(led2, HIGH);
    delay(700);
    digitalWrite(led2, LOW);
    delay(700);
  }
}

void loop() {
  
}

有一个细节值得点出来:loop() 本身默认跑在 core 1,所以其实可以不建 Task2,把它的代码直接塞进 loop() 里,效果一样。这份教程为了演示双核才特意建了两个任务。但真到了任务多、逻辑散的项目里,把代码按任务组织反而更清爽,看个人取舍。

接线本身没有玄机:LED 长脚那端经 330 欧电阻接到 GPIO,短脚接 GND,两路一模一样。烧录前确认 Arduino IDE 里选的板子型号和 COM 口没选错,这是新手最常翻车的一步。

代码是怎么跑起来的

开头声明两个句柄 Task1 和 Task2,再把 GPIO 2 和 GPIO 4 分给两个 LED,初始化串口和 pinMode 之后,setup() 里两次调用 xTaskCreatePinnedToCore,唯一的关键差别就是最后一个参数:一次给 0,一次给 1。两个任务的优先级都给了 1,栈大小都是 10000 words。

任务函数里先打印一句 xPortGetCoreID() 的结果自报家门,方便调试确认任务真的落在了预期的核上,然后进 for(;;) 无限循环各干各的。

烧录时选对板子和 COM 口,打开 115200 波特率的串口监视器,应该看到这样的输出:

串口监视器显示 Task1 running on core 0,Task2 running on core 1

与此同时,面包板上一颗 LED 每秒闪一次,另一颗每 700 毫秒闪一次,节奏完全独立:

两个 LED 以 1 秒和 700 毫秒两种频率各自闪烁

两颗 LED 节奏互不干扰,说明两个任务确实在两颗核上并行跑着,不是单核上排队轮转的假并行。

什么场合值得把任务钉到核上

闪 LED 这种量级的活,其实感受不出单核轮转和双核并行的差别。真正值得拆核的场景是两段代码都很重的时候:比如一颗核专门阻塞式地读传感器,另一颗核维持网络服务、对外发布数据,谁也别拖累谁。

顺着这个思路,可以把 FreeRTOS 的其他机制也补上:任务之间传数据用队列(queue),协调资源用信号量(semaphore),定时触发用软件定时器。这些都是围绕任务展开的配套工具,掌握了 xTaskCreatePinnedToCore 之后再看它们会顺很多。

动手建议从这篇的双 LED 实验起步,跑通之后把 Task1code 里的闪灯换成你自己的重活,比如传感器读数或者 WiFi 上报,感受一下两颗核各管一摊的清爽。代码不长,十分钟就能烧完看到结果。

最后把这篇的核心结论收拢一下:大多数 ESP32 型号是双核的;Arduino 代码默认跑 core 1;想用 core 0 就得建 FreeRTOS 任务;xTaskCreatePinnedToCore 负责把任务钉到指定的核,最后一个参数说了算。这套方法的价值在于让两个任务真正独立并行地跑起来,而不是在单核上排队。数字和参数我都对照原教程核对过,栈大小 10000 words、优先级 1、LED 的 GPIO 2 和 GPIO 4 这些值可以直接照抄。


作者:数据媛小满
链接:https://www.aiddithome.com/p/91a6f64dc652e.html
本作版权归作者所有,未经允许请勿转载。若存在违规行为,您可以点击 “举报”。


登录 后发表评论
5条评论
隔壁工位的好奇宝
1楼 · 22分钟前

试了,一个 1s 一个 700ms,两颗 LED 各闪各的,串口输出和文章截图一模一样

阿强焊点总飞
2楼 · 22分钟前

钉核这个说法比绑定接地气,最后一个参数给 0 还是给 1 就是全部秘密了

锡点
3楼 · 22分钟前

这个 xPortGetCoreID() 的自查思路很实用。我之前一直搞不懂为什么两个任务写在同一个 loop 里节奏会互相影响,看完才知道默认全挤在 core 1 上。照这篇把读传感器和刷屏分开钉到两个核之后,OLED 那边再也没掉过帧。栈大小 10000 words 那个参数我一开始不敢给,实际跑下来没问题,官方例程就是这个值。

只看不说话
4楼 · 22分钟前

七参数表格好评,比看文档顺眼多了

线比代码多
5楼 · 22分钟前

Task2 其实可以省掉直接用 loop 这点讲得清楚,省得新手以为必须建满两个任务