精华内容
下载资源
问答
  • FreeRTOS调度原理

    千次阅读 2020-03-29 21:29:22
    以个人理解,FreeRTOS内核调度的本质是利用了从异常中断返回时,切换任务栈的机制,使得进入新的任务下进行执行任务,实现内核调度功能。 内核进入第一个空闲任务分析 内核版本:FreeRTOS V9.0.0 硬件平台:STM32...

    前言

    以个人理解,FreeRTOS内核调度的本质是利用了从异常中断返回时,切换任务栈的机制,使得进入新的任务下进行执行任务,实现内核调度功能。

    内核进入第一个空闲任务分析

    • 内核版本:FreeRTOS V9.0.0
    • 硬件平台:STM32F103ZE
    • 仿真平台:MDK5.23

    启动代码分析

    // startup_stm32f10x_hd.s
    ; Reset handler
    Reset_Handler   PROC
                    EXPORT  Reset_Handler             [WEAK]
                    IMPORT  __main
                    IMPORT  SystemInit
                    LDR     R0, =SystemInit
                    BLX     R0               
                    LDR     R0, =__main
                    BX      R0
                    ENDP
    
    

    内核初始化时首先执行startup_stm32f10x_hd.s这个文件内部的内容:

    • 首先设置初始化堆栈
    • 然后设置PC指针为Reset_Handler标签
      这样在进入复位向量后,先初始化时钟系统,然后进入main函数执行应用程序代码。
    // src code
    int main(void)
    {
    	BaseType_t xReturn = pdPASS;//定义一个创建信息返回值,默认为 pdPASS 
    	BspInit();
    	printf("FreeRTOSTask\r\n");
    	printf("Please send queue message by press KEY2 or KEY_UP\n");
    	printf("ReceiveTask receive message echo in USART\n\n");
    	if(pdPASS == xReturn)
    	{
    		vTaskStartScheduler();//启动任务,开始调度
    	}		
    	else
    	{
    		return -1;
    	}
    	while(1);//正常不会执行到这里	
    }
    

    上面进入初始化bsp相关外设后,会直接进入系统调度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,
    												"IDLE",
    												ulIdleTaskStackSize,
    												( void * ) NULL,
    												( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
    												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,
    								"IDLE", 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. */
    	}
    	#endif /* configSUPPORT_STATIC_ALLOCATION */
    
    	#if ( configUSE_TIMERS == 1 )
    	{
    		if( xReturn == pdPASS )
    		{
    			xReturn = xTimerCreateTimerTask();
    		}
    		else
    		{
    			mtCOVERAGE_TEST_MARKER();
    		}
    	}
    	#endif /* configUSE_TIMERS */
    
    	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();
    
    		#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 ) 0U;
    
    		/* 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. */
    		portCONFIGURE_TIMER_FOR_RUN_TIME_STATS();
    		
    		"上面申请完空闲任务后,会开始进入系统调度,按照已有的概念,我们知道最终会进入空闲"
    		"任务中执行,如何进入这个空闲任务,是接下来我们分析xPortStartScheduler的重点:"
    		/* 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. */
    		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的重点:

    // ARM_CM3/port.c
    BaseType_t xPortStartScheduler( void )
    {
    ...
    	/* Make PendSV and SysTick the lowest priority interrupts. */
    	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. */
    	vPortSetupTimerInterrupt();
    	
    	/* Initialise the critical nesting count ready for the first task. */
    	uxCriticalNesting = 0;
    	
    	"重点会进入这个函数,开启第一个任务:"
    	/* Start the first task. */
    	prvStartFirstTask();
    
    	/* Should not get here! */
    	return 0;
    }
    
    
    __asm void prvStartFirstTask( void )
    {
    	PRESERVE8    "8字节对齐"
    
    	/* Use the NVIC offset register to locate the stack. */
    	ldr r0, =0xE000ED08    "这个地址是NVIC偏移寄存器的地址,用来定位当前的栈的"
    	ldr r0, [r0]
    	ldr r0, [r0] "执行完这句后,r0寄存器内部存放的就是当前的栈地址"
    
    	/* Set the msp back to the start of the stack. */
    	msr msp, r0  "由于CM3的双堆栈特性,初始化时,会进入主堆栈。因此将r0的值传输msp"
    	/* Globally enable interrupts. */
    	cpsie i   "强制使能所有的中断"
    	cpsie f
    	dsb "同步数据,主要是担心某些存储器写入存在缓冲机制"
    	isb
    	/* Call SVC to start the first task. */
    	svc 0   
    	nop
    	nop
    }
    

    svc 0 可以触发一个系统调用,使其接下来的pc指针强制进入svc下的异常处理回调函数:

    // ARM_CM3/port.c
    __asm void vPortSVCHandler( void )
    {
    	PRESERVE8
    	
    	"下面这一句,是将当前存储TCB变量的地址放入r3寄存器中"
    	ldr	r3, =pxCurrentTCB	/* Restore the context. */
    	"下面这一句,是将当前的TCB指向的地址取出放入r1寄存器中"
    	ldr r1, [r3]			/* Use pxCurrentTCBConst to get the pxCurrentTCB address. */
    	"下面这一句,是将TCB结构体中的第一个变量取出来放入r0寄存器中;"
    	"TCB结构体中的第一个变量也就是当前的栈顶地址"
    	ldr r0, [r1]			/* The first item in pxCurrentTCB is the task top of stack. */
    	"下面这一句,是将r0处读取多个数据,依次放入r4-r11寄存器中,每读取一次,r0自增一次"
    	"这里相当于从当前的栈顶指针出栈8个数据到r4-r11寄存器中"
    	ldmia r0!, {r4-r11}		/* Pop the registers that are not automatically saved on exception entry and the critical nesting count. */
    	"这个时候将r0处的栈顶指针放入psp中,psp属于进程堆栈,专门用来保存正在执行的任务堆栈"
    	"此时r0寄存器存放的是空闲任务携带参数的地址"
    	msr psp, r0				/* Restore the task stack pointer. */
    	"强制指令同步,数据同步"
    	isb
    	"下面这一句,是将r0寄存器清0"
    	mov r0, #0
    	"下面这一句,是将特殊寄存器basepri置0,表示开启所有的中断使能"
    	msr	basepri, r0
    	"下面这一句,逻辑或的意思,将r14寄存器内部的值与0x0d或后,再存入r14"
    	"这一步很关键,为什么要或0x0d?下面详细说明"
    	orr r14, #0xd
    	"异常返回指令,这个返回指令有两种意思:在普通函数返回时,即将使用的PC指针就是目前r14"
    	"寄存器中的值;在异常函数中返回时,r14表示一个exc_return数值,硬件系统会根据这个"
    	"值来判断下一步的堆栈、pc值"
    	bx r14
    }
    

    关于exc_return的解释:
    在这里插入图片描述

    从上图看出,使用0x0d或操作,是为了返回线程模式,并使用线程堆栈。
    那么是如何使用线程堆栈的呢?此处的使用的硬件平台是stm32f103系列,所以对应的内核为cortex-m3内核,使用的指令架构为armv7-m指令集,因此可以查阅该指令集下异常返回伪代码:

    Exception return operation
    The ExceptionReturn() pseudocode function describes the exception return operation:
    // ExceptionReturn()
    // =================
    ExceptionReturn(bits(28) EXC_RETURN)
    assert CurrentMode == Mode_Handler;
    if HaveFPExt() then
    if !IsOnes(EXC_RETURN<27:5>) then UNPREDICTABLE;
    else
    if !IsOnes(EXC_RETURN<27:4>) then UNPREDICTABLE;
    integer ReturningExceptionNumber = UInt(IPSR<8:0>);
    integer NestedActivation; // used for Handler => Thread check when value == 1
    NestedActivation = ExceptionActiveBitCount(); // Number of active exceptions
    if ExceptionActive[ReturningExceptionNumber] ==0’ then
    DeActivate(ReturningExceptionNumber);
    UFSR.INVPC =1;
    LR = 0xF0000000 + EXC_RETURN;
    ExceptionTaken(UsageFault); // returning from an inactive handler
    return;
    else
    case EXC_RETURN<3:0> of
    when ‘0001// return to Handler
    frameptr = SP_main;
    CurrentMode = Mode_Handler;
    CONTROL.SPSEL =0;
    when ‘1001// returning to Thread using Main stack
    if NestedActivation != 1 && CCR.NONBASETHRDENA ==0’ then
    DeActivate(ReturningExceptionNumber);
    UFSR.INVPC =1;
    LR = 0xF0000000 + EXC_RETURN;
    ExceptionTaken(UsageFault); // return to Thread exception mismatch
    return;
    else
    frameptr = SP_main;
    CurrentMode = Mode_Thread;
    CONTROL.SPSEL =0;
    when ‘1101// returning to Thread using Process stack
    if NestedActivation != 1 && CCR.NONBASETHRDENA ==0’ then
    DeActivate(ReturningExceptionNumber);
    UFSR.INVPC =1;
    LR = 0xF0000000 + EXC_RETURN;
    ExceptionTaken(UsageFault); // return to Thread exception mismatch
    return;
    else
    frameptr = SP_process;
    CurrentMode = Mode_Thread;
    CONTROL.SPSEL =1;
    otherwise
    DeActivate(ReturningExceptionNumber);
    UFSR.INVPC =1;
    LR = 0xF0000000 + EXC_RETURN;
    ExceptionTaken(UsageFault); // illegal EXC_RETURN
    return;
    DeActivate(ReturningExceptionNumber);
    PopStack(frameptr);
    if CurrentMode==Mode_Handler AND IPSR<8:0> ==000000000’ then
    UFSR.INVPC =1;
    PushStack(); // to negate PopStack()
    LR = 0xF0000000 + EXC_RETURN;
    ExceptionTaken(UsageFault); // return IPSR is inconsistent
    return;
    if CurrentMode==Mode_Thread AND IPSR<8:0> !=000000000’ then
    UFSR.INVPC =1;
    "出栈的关键部分"
    PushStack(); // to negate PopStack()
    LR = 0xF0000000 + EXC_RETURN;
    ExceptionTaken(UsageFault); // return IPSR is inconsistent
    return;
    ClearExclusiveLocal();
    SetEventRegister() // see WFE instruction for more details
    InstructionSynchronizationBarrier();
    if CurrentMode==Mode_Thread AND NestedActivation == 0 AND SCR.SLEEPONEXIT ==1’ then
    SleepOnExit(); // IMPLEMENTATION DEFINED
    

    上述伪代码中PushStack为出栈部分,当异常返回时,正常的话会执行出栈操作,这里我们可以找到pc寄存器即将放置哪个值:

    // PopStack()
    // ==========
    PopStack(bits(32) frameptr) /* only stack locations, not the load order, are architected */
    if HaveFPExt() && EXC_RETURN<4> ==0’ then
    framesize = 0x68;
    forcealign =1;
    else
    framesize = 0x20;
    forcealign = CCR.STKALIGN;
    "此处的frameptr就是对应psp的值"
    R[0] = MemA[frameptr,4];
    R[1] = MemA[frameptr+0x4,4];
    R[2] = MemA[frameptr+0x8,4];
    R[3] = MemA[frameptr+0xC,4];
    R[12] = MemA[frameptr+0x10,4];
    LR = MemA[frameptr+0x14,4];
    "下一句,就是取(psp+向右偏移24位bits)地址内的32位值,将此值赋值给pc"
    "此时新的pc的值必须半字对齐"
    PC = MemA[frameptr+0x18,4]; // UNPREDICTABLE if the new PC not halfword aligned
    psr = MemA[frameptr+0x1C,4];
    if HaveFPExt() then
    if EXC_RETURN<4> ==0’ then
    if FPCCR.LSPACT ==1’ then
    FPCCR.LSPACT =0; // state in FP is still valid
    else
    CheckVFPEnabled();
    for i = 0 to 15
    S[i] = MemA[frameptr+0x20+(4*i),4];
    FPSCR = MemA[frameptr+0x60,4];
    CONTROL.FPCA = NOT(EXC_RETURN<4>);
    spmask = Zeros(29)((psr<9> AND forcealign):00;
    case EXC_RETURN<3:0> of
    when ‘0001// returning to Handler
    SP_main = (SP_main + framesize) OR spmask;
    when ‘1001// returning to Thread using Main stack
    SP_main = (SP_main + framesize) OR spmask;
    when ‘1101// returning to Thread using Process stack
    SP_process = (SP_process + framesize) OR spmask;
    APSR<31:27> = psr<31:27>; // valid APSR bits loaded from memory
    if HaveDSPExt() then
    APSR<19:16> = psr<19:16>;
    IPSR<8:0> = psr<8:0>; // valid IPSR bits loaded from memory
    EPSR<26:24,15:10> = psr<26:24,15:10>; // valid EPSR bits loaded from memory
    return;
    

    从上面的伪代码可以发现异常返回时,先判断r14的值,再根据相应的值进行出栈,然后进入新的任务栈中执行任务。所以内核就是这样进入第一个空闲任务的:

    • 建立空闲任务堆栈,并初始化,填充任务回调函数等参数;
    • 触发svc中断,进入svc中断处理函数;
    • 在svc中断处理函数中,手动调整psp堆栈为空闲任务堆栈值;
    • 返回时,借助异常返回指令,最终正确跳转到空闲任务的回调函数中。

    仿真演示

    打开仿真软件,配置如下:
    在这里插入图片描述

    打开工程,启动断点调试:
    在这里插入图片描述

    此处是建立空闲任务后,初始化空闲任务的堆栈。
    在这里插入图片描述

    这里注意寄存器的值,还有此处堆栈处的值,在右下角内存地址处可以看到。

    在这里插入图片描述

    上面这张图可以发现r14寄存器的低4位是0x0d,说明异常返回时即将进入的线程模式下的进程堆栈。
    在这里插入图片描述

    上面这个图,注意看到psp的地址位0x20000578,r15的地址位0x08000fe8,r14的地址为0x080011dd,是不是与右下角的内存地址全部对应上了。

    仿真的结果与理论一致,所以内核就是这样进入第一个空闲任务的。那么后面是如何调度的呢?这就需要借助挂起中断了。

    内核如何调度

    上面介绍了内核如何进入第一个任务。那么在进入第一个任务之后,在运行过程中,内核是如何调度到第二个任务?其实内核切换任务是利用了PendSV(可悬起中断/系统调用)机制。在多任务环境下,内核每次切换任务时,都会进入PendSV中断服务函数里,进行切换任务栈操作。

    内核调度分析

    // ARM_CM3/port.c
    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. */
    			"下面这一句,是将pendsv中断寄存器位置1;执行完此句话后,会触发"
    			"pendsv中断,进入pendsv中断服务程序"
    			portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
    		}
    	}
    	vPortClearBASEPRIFromISR();
    }
    

    上面是一个系统定时器中断服务程序,当定时到时,会进入该中断,当满足特定条件时,会将pendsv中断打开触发,然后进入pendsv中断服务程序。

    // ARM_CM3/port.c
    __asm void xPortPendSVHandler( void )
    {
    	"这是一个内嵌汇编pendsv中断服务程序,下面3句是引用3个全局变量"
    	extern uxCriticalNesting;
    	extern pxCurrentTCB;
    	extern vTaskSwitchContext;
    
    	PRESERVE8  "8字节对齐"
    	
    	"下面这一句是将当前psp堆栈值寄存在r0中,因为psp等下会变"
    	mrs r0, psp 
    	isb  "强制指令清空,我的理解是将三级流水线内的指令清空"
        "下面这一句,是将pxCurrentTCB变量的地址寄存在r3中"
    	ldr	r3, =pxCurrentTCB		/* Get the location of the current TCB. */
    	"下面这一句,是将pxCurrentTCB指向内存区域的第一个元素地址寄存在r2中"
    	"而pxCurrentTCB指向内存区域的第一个元素值,"
    	"其实就是记录当前TCB的栈顶地址变量."
    	"执行完下一句后,r2中的值就是指向当前TCB栈顶地址的变量"
    	ldr	r2, [r3]
    	"下面这一句是将寄存器r4-r11的值依次压入当前栈中,r0中的值会减少32(4x8)"
    	stmdb r0!, {r4-r11}			/* Save the remaining registers. */
    	"此时的r0中的值就是当前任务压栈后的栈顶地址,执行完下一句后,"
    	"就是将当前TCB栈顶地址的变量重新指向更新后的栈顶地址"
    	str r0, [r2]				/* Save the new top of stack into the first member of the TCB. */
    	"因为该函数是中断服务程序,由于CM3的双堆栈特性,所以,"
    	"此处的sp表示的是msp(主堆栈)。下面这一句,是依次将r14、r3存入msp中"
    	"存放r14的原因是,后面需要使用到这个lr值;存放r3的原因是,后面要"
    	"使用r3获取pxCurrentTCB,其实使用pxCurrentTCB重新加载到寄存器中也可以"
    	stmdb sp!, {r3, r14}
    	"下面这一句,是将系统调用的最大中断优先级值寄存到r0中,为了屏蔽一部分的中断"
    	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
    	msr basepri, r0
    	"强制同步数据"
    	dsb
    	"强制清除指令,清除3级流水线中的指令(个人理解)"
    	isb
    	"跳转到vTaskSwitchContext函数,此函数的目的是为了更新pxCurrentTCB"
    	"指向的地址。同时也可以看出为什么上面要压栈r14、r13了,因为存在子函数"
    	bl vTaskSwitchContext
    	"在更新pxCurrentTCB之后,下面这2句,是将所有的中断打开"
    	mov r0, #0
    	msr basepri, r0
    	"下面这一句,是将当初压栈的r3、r14出栈"
    	ldmia sp!, {r3, r14}
    	"下面这两句,是将r0存入更新后TCB的栈顶变量"
    	ldr r1, [r3]
    	ldr r0, [r1]				/* The first item in pxCurrentTCB is the task top of stack. */
    	"下面这一句,是将栈中的前8个值依次出栈到r4-r11"
    	ldmia r0!, {r4-r11}			/* Pop the registers and the critical nesting count. */
    	"下面这一句,是将r0中的值赋值到psp上,其实在初始化栈的时候,"
    	"对应位置,也是r0那个位置顺序"
    	msr psp, r0
    	"清除指令"
    	isb
    	"跳转指令,这个会对r14的值进行判断,如果后4位是0x0d,"
    	"会根据psp的值,出栈到pc、r1等寄存器"
    	bx r14
    	nop
    }
    

    上面的bx r14条转指令是神奇的指令,在这一步,才决定了pc到底指向何方。上面的汇编程序就说明了内核如何调度了,每次进入pendsv中断函数内:

    • 压栈所需要的寄存器值,比如r14、r3
    • 进入子程序,更新当前任务指针pxCurrentTCB
    • 退出子程序,出栈,结束跳转
    • 在跳转指令中,指令系统会计算出pc应该指向的值

    内核调度演示

    在这里插入图片描述

    在这里插入图片描述

    上面图片显示在执行到bx r14时,lr的值的低4位是0x04,表明即将要进入用户模式的进程堆栈中,所以指令系统会将psp出栈,更新pc,进入新的任务栈,完成一次系统调度。

    内核如何更新pxCurrentTCB

    按照优先级大小,轮询调度

    • 优先级相同时,每次先调度先加入链表中的任务,然后再调用后加入链表中的任务
    • 优先级不同时,先调度优先高的,后依然调度优先级高的,所以称为可抢占调度内核
    • 没有处于就绪状态的任务,不参与每次调度
    展开全文
  • 一、FreeRTOS任务在多数通用操作系统(如Linux、Windows等)中,线程为系统的调度最小单元,进程为独立应用程序的最小运行过程。而在实时操作系统中,多数情况下不区分线程与进程进...

    一、FreeRTOS任务

    在多数通用操作系统(如Linux、Windows等)中,线程为系统的调度最小单元,进程为独立应用程序的最小运行过程。而在实时操作系统中,多数情况下不区分线程与进程进行独立管理,为了减小系统资占用以及提高实时性,往往将线程与进程合二为一,采用任务作为应用程序的最小调度运行单元,使用TCB(任务控制块)对任务进行管理。FreeRTOS作为常用的RTOS系统中的一员,它的任务调度以及任务之间的通信的设计遵从多数RTOS的设计。FreeRTOS简捷、方便、易用的任务机制,是其近几年成为市场份额最大的RTOS因素之一,本文主要对其任务调度实现以及通信原理进行描述介绍。

    二、FreeRTOS任务调度

    FreeRTOS的任务被动态创建或者静态创建后,不进行删除,目前具备的任务运行态为Running态、Ready态、Blocked态以及Suspended态四个状态。从FreeRTOS官方给出任务资料以及任务状态示意图可以看出,任务的调度主要分为三大阶段:

    阶段一:任务创建与调度器启动阶段(下图蓝色部分)

    阶段二:任务运行后,调用任务API或者调度器仲裁切换任务状态阶段(下图绿色部分)

    阶段三:任务删除销毁阶段(下图红色部分)

    07b5e268ae47e40b0645e8473b1e95d2.png

    1. 任务创建启动阶段

    FreeRTOS调用xTaskCreate(动态创建)或者xTaskCreateStatic(静态创建)API进行任务的创建。动态创建任务时,为任务申请任务堆栈、申请任务控制块(TCB)内存、初始化申请到的内存、调用prvInitialiseNewTask函数初始化任务、最后调用prvAddNewTaskToReadyList函数将新建的任务加入就绪任务列表中(如下官方代码)。

    211deb4cf4bae6d5eaf5977c16eed58a.png

    创建完成任务后,多数情况下紧随其后调用任务API函数vTaskStartScheduler开启任务的调度。以采用动态创建任务的方式为例,vTaskStartScheduler函数中先是创建空闲任务(idle任务)、若使用软件定时器创建软件定时器、关闭中断、标记调度器开始运行、若配置时间统计开启时间统计功能、调用xPortStartScheduler函数(此函数适配不同的硬件平台,一般用于初始化硬件资源,滴答定时器等;如cortex-m系列用于初始化FPU单元、滴答定时器、PendSV中断等),流程如下图。

    3ab088f7c92c922c82de9c681d6ddaa8.png

    2. 任务运行阶段

    任务在调度器启动后,在创建的任务未被删除销毁之前,任务会在Running(运行)、Ready(就绪)态Blocked(阻塞)以及Suspended(挂起)四个状态中进行切换。

    运行态的任务调用系统延时函数(如vTaskDelay函数)、调用ulTaskNotifyTake等待通知、调用xSemaphoreTake等待信号量等行为进入阻塞状态;正在运行的任务死等资源或者调用vTaskSuspend接口可以将自身挂起;正在运行的任务因被中断或者优先级高于或等于(时间片轮转方式使能)的任务打断进入就绪的任务队列中。

    阻塞态的任务等到所需的资源或者调用的系统等待时间到点后,会被唤醒重新进入就绪任务列表中;此外也可以通过使用xSemaphoreTake接口将阻塞的任务挂起。

    挂起态的任务只能通过调用系统API接口vTaskResume函数恢复到就绪的队列中,挂起态任务只能切换到就绪态。

    就绪态的任务在调度器仲裁获取到CPU的执行权后,直接进入运行态,并且可以通过使用vTaskSuspend接口将其挂起。

    FreeRTOS的运行态与就绪态之间的切换主要依赖调度器进行仲裁,仲裁算法同时支持优先级抢占式以及时间片调度方式(使能configUSE_PORT_OPTIMISED_TASK_SELECTION=1)。

    第一种为优先级抢占的方式:FreeRTOS使用vTaskSwitchContext系统API函数进行任务之间切换,调用taskSELECT_HIGHEST_PRIORITY_TASK()函数仲裁,根据任务优先级获取下一个运行的任务,vTaskSwitchContext函数原型如下:

    3c23780838ed9e161dc48310c799e0e5.png

    第二种为时间片轮转的调度方式:FreeRTOS使能时间片轮转的方式后,FreeRTOS提供系统API接口xTaskIncrementTick函数进行同个优先级的任务进行时间片轮转调度,每个时间片大小一般为滴答定时器的中断周期。多数情况下会在滴答定时器的中断回调中调用xTaskIncrementTick函数判断是否进行同个优先级任务的轮询切换(若任务在某一个滴答时钟周期内执行完毕,会强制调用portYIELD接口进行任务切换,放弃剩余的时间片时间)。如下图所示,TaskA的优先级等于TaskB,滴答定时器周期为1ms。

    58cfa6dedd18d149012cbdbb03633c8b.png

    3. 任务删除销毁阶段

    就绪、阻塞、挂起的任务都可以调用vTaskDelete系统API接口将其进行删除销毁,被销毁的任务的任务控制块(TCB)以及任务堆栈会在进入idle任务后,在idle任务中进行删除以及释放。

    三、任务通信

    FreeRTOS具有一般操作系统都具备的几种任务之间的通信方式,提供给用户的常用的通信方式有:消息队列、信号量、互斥锁(互斥信号量)、事件标志组以及任务通知等。

    FreeRTOS的任务通知具有32位的消息通知值,只有一个任务接收任务通知,可以减少RAM的使用,运行效率比二值信号量快45%(官方给出的数据)。

    事件标志组与信号量的在于:时间标志组主要用与某个任务与多个事件进行同步,或者某个任务与多个任务进行同步。

    1. 信号量

    FreeRTOS的信号量主要用于某个任务与单个事件或任务进行同步,FreeRTOS常用的信号量为以下几种:

    a. 二值信号量

    b. 计数型信号量

    c. 互斥信号量

    d. 递归互斥型号量

    以下为官方提供的二值信号量Gif图演示信号量的基本工作原理:

    a8b292af5f346457bb5a539d5e37657a.gif

    2. 消息队列

    FreeRTOS的消息队列主要用于任务中传递大量的消息,以下为官方提供的消息队列Gif图演示消息队列的基本工作原理:

    88ab6cf45cf6b7a8846a7697a14013a2.gif


    四、小结

     FreeRTOS的不同优先级的任务采用强抢占式调度(高优先抢占)保持其作为RTOS特有实时性,同级优先级的任务采用时间片轮转的方式兼顾同级平等的得到调度运行。任务与任务之间,提供信号量、消息队列、通知、事件标志组等方式进行通信,确保中断事件与任务、任务与任务之间可以有效、高效地进行通信。又因其特有免费开源的特性,使其在RTOS的市场占有份额越来越高。

    参考文献

    FreeRTOS官方文档:https://www.freertos.org/features.html

    FreeRTOS官方代码:https://www.freertos.org/a00104.html

    04eda37cf58032474054d3b3a134103f.gif

    长按关注

    内核工匠微信

    Linux 内核黑科技 | 技术文章 | 精选教程

    展开全文
  • freertos任务调度简介

    2021-03-28 17:13:37
    c语言编写的调度算法,适用于所有的控制器;大概流程如下: 1.listLIST_IS_EMPTY()查找任务就绪列表数组pxReadyTasksLists[]中的最高优先级任务列表是否为空,为空即没有列表项,没有列表项即没有线程。为空的情况...

    通用方式

    c语言编写的调度算法,适用于所有的控制器;大概流程如下:

         1.listLIST_IS_EMPTY()查找任务就绪列表数组pxReadyTasksLists[]中的最高优先级任务列表是否为空,为空即没有列表项,没有列表项即没有线程。为空的情况几乎是不可能出现的,所以这个查找算法只是存在可能的时间不确定性,总的来说时间是确定的;

         2.由于一个任务列表对应一个优先级,一个优先级对应一个或多个任务,所以还需要调用listGET_OWNER_OF_NEXT_ENTRY()查找下一个运行的任务。一个任务对应一个列表项,这里查找的是列表项。

    硬件方法:使用Cotex-m3的前导0指令CLZ;大概流程如下:

    1. portGET_HIGHEST_PRIORITY()利用CLZ命令查找变量uxTopReadyPriority中最高优先级二进制位;
    2. 调用listGET_OWNER_OF_NEXT_ENTRY()查找下一个运行的任务。

    可以看到通用方法和硬件方法的不同点在第一步,第二步是一样的。其实另外还有一个不同点这里没表现出来,就是全局变量uxTopReadyPriority。在通用方法中列表数组的下标初始值也是调用了该全局变量,这个时候全局变量表示的是一个优先级数值。在硬件方法中该全局变量就是优先级的位图,每一位代表一个优先级。相关实现请查阅源码。

    时间片调度

    原理:同等级的多个任务在一定时间内公平的使用cpu的。在freertos中,一个列表对应一个优先级,一个列表项对应一个任务。所以时间片任务调度就是在一个列表内切换多个列表项。主要由宏函数listGET_OWNER_OF_NEXT_ENTRY()实现。

    用法:需要将宏configUSE_PREEMPTION和configUSE_TIME_SLICING都置1,时间片大小由宏configTICK_RATE_HZ确定,该宏也是滴答定时器的中断周期。所以时间片的任务切换是在滴答定时器中断里触发的。

    展开全文
  • 1:多任务启动 1:创建空闲任务 2:配置SysTick和PendSV为最低优先级 3:配置SysTick寄存器 4:调用SVC中断 2:SVC业务流程 1:获取当前任务栈顶 2:手动出栈 r4-r11,r14 3:更新栈顶到 PSP 4:使能全局中断,调用...

    1:多任务启动

    1:创建空闲任务
    2:配置SysTick和PendSV为最低优先级
    3:配置SysTick寄存器
    4:调用SVC中断
    

    2:SVC业务流程

    1:获取当前任务栈顶
    2:手动出栈 r4-r11,r14
    3:更新栈顶到 PSP
    4:使能全局中断,调用异常返回指令
    

    3:PendSV业务流程

    1:读取当前PSP值,获取当前任务栈顶
    2:保存 s16-s31 到栈中,保存 r4-r11,r14到当前栈中
    3:更新栈顶到当前任务控制块中,保存 r3 到栈中,关闭中断
    4:查找优先级最高的任务,更新当前任务控制快,开启中断,出栈 r3 值
    5:出栈 r 4-r11,r14 到当前栈中,出栈 s16-s31 到栈中,更新栈顶到PSP,调用移除返回指令
    
    展开全文
  • 本次FreeRTOS任务调度通过S32K144来实现,首先进行工程的创建,然后做代码分析。 选择file->new->S32DS Application Project 填写工程名称,选择S32K144,点击next FPU Support选择Hardware:-mfloat -abi = hard...
  • freertos调度配置mem

    2021-06-17 19:33:50
    freertos移植教程_freertos任务调度原理http://www.elecfans.com/emb/xitong/202004161204612.html
  • FreeRTOS原理剖析:任务调度器启动

    千次阅读 2019-10-08 06:52:35
    介绍了任务调度器启动的过程。
  • 嵌入式操作系统学习(3)FreeRTOS任务调度机制

    万次阅读 多人点赞 2018-07-09 16:29:15
    FreeRTOS可以创建多个任务,但是对于单核cpu来说,在任意给定时间,实际上只有一个任务被执行,这样就可以把任务分成2个状态,即运行状态和非运行状态。 当任务处于运行状态时,处理器就执行该任务的代码。处于非...
  • FreeRTOS可以运行多任务,在于其内核的任务调度功能,本篇介绍任务调度的基本思路与部分源码分析。 1 裸机编程与RTOS 的区别 1.1 裸机程序基本框架 /*主函数*/ int main() { init();//一些初始化 /*死循环*/ ...
  • 一、概述 FreeRtos在创建任务之后,需要启动任务调度器才能使任务正常有序的运行。任务调度器的开启依赖于底层硬件,对于CortexM3内核而言,任务调度器需要...FreeRtos任务调度需要用到CortexM3和汇编知识,可以结合C
  • FreeRTOS最核心的就是任务调度FreeRTOS 操作系统支持的任务调度方式:抢占式,时间片和合作式。合作式调度器由于占用资源大已经很少使用,官方没有删除,但以后不会升级了。我们平时默认使用的就是抢占式调度器。...
  • FreeRTOS任务管理

    2021-09-06 20:37:38
    文章目录抢占式多任务系统时间片调度任务状态运行态就绪态...  高优先级的任务执行完成以后,重新把CPU的使用权归还给低优先级的任务,这个就是抢占式多任务系统的基本原理。 时间片调度   对于同等优先级任务
  • FreeRTOS的核心就是任务管理,而任务管理的核心就是如何进行任务切换。
  • FreeRTOS任务03. FreeRTOS协程04. 任务状态05. 任务优先级06. 任务实现07. 任务控制块08. 任务堆栈09. 附录10. 参考 01. 概述 我们以前使用51、AVR、STM32单片机裸机的时候一般都是在main函数里面用while(1)做一个...
  • 文章目录任务控制块数据结构任务创建函数定义就绪表就绪表初始化启动调度任务控制块数据结构 任务控制块数据结构在task.c声明 typedef struct tskTaskControlBlock { volatile StackType_t * pxTopOfStack; //...
  • FreeRTOS任务调度是个大头,也是一个操作系统的核心。 其实个人理解,FreeRTOS调度规则很好理解,原则就是“优先级高抢占”,因为FreeRTOS是一个抢占式实时内核,一定会保证就绪态的高优先级任务可以先运行。 所有...
  • FreeRTOS基本原理

    2020-07-09 16:26:04
    FreeRTOS基本原理 多任务 内核是操作系统的核心部分,操作系统例如Linux通过内核使用户看似同时的访问电脑,多...调度原理 调用度是内核中负责决定在某一时刻该执行什么任务的部分。内核可以挂起然后恢复一个任务许多次
  • 多任务系统 单任务系统,也称作前后台系统,中断服务函数作为前台程序,大循环 while(1)作为后台程序 ... FreeRTOS 是一个抢占式的实时多任务系统, 任务调度器也是抢占式。 抢占式多任务系统的基本...
  • 嵌入式FreeRTOS操作系统原理

    千次阅读 2016-09-23 10:48:21
    FreeRTOS内核支持优先级调度算法,每个任务可根据重要程度的不同被赋予一定的优先级,CPU总是让处于就绪态的、优先级最高的任务先运行。FreeRT0S内核同时支持轮换调度算法,系统允许不同的任务使用相同的优先级,
  • 文章目录0 前言1 任务调度器的开启1.1 如何启动第一个任务的2 任务的切换2.1 PendSV 异常2.2 两个事件引起PendSV 异常2.3 PendSV 的中断服务函数 在这里面进行任务切换 0 前言 @ Author :Dargon @ Record Date :...
  •  RTOS 系统的核心就是任务管理,FreeRTOS 也不例外,而且大多数学习RTOS 系统的工程师或者学生主要就是为了使用RTOS 的多任务处理功能,初步上手RTOS 系统首先必须掌握的也是任务的创建、删除、挂起和恢复等操作,...
  • FreeRTOS工作原理

    2018-11-05 10:54:49
    备注:资源清晰度有所欠缺,但是不影响观看,内容对于初学者来说,理解FreeRTOS是非常很有帮助。 1. 多任务操作系统运行机制; 2. 任务的上下文切换,以一个实际的例子说明多任务运行机制; 3. 举了一个实例,说明...
  • 主要介绍FreeRTOS任务删除功能。通过分析函数vTaskDelete()剖析任务删除的原理,同时,对于该函数内部调用的函数也重点分析。
  • 前言 FreeRTOS是一个是实时内核,任务是程序执行的最小单位,也是调度器处理的基本单位,移植了FreeRTOS,则避免不了对任务的管理,在多个任务运行的时候,任务切换显得尤为重要。而任务切换的效率会决定了系统的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 653
精华内容 261
关键字:

freertos任务调度原理