FreeRTOS学习笔记(二)——内核机制

0x01 临界段

临界段用一句话概括就是一段在执行的时候不能被中断的代码段。临界段会被打断是在以下两种情况下:一个是系统调度,还有一个就是外部中断。在FreeRTOS,系统调度,最终也是产生 PendSV 中断,在 PendSV Handler 里面实现任务的切换,所以还是可以归结为中断。既然这样,FreeRTOS 对临界段的保护最终还是回到对中断的开和关的控制

Cortex-M内核快速关中断指令

三个中断屏蔽寄存器有:PRIMASKFAULTMASTBASEPRI

在FreeRTOS上对中断的开和关使用的寄存器是BASEPRI,在 FreeRTOS 中,对中断的开和关是通过操作 BASEPRI 寄存器来实现的,即大于等于 BASEPRI 的值的中断会被屏蔽,小于 BASEPRI 的值的中断则不会被屏蔽,不受FreeRTOS 管理。用户可以设置 BASEPRI 的值来选择性的给一些非常紧急的中断留一条后路。

关中断

FreeRTOS 关中断的函数在 portmacro.h 中定义,分不带返回值带返回值两种:

对于不带返回值的关中断函数,不可以进行嵌套,不能在中断中进行使用:

#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()

static portFORCE_INLINE void vPortRaiseBASEPRI( void )
{
    
    
	uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;

	__asm
	{
    
    
		msr basepri, ulNewBASEPRI
		dsb
		isb
	}
}
  • 不带返回值的关中断函数,不能嵌套,不能在中断里面使用。不带返回值的意思是:在往 BASEPRI 写入新的值的时候,不用先将 BASEPRI 的值保存起来,即不用管当前的中断状态是怎么样的,既然不用管当前的中断状态,也就意味着这样的函数不能在中断里面调用。
  • configMAX_SYSCALL_INTERRUPT_PRIORITY 是 一 个 在FreeRTOSConfig.h 中定义的宏,即要写入到 BASEPRI 寄存器的值。存入后高四位有效,优先级大于此值会被屏蔽,小于此值的中断不受RTOS管理。
  • 将 configMAX_SYSCALL_INTERRUPT_PRIORITY 的值写入BASEPRI 寄存器,实现关中断(准确来说是关部分中断)。

对于带返回值的关中断函数,可以进行嵌套,可以在中断中进行使用:

#define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI()

static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void )
{
    
    
	uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;

	__asm
	{
    
    
		/* Set BASEPRI to the max syscall priority to effect a critical
		section. */
		mrs ulReturn, basepri
		msr basepri, ulNewBASEPRI
		dsb
		isb
	}

	return ulReturn;
}
  • 带返回值的关中断函数,可以嵌套,可以在中断里面使用。带返回值的意思是:在往 BASEPRI 写入新的值的时候,先将 BASEPRI 的值保存起来,在更新完BASEPRI 的值的时候,将之前保存好的 BASEPRI 的值返回,返回的值作为形参传入开中断函数

  • 其configMAX_SYSCALL_INTERRUPT_PRIORITY的作用同上。

  • 保存 BASEPRI 的值,记录当前哪些中断被关闭。

  • 更新 BASEPRI 的值。

  • 返回原来 BASEPRI 的值。

开中断

#define portENABLE_INTERRUPTS()					vPortSetBASEPRI(0)   	// 不带中断保护的开中断函数
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x)	vPortSetBASEPRI(x)	   // 带中断保护的开中断函数

static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
    
    
	__asm
	{
    
    
		/* Barrier instructions are not used as this function is only used to
		lower the BASEPRI value. */
		msr basepri, ulBASEPRI
	}
}
  • 开中断函数,具体是将传进来的形参更新到 BASEPRI 寄存器。根据传进来形参的不同,分为中断保护版本与非中断保护版本。
  • 不带中断保护的开中断函数,直接将 BASEPRI 的值设置为 0,与portDISABLE_INTERRUPTS()成对使用。
  • 带中断保护的开中断函数,将上一次关中断时保存的 BASEPRI 的值作为形参 ,与 portSET_INTERRUPT_MASK_FROM_ISR()成对使用。

进入和退出临界段的宏

进入和退出临界段的宏分中断保护版本非中断版本,但最终都是通过开/关中断来实现,主要是由如下的宏配置:

#define taskENTER_CRITICAL() 			portENTER_CRITICAL()
#define taskENTER_CRITICAL_FROM_ISR() 	portSET_INTERRUPT_MASK_FROM_ISR()
#define taskEXIT_CRITICAL() 			portEXIT_CRITICAL()
#define taskEXIT_CRITICAL_FROM_ISR( x ) portCLEAR_INTERRUPT_MASK_FROM_ISR( x )

