FreeRTOS Six RTOS Task Scheduling Principle Analysis Systick, PendSV, SVC

  On the Cortex-M kernel, FreeRTOS uses the Systick timer as the heartbeat clock. Generally, the default heartbeat clock is 1ms. After entering the Systick interrupt, the kernel will enter the processing mode for processing. In the Systick interrupt processing, the system will find tasks to be executed in the ReadList ready list from high priority to low priority, and schedule them. If the state of any task changes and the state list is changed, a PendSV exception will be generated and enter PendSV exception, switch to a different task by changing the process stack pointer (PSP).

  1. For tasks with the same priority, every other Systick, the running tasks are automatically discharged to the end of the priority list (time slice scheduling)
  2. Users can also actively trigger PendSV in thread mode to switch tasks.
  3. SVC is used only once in FreeRTOS (not used in M0), which is the first time.
  4. FreeRTOS enters the critical section by configuring the BASEPRI register.

Systick

  We already know that in the Cortex-M series, Systick is the heartbeat clock of FreeRTOS and the core of the scheduler. The system is context switching in Systick. So how does he perform context switching, then we have to talk about the interrupt management of the kernel, remember a sentence, the entry of the operating system is an interrupt (it seems to be nonsense, the entry of all embedded programs is interrupt ==! )

Systick source code analysis

Systick initialization

Systick is initialized port.cin vPortSetupTimerInterruptthe function in :

/*
 * Setup the systick timer to generate the tick interrupts at the required
 * frequency.
 */
__attribute__(( 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 */

	/* 
	Stop and clear the SysTick.
	清 0,保证上电后的准确性 
	*/
	portNVIC_SYSTICK_CTRL_REG = 0UL;
	portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;
	/* 
	Configure SysTick to interrupt at the requested rate.
	portNVIC_SYSTICK_LOAD_REG  systick 装载值
	portNVIC_SYSTICK_CTRL_REG  systick 控制寄存器  配置系统时钟源,开启中断,使能
	*/
	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 );
}
/*-----------------------------------------------------------*/

Systick interrupt service function

Each beat enters a Systick interrupt. If the scheduler returns true, a pendSV exception is triggered:

/*-----------------------------------------------------------*/

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.
	进入临界区,在上面一篇文章讲过,通过配置 BASEPRI 寄存器,关闭的中断等级在 CubeMX 中设置
	 */
	portDISABLE_INTERRUPTS();
	{
    
    
		/* 
		Increment the RTOS tick. 
		操作系统调度接口
		如果调度器返回 true,触发 PendSV 异常
		*/
		if( xTaskIncrementTick() != pdFALSE )
		{
    
    
			/* 
			A context switch is required.  Context switching is performed in
			the PendSV interrupt.  Pend the PendSV interrupt. 
			往中断控制及状态寄存器 ICSR(地址:0xE000_ED04)的 bit28 写 1 挂起一次 PendSV 中断
			触发 pendSV
			*/
			portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
		}
	}
	/*
	开中断,执行 pendSV
	*/
	portENABLE_INTERRUPTS();
}

Systick task scheduling

