FreeRTOS任務切換的實現

来源:https://www.cnblogs.com/RAM-YAO/p/18351408
-Advertisement-
Play Games

第十八章 machine.Timer類實驗 1)實驗平臺:正點原子DNK210開發板 2)章節摘自【正點原子】DNK210使用指南 - CanMV版 V1.0 3)購買鏈接:https://detail.tmall.com/item.htm?&id=782801398750 4)全套實驗源碼+手冊+ ...


在進行FreeRTOS任務切換的介紹前,我們先來瞭解一下SVC和PendSV。

SVC和PendSV

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

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

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

1.執行一個系統調用

2.系統滴答定時器(SYSTICK)中斷

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

在典型的RTOS中,任務的執行時間被分為多個時間片,每個任務執行相對應時間片。上圖的任務切換是在Systick中斷中完成的,每觸發一次Systick中斷就會進行任務切換。但若在產生Systick異常時正在響應一個中斷,則Systick異常會搶占其中斷。在這種情況下,OS不得執行上下文切換,否則將使中斷請求被延遲,,而且在真實系統中,延時時間往往是不可預知的,這對於實時操作系統來說是不能容忍的。由於存在中斷請求,ARM Cortex-M 不允許返回線程模式,因此,將會產生用法錯誤異常(Usage Fault)。

在一些 RTOS 的設計中,會通過判斷是否存在中斷請求,來決定是否進行任務切換。雖然可以通過檢查 xPSR 或 NVIC 中的中斷活躍寄存器來判斷是否存在中斷請求,但是這樣可能會影響系統的性能,甚至可能出現當某中斷源的頻率和 SysTick 異常的頻率比較接近時,會發生“共振”。中斷源在 SysTick 中斷前後不斷產生中斷請求,導致系統無法進行任務切換的情況。

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

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

2.內核接到請求,做好上下文切換的準備,並且懸起一個PendSV異常。

3.當退出SVC後,立即進入PendSV,在PendSV中執行上下文切換。

4.當PendSV執行完畢後,返回線程模式,切換到執行任務B。

5.發生了一個中斷,開始執行中斷服務程式。

6.在中斷執行過程中,發生了Systick異常(用於內核時鐘節拍),並且搶占了該中斷。

7.操作系統執行必要的操作,如更新內部時間計數、遍歷延遲任務隊列等,然後掛起PendSV異常,準備進行任務切換。

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

9.當中斷執行完畢後,PendSV服務常式開始執行,完成任務切換。

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

FreeRTOS中的任務切換就是在PendSV中完成的。

PendSV 中斷服務函數

FreeRTOS 在 PendSV 的中斷中,完成任務切換,PendSV 的中斷服務函數由 FreeRTOS 編寫,將 PendSV 的中斷服務函數定義成函數 xPortPendSVHandler()。針 對 ARM Cortex-M3 和針對 ARM Cortex-M4 和 ARM Cortex-M7 內 核 的 函 數xPortPendSVHandler()稍有不同,其主要原因在於 ARM Cortex-M4 和 ARM Cortex-M7 內核具有浮點單元,因此在進行任務切換的時候,還需考慮是否保護和恢復浮點寄存器的值。針對 ARM Cortex-M3 內核的函數xPortPendSVHandler(),具體的代碼如下所示