进入临界段

不带中断保护,不可以进行嵌套:

#define portENTER_CRITICAL() 			vPortEnterCritical()
#define portENTER_CRITICAL() 			vPortEnterCritical()
void vPortEnterCritical( void )
{
    
    
	portDISABLE_INTERRUPTS();
    
   	// uxCriticalNesting 是在 port.c 中定义的静态变量,表示临界段嵌套计数 器 , 默认初始化为 0xaaaaaaaa , 在 调 度 器 启动时会被重新初始化为0 :vTaskStartScheduler()->xPortStartScheduler()->uxCriticalNesting = 0。
	uxCriticalNesting++;

	/* This is not the interrupt safe version of the enter critical function so
	assert() if it is being called from an interrupt context.  Only API
	functions that end in "FromISR" can be used in an interrupt.  Only assert if
	the critical nesting count is 1 to protect against recursive calls if the
	assert function also uses a critical section. */
	if( uxCriticalNesting == 1 )
	{
    
    
        // 如果 uxCriticalNesting 等于 1,即一层嵌套,要确保当前没有中断活跃,即内核外设 SCB 中的中断和控制寄存器 SCB_ICSR 的低 8 位要等于 0。
		configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
	}
}

#define portDISABLE_INTERRUPTS() 			vPortRaiseBASEPRI()
static portFORCE_INLINE void vPortRaiseBASEPRI( void )
{
    
    
	uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;

	__asm
	{
    
    
		msr basepri, ulNewBASEPRI
		dsb
		isb
	}
}

带中断保护保本,可以进行嵌套:

#define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR()
#define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI()

static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void )
{
    
    
	uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;

	__asm
	{
    
    
		/* Set BASEPRI to the max syscall priority to effect a critical
		section. */
		mrs ulReturn, basepri
		msr basepri, ulNewBASEPRI
		dsb
		isb
	}

	return ulReturn;
}

退出临界段

不带中断保护版本,不能嵌套:

#define taskEXIT_CRITICAL() portEXIT_CRITICAL()
#define portEXIT_CRITICAL() vPortExitCritical()

void vPortExitCritical( void )
{
    
    
	configASSERT( uxCriticalNesting );
	uxCriticalNesting--;
	if( uxCriticalNesting == 0 )
	{
    
    
		portENABLE_INTERRUPTS();
	}
}

#define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 )
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
    
    
	__asm
	{
    
    
		/* Barrier instructions are not used as this function is only used to
		lower the BASEPRI value. */
		msr basepri, ulBASEPRI
	}
}

带中断的保护版本,可以嵌套:

#define taskEXIT_CRITICAL_FROM_ISR( x ) 		portCLEAR_INTERRUPT_MASK_FROM_ISR( x ) 
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) 	vPortSetBASEPRI(x)

static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
    
    
	__asm
	{
    
    
		/* Barrier instructions are not used as this function is only used to
		lower the BASEPRI value. */
		msr basepri, ulBASEPRI
	}
}

临界段代码应用

进程进入临界区的调度原则是:

  • 如果有若干进程要求进入空闲的临界区,一次仅允许一个进程进入。
  • 任何时候,处于临界区内的进程不可多于一个。如已有进程进入自己的临界区,则其它所有试图进入临界区的进程必须等待。
  • 进入临界区的进程要在有限时间内退出,以便其它进程能及时进入自己的临界区。
  • 如果进程不能进入自己的临界区,则应让出CPU,避免进程出现“忙等”现象

访问一个被多任务共享,或是被多任务和中断共享的资源时,需要采用**“互斥”技术**以保证数据在任何时候都保持一致性。这样做的目的是要确保任务从开始访问资源就具有排它性,直到这个资源又恢复到完整状态。

可以使用在中断场合场合,可以使用如下代码进行操作:

uint32_t ulReturn;
/* 进入临界段,临界段可以嵌套 */
ulReturn = taskENTER_CRITICAL_FROM_ISR();

/* 临界段代码 */

/* 退出临界段 */
taskEXIT_CRITICAL_FROM_ISR( ulReturn );

对于在非中断场合,临界段不可以进行嵌套:

/* 进入临界段 */
taskENTER_CRITICAL();

/* 临界段代码 */

/* 退出临界段*/
taskEXIT_CRITICAL();

0x02 空闲任务与阻塞延时的实现

在实现打印效果的时候,可以发现其打印时间并不是“一起发生的”,而是间隔着一秒一秒的时间发生的,类似于裸机系统的处理:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PgcLnxwp-1682495949162)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230417115221475.png)]

