官术网_书友最值得收藏!

第1章 實時操作系統及μC/OS-III簡介

本章首先介紹從單片機應用程序框架引入的實時操作系統,接著介紹怎么學習μC/OS-III源碼,然后簡單介紹μC/OS-III的文件結構和數據結構、任務、內核對象、常見代碼段等,為后面的章節做鋪墊。有些概念剛開始接觸可能不是很容易理解,可以先忽略它們繼續閱讀下去,后面就會有“柳暗花明又一村”的感覺。

1.1 單片機應用程序框架

1.1.1 前后臺系統

在單片機應用程序中,最常用的就是前后臺系統,通常由一個大循環和中斷組成,大循環是后臺,中斷是前臺。前后臺系統的程序流程比較簡單,但是,當芯片處理的事情越來越多、越來越復雜的時候,前后臺系統并不能保證實時性。這在一些協議棧等實時性要求高的場合是不允許的,這時編寫程序的人就要重新設計程序框架或者使用本書介紹的實時操作系統幫忙進行合理的任務調度,讓緊急的事務優先執行。

順序執行的前后臺系統的程序如代碼清單1-1所示。

代碼清單1-1 前后臺系統程序框架


    main()
    {
        Peripheral_Init();           //外設初始化
        while(1)
        {
            process();               //程序主要處理部分
        }
    }

除了簡單的順序執行那樣的前后臺系統程序外,還可以采用時間片輪詢法加以改進。

時間片輪詢法是多個任務以一定的頻率執行,就像多個任務一起無干擾地執行一樣。下面看看一個簡單的時間片輪詢法的代碼框架是怎么實現的。

本次介紹使用時間片輪詢法創建多個任務并一起運行,LED1任務每500ms轉換一次狀態, LED2任務每1s轉換一次狀態,LED3任務每2s轉換一次狀態,實現起來非常簡單。首先看看main函數的流程,時間片輪詢法中任務的編寫方式如代碼清單1-2所示,并且放入while(1)循環中。

代碼清單1-2 任務編寫方式


        01     if(Task_delay[i]==0)
        02     {
        03       Task(i);
        04       Task_Delay[i]=XXX;
        05     }

Task_delay是一個數組,有多少個任務就有多少個數組元素,我們在例程1-1中設置為3個,但一開始數組元素都設置為0。第一次判斷肯定會進入if判斷里面執行任務Task(i)的代碼,執行完這個任務之后,Task_Delay[i]變成了XXXX。這里假設XXXX=1000,下次判斷如果Task_Delay[i]不為0,就不會執行任務Task(i)。如果使用一個定時器每1ms將Task_Delay[i]減1,那么在1000ms之后的判斷條件又成立了,并且任務執行的間隔是1000ms,如此循環往復,就可以讓任務Task(i)的執行頻率大致為1Hz。在while(1)這個循環中,還可以創建其他任務,并且可以通過設置Task_delay[j]的大小來決定任務執行的頻率。想要理解好上面的內容,需要忽略任務運行的時間、定時器中斷服務程序執行的時間。只要不在任務代碼里添加太長的延時、執行時間長的任務,那么任務的執行頻率跟理想化的不會相差太遠,而導致前后臺系統實時性差的也正是這些原因。

        01 /*****************************************************************************
        02 * 函數名稱: main
        03 * 輸入參數: void
        04 * 輸出參數: int
        05 * 功    能: 程序入口
        06 * 作    者: 驍龍Lyc
        07 * 舉    例: 無
        08 ***************************************************************************/
        09 int main(void)
        10 {
        11   /*LED燈管腳的配置*/
        12   LED_ Config();
        13
        14   /*這里中斷頻率配置為1000Hz*/
        15   Interrupte _Config (72000);
        16
        17   /* Infinite loop */
        18   while (1)
        19   {
        20     if(Task_Delay[0]==0)
        21     {
        22       LED1_TOGGLE;
        23
        24       Task_Delay[0]=500;
        25     }
        26
        27     if(Task_Delay[1]==0)
        28     {
        29       LED2_TOGGLE;
        30
        31       Task_Delay[1]=1000;
        32     }
        33
        34     if(Task_Delay[2]==0)
        35     {
        36       LED3_TOGGLE;
        37
        38       Task_Delay[2]=2000;
        39     }
        40   }
        41 }

前面介紹了中斷服務程序會在每次中斷時將Task_delay這個數組的所有元素執行減1操作。接下來看看嘀嗒定時器的中斷服務程序。

    01 /*****************************************************************************
    02 * 函數名稱: Interrupte_Handler
    03 * 輸入參數: void
    04 * 輸出參數: void
    05 * 功    能: 時間片輪詢法中定時器的中斷服務程序
    06 * 作    者: 驍龍Lyc
    07 * 舉    例: 1ms,1次中斷
    08 ***************************************************************************/
    09 void Interrupte_Handler(void)
    10 {
    11   unsigned char i;
    12   for(i=0;i<NumOfTask;i++)
    13   {
    14     if(Task_Delay[i])
    15     {
    16       Task_Delay[i]--;
    17     }
    18   }
    19 }

