uC/OS-Ⅲ實時操作系統內核原理總結

JeckXu666 2021-08-16 01:19:55 阅读数:130

本文一共[544]字,预计阅读时长:1分钟~
uc os- os 操作 原理

uC/OS-Ⅲ實時操作系統內核原理總結

學習uC/OS-Ⅲ時做的一些記錄,整理了一下,結合自己的理解,做一篇總結(本總結適合有一定的基礎的同學食用,主要還是自己看)

注意:文章插圖有些不清晰,有需要可以私信我找我要哈

參考書籍:

  • uC/OS-III 內核實現與應用開發實戰指南
  • uC/OS-III 中文翻譯(屈環宇譯)
  • uC/OS-III 技術內幕
  • uC/OS-III 源碼
  • uC-OS-III 3.06.01 API Reference

一、為什麼要用RTOS?

玩單片機上RTOS前肯定有個疑惑,為什麼要上RTOS?使用裸機編程不可以嗎?

首先RTOS和裸機並不是誰絕對的好或者不好,更多情况下我們要根據實際情况來選擇使用使用他,裸機編程方式適合代碼量小,邏輯複雜度低的情景,其編程簡單,開發速度快,而RTOS則適合代碼量較大,邏輯複雜,適合穩定性要求高的場景中,缺點就是編程複雜,學習周期較長。

其次對於想要走上嵌入式驅動開發的工程師來說,其一般的路線如下:

在這裏插入圖片描述

可以看到RTOS的學習可以作為我麼向Linux學習的跳板,為後面的進階打下基礎,好了以上就是我個人對RTOS學習必要性的一些小理解

二、內核探索篇

2.1 任務定義與切換

​ uCOS的運行與裸機一個很明顯的區別就是uCOS將要運行的程序拆分成各個任務快,任務塊間進行切換運行,切換時間很短,每個子任務執行時間也很短,所以當系統跑起來之後,就像並行運行一樣,這樣我們設計程序的時候就可以把程序設計成多個部分獨立執行,大大降低了程序的設計難度,方便程序的維護與管理

​ 任務的定義呢就是定義一個函數,裏面放上一個死循環作為任務運行的實體,除了任務實體以外,任務還有許多其他的屬性,比如給任務分配的堆棧大小,任務的執行優先級別等等,這些任務的屬性通過一個結構體:TASK_TCB(任務控制塊)進行關聯,通過訪問對應任務的任務控制塊的結構體的成員,我們就可以訪問他的實體函數入口,優先級,堆棧大小等等信息;

​ 任務的切換就是切換上下文的與運行環境,任務運行時後的環境我們叫他上下文運行環境,當任務需要切換程序的時候,會觸發PendSV异常,觸發PendSV 异常後,就會進入PendSV 异常服務函數,然後在裏面進行任務切換,PendSV 异常服務函數其實就是保存上一個任務運行時的環境變量,自動或手動將數據壓棧,然後切換堆棧指針,將下一個任務數據彈出來,完成任務切換

叫上下文而不叫運行環境大概是因為最開始context是編譯原理中的概念,context-free grammar譯為“上下文無關語法”是很貼切的。後來context應用在編程中,翻譯的時候還是照搬了編譯原理中的譯法,雖然意思已經變了

下圖是我學習野火《uC/OS-III 內核實現與應用開發實戰指南》時記錄的定義任務的流程,原文鏈接:uCOS-Ⅲ學習筆記:任務定義與切換

在這裏插入圖片描述

2.2 系統時基

​ RTOS 必須要一個時基來驅動,該時基本質上是一個定時器,每次計數到比特後會進行一次中斷,中斷程序對時基進行校准,更新時基計數值,然後在系統任務(時基任務)中處理系統的運行狀態,比如掃描任務阻塞延時的等待列錶,看哪個任務等待結束了,把他脫離等待列錶,或者看看其他內核對象的等待列錶有沒有可以脫離的任務,將他們加入到任務就緒列錶裏面去,可以說系統時基是系統的心髒,操作系統沒有時基,根本運行不起來,同時系統任務調度的頻率等於該時基的頻率,通常該時基由一個定時器來提供,也可以從其它周期性的信號源獲得,每個時鐘運行周期稱為一個TICK