这里使用的延时效果是使用软件延时,也就是还让CPU空着等来达到延时的效果,这样其实也是一种浪费资源的行为。RTOS中的延时称为阻塞延时,即任务需要延时的时候会放弃CPU的使用权,CPU可以去干其他事情。当任务进行延时时,如果CPU没有其他任务需要运行,RTOS会给CPU创建一个空闲任务给CPU执行。

空闲任务为FreeRTOS中启动调度器的时候创建的优先级最低的任务。空闲任务主要是做一些系统内存的清理工作。在实际应用中,当系统进入空闲任务时,可在空闲任务中让单片机进入休眠或者低功耗等操作。

空闲任务的创建

  • 定义空闲任务的栈

    #define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 ) 
    StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE]; 
    
  • 定义空闲任务的任务控制块

    TCB_t IdleTaskTCB;
    
  • 创建空闲任务

    extern TCB_t IdleTaskTCB;
    void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer,
                                        StackType_t **ppxIdleTaskStackBuffer,
                                        uint32_t *pulIdleTaskStackSize );
    void vTaskStartScheduler( void )
    {
          
          
        TCB_t *pxIdleTaskTCBBuffer = NULL; 			/* 用于指向空闲任务控制块 */ 
        StackType_t *pxIdleTaskStackBuffer = NULL;	/* 用于空闲任务栈起始地址 */ 
        uint32_t ulIdleTaskStackSize;
        
        /* 获取空闲任务的内存:任务栈和任务 TCB */ 
        vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer, 
                                        &pxIdleTaskStackBuffer, 
                                        &ulIdleTaskStackSize );
        /* 创建空闲任务 */ 
        xIdleTaskHandle = 
                    xTaskCreateStatic( (TaskFunction_t)prvIdleTask, /* 任务入口 */ 
                    (char *)"IDLE", /* 任务名称,字符串形式 */ 
                    (uint32_t)ulIdleTaskStackSize , /* 任务栈大小,单位为字 */ 
                    (void *) NULL, /* 任务形参 */ 
                    (StackType_t *)pxIdleTaskStackBuffer, /* 任务栈起始地址 */ 
                    (TCB_t *)pxIdleTaskTCBBuffer ); /* 任务控制块 */ 
        /* 将任务添加到就绪列表 */ 
        // 将空闲任务插入到就绪列表的开头。
        vListInsertEnd( &( pxReadyTasksLists[0] ), 
        &( ((TCB_t *)pxIdleTaskTCBBuffer)->xStateListItem ) );
        
        ...
    }
    

    函数vApplicationGetIdleTaskMemory用于获取空闲任务的内存,即将pxIdleTaskTCBBufferpxIdleTaskStackBuffer这两个接下来要作为形参传到xTaskCreateStatic函数中的指针分别指向空闲任务的TCB和栈的起始地址,这个操作由函数vApplicationGetIdleTaskMemory()来实现,该函数需要用户自定义。

    void vApplicationGetIdleTaskMemory( StaticTask_t **ppxIdleTaskTCBBuffer, StackType_t **ppxIdleTaskStackBuffer, uint32_t *pulIdleTaskStackSize )
    {
          
          
          *ppxIdleTaskTCBBuffer = &xIdleTaskTCBBuffer;
          *ppxIdleTaskStackBuffer = &xIdleStack[0];
          *pulIdleTaskStackSize = configMINIMAL_STACK_SIZE;
          /* place for user code */
    }
    

实现阻塞延时

  • 使用vTaskDelay()

阻塞延时的阻塞是指任务调用该延时函数后,任务会被剥离 CPU 使用权,然后进入阻塞状态,直到延时结束,任务重新获取 CPU 使用权才可以继续运行。在任务阻塞的这段时间,CPU 可以去执行其它的任务,如果其它的任务也在延时状态,那么 CPU 就将运行空闲任务。

#if ( INCLUDE_vTaskDelay == 1 )

	void vTaskDelay( const TickType_t xTicksToDelay )
	{
    
    
		BaseType_t xAlreadyYielded = pdFALSE;

		/*延迟时间为零只是强制重新安排。 */
		if( xTicksToDelay > ( TickType_t ) 0U )
		{
    
    
			configASSERT( uxSchedulerSuspended == 0 );
			vTaskSuspendAll();
			{
    
    
				traceTASK_DELAY();

                /*从事件列表中删除任务,调度器被挂起不会被放置在准备中,列表或从阻塞列表中删除,直到调度程序已恢复*/
                /*此任务不能在事件列表中,因为它是当前任务执行任务*/
				prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );
			}
			xAlreadyYielded = xTaskResumeAll();
		}
		else
		{
    
    
			mtCOVERAGE_TEST_MARKER();
		}

		/* Force a reschedule if xTaskResumeAll has not already done so, we may
		have put ourselves to sleep. */
		if( xAlreadyYielded == pdFALSE )
		{
    
    
			portYIELD_WITHIN_API();
		}
		else
		{
    
    
			mtCOVERAGE_TEST_MARKER();
		}
	}

