FreeRTOS多任务调度原理(基于Cortex-M4)

目录

1.Cortex-M4中SysTick的重要性

2.Cortex-M4中的中断管理

3.Cortex-M4中影子栈指针

4.Cortex-M4中SVC和PendSV异常

5.多任务启动

6.PendSV业务流程

7.系统时钟节拍详解 

8.SysTick中断服务函数

9.SysTick任务调度


1.Cortex-M4中SysTick的重要性

2.Cortex-M4中的中断管理

        2.1.Cortex-M4 内核支持 256 个中断,其中包含了 16 个内核中断和 240 个外部中断,并且具有256 级的可编程中断优先级设置。但 STM32F4 并没有使用 Cortex-M4 内核的全部东西,而是只用了它的一部分。
        2.2.Cortex-M4处理器中,每一个外部中断都可以被使能或者禁止,并且可以被设置为挂起状态或者清除状态

        2.3.中断和异常向量表

        2.4. Cortex-M4寄存器组——特殊寄存器

                2.4.1.  xPSR:组合程序状态寄存器,该寄存器由三个程序状态寄存器组成:

                           应用PSR(APSR) :  包含前一条指令执行后的条件标志

                           中断PSR(IPSR)  :  包含当前ISR的异常编号

                           执行PSR(EPSR) :  包含Thumb状态位

                2.4.2.  PRIMSK:中断屏蔽特殊寄存器

                2.4.3.  CONTROL:控制寄存器

                           [PRIV]: 为0,处理器处于线程模式的特权级,为1为非特权级
                           [SPSEL] :  为0时,线程模式使用MSP,为1时使用PSP

                           处理器模式时,固定使用MSP

3.Cortex-M4中影子栈指针

4.Cortex-M4中SVC和PendSV异常

5.多任务启动

        5.1.创建空闲任务

        5.2.配置SysTick,PendSV为最低优先级

                优先外部中断触发,保证实时响应外部中断

        5.3.配置SysTick寄存器

        5.4.调用SVC中断

//多任务启动
osKernelStart ();---------->
  vTaskStartScheduler();---------->
    xPortStartScheduler();---------->
        prvStartFirstTask();---------->
            SVC------------------------->
            
