FreeRTOS时间片调度及实践测试

发布于:2025-07-03 ⋅ 阅读:(13) ⋅ 点赞:(0)

前言

上一篇文章《硬核解析FreeRTOS任务切换:从寄存器底层到PendSV机制,一文吃透核心原理》介绍了不同优先级的任务之间如何切换,本质上是“抢占式调度”。那么问题来了,相同优先级且都处于就绪态的任务如何进行切换呢?

一、时间片调度的原理

FreeRTOS中,时间片调度是多任务调度机制中的一种重要方式,它允许具有相同优先级的任务轮流执行,确保每个任务都有机会使用 CPU 资源。

1. 时间片调度的基本概念

时间片,也称为时间配额,是指为每个任务分配的一段固定的 CPU 执行时间。在 FreeRTOS 中,当多个任务具有相同优先级时,系统会按照时间片轮转的方式,让这些任务依次运行一个时间片的时长,然后切换到下一个任务,从而实现多个同优先级任务的并发执行效果,从宏观上看,就好像这些任务在同时运行。

2. 时间片调度的实现原理

  • SysTick 定时器:FreeRTOS 使用 SysTick(系统滴答定时器)作为系统的时基。通常会配置 SysTick 定时器产生周期性的中断,比如每 1ms 产生一次中断。每次 SysTick 中断发生时,系统会执行中断服务函数,在这个函数中对系统时间进行更新,并检查是否需要进行任务调度。
  • 任务切换机制:当 SysTick 中断触发任务调度时,如果当前正在运行的任务的时间片已经用完(通过一个计数器记录任务已执行的时间片数量来判断),并且存在其他同优先级的就绪任务,调度器就会触发上下文切换,保存当前任务的现场(将 CPU 寄存器的值保存到任务的堆栈中),并恢复下一个同优先级就绪任务的现场(从该任务的堆栈中恢复 CPU 寄存器的值到 CPU 中),让下一个任务开始执行。
  • 就绪任务列表:FreeRTOS 维护了多个就绪任务列表,按照任务优先级进行组织。对于每个优先级,都有一个对应的列表,同优先级的任务都被添加到该优先级对应的列表中。当进行时间片调度时,调度器会从当前优先级的就绪任务列表中选择下一个要执行的任务,通常是按照先进先出(FIFO)的顺序选取。

3. 四个问题

1. 时间片的长度谁决定?

宏定义 configTICK_RATE_HZ
在 FreeRTOS 的配置文件 FreeRTOSConfig.h 中,configTICK_RATE_HZ 用于设置 SysTick 定时器的中断频率,即系统时基的频率。例如,若将其设置为 1000,就表示每 1ms 产生一次 SysTick 中断。这个值的大小会影响时间片的精度和系统的整体性能。
例如:

#define configTICK_RATE_HZ				( ( TickType_t ) 1000 )
2. FreeRTOS 维护了多少个就绪任务列表?

我们先看代码中是怎么说的。下面定义了一个数组,就是就绪状态的列表数组。所以数组中元素的个数,就是就绪列表的个数。

PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ];/*< Prioritised ready tasks. */

查看这个宏定义,发现我当前代码中,这个宏等于5。

#define configMAX_PRIORITIES			( 5 )
3. 这意味着什么?

紧接着上面的问题,宏定义configMAX_PRIORITIES等于5,意味着我的任务优先级必须是0-4!在任务创建的时候,我们需要指定任务的优先级,这个时候就需要关注一下:我的任务最多可以设置多大的优先级。如果设置的优先级超过了这个范围,FreeRTOS会自动设置为最大优先级。具体操作如下:

	if( uxPriority >= ( UBaseType_t ) configMAX_PRIORITIES )
	{
		uxPriority = ( UBaseType_t ) configMAX_PRIORITIES - ( UBaseType_t ) 1U;
	}

代码里的宏上面已经说了,就不赘述了。

4. 使能时间片调度的宏有哪些?
  1. 宏定义configUSE_TIME_SLICING必须使能!该配置项用于开启或关闭时间片调度功能。

    • 将其设置为1表示启用时间片调度,当有多个同优先级任务就绪时,会按照时间片轮转的方式进行调度。
    • 设置为0则表示禁止时间片调度,同优先级的任务会按照其他方式(如先到先服务,直到任务阻塞或主动让出 CPU )执行。
  2. 另外,宏configUSE_PREEMPTION也必须使能!否则无法完成时间片调度。这部分内容在二、实践测试章节会细讲。