#endif /* INCLUDE_vTaskDelay */

xTicksToDelay 是任务控制块的一个成员,用于记录任务需要延时的时间,单位为 SysTick 的中断周期

  • 修改**vTaskSwitchContext()**函数,在这里多了个空闲任务,在这里可以通过判断当前任务是否为空闲任务,之后执行对应的程序,去判断延时时间是否已经到达,若到达则执行,若没有到达则切换

  • SysTick中断服务函数xPortSysTickHandler(),在FreeRTOS中,xTicksToDelay是以SysTick中断提供,操作系统里面的最小的时间单位就是SysTick 的中断周期。

    void xPortSysTickHandler( void )
    {
          
          
        // 关中断
    	vPortRaiseBASEPRI();
    	{
          
          
            //更新系统时基
    		if( xTaskIncrementTick() != pdFALSE )
    		{
          
          
    			/*需要上下文切换。进行上下文切换PendSV中断。*/
    			portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
    		}
    	}
        // 开中断
    	vPortClearBASEPRIFromISR();
    }
    
  • **xTaskIncrementTick()**更新系统时基函数,这个函数主要用于xTicksToDelay变量的递减,更新系统时基计数器 xTickCount,加一操作。xTickCount 是一个在port.c 中定义的全局变量,在函数 vTaskStartScheduler()中调用 xPortStartScheduler()函数前初始化。

    BaseType_t xTaskIncrementTick( void )
    {
          
          
    	TCB_t * pxTCB;
    	TickType_t xItemValue;
    	BaseType_t xSwitchRequired = pdFALSE;
    
    	/* 每次发生tick中断时由可移植层调用。增加刻度,然后检查新的刻度值是否会产生任何刻度要解除阻塞的任务*/
    	traceTASK_INCREMENT_TICK( xTickCount );
    	if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
    	{
          
          
    		/* 在此块中,刻度计数不能更改。 */
    		const TickType_t xConstTickCount = xTickCount + ( TickType_t ) 1;
    
    		/* 增加RTOS滴答,如果它包装为0,则切换延迟和溢出的延迟列表。 */
    		xTickCount = xConstTickCount;
    
    		if( xConstTickCount == ( TickType_t ) 0U ) /*防止溢出*/
    		{
          
          
    			taskSWITCH_DELAYED_LISTS();
    		}
    		else
    		{
          
          
    			mtCOVERAGE_TEST_MARKER();
    		}
    
    		/* 看看这个标记是否使超时过期。任务按照唤醒时间的顺序存储在队列中——这意味着一旦找到了一个没有超时的任务,就不需要再往下看了 */
    		if( xConstTickCount >= xNextTaskUnblockTime )
    		{
          
          
    			for( ;; )
    			{
          
          
    				if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
    				{
          
          
    					/*延迟列表为空。将xNextTaskUnblockTime设置为可能的最大值,这样if(xTickCount >= xNextTaskUnblockTime)测试下次通过的可能性极小。 */
    					xNextTaskUnblockTime = portMAX_DELAY; /*MISRA异常,因为强制转换仅对某些端口是冗余的。 */
    					break;
    				}
    				else
    				{
          
          
    					/*延迟列表不为空,获取延迟列表头部项的值。这是必须将处于延迟列表头部的任务从阻塞状态中移除的时间。 */
    					pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
    					xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );
    
    					if( xConstTickCount < xItemValue )
    					{
          
          
    						/*现在还不是解除阻塞的时候,但是项目值是阻塞列表头部的任务必须从阻塞状态中移除的时间,因此在xNextTaskUnblockTime中记录项目值。 */
    						xNextTaskUnblockTime = xItemValue;
    						break;
    					}
    					else
    					{
          
          
    						mtCOVERAGE_TEST_MARKER();
    					}
    
    					/* 是时候从Blocked状态中移除项目了。 */
    					( void ) uxListRemove( &( pxTCB->xStateListItem ) );
    
    					/* 任务是否也在等待事件?如果是,将其从事件列表中移除。 */
    					if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
    					{
          
          
    						( void ) uxListRemove( &( pxTCB->xEventListItem ) );
    					}
    					else
    					{
          
          
    						mtCOVERAGE_TEST_MARKER();
    					}
    
    					/* 将未阻塞的任务放入适当的就绪列表中。 */
    					prvAddTaskToReadyList( pxTCB );
    
    					/* 如果抢占被关闭,正在解除阻塞的任务不会立即导致上下文切换。 */
    					#if (  configUSE_PREEMPTION == 1 )
    					{
          
          
    						/* 抢占是打开的,但是只有当未阻塞的任务的优先级等于或高于当前正在执行的任务时,才应该执行上下文切换。 */
    						if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
    						{
          
          
    							xSwitchRequired = pdTRUE;
    						}
    						else
    						{
          
          
    							mtCOVERAGE_TEST_MARKER();
    						}
    					}
    					#endif /* configUSE_PREEMPTION */
    				}
    			}
    		}
    
    		/*如果抢占开启,且应用程序编写人员没有显式地关闭时间切片,那么与当前运行任务具有相同优先级的任务将共享处理时间(时间切片) */
    		#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 )
    		{
          
          
    			if( uxPendedTicks == ( UBaseType_t ) 0U )
    			{
          
          
    				vApplicationTickHook();
    			}
    			else
    			{
          
          
    				mtCOVERAGE_TEST_MARKER();
    			}
    		}
    		#endif /* configUSE_TICK_HOOK */
    	}
    	else
    	{
          
          
    		++uxPendedTicks;
    
    		/*即使调度程序被锁定,tick钩子也会定期被调用*/
    		#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的中断服务函数顺利执行,这个时候需要将Systick进行初始化,使用函数vPortSetupTimerInterrupt()

// 配置 SysTick 需要用到的寄存器和宏定义
#define portNVIC_SYSTICK_CTRL_REG			( * ( ( volatile uint32_t * ) 0xe000e010 ) )
#define portNVIC_SYSTICK_LOAD_REG			( * ( ( volatile uint32_t * ) 0xe000e014 ) )

#ifndef configSYSTICK_CLOCK_HZ
	#define configSYSTICK_CLOCK_HZ configCPU_CLOCK_HZ
	/*确保SysTick的时钟频率与内核相同*/
	#define portNVIC_SYSTICK_CLK_BIT	( 1UL << 2UL )
#else
	/*SysTick的打卡方式不会被修改,以防它与内核不一样。*/
	#define portNVIC_SYSTICK_CLK_BIT	( 0 )
#endif

#define portNVIC_SYSTICK_INT_BIT			( 1UL << 1UL )
#define portNVIC_SYSTICK_ENABLE_BIT			( 1UL << 0UL )

SysTick 初始化函 数 vPortSetupTimerInterrupt() , 在**xPortStartScheduler()**中被调用。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5JNbyN4t-1682495949163)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230417170359749.png)]