Cortex-M 內核中有一個系統定時器SysTick,它內嵌在NVIC 中,是一個24 比特的遞减的計數器,計數器每計數一次的時間為1/SYSCLK。當重裝載數值寄存器的值遞减到0 的時候,系統定時器就產生一次中斷,以此循環往複。因為SysTick 是嵌套在內核中的,所以使得OS 在Cortex-M 器件中編寫的定時器代碼不必修改,使移植工作一下子變得簡單很多,SysTick 是最適合給操作系統提供時基,用於維護系統心跳的定時器。

學習野火《uC/OS-III 內核實現與應用開發實戰指南》時記錄的定義任務的流程,原文鏈接:uCOS-Ⅲ學習筆記-時基列錶

2.3 阻塞延時與空閑任務

​ 裸機編程裏面的延時,通常使用的是軟件延時,即還是讓CPU 空等來達到延時的效果,比如兩個for循環嵌套,空耗CPU,這樣太浪費CPU性能了,使用RTOS 的很大優勢就是榨幹CPU 的性能,永遠不能讓它閑著,如果要延時絕不能讓 CPU 空等來實現延時的效果,那麼RTOS中的延時是怎麼實現的呢?

​ 在RTOS 中的延時叫阻塞延時,即任務需要延時的時候,任務會放弃CPU 的使用權,進入到等待列錶裏面凉快去,CPU趁著這個功夫可以去幹其它的事情,當任務延時時間到,對應的任務重新獲取CPU 使用權,脫離等待列錶,回到就緒列錶,任務繼續運行,這樣就充分地利用了CPU 的資源,榨幹CPU

​ 但上面的延時會有一個問題,如果所有的任務都在等待狀態,那CPU 又去幹什麼事情了?此時就引入了空閑任務,如果沒有其它任務可以運行,RTOS 都會為CPU創建一個空閑任務,這個時候CPU 就運行空閑任務。在uC/OS-III中,空閑任務是系統在初始化的時候創建的優先級最低的任務,空閑任務主體很簡單,只是對一個全局變量進行計數。鑒於空閑任務的這種特性,在實際應用中,當系統進入空閑任務的時候,可在空閑任務中讓單片機進入休眠或者低功耗等操作

2.4 系統時間戳

​ 在RTOS裏面經常需要精准計算一段時間的長度,因此引入時間戳的概念,時間戳實際上就是一個時間點,記錄了一個隨著程序運行不斷自加的運行值,或許有同學會以為是幾個TIM定時器,但實際上不是,定時器的時間精度一般是us級別,但程序運行可不是us級別的,比如主頻72M的單片機,時鐘周期才1/72M秒,執行一條指令也就幾個時鐘周期也就是幾ns,精度特別高,所以這裏用來記錄時間戳的外設肯定不是定時器,而是一個叫 DWT 的外設,,該外設有一個32 比特的寄存器叫 CYCCNT,它是一個向上的 計數器,記錄的是內核時鐘HCLK(高速時鐘)的運行的個數,當CYCCNT溢出之後,會清0 重新開始向上計數,因此剛好可以用它來做時間戳!內核代碼使用他很簡單,關掉中斷直接讀寄存器就能獲取到時間戳

2.5 系統臨界段

​ 臨界段是一個代碼段,這一段代碼段有著特殊的性質,不可分割,相當於原子操作,嚴格禁止被打斷,所以在進入臨界段的時候要進行中斷屏蔽,防止系統的調度中斷打斷了臨界段的執行。臨界段的內核代碼實現挺簡單,因為Cortex-M 內核專門設置了一條 CPS 指令有 4 種用法,可以用來控制開關中斷和開關异常,使用的時候直接調用就行

1 CPSID I ;PRIMASK=1 ;關中斷
2 CPSIE I ;PRIMASK=0 ;開中斷
3 CPSID F ;FAULTMASK=1 ;關异常
4 CPSIE F ;FAULTMASK=0 ;開异常

2.6 任務就緒列錶

​ 准備運行的任務被放置於就緒列錶中。就緒列錶包括2 個部分:一個錶示任務優先級的優先級錶,一個存儲任務TCB 的雙向鏈錶;優先級錶本質上就是一個32比特整形,錶示32 個優先級,如果要擴展優先級的話,在增加幾個32比特整形來擴展優先級列錶就行,但一般32比特足够使用,優先級列錶中越低比特代錶的優先級越高,當對應優先級有任務就緒的時候,會把優先級錶中的對應比特置1,錶示該優先級至少有一個任務已經就緒了,在優先級錶中遍曆找0的方法主要有兩種:一種是前導0(從高比特向低比特遍曆),另外一種是後導0(從低比特向高比特遍曆)