//启动任务调度器
void vTaskStartScheduler( void )
{
BaseType_t xReturn;

	/* Add the idle task at the lowest priority. */
	#if( configSUPPORT_STATIC_ALLOCATION == 1 )
	{
	}
	#else
	{
        //动态创建一个空闲任务
		/* The Idle task is being created using dynamically allocated RAM. */
		xReturn = xTaskCreate(	prvIdleTask,
								configIDLE_TASK_NAME,
								configMINIMAL_STACK_SIZE,
								( void * ) NULL,
								portPRIVILEGE_BIT, /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */
								&xIdleTaskHandle ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
	}
	#endif /* configSUPPORT_STATIC_ALLOCATION */

	if( xReturn == pdPASS )    //如果创建成功
	{
        //关闭中断
		/* Interrupts are turned off here, to ensure a tick does not occur
		before or during the call to xPortStartScheduler().  The stacks of
		the created tasks contain a status word with interrupts switched on
		so interrupts will automatically get re-enabled when the first task
		starts to run. */
		portDISABLE_INTERRUPTS();

        //下一个任务的锁定时间赋值为最大值
        //#define portMAX_DELAY ( TickType_t ) 0xffffffffUL
        //不让时间片进行调度
		xNextTaskUnblockTime = portMAX_DELAY;
        //调度器的运行状态置位,标记开始运行了
		xSchedulerRunning = pdTRUE;
        //初始化  系统的节拍值为0
        //#define configINITIAL_TICK_COUNT 0
		xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT;

        //启动调度器
		/* Setting up the timer tick is hardware specific and thus in the
		portable interface. */
		if( xPortStartScheduler() != pdFALSE )
		{
            //因为不会执行到这里,所以没有代码
			/* Should not reach here as if the scheduler is running the
			function will not return. */
		}
		else
		{
			/* Should only reach here if a task calls xTaskEndScheduler(). */
		}
	}
	else
	{
	}

}
//启动调度器
BaseType_t xPortStartScheduler( void )
{
    //配置PendSV和SysTick为最低优先级,为了保证系统的实时性
	/* Make PendSV and SysTick the lowest priority interrupts. */
	portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
	portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;

    /*
        1.初始化systick---->配置为1ms的中断产生时基
        2.开启systick中断
    */
	/* Start the timer that generates the tick ISR.  Interrupts are disabled
	here already. */
	vPortSetupTimerInterrupt();

    //初始化临界段嵌套值
	/* Initialise the critical nesting count ready for the first task. */
	uxCriticalNesting = 0;

    //初始化浮点数寄存器,M4特有,需要初始化
	/* Ensure the VFP is enabled - it should be anyway. */
	prvEnableVFP();

	/* Lazy save always. */
	*( portFPCCR ) |= portASPEN_AND_LSPEN_BITS;

    //启动第一个任务
	/* Start the first task. */
	prvStartFirstTask();

	/* Should not get here! */
	return 0;
}
//启动第一个任务
__asm void prvStartFirstTask( void )
{
	PRESERVE8    //8字节对齐,AAPCS的标准,ARM特有

    /*
        0xE000ED08是中断向量表的一个地址,存储的是MSP的指针
        最终获取到MSP的RAM地址
    */
	/* Use the NVIC offset register to locate the stack. */
	ldr r0, =0xE000ED08
	ldr r0, [r0]
	ldr r0, [r0]

    /*
        重新把MSP的地址,赋值为MSP
        作用:
            1.如果没有在线升级的功能,可以屏蔽这段代码
            2.BSP Bootloader----可以选择程序运行的代码段
            3.这个时候,中断向量表会更新,所以要重新赋值MSP
    */
	/* Set the msp back to the start of the stack. */
	msr msp, r0
	/* Clear the bit that indicates the FPU is in use in case the FPU was used
	before the scheduler was started - which would otherwise result in the
	unnecessary leaving of space in the SVC stack for lazy saving of FPU
	registers. */
	mov r0, #0
	msr control, r0

    //开启全局中断
	/* Globally enable interrupts. */
	cpsie i
	cpsie f
	dsb
	isb
    
    //调用SVC系统
	/* Call SVC to start the first task. */
	svc 0
	nop
	nop
}
//svc异常处理
__asm void vPortSVCHandler( void )
{
	PRESERVE8    //8字节对齐

    /*
        获取当前任务控制块
            任务控制块的第一成员是  任务的栈顶
            获取到栈顶之后,剩下的事就是出栈工作
            出栈--------任务的堆栈
    */
	/* Get the location of the current TCB. */
	ldr	r3, =pxCurrentTCB
	ldr r1, [r3]
	ldr r0, [r1]

    /*
        出栈内核寄存器
            R14就是异常返回值    #define portINITIAL_EXC_RETURN		( 0xfffffffd )
            返回处理模式并在返回后使用进程栈(表示异常退出后,使用PSP)
	*/
    /* Pop the core registers. */
	ldmia r0!, {r4-r11, r14}
    
    //更新栈指针到PSP
	msr psp, r0
	isb
    
    //把basepri赋值为0,即打开屏蔽中断
	mov r0, #0
	msr	basepri, r0

    //异常退出
	bx r14

    //为什么没有恢复其他寄存器?
    //其他寄存器在出栈的时候会自动恢复,由硬件处理
    //最终跳转到任务的执行函数里面
}

6.PendSV业务流程

        6.1.读取当前PSP值获取当前任务栈顶

        6.2.保存s16-s31到栈中保存r4-r11,r14到当前栈中

        6.3.更新栈顶到当前任务控制块中保存r3到栈中关闭中断

        6.4.查找优先级最高的任务,更新当前任务控制块开启中断出栈r3值

                出栈r3,即出栈下一个任务的栈空间

        6.5.出栈r4-r11,r14到当前栈中出栈s16-s31到栈中更新栈顶到PSP调用异常返回指令

__asm void xPortPendSVHandler( void )
{
	extern uxCriticalNesting;
	extern pxCurrentTCB;
	extern vTaskSwitchContext;

	PRESERVE8

	mrs r0, psp
	isb

    //获取当前任务控制块,其实就是获取任务栈顶
	/* Get the location of the current TCB. */
	ldr	r3, =pxCurrentTCB
	ldr	r2, [r3]

    //浮点数处理,如果使能浮点数,就需要入栈
	/* Is the task using the FPU context?  If so, push high vfp registers. */
	tst r14, #0x10
	it eq
	vstmdbeq r0!, {s16-s31}

    //保存内核寄存器----调用者需要做的
	/* Save the core registers. */
	stmdb r0!, {r4-r11, r14}

    //保存当前任务栈顶,把栈顶指针入栈
	/* Save the new top of stack into the first member of the TCB. */
	str r0, [r2]

	stmdb sp!, {r0, r3}

    //使能可屏蔽的中断----临界段
	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
	msr basepri, r0
	dsb
	isb

    //执行上下文切换
	bl vTaskSwitchContext

    //使能可屏蔽的中断
	mov r0, #0
	msr basepri, r0

    //恢复任务控制块指向的栈顶
    //在上下文切换的时候,当前任务的指针,已经指向了优先级最高的
	ldmia sp!, {r0, r3}

    //获取当前栈顶
	/* The first item in pxCurrentTCB is the task top of stack. */
	ldr r1, [r3]
	ldr r0, [r1]

    //出栈
	/* Pop the core registers. */
	ldmia r0!, {r4-r11, r14}

    //出栈
	/* Is the task using the FPU context?  If so, pop the high vfp registers
	too. */
	tst r14, #0x10
	it eq
	vldmiaeq r0!, {s16-s31}

    //更新PSP指针
	msr psp, r0
	isb

    //异常返回,下面要执行的代码,就是要切换的任务代码了
	bx r14
}
//上下文切换
void vTaskSwitchContext( void )
{
	if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
	{
        //标记调度器状态
		/* The scheduler is currently suspended - do not allow a context
		switch. */
		xYieldPending = pdTRUE;
	}
	else
	{
        //标记调度器状态
		xYieldPending = pdFALSE;

        //检查任务栈是否溢出
		/* Check for stack overflow, if configured. */
		taskCHECK_FOR_STACK_OVERFLOW();

        //选择优先级最高的任务,把当前的任务控制块进行赋值
		/* Select a new task to run using either the generic C or port
		optimised asm code. */
		taskSELECT_HIGHEST_PRIORITY_TASK(); /*lint !e9079 void * is used as this macro is used with timers and co-routines too.  Alignment is known to be fine as the type of the pointer stored and retrieved is the same. */
		traceTASK_SWITCHED_IN();

	}
}

7.系统时钟节拍详解 

        7.1.配置SysTick装载值

                默认时基为1ms,不要低于1ms;可按10的倍数设置,如10ms,100ms等

        7.2.使能SysTick时钟源;使能SysTick中断;使能SysTick

//系统节拍初始化
#if( configOVERRIDE_DEFAULT_TICK_CONFIGURATION == 0 )

	__weak void vPortSetupTimerInterrupt( void )
	{

		/* Stop and clear the SysTick. */
		portNVIC_SYSTICK_CTRL_REG = 0UL;
		portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;

        /*
            1.操作系统是针对寄存器操作的,因为效率高
            2.首先赋值装载寄存器值 = (CPU频率/频率周期)-1
                2.1.CPU频率在硬件启动时获取
                2.2.configTICK_RATE_HZ是由CubeMX配置而得
            3.配置控制寄存器
                3.1.开启时钟源
                3.2.使能中断
                3.3.使能systick
            4.可以参考M4权威指南,9.5章节----systick定时器
        */
		/* Configure SysTick to interrupt at the requested rate. */
		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 );
	}