设置重装载寄存器的值,决定 SysTick 的中断周期。同时 configTICK_RATE_HZ 也在 FreeRTOSConfig.h 中定义:

#define configCPU_CLOCK_HZ (( unsigned long ) 25000000) 
#define configTICK_RATE_HZ (( TickType_t ) 100)

在函数vPortSetupTimerInterrupt()中,配置SysTick 每秒中断多少次,目前配置为100,即每10ms中断一次,最后设置系统定时器的时钟等于内核时钟,使能 SysTick 定时器中断,使能 SysTick 定时器。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1aoZXpo8-1682495949164)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230417165015597.png)]

阻塞等待总结

在这里总结一下如何实现阻塞等待:

  • 在前面我们在配置时钟的时候得到了我们当前系统时钟与RTOS的时钟并非同一个时钟,RTOS使用的时钟为SysTick,后续跟rtos相关的API都是用改时钟源,例vtaskdelay。它会在每次计数的时候判断此时 就绪队列中是否有delay为0的任务如果有则进行调度。

  • 单片机系统的时钟源可以自己设置,比如TIMx,跟单片机相关的API都用该时钟源,例如hal delay,里面就是用TIMx来计数从而实现延时。

最后实现的效果伪代码如下:

vtaskdelay的流程:
{
    
    
    装入延时的值进行任务调度(假设目前只有task1和task2,此次发起延时的任务是task1,则调用YIELD之后会进行task2)
}

运行task2过程中:
{
    
    
    systick不断计数,每次计数都会判断task1的delay是否到期,到期则重新切换回task1
}

在这个过程中使用的一直为RTOS的时钟源。

0x03 多优先级

在就绪列表pxReadyTasksLists[ configMAX_PRIORITIES ]是一个数组,数组里面存的是就绪任务的 TCB(准确来说是 TCB 里面的 xStateListItem 节点),数组的下标对应任务的优先级,优先级越低对应的数组下标越小。空闲任务的优先级最低,对应的是下标为 0 的链表。任务在创建的时候,会根据任务的优先级将任务插入到就绪列表不同的位置。相同优先级的任务插入到就绪列表里面的同一条链表中,这个时候就需要用到时间片。

pxCurrenTCB 是一个全局的 TCB 指针,用于指向优先级最高的就绪任务的 TCB,即当前正在运行的 TCB 。要做到多优先级只需要在任务切换的时候将指针指向最高优先级的就绪任务TCB即可。

