Task

多任务系统

1.FreeRTOS 是一个抢占式的实时多任务系统, 那么其任务调度器也是抢占式的,运行过程如下图所示:


多任务抢占系统

任务(Task)的特性

1、简单。
2、没有使用限制。
3、支持抢占
4、支持优先级
5、每个任务都拥有堆栈导致了 RAM 使用量增大。
6、如果使用抢占的话的必须仔细的考虑重入的问题。

任务状态

● 运行态
当一个任务正在运行时, 那么就说这个任务处于运行态, 处于运行态的任务就是当前正在
使用处理器的任务。 如果使用的是单核处理器的话那么不管在任何时刻永远都只有一个任务处
于运行态。
● 就绪态
处于就绪态的任务是那些已经准备就绪(这些任务没有被阻塞或者挂起), 可以运行的任务,
但是处于就绪态的任务还没有运行,因为有一个同优先级或者更高优先级的任务正在运行!
● 阻塞态
如果一个任务当前正在等待某个外部事件的话就说它处于阻塞态, 比如说如果某个任务调
用了函数 vTaskDelay()的话就会进入阻塞态, 直到延时周期完成。任务在等待队列、信号量、事
件组、通知或互斥信号量的时候也会进入阻塞态。任务进入阻塞态会有一个超时时间,当超过
这个超时时间任务就会退出阻塞态,即使所等待的事件还没有来临!
● 挂起态
像阻塞态一样,任务进入挂起态以后也不能被调度器调用进入运行态, 但是进入挂起态的
任务没有超时时间。任务进入和退出挂起态通过调用函数 vTaskSuspend()和 xTaskResume()。
任务状态之间的转换如下图所示:


任务状态之间的转换

任务优先级

每 个 任 务 都 可 以 分 配 一 个 从 0~(configMAX_PRIORITIES-1) 的 优 先 级configMAX_PRIORITIES 在文件 FreeRTOSConfig.h 中有定义,优先级数字越低表示任务的优先级越低, 0 的优先级最低,configMAX_PRIORITIES-1 的优先级最高。空闲任务的优先级最低,为 0。

任务实现

FreeRTOS 官方给出的任务函数模板如下:

void vATaskFunction(void *pvParameters) (1)
{
for( ; ; ) (2)
{
--任务应用程序-- (3)
vTaskDelay(); (4)
}
/* 不 能 从 任 务 函 数 中 返 回 或 者 退 出 , 从 任 务 函 数 中 返 回 或 退 出 的 话 就 会 调 用
configASSERT(),前提是你定义了 configASSERT()。如果一定要从任务函数中退出的话那一定
要调用函数 vTaskDelete(NULL)来删除此任务。 */
vTaskDelete(NULL); (5)
}

(1)、任务函数本质也是函数,所以肯定有任务名什么的,不过这里我们要注意:任务函数的返回类型一定要为 void 类型,也就是无返回值,而且任务的参数也是 void 指针类型的!任务函数名可以根据实际情况定义。
(2)、 任务的具体执行过程是一个大循环, for(; ; )就代表一个循环,作用和 while(1)一样,笔者习惯用 while(1)。
(3)、循环里面就是真正的任务代码了,此任务具体要干的活就在这里实现!
(4)、 FreeRTOS 的延时函数,此处不一定要用延时函数,其他只要能让 FreeRTOS 发生任务切换的 API 函数都可以,比如请求信号量、队列等,甚至直接调用任务调度器。 只不过最常用的就是 FreeRTOS 的延时函数。
(5)、 任务函数一般不允许跳出循环,如果一定要跳出循环的话在跳出循环以后一定要调用函数 vTaskDelete(NULL)删除此任务!FreeRTOS 的任务函数和 uC/OS 的任务函数模式基本相同的,不止 FreeRTOS,其他 RTOS的任务函数基本也是这种方式的。

任务控制块

此结构体在文件 tasks.c 中有定义,如下:

typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; //任务堆栈栈顶
#if ( portUSING_MPU_WRAPPERS == 1 )
xMPU_SETTINGSxMPUSettings; //MPU 相关设置
#endif
ListItem_t xStateListItem; //状态列表项
ListItem_t xEventListItem; //事件列表项
UBaseType_t uxPriority; //任务优先级
StackType_t *pxStack; //任务堆栈起始地址
char pcTaskName[ configMAX_TASK_NAME_LEN ];//任务名字
#if ( portSTACK_GROWTH > 0 )
StackType_t *pxEndOfStack; //任务堆栈栈底
#endif
#if ( portCRITICAL_NESTING_IN_TCB == 1 )
UBaseType_t uxCriticalNesting; //临界区嵌套深度
#endif
#if ( configUSE_TRACE_FACILITY == 1 ) //trace 或到 debug 的时候用到
UBaseType_t uxTCBNumber;
UBaseType_t uxTaskNumber;
#endif
#if ( configUSE_MUTEXES == 1 )
UBaseType_t uxBasePriority; //任务基础优先级,优先级反转的时候用到
UBaseType_t uxMutexesHeld; //任务获取到的互斥信号量个数
#endif
#if ( configUSE_APPLICATION_TASK_TAG == 1 )
TaskHookFunction_t pxTaskTag;
#endif
#if( configNUM_THREAD_LOCAL_STORAGE_POINTERS > 0 ) //与本地存储有关
void
*pvThreadLocalStoragePointers[ configNUM_THREAD_LOCAL_STORAGE_POINTERS ];
#endif
#if( configGENERATE_RUN_TIME_STATS == 1 )
uint32_t ulRunTimeCounter; //用来记录任务运行总时间
#endif
#if ( configUSE_NEWLIB_REENTRANT == 1 )
struct _reent xNewLib_reent; //定义一个 newlib 结构体变量
#endif
#if( configUSE_TASK_NOTIFICATIONS == 1 )//任务通知相关变量
volatile uint32_t ulNotifiedValue; //任务通知值
volatile uint8_t ucNotifyState; //任务通知状态
#endif
#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
//用来标记任务是动态创建的还是静态创建的,如果是静态创建的此变量就为 pdTURE,
//如果是动态创建的就为 pdFALSE
uint8_t ucStaticallyAllocated;
#endif
#if( INCLUDE_xTaskAbortDelay == 1 )
uint8_t ucDelayAborted;
#endif
} tskTCB;
//新版本的 FreeRTOS 任务控制块重命名为 TCB_t,但是本质上还是 tskTCB,主要是为了兼容
//旧版本的应用。
typedef tskTCB TCB_t;

任务堆栈

FreeRTOS 任务相关 API 函数

任务创建和删除 API 函数

1、 函数 xTaxkCreate()

函数原型如下:

BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
const char * const pcName,
const uint16_t usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask )

