FreeRTOS——空閒任務與阻塞延遲實現

在裸機運行中,我們是使用軟件延時來實現延時的功能(delay()),即是讓CPU空等來達到延時的目的。使用RTOS的很大優勢就是榨乾CPU性能,永遠不讓他閒着,任務需要延時也就不需要讓CPU空等來實現延時的效果。
RTOS中的延時叫做阻塞延時,即任務需要延時的時候,任務會放棄CPU的使用權,CPU可以去幹其他的事情,當任務延時時間到,重新獲取CPU使用權,任務繼續運行。這樣就可以充分利用CPU資源了。
當任務進入阻塞狀態,如果沒有其他任務可以運行,RTOS都會爲CPU創建一個空閒任務,這個時候CPU就去運行空閒任務。在FreeRTOS中,空閒任務是系統在【啓動調度器】的時候創建的優先級最低的任務,空閒任務主要做一些系統內存清理的任務。

一、實現空閒任務

目前我們在創建任務時使用的棧和TCB都是使用的靜態內存,即預先定義好的內存,空閒任務也不例外。

1、定義空閒任務的棧

空閒任務的棧在main.c 中定義:

/* 定義空閒任務的棧 */
#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 ) (2)
StackType_t IdleTaskStack[configMINIMAL_STACK_SIZE]; (1)

(1):空閒任務的棧是一個定義好的數組,大小由FreeRTOSConfig.h 中
定義的宏 configMINIMAL_STACK_SIZE 控制,大小爲128,單位是字。

2、定義空閒任務的任務控制塊

空閒任務的任務控制塊在main.c中定義:

/* 定義空閒任務的任務控制塊 */
TCB_t IdleTaskTCB;

3、定義空閒任務的任務控制塊

空閒任務在調度器啓動函數vTaskStartScheduler()中創建:

extern TCB_t IdleTaskTCB;
void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer,
									StackType_t **ppxIdleTaskStackBuffer,
									uint32_t *pulIdleTaskStackSize );
									void vTaskStartScheduler( void )
{
 /*=======================創建空閒任務 start=======================*/
 TCB_t *pxIdleTaskTCBBuffer = NULL; /* 用於指向空閒任務控制塊 */
 StackType_t *pxIdleTaskStackBuffer = NULL; /* 用於空閒任務棧起始地址 */
 uint32_t ulIdleTaskStackSize;

 /* 獲取空閒任務的內存:任務棧和任務 TCB */ (1)
 vApplicationGetIdleTaskMemory( &pxIdleTaskTCBBuffer,
								&pxIdleTaskStackBuffer,
								&ulIdleTaskStackSize );
 /* 創建空閒任務 */ (2)
 xIdleTaskHandle =
 xTaskCreateStatic( (TaskFunction_t)prvIdleTask, /* 任務入口 */
					(char *)"IDLE", /* 任務名稱,字符串形式 */
					(uint32_t)ulIdleTaskStackSize , /* 任務棧大小,單位爲字 */
					(void *) NULL, /* 任務形參 */
					(StackType_t *)pxIdleTaskStackBuffer, /* 任務棧起始地址 */
					(TCB_t *)pxIdleTaskTCBBuffer ); /* 任務控制塊 */
 /* 將任務添加到就緒列表開頭 */ (3)
 vListInsertEnd( &( pxReadyTasksLists[0] ),&( ((TCB_t *)pxIdleTaskTCBBuffer)->xStateListItem ) );
 /*==========================創建空閒任務 end=====================*/

 /* 手動指定第一個運行的任務 */
 pxCurrentTCB = &Task1TCB;

 /* 啓動調度器 */
 if ( xPortStartScheduler() != pdFALSE )
 {
 /* 調度器啓動成功,則不會返回,即不會來到這裏 */
 }
}

(1):獲 取 空 閒 任 務 的 內 存 , 即 將 pxIdleTaskTCBBuffer 和pxIdleTaskStackBuffer 這兩個接下來要作爲形參傳到xTaskCreateStatic()函數的指針分別指向空閒任務的 TCB 和棧的起始地址,這個操作由函數 vApplicationGetIdleTaskMemory()來實現:
vApplicationGetIdleTaskMemory():