如果找到最高优先级的就绪任务的 TCB,在FreeRTOS上具有两套方法,于task.c中定义,查找最高优先级的就绪任务有两种方法 , 具 体 由configUSE_PORT_OPTIMISED_TASK_SELECTION 这个宏控制,定义为 0 选择通用方法,定义为 1 选择根据处理器优化的方法,该宏默认在 portmacro.h 中定义为 1,即使用优化过的方法:

#ifndef configUSE_PORT_OPTIMISED_TASK_SELECTION
	#define configUSE_PORT_OPTIMISED_TASK_SELECTION 1
#endif

通用方法:

taskRECORD_READY_PRIORITY()

此函数用于更新uxTopReadyPriority的值。这个是一个在task.c定义的静态变量,用于创建任务的最高优先级,默认初始化为0,即空闲任务的优先级:

#define tskIDLE_PRIORITY			( ( UBaseType_t ) 0U )
PRIVILEGED_DATA static volatile UBaseType_t uxTopReadyPriority 		= tskIDLE_PRIORITY;

通用方法:

#define taskRECORD_READY_PRIORITY( uxPriority )														\
{
      
      																									\
    if( ( uxPriority ) > uxTopReadyPriority )														\
    {
      
      																								\
        uxTopReadyPriority = ( uxPriority );														\
    }																								\
} /* taskRECORD_READY_PRIORITY */

taskSELECT_HIGHEST_PRIORITY_TASK()

#define taskSELECT_HIGHEST_PRIORITY_TASK()															\
{
      
      																									\
	UBaseType_t uxTopPriority = uxTopReadyPriority;														\
                                                                                                    \
    /* 从最高优先级对应的就绪列表数组下标开始寻找当前链表下是否有任务存在,如果没有,则 uxTopPriority 减一操作,继续寻找下一个优先级对应的链表中是否有任务存在,如果有则跳出 while 循环,表示找到了最高优先级的就绪任务。之所以可以采用从最高优先级往下搜索,是因为任务的优先级与就绪列表的下标是一一对应的,优先级越高,对应的就绪列表数组的下标越大。 */								\
    while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) )							\
    {
      
      																								\
        configASSERT( uxTopPriority );																\
        --uxTopPriority;																			\
    }																								\
                                                                                                    \
    /* 获取优先级最高的就绪任务的 TCB,然后更新到 pxCurrentTCB */									\
    listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );			\
    //更新uxTopReadyPriority
    uxTopReadyPriority = uxTopPriority;																\
} /* taskSELECT_HIGHEST_PRIORITY_TASK */

此函数用于查找优先级最高的就绪任务,更新uxTopReadyPrioritypxCurrentTCB的值。

优化方法:

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

如果 uxTopReadyPriority 的每个位号对应的是任务的优先级,任务就绪时,则将对应的位置 1,反之则清零。那么可以利用前导零指令计算出当前就绪任务的最高优先级,将高优先级的放在高位。

taskRECORD_READY_PRIORITY()

taskRECORD_READY_PRIORITY()用于根据传进来的形参(通常形参就是任务的优先级)将变量 uxTopReadyPriority 的某个位置 1。与通用方法中用来表示创建的任务的最高优先级不一样,它在优化方法中担任的是一个优先级位图表的角色,即该变量的每个位对应任务的优先级,如果任务就绪,则将对应的位置 1,反之清零。

其主要实现是靠如下的宏:

#define portRECORD_READY_PRIORITY( uxPriority, uxReadyPriorities ) ( uxReadyPriorities ) |= ( 1UL << ( uxPriority ) )

#define taskRESET_READY_PRIORITY( uxPriority )														\
{
      
      																									\
    if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ ( uxPriority ) ] ) ) == ( UBaseType_t ) 0 )	\
    {
      
      																								\
        portRESET_READY_PRIORITY( ( uxPriority ), ( uxTopReadyPriority ) );							\
    }																								\
}

taskRESET_READY_PRIORITY()

#define portRESET_READY_PRIORITY( uxPriority, uxReadyPriorities ) ( uxReadyPriorities ) &= ~( 1UL << ( uxPriority ) )

#define taskRESET_READY_PRIORITY( uxPriority )														\
{
      
      																									\
    if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ ( uxPriority ) ] ) ) == ( UBaseType_t ) 0 )	\
    {
      
      																								\
        portRESET_READY_PRIORITY( ( uxPriority ), ( uxTopReadyPriority ) );							\
    }																								\
}

