FreeRTOS 筆記之④:任務的定義與任務的切換

目錄

1. 任務是什麼

2. 創建任務

2.1 定義任務棧

2.2 定義任務函數

2.3 定義任務控制塊

2.4  實現任務創建函數

2.4.1 xTaskCreateStatic()函數

2.4.2 prvInitialiseNewTask()函數

2.4.3 pxPortInitialiseStack()函數

3. 實現就緒列表

3.1 定義就緒列表

3.2 就緒列表初始化

3.3 將任務插入到就緒列表

4. 實現調度器

4.1 啓動調度器

4.1.1 vTaskStartScheduler()函數

4.1.2 xPortStartScheduler()函數

4.1.3 prvStartFirstTask()函數

4.1.4 vPortSVCHandler()函數

4.2 任務切換

4.2.1 taskYIELD()

4.2.2 xPortPendSVHandler()函數

4.2.3 TaskSwitchContext()函數

5. 例程試驗

6. SVC 和PendSV


1. 任務是什麼

在裸機系統中,系統的主體就是main函數裏面順序執行的無限循環,這個無限循環裏面CPU按照順序完成各種事情。在多任務系統中,我們根據功能的不同,把整個系統分割成一個個獨立的且無法返回的函數,這個函數我們稱爲任務。

任務(task),是抽象的東西,並沒有一個嚴格的定義,一般是指由軟件完成的一個活動。對於類似freeRTOS的系統來說,任務即線程/進程。

任務的大概形式具體見代碼

void task_entry( void * parg)
{
    /* 任務主體,無限循環且不能返回 */
    for (;;) {
        /* 任務主體代碼 */
    }
}

2. 創建任務

2.1 定義任務棧

在一個裸機系統中,如果有全局變量,有子函數調用,有中斷髮生。那麼系統在運行的時候,全局變量放在哪裏,子函數調用時,局部變量放在哪裏,中斷髮生時,函數返回地址放哪裏。如果只是單純的裸機編程,它們放哪裏我們不用管,但是如果要寫一個RTOS,這些種種環境參數,我們必須弄清楚他們是如何存儲的。在裸機系統中,他們統統放在一個叫棧的地方,棧是單片機RAM裏面一段連續的內存空間,棧的大小一般在啓動文件或者鏈接腳本里面指定,最後由C庫函數_main進行初始化。

但是,在多任務系統中,每個任務都是獨立的,互不干擾的,所以要爲每個任務都分配獨立的棧空間,這個棧空間通常是一個預先定義好的全局數組,也可以是動態分配的一段內存空間,但它們都存在於RAM中。

實現兩個變量按照一定的頻率輪流的翻轉,每個變量對應一個任務,那麼就需要定義兩個任務棧,具體見代碼清單。在多任務系統中,有多少個任務就需要定義多少個任務棧。

#define TASK1_STACK_SIZE                    128
StackType_t Task1Stack[TASK1_STACK_SIZE];

#define TASK2_STACK_SIZE                    128
StackType_t Task2Stack[TASK2_STACK_SIZE];

任務棧其實就是一個預先定義好的全局數據,數據類型爲 StackType_t,大小由TASK1_STACK_SIZE這個宏來定義,默認爲128,單位爲字,即512字節,這也是FreeRTOS推薦的最小的任務棧。

2.2 定義任務函數

任務是一個獨立的函數,函數主體無限循環且不能返回。

/* 軟件延時 */
void delay (uint32_t count)
{
	for(; count!=0; count--);
}
/* 任務1 */
void Task1_Entry( void *p_arg )
{
	for( ;; )
	{
		flag1 = 1;
		delay( 100 );		
		flag1 = 0;
		delay( 100 );
	}
}

/* 任務2 */
void Task2_Entry( void *p_arg )
{
	for( ;; )
	{
		flag2 = 1;
		delay( 100 );		
		flag2 = 0;
		delay( 100 );
	}
}

2.3 定義任務控制塊