xTaskIncrementTickThe task scheduling called in the Systick interrupt is as follows, the source code comment:

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

	/* Called by the portable layer each time a tick interrupt occurs.
	Increments the tick then checks to see if the new tick value will cause any
	tasks to be unblocked. */
	traceTASK_INCREMENT_TICK( xTickCount );
	/*
		uxSchedulerSuspended 表示内核调度器是否挂起
		pdFALSE 表示内核没有挂起
	*/
	if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
	{
    
    
		/* 
		Minor optimisation.  The tick count cannot change in this
		block. 
		tick计 数增加 1
		*/
		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();
		}

		/* 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
				{
    
    
					/* 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. 
					1、从任务延时列表中,获取第一个任务控制块
					2、延时列表,插入永远是把时间片最短的任务,放在第一个		
					3、获取任务控制块的延时时间
					*/
					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. 
		时间片处理机制
		1、获取就绪列表长度
		2、就绪列表指的是,当前任务优先级的列表
		3、如果有其他任务在就绪列表中,就开始调度
		*/
		#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_TICK_HOOK == 1 )
		{
    
    
			/* Guard against the tick hook being called when the pended tick
			count is being unwound (when the scheduler is being unlocked). */
			if( uxPendedTicks == ( UBaseType_t ) 0U )
			{
    
    
				vApplicationTickHook();
			}
			else
			{
    
    
				mtCOVERAGE_TEST_MARKER();
			}
		}
		#endif /* configUSE_TICK_HOOK */
	}
	else //内核调度器挂起了
	{
    
    
		++uxPendedTicks;//挂起的tick+1

		/* The tick hook gets called at regular intervals, even if the
		scheduler is locked. */
		#if ( configUSE_TICK_HOOK == 1 )
		{
    
    
			vApplicationTickHook();
		}
		#endif
	}
	/*
	如果是抢占模式,要开启调度
	*/
	#if ( configUSE_PREEMPTION == 1 )
	{
    
    
		if( xYieldPending != pdFALSE )
		{
    
    
			xSwitchRequired = pdTRUE;
		}
		else
		{
    
    
			mtCOVERAGE_TEST_MARKER();
		}
	}
	#endif /* configUSE_PREEMPTION */

	return xSwitchRequired;//返回调度器状态
}

Systick Priority Analysis

  Combined with the content related to interrupt management and task scheduling later, it is necessary to explain the problem of Systick priority. Let's take a look at a simple task scheduling model.
insert image description here
  In the illustration above, you can see that SysTick has the highest priority! So how does this conflict with the SysTick priority we often hear needs to be set to the lowest priority? Beginners are often confused on this issue.

  First of all, you must understand: SysTick is an interrupt, and the interrupt priority has nothing to do with the task priority. No matter what the interrupt priority is, the interrupt priority is always higher than the priority of any thread task. Then in the thread in the above figure, no matter what thread, the SysTick interrupt must be executed when the SysTick interrupt comes.

  There is also an IRQ in the above figure, which has a lower priority than SysTick. This is also possible, but in fact, in our application process, the SysTick priority is generally set to the lowest, because we do not want SysTick interrupts to interrupt the user's IRQ interrupts. So how are the SysTick interrupt priority and peripheral interrupt priority determined?

  1. SysTick belongs to kernel exception and is used SHPRx(x=1.2.3)to set its priority; peripheral interrupt belongs to ISR and is used NVIC_IPRxto set priority.
      SPRH1 - SPRH3It is a 32-bit register, which can only be accessed by bytes, and each 8 fields controls the configuration of the interrupt priority of a core peripheral. Bit 7:4 The upper four bits are effective, so it can be programmed as 0 ~ 15. If the software priority configurations are the same, the priority will be determined according to their position numbers in the interrupt vector table. The smaller the number, the higher the priority.
      For the configuration of SysTick, the default configuration of the system is 15, ( 1UL << __NVIC_PRIO_BITS) - 1UL) __NVIC_PRIO_BITSis :

    /*
    core_cm0/3/4.h 中关于 SysTick_Config
     m3 m4 中是 4 ,4 位就是 0 ~ 15
      #define __NVIC_PRIO_BITS          4U
     m0 中是 2, 2 位就是 0 ~ 3 
      #endif
      */
    __STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks)
    {
          
          
      if ((ticks - 1UL) > SysTick_LOAD_RELOAD_Msk)
      {
          
          
        return (1UL);                                                   /* Reload value impossible */
      }
    
      SysTick->LOAD  = (uint32_t)(ticks - 1UL);                         /* set reload register */
      NVIC_SetPriority (SysTick_IRQn, (1UL << __NVIC_PRIO_BITS) - 1UL); /* set Priority for Systick Interrupt */
      SysTick->VAL   = 0UL;                                             /* Load the SysTick Counter Value */
      SysTick->CTRL  = SysTick_CTRL_CLKSOURCE_Msk |
                       SysTick_CTRL_TICKINT_Msk   |
                       SysTick_CTRL_ENABLE_Msk;                         /* Enable SysTick IRQ and SysTick Timer */
      return (0UL);                                                     /* Function successful */
    }
    
    /*
    NVIC_SetPriority 对中断分了类,分内核中断和外设中断,
    内核外设中断枚举值小于 0,普通外设 >=0。其中,SysTick_IRQn = -1。
    */
    __STATIC_INLINE void NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority)
    {
          
          
      if ((int32_t)(IRQn) < 0)
      {
          
          
        SCB->SHP[(((uint32_t)(int32_t)IRQn) & 0xFUL)-4UL] = (uint8_t)((priority << (8U - __NVIC_PRIO_BITS)) & (uint32_t)0xFFUL);
      }
      else
      {
          
          
        NVIC->IP[((uint32_t)(int32_t)IRQn)]               = (uint8_t)((priority << (8U - __NVIC_PRIO_BITS)) & (uint32_t)0xFFUL);
      }
    }
    
  2. The NVIC's interrupt priority grouping is not only valid for on-chip peripherals, but also for core peripherals.
      The priority of Systick 15 converted into binary value is 1111, and because the priority group of NVIC is 2, then the first two digits of 11 are 3, 3 preemption, and the last two digits of 11 are also 3, 3 sub-priority. This can be compared with the priority of the peripheral.
      If the priority of peripheral interrupts is also divided into 15, no matter how they are grouped, the priority of SYSTICK is higher than that of peripherals with the same priority (after all, the priority of kernel exceptions is higher than that of peripheral interrupts, because the position number in the interrupt vector table depends on the core formerly smaller).

  3. The method of setting Systick priority NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 15); that is, SCB->SHP[11] = 0x00;setting the highest value can get precise delay, but it will frequently interrupt the interrupt program used by the user, so it is not recommended.