此函数是将变量uxTopReadyPriority 的某个位清零。实际上根据优先级调用 taskRESET_READY_PRIORITY()函数复位 uxTopReadyPriority 变量中对应的位时,要先确保就绪列表中对应该优先级下的链表没有任务才行,如果此时列表中具有延时任务,不能将一个非就绪任务从就绪列表中移除,所以只能通过优先级的变量在对应的位中进行清零。

taskSELECT_HIGHEST_PRIORITY_TASK()

#define portGET_HIGHEST_PRIORITY( uxTopPriority, uxReadyPriorities ) uxTopPriority = ( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) )

#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 ] ) );		\
} /* 

根据 uxTopReadyPriority 的值,找到最高优先级,然后更新到uxTopPriority 这个局部变量中。之后根据 uxTopPriority 的值,从就绪列表中找到就绪的最高优先级的任务的 TCB,然后将 TCB 更新到 pxCurrentTCB。

prvAddTaskToReadyList()

根据优先级将位图uxTopReadyPriority中的对应位置位,并且根据优先级将任务插入就绪列表pxReadyTasksLists[]

#define prvAddTaskToReadyList( pxTCB )																\
	traceMOVED_TASK_TO_READY_STATE( pxTCB );														\
	taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority );												\
	vListInsertEnd( &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ), &( ( pxTCB )->xStateListItem ) ); \
	tracePOST_MOVED_TASK_TO_READY_STATE( pxTCB )

0x04 任务延时列表

工作原理:在FreeRTOS中,当任务需要延时的时候,先将任务进行挂起,即先将任务从就绪列表删除,然后插入到任务延时列表,同时更新下一个任务的解锁时刻变量:xNextTaskUnblockTime的值。

xNextTaskUnblockTime 的值等于系统时基计数器的值 xTickCount 加上任务需要延时的值 xTicksToDelay。当系统时基计数器xTickCount 的值与 xNextTaskUnblockTime 相等时,就表示有任务延时到期了,需要将该任务就绪。

任务延时列表表维护着一条双向链表,每个节点代表了正在延时的任务,节点按照延时时间大小做升序排列。当每次时基中断(SysTick 中断)来临时,就拿系统时基计数器的值 xTickCount 与下一个任务的解锁时刻变量 xNextTaskUnblockTime 的值相比较,如果相等,则表示有任务延时到期,需要将该任务就绪,否则只是单纯地更新系统时基计数器xTickCount 的值,然后进行任务切换。FreeRTOS定义了如下的延时列表:

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

前面为两个任务延时列表,当系统时基计数器xTickCount没有溢出时,只用一条列表,当xTickCount溢出后,用另外一条列表。

后面为指向这两个列表的指针。

任务延时列表初始化

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IweM4qv1-1682495949164)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230418113746933.png)]

使用函数vListInitialise进行初始化。

定义xNextTaskUnblockTime

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

xNextTaskUnblockTime 是一个在 task.c 中定义的静态变量,用于表示下一个任务的解锁时刻。xNextTaskUnblockTime 的值等于系统时基计数器的值 xTickCount 加上任务需要延时值 xTicksToDelay。当系统时基计数器 xTickCount 的值与 xNextTaskUnblockTime 相等时,就表示有任务延时到期了,需要将该任务就绪。

初始化xNextTaskUnblockTime位于函数vTaskStartScheduler()中,初始化为portMAX_DELAY,位于portmacro.h

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4ITaf3mO-1682495949165)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230418114253008.png)]

并且在延时函数vTaskDelay()中加入延时列表prvAddCurrentTaskToDelayedList

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qotkXcxa-1682495949165)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230418115318534.png)]

prvAddCurrentTaskToDelayedList()

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MlQVUthU-1682495949165)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230418200238699.png)]

通过判断当前时间是否已经满足等待的时间,如果是则加入溢出队列,否则时间还没到,继续加入等待队列。只需要让系统时基计数器 xTickCount 与 xNextTaskUnblockTime 的值先比较就知道延时最快结束的任务是否到期。

xTaskIncrementTick()

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mTJH5Ka4-1682495949166)(C:\Users\user\AppData\Roaming\Typora\typora-user-images\image-20230418200938069.png)]

对于此函数,主要是在判断变量xConstTickCount是否溢出,若溢出则切换延时列表。如果延时任务列表为空,则设置xNextTaskUnblockTime为可能的最大值。如果不为空,则要知道将延时任务中所有延时到期的任务移除才能跳出for循环。若找到已经延时结束的任务,则消除等待状态,将解除等待的任务添加到就绪列表。

taskSWITCH_DELAYED_LISTS()

#define taskSWITCH_DELAYED_LISTS()																	\
{
      
      																									\
	List_t *pxTemp;																					\
																									\
	/* The delayed tasks list should be empty when the lists are switched. */						\
	configASSERT( ( listLIST_IS_EMPTY( pxDelayedTaskList ) ) );										\
																									\
	pxTemp = pxDelayedTaskList;																		\
	pxDelayedTaskList = pxOverflowDelayedTaskList;													\
	pxOverflowDelayedTaskList = pxTemp;																\
	xNumOfOverflows++;																				\
	prvResetNextTaskUnblockTime();																	\
}