参数:
pxTaskCode: 任务函数。
pcName: 任务名字,一般用于追踪和调试,任务名字长度不能超过。configMAX_TASK_NAME_LEN。
usStackDepth: 任务堆栈大小,注意实际申请到的堆栈是 usStackDepth 的 4 倍。其中空闲任务的任务堆栈大小为 configMINIMAL_STACK_SIZE。
pvParameters: 传递给任务函数的参数。
uxPriotiry: 任务优先级,范围 0~ configMAX_PRIORITIES-1。
pxCreatedTask: 任务句柄,任务创建成功以后会返回此任务的任务句柄, 这个句柄其实就是任务的任务堆栈。 此参数就用来保存这个任务句柄。其他 API 函数可能会使用到这个句柄。
返回值:
pdPASS: 任务创建成功。
errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY: 任务创建失败,因为堆内存不足!

2、 函数 xTaskCreateStatic()

函数原型如下:

TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode,
const char * const pcName,
const uint32_t ulStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
StackType_t * const puxStackBuffer,
StaticTask_t * const pxTaskBuffer )

参数:
pxTaskCode: 任务函数。
pcName: 任务名字,一般用于追踪和调试,任务名字长度不能超过。
configMAX_TASK_NAME_LEN。
usStackDepth: 任务堆栈大小,由于本函数是静态方法创建任务,所以任务堆栈由用户给出,一般是个数组,此参数就是这个数组的大小。
pvParameters: 传递给任务函数的参数。
uxPriotiry: 任务优先级,范围 0~ configMAX_PRIORITIES-1。
puxStackBuffer: 任务堆栈,一般为数组,数组类型要为 StackType_t 类型。
pxTaskBuffer: 任务控制块。
返回值:
NULL: 任务创建失败, puxStackBuffer 或 pxTaskBuffer 为 NULL 的时候会导致这个错误的发生。
其他值: 任务创建成功,返回任务的任务句柄。

3、 函数 xTaskCreateRestricted()

BaseType_t xTaskCreateRestricted( const TaskParameters_t * const pxTaskDefinition,
TaskHandle_t * pxCreatedTask )

参数:
pxTaskDefinition: 指向一个结构体 TaskParameters_t,这个结构体描述了任务的任务函数、堆栈大小、优先级等。此结构体在文件 task.h 中有定义。
pxCreatedTask: 任务句柄。
返回值:
pdPASS: 任务创建成功。
其他值: 任务未创建成功, 很有可能是因为 FreeRTOS 的堆太小了

4、 函数 vTaskDelete()

删除一个用函数 xTaskCreate()或者 xTaskCreateStatic()创建的任务,被删除了的任务不再存
在,也就是说再也不会进入运行态。 任务被删除以后就不能再使用此任务的句柄!如果此任务
是使用动态方法创建的,也就是使用函数 xTaskCreate()创建的,那么在此任务被删除以后此任
务之前申请的堆栈和控制块内存会在空闲任务中被释放掉,因此当调用函数 vTaskDelete()删除
任务以后必须给空闲任务一定的运行时间。
只有那些由内核分配给任务的内存才会在任务被删除以后自动的释放掉,用户分配给任务
的内存需要用户自行释放掉,比如某个任务中用户调用函数 pvPortMalloc()分配了 500 字节的内
存,那么在此任务被删除以后用户也必须调用函数 vPortFree()将这 500 字节的内存释放掉,否
则会导致内存泄露。此函数原型如下:

vTaskDelete( TaskHandle_t xTaskToDelete )

参数:
xTaskToDelete: 要删除的任务的任务句柄。
返回值:

任务挂起和恢复 API 函数

1、 函数 vTaskSuspend()

此函数用于将某个任务设置为挂起态, 进入挂起态的任务永远都不会进入运行态。退出挂
起态的唯一方法就是调用任务恢复函数 vTaskResume()或 xTaskResumeFromISR()。,函数原型如
下:

void vTaskSuspend( TaskHandle_t xTaskToSuspend)

参数:
xTaskToSuspend: 要挂起的任务的任务句柄,创建任务的时候会为每个任务分配一个任务句柄。如果使用函数 xTaskCreate()创建任务的话那么函数的参数
pxCreatedTask 就是此任务的任务句柄,如果使用函数 xTaskCreateStatic()创建任务的话那么函数的返回值就是此任务的任务句柄。也可以通过函数 xTaskGetHandle()来根据任务名字来获取某个任务的任务句柄。
注意! 如果参数为 NULL 的话表示挂起任务自己。
返回值:
无。

2、 函数 vTaskResume()

将一个任务从挂起态恢复到就绪态, 只有通过函数 vTaskSuspend()设置为挂起态的任务才可以使用 vTaskRexume()恢复!函数原型如下:

void vTaskResume( TaskHandle_t xTaskToResume)

参数:
xTaskToResume: 要恢复的任务的任务句柄。
返回值:
无。

3、 函数 xTaskResumeFromISR()

此函数是 vTaskResume()的中断版本,用于在中断服务函数中恢复一个任务。函数原型如下:

BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume)

参数:
xTaskToResume: 要恢复的任务的任务句柄。
返回值:
pdTRUE: 恢复运行的任务的任务优先级等于或者高于正在运行的任务(被中断打断的任务), 这意味着在退出中断服务函数以后必须进行一次上下文切换。pdFALSE: 恢复运行的任务的任务优先级低于当前正在运行的任务(被中断打断的任务),这意味着在退出中断服务函数的以后不需要进行上下文切换。

FreeRTOS 列表和列表项

什么是列表和列表项?

列表

在 list.h 中定义了一个叫 List_t 的
结构体,如下:

typedef struct xLIST
{
listFIRST_LIST_INTEGRITY_CHECK_VALUE (1)
configLIST_VOLATILE UBaseType_t uxNumberOfItems; (2)
ListItem_t * configLIST_VOLATILE pxIndex; (3)
MiniListItem_t xListEnd; (4)
listSECOND_LIST_INTEGRITY_CHECK_VALUE (5)
} List_t;

(1) 和 (5) 、 这 两 个 都 是 用 来 检 查 列 表 完 整 性 的 , 需 要 将 宏
configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES 设置为 1,开启以后会向这两个地方分别添加一个变量 xListIntegrityValue1 和 xListIntegrityValue2,在初始化列表的时候会这两个变量中写入一个特殊的值,默认不开启这个功能。 以后我们在学习列表的时候不讨论这个功能!
(2)、 uxNumberOfItems 用来记录列表中列表项的数量。
(3)、 pxIndex 用来记录当前列表项索引号,用于遍历列表。
(4)、 列表中最后一个列表项,用来表示列表结束,此变量类型为 MiniListItem_t,这是一个迷你列表项,关于列表项稍后讲解。

列表项

定义如下:

struct xLIST_ITEM
{
listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE (1)
configLIST_VOLATILE TickType_t xItemValue; (2)
struct xLIST_ITEM * configLIST_VOLATILE pxNext; (3)
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; (4)
void * pvOwner; (5)
void * configLIST_VOLATILE pvContainer; (6)
listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE (7)
};
typedef struct xLIST_ITEM ListItem_t;

