FreeRTOS学习记录 02--任务篇

0 前言

@ Author         :Dargon
@ Record Date    :2021/07/12
@ Reference Book : `FreeRTOS源码详解与应用开发`,`ARM Cortex-M3与Cortex-M4权威指南`,`B站正点原子FreeRTOS讲解视频`
@ Purpose        :学习正点原子的miniFly,该飞控基于FreeRTOS系统开发的,所以学习一下记录下关于RTOS系统的一些基本操作,大概了解系统的工作原理,如何创建,运行,切换任务等等基本操作流程。在此进行学习的记录。

1 任务基础知识

1.1 任务优先级

  • 与中断优先级不是同一回事的,中断优先级是数值越小对应的优先级就越高。
  • 任务的优先级,对应的是0~configMAX_PRIORITIES =32,在任务调度的时候,会根据这个优先级的大小进行选择要运行的任务,即是处于就绪态的最高优先级的任务才会被运行。
  • 当任务进入就绪列表的排列的时候,列表的排列是按照优先级从小到大进行排列的,例如当前优先级为4 5 ,对应的列表项的值为(32-4)和(32-5)进行入就绪态的队列操作,相应的优先级为5 的就在优先级为4 的前面。

1.2 任务控制块TCB_t

  • 一个任务所包含的信息 多对应的结构体

// --任务块的结构体
typedef struct tskTaskControlBlock
{
    
    
    // --volatile 表示每次都是直接从 内存地址去读取,而不是通过cache或者register读取,告诉编译器这个变量不需要优化
    // --任务控制块结构体 第一项就是对应的 任务堆栈指针
    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. */

    // --xStateListItem 和链表有关的 根据该任务的状态,将此列表项添加到对应的状态列表中,
    // --xEventListItem 任务的事件列表项添加到 某个事件列表中。 
    ListItem_t			xStateListItem;	/*< The list that the state list item of a task is reference from denotes the state of that task (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. */
    // --申请内存 都已经是16个char了
    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 ( configUSE_MUTEXES == 1 )
        UBaseType_t		uxBasePriority;		/*< The priority last assigned to the task - used by the priority inheritance mechanism. */
        UBaseType_t		uxMutexesHeld;
    #endif
} tskTCB;
typedef tskTCB TCB_t;
  • 详细的解释一下各项的作用,以及后面所发挥的作用

  • volatile StackType_t *pxTopOfStack
    指向任务的堆栈的最后一个位置,为什么这样说,因为在申请任务堆栈的时候,通过malloc函数,返回的内存指针,指向的是这块新内存的首地址。我们若是需要发生一次任务切换的话,就需要返回堆栈的作用,来保存现场,需要把当前的这些寄存器东西存入堆栈中,以便下次回来继续执行。但是对于STM32来说,堆栈是向下生长的,所以就需要一个,栈顶的位置pxTopOfStack,来向下进行遍历,进行–操作,一次次的存入堆栈中。

  • ListItem_t xStateListItem
    根据该任务的现在所处的状态,将此列表项添加到对应的状态列表中,例如该任务现在处于就绪态,就会根据其优先级 挂到 就绪list中。

  • ListItem_t xEventListItem
    当该任务在等待某个消息或者信号量的时候,一直没有等到而进入阻塞态的任务就会将该任务的这个列表项,挂到对应的队列的xTasksWaitingToSendlist上,(在queue进行初始化的时候,会初始化相应的两个列表xTasksWaitingToSendxTasksWaitingToReceive)

  • UBaseType_t uxPriority
    对应的任务初始化的时候,设定的该任务的优先级

  • StackType_t *pxStack
    申请的任务堆栈,算作栈底吧!在初始化的时候会有这样一句话;

    pxNewTCB->pxStack = ( StackType_t * ) pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t )
    
  • char pcTaskName[ configMAX_TASK_NAME_LEN ]
    一个char字符串,保存任务的名字,configMAX_TASK_NAME_LEN =16,最多是15个char 其中需要\0来结尾的,
    不管怎样,你申请这个结构体内存之后,即使你用不完这个名字内存,它也是占用16个char 的位置。

  • 最后一个关于 如果使用了互斥信号量的时候,所进行的条件编译。

