手把手,嘴對嘴,講解UCOSII嵌入式操做系統的任務調度策略(一)

 

剛參加工做那幾年作MCU程序,因爲實現的功能和需求都比較簡單,外圍模塊也不多,因此大多數的項目直接就在裸機上寫代碼。程序員

當時也沒有任務和線程的概念,腦子裏想的只有單個函數的調度,變量的控制等等。工做時先把流程圖畫出來,而後按照必定的邏輯把全部的函數都調用起來,最後實現本身的需求。面試

隨着業務的深刻,後來發如今某些比較複雜,或者說是外圍功能比較多的項目上,若是依然用裸機的單線程來寫代碼,雖然最終也能實現需求,可是對於軟件的架構上就會複雜許多,按照軟件定律來講,軟件的架構越複雜,bug的個數必然也會大大的增長,因此我慢慢開始接觸嵌入式的操做系統。算法

最開始接觸的即是在國內很流行的UCOSII,開頭對於操做系統也只是使用,不求甚解,只要求工程可以跑起來就行,等後來有時間之後,本身深刻了研究了一下,隨着學習,不少之前困擾本身的問題也迎刃而解,如今把本身的經驗分享出來,但願能幫到一些剛剛踏上這條不歸路的同志,固然,因爲本人能力有限,水平通常,若是文中出現了瑕疵和紕漏,還望不吝賜教。數組

--------------------------------------------------正文----------------------------------------------------------------------架構

所謂操做系統,即是隔絕硬件層與應用層的平臺,讓工程師能夠最大限度的忽視硬件,直接進行邏輯開發,它最大的特色,即是可讓多任務併發執行,但並不是是同時執行,形象點來講,假如我有4個任務(LED點燈,喇叭鳴叫,串口通訊,數據計算),讓每一個任務都執行幾十個毫秒,雖然實際上在任何一個時間點,都有且只有一個任務的一條代碼在執行,可是從宏觀上看來,這4個任務幾乎是同時執行的,這4個任務的調度,就是切換是由操做系統根據自身的策略來完成(思考題:UCOSII的調度策略是什麼?),程序員所關注的,只是任務中實際的處理部分,不須要在乎框架,這樣即可以大大減小開發的難度和工做量。併發

UCOSII是一款適用於低性能MCU的嵌入式實時操做系統,低性能也就是日常所使用的單片機,本文便基於經常使用的STM32F103來進行講解。app

----------------------------------------------------------------------------------------------------------------------------框架

記得有一次找工做,面試官問我了一個問題:「你既然用過UCOSII實時操做系統,那麼請說一下,這款操做系統是如何保證它的實時性的?」函數

當時我剛接觸操做系統不久,只是知其然而不知其因此然,若是僅僅是移植一下,創建幾個任務,讓keil工程正常的跑起來還能作到,至於它原理性的東西那就有些懵逼了。性能

此後,基於這個問題,我抽出了很多時間去學習,如今回想起來,若是這個問題今天再問我,那我應該能夠講出個八九不離十。

--------------------那麼UCOSII究竟是如何保證它的實時性的呢?

基於這個問題的解答,我用老百姓都能聽懂的語言,大膽的講解一下嵌入式實時操做系統UCOSII的運行原理,但願語言通俗到只要學過C語言的同窗就能理解的程度。

 

UCOSII系統最簡單的用法

對於一個剛接觸ucosii的同窗而言,用法其實比較簡單,若是工程是完備的,那麼創建一個能跑起來的工程的步驟以下:

1.定義任務名,任務優先級,任務堆棧及大小。

2.從main()中作操做系統的初始化(函數:OSInit()),建立起始任務,而且啓動操做系統(函數:OSStart())。

3.在啓動任務中,進行MCU硬件的初始化,中斷的配置,而後根據本身的需求,建立任意多個任務(64個如下,有些優先級是系統保留,好比統計和空閒,咱們能夠用的大概有50幾個)。

這個起始任務只執行一遍,由於它的做用僅僅是啓動別的任務,執行完畢之後將它掛起。