4. 时间片调度的优缺点

  • 优点
    • 公平性:确保了同优先级的任务都能公平地获得 CPU 执行时间,不会出现某个任务长时间占用 CPU 而导致其他同优先级任务得不到执行的情况。
    • 提高响应性:对于一些对实时性要求不是特别高,但又需要并发执行的任务,时间片调度可以在一定程度上提高系统对这些任务的响应性,使它们看起来像是在同时运行。
  • 缺点
    • 上下文切换开销:每次时间片切换都需要进行上下文切换操作,保存和恢复任务的现场,这会带来一定的 CPU 开销。如果时间片设置得过短,频繁的上下文切换可能会降低系统的整体性能。
    • 无法满足严格实时性需求:对于一些对时间要求极其严格的实时任务,时间片调度可能无法保证它们在特定的时间内得到执行,因为任务的执行顺序和时间取决于时间片的轮转,存在一定的不确定性。

5. 时间片调度的应用场景

  • 用户接口任务:在一些嵌入式设备中,可能存在多个用户接口相关的任务,如按键处理任务、显示更新任务等,这些任务优先级相同且对实时性要求不是非常苛刻,适合采用时间片调度,使它们能够轮流执行,保证用户操作的流畅响应。
  • 数据采集与处理任务:当系统中有多个数据采集传感器,且对应的数据处理任务优先级相同时,可以使用时间片调度,让这些任务依次获取 CPU 资源进行数据处理,实现并发采集和处理的效果。

二、实践测试

下面我们实操测试一下,深入理解一下时间片调度的逻辑。

1. 使能相关的宏定义

#define configUSE_TIME_SLICING       1 //FreeRTOS.h
#define configUSE_PREEMPTION			1 //FreeRTSOConfig.h
#define configTICK_RATE_HZ				( ( TickType_t ) 20 )

注意:这里强行将滴答定时器的中断溢出频率改为20Hz,也就是说,时间片的长度是50ms!等测试完成要改回到1000。

#define configTICK_RATE_HZ				( ( TickType_t ) 20 )

对于宏configTICK_RATE_HZ已经在上面讲过了,下面讲一下宏configUSE_PREEMPTION的含义。

1. configUSE_PREEMPTION

在 FreeRTOS 中,configUSE_PREEMPTION 是一个核心的配置宏(定义在 FreeRTOSConfig.h 文件中),用于控制任务调度的基本行为,决定系统是采用 抢占式调度(Preemptive) 还是 协作式调度(Cooperative)。以下是它的详细作用:

