FreeRTOS:2.任务调度

FreeRTOS任务调度

Middlewares\Third_Party\FreeRTOS\Source\tasks.c

Middlewares\Third_Party\FreeRTOS\Source\include\task.h

万字总结FreeRTOS的任务调度

参考链接:

FreeRTOS-实现任务调度器_freertos任务调度器-CSDN博客

FreeRTOS-时间片与任务阻塞的实现_freertos 信号获取阻塞-CSDN博客

FreeRTOS-任务管理_第四章 freertos 任务管理-CSDN博客

FreeRTOS-时间片与任务阻塞的实现_freertos 信号获取阻塞-CSDN博客

一、任务

1. 任务栈

多任务系统中,每个任务都是独立的,互不干扰的,所以要为每个任务都分配独立的栈空间,这个栈空间通常是一个预先定义好的全局数组,也可以是动态分配的一段内存空间,但它们都存在于 RAM(内存中)

除了任务分配的栈之外,还有一个主栈(就是在我们启动文件里面定义的栈)供FreeRTOS内核使用以及中断服务函数使用,而任务中的函数就使用该任务的栈。也就是双堆栈机制。后续再讲

2. 任务函数

任务就是一个独立的函数,每个任务实现它相应的功能,各任务独立运行,轮流运行,当切换的够快时,相当于各任务在同时运行。

3. 任务控制块TCB

任务控制块其实就是一个任务结构体,包含任务的所有信息,比如任务的栈顶指针、任务的优先级、任务栈的起始地址、任务的名称等。下面的代码删除了很多一些配置宏,只有声明了对应的宏才会在TCB结构体中添加定义。

typedef struct tskTaskControlBlock 			
{
	volatile StackType_t	*pxTopOfStack;	// 任务栈顶指针
	ListItem_t			xStateListItem;		// 任务的链表项,用于插入各种任务状态的链表,如就绪链表、阻塞链表
	ListItem_t			xEventListItem;		// 任务的链表项,用于表示任务等待某个事件
	UBaseType_t			uxPriority;			//任务优先级
	StackType_t			*pxStack;			//任务栈起始地址
	char				pcTaskName[ configMAX_TASK_NAME_LEN ];	//任务名称
	
    ...
} tskTCB;

可以看到任务控制块里面并没有任务函数和任务函数参数,这两个关键数据存放在任务栈中。

二、任务管理

1. 任务的四种状态及相互转换

// include\task.h
/* Task states returned by eTaskGetState. */
typedef enum
{
	eRunning = 0,	/* A task is querying the state of itself, so must be running. */
	eReady,			/* The task being queried is in a read or pending ready list. */
	eBlocked,		/* The task being queried is in the Blocked state. */
	eSuspended,		/* The task being queried is in the Suspended state, or is in the Blocked state with an infinite time out. */
	eDeleted,		/* The task being queried has been deleted, but its TCB has not yet been freed. */
	eInvalid		/* Used as an 'invalid state' value. */
} eTaskState;

任务具有4种状态:运行态(Running)、就绪态(Ready)、阻塞态(Blocked)、挂起/暂停态(Suspended)

在FreeRTOS中,任务处于什么状态,就被插入到什么链表当中。

在这里插入图片描述

在prvInitialiseTaskLists函数中,这些链表被初始化

在这里插入图片描述

就绪态任务位于其优先级对应的就绪链表中,阻塞态和暂停态任务分别位于什么链表中

在这里插入图片描述

  • 运行态→阻塞态:正在运行的任务发生阻塞(延时调用vTaskDelay()、读信号量等待)时,该任务会从就绪列表中删除,任务状态由运行态变成阻塞态,然后会发生一次任务切换,运行就绪列表中当前最高优先级任务。
  • 阻塞态→就绪态:阻塞的任务被恢复后(延时时间超时、读信号量超时或读到信号量等),此时被恢复的任务会被加入就绪列表,从而由阻塞态变成就绪态,如果此时被恢复任务的优先级高于正在运行任务的优先级,则会发生任务切换,将该任务将再次转换任务状态,由就绪态变成运行态。
  • 就绪态、阻塞态、运行态→挂起态:任务可以通过调用 vTaskSuspend() API 函数都可以将处于任何状态的任务挂起,被挂起的任务得不到CPU 的使用权,也不会参与调度,除非它从挂起态中解除(其他任务调用vTaskResume()或 vTaskResumeFromISR()函数)。
  • 挂起态→就绪态:把一个挂起态的任务恢复的唯一途径就是调用 vTaskResume() 或 vTaskResumeFromISR() API函数,如果此时被恢复任务的优先级高于正在运行任务的优先级,则会发生任务切换,将该任务将再次转换任务状态,由就绪态变成运行态。

2. 任务创建

任务创建可以分为静态创建和动态创建,分别为xTaskCreateStatic和xTaskCreate函数

1. 静态创建

所谓静态指的是任务所需要的内存(任务的栈,任务的任务结构体(TCB) )是静态分配的,也就是手动分配全局变量,用全局变量来定义任务的栈(全局数组),任务结构体(全局的结构体变量)。

因此任务销毁后,这部分内存无法被释放,只有在程序结束后才会被释放。