代碼以下:

 

  1 /* Includes ------------------------------------------------------------------*/
  2 #include "app.h"
  3 #include "includes.h"
  4 #include "delay.h"
  5 /////////////////////////UCOSII任務設置/////////////////////////////////////////
  6 //START 任務
  7 //設置任務優先級
  8 #define START_TASK_PRIO                 (8) //開始任務的優先級設置爲最低
  9 //設置任務堆棧大小
 10 #define START_STK_SIZE                  (256)
 11 //任務堆棧
 12 OS_STK START_TASK_STK[START_STK_SIZE];
 13 //任務函數
 14 void start_task(void *pdata);
 15 
 16 //APP0任務
 17 //設置任務優先級
 18 #define APP0_TASK_PRIO                  (0)
 19 //設置任務堆棧大小
 20 #define APP0_STK_SIZE                   (256)
 21 //任務堆棧
 22 OS_STK APP0_TASK_STK[APP0_STK_SIZE];
 23 //任務函數
 24 void App0_task(void *pdata);
 25 
 26 
 27 //APP1任務
 28 //設置任務優先級
 29 #define APP1_TASK_PRIO                  (1)
 30 //設置任務堆棧大小
 31 #define APP1_STK_SIZE                   (256)
 32 //任務堆棧
 33 OS_STK APP1_TASK_STK[APP1_STK_SIZE];
 34 //任務函數
 35 void App1_task(void *pdata);
 36 
 37 
 38 //APP2任務
 39 //設置任務優先級
 40 #define APP2_TASK_PRIO                  (2)
 41 //設置任務堆棧大小
 42 #define APP2_STK_SIZE                   (256)
 43 //任務堆棧
 44 OS_STK APP2_TASK_STK[APP2_STK_SIZE];
 45 //任務函數
 46 void App2_task(void *pdata);
 47 
 48 
 49 //APP3任務
 50 //設置任務優先級
 51 #define APP3_TASK_PRIO                  (3)
 52 //設置任務堆棧大小
 53 #define APP3_STK_SIZE                   (64)
 54 //任務堆棧
 55 OS_STK APP3_TASK_STK[APP3_STK_SIZE];
 56 //任務函數
 57 void App3_task(void *pdata);
 58 
 59 
 60 /*******************************************************************************
 61 *  函 數 名: main
 62 *  功    能: 系統初始化 + 啓動起始線程
 63 *  輸    入: 無
 64 *  輸    出: 無
 65 *  返 回 值: 無
 66 *  備    注: 無
 67 *******************************************************************************/
 68 int main(void)
 69 {
 70 
 71     OSInit();//操做系統初始化
 72     /* 建立起始任務 */
 73     OSTaskCreate(start_task,(void *)0,(OS_STK *)&START_TASK_STK[START_STK_SIZE-1],START_TASK_PRIO );
 74     OSStart();//操做系統啓動 開始任務調度
 75 }
 76 
 77 /*******************************************************************************
 78 *  函 數 名: start_task
 79 *  功    能: 起始線程
 80 *  輸    入: 無
 81 *  輸    出: 無
 82 *  返 回 值: 無
 83 *  備    注: 建立任務線程
 84 *******************************************************************************/
 85 void start_task(void *pdata)
 86 {
 87     OS_CPU_SR cpu_sr=0;
 88 
 89     pdata = pdata;
 90 
 91     /* 設置中斷優先級分組爲組2:2位搶佔優先級,2位響應優先級 */
 92     NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
 93     /* 延時函數初始化 */
 94     delay_init();
 95     /* 啓動統計任務,便於統計CPU的利用率以及負荷 */
 96     OSStatInit();
 97     /* 系統初始化 其中分爲硬件初始化和變量初始化 */
 98     System_Init();
 99     /* 進入臨界區(沒法被中斷打斷) */
100     OS_ENTER_CRITICAL();
101     /* 建立線程 */
102 
103     OSTaskCreate(App0_task,(void *)0,(OS_STK*)&APP0_TASK_STK[APP0_STK_SIZE-1],APP0_TASK_PRIO);
104     OSTaskCreate(App1_task,(void *)0,(OS_STK*)&APP1_TASK_STK[APP1_STK_SIZE-1],APP1_TASK_PRIO);
105     OSTaskCreate(App2_task,(void *)0,(OS_STK*)&APP2_TASK_STK[APP2_STK_SIZE-2],APP2_TASK_PRIO);
106     OSTaskCreate(App3_task,(void *)0,(OS_STK*)&APP3_TASK_STK[APP3_STK_SIZE-2],APP3_TASK_PRIO);
107     /* 刪除起始任務 */
108     OSTaskDel(OS_PRIO_SELF);
109     /* 退出臨界區(能夠被中斷打斷) */
110     OS_EXIT_CRITICAL();
111 }

