--- 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 则是"以后每周二下午三点我来,雷打不动"。 ## 概念介绍 ### Time slice scheduling **时间片调度**指多个相同优先级的任务轮流获得 CPU 使用权,每个任务执行一个时间片(一个 Tick)后被切换到下一个同优先级任务。 关键配置: - `configUSE_TIME_SLICING`:启用/禁用时间片调度(默认开启) - `configUSE_PREEMPTION`:启用抢占式调度(必须开启,否则时间片不生效) - `configTICK_RATE_HZ`:Tick 频率,决定时间片长度 时间片 = 1 个 Tick = 1 / configTICK_RATE_HZ 秒 ### 工作原理 **抢占式调度 + 时间片调度**: ``` 高优先级任务 → 总是先运行(抢占) 同优先级任务 → 轮流运行(时间片轮转) 低优先级任务 → 高/同优先级任务阻塞时才运行 ``` - `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 ``` ## 实验 ### 需求描述 **P05(时间片调度)**: - 2 个同优先级任务(优先级均为 2),各自进入临界区打印计数后退出 - 使用 `HAL_Delay(10)` 模拟工作量 - 观察两个任务交替执行(时间片轮转) **P08(延时对比)**: - task1:使用 `vTaskDelay(500)` + `HAL_Delay(20)` 模拟工作 → 演示漂移 - task2:使用 `vTaskDelayUntil(&pxPreviousWakeTime, 500)` + `HAL_Delay(20)` → 演示固定周期 ### 代码实现 #### P05:时间片调度 **代码来源:**03_代码/05_时间片调度/Core/Src/freertos_demo.c ```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:延时函数对比 **代码来源:**03_代码/08_时间延时函数/Core/Src/freertos_demo.c ```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影响 } ``` ### 关键配置 **P05 FreeRTOSConfig.h**(降低 Tick 频率便于观察时间片): ```c #define configTICK_RATE_HZ 20 // Tick频率20Hz,时间片=50ms #define configUSE_TIME_SLICING 1 // 启用时间片调度(默认即开启) #define configUSE_PREEMPTION 1 // 启用抢占式调度(必须) ``` ## 核心函数速查表 | 函数 | 参数 | 返回值 | 功能简述 | | ------------------------ | ---------------------------------- | ---------- | ---------------------------- | | `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) | ## 常见问题与避坑 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. **时间片观测技巧**:在临界区中打印计数可以观察到时间片行为——同一任务在一次时间片内可能执行多次循环。