xTaskCreateStatic

静态创建任务的函数xTaskCreateStatic如下:

在这里插入图片描述

xTaskCreateStatic完成的工作:将任务栈设置为静态分配的任务栈,将TCB设置为静态分配的TCB,调用prvInitialiseNewTask函数对任务进行进一步的初始化,然后将这个任务插入到就绪链表。

prvInitialiseNewTask

xTaskCreateStatic中调用了prvInitialiseNewTask,删去一些判断进行注释讲解。

在这里插入图片描述

在这里插入图片描述

prvInitialiseNewTask函数主要工作:拷贝任务的名字到TCB中,对TCB中的链表项进行初始化,将链表项指向TCB,修改TCB中的任务优先级,然后调用pxPortInitialiseStack函数对任务栈进行初始化。

pxPortInitialiseStack

这个函数主要对任务栈进行初始化,构造一个现场,任务切换需要去保存CPU寄存器的值

在这里插入图片描述

在刚开始任务还没有运行的时候,需要伪造一个现场,统一由中断函数启动或切换任务时,交给系统去进行,而不是由用户去启动一个任务。这段代码就是进行伪造现场,保证任务第一次启动时顺利。

  • xPSR的bit24必须置1,表示Thumb状态。PC指针置为任务函数的入口地址,LR寄存器置为error,因为一般任务函数都是死循环,因此如果函数返回说明出现error。R0存放任务函数的形参。
  • LR、R12、R3、R2、R1为硬件自动保存
  • R4-R11需要由调用者手动保存。

关于CotexM3 的寄存器组,后续再进行讲解。

任务栈的内存分布如下图:

在这里插入图片描述

创建任务函数最重要的就是伪造现场,而伪造现场最重要的就是将任务函数地址放入PC寄存器,任务函数参数放入R0寄存器,当启动第一个任务时将现场恢复时(将栈里的寄存器数值恢复到CPU寄存器中),则PC寄存器就存放了任务函数的地址,R0就存放了函数的参数,则PC为程序计数器,则会程序会跳转至该任务函数执行,而R0寄存器就是该任务函数的参数传递过去。

除了去初始化任务控制块的成员变量,最重要的一步还是去任务栈里面伪造现场,这下就能解释为什么任务控制块里面没有体现出任务函数及其参数,其实都保存在任务的栈中,然后再保存新的栈顶指针到任务控制块(方便下次从该位置恢复寄存器),等启动任务时恢复现场将运行第一个任务。

2. 动态创建

什么是动态创建任务:所谓动态就是使用类似于malloc函数去堆上分配内存(为任务的栈与TCB分配空间)。

FreeRTOS 做法是在 SRAM 里面定义一个大数组,也就是堆内存,供 FreeRTOS 的动态内存分配函数malloc使用,在第一次使用的时候,系统会将定义的堆内存进行初始化,这些代码在 FreeRTOS 提供的内存管理方案中实现(heap_1.c、heap_2.c、heap_4.c ),后续再对FreeRTOS的内存管理进行分析

在FreeRTOS 所谓的动态内存只不过是一个全局数组,只不过这个数组的空间可以反复利用,因为动态动态有使用就有回收(malloc使用与free回收)

#define configTOTAL_HEAP_SIZE                    ((size_t)15360)
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];

在pvPortMalloc中进行堆内存分配,如果是第一次分配,会调用prvHeapInit()函数进行堆的初始化

xTaskCreate

这里截取xTaskCreate函数的一部分,xTaskCreate函数和静态分配xTaskCreateStatic的不同之处只在于TCB和任务栈的分配部分,在静态分配函数中TCB和任务栈都要提前进行静态分配,定义为全局变量。但在动态分配中,采用pvPortMalloc函数在堆中为TCB和任务栈进行分配空间。

在分配完空间之后,就和静态分配函数类似了,调用prvInitialiseNewTask函数,进一步初始化TCB和任务栈,然后把当前任务加入到就绪链表中。

在这里插入图片描述

3. 任务挂起与恢复

任务的挂起和恢复涉及到两个函数vTaskSuspend、vTaskResume函数

vTaskSuspend

挂起任务:不能得到执行的任务(得不到CPU的使用权)

运行中的任务可以通过调用 vTaskSuspend()函数,将处于任何状态的任务挂起(传入NULL挂起自己),只能通过vTaskResume()恢复该任务。

要支持挂起,必须定义指定的宏

// Core\Inc\FreeRTOSConfig.h
#define INCLUDE_vTaskSuspend                 1

在这里插入图片描述

vTaskResume

在这里插入图片描述

4. 任务的删除与空闲任务

vTaskDelete

任务的删除需要做的事情有:

  1. 将任务从挂入的(就绪/延时/挂起/事件链表)中删除
  2. 删除任务结构体TCB
  3. 删除任务栈

当一个正在运行的任务调用了vTaskDelete(NULL),说明想要删除任务自己,那么任务只会从挂入的链表中被删除,而任务结构体TCB和任务栈交给空闲任务去释放。因为调用vTaskDelete()函数需要栈,所以删除自身的时候并没有彻底的删除,而是将TCB和栈的释放工作交给空闲任务(也就是将这个任务挂入等待空闲任务删除的链表)。如果是动态创建的TCB和栈,则会调用vPortFree进行内存的释放,静态创建的则无法释放。