在裸機系統中,程序的主體是CPU按照順序執行的。而在多任務系統中,任務的執行是由系統調度的。系統爲了順利的調度任務,爲每個任務都額外定義了一個任務控制塊,這個任務控制塊就相當於任務的身份證,裏面存有任務的所有信息,比如任務的棧指針,任務名稱,任務的形參等。有了這個任務控制塊之後,以後系統對任務的全部操作都可以通過這個任務控制塊來實現。定義一個任務控制塊需要一個新的數據類型,該數據類型在task.c這C頭文件中聲明

typedef struct tskTaskControlBlock
{
	volatile StackType_t    *pxTopOfStack;    /* 棧頂 */

	ListItem_t			    xStateListItem;   /* 任務節點 */
    
    StackType_t             *pxStack;         /* 任務棧起始地址 */
	                                          /* 任務名稱,字符串形式 */
	char                    pcTaskName[ configMAX_TASK_NAME_LEN ];  
} tskTCB;
typedef tskTCB TCB_t;
TCB_t Task1TCB;

TCB_t Task2TCB;

2.4  實現任務創建函數

任務的棧,任務的函數實體,任務的控制塊最終需要聯繫起來才能由系統進行統一調度。那麼這個聯繫的工作就由任務創建函數xTaskCreateStatic()來實現,該函數在task.c定義。

2.4.1 xTaskCreateStatic()函數

#if( configSUPPORT_STATIC_ALLOCATION == 1 )

TaskHandle_t xTaskCreateStatic(	TaskFunction_t pxTaskCode,           /* 任務入口 */
					            const char * const pcName,           /* 任務名稱,字符串形式 */
					            const uint32_t ulStackDepth,         /* 任務棧大小,單位爲字 */
					            void * const pvParameters,           /* 任務形參 */
					            StackType_t * const puxStackBuffer,  /* 任務棧起始地址 */
					            TCB_t * const pxTaskBuffer )         /* 任務控制塊指針 */
{
	TCB_t *pxNewTCB;
	TaskHandle_t xReturn;

	if( ( pxTaskBuffer != NULL ) && ( puxStackBuffer != NULL ) )
	{		
		pxNewTCB = ( TCB_t * ) pxTaskBuffer; 
		pxNewTCB->pxStack = ( StackType_t * ) puxStackBuffer;

		/* 創建新的任務 */
		prvInitialiseNewTask( pxTaskCode,        /* 任務入口 */
                              pcName,            /* 任務名稱,字符串形式 */
                              ulStackDepth,      /* 任務棧大小,單位爲字 */ 
                              pvParameters,      /* 任務形參 */
                              &xReturn,          /* 任務句柄 */ 
                              pxNewTCB);         /* 任務棧起始地址 */      

	}
	else
	{
		xReturn = NULL;
	}

	/* 返回任務句柄,如果任務創建成功,此時xReturn應該指向任務控制塊 */
    return xReturn;
}

#endif /* configSUPPORT_STATIC_ALLOCATION */
  • FreeRTOS中,任務的創建有兩種方法,一種是使用動態創建,一種是使用靜態創建。動態創建時,任務控制塊和棧的內存是創建任務時動態分配的,任務刪除時,內存可以釋放。靜態創建時,任務控制塊和棧的內存需要事先定義好,是靜態的內存,任務刪除時,內存不能釋放。目前以靜態創建爲例來講解,configSUPPORT_STATIC_ALLOCATION在FreeRTOSConfig.h中定義,我們配置爲1。
  • 任務入口,即任務的函數名稱。TaskFunction_t是在projdefs.h(projdefs.h第一次使用需要在include文件夾下面新建然後添加到工程freertos/source這個組文件)中重定義的一個數據類型,實際就是空指針。
#ifndef PROJDEFS_H
#define PROJDEFS_H

typedef void (*TaskFunction_t)( void * );

#define pdFALSE			( ( BaseType_t ) 0 )
#define pdTRUE			( ( BaseType_t ) 1 )

#define pdPASS			( pdTRUE )
#define pdFAIL			( pdFALSE )


#endif /* PROJDEFS_H */
  • 調用prvInitialiseNewTask()函數,創建新任務,該函數在task.c實現

2.4.2 prvInitialiseNewTask()函數

