08-时间片调度与延时管理.md 9.9 KB


tags: [source-summary] type: source source: "尚硅谷嵌入式技术之FreeRTOS实时操作系统 V1.0.3 — 第7章 时间片调度 + 第9章 时间管理/延时函数" author: "尚硅谷研究院" date: 2026-07-17

created: 2026-07-17

时间片调度与延时管理

用生活理解:时间片调度就像自助餐厅的取餐规则——同等级的人轮流取菜,每人吃一口就轮到下一位(Round-Robin)。vTaskDelay 相当于"我过一会儿再来排队",vTaskDelayUntil 则是"以后每周二下午三点我来,雷打不动"。

Concept

Time slice scheduling

时间片调度指多个相同优先级的任务轮流获得 CPU 使用权,每个任务执行一个时间片(一个 Tick)后被切换到下一个同优先级任务。

关键配置:

  • configUSE_TIME_SLICING:启用/禁用时间片调度(默认开启)
  • configUSE_PREEMPTION:启用抢占式调度(必须开启,否则时间片不生效)
  • configTICK_RATE_HZ:Tick 频率,决定时间片长度

时间片 = 1 个 Tick = 1 / configTICK_RATE_HZ 秒

Working principle

抢占式调度 + 时间片调度

高优先级任务 → 总是先运行(抢占)
同优先级任务 → 轮流运行(时间片轮转)
低优先级任务 → 高/同优先级任务阻塞时才运行
  • configTICK_RATE_HZ = 20 → 每个 Tick 50ms → 时间片 50ms
  • configTICK_RATE_HZ = 1000 → 每个 Tick 1ms → 时间片 1ms

延时函数对比

函数 类型 描述 漂移
vTaskDelay(Tick) 相对延时 从调用开始等待指定 Tick 数 ❌ 会漂移
vTaskDelayUntil(pxPrevWakeTime, Tick) 绝对延时 以固定周期执行,不受执行时间影响 ✅ 固定周期
HAL_Delay(ms) 忙等待 占用 CPU 循环等待 阻塞式查询

vTaskDelay 的漂移问题

第1周期:任务执行 20ms + 延时 500ms = 间隔 520ms
第2周期:任务执行 35ms + 延时 500ms = 间隔 535ms
→ 实际周期越跑越慢(漂移累积)

vTaskDelayUntil 的固定周期

第1周期:记录唤醒时间 t1,延时到 t1+500
第2周期:记录唤醒时间 t2,延时到 t2+500
→ 无论任务执行多久,周期始终为 500ms

Experiment

Requirements

P05(时间片调度)

  • 2 个同优先级任务(优先级均为 2),各自进入临界区打印计数后退出
  • 使用 HAL_Delay(10) 模拟工作量
  • 观察两个任务交替执行(时间片轮转)

P08(延时对比)

  • task1:使用 vTaskDelay(500) + HAL_Delay(20) 模拟工作 → 演示漂移
  • task2:使用 vTaskDelayUntil(&pxPreviousWakeTime, 500) + HAL_Delay(20) → 演示固定周期

Code Implementation

P05:时间片调度

File: 05_时间片调度/Core/Src/freertos_demo.c

#include "freertos_demo.h"
#include "FreeRTOS.h"
#include "task.h"
#include "LED.h"

#define START_TASK_STACK 128
#define START_TASK_PRIORITY 1
TaskHandle_t start_task_handle;
void start_task(void *pvParameters);

/* 两个任务使用相同优先级(均为2)→ 时间片轮转 */
#define TASK1_STACK 128
#define TASK1_PRIORITY 2
TaskHandle_t task1_handle;
void task1(void *pvParameters);

#define TASK2_STACK 128
#define TASK2_PRIORITY 2
TaskHandle_t task2_handle;
void task2(void *pvParameters);

void freertos_start(void)
{
    xTaskCreate((TaskFunction_t)start_task,
                (char *)"start_task",
                (configSTACK_DEPTH_TYPE)START_TASK_STACK,
                (void *)NULL,
                (UBaseType_t)START_TASK_PRIORITY,
                (TaskHandle_t *)&start_task_handle);

    vTaskStartScheduler();
}

void start_task(void *pvParameters)
{
    taskENTER_CRITICAL();

    xTaskCreate((TaskFunction_t)task1, (char *)"task1",
                (configSTACK_DEPTH_TYPE)TASK1_STACK, (void *)NULL,
                (UBaseType_t)TASK1_PRIORITY, (TaskHandle_t *)&task1_handle);
    xTaskCreate((TaskFunction_t)task2, (char *)"task2",
                (configSTACK_DEPTH_TYPE)TASK2_STACK, (void *)NULL,
                (UBaseType_t)TASK2_PRIORITY, (TaskHandle_t *)&task2_handle);

    vTaskDelete(NULL);
    taskEXIT_CRITICAL();
}

/**
 * @description: task1——进入临界区打印计数,模拟工作
 * 注意:临界区内不触发任务切换,所以每次打印会连续输出多次后切换到task2
 */
void task1(void *pvParameters)
{
    uint16_t task1_count = 0;
    while (1)
    {
        taskENTER_CRITICAL();                // 进入临界区:防止切换
        printf("task1正在运行[%d]次...\r\n", ++task1_count);
        // vTaskDelay(500);                  // 使用vTaskDelay会触发调度,观察不到时间片
        HAL_Delay(10);                       // 忙等待模拟工作负载,不切换任务
        taskEXIT_CRITICAL();                 // 退出临界区:允许切换
    }
}

/**
 * @description: task2——与task1同优先级,时间片轮转
 */