kernel interrupt management

  Interrupts are sent from the outside of the microprocessor and sent into the processor through the interrupt channel, usually caused by hardware; and exceptions usually occur inside the microprocessor, mostly caused by software, such as division error exceptions and privilege call exceptions.

Exception types for Cortex-M

As shown below:
insert image description here

Cortex-M registers

As shown in the figure below:
insert image description here
This figure mainly remembers the R13 register, which has two pointers: MSP (main stack pointer) and PSP (process stack pointer), and the relevant instructions are as follows:
insert image description here

Cortex-M special registers

As shown below:
insert image description here

xPSR

Combined program status register, which consists of three program status registers:
insert image description here

  • Applied PSR (APSR): Contains condition flags after the execution of the previous instruction
  • Interrupt PSR (IPSR): Contains the exception number of the current ISR
  • Executing PSR (EPSR): Contains Thumb status bits

PRIMSK

PRIMSK: interrupt mask special register. With PRIMSK, all exceptions except HardFault and NMI can be suppressed.
insert image description here

THEY ARE PRIVATE

Use the BASEPRI register to selectively mask exceptions or interrupts below a certain priority level. (The register used to enter the critical section in the previous blog post is this register)
insert image description here

CONTROL

CONTROL: Control register, partly introduced as follows:
insert image description here

Cortex-M working mode

Cortex-M has two working modes and two working states

  • model
    • Thread Mode: After the chip is reset, it enters the thread mode and executes the user program;
    • Handler Mode: When an exception or interrupt occurs in the processor, it enters the Handler Mode and returns to the Thread Mode after processing.
  • state:
    • Thumb state: The state of the processor during normal operation
    • Debug state: The state of the processor when debugging the program

insert image description here
  After entering the Systick, if an exception occurs, it will enter the processing mode for processing. If it is bare-metal programming, you can return to wherever you enter. But if the operating system is used, where should it be returned? So here it is necessary to explain MSP and PSP separately

shadow stack pointer

  In the above Cortex-M register diagram we marked R13the register : stack pointer SP. In processing mode, only the main stack (MSP) can be used. In thread mode, either the main stack or the process stack can be used. Controlled by CONTROLregisters , as follows:
insert image description here
insert image description here

PendSV and SVC exceptions

  The PendSV exception is used for task switching. In order to ensure the real-time performance of the operating system, in addition to using the time slice scheduling of Systick, it is necessary to add PendSV exception and preemptive scheduling.

  PendSV (suspendable system call), exception number 14, programmable. The PendSV exception can be triggered by writing to the Interrupt Control and Status Register (ICSR) to set the pending bit. It is imprecise. Therefore, its pending state can be set within a higher priority exception handler, and it will be executed after the higher priority handler completes.

