前面我们写的所有程序都是"一条道走到黑"——在 loop() 里轮流执行各个功能。一旦某个操作耗时较长(比如等待传感器数据、刷新屏幕、处理网络请求),其他功能就会被"卡住"。这在真实产品中是不可接受的:你不能因为屏幕在刷新就让按键失灵,也不能因为等待WiFi响应就让电机停转。
FreeRTOS 是 ESP32 内置的实时操作系统内核,它让你可以把一个大程序拆成多个"任务"(Task),由操作系统调度器自动分配CPU时间。更厉害的是,ESP32 是双核处理器——Core 0 和 Core 1 可以真正同时执行两个任务,而不是快速切换造成的"假并行"。
今天我们从零上手 FreeRTOS:创建任务、设置优先级、用互斥量保护共享资源、用消息队列实现任务间通信。学完你会发现,之前所有单任务教程都可以用多任务架构重构一遍,性能直接翻倍。
| 物料 | 型号/参数 | 数量 | 参考价格 |
|---|---|---|---|
| ESP32 开发板 | ESP32-WROOM-32(双核240MHz) | 1 | ¥18-25 |
| LED 发光二极管 | 5mm 红色/绿色/蓝色各一 | 3 | ¥0.5 |
| 220Ω 限流电阻 | 1/4W 色环电阻 | 3 | ¥0.3 |
| 按键开关 | 6×6mm 四脚轻触开关 | 1 | ¥0.5 |
| DHT22 温湿度模块 | (可选,用于进阶实战) | 1 | ¥6-8 |
| 面包板 + 杜邦线 | 830孔面包板,公对公杜邦线若干 | 1套 | ¥8-12 |
| 总计: | 约 ¥35 | ||
接线方案(双LED + 按键): LED红 (+)──220Ω──GPIO25 LED红 (-)──────────GND LED绿 (+)──220Ω──GPIO26 LED绿 (-)──────────GND 按键 一脚─────────GPIO33(启用内部上拉) 按键 另一脚────────GND 板载LED(无需接线): ESP32 板载 LED → GPIO2
下面的代码创建了三个独立任务:Task1(板载LED 500ms闪烁)、Task2(红灯 700ms闪烁)、Task3(绿灯 1100ms闪烁)。三个任务互不干扰,各自独立运行。
// freertos_basic.ino — ESP32 FreeRTOS 三任务并行闪烁 // 无需安装额外库,Arduino IDE 自带 FreeRTOS 支持 // ====== 任务句柄(用于后续管理)====== TaskHandle_t Task1_Handle = NULL; TaskHandle_t Task2_Handle = NULL; TaskHandle_t Task3_Handle = NULL; // ====== 引脚定义 ====== #define LED_BUILTIN 2 // 板载LED #define LED_RED 25 // 红色LED #define LED_GREEN 26 // 绿色LED // ====== 任务1:板载LED闪烁(500ms周期)====== void Task_BlinkBuiltin(void *pvParameters) { pinMode(LED_BUILTIN, OUTPUT); for (;;) { // FreeRTOS任务必须是无尽循环 digitalWrite(LED_BUILTIN, HIGH); vTaskDelay(500 / portTICK_PERIOD_MS); digitalWrite(LED_BUILTIN, LOW); vTaskDelay(500 / portTICK_PERIOD_MS); } } // ====== 任务2:红色LED闪烁(700ms周期)====== void Task_BlinkRed(void *pvParameters) { pinMode(LED_RED, OUTPUT); for (;;) { digitalWrite(LED_RED, HIGH); vTaskDelay(700 / portTICK_PERIOD_MS); digitalWrite(LED_RED, LOW); vTaskDelay(700 / portTICK_PERIOD_MS); } } // ====== 任务3:绿色LED闪烁(1100ms周期)====== void Task_BlinkGreen(void *pvParameters) { pinMode(LED_GREEN, OUTPUT); for (;;) { digitalWrite(LED_GREEN, HIGH); vTaskDelay(1100 / portTICK_PERIOD_MS); digitalWrite(LED_GREEN, LOW); vTaskDelay(1100 / portTICK_PERIOD_MS); } } void setup() { Serial.begin(115200); Serial.println("🚀 FreeRTOS 三任务演示启动"); // 创建三个任务(xTaskCreate参数说明见下方表格) xTaskCreate( Task_BlinkBuiltin, // 任务函数 "Blink Builtin", // 任务名称(调试用,最多16字符) 2048, // 栈大小(字,不是字节!ESP32 1字=1字节) NULL, // 传递给任务的参数 1, // 优先级(0~24,数值越大越优先) &Task1_Handle // 任务句柄(可选,用于后续管理) ); xTaskCreate(Task_BlinkRed, "Blink Red", 2048, NULL, 1, &Task2_Handle); xTaskCreate(Task_BlinkGreen, "Blink Green", 2048, NULL, 1, &Task3_Handle); Serial.println("✅ 三个FreeRTOS任务已创建,观察LED闪烁节奏"); } void loop() { // setup() 中已启动三个任务,loop() 可以空闲或做监控 Serial.printf("📊 系统运行中 | 剩余堆内存: %u 字节\n", xPortGetFreeHeapSize()); vTaskDelay(5000 / portTICK_PERIOD_MS); }
| 参数 | 含义 | 本示例取值 |
|---|---|---|
| pvTaskCode | 任务函数指针 | Task_BlinkBuiltin |
| pcName | 任务名称(≤16字符) | "Blink Builtin" |
| usStackDepth | 栈大小(字数),ESP32 1字=1字节 | 2048 |
| pvParameters | 传入参数指针 | NULL |
| uxPriority | 优先级 0~24,高优先级的任务优先执行 | 1 |
| pxCreatedTask | 任务句柄指针(NULL=不需要管理) | &Task1_Handle |
多个任务同时操作同一个串口(Serial.print)会导致输出混乱。互斥量(Mutex)就像"厕所门锁"——谁先进去就锁门,其他人排队等。
// mutex_demo.ino — 互斥量保护串口输出 SemaphoreHandle_t xSerialMutex; // 互斥量句柄 void Task_SafePrint(void *pvParams) { int taskID = (int)pvParams; // 用参数区分任务 int count = 0; for (;;) { // 🔒 获取互斥量(最多等100ms,超时就放弃) if (xSemaphoreTake(xSerialMutex, 100 / portTICK_PERIOD_MS) == pdTRUE) { Serial.print("[Task "); Serial.print(taskID); Serial.print("] count = "); Serial.println(count++); // 🔓 释放互斥量 xSemaphoreGive(xSerialMutex); } vTaskDelay(300 / portTICK_PERIOD_MS); } } void setup() { Serial.begin(115200); // 创建互斥量 xSerialMutex = xSemaphoreCreateMutex(); xTaskCreate(Task_SafePrint, "Printer1", 2048, (void*)1, 1, NULL); xTaskCreate(Task_SafePrint, "Printer2", 2048, (void*)2, 1, NULL); xTaskCreate(Task_SafePrint, "Printer3", 2048, (void*)3, 1, NULL); } void loop() { vTaskDelete(NULL); } // 删除loop任务,释放内存
消息队列(Queue)是任务间传递数据的安全通道。下面的例子中,Task_Sensor 模拟采集传感器数据发送到队列,Task_Display 从队列中读取数据并输出。
// queue_demo.ino — 消息队列实现生产者-消费者模式 QueueHandle_t xDataQueue; // 队列句柄 // 定义消息结构体 typedef struct { int sensor_id; float temperature; uint32_t timestamp; } SensorData_t; // ====== 生产者任务:模拟传感器 ====== void Task_Sensor(void *pvParams) { SensorData_t data; for (;;) { data.sensor_id = 1; data.temperature = 25.0 + (float)random(0, 100) / 10.0; data.timestamp = millis(); // 发送到队列(最多等100ms,队列满就放弃) if (xQueueSend(xDataQueue, &data, 100/portTICK_PERIOD_MS) != pdTRUE) { Serial.println("⚠️ 队列已满,数据丢弃"); } vTaskDelay(2000 / portTICK_PERIOD_MS); // 每2秒采集一次 } } // ====== 消费者任务:处理数据 ====== void Task_Display(void *pvParams) { SensorData_t received; for (;;) { // 阻塞等待消息(portMAX_DELAY = 一直等到有数据) if (xQueueReceive(xDataQueue, &received, portMAX_DELAY) == pdTRUE) { Serial.printf("📡 传感器%d | 温度: %.1f°C | 时间: %u\n", received.sensor_id, received.temperature, received.timestamp); } } } void setup() { Serial.begin(115200); // 创建队列:容量10个元素,每个元素大小为 SensorData_t xDataQueue = xQueueCreate(10, sizeof(SensorData_t)); xTaskCreate(Task_Sensor, "Sensor", 2048, NULL, 2, NULL); xTaskCreate(Task_Display, "Display", 2048, NULL, 1, NULL); } void loop() { vTaskDelete(NULL); }
场景:做一个实时温湿度监控系统,要求在刷新 OLED 屏幕的同时:① 每5秒采集一次 DHT22 数据;② 按键按下时锁定/解锁屏幕显示;③ 板载LED根据温度变化闪烁频率。三个功能必须同时运行,互不阻塞。
物料组合:ESP32 + DHT22 + OLED SSD1306 + 按键 + LED
多任务架构设计:
| 任务 | 核心职责 | 优先级 | 联动之前课程 |
|---|---|---|---|
| Task_Sensor | 每5秒采集DHT22,通过队列发送数据 | 3(高) | DHT22 (5/16) + 队列(本节课) |
| Task_Display | 接收队列数据,刷新OLED显示 | 2(中) | OLED SSD1306 (5/14) + I2C (5/12) |
| Task_Button | 轮询按键状态,切换显示锁定标志 | 1 | GPIO输入 (5/3) |
| Task_AlertLED | 根据温度值调节LED闪烁频率 | 1 | GPIO LED (5/3) |
场景:做一个可通过网页遥控的小车,ESP32 同时运行 WebServer 和电机控制。如果像之前教程那样在 loop() 里轮询,网络请求卡顿时小车就会失控。用 FreeRTOS 分离控制逻辑和网络服务。
物料组合:ESP32 + L298N电机驱动 + 小车底盘 + 双电机
多任务架构设计:
| 任务 | 核心职责 | 优先级 | 联动之前课程 |
|---|---|---|---|
| Task_WebServer | 运行 WebServer,接收HTTP遥控指令,写入命令队列 | 2 | WebServer (5/8) + 队列(本节课) |
| Task_MotorCtrl | 从命令队列读取指令,驱动L298N | 3(高) | L298N (5/25) + 队列 |
| Task_Watchdog | 超时检测:500ms内无新指令则自动停车 | 4(最高) | 定时器 + 本节课 |
ESP32 有两个核心,你可以明确指定任务在哪个核心上运行:
// 在 Core 0 上运行(协议栈默认在此) xTaskCreatePinnedToCore( Task_WiFiHandler, "WiFi", 4096, NULL, 2, NULL, 0 // ← 指定 Core 0 ); // 在 Core 1 上运行(Arduino loop 默认在此) xTaskCreatePinnedToCore( Task_MotorControl, "Motor", 2048, NULL, 3, NULL, 1 // ← 指定 Core 1 );
| API | 功能 | 备注 |
|---|---|---|
| xTaskCreate() | 创建任务 | 调度器自动分配核心 |
| xTaskCreatePinnedToCore() | 创建任务并指定核心 | 最后一个参数:0或1 |
| vTaskDelay(ticks) | 任务延时(让出CPU) | pdMS_TO_TICKS(ms) 转换毫秒 |
| vTaskDelete(NULL) | 删除当前任务 | 释放栈内存 |
| vTaskSuspend(handle) | 挂起任务 | 暂停但不删除 |
| vTaskResume(handle) | 恢复被挂起的任务 | |
| xSemaphoreCreateMutex() | 创建互斥量 | 保护共享资源 |
| xQueueCreate(len, size) | 创建消息队列 | 指定容量和元素大小 |
| uxTaskGetStackHighWaterMark() | 查询任务最小剩余栈 | 调试栈溢出 |
| xPortGetFreeHeapSize() | 查询剩余堆内存 | 监控内存使用 |
FreeRTOS 是 ESP32 进阶开发的第一道分水岭。从今天起,你的项目不再是"单线程排队",而是"多任务并行":
✅ 任务创建 — xTaskCreate / xTaskCreatePinnedToCore
✅ 任务延时 — vTaskDelay(不是 delay!)
✅ 互斥量 — 保护串口、I2C总线、SPI等共享资源
✅ 消息队列 — 安全解耦生产者与消费者
✅ 双核分配 — Core 0 管通信,Core 1 管计算
建议你把之前学过的 OLED 显示、DHT22 采集、舵机控制等教程全部用多任务架构重写一遍——这本身就是最好的练习。下次我们将深入 FreeRTOS 的 软件定时器(Software Timer)和事件组(Event Group),进一步解锁更复杂的实时控制场景。