【esp32学习笔记】ESP32基于Arduino框架的多任务系统实现和 loop() 函数的本质(PlatformIO)
问:
ESP32基于Arduino框架在 PlatformIO 的多任务系统,A任务每隔50ms执行一次,B任务每隔100ms执行一次,C任务每隔1000ms执行一次。
在ESP32上使用Arduino框架实现多任务系统,可以通过FreeRTOS(已集成在ESP32 Arduino核心中)来实现。以下是实现A、B、C三个任务以不同频率运行的代码示例:
完整代码实现
#include <Arduino.h>
// 定义任务句柄
TaskHandle_t TaskAHandle = NULL;
TaskHandle_t TaskBHandle = NULL;
TaskHandle_t TaskCHandle = NULL;
// 任务A:每50ms执行一次
void TaskA(void *pvParameters) {
const TickType_t xDelay = 50 / portTICK_PERIOD_MS; // 50ms转换为FreeRTOS ticks
for(;;) {
// 任务A的工作代码
Serial.println("Task A executed");
vTaskDelay(xDelay); // 延迟50ms
}
}
// 任务B:每100ms执行一次
void TaskB(void *pvParameters) {
const TickType_t xDelay = 100 / portTICK_PERIOD_MS; // 100ms转换为FreeRTOS ticks
for(;;) {
// 任务B的工作代码
Serial.println("Task B executed");
vTaskDelay(xDelay); // 延迟100ms
}
}
// 任务C:每1000ms执行一次
void TaskC(void *pvParameters) {
const TickType_t xDelay = 1000 / portTICK_PERIOD_MS; // 1000ms转换为FreeRTOS ticks
for(;;) {
// 任务C的工作代码
Serial.println("Task C executed");
vTaskDelay(xDelay); // 延迟1000ms
}
}
void setup() {
Serial.begin(115200);
// 创建任务A
xTaskCreate(
TaskA, // 任务函数
"TaskA", // 任务名称
2048, // 堆栈大小(字节)
NULL, // 任务参数
1, // 优先级(0-24,数字越大优先级越高)
&TaskAHandle // 任务句柄
);
// 创建任务B
xTaskCreate(
TaskB,
"TaskB",
2048,
NULL,
1,
&TaskBHandle
);
// 创建任务C
xTaskCreate(
TaskC,
"TaskC",
2048,
NULL,
1,
&TaskCHandle
);
// 如果不需要在setup中执行其他操作,可以删除以下行
// 因为FreeRTOS调度器会自动接管
}
void loop() {
// 在基于FreeRTOS的应用中,loop()通常为空
// 因为所有工作都在任务中完成
vTaskDelete(NULL); // 删除loop任务(可选)
}
代码说明
-
任务创建:
-
使用
xTaskCreate()函数创建三个任务 -
每个任务有自己的函数、名称、堆栈大小、优先级和句柄
-
-
任务频率控制:
-
使用
vTaskDelay()函数控制任务执行频率 -
portTICK_PERIOD_MS是FreeRTOS的tick周期(通常为1ms)
-
-
优先级:
-
所有任务设置为相同优先级(1),这意味着它们将平等分享CPU时间
-
如果需要,可以调整优先级(0-24,数字越大优先级越高)
-
-
堆栈大小:
-
每个任务分配了2048字节的堆栈,对于简单任务可能过大,可以根据实际需求调整
-
PlatformIO配置建议
在platformio.ini中添加以下配置:
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
替代方案:使用定时器
如果任务非常简单,也可以使用硬件定时器实现:
#include <Arduino.h>
#include <Ticker.h>
Ticker timerA, timerB, timerC;
void taskA() {
Serial.println("Task A executed");
}
void taskB() {
Serial.println("Task B executed");
}
void taskC() {
Serial.println("Task C executed");
}
void setup() {
Serial.begin(115200);
timerA.attach_ms(50, taskA);
timerB.attach_ms(100, taskB);
timerC.attach_ms(1000, taskC);
}
void loop() {
// 空循环
}
选择哪种方案取决于任务复杂度和实时性要求。FreeRTOS任务更适合复杂的、需要独立堆栈的任务,而定时器适合简单的回调函数。
使用FreeRTOS后还会执行loop()函数吗
在使用 FreeRTOS 的 ESP32 Arduino 环境中,loop() 函数仍然会被执行,但它本身也是一个 FreeRTOS 任务(称为 loopTask),其优先级通常较低(默认为 1)。具体行为如下:
1. loop() 的运行机制
-
在 Arduino-ESP32 框架中,
loop()实际上是在一个 FreeRTOS 任务中运行的,其实现类似于:void loopTask(void *pvParameters) { setup(); // 先执行 setup() for (;;) { loop(); // 循环执行 loop() } } -
这个任务的优先级通常为
1(与示例代码中的TaskA/B/C相同),因此它会与其他同优先级任务平等分享 CPU 时间(通过 FreeRTOS 的时间片轮转调度)。
2. loop() 与其他任务的交互
-
如果其他任务优先级更高(例如优先级 >1),则高优先级任务会抢占
loop()。 -
如果优先级相同(如示例代码中的任务优先级均为 1),则
loop()会与其他任务按时间片轮流执行。 -
如果
loop()中有阻塞操作(如delay()),会主动让出 CPU 给其他任务。
3. 是否需要保留 loop()?
-
保留
loop():可以用于低优先级任务(如状态监测、非实时逻辑)。 -
删除
loop():如果所有功能都已通过 FreeRTOS 任务实现,可以在setup()中调用vTaskDelete(NULL)删除loopTask(如示例代码末尾所示),以节省资源。
4. 关键注意事项
-
避免在
loop()中使用阻塞函数:
如delay(1000)会阻塞整个loopTask,改用vTaskDelay()以释放 CPU。void loop() { // 错误:阻塞整个 loopTask // delay(1000); // 正确:释放 CPU 给其他任务 vTaskDelay(pdMS_TO_TICKS(1000)); } -
优先级冲突:
如果loop()和其他任务优先级相同,且loop()中有大量计算,可能导致其他任务无法及时执行。此时可以:-
降低
loop()的优先级(需修改 Arduino-ESP32 底层配置)。 -
将
loop()中的逻辑移到独立任务中。
-
-
默认
loop()的堆栈大小:
Arduino-ESP32 中loopTask的默认堆栈较小(约 4KB),复杂操作可能导致栈溢出。可通过修改menuconfig调整(PlatformIO 中需自定义sdkconfig)。
5. 如何彻底禁用 loop()?
如果需要完全移除 loop(),可以在 setup() 中删除 loopTask:
void setup() {
// 创建其他任务...
vTaskDelete(NULL); // 删除当前任务(即 loopTask)
}
void loop() {
// 不会执行到这里
}
总结
| 场景 | loop() 的行为 |
|---|---|
| 不主动干预 | 作为优先级=1 的任务与其他任务并发运行 |
loop() 中有阻塞操作 |
使用 delay() 会冻结任务,改用 vTaskDelay() |
删除 loopTask |
调用 vTaskDelete(NULL) 可释放资源 |
建议在复杂的多任务系统中,将逻辑拆分到独立的 FreeRTOS 任务中,并减少 loop() 的使用,以提高可预测性和实时性。

浙公网安备 33010602011771号