在这切换了延时列表,实际就是更换pxDelayedTaskListpxOverflowDelayedTaskList这两个指针的指向。之后复位xNextTaskUnblockTime的值。

static void prvResetNextTaskUnblockTime( void )
{
    
    
TCB_t *pxTCB;

	if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
	{
    
    
		/* 当前延时列表为空,则设置 xNextTaskUnblockTime 等于最大值 */
		xNextTaskUnblockTime = portMAX_DELAY;
	}
	else
	{
    
    
		/* 当前列表不为空,则有任务在延时,则获取当前列表下第一个节点的排序值然后将该节点的排序值更新到 xNextTaskUnblockTime */
		( pxTCB ) = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
		xNextTaskUnblockTime = listGET_LIST_ITEM_VALUE( &( ( pxTCB )->xStateListItem ) );
	}
}

taskRESET_READY_PRIORITY()

#define taskRESET_READY_PRIORITY( uxPriority )														\
{
      
      																									\
    if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ ( uxPriority ) ] ) ) == ( UBaseType_t ) 0 )	\
    {
      
      																								\
        portRESET_READY_PRIORITY( ( uxPriority ), ( uxTopReadyPriority ) );							\
    }																								\
}

需要判断就绪列表 pxReadyTasksLists[]在当前优先级下对应的链表的节点是否为 0,只有当该链表下没有任务时才真正地将任务在优先级位图表 uxTopReadyPriority 中对应的位清零。

0x05 时间片

在同一个优先级下可以有多个任务,每个任务轮流地享有相同的 CPU 时间,享有 CPU 的时间叫时间片。

在 RTOS 中,最小的时间单位为一个 tick,即 SysTick 的中断周期,RT-Thread 和 μC/OS 可以指定时间片的大小为多个 tick,但是 FreeRTOS 不一样,时间片只能是一个 tick。

如何实现一个优先级下多个任务的时间片处理,主要由函数taskRESET_READY_PRIORITY()和taskSELECT_HIGHEST_PRIORITY_TASK()这两个函数的实现方法。

系统在任务系统切换时,总会从就绪列表中寻找优先级最高的任务来执行,寻找优先级最高的任务使用函数taskSELECT_HIGHEST_PRIORITY_TASK

#define taskSELECT_HIGHEST_PRIORITY_TASK()														\
{
      
      																								\
	UBaseType_t uxTopPriority;																		\
                                                                                                \
    /* 寻找就绪任务的最高优先级 */								\
    portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority );								\
    configASSERT( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ uxTopPriority ] ) ) > 0 );		\
    //  获取优先级最高的就绪任务的 TCB,然后更新到 pxCurrentTCB
    listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) );		\
} /* taskSELECT_HIGHEST_PRIORITY_TASK() */

如果当前优先级上具有任务1和任务2,那么如何进行选择和切换其任务,具体在函数listGET_OWNER_OF_NEXT_ENTRY

#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList )										\
{
      
      																							\
	List_t * const pxConstList = ( pxList );													\
	/* 节点索引指向链表第一个节点调整节点索引指针,指向下一个节点,如果当前链表有 N 个节点,当第 N 次调用该函数时,pxIndex 则指向第 N 个节点*/				\						\
	( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;							\
	//  当遍历完链表后,pxIndex 回指到根节点
     if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) )	\
	{
    
    																						\
		( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;						\
	}																						\
	// 获取节点的 OWNER,即 TCB
   	( pxTCB ) = ( pxConstList )->pxIndex->pvOwner;											\
}

在这个函数中不断执行链表的遍历操作,每个任务都轮流执行,享有相同的CPU时间,即所谓的时间片。

其实 FreeRTOS 的这种时间片功能不能说是真正意义的时间片,因为它不能随意的设置时间为多少个 tick,而是默认一个 tick,然后默认在每个 tick 中断周期中进行任务切换而已。

0x06 总结

对于具有多个优先级的任务,他们是通过设置优先级进行抢占式运行,但是对于同个优先级下的任务,则是通过时间片分配来执行的。但如果你的同优先级的任务中加入了死循环,那么在这个优先级后的任务则无法执行,所以这个时候需要函数vTaskDelay()来将任务进行挂起,之后执行低优先级的任务,才可以顺序执行。

猜你喜欢

转载自blog.csdn.net/Alkaid2000/article/details/130388184