__asm void xPortPendSVHandler( void )
{
	/* 導入全局變數及函數 */
	extern uxCriticalNesting;
	extern pxCurrentTCB;
	extern vTaskSwitchContext;
	
	/* 8 位元組對齊 */
	PRESERVE8

	/* 從 PSP 寄存器(進程堆棧指針)中獲取當前任務的堆棧指針 */
	mrs r0, psp
	
	/* isb 指令用於確保指令的嚴格順序執行,以提高代碼的可靠性和正確性 */
	isb
	/* 將 pxCurrentTCB 的 地址值 載入到 r3 寄存器中,即指向當前運行任務控制塊的指針 */
	ldr	r3, =pxCurrentTCB		/* Get the location of the current TCB. */
	/* 將 r3 寄存器中存儲的記憶體地址所對應的數據載入到 r2 寄存器中,即當前運行任務控制塊的首地址 */
	ldr	r2, [r3]

	/* 將 R4~R11 入棧到當前運行任務的任務棧中 */
	stmdb r0!, {r4-r11}			/* Save the remaining registers. */
	/* r0 的內容會被存儲到由 R2 寄存器指定的記憶體地址中
	將當前任務的堆棧指針中的內容存到 R2. R2 指向的地址為此時的任務棧指針 */
	str r0, [r2]				/* Save the new top of stack into the first member of the TCB. */


	/* 將 r3 和 r14 保存到內核堆棧MSP中,然後設置 basepri 寄存器的值為
	configMAX_SYSCALL_INTERRUPT_PRIORITY,屏蔽受 FreeRTOS 管理的所有中斷。
	然後執行數據同步操作(dsb)和指令同步操作(isb),確保之前的操作已完成 */
	stmdb sp!, {r3, r14}
	mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
	msr basepri, r0
	dsb
	isb

	/* 跳轉到函數 vTaskSeitchContext
	 * 主要用於更新 pxCurrentTCB,
	 * 使其指向最高優先順序的就緒態任務
	 */
	bl vTaskSwitchContext
	mov r0, #0
	/* 將 basepri 寄存器的值恢復為 0,為 0 表示允許所有優先順序的中斷 */
	msr basepri, r0
	
	/* 這部分代碼從棧中彈出 r3 和 r14 的值,這兩個寄存器用於保存任務
	控制塊指針和鏈接寄存器。然後,它載入當前任務控制塊中的棧頂指針
	到 r0 中,接著將 r4-r11 的值從棧中彈出並存儲到相應的寄存器中,
	最後使用 msr 指令將當前任務的棧指針存儲到 psp 寄存器中 */
	

	/* 將 R3、R14 重新從 MSP 指向的棧中出棧 
	R3 為 pxCurrentTCB 的地址值,
	pxCurrentTCB 已經在函數 vTaskSwitchContext 中更新為最高優先順序的就緒態任務
	因此 r1 為 pxCurrentTCB 的值,即當前最高優先順序就緒態任務控制塊的首地址 */
	ldmia sp!, {r3, r14}
	ldr r1, [r3]
	/* r0 為最高優先順序就緒態任務的任務棧指針 */
	ldr r0, [r1]				/* pxCurrentTCB中的第一項是任務棧棧頂 */
	
	/* 從最高優先順序就緒態任務的任務棧中出棧 R4~R11 和 R14 
	註意:這裡出棧的 R14 為 EXC_RETURN,其保存了任務是否使用浮點單元的信息 */
	ldmia r0!, {r4-r11}			/* Pop the registers and the critical nesting count. */
	/* 使用 msr 指令將 r0 的值存儲到 psp 寄存器中,用作任務的棧指針 */
	msr psp, r0

	isb
	/*  使用 bx 指令 跳轉到切換後的任務運行
		執行此指令,CPU 會自動從 PSP 指向的任務棧中,
		出棧 R0、R1、R2、R3、R12、LR、PC、xPSR 寄存器,
		接著 CPU 就跳轉到 PC 指向的代碼位置運行,
		也就是任務上次切換時運行到的位置 */
	bx r14
	nop
}

所以從上面的代碼可以分析出,FreeRTOS在進行任務切換的時候首先會對當前任務的狀態信息進行入棧保存,保存到任務的任務棧中。然後調用vTaskSwitchContext函數更新當前最高優先順序任務的指向,然後從切換後的任務的任務棧中進行出棧,恢復新的任務的狀態信息,最後使用bx指令跳轉到切換後的任務執行。

從該函數中只看到了程式保存和恢復CPU信息中的部分寄存器信息(R4R11),這是因為硬體會自動入棧和出棧其他CPU寄存器信息。在任務運行的時候,CPU使用進程堆棧指針PSP最為棧空間來使用,也就是使用運行任務的任務棧。當Systick中斷發生時,在跳轉到Systick中斷服務函數運行前,硬體會自動除將R4R11寄存器的其他CPU寄存器入棧,因此就將任務切換前的部分信息保存到了對應任務的任務棧中,當退出PendSV時,會自動從棧空間中恢復這部分信息,以供任務正常運行。

可以通過結合下麵兩張圖來結合理解:

任務棧如下圖所示:

vTaskSwitchContext() 函數

void vTaskSwitchContext(void)
{
	/* 任務調度器未在運行 */
	if (uxSchedulerSuspended != (UBaseType_t)pdFALSE)
	{
		/* 調度程式當前掛起-不允許進行上下文切換,直接退出函數 */
		xYieldPending = pdTRUE;
	}
	else
	{
		xYieldPending = pdFALSE;
		traceTASK_SWITCHED_OUT();

#if (configGENERATE_RUN_TIME_STATS == 1)
		{
#ifdef portALT_GET_RUN_TIME_COUNTER_VALUE
			portALT_GET_RUN_TIME_COUNTER_VALUE(ulTotalRunTime);
#else
			ulTotalRunTime = portGET_RUN_TIME_COUNTER_VALUE();
#endif

			if (ulTotalRunTime > ulTaskSwitchedInTime)
			{
				pxCurrentTCB->ulRunTimeCounter += (ulTotalRunTime - ulTaskSwitchedInTime);
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
			ulTaskSwitchedInTime = ulTotalRunTime;
		}
#endif /* configGENERATE_RUN_TIME_STATS */

		taskCHECK_FOR_STACK_OVERFLOW();

		/* 此函數用於將 pxCurrentTCB 更新為指向優先順序最高的就緒態任務 */
		taskSELECT_HIGHEST_PRIORITY_TASK();
		traceTASK_SWITCHED_IN();

#if (configUSE_NEWLIB_REENTRANT == 1)
		{
			_impure_ptr = &(pxCurrentTCB->xNewLib_reent);
		}
#endif /* configUSE_NEWLIB_REENTRANT */
	}
}

該函數內部通過調用巨集函數 taskSELECT_HIGHEST_PRIORITY_TASK(),來將pxCurrentTCB 設置為指向優先順序最高的就緒態任務。

taskSELECT_HIGHEST_PRIORITY_TASK()如下:

#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);  \
		/* 讓 pxCurrentTCB 指向該任務優先順序就緒態任務列表中的任務 */ \
		listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB, &(pxReadyTasksLists[uxTopPriority]));  \
	}\