(1)和(7)、用法和列表一样,用来检查列表项完整性的。 以后我们在学习列表项的时候不讨
论这个功能!
(2)、 xItemValue 为列表项值。
(3)、 pxNext 指向下一个列表项。
(4)、 pxPrevious 指向前一个列表项,和 pxNext 配合起来实现类似双向链表的功能。
(5)、 pvOwner 记录此链表项归谁拥有,通常是任务控制块。
(6)、 pvContainer 用来记录此列表项归哪个列表。注意和 pvOwner 的区别,在前面讲解任务控制块 TCB_t 的时候说了在 TCB_t 中有两个变量 xStateListItem 和 xEventListItem,这两个变量的类型就是 ListItem_t,也就是说这两个成员变量都是列表项。以 xStateListItem 为例,当创建一个任务以后 xStateListItem 的 pvOwner 变量就指向这个任务的任务控制块,表示 xSateListItem属于此任务。当任务就绪态以后 xStateListItem 的变量 pvContainer 就指向就绪列表,表明此列表项在就绪列表中。举个通俗一点的例子:小王在上二年级,他的父亲是老王。如果把小王比作列表项,那么小王的 pvOwner 属性值就是老王,小王的 pvContainer 属性值就是二年级。

迷你列表项

迷你列表项在文件 list.h 中有定义,
如下:

struct xMINI_LIST_ITEM
{
listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE (1)
configLIST_VOLATILE TickType_t xItemValue; (2)
struct xLIST_ITEM * configLIST_VOLATILE pxNext; (3)
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; (4)
};
typedef struct xMINI_LIST_ITEM MiniListItem_t;

(1)、用于检查迷你列表项的完整性。
(2)、 xItemValue 记录列表列表项值。
(3)、 pxNext 指向下一个列表项。
(4)、 pxPrevious 指向上一个列表项。
可以看出迷你列表项只是比列表项少了几个成员变量,迷你列表项有的成员变量列表项都有的,没感觉有什么本质区别啊?那为什么要弄个迷你列表项出来呢?那是因为有些情况下我们不需要列表项这么全的功能,可能只需要其中的某几个成员变量,如果此时用列表项的话会造成内存浪费!比如上面列表结构体 List_t 中表示最后一个列表项的成员变量 xListEnd 就是MiniListItem_t 类型的。

列表和列表项初始化

列表初始化

此函数在 list.c 中有
定义,函数如下:

void vListInitialise( List_t * const pxList )
{
pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd ); (1)
pxList->xListEnd.xItemValue = portMAX_DELAY; (2)
pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd ); (3)
pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd ); (4)
pxList->uxNumberOfItems = ( UBaseType_t ) 0U; (5)
listSET_LIST_INTEGRITY_CHECK_1_VALUE( pxList ); (6)
listSET_LIST_INTEGRITY_CHECK_2_VALUE( pxList ); (7)
}

(1)、 xListEnd 用来表示列表的末尾,而 pxIndex 表示列表项的索引号,此时列表只有一个列表项,那就是 xListEnd,所以 pxIndex 指向 xListEnd。
(2)、 xListEnd 的列表项值初始化为 portMAX_DELAY, portMAX_DELAY 是个宏,在文件portmacro.h 中有定义。根据所使用的 MCU 的不同, portMAX_DELAY 值也不相同,可以为 0xffff或者 0xffffffffUL,本教程中为 0xffffffffUL。
(3)、初始化列表项 xListEnd 的 pxNext 变量,因为此时列表只有一个列表项 xListEnd,因此 pxNext 只能指向自身
(4)、同(3)一样,初始化 xListEnd 的 pxPrevious 变量,指向 xListEnd 自身。
(5)、由于此时没有其他的列表项,因此 uxNumberOfItems 为 0,注意,这里没有算 xListEnd。
(6) 和 (7) 、 初 始 化 列 表 项 中 用 于 完 整 性 检 查 字 段 , 只 有 宏configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES 为 1 的时候才有效。

列表项初始化

函数如下:

void vListInitialiseItem( ListItem_t * const pxItem )
{
pxItem->pvContainer = NULL; //初始化 pvContainer 为 NULL
//初始化用于完整性检查的变量,如果开启了这个功能的话。
listSET_FIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem );
listSET_SECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem );
}

列表项的初始化很简单,只是将列表项成员变量 pvContainer 初始化为 NULL,并且给用于完整性检查的变量赋值。 有朋友可能会问,列表项的成员变量比列表要多,怎么初始化函数就这么短?其他的成员变量什么时候初始化呢?这是因为列表项要根据实际使用情况来初始化,比如任务创建函数 xTaskCreate()就会对任务堆栈中的 xStateListItem 和 xEventListItem 这两个列表项中的其他成员变量在做初始化,任务创建过程后面会详细讲解。

列表项插入

列表项的插入操作通过函数 vListInsert()来完成,函数原型如下:

void vListInsert( List_t * const pxList,
ListItem_t * const pxNewListItem )

参数:
pxList: 列表项要插入的列表。
pxNewListItem: 要插入的列表项。
返回值:

函数代码如下:

void vListInsert( List_t * const pxList, ListItem_t * const pxNewListItem )
{
ListItem_t *pxIterator;
const TickType_t xValueOfInsertion = pxNewListItem->xItemValue; (1)
listTEST_LIST_INTEGRITY( pxList ); (2)
listTEST_LIST_ITEM_INTEGRITY( pxNewListItem );
if( xValueOfInsertion == portMAX_DELAY ) (3)
{
pxIterator = pxList->xListEnd.pxPrevious; (4)
}
else
{
for( pxIterator = ( ListItem_t * ) &( pxList->xListEnd ); pxIterator->\ (5)
pxNext->xItemValue <=xValueOfInsertion; pxIterator = pxIterator->pxNext )
{
//空循环,什么也不做!
}
}
pxNewListItem->pxNext = pxIterator->pxNext; (6)
pxNewListItem->pxNext->pxPrevious = pxNewListItem;
pxNewListItem->pxPrevious = pxIterator;
pxIterator->pxNext = pxNewListItem;
pxNewListItem->pvContainer = ( void * ) pxList; (7)
( pxList->uxNumberOfItems )++; (8)
}

