当前位置: 首页 > article >正文

FreeRTOS - 任务管理

在学习FreeRTOS过程中,结合韦东山-FreeRTOS手册和视频、野火-FreeRTOS内核实现与应用开发、及网上查找的其他资源,整理了该篇文章。如有内容理解不正确之处,欢迎大家指出,共同进步。

参考:https://rtos.100ask.net/zh/FreeRTOS/DShanMCU-F103/chapter9.html#_9-5-%E7%A4%BA%E4%BE%8B5-%E4%BB%BB%E5%8A%A1%E6%9A%82%E5%81%9C

https://freertos.blog.csdn.net/article/details/50312443

1. 任务

对于整个单片机程序,我们称之为 application,应用程序。

使用FreeRTOS时,我们可以在application中创建多个任务(task),有些文档把任务也称为线程(thread)。

1.1 任务特性
  • 使用RTOS的实时应用程序可认为是一系列独立任务的集合。
  • 每个任务在自己的环境中运行,不依赖于系统中的其它任务或者RTOS调度器。
  • 在任何时刻,只有一个任务得到运行,RTOS调度器决定运行哪个任务。
  • 调度器会不断的启动、停止每一个任务,宏观看上去就像整个应用程序都在执行。
  • 作为任务,不需要对调度器的活动有所了解,在任务切入切出时保存上下文环境(寄存器值、堆栈内容)是调度器主要的职责。
  • 为了实现这点,每个任务都需要有自己的堆栈。
  • 当任务切出时,它的执行环境会被保存在该任务的堆栈中,这样当再次运行时,就能从堆栈中正确的恢复上次的运行环境。
1.2 任务状态

  • 运行:如果一个任务正在执行,那么说这个任务处于运行状态。此时它占用处理器。
  • 就绪:就绪的任务已经具备执行的能力(不同于阻塞和挂起),但是因为有一个同优先级或者更高优先级的任务处于运行状态而还没有真正执行。
  • 阻塞:如果任务当前正在等待某个时序或外部中断,我们就说这个任务处于阻塞状态。比如一个任务调用vTaskDelay()后会阻塞到延时周期到为止。任务也可能阻塞在队列或信号量事件上。进入阻塞状态的任务通常有一个“超时”周期,当事件超时后解除阻塞。
  • 挂起:处于挂起状态的任务同样对调度器无效。仅当明确的分别调用vTaskSuspend() 和xTaskResume() API函数后,任务才会进入或退出挂起状态。不可以指定超时周期事件(不可以通过设定超时事件而退出挂起状态)
  1. 阻塞状态

在阻塞状态的任务,它可以等待两种类型的事件:

  • 时间相关的事件
    • 可以等待一段时间:我等2分钟
    • 也可以一直等待,直到某个绝对时间:我等到下午3点
  • 同步事件:这事件由别的任务,或者是中断程序产生
    • 例子1:任务A等待任务B给它发送数据
    • 例子2:任务A等待用户按下按键
    • 同步事件的来源有很多(这些概念在后面会细讲):
      • 队列(queue)
      • 二进制信号量(binary semaphores)
      • 计数信号量(counting semaphores)
      • 互斥量(mutexes)
      • 递归互斥量、递归锁(recursive mutexes)
      • 事件组(event groups)
      • 任务通知(task notifications)

在等待一个同步事件时,可以加上超时时间。比如等待队里数据,超时时间设为10ms:

- 10ms之内有数据到来:成功返回
- 10ms到了,还是没有数据:超时返回
  1. 挂起状态

在日常生活的例子中,母亲正在电脑前跟同事沟通,母亲可以暂停:

  • 好烦啊,我暂停一会
  • 领导说:你暂停一下

FreeRTOS中的任务也可以进入暂停状态,唯一的方法是通过vTaskSuspend函数。函数原型如下:

void vTaskSuspend( TaskHandle_t xTaskToSuspend );

参数xTaskToSuspend表示要暂停的任务,如果为NULL,表示暂停自己。

要退出暂停状态,只能由别人来操作:

  • 别的任务调用:vTaskResume
  • 中断程序调用:xTaskResumeFromISR
1.3 空闲任务及其钩子函数
1.3.1 空闲任务

空闲任务(Idle任务)的作用之一:释放被删除的任务的内存。

除了上述目的之外,为什么必须要有空闲任务?一个良好的程序,它的任务都是事件驱动的:平时大部分时间处于阻塞状态。有可能我们自己创建的所有任务都无法执行,但是调度器必须能找到一个可以运行的任务:所以,我们要提供空闲任务。在使用vTaskStartScheduler()函数来创建、启动调度器时,这个函数内部会创建空闲任务。

#define tskIDLE_PRIORITY			( ( UBaseType_t ) 0U )