Why is the PendSV exception needed?

  As shown in the figure below, if the interrupt request is generated before the Systick is abnormal, the Systick may preempt the IRQ processing (the IRQ priority in the figure is lower than that of the Systick). Executing context switching in this way will cause IRQ delay processing. This behavior is intolerable in any real-time operating system. In Cortex-M3, if the OS tries to switch to thread mode while it is active, a Fault exception will be triggered.
insert image description here
  To solve the above problem, the PendSV exception is used. The PendSV exception automatically delays the context switch request until all other ISRs have finished processing. To implement this mechanism, PendSV needs to be programmed as the lowest priority exception.

  In FreeRTOS, every time it enters the Systick interrupt, the system will detect whether there is a new ready task to run, and if so, hang the PendSV exception to delay the execution of the context switch.
insert image description here
A PendSV exception will be suspended in the Systick for context switching. Every time a Systick is generated, the system will trigger a PendSV when the system detects a change in the task list, as shown in the figure below:
insert image description here

PendSV business process

  During the interrupt process, not only the common C function calls (R0-R3, R12, LR, PSR) should be saved, but also the interrupt return address (return address) should be saved. The hardware mechanism of the interrupt will EXC_RETURNput in LRand trigger the interrupt to return when the interrupt returns.
insert image description here

How to trigger PendSV exception

  Trigger a PendSV exception, write 1 to the PendSV interrupt register, and trigger a PendSV exception. Users can actively call portYIELDthe function to switch tasks, portYIELDthe function is as follows:

/* Scheduler utilities. */
#define portYIELD() 															\
{
      
      																				\
	/* Set a PendSV to request a context switch. */								\
	portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;								\
																				\
	/* Barriers are normally not required but do ensure the code is completely	\
	within the specified behaviour for the architecture. */						\
	__asm volatile( "dsb" ::: "memory" );										\
	__asm volatile( "isb" );													\
}

Brief analysis of PendSV source code

PendSV interrupt service function

I am using the source code xPortPendSVHandler of FreeRTOS in the M0 kernel:

void xPortPendSVHandler( void )
{
    
    
	/* This is a naked function.
		1.产生PendSV中断,硬件自动保存栈帧到任务A的栈中
		2.读取当前任务A的栈指针PSP,手动把一些寄存器压栈到当前任务栈。
		3.把当前任务A栈顶指针保存到任务A的任务控制块中。
		4.找到下一个任务B的任务控制块。(查找下一个优先级最高的就绪任务)
		5.把任务B控制块的栈顶指针指向的数据弹出到寄存器中
		6.更新PSP为任务B的栈顶指针。
		7.跳出PendSV中断。
		8.硬件自动弹出任务B栈中的栈帧。
 */
	__asm volatile
	(
	"	.syntax unified						\n" 
	"	mrs r0, psp							\n"/*将psp值放到r0,此时sp得值为msp*/
	"										\n"
	"	ldr	r3, pxCurrentTCBConst			\n" /* Get the location of the current TCB. 获取当前任务控制块,其实就获取任务栈顶 */
	"	ldr	r2, [r3]						\n"/*将r3寄存器值作为指针取内容存到r2,此时r2保存的为任务控制块首地址*/
	"										\n"
	"	subs r0, r0, #32					\n" /* Make space for the remaining low registers. */
	"	str r0, [r2]						\n" /* Save the new top of stack. */
	"	stmia r0!, {r4-r7}					\n" /* Store the low registers that are not saved automatically. */
	" 	mov r4, r8							\n" /* Store the high registers. */
	" 	mov r5, r9							\n"
	" 	mov r6, r10							\n"
	" 	mov r7, r11							\n"
	" 	stmia r0!, {r4-r7}					\n"
	"										\n"
	"	push {r3, r14}						\n"
	"	cpsid i								\n"
	"	bl vTaskSwitchContext				\n"/*执行上线文切换*/
	"	cpsie i								\n"
	"	pop {r2, r3}						\n" /* lr goes in r3. r2 now holds tcb pointer. */
	"										\n"
	"	ldr r1, [r2]						\n"
	"	ldr r0, [r1]						\n" /* The first item in pxCurrentTCB is the task top of stack. */
	"	adds r0, r0, #16					\n" /* Move to the high registers. */
	"	ldmia r0!, {r4-r7}					\n" /* Pop the high registers. */
	" 	mov r8, r4							\n"
	" 	mov r9, r5							\n"
	" 	mov r10, r6							\n"
	" 	mov r11, r7							\n"
	"										\n"
	"	msr psp, r0							\n" /* Remember the new top of stack for the task.记住新的栈顶指针 */
	"										\n"
	"	subs r0, r0, #32					\n" /* Go back for the low registers that are not automatically restored. */
	" 	ldmia r0!, {r4-r7}					\n" /* Pop low registers.  */
	"										\n"
	"	bx r3								\n"
	"										\n"
	"	.align 4							\n"
	"pxCurrentTCBConst: .word pxCurrentTCB	  "
	);
}