(1)、获取要插入的列表项值,即列表项成员变量 xItemValue 的值,因为要根据这个值来确定列表项要插入的位置。
(2)、 这一行和下一行代码用来检查列表和列表项的完整性的。其实就是检查列表和列表项中用于完整性检查的变量值是否被改变。这些变量的值在列表和列表项初始化的时候就被写入了,这两行代码需要实现函数 configASSERT()!
(3)、 要插入列表项,第一步就是要获取该列表项要插入到什么位置!如果要插入的列表项的值等于 portMAX_DELAY,也就是说列表项值为最大值, 这种情况最好办了,要插入的位置就是列表最末尾了。
(4)、 获取要插入点,注意!列表中的 xListEnd 用来表示列表末尾,在初始化列表的时候xListEnd的列表值也是portMAX_DELAY, 此时要插入的列表项的列表值也是portMAX_DELAY。这两个的顺序该怎么放啊?通过这行代码可以看出要插入的列表项会被放到 xListEnd 前面。
(5)、要插入的列表项的值如果不等于 portMAX_DELAY 那么就需要在列表中一个一个的找自己的位置,这个 for 循环就是找位置的过程,当找到合适列表项的位置的时候就会跳出。由于这个 for 循环是用来寻找列表项插入点的,所以 for 循环体里面没有任何东西。 这个查找过程是按照升序的方式查找列表项插入点的。
(6)、经过上面的查找,我们已经找到列表项的插入点了,从本行开始接下来的四行代码就是将列表项插入到列表中, 插入过程和数据结构中双向链表的插入类似。 像 FreeRTOS 这种RTOS 系统和一些协议栈都会大量用到数据结构的知识, 所以建议大家没事的时候多看看数据结构方面的书籍,否则的话看源码会很吃力的。
(7)、列表项已经插入到列表中了,那么列表项的成员变量 pvContainer 也该记录此列表项属于哪个列表的了。
(8)、 列表的成员变量 uxNumberOfItems 加一,表示又添加了一个列表项。

列表项插入过程图示1、插入值为 40 的列表项

在一个空的列表 List 中插入一个列表值为 40 的列表项 ListItem1,插入完成以后如图所示:


插入列表项 ListItem1

注意观察插入完成以后列表 List 和列表项 ListItem1 中各个成员变量之间的变化,比如列表 List 中的 uxNumberOfItems 变为了 1,表示现在列表中有一个列表项。列表项 ListItem1 中的pvContainer 变成了 List,表示此列表项属于列表 List。 通过图 7.3.2.1 可以看出,列表是一个环形的,即环形列表!
接着再插入一个值为 60 的列表项 ListItem2,插入完成以后如图所示:


插入列表项 ListItem2

上面再讲解函数 vListInsert()的时候说过了列表项是按照升序的方式插入的,所以 ListItem2肯定是插入到 ListItem1 的后面、 xListEnd 的前面。 同样的,列表 List 的 uxNumberOfItems 再次加一变为 2 了,说明此时列表中有两个列表项。
在上面的列表中再插入一个值为 50 的列表项 ListItem3, 插入完成以后如图所示
插入列表项 ListItem3

按照升序排列的方式, ListItem3 应该放到 ListItem1 和 ListItem2 中间,大家最好通过对照
这三幅图片来阅读函数 vListInsert()的源码,这样就会对函数有一个直观的认识。

列表项末尾插入

函数原型如下:

void vListInsertEnd( List_t * const pxList,
ListItem_t * const pxNewListItem )

参数:
pxList: 列表项要插入的列表。
pxNewListItem: 要插入的列表项。
返回值:

函数 vListInsertEnd()源码如下:

void vListInsertEnd( List_t * const pxList, ListItem_t * const pxNewListItem )
{
ListItem_t * const pxIndex = pxList->pxIndex;
listTEST_LIST_INTEGRITY( pxList ); (1)
listTEST_LIST_ITEM_INTEGRITY( pxNewListItem );
pxNewListItem->pxNext = pxIndex; (2)
pxNewListItem->pxPrevious = pxIndex->pxPrevious;
mtCOVERAGE_TEST_DELAY();
pxIndex->pxPrevious->pxNext = pxNewListItem;
pxIndex->pxPrevious = pxNewListItem;
pxNewListItem->pvContainer = ( void * ) pxList; (3)
( pxList->uxNumberOfItems )++; (4)
}

(1)、与下面的一行代码完成对列表和列表项的完整性检查。
(2)、从本行开始到(3)之间的代码就是将要插入的列表项插入到列表末尾。 使用函数vListInsert()向列表中插入一个列表项的时候这个列表项的位置是通过列表项的值,也就是列表项成员变量 xItemValue 来确定。 vListInsertEnd()是往列表的末尾添加列表项的,我们知道列表中的 xListEnd 成员变量表示列表末尾的,那么函数 vListInsertEnd()插入一个列表项是不是就是插到 xListEnd 的前面或后面啊?这个是不一定的, 这里所谓的末尾要根据列表的成员变量pxIndex 来确定的! 前面说了列表中的 pxIndex 成员变量是用来遍历列表的, pxIndex 所指向的列表项就是要遍历的开始列表项,也就是说 pxIndex 所指向的列表项就代表列表头! 由于是个环形列表,所以新的列表项就应该插入到 pxIndex 所指向的列表项的前面。
(3)、标记新的列表项 pxNewListItem 属于列表 pxList。
(4)、记录列表中的列表项数目的变量加一,更新列表项数目。

列表项末尾插入图示

在插入列表项之前我们先准备一个默认列表,如图所示:


默认列表

在上面的列表中插入一个值为 50 的列表项 ListItem3,插入完成以后如图所示:


插入列表项 ListItem3

列表 List 的 pxIndex 指向列表项 ListItem1,因此调用函数 vListInsertEnd()插入 ListItem3 的话就会在 ListItem1 的前面插入。

列表项的删除

有列表项的插入,那么必然有列表项的删除,列表项的删除通过函数 uxListRemove()来完
成,函数原型如下:

UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove )

参数:
pxItemToRemove: 要删除的列表项。
返回值: 返回删除列表项以后的列表剩余列表项数目。
注意,列表项的删除只是将指定的列表项从列表中删除掉,并不会将这个列表项的内存给
释放掉!如果这个列表项是动态分配内存的话。函数 uxListRemove()的源码如下:

UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove )
{
List_t * const pxList = ( List_t * ) pxItemToRemove->pvContainer; (1)
pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious; (2)
pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext;
mtCOVERAGE_TEST_DELAY();
if( pxList->pxIndex == pxItemToRemove )
{
pxList->pxIndex = pxItemToRemove->pxPrevious; (3)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
pxItemToRemove->pvContainer = NULL; (4)
( pxList->uxNumberOfItems )--;
return pxList->uxNumberOfItems; (5)
}

(1)、 要删除一个列表项我们得先知道这个列表项处于哪个列表中,直接读取列表项中的成
员变量 pvContainer 就可以得到此列表项处于哪个列表中。
(2)、与下面一行完成列表项的删除,其实就是将要删除的列表项的前后两个列表项“连接”
在一起。
(3)、 如果列表的 pxIndex 正好指向要删除的列表项,那么在删除列表项以后要重新给
pxIndex 找个“对象” 啊,这个新的对象就是被删除的列表项的前一个列表项。
(4)、被删除列表项的成员变量 pvContainer 清零。
(5)、 返回新列表的当前列表项数目。

列表的遍历

介绍列表结构体的时候说过列表 List_t 中的成员变量 pxIndex 是用来遍历列表的, FreeRTOS
提供了一个函数来完成列表的遍历,这个函数是 listGET_OWNER_OF_NEXT_ENTRY()。 每调
用一次这个函数列表的 pxIndex 变量就会指向下一个列表项,并且返回这个列表项的 pxOwner
变量值。 这个函数本质上是一个宏,这个宏在文件 list.h 中如下定义:

#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList ) \ (1)
{ \
List_t * const pxConstList = ( pxList ); \
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \ (2)
if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) )\ (3)
{ \
( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext; \ (4)
} \
( pxTCB ) = ( pxConstList )->pxIndex->pvOwner; \ (5)
}