20210815222642

​ 就緒列錶的另外一個部分就是一個用於存儲任務TCB 的雙向鏈錶,准確來說是一個數組,數組長度和優先級的長度一致,數組每個成員指向一個鏈錶的首尾,就像下圖:

20210815225014

​ 每個數組成員指向的鏈錶裏面的每個節點都是同一個優先級的任務TCB,因為和優先級錶對應,所以內核根據優先級錶可以很快的索引到就緒任務的TCB,進行調度操作

2.8 任務時間片運行

​ 剛剛了解了就緒列錶的概念,有的同學可能有疑惑,OSRdyList[]每個數組成員指向的鏈錶裏面的每個節點都是同一個優先級的任務TCB,那這些同優先級的任務如何運行呢?這就引入了任務時間片運行的概念

​ 時間片運行就是相同優先級的任務可以分配不同的時間片(TiCK數量),任務每運行一次心跳時鐘(TICK)消耗一個時間片,當任務時間片用完的時候,任務會從同優先級鏈錶的頭部移動到尾部,讓下一個任務共享時間片,以此循環,內核實現的方式也挺簡單,就是TCB增加一個時間片計數成員變量,每次調度時候計算在統計一下,為0就進行一次鏈錶的節點移動操作,就像下圖的Task2和Task3優先級都是2,TCB中有TimeQuanta 錶示任務需要多少個時間片,TimeQuantaCtr 錶示任務還剩下多少個時間片,當它為0時,任務2和任務3進行切換

20210815230941

這裏有一個我以前寫的時間片切換流程圖參考

時間片

2.9 系統時基列錶

​ 時基列錶我在前面寫阻塞延時的時候有提到,由名字我們可以看出時基列錶是跟時間相關的,處於延時的任務和等待事件有超時限制的任務都會從就緒列錶中移除,然後插入到時基列錶這個小黑屋裏面,每次TICK都對他們單獨計時,任務延時完成後從小黑屋出來插回到就緒列錶去,而等待超時了之後還要在進行一個小判斷,看任務是進行恢複還是掛起等其他操作,時基列錶的結構很簡單,在代碼層面上由全局數組OSCfg_TickWheel[]和全局變量OSTickCtr 構成

全局數組OSCfg_TickWheel[]的每個成員都包含一條單向鏈錶,被插入到該條鏈錶的TCB 會按照延時時間做昇序排列,其中FirstPtr 用於指向這條單向鏈錶的第一個節點。

20210815232449

全局變量OSTickCtr 則記錄了系統啟動後經過了多少個TICK周期

當然每個任務的TCB裏面還要有時基列錶的計算單元,用來統計任務的延時時間和超時等待時間

/*時基列錶相關字段*/
OS_TCB *TickNextPtr; (1)
OS_TCB *TickPrevPtr; (2)
OS_TICK_SPOKE *TickSpokePtr; (5)
OS_TICK TickCtrMatch; (4)
OS_TICK TickRemain; (3)

其中TickCtrMatch 的值等於時基計數器OSTickCtr 的值加上TickRemain 的值,當TickCtrMatch 的值等於OSTickCtr 的值的時候,錶示等待到期,TCB會從鏈錶中删除;每個被插入到鏈錶的TCB 都包含一個字段TickSpokePtr,用於回指到鏈錶的根部,方便快速定比特,因為時基列錶操作很頻繁,犧牲一點小內存換取速度,就像下圖,有三個TCB在就緒列錶裏面等待

20210815233337

下圖是我以前記錄的uCos時基列錶運行流程圖

時基列錶

2.10 任務掛起與恢複

uCOS-Ⅲ 的任務支持掛起和恢複的功能,掛起就相當於按下暫停鍵,任務停止,暫停後的任務從就緒列錶中移除,任務恢複即重新將任務插入到就緒列錶,一個任務掛起多少次就要被恢複多少次才能重新運行,一般情况下任務等待信號量、mutex、事件標志組、消息隊列時,該任務會被放入掛起隊列,但也有手動掛起的API接口,由用戶進行掛起;uCOS的任務有多種狀態,具體如下