PendSV context switching function

  xPortPendSVHandlerThe core task of the context switch called in vTaskSwitchContextis to find the highest priority task that is currently in the ready state:

void vTaskSwitchContext( void )
{
    
    
	if( uxSchedulerSuspended != ( UBaseType_t ) pdFALSE )
	{
    
    
		/* The scheduler is currently suspended - do not allow a context
		switch. 
		标记调度器状态
		*/
		xYieldPending = pdTRUE;
	}
	else
	{
    
    
		xYieldPending = pdFALSE;
		traceTASK_SWITCHED_OUT();

		#if ( configGENERATE_RUN_TIME_STATS == 1 )
		{
    
    
		}
		#endif /* configGENERATE_RUN_TIME_STATS */

		/* 
		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();
		traceTASK_SWITCHED_IN();

		#if ( configUSE_NEWLIB_REENTRANT == 1 )
		{
    
    
		}
		#endif /* configUSE_NEWLIB_REENTRANT */
	}
}

Find the highest priority function

vTaskSwitchContextCalled in  context switch taskSELECT_HIGHEST_PRIORITY_TASK()to find the highest priority task

  • taskSELECT_HIGHEST_PRIORITY_TASK()The hardware way:
    	/*-----------------------------------------------------------*/
    	/*
    	这里注释解释
    	#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL - ( uint32_t ) ucPortCountLeadingZeros( ( uxReadyPriorities ) ) )
    
    	#define configASSERT( x ) if ((x) == 0) {taskDISABLE_INTERRUPTS(); for( ;; );} 
    
    	#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList )										\
    	{																							\
    	List_t * const pxConstList = ( pxList );		
    		// pxIndex 为上一个任务索引,下一个要执行的即pxNext											\
    		( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;							\
    		if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) )	\
    		{		
    			// 由于是环形列表,切默认有一个结束项(xListEnd),如果pxIndex刚好为最后一项,则再指向后面一项																				\
    			( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;						\
    		}																						\
    		( pxTCB ) = ( pxConstList )->pxIndex->pvOwner;											\
    	}
    	由于相同优先级的任务可能会存在多个,需要从就绪任务列表中找到位于最前面的任务
    	将其赋值给pxCurrentTCB。
    	*/
    	#define taskSELECT_HIGHEST_PRIORITY_TASK()														\
    	{
            
            																								\
    	UBaseType_t uxTopPriority;																		\
    																									\
    		/* Find the highest priority list that contains ready tasks. */								\
    		portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );								\
    		configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 );		\
    		listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );		\
    	} /* taskSELECT_HIGHEST_PRIORITY_TASK() */
    
  • taskSELECT_HIGHEST_PRIORITY_TASK()The generic way:
    /*-----------------------------------------------------------*/
    
    #define taskSELECT_HIGHEST_PRIORITY_TASK()															\
    {
            
            
    /* uxTopReadyPriority 在每次把任务添加到就绪列表的时候会更新*/ 										\
    UBaseType_t uxTopPriority = uxTopReadyPriority;														\
    																									\
    	/* Find the highest priority queue that contains ready tasks. 
    	一个优先级一个列表,查看当前最高优先级就绪列表下是否有任务 
    	*/																								\
    	while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) )							\
    	{
          
          																								\
    		configASSERT( uxTopPriority );																\
    		/* 如果当前最高优先级就绪列表没任务就查看下一个优先级列表 */ 									\
    		--uxTopPriority;																			\
    	}																								\
    																									\
    	/* listGET_OWNER_OF_NEXT_ENTRY indexes through the list, so the tasks of						\
    	the	same priority get an equal share of the processor time. 
    	 获取下一个优先级最高任务的任务控制块*/															\
    	listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );			\
    	uxTopReadyPriority = uxTopPriority;																\
    } /* taskSELECT_HIGHEST_PRIORITY_TASK */
    

