一、FreeRTOS时间片调度概述
FreeRTOS支持多个任务同时拥有一个优先级,这些任务的调度就可以使用时间片来进行调度。在FreeRTOS中允许一个任务允许一个时间片(一个时钟节拍的长度)后让出CPU的使用权,让拥有同优先级的下个任务运行。下图展示了运行在同一优先级的执行时间图。其中的task1、task2、task3是同一优先级N就绪的任务。
- 1、任务3正在运行。
- 2、这时一个时钟节拍中断(滴答定时器中断)发生,任务3的时间片用完,但是任务3还没有执行完。
- 3、FreeRTOS将任务切换到任务1,任务1是优先级N下的下一个就绪任务。
- 4、任务1连续运行至时间片用完。
- 5、任务3再次获取到CPU使用权,接着运行。
- 6、任务3运行完成,调用任务切换函数portYIELD()强行进行任务切换放弃剩余的时间片,从而使优先级N下的下一个就绪的任务运行。
- 7、FreeRTOS切换到任务1。
- 8、任务1执行完其时间片。
二、开启时间片调度
要使用时间片调度的话宏configUSE_PREEMPTION和宏configUSE_TIME_SLICING必须为1。时间片的长度由宏configTICK_RATE_HZ来确定,一个时间片的长度就是滴答定时器的
中断周期,比如configTICK_RATE_HZ为1000,那么一个时间片的长度就是1ms。时间片调度发生在滴答定时器的中断服务函数中,前面讲解滴答定时器中断服务函数的时候说了在中断服务函数SysTick_Handler()中会调用FreeRTOS的API函数xPortSysTickHandler(),而函数xPortSysTickHandler()会引发任务调度,但是这个任务调度是有条件的,函数xPortSysTickHandler()如下:
void xPortSysTickHandler( void )
{vPortRaiseBASEPRI();{/* Increment the RTOS tick. */if( xTaskIncrementTick() != pdFALSE ){portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;}}vPortClearBASEPRIFromISR();
}
上述代码中只有函数xTaskIncrementTick()的返回值不为pdFALSE的时候就会进行任务调度!其中xTaskIncrementTick()函数代码如下:
BaseType_t xTaskIncrementTick( void )
{
TCB_t * pxTCB;
TickType_t xItemValue;
BaseType_t xSwitchRequired = pdFALSE;traceTASK_INCREMENT_TICK( xTickCount );if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE ){#if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) (1){if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 ) (2){xSwitchRequired = pdTRUE; (3)}else{mtCOVERAGE_TEST_MARKER();}}#endif /* ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) */#if ( configUSE_TICK_HOOK == 1 ){if( uxPendedTicks == ( UBaseType_t ) 0U ){vApplicationTickHook();}else{mtCOVERAGE_TEST_MARKER();}}#endif /* configUSE_TICK_HOOK */}else{++uxPendedTicks;/* The tick hook gets called at regular intervals, even if thescheduler is locked. */#if ( configUSE_TICK_HOOK == 1 ){vApplicationTickHook();}#endif}#if ( configUSE_PREEMPTION == 1 ){if( xYieldPending != pdFALSE ){xSwitchRequired = pdTRUE;}else{mtCOVERAGE_TEST_MARKER();}}#endif /* configUSE_PREEMPTION */return xSwitchRequired;
}
- (1)、当宏configUSE_PREEMPTION和宏configUSE_PREEMPTION都为1的时候下面的代码才会编译。所以要想使用时间片调度的话这这两个宏都必须为1,缺一不可!
- (2)、判断当前任务所对应的优先级下是否还有其他的任务。
- (3)、如果当前任务所对应的任务优先级下还有其他的任务那么就返回pdTRUE。从上面的代码可以看出,如果当前任务所对应的优先级下有其他的任务存在,那么函数xTaskIncrementTick0就会返回pdTURE,由于函数返回值为pdTURE,因此函数xPortSysTickHandler()就会进行一次任务切换。
三、实验程序展示
int main(void)
{ NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组4delay_init(168); //初始化延时函数uart_init(115200); //初始化串口LED_Init(); //初始化LED端口LCD_Init();POINT_COLOR = RED;LCD_ShowString(30,10,200,16,16,"LIU YI NIU BI");LCD_ShowString(30,40,200,16,16,"2021/10/18");//创建开始任务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(); //开启任务调度
}
//开始任务任务函数
void start_task(void *pvParameters)
{taskENTER_CRITICAL(); //进入临界区//创建task1任务xTaskCreate((TaskFunction_t )task1_task, (const char* )"task1_task", (uint16_t )TASK1_STK_SIZE, (void* )NULL, (UBaseType_t )TASK1_TASK_PRIO, (TaskHandle_t* )&TASK1Task_Handler); //创建LED1任务xTaskCreate((TaskFunction_t )task2_task, (const char* )"task2_task", (uint16_t )TASK2_STK_SIZE, (void* )NULL,(UBaseType_t )TASK2_TASK_PRIO,(TaskHandle_t* )&TASK2Task_Handler); vTaskDelete(StartTask_Handler); //删除开始任务taskEXIT_CRITICAL(); //退出临界区
}
//TASK1任务函数
void task1_task(void *pvParameters){u8 task1_num=0;while(1){task1_num++;LED0=!LED0;taskENTER_CRITICAL();//进入临界区printf("任务1已经执行:%d次\r\n",task1_num);taskEXIT_CRITICAL();//退出临界区
// delay_xms(10);}
}
//TASK2任务函数
void task2_task(void *pvParameters){u8 task2_num=0;while(1){task2_num++;LED1=!LED1;taskENTER_CRITICAL();//进入临界区printf("任务2已经执行:%d次\r\n",task2_num);taskEXIT_CRITICAL();//退出临界区
// delay_xms(10);}
}
通过串口调试助手可以看到打印的信息,就知道执行的过程。
不管是task1_task还是task2_task都是连续执行4、5次,和前面程序设计的一样,说明在一个时间片内一直在运行一个任务,当时间片用完后就切换到下一个任务运行。