void vTaskStartScheduler( void )
{
    BaseType_t xReturn;
	/* The Idle task is being created using dynamically allocated RAM. */
	xReturn = xTaskCreate(	prvIdleTask,
							configIDLE_TASK_NAME,
							configMINIMAL_STACK_SIZE,
							( void * ) NULL,
							( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
							&xIdleTaskHandle ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
}
  • 空闲任务优先级为0:它不能阻碍用户任务运行
  • 空闲任务要么处于就绪态,要么处于运行态,永远不会阻塞
  • 空闲任务的优先级为0,这意味着一旦某个用户的任务变为就绪态,那么空闲任务马上被切换出去,让这个用户任务运行。在这种情况下,我们说用户任务"抢占"(pre-empt)了空闲任务,这是由调度器实现的。
  • 要注意的是:如果使用vTaskDelete()来删除任务,那么你就要确保空闲任务有机会执行,否则就无法释放被删除任务的内存。

我们可以添加一个空闲任务的钩子函数(Idle Task Hook Functions),空闲任务的循环每执行一次,就会调用一次钩子函数。钩子函数的作用有这些:

  • 执行一些低优先级的、后台的、需要连续执行的函数
  • 测量系统的空闲时间:空闲任务能被执行就意味着所有的高优先级任务都停止了,所以测量空闲任务占据的时间,就可以算出处理器占用率。
void vApplicationIdleHook( void )
{
	int i;
    // 用于获取系统运行时间和任务运行时间的统计信息。
	vTaskGetRunTimeStats(pcWriteBuffer);
	for (i = 0; i < 16; i++)
		printf("-");
	printf("\n\r");
	printf("%s\n\r", pcWriteBuffer);
}
  • 让系统进入省电模式:空闲任务能被执行就意味着没有重要的事情要做,当然可以进入省电模式了。

空闲任务的钩子函数的限制:

  • 不能导致空闲任务进入阻塞状态、暂停状态
  • 如果你会使用vTaskDelete()来删除任务,那么钩子函数要非常高效地执行。如果空闲任务执行卡在钩子函数里的话,它就无法释放内存。
1.3.2 使用钩子函数的前提

在FreeRTOS\Source\tasks.c中,可以看到如下代码,所以前提就是:

  • 把这个宏定义为1:configUSE_IDLE_HOOK
  • 实现vApplicationIdleHook函数

2. 支持多优先级

2.1 任务优先级
  • 每个任务都要被指定一个优先级,取值范围:0~(configMAX_PRIORITIES - 1),configMAX_PRIORITIES定义在FreeRTOSConfig.h中。
  • FreeRTOS的调度器可以使用2种方法来快速找出优先级最高的、可以运行的任务。使用不同的方法时,configMAX_PRIORITIES 的取值有所不同。
    • 通用方法:
      • 使用C函数实现,对所有的架构都是同样的代码。对configMAX_PRIORITIES的取值没有限制。但是configMAX_PRIORITIES的取值还是尽量小,因为取值越大越浪费内存,也浪费时间。
      • configUSE_PORT_OPTIMISED_TASK_SELECTION被定义为0、或者未定义时,使用此方法。
    • 架构相关的优化的方法
      • 架构相关的汇编指令,可以从一个32位的数里快速地找出为1的最高位。使用这些指令,可以快速找出优先级最高的、可以运行的任务。
      • 使用这种方法时,configMAX_PRIORITIES的取值不能超过32。
      • configUSE_PORT_OPTIMISED_TASK_SELECTION被定义为1时,使用此方法。
  • 低优先级数值代表低优先级。
  • 空闲任务(idle task)的优先级为0(tskIDLE_PRIORITY)。
  • FreeRTOS调度器确保处于最高优先级的就绪或运行态任务获取处理器,换句话说,处于就绪状态的任务,只有其中的最高优先级任务才会运行。
  • 任何数量的任务可以共享同一个优先级。如果宏configUSE_TIME_SLICING定义为1,处于就绪态的多个相同优先级任务将会以时间片切换的方式共享处理器。
2.2 查找最高优先级的就绪任务

就绪列表 pxReadyTasksLists[ configMAX_PRIORITIES ]是一个数组,数组里面存的是就绪任务的 TCB(准确来说是 TCB 里面的 xStateListItem 节点),数组的下标对应任务的优先级,优先级越低对应的数组下标越小。

空闲任务的优先级最低,对应的是下标为 0 的链表。

任务在创建的时候,会根据任务的优先级将任务插入到就绪列表不同的位置。相同优先级的任务插入到就绪列表里面的同一条链表中。

pxCurrentTCB 是一个全局的 TCB 指针,用于指向优先级最高的就绪任务的 TCB,即当前正在运行的 TCB 。那么我们要想让任务支持优先级,即在任务切换(taskYIELD)的时候,让pxCurrenTCB 指向**最高优先级的就绪任务的 TCB **就可以。

查找最高优先级的就绪任务有两种方法,具体由configUSE_PORT_OPTIMISED_TASK_SELECTION</font> 这个宏控制,定义为 0 选择通用方法,定义为 1 选择根据处理器优化的方法,该宏默认在 portmacro.h 中定义为 1,即使用优化过的方法。

2.3 通用方法
/* 空闲任务优先级宏定义,在 task.h 中定义 */
#define tskIDLE_PRIORITY ( ( UBaseType_t ) 0U )

/* 定义 uxTopReadyPriority,在 task.c 中定义 */
static volatile UBaseType_t uxTopReadyPriority = tskIDLE_PRIORITY;
2.3.1 taskRECORD_READY_PRIORITY()

taskRECORD_READY_PRIORITY()用于更新 uxTopReadyPriority 的值。

uxTopReadyPriority 用于表示创建的任务的最高优先级,默认初始化为 0,即空闲任务的优先级。

2.3.2 taskSELECT_HIGHEST_PRIORITY_TASK()

taskSELECT_HIGHEST_PRIORITY_TASK()用于寻找优先级最高的就绪任务,实质就是更新 uxTopReadyPriority 和 pxCurrentTCB 的值。

在创建时已通过taskRECORD_READY_PRIORITY记录当前就绪任务的最高优先级uxTopReadyPriority,在这里再次更新uxTopReadyPriority,是因为:最高优先级的任务可能会被挂起或阻塞,那就会从其就绪链表中移除,若最高优先级的就绪链表中再没有其他就绪任务,则该优先级的就绪链表就为空。所以需要从高优先级到低优先级遍历就绪链表,找到第一个非空的就绪链表。

#if ( configUSE_PORT_OPTIMISED_TASK_SELECTION == 0 )
    #define taskRECORD_READY_PRIORITY( uxPriority )	\
	{	\
        /* uxTopReadyPriority 存的是就绪任务的最高优先级 */\
		if( ( uxPriority ) > uxTopReadyPriority )\
		{										\
			uxTopReadyPriority = ( uxPriority );\
		}										\
	} /* taskRECORD_READY_PRIORITY */


    #define taskSELECT_HIGHEST_PRIORITY_TASK()      \
	{										        \
    	UBaseType_t uxTopPriority = uxTopReadyPriority;	\
													\
		/* 寻找包含就绪任务的最高优先级的队列 */\
		while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) )\
		{																	\
			--uxTopPriority;				\
		}									\
		/* 获取优先级最高的就绪任务的 TCB,然后更新到 pxCurrentTCB */	\
		listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );\
		/*更新 uxTopReadyPriority */\
        uxTopReadyPriority = uxTopPriority;	\
	} /* taskSELECT_HIGHEST_PRIORITY_TASK */
2.4 优化方法

这得益于 Cortex-M 内核有一个计算前导零的指令CLZ,所谓前导零就是计算一个变量(Cortex-M 内核单片机的变量为 32位)从高位开始第一次出现 1 的位的前面的零的个数。比如:一个 32 位的变量uxTopReadyPriority,其位 0、位 24 和 位 25 均 置 1 , 其 余 位 为 0 , 具 体 见 。 那 么 使 用 前 导 零 指 令 __CLZ (uxTopReadyPriority)可以很快的计算出 uxTopReadyPriority 的前导零的个数为 6。

如果 uxTopReadyPriority 的每个位号对应的是任务的优先级,任务就绪时,则将对应的位置 1,反之则清零。那么图 10-2 就表示优先级 0、优先级 24 和优先级 25 这三个任务就绪,其中优先级为 25的任务优先级最高。利用前导零计算指令可以很快计算出就绪任务中的最高优先级为:

( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) ) = ( 31UL - ( uint32_t ) 6 ) = 25。

#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) \
        uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )

#define portRECORD_READY_PRIORITY( uxPriority, uxReadyPriorities ) \
             ( uxReadyPriorities ) |= ( 1UL << ( uxPriority ) )

#define portRESET_READY_PRIORITY( uxPriority, uxReadyPriorities ) \
             ( uxReadyPriorities ) &= ~( 1UL << ( uxPriority ) )   
2.4.1 taskRECORD_READY_PRIORITY()

根据传进来的形参(通常形参就是任务的优先级)将变量 uxTopReadyPriority 的某个位置 1。

