剛參加工做那幾年作MCU程序,因爲實現的功能和需求都比較簡單,外圍模塊也不多,因此大多數的項目直接就在裸機上寫代碼。程序員
當時也沒有任務和線程的概念,腦子裏想的只有單個函數的調度,變量的控制等等。工做時先把流程圖畫出來,而後按照必定的邏輯把全部的函數都調用起來,最後實現本身的需求。面試
隨着業務的深刻,後來發如今某些比較複雜,或者說是外圍功能比較多的項目上,若是依然用裸機的單線程來寫代碼,雖然最終也能實現需求,可是對於軟件的架構上就會複雜許多,按照軟件定律來講,軟件的架構越複雜,bug的個數必然也會大大的增長,因此我慢慢開始接觸嵌入式的操做系統。算法
最開始接觸的即是在國內很流行的UCOSII,開頭對於操做系統也只是使用,不求甚解,只要求工程可以跑起來就行,等後來有時間之後,本身深刻了研究了一下,隨着學習,不少之前困擾本身的問題也迎刃而解,如今把本身的經驗分享出來,但願能幫到一些剛剛踏上這條不歸路的同志,固然,因爲本人能力有限,水平通常,若是文中出現了瑕疵和紕漏,還望不吝賜教。數組
--------------------------------------------------正文----------------------------------------------------------------------架構
所謂操做系統,即是隔絕硬件層與應用層的平臺,讓工程師能夠最大限度的忽視硬件,直接進行邏輯開發,它最大的特色,即是可讓多任務併發執行,但並不是是同時執行,形象點來講,假如我有4個任務(LED點燈,喇叭鳴叫,串口通訊,數據計算),讓每一個任務都執行幾十個毫秒,雖然實際上在任何一個時間點,都有且只有一個任務的一條代碼在執行,可是從宏觀上看來,這4個任務幾乎是同時執行的,這4個任務的調度,就是切換是由操做系統根據自身的策略來完成(思考題:UCOSII的調度策略是什麼?),程序員所關注的,只是任務中實際的處理部分,不須要在乎框架,這樣即可以大大減小開發的難度和工做量。併發
UCOSII是一款適用於低性能MCU的嵌入式實時操做系統,低性能也就是日常所使用的單片機,本文便基於經常使用的STM32F103來進行講解。app
----------------------------------------------------------------------------------------------------------------------------框架
記得有一次找工做,面試官問我了一個問題:「你既然用過UCOSII實時操做系統,那麼請說一下,這款操做系統是如何保證它的實時性的?」函數
當時我剛接觸操做系統不久,只是知其然而不知其因此然,若是僅僅是移植一下,創建幾個任務,讓keil工程正常的跑起來還能作到,至於它原理性的東西那就有些懵逼了。性能
此後,基於這個問題,我抽出了很多時間去學習,如今回想起來,若是這個問題今天再問我,那我應該能夠講出個八九不離十。
--------------------那麼UCOSII究竟是如何保證它的實時性的呢?
基於這個問題的解答,我用老百姓都能聽懂的語言,大膽的講解一下嵌入式實時操做系統UCOSII的運行原理,但願語言通俗到只要學過C語言的同窗就能理解的程度。
對於一個剛接觸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種狀況下,任務調度的前因後果。
咱們從代碼運行的流程梳理一下,忽略操做系統自己,代碼從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位又有什麼關係?