在这里插入图片描述

空闲任务IdleTask
// Middlewares\Third_Party\FreeRTOS\Source\tasks.c
PRIVILEGED_DATA static TaskHandle_t xIdleTaskHandle					= NULL;			/*< Holds the handle of the idle task.  The idle task is created automatically when the scheduler is started. */

FreeRTOS中必须时时刻刻需要一个任务正在运行,当我们自己创建的所有应用任务都无法执行,但是调度器必须能找到一个可以运行的任务。在使用 vTaskStartScheduler() 函数来创建、启动调度器时,这个函数内部会创建空闲任务IdleTask

空闲任务的作用:执行一些任务的清理工作,执行用户定义的钩子函数Hook,钩子函数可以执行一些低优先级的事情,也可以用来测量系统的空闲任务,但不能导致空闲任务阻塞或暂停,并且最好能够快进快出。

空闲任务的任务函数如下:

在这里插入图片描述

5. 任务的延时与任务阻塞

RreeRTOS的对任务的阻塞延时是vTaskDelay ()函数实现的,当任务调用该函数时,任务会被剥离 CPU 使用权,也就是停止运行,该任务会从就绪链表移除并挂入一个延时链表中,并尝试发起一次任务调度(任务切换),在任务阻塞期间,CPU 可以去执行其它的任务,直到延时结束,任务从延时链表移除重新挂入一个就绪链表,然后再获取 CPU 使用权才可以继续运行。

在 FreeRTOS 中,有两条任务延时链表(一条是正常,一条是用于保存已超出当前tick计数的任务的延迟任务链表(计数器溢出),它们的实现方式与作用是一样的,当任务需要延时的时候,则先将任务挂起,即先将任务从就绪链表删除,然后插入到任务延时链表,同时更新任务的解锁时刻变量:xNextTaskUnblockTime 的值。

在任务延时插入到延时链表时,任务会按照延时时间的大小从小到大升序排序插入延时链表中,也就是延时链表中第一个任务是延时时间最小的,当每次时基中断(SysTick 中断)来临时,就拿系统时基计数器的值 xTickCount 与下一个任务的解锁时刻变量xNextTaskUnblockTime的值相比较,(xNextTaskUnblockTime 的值等于系统时基计数器的值 xTickCount 加上任务需要延时的值 xTicksToDelay) 如果相等,则表示有任务延时到期,需要将该任务就绪,否则只是单纯地更新系统时基计数器xTickCount 的值(加1),然后进行任务切换。

延时链表及xNextTaskUnblockTime

延时链表和延时链表指针如下定义:

PRIVILEGED_DATA static List_t xDelayedTaskList1;						/*< Delayed tasks. */
PRIVILEGED_DATA static List_t xDelayedTaskList2;						/*< Delayed tasks (two lists are used - one for delays that have overflowed the current tick count. */
PRIVILEGED_DATA static List_t * volatile pxDelayedTaskList;				/*< Points to the delayed task list currently being used. */
PRIVILEGED_DATA static List_t * volatile pxOverflowDelayedTaskList;		/*< Points to the delayed task list currently being used to hold tasks that have overflowed the current tick count. */

**xNextTaskUnblockTime变量(记录下一个要延时时间到达的时刻)**定义如下:

PRIVILEGED_DATA static volatile TickType_t xNextTaskUnblockTime		= ( TickType_t ) 0U; /* Initialised to portMAX_DELAY before the scheduler starts. */

prvResetNextTaskUnblockTime函数用于更新xNextTaskUnblockTime,为最小的延时到期时间

在这里插入图片描述

任务延时vTaskDelay

vTaskDelay函数首先调用prvAddCurrentTaskToDelayedList函数将任务插入到延时链表,然后尝试切换一次任务。

在这里插入图片描述

prvAddCurrentTaskToDelayedList函数首先获取当前tickcount,将任务从就绪链表中清除,然后根据计算得到的到时时间将任务插入到延时/溢出链表中。(延时/溢出链表是根据延时到期时间升序排列的,xNextTaskUnblockTime变量是延时链表首元素的到时时间

这里要注意TickType_t为uint32_t类型的变量,可能会溢出,而溢出相当于就是取模,也就是如果溢出,会出现到时时间小于当前的时间,那么这个任务应该插入到溢出链表中,如果没有溢出,则插入到正常的延时链表当中,并更新最小的下一个解锁时刻变量的值。

延时到期后的任务恢复

FreeRTOS在每个systick中断的中断处理函数xPortSysTickHandler中会判断是否存在有任务的延时时间到期,如果有则对任务进行恢复,如果设置了抢占或者时间片,则会尝试发起调度。

在systick中断的处理函数xPortSysTickHandler中调用了xTaskIncrementTick函数,当xTaskIncrementTick函数返回pdTrue时切换任务

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();
	{
		/* Increment the RTOS tick. */
		if( xTaskIncrementTick() != pdFALSE )
		{
			/* A context switch is required.  Context switching is performed in
			the PendSV interrupt.  Pend the PendSV interrupt. */
			portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; // 设置PendSV中断标志,发起调度
		}
	}
	vPortClearBASEPRIFromISR();
}

xTaskIncrementTick函数:如果计时溢出则交换延时链表,并对所有延时时间到期的任务进行恢复,并根据恢复任务的优先级,是否支持抢占、时间片来判断是否要发起调度

在这里插入图片描述

taskSWITCH_DELAYED_LISTS函数:交换延时链表

在这里插入图片描述

三、调度器管理

本篇设计到MSP、PSP双堆栈机制、Cortex M3的寄存器,后续会出一篇文章进行介绍,参见…

1. 启动调度器

启动过程为:vTaskStartScheduler -> xPortStartScheduler -> prvStartFirstTask -> SVC中断

主要完成如下工作:创建空闲任务、配置pendsv中断和systick中断的优先级、启动第一个任务

调度器的启动由 vTaskStartScheduler()函数来完成,vTaskStartScheduler函数首先创建空闲任务,然后调用xPortStartScheduler函数去进一步启动调度器。

void vTaskStartScheduler( void )
{
	BaseType_t xReturn;
	
    StaticTask_t *pxIdleTaskTCBBuffer = NULL;
    StackType_t *pxIdleTaskStackBuffer = NULL;
    uint32_t ulIdleTaskStackSize;
	// 分配空闲任务的TCB、任务栈的空间
    vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );
    xIdleTaskHandle = xTaskCreateStatic(	prvIdleTask,
                                        configIDLE_TASK_NAME,
                                        ulIdleTaskStackSize,
                                        ( void * ) NULL, 
                                        portPRIVILEGE_BIT, 
                                        pxIdleTaskStackBuffer,
                                        pxIdleTaskTCBBuffer ); 

	// 调用xPortStartScheduler函数
    if( xPortStartScheduler() != pdFALSE )
    {
	
    }
    else
    {
        /* Should only reach here if a task calls xTaskEndScheduler(). */
    }
}