#endif /* configOVERRIDE_DEFAULT_TICK_CONFIGURATION */

8.SysTick中断服务函数

        8.1.关闭中断

                由于SysTick中断优先级最低,需要关闭中断,否则会被不停抢断

        8.2.Tick值增加SysTick任务调度启动PendSV

        8.3.开启中断

void xPortSysTickHandler( void )
{
    /*
        1.配置中断屏蔽寄存器
        2.不让中断打断systick中断服务
        3.其实就是进入临界段
    */
	/* 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();
	{
        /*
            操作系统调度接口
            如果调度器返回true,触发pendSV异常
        */
		/* Increment the RTOS tick. */
		if( xTaskIncrementTick() != pdFALSE )
		{
            //触发PendSV
			/* 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();
}

9.SysTick任务调度

        9.1.系统节拍数加1判断是否溢出(uint32)溢出更新任务锁定时间

        9.2.判断是否有任务需要解除阻塞获取延时列表第一个任务控制块(时间排序,第一个时间最短)获取状态列表值,判断时间是否到达,未到达退出

        9.3.任务阻塞事件到达从延时列表中删除从事件列表中删除添加到就绪列表

        9.4.如果使用抢占内核判断任务优先级是否大于当前任务开启任务调度

        9.5.如果使用时间片调度判断当前优先级下是否还有其他任务开启任务调度器

//SysTick任务调度
BaseType_t xTaskIncrementTick( void )
{
TCB_t * pxTCB;
TickType_t xItemValue;
//返回值,表示是否进行上下文切换
BaseType_t xSwitchRequired = pdFALSE;

    //uxSchedulerSuspended 表示内核的调度器是否挂起,pdFALSE表示内核没有挂起
	if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
	{
        //tick计数增加1
		/* Minor optimisation.  The tick count cannot change in this
		block. */
		const TickType_t xConstTickCount = xTickCount + ( TickType_t ) 1;
		/* Increment the RTOS tick, switching the delayed and overflowed
		delayed lists if it wraps to 0. */
		xTickCount = xConstTickCount;

        //判断tick是否溢出
		if( xConstTickCount == ( TickType_t ) 0U ) /*lint !e774 'if' does not always evaluate to false as it is looking for an overflow. */
		{
            //如果溢出,要更新延时列表
			taskSWITCH_DELAYED_LISTS();
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}

        /*
            1.当前节拍大于时间片的锁定时间
            2.就说明有任务需要调度了,时间片用完了
        */
		/* See if this tick has made a timeout expire.  Tasks are stored in
		the	queue in the order of their wake time - meaning once one task
		has been found whose block time has not expired there is no need to
		look any further down the list. */
		if( xConstTickCount >= xNextTaskUnblockTime )
		{
			for( ;; )    //一直遍历整个任务延时列表,主要目的是找到时间片最短的任务,进行切换
			{
                //判断任务延时列表中是否为空,即有没有任务在等待调度
				if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
				{
                    //如果没有任务等待,把时间片赋值为最大值,不再调度
					/* The delayed list is empty.  Set xNextTaskUnblockTime
					to the maximum possible value so it is extremely
					unlikely that the
					if( xTickCount >= xNextTaskUnblockTime ) test will pass
					next time through. */
					xNextTaskUnblockTime = portMAX_DELAY; /*lint !e961 MISRA exception as the casts are only redundant for some ports. */
					break;
				}
				else
				{
                    /*
                        1.从任务延时列表中,获取第一个任务控制块
                            延时列表插入永远是把时间片最短的任务,放在第一个
                        2.获取任务控制块的延时时间
                    */
					/* The delayed list is not empty, get the value of the
					item at the head of the delayed list.  This is the time
					at which the task at the head of the delayed list must
					be removed from the Blocked state. */
					pxTCB = listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList ); /*lint !e9079 void * is used as this macro is used with timers and co-routines too.  Alignment is known to be fine as the type of the pointer stored and retrieved is the same. */
					xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );

                    //再次判断,这个任务的时间片是否到达
					if( xConstTickCount < xItemValue )
					{
                        //没有到达,把此任务的时间片更新为当前系统的时间片
						/* It is not time to unblock this item yet, but the
						item value is the time at which the task at the head
						of the blocked list must be removed from the Blocked
						state -	so record the item value in
						xNextTaskUnblockTime. */
						xNextTaskUnblockTime = xItemValue;
                        //直接退出,不用调度
						break; /*lint !e9011 Code structure here is deedmed easier to understand with multiple breaks. */
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}
                
                    //把任务从延时列表中移除
					/* It is time to remove the item from the Blocked state. */
					( void ) uxListRemove( &( pxTCB->xStateListItem ) );
    
                    //把任务从事件列表中移除
					/* Is the task waiting on an event also?  If so remove
					it from the event list. */
					if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
					{
						( void ) uxListRemove( &( pxTCB->xEventListItem ) );
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}

                    //把任务添加到就绪列表中
					/* Place the unblocked task into the appropriate ready
					list. */
					prvAddTaskToReadyList( pxTCB );

                    //抢占式处理
					/* A task being unblocked cannot cause an immediate
					context switch if preemption is turned off. */
					#if (  configUSE_PREEMPTION == 1 )
					{
                        /*
                            判断优先级是否大于当前任务
                                大于则进行调度
                        */
						/* Preemption is on, but a context switch should
						only be performed if the unblocked task has a
						priority that is equal to or higher than the
						currently executing task. */
						if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
						{
							xSwitchRequired = pdTRUE;
						}
						else
						{
							mtCOVERAGE_TEST_MARKER();
						}
					}
					#endif /* configUSE_PREEMPTION */
				}
			}
		}

        //时间片处理机制
		/* Tasks of equal priority to the currently running task will share
		processing time (time slice) if preemption is on, and the application
		writer has not explicitly turned time slicing off. */
		#if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
		{
            /*
                获取就绪列表长度
                    就绪列表指的是当前任务优先级列表
                    如果有其他任务在就绪列表中,就开始调度
            */
			if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
			{
				xSwitchRequired = pdTRUE;
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		#endif /* ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) ) */

        //如果是抢占模式,要开启调度
		#if ( configUSE_PREEMPTION == 1 )
		{
			if( xYieldPending != pdFALSE )
			{
				xSwitchRequired = pdTRUE;
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		#endif /* configUSE_PREEMPTION */
	}
	else    //内核调度器挂起了
	{
        //挂起的tick+1
		++xPendedTicks;
	}

	return xSwitchRequired;
}

  • 4
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值