中斷服務程序其實很容易理解,在for循環中將NumOfTask個任務對應于數組Task_Delay中的元素都減1,但是在減1之前要判斷元素是否為0,如果為0就停止減1操作。通過上述代碼,就簡單地實現了時間片輪詢法,可以在此基礎上任意地添加任務。想對一般程序來說,這段代碼的實時性比搶占式實時操作系統差。宏觀上,時間片輪詢法的各個任務也不會互相影響,好似多個任務一起相安無事地在進行著,實際上,若任務很多或者任務執行的時間比較長,還是會影響任務的執行頻率。比如其中一個任務霸占CPU的時間足1s,而其他任務在這1s內完全是不執行的,即執行頻率變為0,這在某些系統中是不允許的。

1.1.2 嵌入式實時操作系統

前后臺系統就像一個人從頭到尾去做一件事情,偶爾還被叫去處理一些突發事件(中斷)。編寫程序的人可完全掌握程序的運行流程,但是當事情多的時候可能無法把所有的事情都做好。而引入實時操作系統就像是請了一個管理團隊,這個管理團隊會幫你協調好這些事情,這樣在處理事情的時候就會顯得井井有條,提高了CPU處理復雜事情的能力。但是請一個管理團隊這個過程會占用一些資源,比如請管理人員的費用等,若公司的運行效率提高了,那么花這些費用還是值得的。在實時操作系統中這就對應著增加CPU的負擔、占用芯片的內存。

實時操作系統通過一系列軟件管理讓一個CPU形成多個線程,就像有多個CPU在一起執行一樣。至于這是怎么實現的,就是本書要講解的重點。

實時操作系統中比較重要的是實時性,即要求系統有比較快的響應速度和執行速度。在時間片輪詢法中,任務是一個一個執行的,如果其中一個任務的執行時間過長,那么會影響到其他任務的執行,這就不適合實時性要求高的系統。為了應對更復雜的程序開發,嵌入式實時操作系統應運而生。不管當前任務是否放棄使用CPU,嵌入式系統中的任務可以隨時切換到優先級比較高的任務。所以,只要將任務的優先級設置得足夠高,這個任務的實時性就很好。實時操作系統可以分為硬實時操作系統和軟實時操作系統。硬實時操作系統要求在規定的時間內必須完成任務,而軟實時操作系統要求越快完成越好。

實時操作系統又可分為不可剝奪型操作系統和可剝奪型操作系統。不可剝奪型內核只有在當前線程放棄使用CPU之后,其他的線程才可以占用CPU。而可剝奪型內核只要存在有更高優先級的線程就緒,低優先級的線程就會被打斷,高優先級線程就占有CPU。μC/OS-III屬于可剝奪型內核。

在可剝奪型操作系統中,任務代碼隨時都可能被打斷而派去執行另外的任務,這時任務可能正在執行往一個變量寫入數值的過程,而這個過程是需要多個指令的。如果寫入過程只進行到一半,任務就被打斷,而且這個變量有可能在中斷或者其他的任務中被讀取,那么讀取到的這個變量將是一個錯誤的值。所以寫入的這個過程就不能被中斷或者被其他任務搶占。這種類似主要的程序段我們稱為臨界段。臨界段是程序不能被中斷或者其他優先級的任務打斷的程序段。一般來說,有兩種方式可以保護臨界段:一種是關中斷,另一種是鎖調度器。其中,鎖調度器可以防止其他任務訪問臨界段代碼,但是不能防止中斷訪問臨界段代碼。

注意:

1)變量的讀取如果只在一個任務的上下文中,那么就不需要保護這個變量的讀/寫過程。

2)如果變量只可能在其他的任務中讀取,而不可能被中斷讀取,那么可以采用鎖調度器的方式,避免了關中斷,節省了關中斷的時間。

因為線程可以隨意被高優先級打斷,所以需要保存當前線程運行的上下文,以及注意臨界段的保護。因此可剝奪型內核實現起來更困難。

1.2 如何使用和學習μC/OS-III源碼

好的學習方法事半功倍。本節將結合筆者學習μC/OS-III源碼的經驗來介紹下如何學習μC/OS-III源碼。

1. 查閱文檔,動手實踐

在學習源碼之前,還是先讓自己對μC/OS-III有個整體的感知,這是本書對讀者的最低要求。一般來說,開發板例程會提供μC/OS-III直接可用的程序,你可以在開發板例程的基礎上照貓畫虎寫幾個任務并運行之?;蛟S你不知道任務的奇怪參數是什么意思,但這也絲毫不會影響你編寫簡單的程序。創建完任務后最好還使用一些內核對象,如信號量、消息隊列、定時器等,這些也可以照貓畫虎,但也可以自己照著μC/OS-III的手冊和函數注釋來使用這些內核對象。下面舉例說明筆者一開始是怎么使用定時器的。筆者剛開始接觸μC/OS-III,是在做一個比較大的項目,需要用到多個定時器,了解用硬件定時器太麻煩,而μC/OS-III能提供軟件定時器。首先到定時器的文件中看看有什么函數可以調用,如圖1-1所示。