static void prvInitialiseNewTask( 	TaskFunction_t pxTaskCode,              /* 任務入口 */
									const char * const pcName,              /* 任務名稱,字符串形式 */
									const uint32_t ulStackDepth,            /* 任務棧大小,單位爲字 */
									void * const pvParameters,              /* 任務形參 */
									TaskHandle_t * const pxCreatedTask,     /* 任務句柄 */
									TCB_t *pxNewTCB )                       /* 任務控制塊指針 */

{
	StackType_t *pxTopOfStack;
	UBaseType_t x;	
	
	/* 獲取棧頂地址 */
	pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 );
	//pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
	/* 向下做8字節對齊 */
	pxTopOfStack = ( StackType_t * ) ( ( ( uint32_t ) pxTopOfStack ) & ( ~( ( uint32_t ) 0x0007 ) ) );	

	/* 將任務的名字存儲在TCB中 */
	for( x = ( UBaseType_t ) 0; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ )
	{
		pxNewTCB->pcTaskName[ x ] = pcName[ x ];

		if( pcName[ x ] == 0x00 )
		{
			break;
		}
	}
	/* 任務名字的長度不能超過configMAX_TASK_NAME_LEN */
	pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0';

    /* 初始化TCB中的xStateListItem節點 */
    vListInitialiseItem( &( pxNewTCB->xStateListItem ) );
    /* 設置xStateListItem節點的擁有者 */
	listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );
    
    
    /* 初始化任務棧 */
	pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );   


	/* 讓任務句柄指向任務控制塊 */
    if( ( void * ) pxCreatedTask != NULL )
	{		
		*pxCreatedTask = ( TaskHandle_t ) pxNewTCB;
	}
}
  • 將棧頂指針向下做8字節對齊。在Cortex-M3(Cortex-M4或Cortex-M7)內核的單片機中,因爲總線寬度是32位的,通常只要棧保持4字節對齊就行,可這樣爲啥要8字節?難道有哪些操作是64位的?確實有,那就是浮點運算,所以要8字節對齊(沒有涉及到浮點運算,只是爲了後續兼容浮點運行的考慮)。如果棧頂指針是8字節對齊的,在進行向下8字節對齊的時候,指針不會移動,如果不是8字節對齊的,在做向下8字節對齊的時候,就會空出幾個字節,不會使用,比如當pxTopOfStack是33,明顯不能整除8,進行向下8字節對齊就是32,那麼就會空出一個字節不使用
  • 調用pxPortInitialiseStack()函數初始化任務棧,並更新棧頂指針,任務第一次運行的環境參數就存在任務棧中。該函數在port.c(port.c第一次使用需要在freertos\portable\RVDS\ARM_CM3(ARM_CM4或ARM_CM7)文件夾下面新建然後添加到工程freertos/source這個組文件)中定義

2.4.3 pxPortInitialiseStack()函數

完整任務棧空間的初始化

static void prvTaskExitError( void )
{
    /* 函數停止在這裏 */
    for(;;);
}

StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters )
{
    /* 異常發生時,自動加載到CPU寄存器的內容 */
	pxTopOfStack--;
	*pxTopOfStack = portINITIAL_XPSR;	                                    /* xPSR的bit24必須置1 */
	pxTopOfStack--;
	*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK;	/* PC,即任務入口函數 */
	pxTopOfStack--;
	*pxTopOfStack = ( StackType_t ) prvTaskExitError;	                    /* LR,函數返回地址 */
	pxTopOfStack -= 5;	/* R12, R3, R2 and R1 默認初始化爲0 */
	*pxTopOfStack = ( StackType_t ) pvParameters;	                        /* R0,任務形參 */
    
    /* 異常發生時,手動加載到CPU寄存器的內容 */    
	pxTopOfStack -= 8;	/* R11, R10, R9, R8, R7, R6, R5 and R4默認初始化爲0 */

	/* 返回棧頂指針,此時pxTopOfStack指向空閒棧 */
    return pxTopOfStack;
}

不熟悉其中斷/異常處理的原理的可能會產生疑問,爲何要這樣操作,那麼這個答案就需要在《Cortex-M3權威指南.中斷的具體行爲》中查找,詳細說明了,入棧的順序以及入棧後堆棧中的內容,及異常返回。

3. 實現就緒列表

3.1 定義就緒列表