(1)、 pxTCB 用来保存 pxIndex 所指向的列表项的 pvOwner 变量值,也就是这个列表项属于
谁的?通常是一个任务的任务控制块。 pxList 表示要遍历的列表。
(2)、 列表的 pxIndex 变量指向下一个列表项。
(3)、如果 pxIndex 指向了列表的 xListEnd 成员变量,表示到了列表末尾。
(4)、如果到了列表末尾的话就跳过 xListEnd, pxIndex 再一次重新指向处于列表头的列表
项,这样就完成了一次对列表的遍历。
(5)、将 pxIndex 所指向的新列表项的 pvOwner 赋值给 pxTCB。
此函数用于从多个同优先级的就绪任务中查找下一个要运行的任务。

FreeRTOS 调度器开启和任务相关函数详解

调度器开启过程分析

任务调度器开启函数分析

vTaskStartScheduler()这个函数的功能就是开启任务调度器的,这个函数在文件 tasks.c
中有定义,缩减后的函数代码如下:

void vTaskStartScheduler( void )
{
BaseType_t xReturn;
xReturn = xTaskCreate( prvIdleTask, (1)
"IDLE", configMINIMAL_STACK_SIZE,
( void * ) NULL,
( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
&xIdleTaskHandle );
#if ( configUSE_TIMERS == 1 ) //使用软件定时器使能
{
if( xReturn == pdPASS )
{
xReturn = xTimerCreateTimerTask(); (2)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_TIMERS */
if( xReturn == pdPASS ) //空闲任务和定时器任务创建成功。
{
portDISABLE_INTERRUPTS(); (3)
#if ( configUSE_NEWLIB_REENTRANT == 1 ) //使能 NEWLIB
{
_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
}
#endif /* configUSE_NEWLIB_REENTRANT */
xNextTaskUnblockTime = portMAX_DELAY;
xSchedulerRunning = pdTRUE; (4)
xTickCount = ( TickType_t ) 0U;
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS(); (5)
if( xPortStartScheduler() != pdFALSE ) (6)
{
//如果调度器启动成功的话就不会运行到这里,函数不会有返回值的
}
else
{
//不会运行到这里,除非调用函数 xTaskEndScheduler()。
}
}
else
{
//程序运行到这里只能说明一点,那就是系统内核没有启动成功,导致的原因是在创建
//空闲任务或者定时器任务的时候没有足够的内存。
configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
}
//防止编译器报错,比如宏 INCLUDE_xTaskGetIdleTaskHandle 定义为 0 的话编译器就会提
//示 xIdleTaskHandle 未使用。
( void ) xIdleTaskHandle;
}

(1)、创建空闲任务,如果使用静态内存的话使用函数 xTaskCreateStatic()来创建空闲任务,优先级为 tskIDLE_PRIORITY,宏 tskIDLE_PRIORITY 为 0,也就是说空闲任务的优先级为最低。
(2)、如果使用软件定时器的话还需要通过函数 xTimerCreateTimerTask()来创建定时器服务任务。定时器服务任务的具体创建过程是在函数 xTimerCreateTimerTask()中完成的,这个函数很简单,大家就 自行查阅一下。
(3)、关闭中断,在 SVC 中断服务函数 vPortSVCHandler()中会打开中断。
(4)、变量 xSchedulerRunning 设置为 pdTRUE,表示调度器开始运行。
(5)、当宏 configGENERATE_RUN_TIME_STATS 为 1 的时候说明使能时间统计功能,此时需要用户实现宏 portCONFIGURE_TIMER_FOR_RUN_TIME_STATS,此宏用来配置一个定时器/计数器。
(6)、调用函数 xPortStartScheduler()来初始化跟调度器启动有关的硬件,比如滴答定时器、FPU 单元和 PendSV 中断等等。

空闲任务

讲解函数 vTaskStartScheduler()说过,此函数会创建一个名为“IDLE”的任务,这个任务叫做空闲任务。顾名思义,空闲任务就是空闲的时候运行的任务,也就是系统中其他的任务由于各种原因不能运行的时候空闲任务就在运行。空闲任务是 FreeRTOS 系统自动创建的,不需要用户手动创建。任务调度器启动以后就必须有一个任务运行!但是空闲任务不仅仅是为了满足任务调度器启动以后至少有一个任务运行而创建的,空闲任务中还会去做一些其他的事情,如下:
1、判断系统中是否有任务删除自己,如果有的话就在空闲任务中释放被删除任务的任务堆栈和任务控制块的内存。
2、运行用户设置的空闲任务钩子函数。
3、判断是否开启低功耗 tickless 模式,如果开启的话还需要做相应的处理空闲任务的任务优先级是最低的,为 0,任务函数为 prvIdleTask(),有关空闲任务的详细内容我们后面会有专门的章节讲解,这里大家只要知道有这个任务就行了。

任务创建过程分析

任务创建函数分析

前面学了任务创建可以使用动态方法或静态方法(不讨论使用 MPU 的情况),它们分别使用函数 xTaskCreate()和 xTaskCreateStatic()。本节我们就以函数 xTaskCreate()为例来分析一下FreeRTOS 的任务创建过程,函数 xTaskCreateStatic()类似,这里不做分析。函数 xTaskCreate()代码如下, 注意这里为了缩小篇幅去掉了函数中的条件编译等不重要的语句!

BaseType_t xTaskCreate(TaskFunction_t pxTaskCode,
const char * const pcName,
const uint16_t usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask )
{
TCB_t *pxNewTCB;
BaseType_t xReturn;
/********************************************************************/
/***************使用条件编译的向上增长堆栈相关代码省略***************/
/********************************************************************/
StackType_t *pxStack;
pxStack = ( StackType_t * ) pvPortMalloc( ( ( ( size_t ) usStackDepth ) *\ (1)
sizeof( StackType_t ) ) );
if( pxStack != NULL )
{
pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) ); (2)
if( pxNewTCB != NULL )
{
pxNewTCB->pxStack = pxStack; (3)
}
else
{
vPortFree( pxStack ); (4)
}
}
else
{
pxNewTCB = NULL;
}
if( pxNewTCB != NULL )
{
#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
{
pxNewTCB->ucStaticallyAllocated =\ (5)
tskDYNAMICALLY_ALLOCATED_STACK_AND_TCB;
}
#endif /* configSUPPORT_STATIC_ALLOCATION */
prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, \ (6)
pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL );
prvAddNewTaskToReadyList( pxNewTCB ); (7)
xReturn = pdPASS;
}
else
{
xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;
}
return xReturn;
}