在函數列表中可看到一個OSTmrCreate函數(見圖1-1),無論是變量名、宏定義還是函數名,μC/OS-III命令都十分規范,讓人很容易見名知意,而通過OSTmrCreate這個函數名稱就可以知道這是一個創建定時器的函數。到底要怎么調用這個函數呢?μC/OS-III函數注釋得非常詳細,如圖1-2所示。

圖1-1 OSTmrCreate函數的位置

找到OSTmrCreate函數的定義處,函數前面就有詳細的注釋。通過注釋可以知道函數的用途,應該輸入什么樣的參數,會有什么返回值,有什么注意事項等,可以根據這些提示來調用函數。注釋不可能面面俱到,讀完注釋后,在調用過程中可能還會有很多不能理解的地方,比如定時的單位是什么。這要靠你對μC/OS-III的理解,可以查閱一些相關書籍,本書也會講解μC/OS-III部分函數的使用。為什么是部分呢?在看源碼注釋的時候,我們會看到一些函數的注釋中包含這樣的注釋:

    * Note(s)     : 1) This function is INTERNAL to uC/OS-III and your application
    MUST NOT call it.

圖1-2 OSTmrCreate函數注釋

如果函數的注釋中有這樣的注釋,說明在使用μC/OS-III的時候不能調用他們,這些函數一般都是內核操作相關的,如果隨便調用可能導致意想不到的錯誤,最好還是不要調用他們,所以本書也就不講解這些函數是怎么使用的。代碼清單1-3展示了怎么調用函數OSTmrCreate。

代碼清單1-3 函數OSTmrCreate的調用


        1 OSTmrCreate ((OS_TMR               *)&TmrOfKey,
        2                              (CPU_CHAR             *)"TmrOfKey",
        3                              (OS_TICK               )0,
        4                              (OS_TICK               )1,
        5                              (OS_OPT                )OS_OPT_TMR_PERIODIC,
        6                              (OS_TMR_CALLBACK_PTR  )cbTimerOfKey,
        7                              (void                 *)0,
        8                              (OS_ERR               *)&err);

本書是在筆者閱讀μC/OS-III源碼和使用μC/OS-III的基礎上進行介紹的。只翻譯注釋來講解每個函數怎么使用是沒有多少意義的。我們在解析每個源碼之前必須先講解怎么使用函數??春瘮翟创a解析的時候最好先按照本書的編排方式看看函數是怎么使用的,函數的使用也可以看成是源碼解析的一部分,因為函數的使用會介紹函數的功能、函數的參數等,這些都是理解函數必不可少的信息。

用OSTmrCreate創建完定時器后,定時器是否就可以運行了呢?實踐證明還是不可以。當時筆者接著查看了定時器文件的函數,又看到另一個函數OSTmrStart,可能還要調用這個函數來開啟定時器才能運行,如圖1-3所示。這樣調用函數之后發現定時器就真的運行了。

由以上過程可以看到,調用μC/OS-III函數的過程還是挺方便的。調用函數其實不難,但要結合注釋或者一些書籍的講解才能真正用好這些函數。本書配套了每個內核對象的使用例程,注釋也十分詳細,跟著例程照貓畫虎地使用這些內核對象也是一種不錯的選擇。

或許有人會想,如果已經有相應移植好的工程,那么在上手μC/OS-III之前就先進行移植。這是完全沒有必要的,因為官方幫我們將大部分μC/OS-III移值到了CPU上,加上移植的過程并不簡單,移植需要建立在對CPU內核和μC/OS-III有足夠理解的基礎上。本書將移植μC/OS-III放在最后介紹,也可以說是因為μC/OS-III的移植是最難的。

2.循環漸近,深入了解

以上介紹了怎么用好μC/OS-III,接下來介紹怎么深入學習μC/OS-III源碼。最開始編寫本書的時候,筆者在想, μC/OS-III中那么多內容究竟應該按照什么樣的順序講解才是最好的。一開始最好挑最容易的,與整個μC/OS-III聯系最少的那些源碼進行講解。本書講解的順序相對來說是比較好的。所以,首先要對嵌入式操作系統和μC/OS-III有一個大致的了解。接著了解比較容易的時鐘節拍和定時器、多值信號量。由于多值信號量跟二值信號量、消息隊列、事件標志聯系比較緊密,所以將這些內容放在一起介紹。最后講解任務切換、任務相關等內容。每學習一個內核對象之前,最好先了解內核對象的數據結構。

圖1-3 OSTmrStart函數的位置

1.3 μC/OS-III文件結構簡介

μC/OS-III文件將按照由底層到上層的排列順序進行整理。下面根據圖1-4中的序號對μC/OS-III的文件結構進行介紹。

①配置文件。通過配置文件定義宏的值就可以輕易地裁剪其他不用的代碼。

②應用程序。應用程序包含任務的定義和聲明,用戶主要編寫的是這部分代碼。

③μC/OS-III與CPU無關代碼。這部分代碼與CPU無關,可以不做修改移植到任何CPU,本書主要講解這部分代碼。

④庫。這部分主要是底層函數庫,比如字符串的一些常規操作、一些通常的數學計算等。

