2022-04-26
依据:[野火]《FreeRTOS内核实现与应用开发实战指南》
目录
1、taskSELECT_HIGHEST_PRIORITY_TASK()函数
istGET_OWNER_OF_NEXT_ENTRY()函数
2、taskRESET_READY_PRIORITY()函数
假设同一优先级内有三个任务:A、B、C
执行时:A\B\C\A\B\C\A\B\C\A\B\C\A\B\C\A\B\C\,
并不是A执行完再执行B,而是类似于A走一步、B走一步、C走一步、A走一步~~~~~循环!!!
FreeRTOS支持时间片的功能。所谓时间片就 是同一个优先级下可以有多个任务,每个任务轮流地享有相同的 CPU 时间,享有 CPU 的时间我们叫时间片。
在 RTOS 中,最小的时间单位为一个 tick,即 SysTick 的中断周期, RT-Thread 和 μC/OS 可以指定时间片的大小为多个 tick,但是 FreeRTOS 不一样,时间片只 能是一个 tick。与其说 FreeRTOS 支持时间片,倒不如说它的时间片就是正常的任务调度。
假设目前系统中有三个任务就绪(算上空闲任务就是 4 个),任务 1 和任务 2 的优先 级为 2,任务 3 的优先级为 3。一定是有空闲任务的!!!!空闲任务优先级为0!
就绪列表:
在此参考正点原子的函数
一、main函数
为了方便在逻辑分析仪中地分辨出任务 1 和任务 2 使用的时间片大小,任务 1 和任务 2 的主体编写成一个无限循环函数,不会阻塞,任务 3 的阻塞时间设置为 1 个 tick。任务 1 和任务 2 的任务主体编写为一个无限循环,这就意味着,优先级低于 2 的任务就会被饿死, 得不到执行,比如空闲任务。在真正的项目中,并不会这样写,这里只是为了实验方便。
//时间片实验
/*
*************************************************************************
* 包含的头文件
*************************************************************************
*/
#include "FreeRTOS.h"
#include "task.h"
/*
*************************************************************************
* 全局变量
*************************************************************************
*/
portCHAR flag1;
portCHAR flag2;
portCHAR flag3;
extern List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
/*
*************************************************************************
* 任务控制块 & STACK
*************************************************************************
*/
TaskHandle_t Task1_Handle;
#define TASK1_STACK_SIZE 128
StackType_t Task1Stack[TASK1_STACK_SIZE];
TCB_t Task1TCB;
TaskHandle_t Task2_Handle;
#define TASK2_STACK_SIZE 128
StackType_t Task2Stack[TASK2_STACK_SIZE];
TCB_t Task2TCB;
TaskHandle_t Task3_Handle;
#define TASK3_STACK_SIZE 128
StackType_t Task3Stack[TASK3_STACK_SIZE];
TCB_t Task3TCB;
/*
*************************************************************************
* 函数声明
*************************************************************************
*/
void delay (uint32_t count);
void Task1_Entry( void *p_arg );
void Task2_Entry( void *p_arg );
void Task3_Entry( void *p_arg );
/*
************************************************************************
* main 函数
************************************************************************
*/
int main(void)
{
/* 硬件初始化 */
/* 将硬件相关的初始化放在这里,如果是软件仿真则没有相关初始化代码 */
/* 创建任务 */
Task1_Handle =
xTaskCreateStatic( (TaskFunction_t)Task1_Entry,
(char *)"Task1",
(uint32_t)TASK1_STACK_SIZE ,
(void *) NULL,
(UBaseType_t) 2,
(StackType_t *)Task1Stack,
(TCB_t *)&Task1TCB );
Task2_Handle =
xTaskCreateStatic( (TaskFunction_t)Task2_Entry,
(char *)"Task2",
(uint32_t)TASK2_STACK_SIZE ,
(void *) NULL,
(UBaseType_t) 2,
(StackType_t *)Task2Stack,
(TCB_t *)&Task2TCB );
Task3_Handle =
xTaskCreateStatic( (TaskFunction_t)Task3_Entry,
(char *)"Task3",
(uint32_t)TASK3_STACK_SIZE ,
(void *) NULL,
(UBaseType_t) 3,
(StackType_t *)Task3Stack,
(TCB_t *)&Task3TCB );
portDISABLE_INTERRUPTS();
/* 启动调度器,开始多任务调度,启动成功则不返回 */
vTaskStartScheduler(); //(1)
for (;;)
{
/* 系统启动成功不会到达这里 */
}
}
/*
************************************************************************
* 函数实现
************************************************************************
*/
/* 软件延时 */
void delay (uint32_t count)
{
for (; count!=0; count--);
}
/* 任务 1 */ //(2)
void Task1_Entry( void *p_arg )
{
for ( ;; )
{
flag1 = 1;
//vTaskDelay( 1 );
delay (100);
flag1 = 0;
delay (100);
//vTaskDelay( 1 );
}
}
/* 任务 2 */ //(3)
void Task2_Entry( void *p_arg )
{
for ( ;; )
{
flag2 = 1;
//vTaskDelay( 1 );
delay (100);
flag2 = 0;
delay (100);
//vTaskDelay( 1 );
}
}
void Task3_Entry( void *p_arg ) //(4)
{
for ( ;; )
{
flag3 = 1;
vTaskDelay( 1 );
//delay (100);
flag3 = 0;
vTaskDelay( 1 );
//delay (100);
}
}
/* 获取空闲任务的内存 */
StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE];
TCB_t IdleTaskTCB;
void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer,
StackType_t **ppxIdleTaskStackBuffer,
uint32_t *pulIdleTaskStackSize )
{
*ppxIdleTaskTCBBuffer=&IdleTaskTCB;
*ppxIdleTaskStackBuffer=IdleTaskStack;
*pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;
}
(2)(3)为了方便观察任务 1 和任务 2 使用的时间片大小,特意将任 务的主体编写成一个无限循环。实际项目中不会这样使用,否则低于任务 1 和任务 2 优先 级的任务就会被饿死,一直没有机会被执行。
(4)因为任务 1 和任务 2 的主体是无限循环的,要想任务 3 有机会执行, 其优先级就必须高于任务 1 和任务 2 的优先级。为了方便观察任务 1 和任务 2 使用的时间 片大小,任务 3 的阻塞延时我们设置为 1 个 tick。
二、实验现象
进入软件调试,全速运行程序,从逻辑分析仪中可以看到任务 1 和任务 2 轮流执行, 每一次运行的时间等于任务 3 中 flag3 输出高电平或者低电平的时间,即一个 tick,具体仿真的波形图见图
在这一个 tick(时间片)里面,任务 1 和任务 2 的 flag 标志位做了很多次的翻转,点 击逻辑分析仪中 Zoom In 按钮将波形放大后就可以看到 flag 翻转的细节。
三、原理分析
之所以在同一个优先级下可以有多个任务 ,最终还是得益于 taskRESET_READY_PRIORITY()和 taskSELECT_HIGHEST_PRIORITY_TASK()这两个函数的实现方法。接下来我们分析下这两个函数是如何在同一个优先级下有多个任务的时 候起作用的。
系统在任务切换的时候总会从就绪列表中寻找优先级最高的任务来执行,寻找优先级最高的任务这个功能由 taskSELECT_HIGHEST_PRIORITY_TASK()函数来实现,该函数在 task.c 中定义
1、taskSELECT_HIGHEST_PRIORITY_TASK()函数
/*-----------------------------------------------------------*/
#define taskSELECT_HIGHEST_PRIORITY_TASK()
{
UBaseType_t uxTopPriority;
/* Find the highest priority list that contains ready tasks. */
/* 寻找就绪任务的最高优先级 */
portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority ); //(1)
/* 获取优先级最高的就绪任务的 TCB,然后更新到 pxCurrentTCB */
listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); //(2)
} /* taskSELECT_HIGHEST_PRIORITY_TASK() */
/*-----------------------------------------------------------*/
(1)寻找就绪任务的最高优先级 。 即根据优先级位图表 uxTopReadyPriority 找到就绪任务的最高优先级,然后将优先级暂存在 uxTopPriority。
(2)获取优先级最高的就绪任务的 TCB,然后更新到 pxCurrentTCB。 目前我们的实验是在优先级 2 上有任务 1和任务 2,假设任务 1 运行了一个 tick,那接下来再从对应优先级 2 的就绪列表上选择任务来运行就应该是选择任务 2。 listGET_OWNER_OF_NEXT_ENTRY(),该函数在 list.h 中定义
istGET_OWNER_OF_NEXT_ENTRY()函数
//list.h
/*
* Access function to obtain the owner of the next entry in a list.
*
* The list member pxIndex is used to walk through a list. Calling
* listGET_OWNER_OF_NEXT_ENTRY increments pxIndex to the next item in the list
* and returns that entry's pxOwner parameter. Using multiple calls to this
* function it is therefore possible to move through every item contained in
* a list.
*
* The pxOwner parameter of a list item is a pointer to the object that owns
* the list item. In the scheduler this is normally a task control block.
* The pxOwner parameter effectively creates a two way link between the list
* item and its owner.
*
* @param pxTCB pxTCB is set to the address of the owner of the next list item.
* @param pxList The list from which the next item owner is to be returned.
*
* \page listGET_OWNER_OF_NEXT_ENTRY listGET_OWNER_OF_NEXT_ENTRY
* \ingroup LinkedList
*/
#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList )
{
List_t * const pxConstList = ( pxList );
/* Increment the index to the next item and return the item, ensuring */
/* we don't return the marker used at the end of the list. */
/* 节点索引指向链表第一个节点调整节点索引指针,指向下一个节点,
* 如果当前链表有 N 个节点,当第 N 次调用该函数时,pxIndex 则指向第 N 个节点 */
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;
/* 当遍历完链表后,pxIndex 回指到根节点 */
if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) ) \
{
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;
}
/* 获取节点的 OWNER,即 TCB */
( pxTCB ) = ( pxConstList )->pxIndex->pvOwner;
}
listGET_OWNER_OF_NEXT_ENTRY()函数的妙处在于它并不是获取链表下的第一个节点的 OWNER,而是用于获取下一个节点的 OWNER。有下一个那么就会有上一个的说 法,怎么理解?假设当前链表有 N 个节点,当第 N 次调用该函数时,pxIndex 则指向第 N 个节点,即每调用一次,节点遍历指针 pxIndex 则会向后移动一次,用于指向下一个节点。
本实验中,优先级 2 下有两个任务,当系统第一次切换到优先级为 2 的任务(包含了 任务 1 和任务 2,因为它们的优先级相同)时,pxIndex 指向任务 1,任务 1 得到执行。当 任务 1 执行完毕,系统重新切换到优先级为 2的任务时,这个时候 pxIndex 指向任务 2,任 务 2 得到执行,任务 1和任务 2 轮流执行,享有相同的 CPU 时间,即所谓的时间片。
本实验中,任务 1 和任务 2 的主体都是无限循环,那如果任务 1 和任务 2 都会调用将 自己挂起的函数(实际运用中,任务体都不能是无限循环的,必须调用能将自己挂起的函数),比如 vTaskDelay()。调用能将任务挂起的函数中,都会先将任务从就绪列表删除, 然 后 将 任 务 在优 先级位图表 uxTopReadyPriority 中对应的位清零,这一功能由 taskRESET_READY_PRIORITY()函数来实现,该函数在 task.c 中定义
2、taskRESET_READY_PRIORITY()函数
//tasks.c
/* A port optimised version is provided, call it only if the TCB being reset
* is being referenced from a ready list. If it is referenced from a delayed
* or suspended list then it won't be in a ready list. */
#define taskRESET_READY_PRIORITY( uxPriority )
{
if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ ( uxPriority ) ] ) ) == ( UBaseType_t ) 0 )
{
portRESET_READY_PRIORITY( ( uxPriority ), ( uxTopReadyPriority ) );
}
}
taskRESET_READY_PRIORITY() 清除优先级位图表 uxTopReadyPriority 中相应的位时候,会先判断当前优先级链表下是否还有其它任务,如果有则不清零。假设当前实验中,任务 1 会调用 vTaskDelay(),会将自己挂起,只能是将任 务 1 从就绪列表删除,不能将任务 1 在优先级位图表 uxTopReadyPriority 中对应的位清 0, 因为该优先级下还有任务 2,否则任务 2 将得不到执行。
四、修改代码,支持优先级问题
代码已经支持了时间片,实现的算法与 FreeRTOS 官方是一样的,即 taskSELECT_HIGHEST_PRIORITY_TASK()和 taskRESET_READY_PRIORITY()这两个函数的实现。但是在代码的编排组织上与 FreeRTOS 官方的还是有点不一样,为了与 FreeRTOS 官方代码统一起来,我们还是稍作修改。
1、xPortSysTickHandler()函数
xPortSysTickHandler() 函数具体见代码,即当 xTaskIncrementTick()函数返回为真时才进行任务切换,原来的 xTaskIncrementTick()是不带返回值的,执行到最后会调用 taskYIELD()执行任务切换。
/*-----------------------------------------------------------*/
void xPortSysTickHandler( void )
{
/* The SysTick runs at the lowest interrupt priority, so when this interrupt
* executes all interrupts must be unmasked. There is therefore no need to
* save and then restore the interrupt mask value as its value is already
* known - therefore the slightly faster vPortRaiseBASEPRI() function is used
* in place of portSET_INTERRUPT_MASK_FROM_ISR(). */
/* 关中断 */
vPortRaiseBASEPRI();
{
//xTaskIncrementTick();
/* 更新系统时基 */
/* Increment the RTOS tick. */
if( xTaskIncrementTick() != pdFALSE )
{
/* A context switch is required. Context switching is performed in
* the PendSV interrupt. Pend the PendSV interrupt. */
/* 任务切换,即触发 PendSV */
//portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}
/* 开中断 */
vPortClearBASEPRIFromISR();
}
/*-----------------------------------------------------------*/
2、修改 xTaskIncrementTick()函数
/*----------------------------------------------------------*/
BaseType_t xTaskIncrementTick( void ) //(1)
{
TCB_t * pxTCB;
TickType_t xItemValue;
BaseType_t xSwitchRequired = pdFALSE; //(2)
const TickType_t xConstTickCount = xTickCount + ( TickType_t ) 1;
/* Increment the RTOS tick, switching the delayed and overflowed
* delayed lists if it wraps to 0. */
xTickCount = xConstTickCount;
if( xConstTickCount == ( TickType_t ) 0U ) /*lint !e774 'if' does not always evaluate to false as it is looking for an overflow. */
{
taskSWITCH_DELAYED_LISTS();
}
if( xConstTickCount >= xNextTaskUnblockTime )
{
for( ; ; )
{
if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
{
/* The delayed list is empty. Set xNextTaskUnblockTime
* to the maximum possible value so it is extremely
* unlikely that the
* if( xTickCount >= xNextTaskUnblockTime ) test will pass
* next time through. */
xNextTaskUnblockTime = portMAX_DELAY; /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
break;
}
else
{
/* The delayed list is not empty, get the value of the
* item at the head of the delayed list. This is the time
* at which the task at the head of the delayed list must
* be removed from the Blocked state. */
pxTCB = listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList ); /*lint !e9079 void * is used as this macro is used with timers and co-routines too. Alignment is known to be fine as the type of the pointer stored and retrieved is the same. */
xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );
if( xConstTickCount < xItemValue )
{
/* It is not time to unblock this item yet, but the
* item value is the time at which the task at the head
* of the blocked list must be removed from the Blocked
* state - so record the item value in
* xNextTaskUnblockTime. */
xNextTaskUnblockTime = xItemValue;
break; /*lint !e9011 Code structure here is deedmed easier to understand with multiple breaks. */
}
/* It is time to remove the item from the Blocked state. */
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
prvAddTaskToReadyList( pxTCB );
/* A task being unblocked cannot cause an immediate
* context switch if preemption is turned off. */
#if ( configUSE_PREEMPTION == 1 ) //(2)
{
/* Preemption is on, but a context switch should
* only be performed if the unblocked task has a
* priority that is equal to or higher than the
* currently executing task. */
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
xSwitchRequired = pdTRUE;
}
}
#endif /* configUSE_PREEMPTION */
}
}
}
/* Tasks of equal priority to the currently running task will share
* processing time (time slice) if preemption is on, and the application
* writer has not explicitly turned time slicing off. */
#if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) //(4)
{
if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
{
xSwitchRequired = pdTRUE;
}
}
#endif /* ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) */
/* 任务切换 */
//portYIELD(); //(5)
return xSwitchRequired;
}
/*-----------------------------------------------------------*/
(1)将 xTaskIncrementTick()函数修改成带返回值的函数。
(2)定 义 一 个 局 部 变 量 xSwitchRequired , 用 于 存 储 xTaskIncrementTick()函数的返回值,当返回值是 pdTRUE 时,需要执行一次任务切换,默认初始化为 pdFALSE。
(3)configUSE_PREEMPTION 是在 FreeRTOSConfig.h 的一个宏,默认为 1,表示有任务就绪且就绪任务的优先级比当前优先级高时,需要执行一次任务切换, 即将 xSwitchRequired 的值置为 pdTRUE。在 xTaskIncrementTick()函数还没有修改成带返回 值的时候,我们是在执行完 xTaskIncrementTick()函数的时候,不管是否有任务就绪,不管 就绪的任务的优先级是否比当前任务优先级高都执行一次任务切换。如果就绪任务的优先 级比当前优先级高,那么执行一次任务切换与加了代码清单(3)这段代码实现的功能 是一样的。如果没有任务就绪呢?就不需要执行任务切换,这样与之前的实现方法相比就 省了一次任务切换的时间。虽然说没有更高优先级的任务就绪,执行任务切换的时候还是 会运行原来的任务,但这是以多花一次任务切换的时间为代价的。
(4)这部分与时间片功能相关。当 configUSE_PREEMPTION 与 configUSE_TIME_SLICING 都为真,且当前优先级下不止一个任务时就执行一次任务切换, 即将 xSwitchRequired 置为 pdTRUE 即可。在 xTaskIncrementTick()函数还没有修改成带返回值之前 ,这部分代码不需要也 是可以实现时间片功能的,即只要在执行完 xTaskIncrementTick() 函数后执行一次任务切换即可。 configUSE_PREEMPTION 在 FreeRTOSConfig.h 中默认定义为 1,configUSE_TIME_SLICING 如果没有定义,则会默认 在 FreeRTOS.h 中定义为 1。
(5)不在这里进行任务切换,而是放到了 xPortSysTickHandler()函数中。当 xTaskIncrementTick()函数的返回值为真时才进行任务切换。
其实 FreeRTOS 的这种时间片功能不能说是真正意义的时间片,因为它不能随意的设 置时间为多少个 tick,而是默认一个 tick,然后默认在每个 tick 中断周期中进行任务切换而已。
从构建结构体→构建链表→任务切换→实现临界段保护→实现延时→优先级→放入延时列表实现延时→切片执行任务。
基本的内容已经实现,下一部分,准备按照文件分析每个文件内的代码。翻译注释
写注释、写文档~~ 哈哈哈哈哈哈 可惜英文不太好,看不懂英文注释!!