uxTopReadyPriority:它在优化方法中担任的是一个优先级位图表的角色,即该变量的每个位对应任务的优先级,如果任务就绪,则将对应的位置 1,反之清零。根据这个原理,只需要计算出 uxTopReadyPriority 的前导零个数就算找到了就绪任务的最高优先级。与taskRECORD_READY_PRIORITY() 作 用 相 反 的 是 taskRESET_READY_PRIORITY() 。

    #define taskRECORD_READY_PRIORITY( uxPriority )\ 
            portRECORD_READY_PRIORITY( uxPriority, uxTopReadyPriority )
2.4.2 taskRESET_READY_PRIORITY()

用于根据传进来的形参(通常形参就是任务的优先级)将变量 uxTopReadyPriority的某个位清零。

根据优先级调用 taskRESET_READY_PRIORITY()函数复位 uxTopReadyPriority 变量中对应的位时,要先确保就绪列表中对应该优先级下的链表没有任务才行, 如果 有则不清零。

假设当前实验中,任务 1 会调用 vTaskDelay(),会将自己挂起,只能是将任务 1 从就绪列表删除,不能将任务 1 在优先级位图表 uxTopReadyPriority 中对应的位清 0,因为该优先级下还有任务 2,否则任务 2 将得不到执行。

#define taskRESET_READY_PRIORITY( uxPriority )\
	{\
		if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ ( uxPriority ) ] ) ) == ( UBaseType_t ) 0 )\
		{\
			portRESET_READY_PRIORITY( ( uxPriority ), ( uxTopReadyPriority ) );	\
		}\
	}
2.4.3 taskSELECT_HIGHEST_PRIORITY_TASK()

用于寻找优先级最高的就绪任务,实质就是更新 uxTopReadyPriority 和 pxCurrentTCB 的值。

根据 uxTopReadyPriority 的值,找到最高优先级,然后更新到uxTopPriority 这个局部变量中。

根据 uxTopPriority 的值,从就绪列表中找到就绪的最高优先级的任务的 TCB,然后将 TCB 更新到 pxCurrentTCB。

/* 查找最高优先级的就绪任务:根据处理器架构优化后的方法 */
#define taskSELECT_HIGHEST_PRIORITY_TASK()\
 {\
     UBaseType_t uxTopPriority;	\
     portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );\
     listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );\
 }  
2.4.3.1 listGET_OWNER_OF_NEXT_ENTRY
#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList )\
{											\
List_t * const pxConstList = ( pxList );\
	/* 节点索引指向链表第一个节点调整节点索引指针,指向下一个节点, */\
	/* 如果当前链表有 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;\
}

用于获取下一个节点的 OWNER 。

假设当前链表有 N 个节点,当第 N 次调用该函数时,pxIndex 则指向第 N个节点,即每调用一次,节点遍历指针 pxIndex 则会向后移动一次,用于指向下一个节点。

3. 创建任务

3.1 什么是任务?

从这个角度想:函数被暂停时,我们怎么保存它、保存什么?怎么恢复它、恢复什么?

  • 什么叫任务:

    • 运行中的函数、被暂停运行的函数
  • 任务是一个函数? 函数需要保存吗?

    • 函数保存在Flash上,不会被破坏,无需保存
    • Flash上的函数无需再次保存
    • 所以:任务不仅仅是函数
  • 任务是变量吗?

    • 单纯通过变量无法做事
    • 所以:任务不仅仅是变量
  • 任务是一个运行中的函数

    • 运行中:可以曾经运行,现在暂停了,但是未退出
    • 怎么描述一个运行中的函数
    • 假设在某一个瞬间时间停止,你怎么记录这个运行中的函数
  • 函数执行到哪里(运行的位置),需要保存吗?

    • 它是一个CPU寄存器,名为“PC”
    • 需要保存,
  • 函数用到了全局变量,全局变量需要保存吗?

    • 全局变量在内存上,不需要保存
  • 函数里用到了局部变量,局部变量需要保存吗?

    • 局部变量在栈里,也是在内存里,只要避免栈不被破坏即可,局部变量无需保存
  • 运算的中间值需要保存吗?中间值保存在哪里?

    • 在CPU寄存器里,另一个任务也要用到CPU寄存器,所以CPU寄存器需要保存
  • 汇总:

    • CPU寄存器需要保存!
    • 保存在任务的栈里
  • 什么叫现场?

    • 暂且认为现场就是:被打断瞬间,CPU中的R0~R15这16个寄存器的值。
  • 怎么保存?

    • 保存进内存里;
  • 保存到内存的哪里?–栈里

    • 把16个寄存器(现场)保存到栈里。

(1)提问:LR被覆盖了怎么办?(LR:用来保存返回地址)

  • 在C入口,划分出自己的栈;保存LR进栈里(在内存中);保存局部变量。

(2)提问:局部变量是在栈里面分配的,如何分配?

  • 局部变量有可能保存在寄存器中,不一定保存在内存里;
  • 或有 volatile,局部变量保存在内存中;
  • 或寄存器不够用时,保存在内存中。

(3)提问:为何每个RTOS任务都有自己的栈?

  • 每个任务都有自己的:{ 调用关系;局部变量;现场 },所以需要自己的栈。
  • 栈:从哪里分配,大小怎样确定?
    • 栈的大小:用估计的方法确定,
      • 局部变量的大小
      • 调用深度
    • 栈从哪里分配
      • 栈就是一块空闲的内存,
      • 从巨大的数组里去划分出一块内存给某个任务用作栈

任务切换时,怎样找到任务? -——通过链表

在freeRTOS中,任务就是一个函数,原型如下:

void ATaskFunction( void *pvParameters );
  • 这个函数不能返回,通常任务函数都是一个死循环。
  • 任务由xTaskCreate()函数创建,由vTaskDelete()函数删除。
  • 同一个函数,可以创建多个任务:换句话说,多个任务可以运行同一个函数;
  • 函数内部,尽量使用局部变量:
    • 每个任务都有自己的栈
    • 每个任务运行这个函数时:
      • 任务A的局部变量放在任务A的栈中,任务B的局部变量放在任务B的栈中
      • 不同的局部变量,有自己的副本
  • 函数使用全局变量、静态变量的话:
    • 只有一个副本:多个任务使用的是同一个副本
    • 要防止冲突
void ATaskFunction( void *pvParameters )
{
    /* 对于不同的任务,局部变量放在任务的栈里,有各自的副本 */
    int32_t lVariableExample = 0;
/* 任务函数通常实现为一个无限循环 */
    for( ;; )
    {
        /* 任务的代码 */
    }
 /* 如果程序从循环中退出,一定要使用vTaskDelete删除自己,*/
    /* NULL表示删除的是自己*/
    vTaskDelete( NULL );
 
 /* 程序不会执行到这里, 如果执行到这里就出错了 */
3.2 创建任务(视频补充)

在嵌入式领域里面有很多RTOS:

  • FreeRTOS操作系统
    • 创建新任务是xTaskCreate
  • RT-Thread操作系统
    • 创建新任务是rt_thread_create

不同的操作系统创建任务的函数不一样,若想统一起来,则要在上面增加一个统一的接口层cmsis_os2.c,该文件会抽象出一个统一的接口,比如osThreadNew函数(348行)。我们写程序的人、写APP的人就可以直接调用该函数,会根据不同的操作系统调用 不同的创建新任务的函数。

画板

使用该原生代码<font style="color:#DF2A3F;">xTaskCreateStatic</font><font style="color:#DF2A3F;">xTaskCreate</font>来创建任务。

3.3 创建第1个多任务程序(9.2.2创建任务)

任务创建:

  • 动态创建
    • 任务控制块和栈的内存是创建任务时动态分配的,任务删除时,内存可以释放。
  • 静态创建
    • 任务控制块和栈的内存需要事先定义好,是静态的内存,任务删除时,内存不能释放。

创建任务时可以使用2个函数:动态分配内存、静态分配内存。

  1. 动态分配内存
BaseType_t xTaskCreate( 
 TaskFunction_t pxTaskCode, // 函数指针, 任务函数
 const char * const pcName, // 任务的名字
 const configSTACK_DEPTH_TYPE usStackDepth, // 栈大小,单位为word,10表示40字节
 void * const pvParameters, // 调用任务函数时传入的参数
 UBaseType_t uxPriority, // 优先级
 TaskHandle_t * const pxCreatedTask ); // 任务句柄, 以后使用它来操作这个任务
  • "02-first-rtos-app"程序

不同的任务,pvParameters不一样

/* 使用同一个函数创建不同的任务 */
  xTaskCreate(LcdPrintTask, "task1", 128, &g_Task1Info, osPriorityNormal, NULL);
  xTaskCreate(LcdPrintTask, "task2", 128, &g_Task2Info, osPriorityNormal, NULL);
  xTaskCreate(LcdPrintTask, "task3", 128, &g_Task3Info, osPriorityNormal, NULL);
3.4 创建任务流程
  1. TCB:
// 已删减

typedef struct tskTaskControlBlock
{
	volatile StackType_t	*pxTopOfStack;	/*< Points to the location of the last item placed on the tasks stack.  THIS MUST BE THE FIRST MEMBER OF THE TCB STRUCT. */

	ListItem_t			xStateListItem;	/*< (Ready, Blocked, Suspended ). */
	ListItem_t			xEventListItem;		/*< Used to reference a task from an event list. */
	UBaseType_t			uxPriority;			/*< The priority of the task.  0 is the lowest priority. */
	StackType_t			*pxStack;			/*< Points to the start of the stack. */
	char				pcTaskName[ configMAX_TASK_NAME_LEN ];/*< Descriptive name given to the task when created.  Facilitates debugging only. */ /*lint !e971 Unqualified char types are allowed for strings and single characters only. */

	#if ( ( portSTACK_GROWTH > 0 ) || ( configRECORD_STACK_HIGH_ADDRESS == 1 ) )
		StackType_t		*pxEndOfStack;		/*< Points to the highest valid address for the stack. */
	#endif

	#if ( portCRITICAL_NESTING_IN_TCB == 1 )
		UBaseType_t		uxCriticalNesting;	/*< Holds the critical section nesting depth for ports that do not maintain their own count in the port layer. */
	#endif

	#if ( configUSE_TRACE_FACILITY == 1 )
		UBaseType_t		uxTCBNumber;		/*< Stores a number that increments each time a TCB is created.  It allows debuggers to determine when a task has been deleted and then recreated. */
		UBaseType_t		uxTaskNumber;		/*< Stores a number specifically for use by third party trace code. */
	#endif

	#if ( configUSE_MUTEXES == 1 )
		UBaseType_t		uxBasePriority;		/* 创建任务时设置的优先级 */
		UBaseType_t		uxMutexesHeld;
	#endif

	#if( configUSE_TASK_NOTIFICATIONS == 1 )
		volatile uint32_t ulNotifiedValue;
		volatile uint8_t ucNotifyState;
	#endif

} tskTCB;

/* The old tskTCB name is maintained above then typedefed to the new TCB_t name
below to enable the use of older kernel aware debuggers. */
typedef tskTCB TCB_t;
  1. 动态创建–函数调用关系如下:

3.4.1 创建新任务prvInitialiseNewTask
// 3.1 获取栈顶地址,并做向下做8字节对齐
    pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 );
    pxTopOfStack = ( StackType_t * ) ( ( ( uint32_t ) pxTopOfStack ) & ( ~( ( uint32_t ) 0x0007 ) ) ); 