⑤μC/OS-III與移植相關代碼,如果讀者想要移植μC/OS-III到不同平臺上,那么可以根據CPU來修改這部分代碼。

⑥μC/OS-III與CPU相關代碼。

圖1-4 μC/OS-III文件結構

從圖1-4中可以看出μC/OS-III文件結構層次分明,我們僅需修改⑤、⑥這兩部分的代碼即可,直到不同的CPU。

1.4 μC/OS-III數據結構簡介

學習OS(操作系統),很重要的也是很基礎的就是要先了解它里面復雜繁多的數據結構。數據結構是程序操作的對象,操作的過程都建立在這些數據結構的基礎上面,操作對象搞明白了,操作的過程也很容易懂。不管學習什么OS的源碼,筆者建議大家一定要在前面花些時間去了解它的數據結構,并做做筆記、畫畫圖。

如果讀者自學μC/OS-III,則可能會被μC/OS-III中“指來指去”的指針搞暈。為什么要搞這么復雜的數據結構呢?數據結構雖然復雜,但是操作卻容易,在函數的入口只要輸入一個簡單的變量就可以找到很多相關的變量。同時,設計好的數據結構也方便程序的編寫。如果讀者還沒有學過數據結構,建議同時看看《大話數據結構》這本書。對于本書來講,只要了解前面的線性表章節就可。本書后面介紹內核對象的章節都會講解相關的數據結構。讀者要是清楚這些數據結構,那就在后面講到這些相關變量操作的時候再回來看看那些圖片。筆者也沒有記住多少內容,只是看名字就大概知道它們的作用以及它們之間的關系。圖片可以讓大家更好地理解數據結構的關系,所以本書筆者精心制作了多幅圖片來講解它們之間的關系。下面以節拍列表的數據結構圖來講解應該怎么看這些圖片(見圖1-5)。

圖1-5 TickList數據結構

第一眼看圖1-5,讀者可能有點看不明白。這種圖在本書中很常見,也常見于數據結構的書,本書用這樣的圖來展示出μC/OS-III中的各種數據結構。從圖1-5中首先可以看到由幾個小方格組成一個大方格。大方格表示一個結構體類型,大方格上面的名稱就是相應的結構體類型的名稱;小方格是大方格的成員,小方格里面的名稱表示成員的名稱。大方格左側是具體的結構體變量的名稱,如圖1-5左上角中的OSCfgTickWheel[0]是一個OS_TICK_SPOKE類型的結構體,結構體中有三個成員,分別是FirstPtr、NbrEntries、NbrEntrMax。Entry表示條目,記載數量的變量名經常有這個單詞。Nbr是number的縮寫,μC/OS-III中有很多變量,千萬不要死記硬背,而要根據其命名和習慣來知道它們代表什么,這點對理解源碼也是很有幫助的。

從圖1-5的右邊可以看到幾個由單向箭頭組成的雙向箭頭,雙向箭頭表示雙向鏈表。值得注意的是,雖然TickNextPtr在圖中看起來是指向下一個結構體的TickNextPtr,但實際上是TickNextPtr存放下一個結構體的地址。如果不知道雙向鏈表的含義,請參閱數據結構相關的書籍。

從圖1-5中還可以看到有很多個箭頭,箭頭代表指向其他變量的指針,如圖1-5中最上方的一個箭頭表示的就是OSCfg_TickWheel[0](結構體)的成員FirstPtr存放的是OS_TCB類型的數據。

后面若有相關的圖,也是按照上面的解釋來解讀。

1.5 任務

在μC/OS-III的管理下,可以創建多個任務,并讓它們看起來是各自有一個CPU在并發運行。例如在計算機上,我們可以一邊瀏覽網頁,一邊聽歌,這也得虧操作系統的管理。任務一般都是死循環,如果只想任務運行一次,那么可以在執行完任務后將其刪除。OS_TCB結構體類型可以定義任務控制塊,每個任務都會有這樣一個結構體變量,里面包含任務的各種信息,包括優先級、任務的狀態、堆棧的基地址、任務名稱等。OS_TCB為每個任務定義的任務控制塊可以看成是任務的“身份證”,“身份證”包含任務的各種信息。

任務的狀態

理解好任務的幾個狀態十分重要,內核中所有的程序幾乎都涉及這些狀態的轉化。任務將其狀態保存在任務控制塊的元素TaskState中。

1)OS_TASK_STATE_RDY:就緒狀態。μC/OS-III可能有多個任務處于就緒狀態,但只能是其中優先級最高的任務占用CPU,因為CPU只有一個,不可能多個任務一起并行運行。運行的任務是就緒狀態。創建任務完成的時候也是就緒狀態。

2)OS_TASK_STATE_DLY:延時狀態。用官方的延時函數(OSTimeDly/OSTimeDlyHMSM)將其設置為這種狀態,這時任務會放棄CPU讓其他就緒任務中優先級最高的任務運行,如果沒有任務就緒,系統就會運行優先級最低的空閑任務——這其實就是不斷地給變量做加法的任務。

3)OS_TASK_STATE_PEND:任務在等待某些事情的發生,比如等待其他任務釋放資源后發送一個信號量來讓任務運行,也可以是其他的任務發送消息過來等。將其設置為這種狀態的一般都是名字中含有Pend的內核函數,比如OSQPend、OSSemPend等。