當以上的初始化部分執行完後,代碼就能本身的跳進本身寫的任務中,而後開始根據優先級實現調度。

 1 /*******************************************************************************
 2 * 函數名  : App0_task0
 3 * 描述    : 任務
 4 * 輸入    : 無
 5 * 返回    : 無
 6 * 說明    : 無
 7 *******************************************************************************/
 8 void App0_task(void *pdata)
 9 {
10 
11     while(1)
12     {
13 #if SYSTEM_IWDG_ENABLE==1
14         /* 清除看門狗 */
15         IWDG_ReloadCounter();
16 #endif
17 
18         delay_ms(100);
19     };
20 }
21 
22 /*******************************************************************************
23 * 函數名  : App1_task
24 * 描述    : 任務
25 * 輸入    : 無
26 * 返回    : 無
27 * 說明    : 無
28 *******************************************************************************/
29 void App1_task(void *pdata)
30 {
31     while(1)
32     {
33 #if SYSTEM_IWDG_ENABLE==1
34         /* 清除看門狗 */
35         IWDG_ReloadCounter();
36 #endif
38         delay_ms(100);
39     };
40 }
41 
42 
43 /*******************************************************************************
44 * 函數名  : App1_task
45 * 描述    : 任務
46 * 輸入    : 無
47 * 返回    : 無
48 * 說明    : 無
49 *******************************************************************************/
50 void App2_task(void *pdata)
51 {54     while(1)
55     {
57 #if SYSTEM_IWDG_ENABLE==1
58         /* 清除看門狗 */
59         IWDG_ReloadCounter();
60 #endif
62         delay_ms(100);
63     };
64 }
65 
66 
67 /*******************************************************************************
68 * 函數名  : App3_task
69 * 描述    : 任務
70 * 輸入    : 無
71 * 返回    : 無
72 * 說明    : 無
73 *******************************************************************************/
74 void App3_task(void *pdata)
75 {
76     while(1)
77     {
78 #if SYSTEM_IWDG_ENABLE==1
79         /* 清除看門狗 */
80         IWDG_ReloadCounter();
81 #endif
83         delay_ms(100);
84     };
85 }

我新建了4個任務,他們會按照優先級(0,1,2,3)從APP0→APP3的順序開始調用,如今它們都是空的,若是須要加入功能,只須要在while(1)裏面加入本身的代碼即可。

如今回到剛纔的問題,

  「你用過UCOSII實時操做系統,那麼請說一下,這款操做系統是如何保證明時性的?」

   用老百姓都聽懂的語言翻譯一下就是:爲啥子程序會從APP0開始執行?爲啥子APP0的優先級就比APP3的優先級高?你們都是一張鍵盤打出來的代碼,它就憑什麼那麼牛逼?

  咱們所給任務定義的優先級,也就是那幾個數字(0,1,2,3),究竟是怎麼影響任務調度順序的呢?

------------------------------------------------------------------------------------------------------------------------------------------------------------

      UCOSII任務調度的時機,也就是切換任務的時間點,我知道的大概有如下幾處:

  1.當前任務進入了延時。

  2.當前任務被掛起或者殺死。

  3.當前任務執行時,發生了某些中斷。

  如今分別講解一下在以上3種狀況下,任務調度的前因後果。

  1.當前任務進入了延時

  咱們從代碼運行的流程梳理一下,忽略操做系統自己,代碼從APP0開始執行,當執行完它須要執行的任務後,會進入一個延時函數delay_ms()。

      如今看一下這個函數體:

  

 1 //延時nms
 2 //nms:要延時的ms數
 3 void delay_ms(u16 nms)
 4 {
 5     if(delay_osrunning && delay_osintnesting==0)//若是OS已經在跑了,而且不是在中斷裏面(中斷裏面不能任務調度)
 6     {
 7         if(nms>=fac_ms)                         //延時的時間大於OS的最少時間週期
 8         {
 9             OSTimeDly(nms/fac_ms);              //OS延時
10         }
11         nms%=fac_ms;                            //OS已經沒法提供這麼小的延時了,採用普通方式延時
12     }
13     delay_us((u32)(nms*1000));                  //普通方式延時
14 }

  這個函數是本身寫的,其餘的不重要,重點看第9行的OSTimeDly()函數,這個函數但是系統自帶的,從如今開始進入系統,

  