// 3.2 将任务的名字存在TCB中
    pxNewTCB->pcTaskName[ x ] = pcName[ x ];
// 3.3 初始化优先级
    pxNewTCB->uxPriority = uxPriority;
// 3.4 初始化链表
    vListInitialiseItem( &( pxNewTCB->xStateListItem ) );
    vListInitialiseItem( &( pxNewTCB->xEventListItem ) );
// 3.6 设置链表的拥有者
    listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );
    listSET_LIST_ITEM_VALUE( &( pxNewTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) uxPriority );
    listSET_LIST_ITEM_OWNER( &( pxNewTCB->xEventListItem ), pxNewTCB );
// 3.5 初始化任务栈
    pxNewTCB->pxTopOfStack = pxPortInitialiseStack(...)
3.4.2 初始化任务栈pxPortInitialiseStack

3.4.3 放入就绪链表prvAddNewTaskToReadyList
prvAddNewTaskToReadyList(...)
    // 进入临界段
    taskENTER_CRITICAL();
    // 全局任务计时器加一操作
    uxCurrentNumberOfTasks++;
    // 如果 pxCurrentTCB 为空,则将 pxCurrentTCB 指向新创建的任务
    if( pxCurrentTCB == NULL )
    {
        pxCurrentTCB = pxNewTCB;
        // 如果是第一次创建任务,则需要初始化任务相关的列表
        if( uxCurrentNumberOfTasks == ( UBaseType_t ) 1 )
    	{
            prvInitialiseTaskLists();
        }
    }
    else
    {   // 如果 pxCurrentTCB 不为空,
        //则根据任务的优先级将 pxCurrentTCB 指向最高优先级任务的 TCB
            if( pxCurrentTCB->uxPriority <= pxNewTCB->uxPriority )
            {
                pxCurrentTCB = pxNewTCB;
            }
    }
prvAddTaskToReadyList( pxNewTCB );
taskEXIT_CRITICAL();    
            
3.4.3.1 就绪链表初始化prvInitialiseTaskLists
static void prvInitialiseTaskLists( void )
{
    UBaseType_t uxPriority;

	for( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ )
	{
		vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );
	}
}

// 链表初始化函数vListInitialise
void vListInitialise( List_t * const pxList )
{
    pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd );
    pxList->xListEnd.xItemValue = portMAX_DELAY;
    pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd );
    pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd );
    pxList->uxNumberOfItems = ( UBaseType_t ) 0U;
}

3.4.3.2 任务放入对应的就绪链表prvAddTaskToReadyList
#define prvAddTaskToReadyList( pxTCB )	\
	traceMOVED_TASK_TO_READY_STATE( pxTCB );\
    /* 根据优先级将优先级位图表 uxTopReadyPriority 中对应的位置位*/ \
	taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority );	\
    /* 根据优先级将任务插入到就绪链表 pxReadyTasksLists[]*/ \
	vListInsertEnd( &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ), \
                    &( ( pxTCB )->xStateListItem ) ); \
	tracePOST_MOVED_TASK_TO_READY_STATE( pxTCB )