4)OS_TASK_STATE_PEND_TIMEOUT:也是任務在等待某些事件的發生,不過這是要經過超時檢測的,一旦任務調用等待函數OSQPend、OSSemPend等將任務處于等待狀態,就要設置超時的時間。如果超時時間設置為0,那么任務就是OS_TASK_STATE_PEND,即無限期地等下去,直到事件發生。如果超時時間設置為N(N>0),設置為等待后的時間N內任務狀態就是OS_TASK_STATE_PEND_TIMEOUT,在等待N個系統節拍后事件還是沒有發生就會退出等待狀態而轉為就緒狀態。

5)OS_TASK_STATE_SUSPENDED:掛起狀態,可以理解為強行暫停一個任務。

6)OS_TASK_STATE_DLY_SUSPENDED:這種情況就是任務自己先產生一個延時,延時沒有結束的時候又被其他的任務掛起。注意,不是掛起后又被延時,因為掛起的時候任務已經被剝奪了CPU的使用權,而延時的時候只能自己延時自己。需要這兩種狀態都解除才可以就緒。

7)OS_TASK_STATE_PEND_SUSPENDED:這種情況也是任務自己先等待一個事件的發生,還沒有等到事件發生就又被掛起。需要兩種狀態都解除才可以就緒。

8)OS_TASK_STATE_PEND_TIMEOUT_SUSPENDED:與上面的狀態一樣,只是加了一個超時檢測。需要兩種狀態都解除才可以就緒。

9)OS_TASK_STATE_DEL:任務被刪除后的狀態。任務被刪除后將不再運行,要讓其恢復運行只能重新創建任務。

1.6 內核對象簡介

內核對象有很大一部分是為任務服務的,比如想在兩個任務之間傳遞數據就可以采用消息隊列。任務之間是相互獨立的,它們之間的“溝通”是通過內核來完成的。所有的內核對象包括任務、堆棧、信號量、事件標志組、消息隊列、消息、互斥信號量、內存分區、軟件定時器等。

1.6.1 信號量

這里的信號量主要是指多值信號量。多值信號量包含兩項功能:一項是管理資源,一項是標志事件的發生。管理資源的一個很通俗的例子就是停車場,比如停車場開始的停車位有10個,每用掉一個就將停車位剩余數量減1,這時信號量就相當于停車場外剩余停車位的個數,這里管理的資源就是停車位。當停車位為0時就不可以將車開進停車場。想象如果停車場外沒有這個標志或者沒有人充當這個標志來管理,那么停車場在滿的情況下外面的車還是不斷地開進去,最后將導致整個停車場癱瘓。管理的資源如內存空間、I/O口等如果沒有管理也會出錯。下面以單片機內部的串口為例進行介紹。如果串口使用過程中被其他任務占用,那么輸出的結果將不倫不類,如同一個句子寫一半后接著另外句子的開頭。這也說明芯片的外設在搶占式實時操作系統的管理下,如果在不同的任務中被使用,則需要用信號量保護起來。操作系統給編程帶來便利的同時也帶來了額外需要考慮的問題。信號量的另一項功能是標志事情的發生,這時初始值要設置為0,表示沒有發生事件。若程序有時候想讓兩件事情同步,就可以先讓信號量作為同步的中介。第一個任務的一個事件先發生后,想等待另外一個任務的另外一個事件發生,就可以先等待(等待的英文為pend)一個信號量。等待的事件發生后調用OS內部提交(提交的英文為post)信號量的函數,函數就會把等待信號量的任務解除等待,并置于就緒隊列。

1.6.2 事件標志組

實際上,信號量的第二項功能才是事件標志組經常干的事情。事件標志組可以用來標志多個事件的發生,同信號量一樣,也可以進行post和pend操作。事件標志組可以設置多個位,最大的位數由數據類型OS_FLAGS決定,在我們的例程中是32位,代表多個事件的情況。其他的任務可以等待某些位被置1或者清0,然后允許任務執行,這里時間標志組充當了中間的媒介。一個現實的例子就是鍵盤的組合操作,通過事件標志就可以很容易設置組合鍵。任務先設置一個兩個位都被置1的等待事件,然后將這兩個位置0,當一個按鍵被按下時就post這個按鍵相應位置1的情況。若兩個按鍵都被按下,那么兩個位都被置1,等待這兩個事件發生的任務不再被阻塞,繼續執行要執行的內容。事件標志組聽起來可能有點抽象,讀者可能不知道具體為何物,這是很正常的,保持好奇心去閱讀就好。

1.6.3 消息隊列

消息隊列可以分為消息和隊列。消息的集合才是隊列,隊列是用來管理消息的。消息隊列主要用來在任務之間傳輸數據。消息可以簡單地理解為一個存儲發送數據的信息(包括信息的地址、信息的字節大小等)的存儲空間。

1.6.4 互斥信號量