xPortStartScheduler函数如下:

在这里插入图片描述

prvStartFirstTask函数如下:

在这里插入图片描述

prvStartFirstTask函数中产生了系统调用,服务号0表示SVC中断,SVC中断的处理函数(vPortSVCHandler)如下:

在这里插入图片描述

SVC中断:首先获取当前任务的TCB,然后从TCB中的栈顶指针开始恢复现场,首先手动恢复寄存器R4-R11,然后在退出SVC中断时,由硬件自动恢复剩下的寄存器如xPSR、PC、R12、R0等,其中最重要的就是PC(存放任务函数的入口地址),以及R0寄存器(存放任务函数的参数)

在这里插入图片描述

2. 调度器挂起与恢复

这一部分内容需要结合任务调度内容进行阅读(需要先阅读任务调度部分内容才更好的阅读本节)

调度器为什么要挂起

挂起调度器的根本原因就是去保护临界资源,什么是临界资源:(比如一些全局的变量),所以任务/中断中能都可以去使用,如果我们不去保护这些全局变量会发生什么情况?

  1. 临界资源被更高优先级的任务修改
    当我们在一个正在执行的任务函数中去使用一个全局变量(临界资源),此时如果一个更高优先级的任务(恢复或者被创建), 则会抢占该正在执行的任务,如果这个更高优先级的任务里也去修改这个全局变量(临界资源),则等回到原来任务时(更高优先级任务主动进入阻塞状态),这个全局变量的值已经被修改了,则后面会乱套了。

  2. 临界资源被中断修改
    (最低优先级的中断都比最高优先级的任务的优先级的要高,所谓任务优先级操作系统人为指定的,再高优先级的任务也只是一个应用程序,必须给中断/异常(更加紧急的事物让步))
    当我们在一个正在执行的任务函数中去使用一个全局变量(临界资源),此时一个中断来临,会打断正在执行的任务转而去执行中断服务函数,如果在中断服务函数中修改了这个全局变量(临界资源),等回到任务运行时,这个全局变量的值已经被修改了,则后面一样会乱套了。

所以如何去保护这些临界资源不被修改呢?

  1. 挂起调度器:当我们挂起调度器中,操作系统不在进行任务切换也就不存在高优先级的任务抢占的情况。

  2. 关中断:当我们关闭了中断(当然不可能是全部关闭,关闭部分中断),中断就不会打断任务的执行了,当然某些低优先级的中断使用了某些临界资源也可以通过关中断(关闭一些优先级的较高的中断)来保护临界资源不被修改。关中断也可以起到挂起调度器的作用:因为任务切换是PendSV中断中进行的,关闭中断也会将PendSV中断屏蔽,因此关中断也会组织任务的切换。

挂起调度器vTaskSuspendAll

调用 xTaskResumeAll()函数可以挂起任务调度器,而且该函数可以嵌套可以重复挂起调度器,但是恢复调度器的时候调用了多少次的 vTaskSuspendAll()就要调用多少次xTaskResumeAll()进行恢复。

uxSchedulerSuspended默认为0表示调度器未被挂起,当调用vTaskSuspendAll()函数让uxSchedulerSuspended全局变量自增1,则uxSchedulerSuspended大于零表示调度器被挂起。