任務創建好之後,我們需要把任務添加到就緒列表裏面,表示任務已經就緒,系統隨時可以調度。就緒列表在task.c中定義。

/* 任務就緒列表 */
List_t pxReadyTasksLists[ configMAX_PRIORITIES ];

就緒列表實際上就是一個List_t類型的數組,數組的大小由決定最大任務優先級的宏configMAX_PRIORITIES決定,configMAX_PRIORITIES在FreeRTOSConfig.h中默認定義爲5,最大支持256個優先級。數組的下標對應了任務的優先級,同一優先級的任務統一插入到就緒列表的同一條鏈表中。

                                                                                                  空的就緒列表

3.2 就緒列表初始化

就緒列表在使用前需要先初始化,就緒列表初始化的工作在函數prvInitialiseTaskLists()裏面實現。初始化完畢,見示意圖。

/* 初始化任務相關的列表 */
void prvInitialiseTaskLists( void )
{
    UBaseType_t uxPriority;
    
    
    for( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ )
	{
		vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );
	}
}

3.3 將任務插入到就緒列表

任務控制塊裏面有一個xStateListItem成員,數據類型爲ListItem_t,我們將任務插入到就緒列表裏面,就是通過將任務控制塊的xStateListItem這個節點插入到就緒列表中來實現的。如果把就緒列表比作是晾衣架,任務是衣服,那xStateListItem就是晾衣架上面的鉤子,每個任務都自帶晾衣架鉤子,就是爲了把自己掛在各種不同的鏈表中。

    
    /* 初始化與任務相關的列表,如就緒列表 */
    prvInitialiseTaskLists();
    
    /* 創建任務 */
    Task1_Handle = xTaskCreateStatic( (TaskFunction_t)Task1_Entry,   /* 任務入口 */
					                  (char *)"Task1",               /* 任務名稱,字符串形式 */
					                  (uint32_t)TASK1_STACK_SIZE ,   /* 任務棧大小,單位爲字 */
					                  (void *) NULL,                 /* 任務形參 */
					                  (StackType_t *)Task1Stack,     /* 任務棧起始地址 */
					                  (TCB_t *)&Task1TCB );          /* 任務控制塊 */
    /* 將任務添加到就緒列表 */                                 
    vListInsertEnd( &( pxReadyTasksLists[1] ), &( ((TCB_t *)(&Task1TCB))->xStateListItem ) );
                                
    Task2_Handle = xTaskCreateStatic( (TaskFunction_t)Task2_Entry,   /* 任務入口 */
					                  (char *)"Task2",               /* 任務名稱,字符串形式 */
					                  (uint32_t)TASK2_STACK_SIZE ,   /* 任務棧大小,單位爲字 */
					                  (void *) NULL,                 /* 任務形參 */
					                  (StackType_t *)Task2Stack,     /* 任務棧起始地址 */
					                  (TCB_t *)&Task2TCB );          /* 任務控制塊 */
    /* 將任務添加到就緒列表 */                                 
    vListInsertEnd( &( pxReadyTasksLists[2] ), &( ((TCB_t *)(&Task2TCB))->xStateListItem ) );

就緒列表的下標對應的是任務的優先級,但是目前我們的任務還不支持優先級 

4. 實現調度器

調度器是操作系統的核心,其主要功能就是實現任務的切換,即從就緒列表裏面找到優先級最高的任務,然後去執行該任務。從代碼上來看,調度器無非也就是由幾個全局變量和一些可以實現任務切換的函數組成,全部都在task.c文件中實現。

4.1 啓動調度器

調度器的啓動由vTaskStartScheduler()函數來完成,該函數在task.c中定義

4.1.1 vTaskStartScheduler()函數

void vTaskStartScheduler( void )
{
    /* 手動指定第一個運行的任務 */
    pxCurrentTCB = &Task1TCB;
    
    /* 啓動調度器 */
    if( xPortStartScheduler() != pdFALSE )
    {
        /* 調度器啓動成功,則不會返回,即不會來到這裏 */
    }
}

4.1.2 xPortStartScheduler()函數