前面已有信號量的存在,為什么還要互斥信號量(互斥信號量英文為Mutex)呢?這里面存在一個稱為優先級反轉的問題。搶占式內核的宗旨就是讓高優先級的任務盡量先占用CPU。信號量(這時信號量用來管理資源)已經被低優先級占用的時候,高優先級就要等待這個低優先級釋放信號量。這是可以理解的,也是很合理的,資源不像CPU那樣可以隨便切換,比如前面講過的串口打印數據,打印一半就切換給高優先級的任務,結果肯定不是我們想要的,我們需要的是各自完整的打印結果。更有甚者,即使目前占用資源的低優先級任務被掛起,也不可以“切換”資源給等待這個信號量的高優先級任務。等待資源的這個過程,高優先級已經被掛起,意味著高優先級任務的優先級已經“降低”到占用信號量的任務的較低優先級,“降低”的原因是受資源不能切換的限制。而這時如果還有一個處于前面提到的這兩種高低優先級中間的優先級任務想要占用CPU,那么CPU就會被中優先級的這個任務占用,因為它的優先級比低優先級高。這不符合我們前面的宗旨,中優先級占用CPU實際上讓高優先級等待更多的時間才運行,所以不能讓中優先級占用CPU!解決辦法就是優先級繼承,就是提高前面提到的低優先級任務的優先級,讓它跟等待資源的高優先級處于同一個優先級。前面提到的優先級“降低”的問題就迎刃而解,“降低”到跟自身一樣相當于沒有“降低”。因此就產生了互斥信號量。互斥信號量比多值信號量多一個優先級反轉的機制,互斥信號量可以避免優先級反轉的問題,但是操作過程占用的時間也更多。

1.6.5 內存分區

內存分區主要用于減少內存碎片,相關內容請參見相應章節。

1.6.6 軟件定時器

跟硬件定時器一樣,軟件定時器也有定時的功能。相對來說,軟件定時器的配置比較容易,但是定時精度很難達到硬件定時器的標準。因此,軟件定時器可以用于精度要求相對不高的一些事務中,比如1分鐘后關閉LCD屏幕的背光等。

1.7 μC/OS-III常見的程序段

本節介紹的是經常在程序中出現的相似的程序段。

1.7.1 中斷嵌套層數統計

每進入一層中斷,μC/OS-III的OSIntNestingCtr就會加1,每退出一層中斷就要減1。中斷嵌套層數功能之一就是可以用來檢查μC/OS-III是否進入了中斷,OSIntNesting大于0就代表至少進入了一層中斷,這樣可以防止一些不能在中斷中調用的函數被調用,比如任務調度函數OSSched,在中斷服務程序運行的時候進行了任務切換,其后果肯定是相當嚴重的。中斷通常是比較緊急的事情,就好像這邊著火了,你肯定不能救火救到一半去看場電影再回來。還有一些函數的臨界段比較長,并且沒有必要在中斷中運行,比如信號量創建函數OSSemCreate等是不允許在中斷中調用的,調用這些函數之前都會先檢查是否進入中斷。進入中斷之前,用戶需要調用增加嵌套層數的函數,如代碼清單1-4所示。

一個中斷服務函數通常如下。

代碼清單1-4 μC/OS-III中斷服務函數示例


    1     void XXX_ISR(void)
    2 {
    3     OSIntEnter();     //嵌套層數加1
    4
    5     XXXXXXXXXXXX;     //中斷執行內容
    6
    7     OSIntExit();       //嵌套層數減1
    8 }

1.7.2 開中斷和關中斷

開關中斷進入臨界段的用法如代碼清單1-5所示。

代碼清單1-5 臨界段程序的編寫


    1 XXX_Fuction(void)
    2 {
    3     CPU_SR_ALLOC();
    4     CPU_CRITICAL_ENTER();
    5
    6     //臨界段代碼
    7
    8     CPU_CRITICAL_Exit();
    9 }

后面介紹的每個函數基本上都有這些代碼段,因為涉及太多內核的東西,剛接觸的讀者可能比較難以理解,所以在這里先不講解其具體代碼的實現,只要知道這些函數的功能和怎么使用即可。

注意:不能在關中斷期間阻塞任務運行,即不能調用阻塞函數,如包含pend或者delay等詞的函數,這些都涉及任務切換。而任務切換跟中斷是相關的,如果在關中斷期間阻塞任務,則不會達到阻塞任務的效果。

1.7.3 使能中斷延遲的鎖住和開啟調度器

使用中斷延遲的時候,進入和退出臨界段有兩種方式。關中斷前面已經介紹了,下面介紹鎖住和開啟調度器。

鎖住調度器在開啟中斷延遲的時候分為兩種情況:一種是調用宏OS_CRITICAL_ENTER(),直接鎖住調度器;另一種是調用宏OS_CRITICAL_ENTER_CPU_CRITICAL_EXIT(),調用這個宏之前先關閉中斷,接著鎖住調度器,恢復中斷在關閉之前的狀態(注意這里不是打開中斷)。這種常用于臨界段前面(部分)不被中斷打斷,而后面部分只要不被其他任務打斷即可。前半部分先關中斷,后半部分調用開執行OS_CRITICAL_ENTER_CPU_CRITICAL_EXIT(),這樣就減少了關中斷的時間。沒有使用中斷延遲的時候整段都要關中斷,這樣就延長了關中斷的時間。

