// =====================================================
// FreeRTOS任务管理实验 - ESP32版(Wokwi平台)
// 适合:嵌入式系统课程 / FreeRTOS入门实验
// 作者:教学用示例
// 最后更新:2026年3月
// =====================================================
// ========== 第一部分:基础知识讲解 ==========
/*
* 【FreeRTOS是什么?】
* FreeRTOS是一个开源的实时操作系统内核,专为嵌入式系统设计。
* 它允许我们在单核/多核处理器上"同时"运行多个任务。
*
* 【ESP32与FreeRTOS】
* ESP32的Arduino核心已经内置了FreeRTOS,我们不需要额外安装。
* 每个任务都有独立的栈空间和优先级,由内核调度器管理。
*/
// ========== 第二部分:引脚定义 ==========
// 这部分定义了硬件连接关系
// 讲解点:GPIO引脚的功能分配
#define LED_TASK1 13 // 任务1指示灯(红色) - 连接到ESP32的GPIO13
#define LED_TASK2 12 // 任务2指示灯(红色) - 连接到ESP32的GPIO12
#define LED_CTRL 14 // 控制任务指示灯(绿色) - 显示控制任务运行状态
#define LED_STATUS 27 // 状态指示灯(黄色) - 用于按键反馈
// 【按键定义】使用INPUT_PULLUP模式,按键按下时读到LOW
#define BTN_SUSPEND 4 // 挂起/恢复任务1的按键 - 连接到GPIO4
#define BTN_DELETE 5 // 删除任务2的按键 - 连接到GPIO5
// ========== 第三部分:任务句柄 ==========
/*
* 【任务句柄是什么?】
* 任务句柄就像任务的"身份证",我们可以通过句柄来操作任务
* 比如:挂起、恢复、删除、查询状态等
*/
TaskHandle_t task1Handle = NULL; // 任务1的句柄,初始为NULL
TaskHandle_t task2Handle = NULL; // 任务2的句柄
TaskHandle_t ctrlTaskHandle = NULL; // 控制任务的句柄
// 任务状态标志
bool task1Suspended = false; // 记录任务1是否被挂起
// ========== 第四部分:任务函数实现 ==========
/*
* 【任务函数的特点】
* 1. 通常是一个无限循环(while(1))
* 2. 必须有阻塞点(vTaskDelay、等待队列、信号量等)
* 3. 不能使用return退出(除非任务要自我删除)
*/
// 【教学点】任务1:快速闪烁(演示最基本的任务)
void task1(void *pvParameters) {
// pvParameters是创建任务时传入的参数,本例中未使用
// 初始化引脚(每个任务需要独立初始化自己使用的硬件)
pinMode(LED_TASK1, OUTPUT);
// 任务主循环
while(1) {
// LED点亮
digitalWrite(LED_TASK1, HIGH);
vTaskDelay(pdMS_TO_TICKS(250)); // 延时250ms,让出CPU
// LED熄灭
digitalWrite(LED_TASK1, LOW);
vTaskDelay(pdMS_TO_TICKS(250)); // 延时250ms,让出CPU
/* ===== 实验任务1开始:让学生添加代码 =====
* 【任务要求】在任务1中添加一个计数器,每闪烁10次后,
* 通过串口打印一次"Task 1 has blinked 10 times"
*
* 提示:需要定义一个静态变量 static int count = 0;
* ===== 实验任务1结束 ===== */
}
}
// 【教学点】任务2:慢速闪烁(演示不同周期的任务)
void task2(void *pvParameters) {
pinMode(LED_TASK2, OUTPUT);
while(1) {
digitalWrite(LED_TASK2, HIGH);
vTaskDelay(pdMS_TO_TICKS(500)); // 亮500ms
digitalWrite(LED_TASK2, LOW);
vTaskDelay(pdMS_TO_TICKS(500)); // 灭500ms
/* ===== 实验任务2开始:让学生添加代码 =====
* 【任务要求】让任务2在运行20次后自动删除自己
* 提示:需要定义一个计数器,当计数达到20时调用
* vTaskDelete(NULL) 删除自己
*
* 注意:删除自己后,while循环就不再执行了
* ===== 实验任务2结束 ===== */
}
}
// 【教学点】控制任务:监测按键输入,管理其他任务
// 这个任务演示了任务间通信和控制
void ctrlTask(void *pvParameters) {
// 初始化所有用到的引脚
pinMode(LED_CTRL, OUTPUT);
pinMode(LED_STATUS, OUTPUT);
pinMode(BTN_SUSPEND, INPUT_PULLUP);
pinMode(BTN_DELETE, INPUT_PULLUP);
// 记录按键上一个状态,用于检测边沿
int lastBtnSuspendState = HIGH;
int lastBtnDeleteState = HIGH;
while(1) {
// 控制任务自身闪烁(表示正在运行)
digitalWrite(LED_CTRL, !digitalRead(LED_CTRL));
// ===== 按键1处理:挂起/恢复任务1 =====
// 【教学点】边沿检测:只检测按键从高到低的变化(按下瞬间)
int btnSuspendState = digitalRead(BTN_SUSPEND);
if (lastBtnSuspendState == HIGH && btnSuspendState == LOW) {
// 按键被按下(下降沿触发)
/* ===== 实验任务3开始:让学生修改代码 =====
* 【任务要求】修改按键1的功能,改为:
* - 短按(<500ms):挂起/恢复任务1
* - 长按(>500ms):删除任务1
*
* 提示:需要记录按键按下的时间
* 使用 millis() 或 xTaskGetTickCount()
* ===== 实验任务3结束 ===== */
if (task1Suspended) {
// 如果任务1被挂起,则恢复它
vTaskResume(task1Handle);
task1Suspended = false;
Serial.println(">>> 恢复任务1 <<<");
} else {
// 如果任务1在运行,则挂起它
vTaskSuspend(task1Handle);
task1Suspended = true;
Serial.println(">>> 挂起任务1 <<<");
}
// 状态LED闪烁一下表示按键响应
digitalWrite(LED_STATUS, HIGH);
vTaskDelay(pdMS_TO_TICKS(100));
digitalWrite(LED_STATUS, LOW);
}
lastBtnSuspendState = btnSuspendState;
// ===== 按键2处理:删除任务2 =====
int btnDeleteState = digitalRead(BTN_DELETE);
if (lastBtnDeleteState == HIGH && btnDeleteState == LOW) {
if (task2Handle != NULL) {
// 【教学点】vTaskDelete() 函数的使用
// 传入任务句柄,删除指定任务
vTaskDelete(task2Handle);
task2Handle = NULL; // 将句柄置为NULL,避免重复删除
Serial.println(">>> 删除任务2 <<<");
// 状态LED快速闪烁表示删除操作
for (int i = 0; i < 3; i++) {
digitalWrite(LED_STATUS, HIGH);
vTaskDelay(pdMS_TO_TICKS(50));
digitalWrite(LED_STATUS, LOW);
vTaskDelay(pdMS_TO_TICKS(50));
}
/* ===== 实验任务4开始:让学生添加代码 =====
* 【任务要求】在删除任务2后,尝试重新创建任务2
* 要求:按下按键1(长按3秒)重新创建任务2
* 提示:需要再次调用 xTaskCreate()
* ===== 实验任务4结束 ===== */
}
}
lastBtnDeleteState = btnDeleteState;
// 控制任务每200ms循环一次
vTaskDelay(pdMS_TO_TICKS(200));
}
}
// ========== 第五部分:setup函数 ==========
/*
* 【教学点】系统初始化
* ESP32启动流程:上电 -> 运行setup() -> 启动调度器 -> 运行任务
*/
void setup() {
// 初始化串口通信,用于调试输出
Serial.begin(115200);
delay(1000); // 等待串口稳定
// 打印实验说明
Serial.println("=================================");
Serial.println("FreeRTOS任务管理实验 - ESP32版");
Serial.println("=================================");
Serial.println("【实验目的】");
Serial.println("1. 理解FreeRTOS的任务创建、删除、挂起和恢复");
Serial.println("2. 掌握任务优先级的概念");
Serial.println("3. 学习任务间的通信和控制");
Serial.println("");
Serial.println("【操作说明】");
Serial.println("按键1 (GPIO4): 挂起/恢复任务1 (红色LED1)");
Serial.println("按键2 (GPIO5): 删除任务2 (红色LED2)");
Serial.println("---------------------------------");
// ===== 实验任务5开始:让学生修改代码 =====
/*
* 【任务要求】创建第四个任务
* 任务功能:读取温度传感器(模拟)
* 硬件:添加一个LM35温度传感器(或者用随机数模拟)
* 每2秒读取一次温度,并在串口打印
*
* 提示:需要先在diagram.json中添加温度传感器
* 传感器类型:"wokwi-lm35"
* ===== 实验任务5结束 =====
*/
// ========== 创建任务 ==========
/*
* 【xTaskCreate()参数详解】
* 参数1: 任务函数指针
* 参数2: 任务名称(用于调试)
* 参数3: 栈大小(单位:字节,ESP32通常2048足够)
* 参数4: 任务参数(可以为NULL)
* 参数5: 任务优先级(0最低,configMAX_PRIORITIES-1最高)
* 参数6: 任务句柄(用于后续控制)
*/
// 创建任务1:优先级1,红色LED快速闪烁
xTaskCreate(
task1, // 任务函数
"Task 1", // 任务名称(方便调试识别)
2048, // 栈大小(字节)
NULL, // 无参数传入
1, // 优先级1(较低)
&task1Handle // 保存任务句柄
);
Serial.println("[创建] 任务1 (优先级1) - 红色LED快速闪烁");
// 创建任务2:优先级1,红色LED慢速闪烁
xTaskCreate(
task2,
"Task 2",
2048,
NULL,
1,
&task2Handle
);
Serial.println("[创建] 任务2 (优先级1) - 红色LED慢速闪烁");
// 创建控制任务:优先级2(稍高),用于管理其他任务
xTaskCreate(
ctrlTask,
"Ctrl Task",
3072, // 控制任务需要更大的栈空间
NULL,
2, // 优先级2(比任务1、2高)
&ctrlTaskHandle
);
Serial.println("[创建] 控制任务 (优先级2) - 绿色LED闪烁");
Serial.println("=================================");
Serial.println("所有任务已创建,仿真开始运行!");
Serial.println("提示:打开串口监视器查看输出信息");
Serial.println("=================================");
}
// ========== 第六部分:loop函数 ==========
/*
* 【教学点】Arduino的loop()在FreeRTOS中的角色
*
* 在ESP32中,loop()运行在一个名为"loopTask"的低优先级任务中
* 它的优先级是1(和task1、task2相同)
*
* 注意:不能在loop()中使用delay(),应该用vTaskDelay()
*/
void loop() {
// 用于周期性打印的计数器
static int counter = 0;
counter++;
// 每100个循环(约50秒)打印一次系统状态
if (counter % 100 == 0) {
Serial.println("\n=== 系统状态报告 ===");
// 打印任务1状态
Serial.print("任务1 ");
if (task1Suspended) {
Serial.println("[挂起]");
} else if (task1Handle != NULL) {
Serial.println("[运行中]");
}
// 打印任务2状态
Serial.print("任务2 ");
if (task2Handle == NULL) {
Serial.println("[已删除]");
} else {
Serial.println("[运行中]");
}
// 获取剩余堆内存
Serial.printf("空闲堆内存: %d 字节\n", esp_get_free_heap_size());
/* ===== 实验任务6开始:让学生添加代码 =====
* 【任务要求】在系统状态报告中添加更多信息
* 1. 显示当前运行的任务数量(提示:uxTaskGetNumberOfTasks())
* 2. 显示每个任务的栈使用情况(提示:uxTaskGetStackHighWaterMark())
* 3. 显示系统运行时间(提示:xTaskGetTickCount())
* ===== 实验任务6结束 ===== */
Serial.println("====================\n");
}
// 必须使用vTaskDelay()而不是delay()
vTaskDelay(pdMS_TO_TICKS(500)); // loop每500ms运行一次
}
// ========== 附录:FreeRTOS常用函数速查表 ==========
/*
* 【任务管理函数】
* xTaskCreate() - 创建任务
* vTaskDelete() - 删除任务
* vTaskSuspend() - 挂起任务
* vTaskResume() - 恢复任务
* vTaskDelay() - 延时任务
* vTaskDelayUntil() - 精确周期性延时
*
* 【任务信息获取】
* xTaskGetCurrentTaskHandle() - 获取当前任务句柄
* uxTaskGetNumberOfTasks() - 获取任务数量
* uxTaskGetStackHighWaterMark() - 获取栈剩余空间
*
* 【注意事项】
* 1. 任务函数中不能使用delay(),要用vTaskDelay()
* 2. 共享资源访问需要使用互斥机制(队列、信号量、互斥锁)
* 3. 优先级高的任务会抢占优先级低的任务
*/