那么uxSchedulerSuspended全局变量大于零是如何挂起调度器,禁止切换任务的呢?

在systick中断中的服务程序中,主要调用了xTaskIncrementTick函数,这个函数在”延时到期后的任务恢复“这一节讲过。

如果调度器正常运行,xTaskIncrementTick函数的作用:

  1. 更新系统时基xTickCount(每中断一次就会调用xTaskIncrementTick加1)
  2. 检查是否有延时到期的任务(如果有则将任务从延时链表->就绪链表中)
  3. 判断是否要切换任务(需要切换返回pdTURE不需要返回pdFALSE
  4. 如果使能了钩子函数,则会执行钩子函数

当uxSchedulerSuspended大于0,也就是不为pdFalse,只会进行操作4

同样,挂起调度器,也会从源头上停止任务切换,也就是在PendSV中断服务函数中进行判断。PendSV中断服务函数中调用了vTaskSwitchContext函数,当调度器挂起时,只会将xYieldPending标志置1,在调度器恢复后由xTaskIncrementTick函数判断xYieldPending是否挂起调度器期间有需要切换任务。没有挂起,就会正常进行调度。

当任务调度器被挂起的时候,就绪链表不能被访问,所以在调度器挂起期间有(某个在等待同步事件的任务等到了该事件就应该从阻塞态变成就绪态)但是调度器挂起无法挂起就绪链表则先将任务挂起到xPendingReadyList链表中等到调度器恢复时,再将xPendingReadyList链表任务一一添加到就绪链表中。

总结:挂起调度器会发生什么?

  1. 在systick中断中,禁止发生任务调度,xTickCount不会增加,也不会去判断是否有任务的延时时间到期,不会置PendSV中断标志位,只会执行tick的钩子函数。
  2. 在PendSV中断中(可能由其他中断触发),也不会发生任务调度,只是置一个标志位表示在调度器挂起时有任务需要切换
  3. 如果在调度器挂起阶段有任务想要切换为就绪态,不会切换,而是挂在另一条链表中(xPendingReadyList链表)
恢复调度器xTaskResumeAll

要恢复调度器就需要调用 xTaskResumeAll()函数,调用vTaskSuspendAll()就要调用多少次挂起调度器就要调用多少次xTaskResumeAll()才能恢复调度器

在这里插入图片描述

总结:恢复调度器会发生什么?

  1. 将在调度器挂起期间理应恢复就绪态的任务进行恢复,变为就绪态
  2. 将在调度器挂起期间,延时时间到期的任务进行恢复,并更新正确的xTickCount
  3. 如果支持抢占,并且有任务变为就绪态,那么在调度器恢复后立即发起调度

四、任务调度

任务的阻塞态与就绪态的相互转化,支持时间片,实现延时链表

1. 多优先级与时间片轮转

不同优先级任务的管理

中断会有优先级之分,优先级高的可以抢占优先级低的,任务同样如此,不过再怎么说任务说白了只是我们写的一个函数,最高优先级的任务还是会被最低优先级的中断打断

FreeRTOS管理不同优先级任务的做法很简单,创建完任务之后,会根据任务的优先级去将任务挂入不同优先级的链表,而这些链表其实就是一个链表数组,数组的每个元素是一个链表,而数组元素的下标对应的就是任务的优先级。最多有56个任务优先级

在程序中如下表示:

#define configMAX_PRIORITIES                     ( 56 )
static List_t pxReadyTasksLists[ configMAX_PRIORITIES ];

任务在创建的时候,会根据任务的优先级将任务插入到就绪链表数组不同的位置(数组下标对应任务的优先级)。相同优先级的任务插入到就绪链表数组里面的同一条链表中。

寻找最高优先级任务

在切换任务时需要去就绪链表(就绪链表数组里面),寻找最高优先级的任务去执行。
任务切换过程调用vTaskSwitchContext函数将这个任务控制块(TCB)的指针pxCurrenTCB去指向下一个任务然后切换到下一个任务,我们只需要将pxCurrenTCB去指向最高优先级的任务即可。

如何寻找到最高优先级的就绪任务的 TCB。FreeRTOS 提供了两套方法,一套是通用的,一套是根据特定的处理器优化过的。

通用方法

在这里插入图片描述

  • taskRECORD_READY_PRIORITY宏函数:用于创建任务,在添加任务至就绪链表之前去更新uxTopReadyPriority的值,确保任务创建完毕之后,uxTopReadyPriority的值表示是这些创建的任务里面最高优先级。(uxTopReadyPriority是一个task.c中定义的静态全局变量)
  • **taskSELECT_HIGHEST_PRIORITY_TASK宏函数:**从最高优先级对应的就绪列表数组下标开始寻找当前链表下是否有任务存在,如果没有,则 uxTopPriority 减一操作,继续寻找下一个优先级对应的链表中是否有任务存在,如果有则跳出 while 循环,表示找到了最高优先级对应的链表(链表中可能会有很多相同优先级的任务),然后链表中取出一个任务执行。由于任务可能会发生阻塞,因此需要从uxTopReadyPriority变量开始向下遍历,找到此时处于就绪态的任务的最高优先级,并更新uxTopReadyPriority变量,同理,在任务恢复时,如果该任务优先级高于uxTopReadyPriority,也会进行更新。
  • listGET_OWNER_OF_NEXT_ENTRY宏函数是实现时间片轮转的重要宏函数,比如当前就绪链表中最高优先级的链表中有3个任务分别为task1、task2、task3,那么当task1被选中作为pxTCB后,下一次pxIndex递增,返回task2,再下一次同理递增pxIndex返回task3。

在这里插入图片描述

  • 在任务切换过程中会调用vTaskSwitchContext()函数,vTaskSwitchContext()函数里面再调用taskSELECT_HIGHEST_PRIORITY_TASK()寻找到优先级最高的就绪任务的 TCB,然后更新到 pxCurrentTCB,然后切换任务(切换到pxCurrentTCB指向的任务)
优化方法

寻找最高优先级的任务,首先需要找到最高优先级的链表(就绪链表的下标),所以说寻找最高优先级的任务的本质是去寻找最高优先级(链表的下标)

优化的方法,这得益于于 Cortex-M 内核有一个计算前导零的指令CLZ,所谓前导零就是计算一个变量(Cortex-M 内核单片机的变量为 32 位)从高位开始第一次出现 1 的位的前面的零的个数。

比如:一个 32 位的变量 uxTopReadyPriority,其位 0、位 24 和 位 25 均 置 1 ,其余位为 0 , 那么使用前导零指令 __CLZ (uxTopReadyPriority)可以很快的计算出 uxTopReadyPriority 的前导零的个数为 6
现在uxTopReadyPriority 是一个32位的变量,uxTopReadyPriority变量的每个位对应的是就绪链表的下标(也就是任务的优先级),0~ 31位对应0~31优先级,任务就绪时,则将对应的位置 1(代表该优先级的链表不为空),如果有任务阻塞时需要判断该优先级的链表是否为空,如果为空则将对应的位清零。

// Middlewares\Third_Party\FreeRTOS\Source\portable\RVDS\ARM_CM4F\portmacro.h
#define portRECORD_READY_PRIORITY( uxPriority, uxReadyPriorities ) ( uxReadyPriorities ) |= ( 1UL << ( uxPriority ) )

// 置uxPriority位为1
#define portRESET_READY_PRIORITY( uxPriority, uxReadyPriorities ) ( uxReadyPriorities ) &= ~( 1UL << ( uxPriority ) )

// 置uxPriority位为0
#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )

在这里插入图片描述

  • taskRECORD_READY_PRIORITY宏函数:找到就绪链表中任务的最高优先级
  • taskSELECT_HIGHEST_PRIORITY_TASK宏函数:寻找就绪链表中最高优先级的任务
  • taskRESET_READY_PRIORITY宏函数:更新位图
总结对比两种方式

通用方法其实就是从高优先级向低优先级遍历链表数组,找到就绪任务的最高优先级,而通用方法就是通过位图的方式,根据一个全局变量的前导0数量计算得到就绪任务中任务的最高优先级。

通用方式,—配置宏定义configUSE_PORT_OPTIMISED_TASK_SELECTION 为 0:
优点:
1.在所有平台中都可以使用通用方式,因为支持C语言就可以了。
2.可用的优先级数量不限制,最大为255个优先级
缺点:
1.由纯C语言编写,比优化方式效率低。

优化方式,—配置宏定义 configUSE_PORT_OPTIMISED_TASK_SELECTION 为为 1:

优点:
1.有些平台架构有专用的汇编指令,比如 CLZ(Count Leading Zeros)指令,通过这些指令可以加速算法执行速度。比通用方式高效。
缺点
1.部分平台支持,需要支持CLZ指令
2.有最大优先级数限制,通常限制为 32 个(优先级0 ~ 31)因为一般是32位的平台,变量最大为32位。

总结:通用方法的优先级数量比优化方式多,平台兼容性好,但是比优化方式的效率要低。

时间片轮转

所谓时间片就是同一个优先级链表中可以有多个任务(每个任务的优先级相同),每个任务轮流地享有相同的 CPU 时间,享有 CPU 的时间我们叫时间片。

在 RTOS 中,最小的时间单位为一个 tick,即 SysTick 的中断周期,也就是说FreeRTOS所有任务最多只能执行一个tick的时间就要发生一次调度,不过可以指定一个tick的大小(改变SysTick 的中断周期就行了),

在FreeRTOS 时间片的本质就是:相同优先级的任务轮流执行一个tick的时间

2. 任务的切换

  • SySTick中断:主要用于操作系统的时钟节拍,提供周期性的时间基准,用于调度任务、超时等定时操作。

  • PendSV中断:主要用于执行上下文切换,即在不同任务之间进行切换。PendSV的设计目的是为了尽可能地延迟上下文切换,直到所有其他更高优先级的中断都已处理完毕。

  • SVC中断:则主要是提供从用户模式到内核模式的切换机制,允许执行受保护的操作或系统调用。(中断处理函数为vPortSVCHandler函数)

在FreeRTOS中第一个任务的调用就是用的SVC,后面就一直是使用PendSV做上下文切换

SVC中断

SVC(Supervisor Call,监督调用)中断是一种特殊的中断,用于ARM架构中从用户模式切换到内核模式,执行一些特权操作。在操作系统环境中,SVC中断可以用来实现系统调用,即允许用户空间的应用程序请求操作系统的核心服务。

SVC的异常类型为11,且优先级可编程。

为什么操作系统需要SVC异常?

  1. 对于需要高可靠性的系统,应用任务可以运行在非特权访问等级,而且有些硬件资源可被设置为只支持特权访问(利用MPU),应用任务只能通过OS的服务访问这些受保护的硬件资源。按照这种方式,由于应用任务无法获得关键硬件的访问权限,嵌人式系统会更加健壮和安全。
    (操作系统不让用户程序直接访问硬件,而是通过提供一些系统服务函数,用户程序使用 SVC 发出对系统服务函数的呼叫请求,以这种方法调用它们来间接访问硬件。)

  2. 它使用户程序无需在特权级下执行,用户程序无需承担因误操作而瘫痪整个系统的风险。

  3. 通过 SVC 的机制,还让用户程序变得与硬件无关,因此在开发应用程序时无需了解硬件的操作细节,从而简化了开发的难度和繁琐度,并且使应用程序跨硬件平台移植成为可能。

  4. 开发应用程序唯一需要知道的就是操作系统提供的应用编程接口(API),并且了解各个请求代号和参数表,然后就可以使用 SVC 来提出要求了(事实上,为使用方便,操作系统往往会提供一层封皮,以使系统调用的形式看起来和普通的函数调用一致,各封皮函数会正确使用 SVC指令来执行系统调用)
    在这里插入图片描述

SVC异常由SVC指令产生,该指令需要一个立即数,这也是参数传递的一种方式。SVC异常处理可以提取出参数并确定它需要执行的动作。

从启动源码简析中我们可以知道系统在启动调度器函数vTaskStartSchedulerp最后运行到 rvPortStartFirstTask中会调用SVC并启动第一个任务。(从“启动调度器那一节可以看到SVC中断的使用”)

SySTick中断

在Cortex-M系列中 systick是作为FreeRTOS 的心跳时钟,是调度器的核心。它给FreeRTOS系统提供时钟,任务的切换即每个任务运行的时间是由SysTick定时器提供的。

当调用了开启任务调度函数vTaskStartScheduler()后里面就会调用vPortSetupTimerInterrupt函数完成SysTick的配置。

#if ( configOVERRIDE_DEFAULT_TICK_CONFIGURATION == 0 ) /*条件编译*/
 
    __weak void vPortSetupTimerInterrupt( void )
 
    {
        /* Calculate the constants required to configure the tick interrupt. */
 
        #if ( configUSE_TICKLESS_IDLE == 1 ) /*条件编译,这段不编译*/
            {
                ulTimerCountsForOneTick = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ );
                xMaximumPossibleSuppressedTicks = portMAX_24_BIT_NUMBER / ulTimerCountsForOneTick;
                ulStoppedTimerCompensation = portMISSED_COUNTS_FACTOR / ( configCPU_CLOCK_HZ / configSYSTICK_CLOCK_HZ );
            }
        #endif /* configUSE_TICKLESS_IDLE */
 
        portNVIC_SYSTICK_CTRL_REG = 0UL;            /*清空控制及状态寄存器*/
        portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;   /*清空当前值寄存器*/
 
        /*设置重装载数值寄存器数值*/
        /*16000000/1000-1 = 15999 */
        portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
        /*设置控制及状态寄存器*/                    
        portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );
                            /*( 1UL << 2UL ) | ( 1UL << 1UL ) | ( 1UL << 0UL ) */
                            /*选择处理器时钟、开定时器中断、使能定时器*/
 
    }
 
