目录
1、什么是任务调度器
2、FreeRTOS中的任务调度器
2.1 抢占式调度
2.2 时间片调度
2.3 协作式调度
3、任务调度案例分析
3.1 实验需求
3.2 CubeMX配置
3.3 代码实现
3.3.1 uart.c 重定向printf
3.3.2 打开freertos.c并添加代码
3.3.4 代码现象
1、什么是任务调度器
任务调度器是实时操作系统(RTOS)的一个关键组件,它负责决定在多个可运行任务中哪一个将获得CPU时间得以执行。它基于任务的优先级和状态来做出这些决定。
在一个RTOS中,可能会有多个任务同时运行,但是在任意时刻,CPU只能执行一个任务。任务调度器的主要目标是按照系统的需求合理分配CPU时间。
调度器就是使用相关的调度算法来决定当前需要执行的哪个任务。
基本功能
任务的选择 | 任务调度器会根据预设的算法从所有可运行的任务中选择一个要执行的任务。 |
任务的优先级 | 任务通常会被分配一个优先级,优先级越高的任务在抢占式调度中会优先执行。 |
任务状态管理 | 任务可能处于就绪状态(可立即运行)、阻塞状态(等待某些条件满足)或者挂起状态(暂停执行)。 |
上下文切换 | 任务调度器负责在不同任务之间进行上下文切换。这意味着将当前任务的状态保存起来,然后加载另一个任务的状态,以便它可以继续执行。 |
定时器管理 | 任务调度器通常会关注系统中的定时器,以便能够在特定事件发生时唤醒相应的任务。 |
中断处理 | 任务调度器需要与系统的中断处理程序协同工作,以确保在中断上下文中也能够正常进行任务调度。 |
2、FreeRTOS中的任务调度器
FreeRTOS 是一个实时操作系统,它所奉行的调度规则:
1. 抢占式调度器:抢占式调度器是一种优先级基础的调度器,高优先级抢占低优先级任务,系统永远执行最高优先级的任务。
2. 时间片调度器:时间片轮转调度器是一种抢占式调度器,它将CPU时间划分成小的时间片段,每个任务在一个时间片段内运行。
3. 协程式调度器:协作式调度器依赖于任务自行释放CPU,没有明确的任务优先级。任务必须自愿放弃CPU控制权,以便其他任务能够运行。(但官方已明确表示不更新,主要是用在小容量的芯片上,用得也不多)
FreeRTOS中开启任务调度的函数是vTaskStartScheduler() ,但在CubeMX中被封装为osKernelStart() 。
2.1 抢占式调度
优点 | 适用于实时要求严格的系统,可以确保高优先级任务及时响应。 |
缺点 | 上下文切换的开销较大,可能会影响系统的性能。 |
1. 任务优先级:每个任务都会被分配一个优先级。优先级较高的任务被认为更紧急,将会在可运行时抢占具有较低优先级的任务。
2. 抢占:当一个高优先级的任务准备好运行时,RTOS会暂停当前正在执行的低优先级任务,并将CPU的控制权转交给高优先级任务。
3. 上下文切换:在抢占发生时,RTOS会保存当前任务的上下文(包括寄存器状态等),然后加载高优先级任务的上下文,使其可以继续执行。这个过程称为上下文切换。
4. 实时性:抢占式调度器可以保证高优先级任务能够及时响应紧急事件。这对于实时系统和需要快速响应的应用程序非常重要。
5. 任务挂起:在抢占式调度中,可以随时挂起(暂停)一个任务,以便给更高优先级的任务让路。挂起(暂停)函数为vTaskSuspend(),恢复函数为vTaskResume()。
6. 任务阻塞:一个任务可以由于等待某些条件的发生而被阻塞。当这些条件满足时,RTOS会将其重新置为就绪状态。
7. 中断处理:抢占式调度需要能够在中断上下文中正确地处理任务的切换。
8. 实时性保证:抢占式调度对于需要在严格的实时约束下工作的系统非常重要,因为它可以确保高优先级任务在预期的时间内得到处理。
总结:
- 高优先级任务,优先执行;
- 高优先级任务不停止,低优先级任务无法执行;
- 被抢占的任务将会进入就绪态。
2.2 时间片调度
优点 | 适用于多个任务优先级相近的场景,可以避免某个任务长时间占用CPU。 |
缺点 | 会引入一定的上下文切换开销。 |
1. 时间片段:CPU的执行时间被划分成固定长度的时间片段。每个时间片段可以是几毫秒或者更短的时间,具体取决于系统的配置。
2. 任务优先级:每个任务都被分配一个优先级。在一个时间片段内,具有相同优先级的任务会依次轮流执行。
3. 抢占:当一个时间片段结束时,RTOS会暂停当前执行的任务,并将CPU的控制权转交给具有相同优先级的下一个任务。
4. 上下文切换:在每个时间片段结束时,RTOS会进行上下文切换,将当前任务的状态保存起来,并加载下一个任务的状态,以便其可以继续执行。
5. 公平分配CPU时间:时间片调度确保了每个任务在一定时间段内都有机会执行,从而实现了相对公平的CPU时间分配。
6. 避免任务长时间占用CPU:时间片调度可以防止某个任务长时间占用CPU,从而保证了其他任务也有机会执行。
7. 适用于优先级相近的任务:时间片调度特别适用于具有相近优先级的任务集,因为它可以确保它们在相对公平的条件下执行。
8. 降低实时性:相比于抢占式调度,时间片调度可能会引入一定的上下文切换开销,从而降低了系统的实时性。
总结:
- 同等优先级任务,轮流执行,时间片流转;
- 一个时间片大小,取决为滴答定时器中断周期;
- 注意没有用完的时间片不会再使用,下次任务Task3得到执行,还是按照一个时间片的时钟节拍运行。
2.3 协作式调度
1. 任务自主控制:在协作式调度中,任务负责自己的执行时间。任务在需要让出CPU时,会主动调用RTOS提供的让出控制权的函数。
2. 没有明确的任务优先级:协作式调度中通常没有明确的任务优先级概念。所有的任务被视为平等,没有任务可以强制其他任务停止执行。
3. 低上下文切换开销:相比抢占式调度,协作式调度通常具有更低的上下文切换开销,因为任务只有在愿意让出CPU时才会发生切换。
4. 任务合作:任务之间需要合作以确保系统的正常运行。如果一个任务长时间占用CPU,其他任务可能会受到影响。
5. 适用于资源受限的系统:协作式调度适用于资源有限的嵌入式系统,因为它减少了抢占式调度中的上下文切换开销。
6. 可能导致响应性降低:如果一个任务不合作,即使有更高优先级的任务需要执行,也可能导致响应性降低,因为任务不会主动让出CPU。
7. 任务挂起和任务阻塞:虽然没有抢占,但任务仍然可以被挂起(暂停)或者阻塞,等待某些条件的发生。
3、任务调度案例分析
3.1 实验需求
创建 4 个任务:taskLED1,taskLED2,taskKEY1,taskKEY2,任务要求如下:
taskLED1:间隔 500ms 闪烁 LED1;
taskLED2:间隔 1000ms 闪烁 LED2;
taskKEY1:如果 taskLED1 存在,则按下 KEY1 后删除 taskLED1 ,否则创建 taskLED1 ;
taskKEY2:如果 taskLED2 正常运行,则按下 KEY2 后挂起 taskLED2 ,否则恢复 taskLED2。
3.2 CubeMX配置
查看原理图配置按键跟LED灯引脚
这里已经将FreeRTOS移植到STM32F103C8T6,具体操作流程看前面的文章。
串口配置
创建任务
点击add即可添加任务并修改参数配置,注意:其中任务的优先级从上往下越来越高,设置成Normal即可,同时Entry Fuction为函数指针,固定格式是Start+函数名字(函数名字首个字母需要大写)
3.3 代码实现
3.3.1 uart.c 重定向printf
#include "stdio.h"
int fputc(int ch,FILE *f)
{
unsigned char temp[1] = {ch};
HAL_UART_Transmit(&huart1,temp,1,0xffff);
return ch;
}
需要打开魔术棒勾上红框内选项实现串口打印
3.3.2 打开freertos.c并添加代码
void StartTaskLED1(void const * argument)
{
for(;;)
{
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_8);
osDelay(500);
}
}
void StartTask02(void const * argument)
{
for(;;)
{
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_9);
osDelay(1000);
}
}
void StartTaskKey1(void const * argument)
{
for(;;)
{
if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == RESET)
{
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == RESET)
{
printf("KEY1被按下!!!\r\n");
if(task_LED1Handle == NULL) //在上面任务创建会返回一个句柄,判断句柄是否为NULL
{
printf("任务1不存在,准备创建任务1\r\n"); //为NULL说明任务1不存在
osThreadDef(task_LED1, StartTask_LED1, osPriorityNormal, 0, 128); //调用上面任务创建函数
task_LED1Handle = osThreadCreate(osThread(task_LED1), NULL);
if(task_LED1Handle != NULL) //此时任务1创建成功,句柄不为NULL
printf("任务1创建完成!\r\n");
}
else
{
printf("删除任务1!\r\n");
osThreadTerminate(task_LED1Handle); //调用删除任务函数,参数为任务返回的句柄
task_LED1Handle = NULL; //手动将句柄置为NULL,否则后面调用到此句柄会有意外
}
}
while(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == RESET); //若按键一直处于按下状态则处于死循环,即松手检测
}
osDelay(10);
}
void StartTaskKey2(void const * argument)
{
static int flag = 0;
for(;;)
{
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET)
{
osDelay(20); // 延时消抖
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET)
{
printf("Key2 被按下\r\n");
if(flag == 0)
{
osThreadSuspend(TaskLED2Handle);//调用任务挂起函数
printf("任务2被挂起暂停\r\n");
flag = 1;
}
else
{
osThreadResume(TaskLED2Handle);//调用任务恢复函数
printf("任务2重新恢复\r\n");
flag = 0;
}
}
while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET);
}
osDelay(10);
}
}
上述代码中:
osThreadTerminate()为删除函数,osThreadSuspend()为挂起函数,osThreadResume()为恢复函数。(这些函数与前面所学的不一样,因为他们都是CubeMX的内置函数)
下图为创建任务后各个任务的句柄和CubeMX创建任务函数。
3.3.4 代码现象
按下KEY1任务1删除(LED1保持不变),再次按下任务1创建(LED1闪烁);按下KEY2任务2挂起(LED2保持不变),再次按下任务2恢复(LED2闪烁)。