BaseType_t xPortStartScheduler( void )
{
    /* 配置PendSV 和 SysTick 的中斷優先級爲最低 */
	portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
	portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI;

	/* 啓動第一個任務,不再返回 */
	prvStartFirstTask();

	/* 不應該運行到這裏 */
	return 0;
}
  • 配置PendSV 和 SysTick 的中斷優先級爲最低。SysTick和PendSV都會涉及到系統調度,系統調度的優先級要低於系統的其它硬件中斷優先級,即優先相應系統中的外部硬件中斷,所以SysTick和PendSV的中斷優先級配置爲最低。
  • 調用函數prvStartFirstTask()啓動第一個任務,啓動成功後,則不再返回,該函數由彙編編寫,在port.c實現

4.1.3 prvStartFirstTask()函數

prvStartFirstTask()函數用於開始第一個任務,主要做了兩個動作,一個是更新MSP的值,二是產生SVC系統調用,然後去到SVC的中斷服務函數裏面真正切換到第一個任務。

/*
 * 參考資料《STM32F10xxx Cortex-M3 programming manual》4.4.3,百度搜索“PM0056”即可找到這個文檔
 * 在Cortex-M中,內核外設SCB的地址範圍爲:0xE000ED00-0xE000ED3F
 * 0xE000ED008爲SCB外設中SCB_VTOR這個寄存器的地址,裏面存放的是向量表的起始地址,即MSP的地址
 */
__asm void prvStartFirstTask( void )
{
	PRESERVE8

	/* 在Cortex-M中,0xE000ED08是SCB_VTOR這個寄存器的地址,
       裏面存放的是向量表的起始地址,即MSP的地址 */
	ldr r0, =0xE000ED08
	ldr r0, [r0]
	ldr r0, [r0]

	/* 設置主堆棧指針msp的值 */
	msr msp, r0
    
	/* 使能全局中斷 */
	cpsie i
	cpsie f
	dsb
	isb
	
    /* 調用SVC去啓動第一個任務 */
	svc 0  
	nop
	nop
}
  • 當前棧需按照8字節對齊,如果都是32位的操作則4個字節對齊即可。在Cortex-M中浮點運算是8字節的。
  • 在Cortex-M中,0xE000ED08是SCB_VTOR寄存器的地址,裏面存放的是向量表的起始地址,即MSP的地址。向量表通常是從內部FLASH的起始地址開始存放,那麼可知memory:0x00000000處存放的就是MSP的值

4.1.4 vPortSVCHandler()函數

VC中斷要想被成功響應,其函數名必須與向量表註冊的名稱一致,在啓動文件的向量表中,SVC的中斷服務函數註冊的名稱是SVC_Handler,所以SVC中斷服務函數的名稱我們應該寫成SVC_Handler,但是在FreeRTOS中,官方版本寫的是vPortSVCHandler(),爲了能夠順利的響應SVC中斷,我們有兩個選擇,改中斷向量表中SVC的註冊的函數名稱或者改FreeRTOS中SVC的中斷服務名稱。這裏,我們採取第二種方法,即在FreeRTOSConfig.h中添加添加宏定義的方法來修改。

#define xPortPendSVHandler   PendSV_Handler
#define xPortSysTickHandler  SysTick_Handler
#define vPortSVCHandler      SVC_Handler
__asm void vPortSVCHandler( void )
{
    extern pxCurrentTCB;
    
    PRESERVE8

	ldr	r3, =pxCurrentTCB	/* 加載pxCurrentTCB的地址到r3 */
	ldr r1, [r3]			/* 加載pxCurrentTCB到r1 */
	ldr r0, [r1]			/* 加載pxCurrentTCB指向的值到r0,目前r0的值等於第一個任務堆棧的棧頂 */
	ldmia r0!, {r4-r11}		/* 以r0爲基地址,將棧裏面的內容加載到r4~r11寄存器,同時r0會遞增 */
	msr psp, r0				/* 將r0的值,即任務的棧指針更新到psp */
	isb
	mov r0, #0              /* 設置r0的值爲0 */
	msr	basepri, r0         /* 設置basepri寄存器的值爲0,即所有的中斷都沒有被屏蔽 */
	orr r14, #0xd           /* 當從SVC中斷服務退出前,通過向r14寄存器最後4位按位或上0x0D,
                               使得硬件在退出時使用進程堆棧指針PSP完成出棧操作並返回後進入線程模式、返回Thumb狀態 */
    
	bx r14                  /* 異常返回,這個時候棧中的剩下內容將會自動加載到CPU寄存器:
                               xPSR,PC(任務入口地址),R14,R12,R3,R2,R1,R0(任務的形參)
                               同時PSP的值也將更新,即指向任務棧的棧頂 */
}

                                                                             任務棧初始化完後空間分佈圖

                                                                        第一個任務啓動成功後,psp的指向