任务控制块里面有一个 xStateListItem 成员,数据类型为 ListItem_t,我们将任务插入到就绪链表里面,就是通过将任务控制块的 xStateListItem 这个链表项插入到就绪链表中来实现的。

4. 常用任务函数

4.1 任务挂起
4.1.1 vTaskSuspend()

仅仅是将任务进入挂起态,其内部的资源都会保留下来,同时也不会参与系统中任务的调度。当调用恢复函数的时候,整个任务立即从挂起态进入就绪态,并且参与任务的调度,如果该任务的优先级是当前就绪态优先级最高的任务,那么立即会按照挂起前的任务状态继续执行该任务。即,挂起任务之前是什么状态,都会被系统保留下来,在恢复的瞬间,继续执行。

void vTaskSuspend( TaskHandle_t xTaskToSuspend )

执行流程:

  1. 获取被挂起的任务的TCB:pxTCB = prvGetTCBFromHandle( xTaskToSuspend );
  2. 从就绪/阻塞列表中删除任务并放入挂起列表中。
if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
{
    taskRESET_READY_PRIORITY( pxTCB->uxPriority );
}
  1. 如果任务在等待事件,也从等待事件列表中移除
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
{
    ( void ) uxListRemove( &( pxTCB->xEventListItem ) );
}
  1. 将任务添加到挂起列表中
vListInsertEnd( &xSuspendedTaskList, &( pxTCB->xStateListItem ) );
  1. 挂起的任务是 pxCurrentTCB
    1. 调度器在运行,挂起的任务是 pxCurrentTCB ,立即切换任务portYIELD()
    2. 调度器没运行,pxCurrentTCB指向的任务被挂起,则必须并将其指向其他任务
      1. 切换任务,空闲任务或其他就绪任务。vTaskSwitchContext();
4.1.2 vTaskSuspendAll()

将所有的任务都挂起,挂起所有的任务就是挂起调度器。调度器被挂起后则不能进行上下文切换,但中断还是使能的。当调度器被挂起的时候,如果有任务需要进行上下文切换,那么这个任务就会被挂起,在调度器恢复之后才执行切换任务。

4.2 任务恢复函数
4.2.1 vTaskResume()

就是让挂起的任务重新进入就绪状态,恢复的任务会保留挂起前的状态信息,在恢复的时候根据挂起时的状态继续运行。如果被恢复任务在所有就绪态任务中,处于最高优先级列表的第一位,那么系统就会进行任务的上下文切换。

void vTaskResume( TaskHandle_t xTaskToResume ) 

执行流程:

  1. 进入临界区 taskENTER_CRITICAL();
  2. 将要恢复的任务从挂起链表中删除,并添加到就绪链表中去
( void ) uxListRemove(  &( pxTCB->xStateListItem ) );
prvAddTaskToReadyList( pxTCB );
  1. 如果刚刚恢复的任务的优先级pxTCB->uxPriority大于当前任务的优先级pxCurrentTCB->uxPriority,则进行任务切换:
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
    taskYIELD_IF_USING_PREEMPTION();
}					
#define taskYIELD_IF_USING_PREEMPTION() \
        portYIELD_WITHIN_API()

#define portYIELD()	\
{\
	/* Set a PendSV to request a context switch. */	\
	portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;	\
	__dsb( portSY_FULL_READ_WRITE );\
	__isb( portSY_FULL_READ_WRITE );\
}
  1. 退出临界区: taskEXIT_CRITICAL();
4.2.2 xTaskResumeFromISR()

xTaskResumeFromISR()与 vTaskResume()一样都是用于恢复被挂起的任务,不一样的

是 xTaskResumeFromISR() 专 门 用 在 中 断 服 务 程 序 中 。 无 论通过调用一次或多次vTaskSuspend()函数而被挂起的任务,也只需调用一次 xTaskResumeFromISR()函数即可解挂。

BaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume ) 
  1. 关中断
  2. 调度器是否被挂起
      1. 如果刚刚恢复的任务的优先级pxTCB->uxPriority大于当前任务的优先级pxCurrentTCB->uxPriority,则进行任务切换,在这里只是记录要进行切换
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
    xYieldRequired = pdTRUE;
}
    2. 将要恢复的任务从挂起链表中删除,并添加到就绪链表中去
( void ) uxListRemove(  &( pxTCB->xStateListItem ) );
prvAddTaskToReadyList( pxTCB );
2. 是
    1.  任务被添加到待处理的就绪列表中,直到调度器恢复再进行任务的处理。
vListInsertEnd( &( xPendingReadyList ), 
               &( pxTCB->xEventListItem ) );
  1. 开中断
  2. 返回 xYieldRequired 结果,在外部选择是否进行任务切换。

使用 xTaskResumeFromISR()的时候有几个需要注意的地方:

  1. 当函数的返回值为 pdTRUE 时:恢复运行的任务的优先级等于或高于正在运行的任务 , 表明在中断服务函数退出 后 必须进行一次上下文切换, 使用 portYIELD_FROM_ISR()进行上下文切换。当函数的返回值为 pdFALSE 时:恢复 运行的任务的优先级低于当前正在运行的任务,表明在中断服务函数退出后不需 要进行上下文切换。
  2. xTaskResumeFromISR() 通常被认为是一个危险的函数,因为它的调用并非是固定 的,中断可能随时来来临。所以,xTaskResumeFromISR()不能用于任务和中断间 的同步,如果中断恰巧在任务被挂起之前到达,这就会导致一次中断丢失(任务 还没有挂起,调用 xTaskResumeFromISR()函数是没有意义的,只能等下一次中 断)。这种情况下,可以使用信号量或者任务通知来同步就可以避免这种情况。
4.3 任务删除vTaskDelete
void vTaskDelete( TaskHandle_t xTaskToDelete );

自杀:vTaskDelete( NULL );B自杀,空闲任务收尸

被杀:别的任务执行vTaskDelete(pvTaskCode);pvTaskCode是自己的句柄。A杀B:A给B收尸(释放栈和TCB)

注:被删除的任务,其在任务创建时由内核分配的存储空间,会由空闲任务释放。如果有应用程序调用vTaskDelete(),必须保证空闲任务获取一定的微控制器处理时间。任务代码自己分配的内存是不会自动释放的,因此删除任务前,应该将这些内存释放。

void vTaskDelete( TaskHandle_t xTaskToDelete ) 
  1. 进入临界区 taskENTER_CRITICAL();
  2. 获取任务控制块,如果 xTaskToDelete 为 null ,则删除任务自身
pxTCB = prvGetTCBFromHandle( xTaskToDelete );
  1. 将任务从就绪列表中移除 ,如果删除后就绪列表的长度为 0,当前没有就绪的任务,应该调用 taskRESET_READY_PRIORITY()函数清除任务的最高就绪优先级变量 uxTopReadyPriority 中的位。