void  OSTimeDly (INT32U ticks)
{
    INT8U      y;
#if OS_CRITICAL_METHOD == 3u
    OS_CPU_SR  cpu_sr = 0u;
#endif

    if (OSIntNesting > 0u) {                     /* 查看延時函數是否在中斷中調用,若是在中斷中調用,不能切換任務 */
        return;
    }
    if (OSLockNesting > 0u) {                    /* 查看當前任務調度是否被系統鎖住,當系統被鎖住,不能切換任務 */
        return;
    }
    if (ticks > 0u) {                            /* 延時參數是否爲0 */
        OS_ENTER_CRITICAL();            /* 禁止中斷 */
        y            =  OSTCBCur->OSTCBY;
        OSRdyTbl[y] &= (OS_PRIO)~OSTCBCur->OSTCBBitX;
        if (OSRdyTbl[y] == 0u) {
            OSRdyGrp &= (OS_PRIO)~OSTCBCur->OSTCBBitY;
        }
        OSTCBCur->OSTCBDly = ticks;
        OS_EXIT_CRITICAL();              /* 開啓中斷 */
        OS_Sched();
    }
}

  當代碼進入這個函數之後,首先進行兩個斷定,1.是否在中斷中,2.任務調度是否屬於容許狀態,若是兩個都不知足,才執行下面的代碼。

      OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()這兩個宏分別是禁止中斷和重啓中斷,通常是成對出現,用來保證一些重要的代碼在執行期間,不會被打斷。

  前面的不重要,重點是if (ticks > 0u)裏面的東西,他裏面到底實現了些什麼?

  

    if (ticks > 0u) {                            /* 延時參數是否爲0 */
        OS_ENTER_CRITICAL();            /* 禁止中斷 */
        y =  OSTCBCur->OSTCBY;
        OSRdyTbl[y] &= (OS_PRIO)~OSTCBCur->OSTCBBitX;
        if (OSRdyTbl[y] == 0u) {
            OSRdyGrp &= (OS_PRIO)~OSTCBCur->OSTCBBitY;
        }
        OSTCBCur->OSTCBDly = ticks;
        OS_EXIT_CRITICAL();              /* 開啓中斷 */
        OS_Sched();
    }

 

        y  =  OSTCBCur->OSTCBY;這一句話表示什麼意思?固然是把一個變量的值賦給另外一個變量……廢話!

  那麼,OSTCBCur->OSTCBY這個變量究竟是表明着什麼?

  這個變量明顯是屬於一個指向結構體的指針,咱們能夠跟蹤它去看看它的定義。

       

OS_EXT  OS_TCB           *OSTCBCur;                        /* Pointer to currently running TCB         */

  從註釋即可知道,這個結構體指針,指向當前正在運行的任務,繼續跟蹤……

typedef struct os_tcb {
    OS_STK          *OSTCBStkPtr;           /* Pointer to current top of stack                         */

    struct os_tcb   *OSTCBNext;             /* Pointer to next     TCB in the TCB list                 */
    struct os_tcb   *OSTCBPrev;             /* Pointer to previous TCB in the TCB list                 */

    INT32U           OSTCBDly;              /* Nbr ticks to delay task or, timeout waiting for event   */
    INT8U            OSTCBStat;             /* Task      status                                        */
    INT8U            OSTCBStatPend;         /* Task PEND status                                        */
    INT8U            OSTCBPrio;             /* Task priority (0 == highest)                            */

    INT8U            OSTCBX;                /* Bit position in group  corresponding to task priority   */
    INT8U            OSTCBY;                /* Index into ready table corresponding to task priority   */
    OS_PRIO          OSTCBBitX;             /* Bit mask to access bit position in ready table          */
    OS_PRIO          OSTCBBitY;             /* Bit mask to access bit position in ready group          */

} OS_TCB;

