freertos内核 任务定义与切换 原理分析
freertos內核 任務定義與切換 原理分析
- 主程序
- 任務控制塊
- 任務創建函數
- 任務棧初始化
- 就緒列表
- 調度器
- 總結任務切換
主程序
這個程序目的就是,使用freertos讓兩個任務不斷切換。看兩個任務中變量的變化情況(波形)。
下面這個圖是任務函數里面delay(100)的結果。
下面這個圖是任務函數里面delay(2)的結果.
多任務系統,CPU好像在同時做兩件事,也就是說,最好預期就是,兩變量的波形應該是完全相同的。
這個實驗,delay減少了,他們兩變量波形中間間距仍然沒有減少,說明這個實驗只是一個入門,遠沒達到RTOS的效能。
這個實驗特點,就是具有任務主動切換能力,這是如何實現的呢,值得研究。
下面兩個圖,直觀顯示了程序的主動切換。觀察CurrentTCB這個參數,可以發現它是一直變動的。
它究竟為什么變動呢,采用逐步debug的方式,可找到,是因為調用了一個SwitchContext函數。
那么先看一下main里面都有啥:
從下面可知,這里面有任務棧、任務控制塊、有任務函數、還得創建任務。有就緒列表、有調度器。
任務棧:
#define TASK1_STACK_SIZE 20 StackType_t Task1Stack[TASK1_STACK_SIZE]; #define TASK2_STACK_SIZE 20 StackType_t Task2Stack[TASK2_STACK_SIZE];任務函數(任務入口):
void Task1_Entry( void *p_arg ) {for( ;; ){flag1 = 1;delay( 100 ); flag1 = 0;delay( 100 );/* 任務切換,這里是手動切換 */taskYIELD();} } void Task2_Entry( void *p_arg ) {for( ;; ){flag2 = 1;delay( 100 ); flag2 = 0;delay( 100 );/* 任務切換,這里是手動切換 */taskYIELD();} }任務控制塊:
TCB_t Task1TCB; TCB_t Task2TCB;就緒列表初始化:
prvInitialiseTaskLists();創建任務:
typedef void * TaskHandle_t; TaskHandle_t Task1_Handle; 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 ) );啟動調度器:
vTaskStartScheduler();任務控制塊
多任務系統,任務執行由系統調度。任務的信息很多,于是就用任務控制塊表示任務,這樣方便系統調度。
任務控制塊類型,包含了任務的所有信息,比如棧頂指針pxTopOfStack、任務節點xStateListItem、任務棧起始地址pxStack、任務名稱pcTaskName。
typedef struct tskTaskControlBlock {volatile StackType_t *pxTopOfStack; /* 棧頂 */ListItem_t xStateListItem; /* 任務節點 */StackType_t *pxStack; /* 任務棧起始地址 *//* 任務名稱,字符串形式 */char pcTaskName[ configMAX_TASK_NAME_LEN ]; } tskTCB; typedef tskTCB TCB_t;任務創建函數
main里面調用xTaskCreateStatic創建了任務,觀察可知這個函數其實改變的是Task1TCB任務控制塊,這個任務控制塊誕生之初,就沒有進行過初始化。調用任務創建函數目的就是初始化任務控制塊。
Task1_Handle = xTaskCreateStatic( (TaskFunction_t)Task1_Entry, /* 任務入口 */(char *)"Task1", /* 任務名稱,字符串形式 */(uint32_t)TASK1_STACK_SIZE , /* 任務棧大小,單位為字 */(void *) NULL, /* 任務形參 */(StackType_t *)Task1Stack, /* 任務棧起始地址 */(TCB_t *)&Task1TCB ); /* 任務控制塊 */直觀表述這個函數內部:
任務控制塊里面的任務節點:下面代碼是初始化過程,其實就是進行鏈表的普通節點初始化。
/* 初始化TCB中的xStateListItem節點 */vListInitialiseItem( &( pxNewTCB->xStateListItem ) );/* 設置xStateListItem節點的擁有者 */listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );這個任務入口體現在哪呢,其實是體現在任務棧里面。在main.c里面初始化任務棧,僅僅開辟了一段內存空間,里面放什么東西都沒有具體說明。調用任務創建函數之后,其實也一并初始化了任務棧(往里面放東西),任務入口就放到這個棧里了。任務棧也初始化完的時候,任務控制塊才算圓滿的初始化完了。
所以任務創建函數里面還得調用任務棧初始化函數。
任務棧初始化
初始化任務棧的函數代碼在下面:
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; } static void prvTaskExitError( void ) {/* 函數停止在這里 */for(;;); }棧頂指針就是pxTopOfStack。pxStack是一個指針指向任務棧起始地址,ulStackDepth是任務棧大小。下面是獲取棧頂指針的代碼。棧是后進先出,先進去的后出。其實也就是,先進棧的被壓到最底下去了(下標最靠后)。所以,如果棧里面什么都沒有,棧頂的位置得在最后面(也就是地址最高的哪個位置)。
/* 獲取棧頂地址 */ pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 );下面兩個圖表述的都是一個意思,只不過右邊的可能好懂點(先進棧的被壓到最底下去了)。
初始化任務棧的函數運行完,棧就發生了變化,里面有內容了,如下圖所示??梢钥吹饺蝿杖肟诘刂反孢M去了,任務形參也存進去了。
#define portINITIAL_XPSR ( 0x01000000 )至此,通過任務創建函數,已經圓滿的初始好了任務控制塊,同時填充了任務棧,任務棧聯系了任務入口地址(任務的函數實體)。任務控制塊成員變量里面有棧頂指針,聯系了任務棧。那么,任務的棧、任務的函數實體、任務的控制塊通過任務創建函數就聯系起來了。
這里面插一句:任務棧一個元素占四個字節!上面那個圖,如果r0地址是0x40,那么pxTopOfStack地址就是0x20(因為0x40-0x20=32),32÷4=8,也就是說八個元素。
#define portSTACK_TYPE uint32_t typedef portSTACK_TYPE StackType_t; StackType_t Task1Stack[TASK1_STACK_SIZE];uint32_t u:代表 unsigned 即無符號,即定義的變量不能為負數; int:代表類型為 int 整形; 32:代表四個字節,即為 int 類型; _t:代表用 typedef 定義的; 整體代表:用 typedef 定義的無符號 int 型宏定義; 位(bit):每一位只有兩種狀態0或1。計算機能表示的最小數據單位。 字節(Byte):8位二進制數為一個字節。計算機基本存儲單元內容用字節表示。就緒列表
下面是main里面就緒列表的定義、初始化,添加任務到就緒列表。
首先緒列表的定義,簡而言之,就緒列表是一個List_t類型的數組(其實數組中每個元素就相當于根節點),數組下標對應任務的優先級。
#define configMAX_PRIORITIES /* 任務就緒列表 */ List_t pxReadyTasksLists[ configMAX_PRIORITIES ]; /* 初始化與任務相關的列表,如就緒列表 */ prvInitialiseTaskLists(); /* 將任務添加到就緒列表 */ vListInsertEnd( &( pxReadyTasksLists[1] ), &( ((TCB_t *)(&Task1TCB))->xStateListItem ) ); /* 將任務添加到就緒列表 */ vListInsertEnd( &( pxReadyTasksLists[2] ), &( ((TCB_t *)(&Task2TCB))->xStateListItem ) );就緒列表初始化函數如下,簡而言之,就是對List_t類型的數組里面每個元素進行初始化(根節點初始化)。
/* 初始化任務相關的列表 */ void prvInitialiseTaskLists( void ) {UBaseType_t uxPriority;for( uxPriority = ( UBaseType_t ) 0U; uxPriority < ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ ){vListInitialise( &( pxReadyTasksLists[ uxPriority ] ) );} }添加任務到就緒列表的函數是vListInsertEnd,這個在之前雙向循環鏈表說過,其實就是把普通節點插到根節點后。
就緒列表在不同任務之間建立一種聯系,圖示如下。
調度器
啟動調度器,是用了一個SVC中斷。
從下面代碼可以看出,pxCurrentTCB指向的是Task1TCB(任務控制塊)的地址。
typedef struct tskTaskControlBlock {volatile StackType_t *pxTopOfStack; /* 棧頂 */ListItem_t xStateListItem; /* 任務節點 */StackType_t *pxStack; /* 任務棧起始地址 *//* 任務名稱,字符串形式 */char pcTaskName[ configMAX_TASK_NAME_LEN ]; } tskTCB; typedef tskTCB TCB_t;//void vTaskStartScheduler( void )函數里 pxCurrentTCB = &Task1TCB;下面這個svc的中斷函數,里面第一步就是把任務棧的棧頂指針給r0寄存器。
可以認為:r0=pxTopOfStack(任務棧的棧頂指針的地址)。
//__asm void vPortSVCHandler( void )函數里 ldr r3, =pxCurrentTCB //加載pxCurrentTCB的地址到r3 ldr r1, [r3] //把r3指向的內容給r1,內容就是Task1TCB的地址 ldr r0, [r1] //把r1指向的內容給r0,內容就是Task1TCB的地址里面的第一個內容,也就是pxTopOfStack接下來:以r0(任務棧的棧頂指針的地址)為基地址,將任務棧里面向上增長的8字節內容加載到CPU寄存器r4-r11。
ldmia r0!, {r4-r11}然后將r0存到psp里。
msr psp, r0下面這個代碼,目的是改EXC_RETURN值為0xFFFFFFD,這樣的話中斷返回就進入線程模式,使用線程堆棧(sp=psp)。
orr r14, #0xd看下面這個圖,異常返回時,出棧用的是PSP指針。PSP指針把任務棧里面剩余的內容(沒有讀到寄存器里的內容)全部給弄出去(自動將棧中的剩余內容加載到cpu寄存器)。那么任務函數的地址就給到了PC,程序就跳到任務函數的地方繼續運行。
圖1如下:注意,動的是psp,pxTopOfStack是不動的。
下面是實驗證明上面關于psp指針運動描述的正確性:
r0一開始存的就是pxTopOfStack的值(任務棧的棧頂指針的地址)
接下來把運動過的r0給psp,此時的psp位置就在圖1psp2那個地方。
下圖這個psp地址仍然是0x40。
程序運行完bx r14,就跑到任務函數里面了,此時的psp=0x60,位置就在圖1的psp3。
現在程序跑到任務函數里面去了,任務函數里面調了taskYIELD()函數,目的就是觸發PendSV中斷(優先級最低,沒有其他中斷運行時才響應)。下面這個圖是進到PendSV中斷服務函數之前的寄存器組狀態。
下面這個圖是進到PendSV中斷服務函數時的寄存器組狀態。可以觀察psp,從0x60變成了0x40。
現在psp的位置就可以知道了,如下圖所示。這是因為,進到xPortPendSVHandler函數之后,上個任務運行的環境將會自動存儲到任務的棧中,同時psp自動更新。
下面這個代碼,把psp的值存到r0里面。
//__asm void xPortPendSVHandler( void )函數 mrs r0, psp //void vTaskStartScheduler( void )函數里 pxCurrentTCB = &Task1TCB;/*pxCurrentTCB有一個地址,這個地址里面的內容是當前任務的地址*//*當前任務地址的第一個內容就是當前任務的棧頂指針*///__asm void xPortPendSVHandler( void )函數里 ldr r3, =pxCurrentTCB /* 加載pxCurrentTCB的地址到r3 */ ldr r2, [r3] /* 把r3指向的內容給r2,內容就是Task1TCB(當前任務)的地址*//*[r2]是當前任務棧的棧頂指針*/ stmdb r0!, {r4-r11} /* 將CPU寄存器r4~r11的值存儲到r0指向的地址 */ str r0, [r2] /* 把r0的地址給當前任務棧的棧頂指針 */經過上面這個代碼,現在r0的位置如下。psp在上面這個過程是沒變化的,變的只有r0。
對照著下面這個圖,更清晰點。r2存的是當前任務的地址。r0存的是棧頂指針的地址。
下面對r3進行說明:r3=0x2000000C,這個地址里面存的第一個內容是當前任務塊的地址0x20000068如下圖所示。
下面對當前任務塊的地址進行說明:當前任務塊的地址0x20000068里面存的第一個內容就是棧頂指針的地址。
下面對棧頂指針的地址進行說明:棧頂指針地址里面內容剛好就是當前任務的任務棧。
可以對比下圖,觀察當前任務棧里面的內容,與此同時內容也對應了地址,地址就可以通過上圖推出,比如,0x20000060地址里面存的就是0x10000000。
下面這個代碼:目的是將r3和r14臨時壓入主棧(MSP指向的棧),因為接下來需要調用任務切換函數,調用函數時,返回地址自動保存到r14里面。r3的內容是當前任務塊的地址(ldr r3, =pxCurrentTCB),調用函數后,pxCurrentTCB會被更新。
stmdb sp!, {r3, r14}執行代碼之前,MSP指向0x20000058這個地址。
執行代碼之后,MSP指向的地址少了8個字節,與此同時r3和r14存到了MSP指向的地址里面。
msp指向的棧里面的具體信息其實可以反推出來,如下綠字:
下面這個代碼:basepri是中斷屏蔽寄存器,下面這個設置,優先級大于等于11的中斷都將被屏蔽。相當于關中斷進入臨界段。
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY msr basepri, r0 /* #define configMAX_SYSCALL_INTERRUPT_PRIORITY 191 /* 高四位有效,即等于0xb0,或者是11 */ 191轉成二進制就是11000000,高四位就是1100 */下面這個代碼:調用了函數vTaskSwitchContext,這個函數目的是選擇優先級最高的任務,然后更新pxCurrentTCB。目前這里面使用的是手動切換。
bl vTaskSwitchContext void vTaskSwitchContext( void ) { /* 兩個任務輪流切換 */if( pxCurrentTCB == &Task1TCB ){pxCurrentTCB = &Task2TCB;}else{pxCurrentTCB = &Task1TCB;} }現在說明一下調用這個函數產生什么后果:
從下圖可知,此時r3=0x2000000C,這個地址里面的的內容就是當前任務塊的地址。
進行到下面這一步,當前任務塊的地址變了,與此同時,0x2000000C地址里面的的內容也變了。也就是說,走出調用函數之后,通過r3就能找到變化后新的任務地址了。
那么此時豁然開朗,為什么調用函數前要把r3入棧呢,看下圖正中間上方的匯編代碼,這個c語言背后的匯編代碼是調用寄存器r0、r1存一些中間變量,為了防止運行函數時往r3寄存器里面存中間變量,才把r3入棧保護起來。想一下,如果往r3寄存器里面存中間變量,那么0x2000000C地址就不存到r3寄存器里了,那也無法通過r3找到變化后新的任務地址了。
下面這個代碼:優先級高于0的中斷被屏蔽,相當于是開中斷退出臨界段。
mov r0, #0 /* 退出臨界段 */ msr basepri, r0下面這個代碼恢復r3和r14
ldmia sp!, {r3, r14} /* 恢復r3和r14 */如下圖,r3和r14被恢復,而且MSP從0x20000550變成了0x20000558。
這里面有個細節,MSP變動之后,MSP指向的棧前面的數(存的r3和r14)卻被留了下來。這讓人不禁思考出棧究竟是什么意思,這里不就只是動了MSP指針嗎。
此時觀察psp地址里面的內容,可發現,還是之前的那個任務棧??戳顺鰲:蚦語言里面實體的出(c語言里面出棧后,出去的內容就不在棧里面了)還不太一樣,這個出棧,動的是指針,內容還在棧里面。
下面這個代碼,進行完,r0里面存的是當前任務棧的棧頂指針的地址。
ldr r1, [r3] ldr r0, [r1] /* 當前激活的任務TCB第一項保存了任務堆棧的棧頂,現在棧頂值存入R0*/下面是當前的任務棧里面的內容。
ldmia r0!, {r4-r11} /* 出棧 */這個時候r0位置變到了0x200000c0。
然后下面把r0給了psp。記得吧,之前psp指向的可是0x20000040,也就是上一個任務的任務棧,這里面切到了另一個任務的任務棧里面了。也就是psp指向0x200000c0。
msr psp, r0下面這個代碼運行完效果如下圖。
bx r14仔細觀察,異常退出時,會以psp作為基地址,將任務棧里面剩下的內容自動加載到CPU寄存器。然后PC指針就拿到了任務棧里面的任務函數地址,然后就跳到任務函數里了。至此,切換完成。
最后,觀察一下psp:由下面兩張圖,就明白了,psp出棧是什么意思。
下面是返回Thread Mode后(進入到了任務函數里面)psp的指向。
下圖是沒有返回到Thread Mode時psp的指向。
總結任務切換
總結一下核心思路:
1.首先是這張圖,在任務函數里面,處于Thread Mode狀態(為什么呢,因為bx r14 指令,里面r14的值設置的是0xFFFFFFFD),然后通過任務函數里面的taskYIELD()函數,進入Handler Mode狀態,里面進行了任務切換操作,就是說,psp指向的任務棧切換了(所以一會pc指向的任務函數也改了),然后結束異常的時候,psp出棧,pc現在指向的是切換后的任務函數地址,于是就又跳到另一個任務函數里。
2.要明白切到任務函數里面的原理
之前創建任務時,已經把任務函數保存在了任務棧內。
出棧的話,psp指向的棧里面剩下的東西,會加載到寄存器里面,如下圖所示:那么任務函數地址就給到pc指針了,那么異常返回之后,程序就跳到任務函數的地方繼續運行,那么就切到任務函數里了。
總結
以上是生活随笔為你收集整理的freertos内核 任务定义与切换 原理分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java 密码生成器_Java课程设计-
- 下一篇: c语言 数据结构 list、queue、