1. 时间片调度简介
同等优先级任务轮流地享有相同的 CPU 时间(可设置), 叫时间片。在FreeRTOS中,一个时间片就等于SysTick 中断周期。
- 像我们的源码,滴答定时器是 1ms 中断一次,那么一个时间片的时间就是 1ms。
- 可设置:设置的就是滴答定时器的中断周期(中断频率)。
运行条件:
- 创建三个任务:Task1、Task2、Task3
- Task1、Task2、Task3的优先级均为1;即3个任务同等优先级
运行过程如下:
- 首先Task1运行完一个时间片后,切换至Task2运行
- Task2运行完一个时间片后,切换至Task3运行
- Task3运行过程中(还不到一个时间片),Task3阻塞了(系统延时或等待信号量等),此时直接切换到下一个任务Task1
- Task1运行完一个时间片后,切换至Task2运行
Task3 只执行不到一个时间片,那下一次再回到 Task3 的时候,怎么样?它剩下的时间片不会要了,同样的也只给它一个时间片的时间去执行。
总结:
- 同等优先级任务,轮流执行;时间片流转
- 一个时间片大小,取决为滴答定时器中断频率
- 注意没有用完的时间片不会再使用,下次任务 Task3 得到执行还是按照一个时间片的时钟节拍运行
2. 时间片调度实验
实验目的:学会对FreeRTOS 时间片调度使用
实验设计:将设计三个任务:start_task、task1、task2,其中task1和task2优先级相同均为2。
为了使现象明显,将滴答定时器的中断频率设置为 50ms 中断一次,即一个时间片 50ms
三个任务的功能如下:
start_task:用来创建其他的2个任务
task1:通过串口打印task1的运行次数
task2:通过串口打印task2的运行次数
注意:使用时间片调度需把宏 configUSE_TIME_SLICING 和 configUSE_PREEMPTION 置1
2.1 宏设置
把宏 configUSE_TIME_SLICING(时间片调度) 和 configUSE_PREEMPTION(抢占式调度) 置1
2.2 滴答定时器中断频率设置
#define configTICK_RATE_HZ 20 /* 定义系统时钟节拍频率, 单位: Hz, 无默认需定义 */
1000Hz 对应 1ms,20Hz 对应 50 ms(倒数关系)。也就是说现在一个时间片的时间就是 50 ms。
2.3 任务函数实现
freertos_demo.c
#include "freertos_demo.h"
#include "./SYSTEM/usart/usart.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
#include "./BSP/KEY/key.h"
#include "./SYSTEM/delay/delay.h"
/*FreeRTOS*********************************************************************************************/
#include "FreeRTOS.h"
#include "task.h"
/******************************************************************************************************/
/*FreeRTOS配置*/
/* START_TASK 任务 配置
* 包括: 任务句柄 任务优先级 堆栈大小 创建任务
*/
#define START_TASK_PRIO 1
#define START_TASK_STACK_SIZE 128
TaskHandle_t start_task_handler;
void start_task( void * pvParameters );
/* TASK1 任务 配置
* 包括: 任务句柄 任务优先级 堆栈大小 创建任务
*/
#define TASK1_PRIO 2
#define TASK1_STACK_SIZE 128
TaskHandle_t task1_handler;
void task1( void * pvParameters );
/* TASK2 任务 配置
* 包括: 任务句柄 任务优先级 堆栈大小 创建任务
*/
#define TASK2_PRIO 2
#define TASK2_STACK_SIZE 128
TaskHandle_t task2_handler;
void task2( void * pvParameters );
/******************************************************************************************************/
/**
* @brief FreeRTOS例程入口函数
* @param 无
* @retval 无
*/
void freertos_demo(void)
{
xTaskCreate((TaskFunction_t ) start_task,
(char * ) "start_task",
(configSTACK_DEPTH_TYPE ) START_TASK_STACK_SIZE,
(void * ) NULL,
(UBaseType_t ) START_TASK_PRIO,
(TaskHandle_t * ) &start_task_handler );
vTaskStartScheduler();
}
void start_task( void * pvParameters )
{
taskENTER_CRITICAL(); /* 进入临界区 */
xTaskCreate((TaskFunction_t ) task1,
(char * ) "task1",
(configSTACK_DEPTH_TYPE ) TASK1_STACK_SIZE,
(void * ) NULL,
(UBaseType_t ) TASK1_PRIO,
(TaskHandle_t * ) &task1_handler );
xTaskCreate((TaskFunction_t ) task2,
(char * ) "task2",
(configSTACK_DEPTH_TYPE ) TASK2_STACK_SIZE,
(void * ) NULL,
(UBaseType_t ) TASK2_PRIO,
(TaskHandle_t * ) &task2_handler );
vTaskDelete(NULL);
taskEXIT_CRITICAL(); /* 退出临界区 */
}
/* 任务一,通过串口打印task1的运行次数 */
void task1( void * pvParameters )
{
uint32_t task1_num = 0;
while(1)
{
taskENTER_CRITICAL(); /* 进入临界区 */
printf("task1运行次数:%d\r\n",++task1_num);
taskEXIT_CRITICAL(); /* 退出临界区 */
delay_ms(10);
}
}
/* 任务二,通过串口打印task2的运行次数 */
void task2( void * pvParameters )
{
uint32_t task2_num = 0;
while(1)
{
taskENTER_CRITICAL(); /* 进入临界区 */
printf("task2运行次数:%d\r\n",++task2_num);
taskEXIT_CRITICAL(); /* 退出临界区 */
delay_ms(10);
}
}
- 调用系统延时函数是会引起任务调度的,因为会把当前正在运行的任务挂载到阻塞列表,那这样的话,我们就体现不到我们一个时间片 50ms 这样的一个作用了,我们现在知道它的运行次数,那任务一阻塞立马就跳到下一个任务,那这样的话现象就不明显了,所以呢我们这里要用死等(死延时)函数,它就不会实现任务调度这些了。
死等(死延时)函数是没有进入阻塞的,是一直在运行的,我们调用这个延时函数是不会进行任务调度的,所以任务一直在就绪列表里面,这时候它不会进入阻塞状态,理论上的话任务会一直在运行。
- delay_ms() + printf() > 10ms,所以一个时间片正常能运行 4~5 次,那这个就是正常范围了。
注意:在 printf 函数执行到一半时进行任务切换将会导致数据就乱了,所以避免这种情况建议大家最好是加上临界区保护,保证 printf 函数执行完再执行任务切换。