/* ---------- 任務的狀態 -------*/
#define OS_TASK_STATE_BIT_DLY (OS_STATE)(0x01u) /* /-------- 掛起比特 */
#define OS_TASK_STATE_BIT_PEND (OS_STATE)(0x02u) /* | /----- 等待比特 */
#define OS_TASK_STATE_BIT_SUSPENDED (OS_STATE)(0x04u) /* | | /--- 延時/超時比特 */
#define OS_TASK_STATE_RDY (OS_STATE)( 0u) /* 0 0 0 就緒 */
#define OS_TASK_STATE_DLY (OS_STATE)( 1u) /* 0 0 1 延時或者超時 */
#define OS_TASK_STATE_PEND (OS_STATE)( 2u) /* 0 1 0 等待 */
#define OS_TASK_STATE_PEND_TIMEOUT (OS_STATE)( 3u) /* 0 1 1 等待+超時 */
#define OS_TASK_STATE_SUSPENDED (OS_STATE)( 4u) /* 1 0 0 掛起 */
#define OS_TASK_STATE_DLY_SUSPENDED (OS_STATE)( 5u) /* 1 0 1 掛起 + 延時或者超時 */
#define OS_TASK_STATE_PEND_SUSPENDED (OS_STATE)( 6u) /* 1 1 0 掛起 + 等待 */
#define OS_TASK_STATE_PEND_TIMEOUT_SUSPENDED (OS_STATE)( 7u) /* 1 1 1 掛起 + 等待 + 超時 */

任務狀態由三比特錶示,按順序分別為掛起比特、等待比特、延時比特,三個比特組合有8種任務狀態,掛起是其中之一

任務掛起的核心單元就是掛起隊列,掛起隊列類似於就緒隊列,掛起隊列中放的是等待內核對象的任務。另外,任務在掛起隊列中是根據優先級分類的。高優先級任務被放置在隊列的頭部,低優先級任務被放置在隊列的尾部,掛起隊列是一個OS_PEND_LIST 類型的數據結構,與前面不同的是掛起隊列中不是指向任務的OS_TCB ,而是指向
OS_PEND_DATA鏈錶,該鏈錶結構如下:

20210815234601

結構前三個成員就是指針域不多說了,主要是下面幾個:

  1. PendObjPtr 指向任務所等待的內核對象
  2. RdyObjPtr 如果任務等待多個內核對象,該指針指向任務被放入掛起列錶前已經被提交的內核對象
  3. 如果任務等待多個內核對象,該指針指向任務被放入掛起列錶後被提交的內核對象

每個內核對象會有一個指針指向他的掛起隊列,就緒下面的信號量的結構一樣,把等待的任務和內核對象關聯起來:

20210815235023

​ 在每個TICK也會檢查任務 TCB 的掛起值,掛起時會將他+1,恢複時則會减1,為0時會脫離掛起隊列恢複到就緒列錶中去,參與內核的調度,這裏有一個我之前記錄的流程圖幫助大家理解,但此處的掛起只是簡單的手動掛起和恢複,不涉及到內核對象

掛起與恢複

2.11 任務删除

​ 任務的删除就很簡單了,斷開TCB與所有內核對象的關聯,清除自身變量,回收資源,完結撒花,但注意一點空閑任務不能被删除,因為RTOS至少有一個任務在運行,大致的流程可以參考我以前做的Mind流程圖

任務删除

三、總結

手碼了這麼多字還是蠻累了,有許多內容寫還不完善,也不清晰,內核這東西還挺複雜麻煩,講清晰挺難,更多要自己去實踐,跟著寫一寫代碼,做做實驗,多調調Bug,翻翻底層代碼也就熟悉使用了,這篇文章內核原理內容就到這了,主要圍繞RTOS運行的基本原理來講解的,下一份總結則是圍繞內核對象來進行講解,內核對象就是信號量、互斥量、時間、消息隊列、內存池、軟件定時器還有一些系統任務比如軟件定時器任務、中斷處理任務、統計任務、時基任務等等

四、文章推薦

你和PID調參大神之間,就差這篇文章!

神器!200元開發板運行神經網絡模型,吊打OpenMV!(保姆級教程)

51單片機多線程神器:Tiny-51操作系統

用樹莓派做服務器運行博客網頁

版权声明:本文为[JeckXu666]所创,转载请带上原文链接,感谢。 https://gsmany.com/2021/08/20210816011924797z.html