4.2 任務切換

任務切換就是在就緒列表中尋找優先級最高的就緒任務,然後去執行該任務。但是目前我們還不支持優先級,僅實現兩個任務輪流切換,任務切換函數taskYIELD()。

4.2.1 taskYIELD()

#define taskYIELD()			portYIELD()

#define portYIELD()																\
{																				\
	/* 觸發PendSV,產生上下文切換 */								                \
	portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;								\
	__dsb( portSY_FULL_READ_WRITE );											\
	__isb( portSY_FULL_READ_WRITE );											\
}

portYIELD的實現很簡單,實際就是將PendSV的懸起位置1,當沒有其它中斷運行的時候響應PendSV中斷,去執行我們寫好的PendSV中斷服務函數,在裏面實現任務切換。

4.2.2 xPortPendSVHandler()函數

PendSV中斷服務函數是真正實現任務切換的地方

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

	PRESERVE8

    /* 當進入PendSVC Handler時,上一個任務運行的環境即:
       xPSR,PC(任務入口地址),R14,R12,R3,R2,R1,R0(任務的形參)
       這些CPU寄存器的值會自動保存到任務的棧中,剩下的r4~r11需要手動保存 */
    /* 獲取任務棧指針到r0 */
	mrs r0, psp
	isb

	ldr	r3, =pxCurrentTCB		/* 加載pxCurrentTCB的地址到r3 */
	ldr	r2, [r3]                /* 加載pxCurrentTCB到r2 */

	stmdb r0!, {r4-r11}			/* 將CPU寄存器r4~r11的值存儲到r0指向的地址 */
	str r0, [r2]                /* 將任務棧的新的棧頂指針存儲到當前任務TCB的第一個成員,即棧頂指針 */				
                               

	stmdb sp!, {r3, r14}        /* 將R3和R14臨時壓入堆棧,因爲即將調用函數vTaskSwitchContext,
                                  調用函數時,返回地址自動保存到R14中,所以一旦調用發生,R14的值會被覆蓋,因此需要入棧保護;
                                  R3保存的當前激活的任務TCB指針(pxCurrentTCB)地址,函數調用後會用到,因此也要入棧保護 */
	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY    /* 進入臨界段 */
	msr basepri, r0
	dsb
	isb
	bl vTaskSwitchContext       /* 調用函數vTaskSwitchContext,尋找新的任務運行,通過使變量pxCurrentTCB指向新的任務來實現任務切換 */ 
	mov r0, #0                  /* 退出臨界段 */
	msr basepri, r0
	ldmia sp!, {r3, r14}        /* 恢復r3和r14 */

	ldr r1, [r3]
	ldr r0, [r1] 				/* 當前激活的任務TCB第一項保存了任務堆棧的棧頂,現在棧頂值存入R0*/
	ldmia r0!, {r4-r11}			/* 出棧 */
	msr psp, r0
	isb
	bx r14                      /* 異常發生時,R14中保存異常返回標誌,包括返回後進入線程模式還是處理器模式、
                                   使用PSP堆棧指針還是MSP堆棧指針,當調用 bx r14指令後,硬件會知道要從異常返回,
                                   然後出棧,這個時候堆棧指針PSP已經指向了新任務堆棧的正確位置,
                                   當新任務的運行地址被出棧到PC寄存器後,新的任務也會被執行。*/
	nop
}

                                                  上一個任務的運行環境自動存儲到任務棧後,psp的指向

                                                          上一個任務的運行環境手動存儲到任務棧後,r0的指向

4.2.3 TaskSwitchContext()函數

任務上下文切換

void vTaskSwitchContext( void )
{    
    /* 兩個任務輪流切換 */
    if( pxCurrentTCB == &Task1TCB )
    {
        pxCurrentTCB = &Task2TCB;
    }
    else
    {
        pxCurrentTCB = &Task1TCB;
    }
}