解開一層調度嵌套也有兩種方式:一種是調用宏OS_CRITICAL_EXIT(),另外一種是調用宏OS_CRITICAL_EXIT_NO_SCHED()。從宏的名字上看,它們的區別是在解開一層調度嵌套后是否嘗試進行任務調度。臨界段中是無法實現任務調度的,退出臨界段的時候可能需要進行任務調度,所以一般調用的是OS_CRITICAL_EXIT()。但是有些函數如提交函數在調用的時候可以選擇是否要進行調度,如果這時退出臨界段就先不要調度。因此,要使用OS_CRITICAL_EXIT_NO_SCHED(),在函數的最后應根據選項情況是否進行調度。

1.7.4 沒有使能中斷延遲的鎖住和開啟調度器

沒有使用中斷延遲的時候,進入和退出臨界段的操作如代碼清單1-6所示。對比使用中斷延遲,以下代碼中的4種鎖住和開啟調度的方式非?!昂唵伪┝Α保苯诱{用開關中斷的宏。這樣做的壞處是導致關中斷的時間變長。

代碼清單1-6 沒有使用中斷延遲進入和退出臨界段的代碼


      1 #define  OS_CRITICAL_ENTER()                     CPU_CRITICAL_ENTER()
      2
      3 #define  OS_CRITICAL_ENTER_CPU_CRITICAL_EXIT()
      4
      5 #define  OS_CRITICAL_EXIT()                      CPU_CRITICAL_EXIT()
      6
      7 #define  OS_CRITICAL_EXIT_NO_SCHED()             CPU_CRITICAL_EXIT()

OS_CRITICAL_ENTER_CPU_CRITICAL_EXIT()之所以是一個空宏,是因為在調用這個函數之前肯定已經關中斷,這時本想切換到鎖住調度器來進入臨界段,可是沒有使能中斷延遲進入臨界段,所以只能關中斷。

注意:開關中斷、開啟和鎖住調度器名稱上有OS和CPU的區別。OS指的是μC/OS-III內核代碼,是屬于軟件層面的,所以OS_CRITICAL_ENTER 就是軟件層面的進入臨界段,即鎖住調度器。CPU是你使用的平臺,這里使用的是基于Cortex-M3內核的STM32單片機,指向的是硬件層面,CPU_CRITICAL_ENTER就是硬件層面的進入臨界段,即關中斷。

1.7.5 中斷嵌套檢測

中斷嵌套檢測程序如代碼清單1-7所示。

代碼清單1-7 中斷嵌套檢測代碼


          1 #if OS_CFG_CALLED_FROM_ISR_CHK_EN > 0u
          2 if (OSIntNestingCtr > (OS_NESTING_CTR)0)
          3 {
          4     *p_err = OS_ERR_XXX;
          5     return;
          6 }
          7 #endif

以上代碼主要用于檢測程序是否在中斷中被調用,如等待函數、延時函數等阻塞線程的函數,甚至創建內核對象、刪除內核對象等都有這段程序,因為這些函數有較長的臨界段。

OSIntNestingCtr這個變量是用來檢測中斷嵌套層數的。

第2行先判斷中斷嵌套的次數,每進入一次中斷,OSIntNestingCtr就會加1。所以,如果大于0,就是至少進入了一層中斷。若沒有進入任何中斷,則OSIntNestingCtr為0。如果是在中斷中調用,且直接返回,那么不再執行下面的程序段。

如果不確定函數是否在中斷中被調用,就查看函數的內容有沒有這段代碼即可,一般阻塞的函數肯定是不可以在中斷中被調用的。

1.7.6 調度器嵌套檢測

調度器嵌套檢測程序如代碼清單1-8所示。

代碼清單1-8 調度器嵌套檢測代碼


    1 if (OSSchedLockNestingCtr > (OS_NESTING_CTR)0u)
    2 {
    3     *p_err = OS_ERR_SCHED_LOCKED;
    4     return;
    5 }

同中斷嵌套,調度器每被鎖住一次,調度器嵌套層數的變量OSSchedLockNestingCtr就會加1。一些需要任務調度的函數,比如延時的時候需要調用系統提供的函數先把當前的任務置于延時狀態,然后調度就緒列表中優先級最高的任務。如代碼清單1-8所示,在延時函數的前面會先檢測調度器是否已經被鎖住,即判斷OSSchedLockNestingCtr是否大于0,如果被鎖住,就退出延時函數,延時失敗。

注意:調度器若嵌套多層,也需要解開多層后才可以執行調度。

1.7.7 時間戳

為了測量任務運行時間、任務等待內核對象時間等,μC/OS-III保存了很多時間戳。時間戳可以理解為是記錄時間點的一個量,如代碼清單1-9所示。在程序段XXX運行的前后分別保存時間戳,并相減得出XXX程序段運行的時間,最后更新該程序段運行的最大時間。定時器任務、節拍任務、統計任務、中斷延遲提交任務都有這段程序,這些任務都是系統任務。

代碼清單1-9 計算程序運行時間代碼段


        ts_start = OS_TS_GET();
        XXX;
        ts_end = OS_TS_GET() - ts_start;
        if (ts_end > OSTickTaskTimeMax)
        {
            OSTickTaskTimeMax = ts_end;
        }

1.7.8 錯誤類型

如果程序運行過程中出現錯誤,則程序會返回這些錯誤,以便用戶得知出錯。為了程序的健壯性,調用系統函數之后最好有相應的錯誤處理。有些錯誤類型可以很容易地從其名字中看出來,而且在后面的程序中出現的概率非常高,因此先放在這里介紹。

1)OS_ERR_NONE:正常情況,沒有錯誤。