(1) 功能说明
  1. 启用抢占式调度(configUSE_PREEMPTION = 1

    • 行为
      • 高优先级任务可以立即抢占正在运行的低优先级任务(例如通过中断、信号量唤醒时)。
      • 调度器会在每个时钟节拍(tick)中断时检查是否有更高优先级的任务就绪,若有则触发任务切换。
    • 特点
      • 实时性强,适合需要快速响应的场景(如硬件控制、实时数据处理)。
      • 默认情况下,FreeRTOS 使用抢占式调度。
  2. 禁用抢占式调度(configUSE_PREEMPTION = 0,协作式)

    • 行为
      • 任务必须主动释放 CPU(通过调用 taskYIELD()vTaskDelay() 或阻塞式 API 如队列/信号量等待),其他任务才能运行。
      • 即使高优先级任务就绪,也不会抢占当前任务。
    • 特点
      • 任务切换由开发者显式控制,确定性高,但实时性较差。
      • 适合资源受限或对任务执行顺序有严格要求的场景。
(2) 与configUSE_TIME_SLICING的关联

configUSE_PREEMPTION=1 时,同优先级任务会按时间片轮转调度(需 configUSE_TIME_SLICING=1)。

注意若禁用抢占,时间片轮转无效

2. configUSE_TIME_SLICING

在 FreeRTOS 中,configUSE_TIME_SLICING 是一个配置宏(定义在 FreeRTOSConfig.h 文件中),用于控制 同优先级任务的时间片轮转调度(Round-Robin Scheduling) 行为。时间片轮转 仅在同优先级任务间生效,高优先级任务始终优先执行。

(1) 功能说明
  • 当多个任务处于 相同优先级 时,configUSE_TIME_SLICING 决定是否允许它们 共享 CPU 时间,按照 固定时间片(Time Slice) 轮流执行。
  • 默认情况下,FreeRTOS 启用时间片轮转(configUSE_TIME_SLICING=1),但仅在 抢占式调度(configUSE_PREEMPTION=1 时生效。
(2) 与taskYIELD()的关系

即使禁用时间片轮转,仍可通过taskYIELD() 手动触发任务切换。

configUSE_TIME_SLICING 效果
=1(默认) 同优先级任务按时间片轮转(每个任务运行一个 tick 周期后切换)。
=0 同优先级任务 不会自动切换,必须 主动让出 CPU(如调用 taskYIELD() 或阻塞)。

2. 配置任务和初始化函数

注意:每个任务的优先级设置成一样的!否则不能测试时间片调度!


#define START_TASK_PRIO     1//任务优先级
#define START_STK_SIZE      128//任务堆栈大小
TaskHandle_t StartTask_Handler;//任务句柄
void start_task(void * pvParameters);//任务函数

#define TASK1_PRIO          3//任务优先级
#define TASK1_STK_SIZE      50//任务堆栈大小
TaskHandle_t Task1_Handler;//任务句柄
void task1(void * p_arg);//任务函数

#define TASK2_PRIO           3//任务优先级
#define TASK2_STK_SIZE       50//任务堆栈大小
TaskHandle_t Task2_Handler;//任务句柄
void task2(void * p_arg);//任务函数

#define TASK3_PRIO              3//任务优先级
#define TASK3_STK_SIZE          50//任务堆栈大小
TaskHandle_t                    Task3_Handler;//任务句柄
void task3(void * p_arg);       //任务函数


int main(void)
{	
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
    delay_init(168);
    uart_init(115200);
    LED_Init();
    KEY_Init();							//初始化按键
     
    //创建开始任务
    xTaskCreate((TaskFunction_t) start_task,
                (const char*) "start_task",
                (uint16_t) START_STK_SIZE,
                (void*) NULL,
                (UBaseType_t)START_TASK_PRIO,
                (TaskHandle_t*) &StartTask_Handler);
    vTaskStartScheduler();

    return 1;
}

//开始任务函数
void start_task(void * pxParameters)
{
	taskENTER_CRITICAL();//进入临界区

    //创建task1任务
	xTaskCreate((TaskFunction_t) task1,
	            (const char*) "task1",
				(uint16_t) TASK1_STK_SIZE,
				(void*) NULL,
				(UBaseType_t)TASK1_PRIO,
				(TaskHandle_t*) &Task1_Handler);
    
    //创建task2任务
	xTaskCreate((TaskFunction_t) task2,
	            (const char*) "task2",
				(uint16_t) TASK2_STK_SIZE,
				(void*) NULL,
				(UBaseType_t)TASK2_PRIO,
				(TaskHandle_t*) &Task2_Handler);

    //创建task3任务
	xTaskCreate((TaskFunction_t) task3,
	            (const char*) "task3",
				(uint16_t) TASK3_STK_SIZE,
				(void*) NULL,
				(UBaseType_t)TASK3_PRIO,
				(TaskHandle_t*) &Task3_Handler);


	vTaskDelete(StartTask_Handler);//删除开始任务
	taskEXIT_CRITICAL();//推出临界区
}

/*********************************************************************
 * task1 时间片是50ms,所以每10ms执行一次,理论上输出5次,但由于printf消耗时间,所以可能是4-5次
 **********************************************************************/
void task1(void * pxParameters)
{
    uint8_t number_task1 = 0;

    while(1)
    {
        taskENTER_CRITICAL();//进入临界区
        printf("Task1 running times: %d\r\n", ++number_task1);
        taskEXIT_CRITICAL();//推出临界区
        delay_ms(10);
    }
}

/*********************************************************************
 * task2 时间片是50ms,所以每10ms执行一次,理论上输出5次,但由于printf消耗时间,所以可能是4-5次
 **********************************************************************/
void task2(void * pxParameters)
{
    uint8_t number_task2 = 0;

    while(1)
    {
        taskENTER_CRITICAL();//进入临界区
        printf("Task2 running times: %d\r\n", ++number_task2);
        taskEXIT_CRITICAL();//推出临界区
        delay_ms(10);
    }
}

/*********************************************************************
 * task3 时间片是50ms,所以每10ms执行一次,理论上输出5次,但由于printf消耗时间,所以可能是4-5次
 **********************************************************************/
void task3(void * pxParameters)
{
    uint8_t number_task3 = 0;

    while(1)
    {
        taskENTER_CRITICAL();//进入临界区
        printf("Task3 running times: %d\r\n", ++number_task3);
        taskEXIT_CRITICAL();//推出临界区
        delay_ms(10);
    }
}

在上面的代码中,我的每一个任务优先级都是3,符合前面说的0-4的范围。另外每个任务做的事也也一样,就是打印出来一个字符串,并且使用非阻塞延时函数延时了10ms。
按理说,在一个时间片,应该执行5次才对,但printf函数本身也要花时间,所以实际上执行的次数是4次。
为了防止打印期间被调度,造成打印出来的数据混乱,我加了临界区保护。如果有亲们对临界区有疑问,可以看这篇文章《FreeRTOS中断屏蔽终极指南:从BASEPRI到临界段,破解99%开发者踩过的寄存器陷阱!》。

3. 实验现象

在这里插入图片描述
实验现象与我们预期一致。

参考文献

《正点原子 FreeRTOS开发手册》


本文结束,如果本文帮助到了您,欢迎点赞、收藏、转发。点关注,不迷路,关注后第一时间可以看到我更新的文章哦!


网站公告

今日签到

点亮在社区的每一天
去签到