5. 例程試驗

int main(void)
{	
    /* 硬件初始化 */
	/* 將硬件相關的初始化放在這裏,如果是軟件仿真則沒有相關初始化代碼 */
    
    /* 初始化與任務相關的列表,如就緒列表 */
    prvInitialiseTaskLists();
    
    /* 創建任務 */
    Task1_Handle = xTaskCreateStatic( (TaskFunction_t)Task1_Entry,   /* 任務入口 */
					                  (char *)"Task1",               /* 任務名稱,字符串形式 */
					                  (uint32_t)TASK1_STACK_SIZE ,   /* 任務棧大小,單位爲字 */
					                  (void *) NULL,                 /* 任務形參 */
					                  (StackType_t *)Task1Stack,     /* 任務棧起始地址 */
					                  (TCB_t *)&Task1TCB );          /* 任務控制塊 */
    /* 將任務添加到就緒列表 */                                 
    vListInsertEnd( &( pxReadyTasksLists[1] ), &( ((TCB_t *)(&Task1TCB))->xStateListItem ) );
                                
    Task2_Handle = xTaskCreateStatic( (TaskFunction_t)Task2_Entry,   /* 任務入口 */
					                  (char *)"Task2",               /* 任務名稱,字符串形式 */
					                  (uint32_t)TASK2_STACK_SIZE ,   /* 任務棧大小,單位爲字 */
					                  (void *) NULL,                 /* 任務形參 */
					                  (StackType_t *)Task2Stack,     /* 任務棧起始地址 */
					                  (TCB_t *)&Task2TCB );          /* 任務控制塊 */
    /* 將任務添加到就緒列表 */                                 
    vListInsertEnd( &( pxReadyTasksLists[2] ), &( ((TCB_t *)(&Task2TCB))->xStateListItem ) );
                                      
    /* 啓動調度器,開始多任務調度,啓動成功則不返回 */
    vTaskStartScheduler();                                      
    
    for(;;)
	{
		/* 系統啓動成功不會到達這裏 */
	}
}

例程工程:

             https://download.csdn.net/download/XieWinter/11970685

6. SVC 和PendSV

SVC(系統服務調用,亦簡稱系統調用)和PendSV(可懸起系統調用),它們多用於在操作系統之上的軟件開發中。SVC 用於產生系統函數的調用請求。例如,操作系統不讓用戶程序直接訪問硬件,而是通過提供一些系統服務函數,用戶程序使用SVC 發出對系統服務函數的呼叫請求,以這種方法調用它們來間接訪問硬件。因此,當用戶程序想要控制特定的硬件時,它就會產生一個SVC 異常,然後操作系統提供的SVC 異常服務例程得到執行,它再調用相關的操作系統函數,後者完成用戶程序請求的服務。

這種“提出要求——得到滿足”的方式,很好、很強大、很方便、很靈活、很能可持續發展。首先,它使用戶程序從控制硬件的繁文縟節中解脫出來,而是由OS 負責控制具體的硬件。第二,OS 的代碼可以經過充分的測試,從而能使系統更加健壯和可靠。第三,它使用戶程序無需在特權級下執行,用戶程序無需承擔因誤操作而癱瘓整個系統的風險。第四,通過SVC 的機制,還讓用戶程序變得與硬件無關,因此在開發應用程序時無需瞭解硬件的操作細節,從而簡化了開發的難度和繁瑣度,並且使應用程序跨硬件平臺移植成爲可能。開發應用程序唯一需要知道的就是操作系統提供的應用編程接口(API),並且瞭解各個請求代號
和參數表,然後就可以使用SVC 來提出要求了(事實上,爲使用方便,操作系統往往會提供一層封皮,以使系統調用的形式看起來和普通的函數調用一致。各封皮函數會正確使用SVC指令來執行系統調用——譯者注)。其實,嚴格地講,操作硬件的工作是由設備驅動程序完成的,只是對應用程序來說,它們也是操作系統的一部分。

                                                                 SVC 作爲操作系統函數門戶示意圖 