#endif /* configOVERRIDE_DEFAULT_TICK_CONFIGURATION */
#define configTICK_RATE_HZ                       ((TickType_t)1000)

根据configTICK_RATE_HZ可知,每个tick是1ms

在systick中断处理函数xPortSysTickHandler中调用xTaskIncrementTick函数来判断是否要发起调度(即是否要悬起PendSV中断标志位)

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();
	{
		/* Increment the RTOS tick. */
		if( xTaskIncrementTick() != pdFALSE )
		{
			/* A context switch is required.  Context switching is performed in
			the PendSV interrupt.  Pend the PendSV interrupt. */
			portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
		}
	}
	vPortClearBASEPRIFromISR();
}

xTaskIncrementTick函数在前面“延时到期后的任务恢复”中详细讲过,就不再赘述。

PendSV中断

PendSV(可挂起的系统调用)异常对OS操作非常重要,可以写人中断控制和状态寄存器(ICSR)设置挂起位以触发PendSV异常,与SVC异常不同,它是不精确的。因此它的挂起状态可在更高优先级异常处理内设置,且会在高优先级处理完成后执行。

利用该特性,若将PendSV设置为最低的异常优先级(任何中断都可以打断),可以让PendSV异常处理在所有其他中断处理任务完成后执行,这对于上下文切换非常有用,也是各种OS设计中的关键。