1.3 任务的状态

  • 运行态、
    一个正在运行的任务,占用CPU的任务,如果对应的单核处理器,那么不管在任何时刻永远都只有一个任务在运行,原来的单核是这个意思啊!

  • 就绪态

  • 阻塞态

  • 挂起态

2 API函数

2.1 任务创建

  • 动态创建任务源码分析
// --任务动态创建过程
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )

	BaseType_t xTaskCreate(	TaskFunction_t pxTaskCode,
							const char * const pcName,
							const uint16_t usStackDepth,
							void * const pvParameters,
							UBaseType_t uxPriority,
							TaskHandle_t * const pxCreatedTask ) /*lint !e971 Unqualified char types are allowed for strings and single characters only. */
	{
    
    
	TCB_t *pxNewTCB; // --申请一个结构体指针
	BaseType_t xReturn;

		/* If the stack grows down then allocate the stack then the TCB so the stack
		does not grow into the TCB.  Likewise if the stack grows up then allocate
		the TCB then the stack. */
		#if( portSTACK_GROWTH > 0 ) // --关于条件编译 不看 关于堆栈指针向上生长 stm32是堆栈向下生长的
		{
    
    
            // ……省略
		}
		#else /* portSTACK_GROWTH */ // --开始任务create
		{
    
    
		StackType_t *pxStack;

			/* Allocate space for the stack used by the task being created. */
			// --malloc 堆栈内存 一个堆栈单元是 32bit =4byte
			pxStack = ( StackType_t * ) pvPortMalloc( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) ); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */

			if( pxStack != NULL )
			{
    
    
				/* Allocate space for the TCB. */
				// --堆栈完事之后 申请该任务控制块的内存
				pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) ); /*lint !e961 MISRA exception as the casts are only redundant for some paths. */

				if( pxNewTCB != NULL )
				{
    
    
					/* Store the stack location in the TCB. */
					// --初始化堆栈内存
					pxNewTCB->pxStack = pxStack;
				}
				else
				{
    
    
					/* The stack cannot be used as the TCB was not created.  Free
					it again. */
					vPortFree( pxStack );
				}
			}
			else
			{
    
    
				pxNewTCB = NULL;
			}
		}
		#endif /* portSTACK_GROWTH */

		if( pxNewTCB != NULL )
		{
    
    
			#if( tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0 )
			{
    
    
				/* Tasks can be created statically or dynamically, so note this
				task was created dynamically in case it is later deleted. */
				pxNewTCB->ucStaticallyAllocated = tskDYNAMICALLY_ALLOCATED_STACK_AND_TCB;
			}
			#endif /* configSUPPORT_STATIC_ALLOCATION */
			// --前面对于给你的 只是申请内存
			// --初始化任务其他项  里面挺多的初始化
			prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL );
			
			// --添加任务到 就绪列表中
			prvAddNewTaskToReadyList( pxNewTCB );
			xReturn = pdPASS;
		}
		else
		{
    
    
			xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;
		}

		return xReturn;
	}