這個結構體有不少成員,是用來記錄任務的基本信息的,這樣的結構體有不少,能夠簡單的理解爲有多少個任務就有多少個這樣的結構體,它的本來的定義很大,我剔除了一些干擾信息,結果如上,這明顯是一個雙向鏈表,咱們須要的OSTCBCur->OSTCBY究竟是什麼意思呢?從註釋上看,應該是與任務優先級對應的就緒索引表有關,聽不懂不要緊,記在腦子裏,下面解釋。

繼續跟蹤變量,發現它是在當前任務建立的時候賦值的:

也就是起始任務中的start_task()→OSTaskCreate()→OS_TCBInit()的裏面

        ptcb->OSTCBY             = (INT8U)(prio >> 3u);

從字面意思上看,它的值應該是優先級的高3位,若是咱們的優先級是12,那麼二進制是00001100,高3位就是001,不過如今咱們的APP0的優先級是0,那麼高3位也就是000。

在這行代碼的旁邊,同時還能夠看到另外一行代碼:

        ptcb->OSTCBX             = (INT8U)(prio & 0x07u);

這個變量裏面保存的優先級的低三位,若是咱們的優先級是12,那麼二進制是00001100,低3位也就是0x100,不過如今咱們的APP0的優先級是0,那麼低3位也就是000。

把優先級的高3位和低3位分開,這麼作的目的是什麼?保存這兩個玩意兒有啥用?

詳細的東西容我之後慢慢講,如今回到剛纔的函數,終於明白了那個變量表示什麼意思了,就是當前任務優先級的高3位,意義爲什麼,雖然如今還不清楚,默默的記住有這麼一個東西就行。

       

    if (ticks > 0u) {                            /* 延時參數是否爲0 */
        OS_ENTER_CRITICAL();            /* 禁止中斷 */
        y            =  OSTCBCur->OSTCBY;
        OSRdyTbl[y] &= (OS_PRIO)~OSTCBCur->OSTCBBitX;
        if (OSRdyTbl[y] == 0u) {
            OSRdyGrp &= (OS_PRIO)~OSTCBCur->OSTCBBitY;
        }
        OSTCBCur->OSTCBDly = ticks;
        OS_EXIT_CRITICAL();              /* 開啓中斷 */
        OS_Sched();
    }

如今看第二句代碼:OSRdyTbl[y] &= (OS_PRIO)~OSTCBCur->OSTCBBitX根據上面那個結構體,咱們能夠找到這個成員的定義以及賦值。

它也是在那個OS_TCBInit函數裏賦值的

        ptcb->OSTCBBitY          = (OS_PRIO)(1uL << ptcb->OSTCBY);
        ptcb->OSTCBBitX          = (OS_PRIO)(1uL << ptcb->OSTCBX);

從算法上看,ptcb->OSTCBBitX是1向左移動本任務優先級的低3位。

舉個例子:假如咱們當前的優先級是12,那麼二進制是00001100,那麼ptcb->OSTCBX變量應該是二進制100,那麼ptcb->OSTCBBitX應該是1向左移動4個位置,結果是0x10。

同理,ptcb->OSTCBBitY即是1向左移動優先級的高3位。

舉個例子:加入咱們當前的優先級是12,那麼二進制是00001100,那麼ptcb->OSTCBtY變量應該是001,那麼ptcb->OSTCBBittY應該是1向左移動1個位置,結果是0x02。

ptcb->OSTCBY,ptcb->OSTCBX,ptcb->OSTCBBitY,ptcb->OSTCBBitX……這四個變量和優先級有關的變量都是在任務建立的時候就賦值好了,之後也不會改變,至於它們的用法,即是須要重點講解的地方。

如今結合那兩句代碼一塊兒看:

        y            =  OSTCBCur->OSTCBY;
        OSRdyTbl[y] &= (OS_PRIO)~OSTCBCur->OSTCBBitX;

仍是舉個例子:假如我當前任務的優先級是12,那麼Y = 001,OSTCBCur->OSTCBBitX = 0x10,結合起來看,第二句有&符號,也有~符號,只要是稍微有點C語言基礎的同窗,那麼都很容易看出這兩句話的意思,把數組OSRdyTbl[1]的第4位清空……

那麼……!@#*()&¥*()&¥……氣的我想罵人,清空?究竟是什麼意思?

優先級12的任務和數組OSRdyTbl[1]有什麼關係,和OSRdyTbl[1]的第4位又有什麼關係?

待續……

相關文章
相關標籤/搜索