为什么不直接在SysTick中切换任务?
首先来看一下上下文切换的几个基本概念。在具有嵌人式OS的典型系统中,处理时间被划分为了多个时间片。若系统中只有两个任务,这两个任务会交替执行。

在这里插入图片描述

上图OS内核的执行由SysTick异常触发,每次它都会决定切换到一个不同的任务。

直接使用SysTick切换任务带来的问题?

  1. 会导致其他中断(紧急的实物)延迟处理。
    若中断请求(IRQ)在SysTick异常前产生,则SysTick异常可能会抢占IRQ处理。在这种情况下,如果去切换任务(会占用一定时间),则IRQ(紧急的中断事物)处理就会被延迟。

在这里插入图片描述

  1. 在SysTick中断内部取切换任务,又恰好是打断了其他的中断,则SysTick中断会尝试切换为线程模式,但是存在活跃中断服务,则使用错误异常会被触发。

早期的 OS 大多会检测当前是否有中断在活跃中,只有没有任何中断需要响应时,才执行上下文切换(切换期间无法响应中断)。然而,这种方法的弊端在于,它可以把任务切换动作拖延很久(因为如果抢占了 IRQ,则本次 SysTick 在执行后不得作上下文切换,只能等待下一次 SysTick 异常),尤其是当某中断源的频率和 SysTick 异常的频率比较接近时,会发生“共振”(迟迟切换不了任务)。