SVC exception

  SVC (request management call), exception number 11, programmable. The interrupt generated by SVC must be responded immediately, otherwise a hard fault will be triggered. System calls handle exceptions, and users interact with the kernel. When users want to do some kernel-related functions, they must pass SVC exceptions to put the kernel in exception mode before calling and executing the source code of the kernel. When an SVC exception is triggered, the SVC exception code will be executed immediately.

  In the following brief analysis of the startup source code, we can know that the system will call SVC and start the first task when the startup scheduler function vTaskStartSchedulerp finally runs to rvPortStartFirstTask. Why start the first task with SVC? Because the OS is used, the tasks are handed over to the kernel. It is impossible to start a task like a bare metal call to a normal function. M4 only triggers SVC abnormality when it is powered on, and starts the first task during SVC abnormality, which only runs once when it is powered on, but not on M0.
insert image description here

Brief analysis of SVC source code

M0 is useless, so I specially generated an M4 to see the source code vPortSVCHandler:

void vPortSVCHandler( void )
{
    
    
	__asm volatile (
					/* 获取当前任务控制块. 
					任务控制块的第一成员是------任务的栈顶
					获取到栈顶之后,剩下的事就是出栈工作
					出栈--------任务的堆栈
					*/
					"	ldr	r3, pxCurrentTCBConst2		\n" /* Restore the context. */
					"	ldr r1, [r3]					\n" /* Use pxCurrentTCBConst to get the pxCurrentTCB address. */
					"	ldr r0, [r1]					\n" /* The first item in pxCurrentTCB is the task top of stack. */
					"	ldmia r0!, {r4-r11, r14}		\n" /* Pop the registers that are not automatically saved on exception entry and the critical nesting count. 出栈内核寄存器 R14其实就是异常返回值
					表示异常退出后,使用PSP*/
					"	msr psp, r0						\n" /* Restore the task stack pointer.更新栈指针到PSP */
					"	isb								\n"
					"	mov r0, #0 						\n"
					"	msr	basepri, r0					\n"/* 把basepri赋值为0,打开屏蔽中断 */
					"	bx r14							\n"
					"									\n"
					"	.align 4						\n"
					"pxCurrentTCBConst2: .word pxCurrentTCB				\n"
				);
				/*
				为什么没有恢复其他寄存器????其他在出栈的时候就会自动恢复(由硬件处理)
				最终跳转到任务的执行函数里面
				*/
}

A brief analysis of FreeRTOS multitasking startup source code

osKernelStart() is called by the main function in main.c;

osStatus osKernelStart (void)
{
    
    
  vTaskStartScheduler();
  
  return osOK;
}

vTaskStartScheduler

Create an idle task and start the task scheduler vTaskStartScheduler:

