2. 任务优先级与抢占式调度

任务说明

本案例在RTOS中创建三个不同优先级的任务,通过LED翻转与串口打印的组合,直观展示高优先级任务对低优先级任务的抢占过程。任务A(最高优先级)每500ms翻转LED1并打印一次;任务B(中等优先级)每1000ms翻转LED2并打印一次;任务C(最低优先级)每2000ms翻转LED3并打印一次。由于任务A周期最短且优先级最高,当任务A就绪时会立即抢占正在运行的低优先级任务,从而在串口输出中观察到任务A的打印总是准时出现,而任务B和任务C的打印可能被延迟。

硬件连接

  • LED1 → GPIO2(开发板载LED)
  • LED2 → GPIO4(外接LED,串联330Ω电阻到GND)
  • LED3 → GPIO5(外接LED,串联330Ω电阻到GND)
  • 串口默认配置:115200-8-N-1
接线示意:ESP32开发板GPIO4/GPIO5分别通过限流电阻连接LED阳极,LED阴极接GND。GPIO2为板载LED,无需外接。

代码实现

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"

#define LED1_GPIO  2
#define LED2_GPIO  4
#define LED3_GPIO  5

// 任务函数声明
void taskA(void *arg);
void taskB(void *arg);
void taskC(void *arg);

void app_main(void)
{
    // 初始化GPIO
    gpio_set_direction(LED1_GPIO, GPIO_MODE_OUTPUT);
    gpio_set_direction(LED2_GPIO, GPIO_MODE_OUTPUT);
    gpio_set_direction(LED3_GPIO, GPIO_MODE_OUTPUT);
    gpio_set_level(LED1_GPIO, 0);
    gpio_set_level(LED2_GPIO, 0);
    gpio_set_level(LED3_GPIO, 0);

    // 创建三个不同优先级的任务
    // 参数:任务函数、任务名、栈大小、参数、优先级、任务句柄
    xTaskCreate(taskA, "taskA", 2048, NULL, 3, NULL);  // 最高优先级
    xTaskCreate(taskB, "taskB", 2048, NULL, 2, NULL);  // 中等优先级
    xTaskCreate(taskC, "taskC", 2048, NULL, 1, NULL);  // 最低优先级
}

// 任务A:最高优先级,每500ms翻转LED1
void taskA(void *arg)
{
    while (1) {
        gpio_set_level(LED1_GPIO, 1);
        printf("[TaskA] LED1 ON, tick=%lu\n", (unsigned long)xTaskGetTickCount());
        vTaskDelay(pdMS_TO_TICKS(500));

        gpio_set_level(LED1_GPIO, 0);
        printf("[TaskA] LED1 OFF, tick=%lu\n", (unsigned long)xTaskGetTickCount());
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

// 任务B:中等优先级,每1000ms翻转LED2
void taskB(void *arg)
{
    while (1) {
        gpio_set_level(LED2_GPIO, 1);
        printf("[TaskB] LED2 ON, tick=%lu\n", (unsigned long)xTaskGetTickCount());
        vTaskDelay(pdMS_TO_TICKS(1000));

        gpio_set_level(LED2_GPIO, 0);
        printf("[TaskB] LED2 OFF, tick=%lu\n", (unsigned long)xTaskGetTickCount());
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

// 任务C:最低优先级,每2000ms翻转LED3
void taskC(void *arg)
{
    while (1) {
        gpio_set_level(LED3_GPIO, 1);
        printf("[TaskC] LED3 ON, tick=%lu\n", (unsigned long)xTaskGetTickCount());
        vTaskDelay(pdMS_TO_TICKS(2000));

        gpio_set_level(LED3_GPIO, 0);
        printf("[TaskC] LED3 OFF, tick=%lu\n", (unsigned long)xTaskGetTickCount());
        vTaskDelay(pdMS_TO_TICKS(2000));
    }
}

抢占调度时序示意

任务抢占调度时序(tick单位:ms) 0 500 1000 1500 2000 TaskA (Prio3) TaskB (Prio2) TaskC (Prio1) TaskA抢占 TaskA抢占 TaskB抢占 运行中 被抢占/阻塞

编译与运行

# 在ESP-IDF环境中编译
idf.py set-target esp32
idf.py build
idf.py -p /dev/ttyUSB0 flash monitor

验证方法与判定标准

  1. 观察LED闪烁节奏:LED1以约500ms周期快速闪烁,LED2约1000ms,LED3约2000ms。若三个LED均按各自周期闪烁,说明三个任务均被正常调度执行。
  2. 串口输出分析:在串口监视器中观察打印信息。TaskA的打印间隔严格保持500ms(tick差值约500),不受其他任务影响;TaskB和TaskC的打印间隔可能因被TaskA抢占而出现微小抖动,但平均周期仍接近其设定值。
  3. 抢占行为确认:当TaskA每次就绪时(tick为500的整数倍),若此时TaskB或TaskC正在运行,TaskA会立即获得CPU控制权。可通过串口时间戳验证:TaskA的打印tick值总是精确出现在500ms的整数倍附近,而TaskB/TaskC的打印tick值可能被推迟到TaskA休眠后才出现。
  4. 判定通过标准:串口输出中TaskA的打印间隔误差小于10ms(即tick差值在490~510之间),且TaskB/TaskC的打印间隔平均值接近其设定周期,即可判定抢占式调度机制工作正常。
预期输出示例(tick值基于1000Hz系统节拍):

[TaskA] LED1 ON, tick=0
[TaskB] LED2 ON, tick=0
[TaskC] LED3 ON, tick=0
[TaskA] LED1 OFF, tick=500
[TaskA] LED1 ON, tick=1000
[TaskB] LED2 OFF, tick=1000
[TaskA] LED1 OFF, tick=1500
[TaskA] LED1 ON, tick=2000
[TaskC] LED3 OFF, tick=2000