而PendSV 完美解决这个问题了。PendSV 异常会自动延迟上下文切换的请求,直到其它所有的 ISR 都完成了处理后才放行。为实现这个机制,需要把 PendSV 编程为最低优先级(这个最低才是真正的最低)的异常。如果 OS 检测到某 IRQ 正在活动并且被 SysTick 抢占,它将悬起一个 PendSV 异常,以便缓期执行上下文切换。

在这里插入图片描述

  1. 任务 A 呼叫 SVC 来请求任务切换(例如,等待某些工作完成)
  2. OS 接收到请求,做好上下文切换的准备,并且 pend 一个 PendSV 异常。
  3. 当 CPU 退出 SVC 后,它立即进入 PendSV,从而执行上下文切换。
  4. 当 PendSV 执行完毕后,将返回到任务 B,同时进入线程模式。
  5. 发生了一个中断,并且中断服务程序开始执行
  6. 在 ISR 执行过程中,发生 SysTick 异常,并且抢占了该 ISR。
  7. OS 执行必要的操作,然后 pend 起 PendSV 异常以作好上下文切换的准备。
  8. 当 SysTick 退出后,回到先前被抢占的 ISR 中,ISR 继续执行
  9. ISR 执行完毕并退出后,PendSV 服务例程开始执行,并且在里面执行上下文切换
  10. 当 PendSV 执行完毕后,回到任务 A,同时系统再次进入线程模式。
#define taskYIELD()					portYIELD()

任务切换函数taskYIELD,portYIELD。

任务切换其实是由PendSV中断来完成的,实际就是将 PendSV 的悬起位置 1,当没有其它中断运行的时候响应 PendSV 中断,去执行我们写好的 PendSV 中断服务函数(因为PendSV中断优先级最低,是为了不影响其它中断(紧急的事情)的响应),在PendSV实现任务切换。

在这里插入图片描述

悬起PendSV中断后,等没有中断运行的时候,PendSV中断服务函数开始执行开始切换任务。

任务切换的流程可以根据下图进行理解:

在这里插入图片描述

具体的PendSV中断处理函数

在这里插入图片描述

相关推荐

  1. FreeRTOS任务调度机制学习】

    2024-04-24 02:46:03       21 阅读
  2. 单片机Freertos入门(二)任务调度的介绍

    2024-04-24 02:46:03       33 阅读
  3. freertos 源码分析六 任务调度

    2024-04-24 02:46:03       38 阅读
  4. 2】STM32·FreeRTOS·任务创建和删除

    2024-04-24 02:46:03       6 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-04-24 02:46:03       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-04-24 02:46:03       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-04-24 02:46:03       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-04-24 02:46:03       20 阅读

热门阅读

  1. 挂在Avalon总线上的AD7656芯片驱动verilog程序实现

    2024-04-24 02:46:03       14 阅读
  2. 【C语言笔记】memcpy和memncpy函数的异同点

    2024-04-24 02:46:03       16 阅读
  3. 力扣经典150题解析之三十四:有效的数独

    2024-04-24 02:46:03       33 阅读
  4. 【无标题】

    2024-04-24 02:46:03       15 阅读
  5. 【Qt事件】

    2024-04-24 02:46:03       11 阅读
  6. 【前端】npm常用命令

    2024-04-24 02:46:03       11 阅读
  7. Shell+sqlldr载数卸数

    2024-04-24 02:46:03       11 阅读
  8. 让多个域名都可以访问一个wordpress网站

    2024-04-24 02:46:03       17 阅读
  9. 前后端开发的非对称机密,Token加密加盐设置

    2024-04-24 02:46:03       14 阅读
  10. 【数据分析】学习笔记day1

    2024-04-24 02:46:03       12 阅读
  11. LOD2-Unity中Shader LOD技术原理以及使用

    2024-04-24 02:46:03       17 阅读