if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
{
    taskRESET_READY_PRIORITY( pxTCB->uxPriority );
}
  1. 如果当前任务在等待事件,那么将任务从事件列表中移除
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
{
    ( void ) uxListRemove( &( pxTCB->xEventListItem ) );
}
  1. 删除的任务是任务自身的话,这不能在任务本身内完成,因为需要上下文切换到另一个任务。
    1. 将任务放在结束列表中 (xTasksWaitingTermination),空闲任务会检查结束列表并在空闲任务中释放删除任务的 控制块和已删除任务的堆栈内存。
    2. 记录有多少个任务需要释放内存,以便空闲任务知道有多少个已删除的任务需要进行内存释放
if( pxTCB == pxCurrentTCB )
{
   vListInsertEnd( &xTasksWaitingTermination, 
                  &( pxTCB->xStateListItem ) ); 
   ++uxDeletedTasksWaitingCleanUp; 
3. 需要发起一次任务切换
if( xSchedulerRunning != pdFALSE )
{
    if( pxTCB == pxCurrentTCB )
    {
        configASSERT( uxSchedulerSuspended == 0 );
        portYIELD_WITHIN_API();
    }
}
  1. 删除的是其他任务块
    1. 当前任务数减一
    2. 释放任务块与堆栈空间
    3. 重置下一个任务的解除阻塞时间。重新计算一下还要多长时间执行下一个任务,如果下个任务的解锁,刚好是被删除的任务,那么这就是不正确的,因为 删除的任务对调度器而言是不可见的,所以调度器是无法对删除的任务进行调度,所以要 重新从延时列表中获取下一个要解除阻塞的任务。调用 prvResetNextTaskUnblockTime()函 数从延时列表的头部来获取下一个要解除任务的 TCB,延时列表按延时时间排序。
--uxCurrentNumberOfTasks;
prvDeleteTCB( pxTCB ); 
prvResetNextTaskUnblockTime();
  1. 退出临界区taskEXIT_CRITICAL();
4.3.1 prvCheckTasksWaitingTermination()
static List_t xTasksWaitingTermination = {0};

空闲任务会检查结束列表 xTasksWaitingTermination 并且释放对应删除任务的内存空间,空闲 任 务 调 用 prvCheckTasksWaitingTermination() 函数进行这些相应操作,该函数是FreeRTOS 内部调用的函数,在 prvIdleTask 中调用,

while( uxDeletedTasksWaitingCleanUp > ( UBaseType_t ) 0U )
{
    taskENTER_CRITICAL();
    {
        /* 获取对应任务控制块 */
        pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( ( &xTasksWaitingTermination ) );
        /* 将任务从列表中删除 */
        ( void ) uxListRemove( &( pxTCB->xStateListItem ) );
        /* 当前任务个数减一 */
        --uxCurrentNumberOfTasks;
        /* uxDeletedTasksWaitingCleanUp 的值减一,
            直到为 0 退出循环 */
        --uxDeletedTasksWaitingCleanUp;
    }
    taskEXIT_CRITICAL();
    /* 删除任务控制块与堆栈 */
    prvDeleteTCB( pxTCB );
}
4.3.2 prvResetNextTaskUnblockTime
static void prvResetNextTaskUnblockTime( void )
{
TCB_t *pxTCB;

	if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
	{
		xNextTaskUnblockTime = portMAX_DELAY;
	}
	else
	{
		( pxTCB ) = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
		xNextTaskUnblockTime = listGET_LIST_ITEM_VALUE( &( ( pxTCB )->xStateListItem ) );
	}
}
4.4 任务延时函数
4.4.1 vTaskDelay()和vTaskDelayUntil() 的区别

vTaskDelayUntil() 与 vTaskDelay () 一 样都是用来实现任务的延时。

  • vTaskDelay ()的延时是相对的,是不确定的,它的延时是等 vTaskDelay ()调用完毕后开始计算的。并且 vTaskDelay ()延时的时间到了之后,如果有高优先级的任务或者中断正在执行,被延时阻塞的任务并不会马上解除阻塞,所有每次执行任务的周期并不完全确定。
  • vTaskDelayUntil()延时是绝对的,适用于周期性执行的任务。(*pxPreviousWakeTime + xTimeIncrement)时间到达后,vTaskDelayUntil()函数立刻返回,如果任务是最高优先级的,那么任务会立马解除阻塞,所以说 vTaskDelayUntil()函数的延时是绝对性的。

4.4.2 vTaskDelay()
void vTaskDelay( const TickType_t xTicksToDelay )

vTaskDelay()用于阻塞延时,调用该函数后,任务将进入阻塞状态,进入阻塞态的任务将让出 CPU 资源。延时的时长由形参 xTicksToDelay 决定,单位为系统节拍周期, 比如系统的时钟节拍周期为 1ms,那么调用vTaskDelay(1)的延时时间则为1ms。

vTaskDelay()延时是相对性的延时,它指定的延时时间是从调用 vTaskDelay()结束后开始计算的,经过指定的时间后延时结束。比如 vTaskDelay(100), 从调用 vTaskDelay()结束后,任务进入阻塞状态,经过 100 个系统时钟节拍周期后,任务解除阻塞。因此,vTaskDelay()并不适用与周期性执行任务的场合。此外,其它任务和中断活动, 也会影响到 vTaskDelay()的调用(比如调用前高优先级任务抢占了当前任务),进而影响到任务的下一次执行的时间。

#if ( INCLUDE_vTaskDelay == 1 )

	void vTaskDelay( const TickType_t xTicksToDelay )
	{
    	BaseType_t xAlreadyYielded = pdFALSE;

		/* 延时时间要大于 0 个 tick,否则会进行强制切换任务 */
		if( xTicksToDelay > ( TickType_t ) 0U )
		{
			configASSERT( uxSchedulerSuspended == 0 );
			vTaskSuspendAll();
			{
				traceTASK_DELAY();

				/* 将任务添加到延时列表中去 */
				prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );
			}
			xAlreadyYielded = xTaskResumeAll();
		}

		/* 强制切换任务,将 PendSV 的 bit28 置 1 */
		if( xAlreadyYielded == pdFALSE )
		{
			portYIELD_WITHIN_API();
		}
	}
4.4.2.1 prvAddCurrentTaskToDelayedList()
  • xTicksToWait : 表示要延时多长时间,单位为系统节拍周期。
  • xCanBlockIndefinitely : 表示是否可以永久阻塞,如果 pdFALSE 表 示不允许永久阻塞,也就是不允许挂起当然任务,而如果是 pdTRUE,则可以永久阻塞。
  • xConstTickCount: 获取当前调用延时函数的时间点
  1. 在将任务添加到延时列表之前,从就绪列表中删除任务
  2. 是否永久阻塞:
    1. 是:
      1. 直接将任务添加到挂起列表,而不是延时列表
    2. 否:
      1. 计算唤醒任务的时间
xTimeToWake = xConstTickCount + xTicksToWait; 
    2. 列表项按唤醒时间顺序插入
listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), 
                        xTimeToWake );
    3. 唤醒时间`xTimeToWake`是否溢出
        1. 溢出了, 添加到延时溢出列表中  
vListInsert( pxOverflowDelayedTaskList, 
            &( pxCurrentTCB->xStateListItem ) );
        2. 没有溢出,
            1. 添加到延时列表中  
vListInsert( pxDelayedTaskList, 
            &( pxCurrentTCB->xStateListItem ) );
            2. 如果进入阻塞状态的任务被放置在被阻塞任务列表的头部,也就是下一个要唤醒的任务就是当前任务,那么就需要更新 xNextTaskUnblockTime 的值
if( xTimeToWake < xNextTaskUnblockTime )
{
    xNextTaskUnblockTime = xTimeToWake;
}
static void prvAddCurrentTaskToDelayedList( 
                TickType_t xTicksToWait, 
                const BaseType_t xCanBlockIndefinitely )
{
    TickType_t xTimeToWake;
    const TickType_t xConstTickCount = xTickCount;

	/* 在将任务添加到阻止列表之前,从就绪列表中删除任务,
    因为两个列表都使用相同的列表项。 */
	if( uxListRemove( &( pxCurrentTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )
	{
		portRESET_READY_PRIORITY( pxCurrentTCB->uxPriority, uxTopReadyPriority );
	}

	#if ( INCLUDE_vTaskSuspend == 1 )
	{
		if( ( xTicksToWait == portMAX_DELAY ) && 
               ( xCanBlockIndefinitely != pdFALSE ) )
		{
			/* 支持挂起,则将当前任务挂起,
            直接将任务添加到挂起列表,而不是延时列表!*/
			vListInsertEnd( &xSuspendedTaskList, &( pxCurrentTCB->xStateListItem ) );
		}
		else
		{
			/* 计算唤醒任务的时间 */
			xTimeToWake = xConstTickCount + xTicksToWait;

			/* 列表项将按唤醒时间顺序插入 */
			listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );

			if( xTimeToWake < xConstTickCount )
			{
				/* 唤醒时间如果溢出了,则会添加到延时溢出列表中 */
				vListInsert( pxOverflowDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
			}
			else
			{
				/* 没有溢出,添加到延时列表中 */
				vListInsert( pxDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );

				/* 如果进入阻塞状态的任务被放置在被阻塞任务列表的头部,
                    也就是下一个要唤醒的任务就是当前任务,那么就需要更新
                    xNextTaskUnblockTime 的值 */
				if( xTimeToWake < xNextTaskUnblockTime )
				{
					xNextTaskUnblockTime = xTimeToWake;
				}
			}
		}
	}
}
4.4.3 vTaskDelayUntil()

vTaskDelayUntil():绝对延时函数 ,这个绝对延时常用于较精确的周期运行任务,比如我有一个任务,希望它以固定频率定期执行,而不受外部的影响,任务从上一次运行开始到下一次运行开始的时间间隔是绝对的,而不是相对的。

void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, 
                     const TickType_t xTimeIncrement )
{
	TickType_t xTimeToWake;
	BaseType_t xAlreadyYielded, xShouldDelay = pdFALSE;

		configASSERT( pxPreviousWakeTime );
		configASSERT( ( xTimeIncrement > 0U ) );
		configASSERT( uxSchedulerSuspended == 0 );

		vTaskSuspendAll();
		{
			/* 获取开始进行延时的时间点 */
			const TickType_t xConstTickCount = xTickCount;

			/* 计算延时到达的时间,也就是唤醒任务的时间 */
			xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;

            /* pxPreviousWakeTime 中保存的是上次唤醒时间,
            唤醒后需要一定时间执行任务主体代码,
            如果上次唤醒时间大于当前时间,说明节拍计数器溢出了*/
			if( xConstTickCount < *pxPreviousWakeTime )
			{
				/*  如果唤醒的时间小于上次唤醒时间,
                并且唤醒时间大于开始计时的时间,
                这样子就是相当于没有溢出,
                也就是保了证周期性延时时间大于任务主体代码的执行时间*/
				if( ( xTimeToWake < *pxPreviousWakeTime ) && ( xTimeToWake > xConstTickCount ) )
				{
					xShouldDelay = pdTRUE;
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
			else
			{
				/*  只是唤醒时间溢出的情况,或者都没溢出,
                     保证了延时时间大于任务主体代码的执行时间*/
				if( ( xTimeToWake < *pxPreviousWakeTime ) || ( xTimeToWake > xConstTickCount ) )
				{
					xShouldDelay = pdTRUE;
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}

			/* 更新上一次的唤醒时间 */
			*pxPreviousWakeTime = xTimeToWake;

			if( xShouldDelay != pdFALSE )
			{
				traceTASK_DELAY_UNTIL( xTimeToWake );

				/* prvAddCurrentTaskToDelayedList()函数需要的是阻塞时间
                    , 而不是唤醒时间,因此减去当前的滴答计数。 */
				prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE );
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		xAlreadyYielded = xTaskResumeAll();

		/* 强制执行一次上下文切换 */
		if( xAlreadyYielded == pdFALSE )
		{
			portYIELD_WITHIN_API();
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
}

#endif /* INCLUDE_vTaskDelayUntil */

xTimeIncrement:任务周期时间。

pxPreviousWakeTime:上一次唤醒任务的时间点。

xTimeToWake:本次要唤醒任务的时间点。

xConstTickCount:进入延时的时间点。

溢出情况:

  1. 如果唤醒的时间小于上次唤醒时间,并且唤醒时间大于开始计时的时间,这样子就是相当于没有溢出,也就是保了证周期性延时时间大于任务主体代码的执行时间
if( ( xTimeToWake < *pxPreviousWakeTime ) && 
     ( xTimeToWake > xConstTickCount ) )
{
    xShouldDelay = pdTRUE;
}

  1. 只是唤醒时间溢出的情况,或者都没溢出,保证了延时时间大于任务主体代码的执行时间
if( ( xTimeToWake < *pxPreviousWakeTime ) ||
    ( xTimeToWake > xConstTickCount ) )
{
    xShouldDelay = pdTRUE;
}

从图 16-2、图 16-3 与图 16-4 可以看出无论是溢出还是没有溢出,都要求在下次唤醒任务之前,当前任务主体代码必须被执行完。也就是说任务执行的时间必须小于任务周期时间 xTimeIncrement,总不能存在任务周期为 10ms 的任务,其主体代码执行时间为 20ms,这样子根本执行不完任务主体代码。计算的唤醒时间合法后,就将当前任务加入延时列表,同样延时列表也有两个。每次产生系统节拍中断,都会检查这两个延时列表,查看延时的任务是否到期,如果时间到,则将任务从延时列表中删除,重新加入就绪列表,任务从阻塞态变成就绪态,如果此时的任务优先级是最高的,则会触发一次上下文切换。

4.4.3.1 绝对延时函数 vTaskDelayUntil()实例
void vTaskA( void * pvParameters )
{
    /* 用于保存上次时间。调用后系统自动更新 */ 
    static portTickType PreviousWakeTime; 
    /* 设置延时时间,将时间转为节拍数 */ 
    const portTickType TimeIncrement = pdMS_TO_TICKS(1000); 
 
    /* 获取当前系统时间 */ 
    PreviousWakeTime = xTaskGetTickCount(); 
 
     while (1)
     {
        /* 调用绝对延时函数,任务时间间隔为 1000 个 tick */ 
        vTaskDelayUntil( &PreviousWakeTime,TimeIncrement ); 
     }

    // ....
    // 这里为任务主体代码
    // ....
}

注意:在使用的时候要将延时时间转化为系统节拍,在任务主体之前要调用延时函数。

任务会先调用 vTaskDelayUntil()使任务进入阻塞态,等到时间到了就从阻塞中解除,然后执行主体代码,任务主体代码执行完毕。会继续调用 vTaskDelayUntil()使任务进入阻塞态,然后就是循环这样子执行。即使任务在执行过程中发生中断,那么也不会影响这个任务的运行周期,仅仅是缩短了阻塞的时间而已,到了要唤醒的时间依旧会将任务唤醒。

5. 任务的设计要点

任务的优先级信息,任务与中断的处理,任务的运行时间、逻辑、状态等都要知道,才能设计出好的系统, 所以,在设计的时候需要根据需求制定框架。在设计之初就应该考虑下面几点因素:任务运行的上下文环境、任务的执行时间合理设计。

FreeRTOS 中程序运行的上下文包括:

  • 中断服务函数。
  • 普通任务。
  • 空闲任务。
  1. 中断服务函数:

中断服务函数是一种需要特别注意的上下文环境,它运行在非任务的执行环境下(一 般为芯片的一种特殊运行模式(也被称作特权模式)),在这个上下文环境中不能使用挂起当前任务的操作,不允许调用任何会阻塞运行的 API 函数接口。

另外需要注意的是,中 断服务程序最好保持精简短小,快进快出,一般在中断服务函数中只做标记事件的发生, 然后通知任务,让对应任务去执行相关处理,因为中断服务函数的优先级高于任何优先级 的任务,如果中断处理时间过长,将会导致整个系统的任务无法正常运行。所以在设计的 时候必须考虑中断的频率、中断的处理时间等重要因素,以便配合对应中断处理任务的工 作。

  1. 任务

任务看似没有什么限制程序执行的因素,似乎所有的操作都可以执行。

但是做为一个优先级明确的实时系统,如果一个任务中的程序出现了死循环操作(此处的死循环是指没有阻塞机制的任务循环体),那么比这个任务优先级低的任务都将无法执行,当然也包括了空闲任务,因为死循环的时候,任务不会主动让出 CPU,低优先级的任务是不可能得到 CPU 的使用权的,而高优先级的任务就可以抢占 CPU。

这个情况在实时操作系统中是必须 注意的一点,所以在任务中不允许出现死循环。如果一个任务只有就绪态而无阻塞态,势 必会影响到其他低优先级任务的执行,所以在进行任务设计时,就应该保证任务在不活跃 的时候,任务可以进入阻塞态以交出 CPU 使用权,这就需要我们自己明确知道什么情况下 让任务进入阻塞态,保证低优先级任务可以正常运行。在实际设计中,一般会将紧急的处理事件的任务优先级设置得高一些。

  1. 空闲任务

空闲任务(idle 任务)是 FreeRTOS 系统中没有其他工作进行时自动进入的系统任务。

因为处理器总是需要代码来执行——所以至少要有一个任务处于运行态。FreeRTOS 为了保证这一点,当调用 vTaskStartScheduler()时,调度器会自动创建一个空闲任务,空闲任务是一个非常短小的循环。

用户可以通过空闲任务钩子方式,在空闲任务上钩入自己的功能函数。通常这个空闲任务钩子能够完成一些额外的特殊功能,例如系统运行状态的指示,系统省电模式等。

除了空闲任务钩子,FreeRTOS 系统还把空闲任务用于一些其他的功能,比如当系统删除一个任务或一个动态任务运行结束时,在执行删除任务的时候,并不会释放任务的内存空间,只会将任务添加到结束列表中,真正的系统资源回收工作在空闲任务完成,空闲任务是唯一一个不允许出现阻塞情况的任务,因为 FreeRTOS 需要保证系统永远都有一个可运行的任务。

对于空闲任务钩子上挂接的空闲钩子函数,它应该满足以下的条件:

  • 永远不会挂起空闲任务;
  • 不应该陷入死循环,需要留出部分时间用于系统处理系统资源回收。
  1. 任务的执行时间

任务的执行时间一般是指两个方面,一是任务从开始到结束的时间,二是任务的周期。

在系统设计的时候这两个时间候我们都需要考虑,例如,对于事件 A 对应的服务任务Ta,系统要求的实时响应指标是 10ms,而 Ta 的最大运行时间是 1ms,那么 10ms 就是任务 Ta 的周期了,1ms 则是任务的运行时间,简单来说任务 Ta 在 10ms 内完成对事件 A 的响应即可。此时,系统中还存在着以 50ms 为周期的另一任务 Tb,它每次运行的最大时间长度是 100us。在这种情况下,即使把任务 Tb 的优先级抬到比 Ta 更高的位置,对系统的实时性指标也没什么影响,因为即使在 Ta 的运行过程中,Tb 抢占了 Ta 的资源,等到 Tb 执行完毕,消耗的时间也只不过是 100us,还是在事件 A 规定的响应时间内(10ms),Ta 能够安全完成对事件 A 的响应。但是假如系统中还存在任务 Tc,其运行时间为 20ms,假如将 Tc的优先级设置比 Ta 更高,那么在 Ta 运行的时候,突然间被 Tc 打断,等到 Tc 执行完毕,那 Ta 已经错过对事件 A(10ms)的响应了,这是不允许的。所以在我们设计的时候,必须考虑任务的时间,一般来说处理时间更短的任务优先级应设置更高一些。


http://www.kler.cn/news/355307.html

相关文章:

  • 解决 burp 抓取 无用包的 困扰
  • 设计模式之委托模式
  • 提高EDM广告发送率和到达率_实用技巧揭秘
  • GIT batch的支持中文的方法和系统建议
  • react项目,通过自定义 separator 属性来修改日期选择器中间的分隔符:
  • I.MX6U 的 EPIT 定时器详解
  • Scrapy的Lambda函数用法:简化数据提取与处理的技巧
  • el-upload照片墙二次封装
  • golang 报错:invalid character ‘‘ looking for beginning of object key string
  • Mobile ALOHA 简介
  • 数据库管理-第252期 深入浅出多主多活数据库技术- Cantian存储引擎(二)(20241017)
  • K8s简介和安装部署
  • 嵌入式开发学习日记——数据结构基础
  • 算法——python实现堆排序
  • leetcode 47.全排列||
  • Flink简介及小案例
  • SpringBoot框架下购物推荐网站的设计模式与实现
  • 网络资源模板--Android Studio 实现简易新闻App
  • 10.15.2024刷华为OD C题型(二)
  • 怎么一键下载网页所有图片?3个方法轻松搞定