2)OS_ERR_OBJ_TYPE:內核對象類型錯誤,根據判斷內核對象變量的元素Type即可知道內核對象類型是否錯誤,比如在一個定時器刪除函數中,可以根據元素Type來判斷輸入的內核對象變量是否是定時器類型。

3)OS_ERR_OPT_INVALID:選項設置有誤。

4)OS_ERR_SCHED_LOCKED:調度器被鎖住。

5)OS_ERR_TIME_DLY_ISR:在中斷中調用延時函數。

1.7.9 參數檢測

參數檢測程序如代碼清單1-10所示。

代碼清單1-10 參數檢測代碼段


    1 #if OS_CFG_ARG_CHK_EN > 0u
    2 if (p_tmr == (OS_TMR *)0)
    3 {
    4     *p_err = OS_ERR_TMR_INVALID;
    5     return (OS_TMR_STATE_UNUSED);
    6 }
    7 #endif

代碼清單1-10中OS_CFG_ARG_CHK_EN是參數檢測的宏,若置1,則會在很多內核函數的前面進行參數檢測。主要用于檢測傳遞的指針是否是空指針、參數范圍是否符合要求等。

1.7.10 內核對象類型檢測

內核對象類型檢測程度如代碼清單1-11所示。

代碼清單1-11 內核對象類型檢測代碼


    1 #if OS_CFG_OBJ_TYPE_CHK_EN > 0u
    2 if (p_tmr->Type != OS_OBJ_TYPE_XXX)
    3 {
    4     *p_err = OS_ERR_OBJ_TYPE;
    5     return (DEF_FALSE);
    6 }
    7 #endif

比如,調用一個定時器刪除函數,則傳入的內核對象參數類型必須是定時器。這里用于檢測傳入的內核對象類型是否是定時器。

1.7.11 安全檢測

安全檢測程序如代碼清單1-12所示。

代碼清單1-12 安全監測代碼段


    1 //是否定義安全檢測的宏
    2 #ifdef OS_SAFETY_CRITICAL
    3 if (p_err == (OS_ERR *)0)
    4 {
    5     //如果傳入的參數p_err是空指針,那么將進入安全關鍵異常,這部分代碼需要用戶自己編寫
    6     OS_SAFETY_CRITICAL_EXCEPTION();
    7     return;
    8 }
    9 #endif

如果定義了安全檢測的宏,那么作為參數輸入的存放返回錯誤類型的指針p_err就不能為空指針,否則在調用OS_SAFETY_CRITICAL_EXCEPTION后返回,而且OS_SAFETY_CRITICAL_EXCEPTION需要自己編寫。

1.7.12 安全關鍵IEC61508

安全關鍵IEC61508程序如代碼清單1-13所示。

代碼清單1-13 安全關鍵IEC61508相關代碼


    1 //是否啟動安全關鍵
    2 #ifdef OS_SAFETY_CRITICAL_IEC61508
    4     if (OSSafetyCriticalStartFlag == DEF_TRUE) {
    5          *p_err = OS_ERR_ILLEGAL_CREATE_RUN_TIME;
    6          return;
    7     }
    8 #endif

在定義了安全關鍵的宏OS_SAFETY_CRITICAL_IEC61508后,并且OSSafetyCriticalStartFlag被置為DEF_TRUE時,不再允許調用相關創建函數。

1.8 總結

本章首先介紹了單片機程序框架,從最簡單的前后臺系統到時間片輪詢法、再到嵌入式實時操作系統。它們分別對應不同層次的應用,有著各自的局限和優勢。

接著介紹了μC/OS-III整體的文件框架,從CPU相關移植代碼到μC/OS-III的主體代碼(跟CPU無關),再到用戶的應用層代碼。層次感非常強,方便移植μC/OS-III到不同的CPU上去。

最后介紹了μC/OS-III任務、內核對象以及常見代碼段,這些是為后面的內容做鋪墊,了解即可。

主站蜘蛛池模板: 晴隆县| 大港区| 西昌市| 武川县| 易门县| 定南县| 新河县| 阳江市| 察隅县| 左云县| 中卫市| 六枝特区| 平和县| 晴隆县| 翼城县| 黎城县| 惠州市| 兴山县| 邹平县| 花垣县| 南丹县| 梧州市| 平舆县| 湖口县| 万州区| 吴江市| 故城县| 镇沅| 绩溪县| 莒南县| 张家口市| 博爱县| 和平县| 阿瓦提县| 延边| 焦作市| 小金县| 醴陵市| 汉沽区| 梨树县| 望谟县|