void task2(void *pvParameters)
{
    uint16_t task2_count = 0;
    while (1)
    {
        taskENTER_CRITICAL();
        printf("task2正在运行[%d]次...\r\n", ++task2_count);
        HAL_Delay(10);
        taskEXIT_CRITICAL();
    }
}

/* 观察现象:
   当configTICK_RATE_HZ=20时,时间片=50ms
   HAL_Delay(10)执行时间<50ms,
   所以每次进入临界区可以在一个时间片内打印多次
   退出临界区后下次调度切换到另一个任务 */

P08:延时函数对比

File: 08_延时函数/Core/Src/freertos_demo.c

#include "freertos_demo.h"
#include "FreeRTOS.h"
#include "task.h"
#include "LED.h"

#define START_TASK_STACK 128
#define START_TASK_PRIORITY 1
TaskHandle_t start_task_handle;
void start_task(void *pvParameters);

#define TASK1_STACK 128
#define TASK1_PRIORITY 2
TaskHandle_t task1_handle;
void task1(void *pvParameters);

#define TASK2_STACK 128
#define TASK2_PRIORITY 3
TaskHandle_t task2_handle;
void task2(void *pvParameters);

void freertos_start(void)
{
    xTaskCreate((TaskFunction_t)start_task,
                (char *)"start_task",
                (configSTACK_DEPTH_TYPE)START_TASK_STACK,
                (void *)NULL,
                (UBaseType_t)START_TASK_PRIORITY,
                (TaskHandle_t *)&start_task_handle);

    vTaskStartScheduler();
}

void start_task(void *pvParameters)
{
    taskENTER_CRITICAL();

    xTaskCreate((TaskFunction_t)task1, (char *)"task1",
                (configSTACK_DEPTH_TYPE)TASK1_STACK, (void *)NULL,
                (UBaseType_t)TASK1_PRIORITY, (TaskHandle_t *)&task1_handle);
    xTaskCreate((TaskFunction_t)task2, (char *)"task2",
                (configSTACK_DEPTH_TYPE)TASK2_STACK, (void *)NULL,
                (UBaseType_t)TASK2_PRIORITY, (TaskHandle_t *)&task2_handle);

    vTaskDelete(NULL);
    taskEXIT_CRITICAL();
}

/**
 * @description: task1——使用vTaskDelay相对延时 + HAL_Delay模拟工作
 *               现象:LED翻转间隔会逐渐漂移(越来越慢)
 */
void task1(void *pvParameters)
{
    while (1)
    {
        LED_Toggle(LED1_Pin);              // 翻转LED1
        HAL_Delay(20);                      // 模拟工作任务耗时20ms
        vTaskDelay(500);                   // 相对延时500ms(不包括HAL_Delay耗时)
    }                                      // 实际周期 = 20ms + 500ms = 520ms
}

/**
 * @description: task2——使用vTaskDelayUntil绝对延时 + HAL_Delay模拟工作
 *               现象:LED翻转周期严格固定为500ms
 */
void task2(void *pvParameters)
{
    TickType_t pxPreviousWakeTime = xTaskGetTickCount();  // 获取当前Tick计数值作为基准
    while (1)
    {
        LED_Toggle(LED2_Pin);              // 翻转LED2
        HAL_Delay(20);                      // 模拟工作任务耗时20ms
        vTaskDelayUntil(&pxPreviousWakeTime, 500);  // 绝对延时:等待到 pxPrevWakeTime + 500
    }                                      // 实际周期始终为500ms,不受HAL_Delay影响
}

Key Config

P05 FreeRTOSConfig.h(降低 Tick 频率便于观察时间片):

#define configTICK_RATE_HZ         20    // Tick频率20Hz,时间片=50ms
#define configUSE_TIME_SLICING     1     // 启用时间片调度(默认即开启)
#define configUSE_PREEMPTION       1     // 启用抢占式调度(必须)

Core API Reference Table

Function Parameters Return Description
vTaskDelay xTicksToDelay void 相对延时,从调用时开始计时
vTaskDelayUntil pxPreviousWakeTime, xTimeIncrement BaseType_t 绝对延时,固定周期
xTaskGetTickCount TickType_t 获取当前 Tick 计数值
configTICK_RATE_HZ 宏定义 配置每秒 Tick 数
configUSE_TIME_SLICING 宏定义 启用/禁用的时间片调度
HAL_Delay Delay void HAL 库忙等待延时(阻塞 CPU)

Common Issues & Pitfalls

  1. vTaskDelay 的漂移累积:如果任务内部的执行时间不固定,vTaskDelay 会导致周期逐渐漂移。对周期有严格要求的场景必须使用 vTaskDelayUntil
  2. vTaskDelayUntil 的初次使用vTaskDelayUntil 要求 pxPreviousWakeTime 初始化为 xTaskGetTickCount(),且在每次调用后会被 API 自动更新。
  3. 时间片调度与临界区的相互作用:在临界区中不会发生任务切换,因此如果在临界区内耗时超过一个时间片,仍不会切换到同优先级任务。
  4. HAL_Delay 在 FreeRTOS 中的问题HAL_Delay 是忙等待不释放 CPU,在 RTOS 中应优先使用 vTaskDelay。如果定时器中断优先级配置不当,HAL_Delay 可能无法正常工作。
  5. configTICK_RATE_HZ 不能过高:Tick 频率过高(如 10000Hz)意味着每个 Tick 0.1ms,频繁的上下文切换会消耗大量 CPU 时间,降低有效运算能力。嵌入式系统常用 100~1000Hz。
  6. 时间片观测技巧:在临界区中打印计数可以观察到时间片行为——同一任务在一次时间片内可能执行多次循环。