#endif /* configSUPPORT_DYNAMIC_ALLOCATION */

  • xTaskCreate() 函数

    1. 申请一个sizeof( TCB_t )大小的内存块
    2. 申请成功后,申请一个任务堆栈内存,返回地址给pxNewTCB->pxStack
    3. 调用函数prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL )进行TCB_t的其它成员的初始化。
    4. 调用函数prvAddNewTaskToReadyList( pxNewTCB ),将该任务挂到,就绪列表中去。
    • prvInitialiseNewTask() 函数
      首先,函数的代码很长,前面的xTaskCreate()只是相当于为TCB_t申请内存,把初始化部分都交给这里了,该函数由于初始化任务控制块TCB_t的其它成员变量,有很多的条件编译,用不到的就直接过了。

      1. 调用memset( pxNewTCB->pxStack, ( int ) tskSTACK_FILL_BYTE, ( size_t ) ulStackDepth * sizeof( StackType_t ) )将任务的堆栈区域进行填充0xa5,这一步等到后面 需要检测堆栈内存是否用完或者剩余,都是基于这里的0xa5值的。
      2. 计算栈顶位置pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 );
      3. 初始化任务名 pxNewTCB->pcTaskName[ x ]
      4. 初始化任务优先级 pxNewTCB->uxPriority
      5. 初始化两个列表项
        vListInitialiseItem( &( pxNewTCB->xStateListItem ) );
        vListInitialiseItem( &( pxNewTCB->xEventListItem ) );
        
      6. 初始化状态列表项里面的变量xStateListItem ->pvOwner 指向该任务。对应着自己的任务状态,相当于将该任务挂到相应的状态列表上。
      7. 初始化事件列表项里面的变量xEventListItem->value,可以看出这里设定的value值,所对应的任务的优先级值越大,则对应的列表项的值就小,在列表的排列就相对的靠前,体现优先级高。
        listSET_LIST_ITEM_VALUE( &( pxNewTCB->xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) uxPriority );
        
      8. 初始化事件列表项里面的变量xEventListItem ->pvOwner 指向该任务
      9. 任务堆栈 的初始化,调用函数pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );,原来被fill 0xa5 的那些内存要初始化成那些,一个重头戏。pxTaskCode是一个函数指针指向任务函数。
      10. 初始化任务句柄 *pxCreatedTask = ( TaskHandle_t ) pxNewTCB; pxCreatedTask是一个指针指向pxNewTCB这个地址
      • pxPortInitialiseStack() 函数 位于port.c文件中
        初始化堆栈区域,堆栈是用来进行上下文切换的时候保存现场的,在创建好一个堆栈后,需要对其进行初始化处理,即对Cortex-M内核的某些寄存器赋予初值,这些初值就先保存子任务的堆栈中。按顺序进行保存。
            pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );
        
        StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters )
        {
                  
                  
            /* Simulate the stack frame as it would be created by a context switch
            interrupt. */
            // --用来保存现场
            /* Offset added to account for the way the MCU uses the stack on entry/exit
            of interrupts, and to ensure alignment. */
            pxTopOfStack--;
            // --xPSR =0x01000000 对应的bit24 为1,对应的功能表示处于 Thumb状态,使用Thumb指令
            *pxTopOfStack = portINITIAL_XPSR;	/* xPSR */
            
            // --将R(15)PC 初始化为对应的任务函数
            pxTopOfStack--;
            *pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK;	/* PC */
            
            // --R(14) LR -- 链接寄存器 初始化为 prvTaskExitError 调用vPortRaiseBASEPRI() 关中断的函数
            pxTopOfStack--;
            *pxTopOfStack = ( StackType_t ) prvTaskExitError;	/* LR */
        
            // --跳过R12, R3, R2 and R1 寄存器,紧接着R0 保存任务函数的参数
            /* Save code space by skipping register initialisation. */
            pxTopOfStack -= 5;	/* R12, R3, R2 and R1. */
            *pxTopOfStack = ( StackType_t ) pvParameters;	/* R0 */
        
            /* A save method is being used that requires each task to maintain its
            own exec return value. */
            // --表示退出SVC和PendSV中断时候 CPU应该处于那种状态 portINITIAL_EXEC_RETURN = 0xfffffffd
            // --对应的bit0--bit3 =1101 表示返回后进入线程模式,使用进程栈指针PSP
            // --这种退出SVC 中断进入的模式,关系到第一个任务从SVC出来之后,后面需要以怎样的方式开始工作
            pxTopOfStack--;
            *pxTopOfStack = portINITIAL_EXEC_RETURN;
        
            // --跳过 R11, R10, R9, R8, R7, R6, R5 and R4 8个寄存器
            pxTopOfStack -= 8;	/* R11, R10, R9, R8, R7, R6, R5 and R4. */
        
            // --重新返回栈顶指针
            return pxTopOfStack;
        }
        
        1. 保存xPSR寄存器值到堆栈中
        2. 保存R15(PC)寄存器值到堆栈中
        3. 保存R14(LR)寄存器值到堆栈中
        4. 保存R0寄存器值任务参数到堆栈中
        5. 保存portINITIAL_EXEC_RETURN值到堆栈中
        6. 跳过8个寄存器,R11~R4,更新pxTopOfStack栈顶的值
        7. 这些寄存器的值,和这个任务相关的,暂时存储在该任务的堆栈区域,等到切换任务的时候,会再次从堆栈中弹出来,到相应的寄存器内,进行现场恢复。
    • prvAddNewTaskToReadyList( pxNewTCB ) 函数 位于task.c文件中

      1. 全局变量uxCurrentNumberOfTasks当前的任务个数+1,新的任务过来了。
      2. 如果创建的任务是第一个任务,调用prvInitialiseTaskLists()函数初始化列表(就是之前的就绪态的列表数组,2个关于阻塞的列表和1个PendingReady列表),初始化的是列表,不是列表项。基本的任务调度,会使用这些列表。
      3. 当前有任务运行,但是任务调度器没有运行,根据创建的任务的优先级,更新一下pxCurrentTCBpxCurrentTCB就对应着下一步要运行的任务了,等着被任务调度器进行调用。
      4. 更新uxTaskNumber,用于记录任务控制块的标号,pxNewTCB->uxTCBNumber = uxTaskNumber;为显示该创建的任务本身是第几个任务。
      5. 调用prvAddTaskToReadyList( pxNewTCB );函数,将添加到就绪列表中,按照优先级值的进行添加到列表中。
      6. 如果任务调度器开启,该创建的任务优先级高于pxCurrentTCB任务优先级,需要进行一次任务切换。
        1. pxCurrentTCB全局变量指向的是正在运行的任务,如果它进行更新了,在任务切换中,会将原来运行的任务现场(相应的寄存器)保存到该任务所对应的堆栈中,将pxCurrentTCB所指向的任务的堆栈内容(自己任务所需要的环境)恢复到相应的寄存器中,准备运行
      • prvAddTaskToReadyList( pxNewTCB ) 函数 位于task.c 文件中
        本身是宏定义
        1. 调用宏 taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority ); 记录/更新 就绪列表中年的最高优先级uxTopReadyPriority
          1. 最终调用( uxReadyPriorities ) |= ( 1UL << ( uxPriority )这样一个操作
          2. 用32bit的变量的位,进行记录当前就绪列表中的最高优先级,
          3. 当从这个变量获取最高优先级的时候,这里使用的硬件方法,计算32bit变量的前导0的个数(对应的是有汇编指令的)
        2. 调用函数vListInsertEnd(),将创建的任务插入到就绪列表中。

2.2 任务删除

  • vTaskDelete() 删除函数源码分析
    1. 利用pxTCB = prvGetTCBFromHandle( xTaskToDelete ),根据参数进行,查看要删除的任务句柄,为null 的话,删除任务本身(pxCurrentTCB),否则就是删除相应的任务。
    2. 将该任务的pxTCB->xStateListItem所挂到的状态列表中进行remove 操作。
    3. 将该任务的pxTCB->xEventListItem所挂到的事件列表中进行remove 操作。比如,该任务因为等待某个队列消息或信号量产生阻塞就挂在该队列的等待消息而产生阻塞的列表上。
    4. uxTaskNumber++,用于任务控制块的编号,在创建新任务的时候也需要++,然后将其赋值给pxNewTCB->uxTCBNumber = uxTaskNumber;
    5. 如果删除的是当前正在运行的任务,将该任务的pxTCB->xStateListItem insert 到列表xTasksWaitingTermination为任务等待终止的列表里面,等待删除。 更新一下等待删除任务的个数++uxDeletedTasksWaitingCleanUp
    6. 删除的任务不是正在运行的任务,将当前任务总个数减1--uxCurrentNumberOfTasks,调用函数prvDeleteTCB( pxTCB );来free 任务堆栈内存和任务控制块TCB_t的内存。调用函数prvResetNextTaskUnblockTime重置一下,下一个阻塞等待时间,(就是,有可能删除的任务,是当前正挂在阻塞列表上的任务)
    7. 如果pxTCB == pxCurrentTCB,要强制进行一次任务切换。

2.3 任务阻塞

  • 一般的在系统中,在一个任务函数中使用延时函数对这个任务进行延时,但是,如何实现该任务的延时,把这个任务根据需要延时的时间挂到阻塞列表上,待一会儿,不就是相当于延时了该时间段了,执行延时函数就会进行任务切换,并且此任务进入阻塞态,直到延时完成,任务重新进入就绪态。

  • 任务延时有两种延时,一种是相对延时vTaskDelay()(就是直接在运行的基础上延时多少多少),另一种是绝对延时vTaskDelayUntil()(相当于给任务设定了一个运行时间,延时一个所设定的时间段)

  • vTaskDelay() 相对延时源码分析

    1. 调用vTaskSuspendAll();挂起任务调度器。
    2. 调用函数prvAddCurrentTaskToDelayedList()把当前任务挂到delay 列表中。
    3. 调用xTaskResumeAll(); 恢复任务调度器。
    • prvAddCurrentTaskToDelayedList() 函数分析
      1. 记录当前时刻值xTickCount
      2. 将该任务从就绪列表中移除 uxListRemove( &( pxCurrentTCB->xStateListItem ) )
      3. 如果延时时间为portMAX_DELAY,直接将任务挂到挂起列表上
      4. 设定该任务的状态列表项的值 为等待时刻值``
        listSET_LIST_ITEM_VALUE( &( pxCurrentTCB->xStateListItem ), xTimeToWake );
        
      5. 计算时间点xTimeToWake发生溢出,则需要挂到对应的溢出的阻塞列表中,pxOverflowDelayedTaskList这是在任务初始化的进行申请的。
      6. 正常操作的话,直接挂到阻塞列表中,pxDelayedTaskList
      7. 对于下一个阻塞时间点xNextTaskUnblockTime,进行更新一下

2.4 任务挂起

  • vTaskSuspend() 任务挂起函数源码分析
    1. 利用pxTCB = prvGetTCBFromHandle( xTaskToDelete ),根据参数进行,查看要挂起的任务句柄,为null 的话,挂起任务本身。
    2. 将该任务的pxTCB->xStateListItem所挂到的状态列表中进行remove 操作。
    3. 将该任务的pxTCB->xEventListItem所挂到的事件列表中进行remove 操作
    4. 将该任务添加到挂起任务列表末尾,挂起任务列表为xSuspendedTaskList,所有被挂起的任务,都会挂在这个列表上面,因为挂起suspend没有恢复时间了,就直接在末尾插入就好,(相对insert插入,需要遍历找一下,末尾插入更快些)。
    5. 调用函数prvResetNextTaskUnblockTime重置一下,下一个阻塞等待时间,(就是,有可能挂起的任务,是当前正挂在阻塞列表上的任务)
    6. 如果pxTCB == pxCurrentTCB
      1. 任务调度器已经开始运行了,则需要强制进行一次任务切换。
      2. 任务调度器未开启,
        1. 如果所有的任务都被挂起,则pxCurrentTCB =null 这种情况是不可能的出现的,因为有空闲任务在哪里撑着的。
        2. 调用函数vTaskSwitchContext();寻找下一个要运行的任务,此时的处境: 当前的任务要被挂起,任务调度器没有开启,任务没有被全部挂起到suspend列表中。所以要进行查找下一个要运行的任务。正常的话,任务的调用是任务调度器来进行完成的,但是任务调度器没有开启,只能自己手动去寻找下一个任务了。

2.5 任务恢复

  • vTaskResume() 任务恢复函数源码分析
    1. 调用uxListRemove( &( pxTCB->xStateListItem ) ),从挂起列表中remove 这个列表项对应的任务。
    2. 将该任务的 pxTCB->xStateListItem 添加到就绪列表中prvAddTaskToReadyList( pxTCB );
    3. 判断恢复的任务的优先级,是否需要进行任务切换。

3 空闲任务

  • 还未进行处理

正常的话,任务的调用是任务调度器来进行完成的,但是任务调度器没有开启,只能自己手动去寻找下一个任务了。

2.5 任务恢复

  • vTaskResume() 任务恢复函数源码分析
    1. 调用uxListRemove( &( pxTCB->xStateListItem ) ),从挂起列表中remove 这个列表项对应的任务。
    2. 将该任务的 pxTCB->xStateListItem 添加到就绪列表中prvAddTaskToReadyList( pxTCB );
    3. 判断恢复的任务的优先级,是否需要进行任务切换。

3 空闲任务

  • 还未进行处理

猜你喜欢

转载自blog.csdn.net/Dallas01/article/details/118720651