(1)、使用函数 pvPortMalloc()给任务的任务堆栈申请内存,申请内存的时候会做字节对齐处理。
(2)、如果堆栈的内存申请成功的话就接着给任务控制块申请内存,同样使用函数pvPortMalloc()。
(3)、任务控制块内存申请成功的话就初始化内存控制块中的任务堆栈字段 pxStack,使用(1)中申请到的任务堆栈。
(4)、如果任务控制块内存申请失败的话就释放前面已经申请成功的任务堆栈的内存。
(5)、标记任务堆栈和任务控制块是使用动态内存分配方法得到的。
(6)、使用函数 prvInitialiseNewTask()初始化任务,这个函数完成对任务控制块中各个字段的初始化工作!
(7)、使用函数 prvAddNewTaskToReadyList()将新创建的任务加入到就绪列表中。

任务初始化函数分析

函数prvInitialiseNewTask()用于完成对任务的初始化,缩减后的函数源码如下:

static void prvInitialiseNewTask( TaskFunction_t pxTaskCode,
const char * const pcName,
const uint32_t ulStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask,
TCB_t * pxNewTCB,
const MemoryRegion_t * const xRegions )
{
StackType_t *pxTopOfStack;
UBaseType_t x;
#if( ( configCHECK_FOR_STACK_OVERFLOW > 1 ) || ( configUSE_TRACE_FACILITY ==\
1 ) || ( INCLUDE_uxTaskGetStackHighWaterMark == 1 ) )
{
( void ) memset( pxNewTCB->pxStack, ( int ) tskSTACK_FILL_BYTE,\ (1)
( size_t ) ulStackDepth * sizeof( StackType_t ) );
}
#endif
pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 ); (2)
pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) &\
( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
for( x = ( UBaseType_t ) 0; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ )
{
pxNewTCB->pcTaskName[ x ] = pcName[ x ]; (3)
if( pcName[ x ] == 0x00 )
{
break;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0'; (4)
if( uxPriority >= ( UBaseType_t ) configMAX_PRIORITIES ) (5)
{
uxPriority = ( UBaseType_t ) configMAX_PRIORITIES - ( UBaseType_t ) 1U;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
pxNewTCB->uxPriority = uxPriority; (6)
#if ( configUSE_MUTEXES == 1 ) (7)
{
pxNewTCB->uxBasePriority = uxPriority;
pxNewTCB->uxMutexesHeld = 0;
}
#endif /* configUSE_MUTEXES */
vListInitialiseItem( &( pxNewTCB->xStateListItem ) ); (8)
vListInitialiseItem( &( pxNewTCB->xEventListItem ) ); (9)
listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB ); (10)
listSET_LIST_ITEM_VALUE( &( pxNewTCB->xEventListItem ), \ (11)
( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) uxPriority );
listSET_LIST_ITEM_OWNER( &( pxNewTCB->xEventListItem ), pxNewTCB ); (12)
#if ( portCRITICAL_NESTING_IN_TCB == 1 ) //使能临界区嵌套
{
pxNewTCB->uxCriticalNesting = ( UBaseType_t ) 0U;
}
#endif /* portCRITICAL_NESTING_IN_TCB */
#if ( configUSE_APPLICATION_TASK_TAG == 1 ) //使能任务标签功能
{
pxNewTCB->pxTaskTag = NULL;
}
#endif /* configUSE_APPLICATION_TASK_TAG */
#if ( configGENERATE_RUN_TIME_STATS == 1 ) //使能时间统计功能
{
pxNewTCB->ulRunTimeCounter = 0UL;
}
#endif /* configGENERATE_RUN_TIME_STATS */
#if( configNUM_THREAD_LOCAL_STORAGE_POINTERS != 0 )
{
for( x = 0; x < ( UBaseType_t ) configNUM_THREAD_LOCAL_STORAGE_POINTERS;\
x++ )
{
pxNewTCB->pvThreadLocalStoragePointers[ x ] = NULL; (12)
}
}
#endif
#if ( configUSE_TASK_NOTIFICATIONS == 1 ) //使能任务通知功能
{
pxNewTCB->ulNotifiedValue = 0;
pxNewTCB->ucNotifyState = taskNOT_WAITING_NOTIFICATION;
}
#endif
#if ( configUSE_NEWLIB_REENTRANT == 1 ) //使能 NEWLIB
{
_REENT_INIT_PTR( ( &( pxNewTCB->xNewLib_reent ) ) );
}
#endif
#if( INCLUDE_xTaskAbortDelay == 1 ) //使能函数 xTaskAbortDelay()
{
pxNewTCB->ucDelayAborted = pdFALSE;
}
#endif
pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode,\ (13)
pvParameters );
if( ( void * ) pxCreatedTask != NULL )
{
*pxCreatedTask = ( TaskHandle_t ) pxNewTCB; (14)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}

(1) 、 如 果 使 能 了 堆 栈 溢 出 检 测 功 能 或 者 追 踪 功 能 的 话 就 使 用 一 个 定 值tskSTACK_FILL_BYTE 来填充任务堆栈,这个值为 0xa5U。
(2)、计算堆栈栈顶 pxTopOfStack,后面初始化堆栈的时候需要用到。
(3)、保存任务的任务名。
(4)、 任务名数组添加字符串结束符’\0’。
(5)、判断任务优先级是否合法,如果设置的任务优先级大于 configMAX_PRIORITIES,则
将优先级修改为 configMAX_PRIORITIES-1。
(6)、初始化任务控制块的优先级字段 uxPriority。
(7)、使能了互斥信号量功能,需要初始化相应的字段。
(8)和(9)、初始化列表项 xStateListItem 和 xEventListItem,任务控制块结构体中有两个列表项,这里对这两个列表项做初始化。
(10)和(12)、设置列表项 xStateListItem 和 xEventListItem 属于当前任务的任务控制块,也就是设置这两个列表项的字段 pvOwner 为新创建的任务的任务控制块。
(11)、设置列表项xEventListItem的字段xItemValue为configMAX_PRIORITIES- uxPriority,比如当前任务优先级 3,最大优先级为 32,那么 xItemValue 就为 32-3=29,这就意味着 xItemValue值越大,优先级就越小。上一章学习列表和列表项的时候我们说过,列表的插入是按照xItemValue 的值升序排列的。
(12)、初始化线程本地存储指针,如果使能了这个功能的话。
(13)、调用函数 pxPortInitialiseStack()初始化任务堆栈。
(14)、生成任务句柄,返回给参数 pxCreatedTask,从这里可以看出任务句柄其实就是任务控制块。

任务堆栈初始化函数分析

--------------------------------后面添加------------------------

添加任务到就绪列表

任务创建完成以后就会被添加到就绪列表中, FreeRTOS 使用不同的列表表示任务的不同
状态,在文件tasks.c 中就定义了多个列表和指向列表的指针来完成不同的功能,这些列表如下:

PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
PRIVILEGED_DATA static List_t xDelayedTaskList1;
PRIVILEGED_DATA static List_t xDelayedTaskList2;
PRIVILEGED_DATA static List_t * volatile pxDelayedTaskList;
PRIVILEGED_DATA static List_t * volatile pxOverflowDelayedTaskList;
PRIVILEGED_DATA static List_t xPendingReadyList;

列表数组 pxReadyTasksLists[]就是任务就绪列表,数组大小为 configMAX_PRIORITIES,也就是说一个优先级一个列表,这样相同优先级的任务就使用一个列表。将一个新创建的任务添加到就绪列表中通过函数 prvAddNewTaskToReadyList()来完成,函数如下:

static void prvAddNewTaskToReadyList( TCB_t *pxNewTCB )
{
taskENTER_CRITICAL();
{
uxCurrentNumberOfTasks++; (1)
if( pxCurrentTCB == NULL )//正在运行任务块为 NULL,说明没有任务运行!
{
pxCurrentTCB = pxNewTCB;//将新任务的任务控制块赋值给 pxCurrentTCB
//新创建的任务是第一个任务!!!
if( uxCurrentNumberOfTasks == ( UBaseType_t ) 1 )
{
prvInitialiseTaskLists(); (2)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
if( xSchedulerRunning == pdFALSE )
{
//新任务的任务优先级比正在运行的任务优先级高。
if( pxCurrentTCB->uxPriority <= pxNewTCB->uxPriority )
{
pxCurrentTCB = pxNewTCB; (3)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
uxTaskNumber++; //uxTaskNumber 加一,用作任务控制块编号。
#if ( configUSE_TRACE_FACILITY == 1 )
{
pxNewTCB->uxTCBNumber = uxTaskNumber;
}
#endif /* configUSE_TRACE_FACILITY */
prvAddTaskToReadyList( pxNewTCB ); (4)
}
taskEXIT_CRITICAL();
if( xSchedulerRunning != pdFALSE )
{
//新任务优先级比正在运行的任务优先级高
if( pxCurrentTCB->uxPriority < pxNewTCB->uxPriority )
{
taskYIELD_IF_USING_PREEMPTION(); (5)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}

(1)、变量 uxCurrentNumberOfTasks 为全局变量,用来统计任务数量。
(2)、变量 uxCurrentNumberOfTasks 为 1 说明正在创建的任务是第一个任务!那么就需要先初始化相应的列表,通过调用函数 prvInitialiseTaskLists()来初始化相应的列表。这个函数很简单,本质就是调用上一章讲的列表初始化函数 vListInitialise()来初始化几个列表,大家可以自行分析一下。
(3)、新创建的任务优先级比正在运行的任务优先级高,所以需要修改 pxCurrentTCB 为新建任务的任务控制块。
(4)、调用函数 prvAddTaskToReadyList()将任务添加到就绪列表中,这个其实是个宏,如下:

#define prvAddTaskToReadyList( pxTCB ) \
traceMOVED_TASK_TO_READY_STATE( pxTCB ); \
taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority ); \
vListInsertEnd( &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ), \
&( ( pxTCB )->xStateListItem ) ); \
tracePOST_MOVED_TASK_TO_READY_STATE( pxTCB )

其中宏 portRECORD_READY_PRIORITY()用来记录处于就绪态的任务,具体是通过操作全局变量 uxTopReadyPriority 来实现的。这个变量用来查找处于就绪态的优先级最高任务,具体操作过程后面讲解任务切换的时候会讲。接下来使用函数 vListInsertEnd()将任务添加到就绪列表末尾。
(5)、如果新任务的任务优先级最高,而且调度器已经开始正常运行了,那么就调用函数taskYIELD_IF_USING_PREEMPTION()完成一次任务切换

任务删除过程分析

vTaskDelete()这个函数的具体实现过程,函数源码如下:

void vTaskDelete( TaskHandle_t xTaskToDelete )
{
TCB_t *pxTCB;
taskENTER_CRITICAL();
{
//如果参数为 NULL 的话那么说明调用函数 vTaskDelete()的任务要删除自身。
pxTCB = prvGetTCBFromHandle( xTaskToDelete ); (1)
//将任务从就绪列表中删除。
if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 ) (2)
{
taskRESET_READY_PRIORITY( pxTCB->uxPriority );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
//任务是否在等待某个事件?
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL ) (3)
{
( void ) uxListRemove( &( pxTCB->xEventListItem ) );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
uxTaskNumber++;
if( pxTCB == pxCurrentTCB ) (4)
{
vListInsertEnd( &xTasksWaitingTermination, &( pxTCB->\ (5)
xStateListItem ) );
++uxDeletedTasksWaitingCleanUp; (6)
portPRE_TASK_DELETE_HOOK( pxTCB, &xYieldPending ); (7)
}
else
{
--uxCurrentNumberOfTasks; (8)
prvDeleteTCB( pxTCB ); (9)
prvResetNextTaskUnblockTime(); (10)
}
traceTASK_DELETE( pxTCB );
}
taskEXIT_CRITICAL();
//如果删除的是正在运行的任务那么就需要强制进行一次任务切换。
if( xSchedulerRunning != pdFALSE )
{
if( pxTCB == pxCurrentTCB )
{
configASSERT( uxSchedulerSuspended == 0 );
portYIELD_WITHIN_API(); (11)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}

(1)、调用函数 prvGetTCBFromHandle()获取要删除任务的任务控制块,参数为任务句柄。如果参数为当前正在执行的任务句柄那么返回值就为 NULL。
(2)、将任务从任务就绪列表中删除。
(3)、查看任务是否正在等待某个事件(如信号量、队列等),因为如果任务等待某个事件的话这个任务会被放到相应的列表中,这里需要将其从相应的列表中删除掉。
(4)、要删除的是当前正在运行的任务.
(5)、要删除任务,那么任务的任务控制块和任务堆栈所占用的内存肯定要被释放掉(如果使用动态方法创建的任务),但是当前任务正在运行,显然任务控制块和任务堆栈的内存不能被立即释放掉!必须等到当前任务运行完成才能释放相应的内存,所以需要打一个“标记”,标记出有任务需要处理。这里将当前任务添加到列表 xTasksWaitingTermination 中,如果有任务要删除自身的话都会被添加到列表 xTasksWaitingTermination 中。那么问题来了?内存释放在哪里完成呢?空闲任务!空闲任务会依次将需要释放的内存都释放掉。
(6)、 uxDeletedTasksWaitingCleanUp 是一个全局变量,用来记录有多少个任务需要释放内存。
(7)、调用任务删除钩子函数,钩子函数的具体内容需要用户自行实现。
(8)、删除的是别的任务,变量 uxCurrentNumberOfTasks 减一,也就是当前任务数减一。
(9)、因为是删除别的任务,所以可以直接调用函数 prvDeleteTCB()删除任务控制块。
(10)、重新计算一下还要多长时间执行下一个任务,也就是下一个任务的解锁时间,防止有任务的解锁时间参考了刚刚被删除的那个任务。
(11)、如果删除的是正在运行的任务那么删除完以后肯定需要强制进行一次任务切换。

任务挂起过程分析

挂起任务使用函数 vTaskSuspend(),函数源码如下:

void vTaskSuspend( TaskHandle_t xTaskToSuspend )
{
TCB_t *pxTCB;
taskENTER_CRITICAL();
{
//如果参数为 NULL 的话说明挂起自身
pxTCB = prvGetTCBFromHandle( xTaskToSuspend ); (1)
traceTASK_SUSPEND( pxTCB );
//将任务从就绪或者延时列表中删除,并且将任务放到挂起列表中
if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 ) (2)
{
taskRESET_READY_PRIORITY( pxTCB->uxPriority );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
//任务是否还在等待其他事件
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL ) (3)
{
( void ) uxListRemove( &( pxTCB->xEventListItem ) );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
vListInsertEnd( &xSuspendedTaskList, &( pxTCB->xStateListItem ) ); (4)
}
taskEXIT_CRITICAL();
if( xSchedulerRunning != pdFALSE )
{
taskENTER_CRITICAL();
{
prvResetNextTaskUnblockTime(); (5)
}
taskEXIT_CRITICAL();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
if( pxTCB == pxCurrentTCB )
{
if( xSchedulerRunning != pdFALSE )
{
configASSERT( uxSchedulerSuspended == 0 );
portYIELD_WITHIN_API(); (6)
}
else
{
if( listCURRENT_LIST_LENGTH( &xSuspendedTaskList ) ==\ (7)
uxCurrentNumberOfTasks )
{
pxCurrentTCB = NULL; (8)
}
else
{
vTaskSwitchContext(); (9)
}
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}

(1)、通过函数 prvGetTCBFromHandle()获取要删除任务的任务控制块。
(2)、将任务从任务就绪列表延时列表中删除。
(3)、查看任务是否正在等待某个事件(如信号量、队列等),如果任务还在等待某个事件的话就将其从相应的事件列表中删除。
(4)、将任务添加到挂起任务列表尾,挂起任务列表为 xSuspendedTaskList,所有被挂起的任务都会被放到这个列表中。
(5)、 重新计算一下还要多长时间执行下一个任务,也就是下一个任务的解锁时间。防止有任务的解锁时间参考了刚刚被挂起的那个任务。
(6)、如果刚刚挂起的任务是正在运行的任务,并且任务调度器运行正常,那么这里就需要调用函数 portYIELD_WITHIN_API()强制进行一次任务切换。
(7)、 pxCurrentTCB 指向正在运行的任务,但是正在运行的任务要挂起了,所以必须给pxCurrentTCB 重新找一个“对象”。也就是查找下一个将要运行的任务,本来这个工作是由任务切换函数来完成的,但是程序运行到这一行说明任务调度器被挂起了,任务切换函数也无能为力了,必须手动查找下一个要运行的任务了。调用函数 listCURRENT_LIST_LENGTH()判断一下系统中所有的任务是不是都被挂起了,也就是查看列表 xSuspendedTaskList 的长度是不是等于 uxCurrentNumberOfTasks。如果等于的话就说明系统中所有的任务都被挂起了(实际上不存在这种情况,因为最少都有一个空闲任务是可以运行的,空闲任务执行期间不会调用任何可以阻塞或者挂起空闲任务的 API 函数,为的就是保证系统中永远都有一个可运行的任务)。
(8)、如果所有任务都被挂起的话 pxCurrentTCB 就只能等于 NULL 了,这样当有新任务被创建的时候 pxCurrentTCB 就可以指向这个新任务。
(9)、有其他的没有被挂起的任务,调用 vTaskSwitchContext()获取下一个要运行的任务, 函数 vTaskSwitchContext()会在下一章详细讲解。

任务恢复过程分析

任务恢复函数有两个 vTaskResume()和 xTaskResumeFromISR(),一个是用在任务中的,一
个是用在中断中的,但是基本的处理过程都是一样的,我们就以函数 vTaskResume()为例来讲解
一下任务恢复详细过程。

void vTaskResume( TaskHandle_t xTaskToResume )
{
TCB_t * const pxTCB = ( TCB_t * ) xTaskToResume; (1)
configASSERT( xTaskToResume );
//函数参数不可能为 NULL。
if( ( pxTCB != NULL ) && ( pxTCB != pxCurrentTCB ) ) (2)
{
taskENTER_CRITICAL(); (3)
{
if( prvTaskIsTaskSuspended( pxTCB ) != pdFALSE ) (4)
{
traceTASK_RESUME( pxTCB );
( void ) uxListRemove( &( pxTCB->xStateListItem ) ); (5)
prvAddTaskToReadyList( pxTCB ); (6)
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority ) (7)
{
taskYIELD_IF_USING_PREEMPTION(); (8)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
taskEXIT_CRITICAL(); (9)
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}

(1)、根据参数获取要恢复的任务的任务控制块,因为不存在恢复正在运行的任务这种情况所以参数也不可能为 NULL(你强行给个为 NULL 的参数那也没办法),这里也就不需要使用函数 prvGetTCBFromHandle()来获取要恢复的任务控制块, prvGetTCBFromHandle()会处理参数为NULL 这种情况。
(2)、任务控制块不能为 NULL 和 pxCurrentTCB,因为不存在说恢复当前正在运行的任务。
(3)、调用函数 taskENTER_CRITICAL()进入临界段
(4)、调用函数 prvTaskIsTaskSuspended()判断要恢复的任务之前是否已经被挂起了,恢复的肯定是被挂起的任务,没有挂起就不用恢复。
(5)、首先将要恢复的任务从原来的列表中删除,任务被挂起以后都会放到任务挂起列表xSuspendedTaskList 中。
(6)、将要恢复的任务添加到就绪任务列表中。
(7)、要恢复的任务优先级高于当前正在运行的任务优先级。
(8) 、 因 为 要 恢 复 的 任 务 其 优 先 级 最 高 , 所 以 需 要 调 用 函 数taskYIELD_IF_USING_PREEMPTION()来完成一次任务切换。
(9)、调用函数 taskEXIT_CRITICAL()退出临界区。

FreeRTOS 任务切换

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容