void vTaskStartScheduler( void )
{
    
    
BaseType_t xReturn;

	/* 
	Add the idle task at the lowest priority. 
	下面部分根据配置的支持动态任何还是静态任务
	创建一个优先级最低的空闲任务
	*/
	#if( configSUPPORT_STATIC_ALLOCATION == 1 )
	{
    
    
		StaticTask_t *pxIdleTaskTCBBuffer = NULL;
		StackType_t *pxIdleTaskStackBuffer = NULL;
		uint32_t ulIdleTaskStackSize;

		/* The Idle task is created using user provided RAM - obtain the
		address of the RAM then create the idle task. */
		vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, &pxIdleTaskStackBuffer, &ulIdleTaskStackSize );
		xIdleTaskHandle = xTaskCreateStatic(	prvIdleTask,
												configIDLE_TASK_NAME,
												ulIdleTaskStackSize,
												( void * ) NULL, /*lint !e961.  The cast is not redundant for all compilers. */
												portPRIVILEGE_BIT, /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */
												pxIdleTaskStackBuffer,
												pxIdleTaskTCBBuffer ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */

		if( xIdleTaskHandle != NULL )
		{
    
    
			xReturn = pdPASS;
		}
		else
		{
    
    
			xReturn = pdFAIL;
		}
	}
	#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 ( configUSE_TIMERS == 1 )
	{
    
    
		if( xReturn == pdPASS )
		{
    
    
			xReturn = xTimerCreateTimerTask();
		}
		else
		{
    
    
			mtCOVERAGE_TEST_MARKER();
		}
	}
	#endif /* configUSE_TIMERS */

	if( xReturn == pdPASS )
	{
    
    
		/* freertos_tasks_c_additions_init() should only be called if the user
		definable macro FREERTOS_TASKS_C_ADDITIONS_INIT() is defined, as that is
		the only macro called by the function. */
		#ifdef FREERTOS_TASKS_C_ADDITIONS_INIT
		{
    
    
			freertos_tasks_c_additions_init();
		}
		#endif

		/* 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. 
		此处关闭中断,以确保不会发生滴答声
        在呼叫 xPortstarts 计划之前或期间 ()。 堆栈
        创建的任务包含打开中断的状态字
        因此,当第一个任务完成时,中断会自动重新启用
        开始运行。
		*/
		portDISABLE_INTERRUPTS();

		#if ( configUSE_NEWLIB_REENTRANT == 1 )
		{
    
    
			/* Switch Newlib's _impure_ptr variable to point to the _reent
			structure specific to the task that will run first. */
			_impure_ptr = &( pxCurrentTCB->xNewLib_reent );
		}
		#endif /* configUSE_NEWLIB_REENTRANT */

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

		/* If configGENERATE_RUN_TIME_STATS is defined then the following
		macro must be defined to configure the timer/counter used to generate
		the run time counter time base.   NOTE:  If configGENERATE_RUN_TIME_STATS
		is set to 0 and the following line fails to build then ensure you do not
		have portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() defined in your
		FreeRTOSConfig.h file. */
		portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();

		traceTASK_SWITCHED_IN();

		/* 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
	{
    
    
		/* This line will only be reached if the kernel could not be started,
		because there was not enough FreeRTOS heap to create the idle task
		or the timer task. 
		只有在无法启动内核时才能到达此行,
        因为没有足够的自由 BrTAS 堆来创建空闲任务
        或时间器任务。
		*/
		configASSERT( xReturn != errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY );
	}

	/* Prevent compiler warnings if INCLUDE_xTaskGetIdleTaskHandle is set to 0,
	meaning xIdleTaskHandle is not used anywhere else. */
	( void ) xIdleTaskHandle;
}

xPortStartScheduler

Start the scheduler xPortStartScheduler:

/*
 * See header file for description.
 */