需要註意的是,FreeRTOS 提供了兩種從任務優先順序記錄中查找優先順序最高任務優先等級的方式,一種是由純 C 代碼實現的,這種方式適用於所有運行 FreeRTOS 的 MCU;另外一種方式則是使用了硬體計算前導零的指令,因此這種方式並不適用於所有運行 FreeRTOS 的 MCU,而僅適用於具有有相應硬體指令的 MCU。具體使用哪種方式,用戶可以在 FreeRTOSConfig.h 文件中進行配置。

使用此方法就限制了系統最大的優先順序數量不能超過 32,即最高優先等級為 31,不過對於絕大多數的應用場合,32 個任務優先順序等級已經足夠使用了。如果不使用次方法,理論上任務優先順序沒有上限。

軟體方式如下:

#define taskSELECT_HIGHEST_PRIORITY_TASK()                                              \
	{                                                                                   \
		UBaseType_t uxTopPriority = uxTopReadyPriority;                                 \
                                                                                        \
		/* Find the highest priority queue that contains ready tasks. */                \
		while (listLIST_IS_EMPTY(&(pxReadyTasksLists[uxTopPriority])))                  \
		{                                                                               \
			configASSERT(uxTopPriority);                                                \
			--uxTopPriority;                                                            \
		}                                                                               \
                                                                                        \
		/* listGET_OWNER_OF_NEXT_ENTRY indexes through the list, so the tasks of        \
		the	same priority get an equal share of the processor time. */                  \
		listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB, &(pxReadyTasksLists[uxTopPriority])); \
		uxTopReadyPriority = uxTopPriority;                                             \
	} /* taskSELECT_HIGHEST_PRIORITY_TASK */

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本章將和大家分享Linux系統中的管道命令、grep命令、sed命令和awk命令。廢話不多說,下麵我們直接進入主題。 一、管道命令 Linux 中的管道命令(pipe)是一種非常強大的特性,它允許你將一個命令的輸出作為另一個命令的輸入。管道命令極大地增強了命令行的靈活性和功能,使得複雜的數據處理任務 ...
  • 1. 配置 1.1 如果是配置全局文件,則編輯/etc/mail.rc 1.2 如果是配置當前用戶,則編輯~/.mailrc 2. 配置文件內容 # 這裡填入smtp地址,這裡的xxx為qq或者163等,如果用的雲伺服器,安全組策略要開放465/25埠,入站和出站都要開放該埠 set smtp= ...
  • 一、背景 在公司軟體的實際開發中,當一個版本的客戶端安裝包本地調試、測試驗證都沒問題後外發,到用戶實際機器上出問題了,怎麼辦? 很多人說,讓客戶給出復現步驟,我來試試!但是按照步驟操作之後還是沒效果。這時你又想到了是不是環境的差異,但是又說不上來是哪裡出問題。提供兩個辦法:1.寫日誌,編一個相近版本 ...
  • 1、用戶操作 阿裡雲預設是 root 用戶,我們一般要自己創建一個用戶,然後給該用戶 sudo 許可權 添加用戶 sudo adduser newUserName 賦予sudo許可權 sudo usermod -aG sudo newUserName 刪除用戶 sudo deluser --remove ...
  • 1、Docker 基本概念 什麼是 Docker? Docker 是一個開源的容器化平臺,允許開發者封裝他們的應用程式及其所有依賴項到一個標準化的單元中,這個單元被稱為“容器”。容器可以在任何支持 Docker 的環境中運行,從而確保應用程式的可移植性和一致性。 Docker 的優勢 一致性和可移植 ...
  • 大家好,我是痞子衡,是正經搞技術的痞子。今天痞子衡給大家分享的是i.MXRT1050在GPIO上增加RC延時電路後導致邊沿中斷誤觸發問題探析。 前段時間有一個 RT1052 客戶反饋了一個有趣的問題,他們設計得是一個帶 LCD 屏交互的應用,應用以官方 SDK 里的 lvgl_demo_widget ...
  • 家裡的機頂盒淘汰下來,博主想要物盡其用,看看是否能將其改造為一臺Linux"開發機",為其安裝Ubuntu系統,故開始倒騰 ...
  • 寫在前面 本隨筆是非常菜的菜雞寫的。如有問題請及時提出。 可以聯繫:[email protected] GitHhub:https://github.com/WindDevil (目前啥也沒有 本節重點 主要是對 任務 的概念進行進一步擴展和延伸:形成 任務運行狀態:任務從開始到結束執行過程中所處的 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...