void vApplicationGetIdleTaskMemory( TCB_t **ppxIdleTaskTCBBuffer,
									StackType_t **ppxIdleTaskStackBuffer,
									uint32_t *pulIdleTaskStackSize )
{
 *ppxIdleTaskTCBBuffer=&IdleTaskTCB;
 *ppxIdleTaskStackBuffer=IdleTaskStack;
 *pulIdleTaskStackSize=configMINIMAL_STACK_SIZE;
}

(2):調用 xTaskCreateStatic()函數創建空閒任務。
(3):將空閒任務插入到就緒列表的開頭。

二、實現阻塞延時

1、vTaskDelay ()函數

阻塞延時的阻塞是指任務調用該延時函數後,任務會被剝離CPU使用權,然後進入阻塞狀態,直到延時結束,任務重新獲取CPU使用權纔可以繼續運行。在任務阻塞這段時間,CPU可以去執行其他的任務,如果其他任務也處於阻塞狀態,CPU就會執行空閒任務。

void vTaskDelay( const TickType_t xTicksToDelay )
{
 TCB_t *pxTCB = NULL;
 /* 獲取當前任務的 TCB */
 pxTCB = pxCurrentTCB; (1)
 /* 設置延時時間 */
 pxTCB->xTicksToDelay = xTicksToDelay; (2)
 /* 任務切換 */
 taskYIELD(); (3)
}

(1):獲取當前任務控制塊。
(2):xTicksToDelay是任務控制塊的一個成員,用於記錄需要延時的時間,單位爲SysTick的中斷週期。
xTicksToDelay定義:

typedef struct tskTaskControlBlock
{
 volatile StackType_t *pxTopOfStack; /* 棧頂 */
 ListItem_t xStateListItem; /* 任務節點 */
 StackType_t *pxStack; /* 任務棧起始地址 */
 char pcTaskName[ configMAX_TASK_NAME_LEN ];/* 任務名稱,字符串形式 */
 TickType_t xTicksToDelay; /* 用於延時 */
} tskTCB;

(3):任務切換。調用taskYIELD()會產生PendSV中斷,在PendSV中斷服務函數終會調用任務切換函數vTaskSwitchContext(),該任務的作用是尋找最高優先級的就緒任務,然後更新pxCurrentTCB.因爲這一章比上一章多一個空閒任務,需要讓pxCurrentTCB 在這三個任務中切換, 算法需要改變,vTaskSwitchContext()函數修改如下:

void vTaskSwitchContext( void )
{
 /* 如果當前任務是空閒任務,那麼就去嘗試執行任務 1 或者任務 2,  
 看看他們的延時時間是否結束,如果任務的延時時間均沒有到期,
 那就返回繼續執行空閒任務 */
 if ( pxCurrentTCB == &IdleTaskTCB ) (1)
 {
 	if (Task1TCB.xTicksToDelay == 0)
 	{
  		pxCurrentTCB =&Task1TCB;
 	}
 	else if (Task2TCB.xTicksToDelay == 0)
 	{
 		pxCurrentTCB =&Task2TCB;
 	}
 	else
 	{
 		return; /* 任務延時均沒有到期則返回,繼續執行空閒任務 */
 	}
 }
 else /* 當前任務不是空閒任務則會執行到這裏 */ (2)
 {
	 /*如果當前任務是任務 1 或者任務 2 的話,檢查下另外一個任務,
	 如果另外的任務不在延時中,就切換到該任務
	 否則,判斷下當前任務是否應該進入延時狀態,
	 如果是的話,就切換到空閒任務。否則就不進行任何切換 */
	 if (pxCurrentTCB == &Task1TCB)
	 {
 		if (Task2TCB.xTicksToDelay == 0)
	 	{
	  		pxCurrentTCB =&Task2TCB;
	 	}
	 	else if (pxCurrentTCB->xTicksToDelay != 0)
	 	{
	 		pxCurrentTCB = &IdleTaskTCB;
	 	}
 		else
		{
		 return; /* 返回,不進行切換,因爲兩個任務都處於延時中 */
		}
 	}
	 else if (pxCurrentTCB == &Task2TCB)
	 {
		 if (Task1TCB.xTicksToDelay == 0)
		 {
		 	pxCurrentTCB =&Task1TCB;
		  }
		 else if (pxCurrentTCB->xTicksToDelay != 0)
		 {
			 pxCurrentTCB = &IdleTaskTCB;
		 }
	 	else
		 {
		 	return; /* 返回,不進行切換,因爲兩個任務都處於延時中 */
		 }
	 }
 }
}

三、SysTick 中斷服務函數

1、SysTick 中斷服務函數