BaseType_t xPortStartScheduler( void )
{
    
    
	/*
	前面一大段不用深入了解,先不看,我们找到配置 systick pendsv开始 
	*/
	/* configMAX_SYSCALL_INTERRUPT_PRIORITY must not be set to 0.
	See http://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html */
	configASSERT( configMAX_SYSCALL_INTERRUPT_PRIORITY );

	/* This port can be used on all revisions of the Cortex-M7 core other than
	the r0p1 parts.  r0p1 parts should use the port from the
	/source/portable/GCC/ARM_CM7/r0p1 directory. */
	configASSERT( portCPUID != portCORTEX_M7_r0p1_ID );
	configASSERT( portCPUID != portCORTEX_M7_r0p0_ID );

	#if( configASSERT_DEFINED == 1 )
	{
    
    
		volatile uint32_t ulOriginalPriority;
		volatile uint8_t * const pucFirstUserPriorityRegister = ( volatile uint8_t * const ) ( portNVIC_IP_REGISTERS_OFFSET_16 + portFIRST_USER_INTERRUPT_NUMBER );
		volatile uint8_t ucMaxPriorityValue;

		/* Determine the maximum priority from which ISR safe FreeRTOS API
		functions can be called.  ISR safe functions are those that end in
		"FromISR".  FreeRTOS maintains separate thread and ISR API functions to
		ensure interrupt entry is as fast and simple as possible.

		Save the interrupt priority value that is about to be clobbered. */
		ulOriginalPriority = *pucFirstUserPriorityRegister;

		/* Determine the number of priority bits available.  First write to all
		possible bits. */
		*pucFirstUserPriorityRegister = portMAX_8_BIT_VALUE;

		/* Read the value back to see how many bits stuck. */
		ucMaxPriorityValue = *pucFirstUserPriorityRegister;

		/* Use the same mask on the maximum system call priority. */
		ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;

		/* Calculate the maximum acceptable priority group value for the number
		of bits read back. */
		ulMaxPRIGROUPValue = portMAX_PRIGROUP_BITS;
		while( ( ucMaxPriorityValue & portTOP_BIT_OF_BYTE ) == portTOP_BIT_OF_BYTE )
		{
    
    
			ulMaxPRIGROUPValue--;
			ucMaxPriorityValue <<= ( uint8_t ) 0x01;
		}

		#ifdef __NVIC_PRIO_BITS
		{
    
    
			/* Check the CMSIS configuration that defines the number of
			priority bits matches the number of priority bits actually queried
			from the hardware. */
			configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == __NVIC_PRIO_BITS );
		}
		#endif

		#ifdef configPRIO_BITS
		{
    
    
			/* Check the FreeRTOS configuration that defines the number of
			priority bits matches the number of priority bits actually queried
			from the hardware. */
			configASSERT( ( portMAX_PRIGROUP_BITS - ulMaxPRIGROUPValue ) == configPRIO_BITS );
		}
		#endif

		/* Shift the priority group value back to its position within the AIRCR
		register. */
		ulMaxPRIGROUPValue <<= portPRIGROUP_SHIFT;
		ulMaxPRIGROUPValue &= portPRIORITY_GROUP_MASK;

		/* Restore the clobbered interrupt priority register to its original
		value. */
		*pucFirstUserPriorityRegister = ulOriginalPriority;
	}
	#endif /* conifgASSERT_DEFINED */

	/* 
	Make PendSV and SysTick the lowest priority interrupts. 
	配置 systick pendsv为最低的优先级,为了保证系统的实时性
	*/
	portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
	portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;

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

	/* 
	Initialise the critical nesting count ready for the first task. 
	初始化关键嵌套计数,为第一个任务做好准备。
	临界段?
	*/
	uxCriticalNesting = 0;

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

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

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

	/* Should never get here as the tasks will now be executing!  Call the task
	exit error function to prevent compiler warnings about a static function
	not being called in the case that the application writer overrides this
	functionality by defining configTASK_RETURN_ADDRESS.  Call
	vTaskSwitchContext() so link time optimisation does not remove the
	symbol. */
	vTaskSwitchContext();
	prvTaskExitError();

	/* Should not get here! */
	return 0;
}

prvPortStartFirstTask

Start the first task prvPortStartFirstTask:

static void prvPortStartFirstTask( void )
{
    
    
	/* Start the first task.  This also clears 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. */
	__asm volatile(
					/* 
						0xE000ED08 它是中断向量表的一个地址
						它存储的是MSP的指针
						最终获取到MSP的RAM的地址
					*/
					" ldr r0, =0xE000ED08 	\n" /* Use the NVIC offset register to locate the stack. */
					" ldr r0, [r0] 			\n"
					" ldr r0, [r0] 			\n"
					/* 
						Set the msp back to the start of the stack. 
						重新把MSP的地址,赋值为MSP 
						为什么需要加这一步,如果我们有在线升级功能
						使用了我们用户的Bootloder,
						中断向量表会更新,所以要重新赋值MSP
					*/
					" msr msp, r0			\n" /* Set the msp back to the start of the stack. 把MSP的地址,赋值为MSP */
					" mov r0, #0			\n" /* Clear the bit that indicates the FPU is in use, see comment above. */
					" msr control, r0		\n"
					" cpsie i				\n" /* Globally enable interrupts. 开启全局中断 */
					" cpsie f				\n"
					" dsb					\n"
					" isb					\n"
					" svc 0					\n" /* System call to start first task. 调用SVC */
					" nop					\n"
				);
}
/*-----------------------------------------------------------*/

おすすめ

転載: blog.csdn.net/ZCShouCSDN/article/details/129275246