SVC 異常通過執行”SVC”指令來產生。該指令需要一個立即數,充當系統調用代號。SVC異常服務例程稍後會提取出此代號,從而解釋本次調用的具體要求,再調用相應的服務函數。

由CM3 的中斷優先級模型可知,你不能在SVC 服務例程中嵌套使用SVC 指令(事實上這樣做也沒意義),因爲同優先級的異常不能搶佔自身。這種作法會產生一個用法fault。同理,在NMI 服務例程中也不得使用SVC,否則將觸發硬fault。

另一個相關的異常是PendSV(可懸起的系統調用),它和SVC 協同使用。一方面,SVC異常是必須立即得到響應的(若因優先級不比當前正處理的高,或是其它原因使之無法立即響應,將上訪成硬fault),應用程序執行SVC 時都是希望所需的請求立即得到響應。另一方面,PendSV 則不同,它是可以像普通的中斷一樣被懸起的(不像SVC 那樣會上訪)。OS 可以利用它“緩期執行”一個異常——直到其它重要的任務完成後才執行動作。懸起PendSV 的方法是:手工往NVIC 的PendSV 懸起寄存器中寫1。懸起後,如果優先級不夠高,則將緩期等待執行。

PendSV 的典型使用場合是在上下文切換時(在不同任務之間切換)。例如,一個系統中有兩個就緒的任務,上下文切換被觸發的場合可以是:

  • 執行一個系統調用
  • 系統滴答定時器(SYSTICK)中斷,(輪轉調度中需要)

讓我們舉個簡單的例子來輔助理解。假設有這麼一個系統,裏面有兩個就緒的任務,並且通過SysTick 異常啓動上下文切換

                                                                             兩個任務間通過SysTick 輪轉調度的簡單模式 

上圖是兩個任務輪轉調度的示意圖。但若在產生SysTick 異常時正在響應一箇中斷,則SysTick 異常會搶佔其ISR。在這種情況下,OS 不得執行上下文切換,否則將使中斷請求被延遲,而且在真實系統中延遲時間還往往不可預知——任何有一丁點實時要求的系統都決不能容忍這種事。因此,在CM3 中也是嚴禁沒商量——如果OS 在某中斷活躍時嘗試切入線程模式,將觸犯用法fault 異常。

                                                                          發生IRQ 時上下文切換的問題 

爲解決此問題,早期的OS 大多會檢測當前是否有中斷在活躍中,只有沒有任何中斷需要響應時,才執行上下文切換(切換期間無法響應中斷)。然而,這種方法的弊端在於,它可以把任務切換動作拖延很久(因爲如果搶佔了IRQ,則本次SysTick 在執行後不得作上下文切換,只能等待下一次SysTick 異常),尤其是當某中斷源的頻率和SysTick 異常的頻率比較接近時,會發生“共振”。

現在好了,PendSV 來完美解決這個問題了。PendSV 異常會自動延遲上下文切換的請求,直到其它的ISR 都完成了處理後才放行。爲實現這個機制,需要把PendSV 編程爲最低優先級的異常。如果OS 檢測到某IRQ 正在活動並且被SysTick 搶佔,它將懸起一個PendSV 異常,以便緩期執行上下文切換。

                                                                                       使用PendSV 控制上下文切換

箇中事件的流水賬記錄如下:

1. 任務 A 呼叫SVC 來請求任務切換(例如,等待某些工作完成)

2. OS 接收到請求,做好上下文切換的準備,並且pend 一個PendSV 異常。

3. 當 CPU 退出SVC 後,它立即進入PendSV,從而執行上下文切換。

4. 當 PendSV 執行完畢後,將返回到任務B,同時進入線程模式。

5. 發生了一箇中斷,並且中斷服務程序開始執行

6. 在 ISR 執行過程中,發生SysTick 異常,並且搶佔了該ISR。

7. OS 執行必要的操作,然後pend 起PendSV 異常以作好上下文切換的準備。

8. 當 SysTick 退出後,回到先前被搶佔的ISR 中,ISR 繼續執行

9. ISR 執行完畢並退出後,PendSV 服務例程開始執行,並且在裏面執行上下文切換

10. 當 PendSV 執行完畢後,回到任務A,同時系統再次進入線程模式

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