在任務上下文切換函數 vTaskSwitchContext ()中,會判斷每個任務的任務控制塊中的延時成員xTicksToDelay的值是否爲0,如果爲0就要將對應的任務就緒,如果不爲0就繼續延時。如果一個任務延時,一開始xTicksToDelay不爲0,當xTicksToDelay變爲0後表示延時結束。在FreeRTOS中,操作系統中的最小時間單位是SysTick的中斷週期,我們稱之爲一個tick ,xTicksToDelay就是以tick爲週期遞減的。
SysTick 中斷服務函數

void xPortSysTickHandler( void )
{
 /* 進入臨界段,關中斷 */
 vPortRaiseBASEPRI(); 
 /* 更新系統時基 */
 xTaskIncrementTick(); 
 /* 退出臨界中斷,開中斷 */
 vPortClearBASEPRIFromISR(); 
}

xTaskIncrementTick()函數

void xTaskIncrementTick( void )
{
 TCB_t *pxTCB = NULL;
 BaseType_t i = 0;

 /* 更新系統時基計數器 xTickCount, xTickCount 是一個在 port.c 中定義的全局變量 */(1)
 const TickType_t xConstTickCount = xTickCount + 1;
 xTickCount = xConstTickCount;

 /* 掃描就緒列表中所有任務的 xTicksToDelay,如果不爲 0,則減 1 */(2)
 for (i=0; i<configMAX_PRIORITIES; i++)
 {
	 pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( ( &pxReadyTasksLists[i] ) );
	 if (pxTCB->xTicksToDelay > 0)
	 {
	 pxTCB->xTicksToDelay --;
	 }
 }

 /* 執行一次任務切換 */(3)
 portYIELD();
}

2、SysTick 初始化函數

SysTick 的中斷服務函數要想被順利執行,則 SysTick 必須先初始化SysTick 初始化函數在 port.c 中定義。
vPortSetupTimerInterrupt()函數:

/* SysTick 控制寄存器 */ (1)
#define portNVIC_SYSTICK_CTRL_REG (*((volatile uint32_t *) 0xe000e010 ))
/* SysTick 重裝載寄存器寄存器 */
#define portNVIC_SYSTICK_LOAD_REG (*((volatile uint32_t *) 0xe000e014 ))

/* SysTick 時鐘源選擇 */
#ifndef configSYSTICK_CLOCK_HZ
	#define configSYSTICK_CLOCK_HZ configCPU_CLOCK_HZ
	/* 確保 SysTick 的時鐘與內核時鐘一致 */
	#define portNVIC_SYSTICK_CLK_BIT ( 1UL << 2UL )
#else
	#define portNVIC_SYSTICK_CLK_BIT ( 0 )
 #endif

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

void vPortSetupTimerInterrupt( void ) (2)
{
 /* 設置重裝載寄存器的值 */ (2)-①
 portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;

 /* 設置系統定時器的時鐘等於內核時鐘 (2)-②
 使能 SysTick 定時器中斷
 使能 SysTick 定時器 */
 portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT |
 portNVIC_SYSTICK_INT_BIT |
 portNVIC_SYSTICK_ENABLE_BIT );
}

SysTick 初 始 化 函 數 vPortSetupTimerInterrupt() , 在
xPortStartScheduler()中被調用。

BaseType_t xPortStartScheduler( void )
{
 /* 配置 PendSV 和 SysTick 的中斷優先級爲最低 */
 portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
 portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;
 /* 初始化 SysTick */
 vPortSetupTimerInterrupt();
 /* 啓動第一個任務,不再返回 */
 prvStartFirstTask();
 /* 不應該運行到這裏 */
 return 0;
}

(2)-①:設置重裝載寄存器的值,決定SysTick 的中斷週期。
configCPU_CLOCK_HZ 與 configTICK_RATE_HZ 宏定義:

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

(1):系統時鐘大小,目前是軟件仿真,需要配置成與system_ARMCM3.c文件中的 SYSTEM_CLOCK的一樣, 即等於 25M。如果有具體的硬件,則配置成與硬件系統時鐘一樣
(2):SysTick 每秒中斷多少次,目前配置爲 100,即每 10ms 中斷一次。
(2)-②:設置系統定時器的時鐘等於內核時鐘,使能 SysTick 定時器中斷,使能 SysTick 定時器。

參考:[野火®]《FreeRTOS 內核實現與應用開發實戰—基於STM32》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章