新聞中心

        EEPW首頁 > 嵌入式系統 > 牛人業話 > 51單片機多任務操作系統的原理與實現

        51單片機多任務操作系統的原理與實現

        作者: 時間:2017-01-06 來源:網絡 收藏

          //任務切換函數(任務調度器)

        本文引用地址:http://www.104case.com/article/201701/342566.htm

          void task_switch()

          {

          task_sp[task_id] = SP;

          if(++task_id == MAX_TASKS)

          task_id = 0;

          SP = task_sp[task_id];

          }

          //任務裝入函數.將指定的函數(參數1)裝入指定(參數2)的任務槽中.如果該槽中原來就有任務,則原任務丟失,但系統本身不會發生錯誤.

          void task_load(unsigned int fn, unsigned char tid)

          {

          task_sp[tid] = task_stack[tid] + 1;

          task_stack[tid][0] = (unsigned int)fn & 0xff;

          task_stack[tid][1] = (unsigned int)fn >> 8;

          }

          //從指定的任務開始運行任務調度.調用該宏后,將永不返回.

          #define os_start(tid) {task_id = tid,SP = task_sp[tid];return;}

          /*==================以下為測試代碼=====================*/

          void task1()

          {

          static unsigned char i;

          while(1){

          i++;

          task_switch();//編譯后在這里打上斷點

          }

          }

          void task2()

          {

          static unsigned char j;

          while(1){

          j+=2;

          task_switch();//編譯后在這里打上斷點

          }

          }

          void main()

          {

          //這里裝載了兩個任務,因此在定義MAX_TASKS時也必須定義為2

          task_load(task1, 0);//將task1函數裝入0號槽

          task_load(task2, 1);//將task2函數裝入1號槽

          os_start(0);

          }

          限于篇幅我已經將代碼作了簡化,并刪掉了大部分注釋,大家可以直接下載源碼包,里面完整的注解,并帶KEIL工程文件,斷點也打好了,直接按ctrl+f5就行了.

          現在來看看這個多任務系統的原理:

          這個多任務系統準確來說,叫作"協同式多任務".

          所謂"協同式",指的是當一個任務持續運行而不釋放資源時,其它任務是沒有任何機會和方式獲得運行機會,除非該任務主動釋放CPU.

          在本例里,釋放CPU是靠task_switch()來完成的.task_switch()函數是一個很特殊的函數,我們可以稱它為"任務切換器".

          要清楚任務是如何切換的,首先要回顧一下堆棧的相關知識.

          有個很簡單的問題,因為它太簡單了,所以相信大家都沒留意過:

          我們知道,不論是CALL還是JMP,都是將當前的程序流打斷,請問CALL和JMP的區別是什么?

          你會說:CALL可以RET,JMP不行.沒錯,但原因是啥呢?為啥CALL過去的就可以用RET跳回來,JMP過去的就不能用RET來跳回呢?

          很顯然,CALL通過某種方法保存了打斷前的某些信息,而在返回斷點前執行的RET指令,就是用于取回這些信息.

          不用多說,大家都知道,"某些信息"就是PC指針,而"某種方法"就是壓棧.

          很幸運,在里,堆棧及堆棧指針都是可被任意修改的,只要你不怕死.那么假如在執行RET前將堆棧修改一下會如何?往下看:

          當程序執行CALL后,在子程序里將堆棧剛才壓入的斷點地址清除掉,并將一個函數的地址壓入,那么執行完RET后,程序就跳到這個函數去了.

          事實上,只要我們在RET前將堆棧改掉,就能將程序跳到任務地方去,而不限于CALL里壓入的地址.

          重點來了......

          首先我們得為每個任務單獨開一塊內存,這塊內存專用于作為對應的任務的堆棧,想將CPU交給哪個任務,只需將棧指針指向誰內存塊就行了.

          接下來我們構造一個這樣的函數:

          當任務調用該函數時,將當前的堆棧指針保存一個變量里,并換上另一個任務的堆棧指針.這就是任務調度器了.

          OK了,現在我們只要正確的填充好這幾個堆棧的原始內容,再調用這個函數,這個任務調度就能運行起來了.

          那么這幾個堆棧里的原始內容是哪里來的呢?這就是"任務裝載"函數要干的事了.

          在啟動任務調度前將各個任務函數的入口地址放在上面所說的"任務專用的內存塊"里就行了!對了,順便說一下,這個"任務專用的內存塊"叫作"私棧",私棧的意思就是說,每個任務的堆棧都是私有的,每個任務都有一個自已的堆棧.

          話都說到這份上了,相信大家也明白要怎么做了:

          1.分配若干個內存塊,每個內存塊為若干字節:

          這里所說的"若干個內存塊"就是私棧,要想同時運行幾少個任務就得分配多少塊.而"每個子內存塊若干字節"就是棧深.記住,每調一層子程序需要2字節.如果不考慮中斷,4層調用深度,也就是8字節棧深應該差不多了.

          unsigned char idata task_stack[MAX_TASKS][MAX_TASK_DEP]

          當然,還有件事不能忘,就是堆指針的保存處.不然光有堆棧怎么知道應該從哪個地址取數據啊

          unsigned char idata task_sp[MAX_TASKS]

          上面兩項用于裝任務信息的區域,我們給它個概念叫"任務槽".有些人叫它"任務堆",我覺得還是"槽"比較直觀

          對了,還有任務號.不然怎么知道當前運行的是哪個任務呢?

          unsigned char task_id

          當前運行存放在1號槽的任務時,這個值就是1,運行2號槽的任務時,這個值就是2....

          2.構造任務調度函函數:

          void task_switch()

          {

          task_sp[task_id] = SP; //保存當前任務的棧指針

          if(++task_id == MAX_TASKS) //任務號切換到下一個任務

          task_id = 0;

          SP = task_sp[task_id]; //將系統的棧指針指向下個任務的私棧.

          }

          3.裝載任務:

          將各任務的函數地址的低字節和高字節分別入在

          task_stack[任務號][0]和task_stack[任務號][1]中:

          為了便于使用,寫一個函數: task_load(函數名, 任務號)

          void task_load(unsigned int fn, unsigned char tid)

          {

          task_sp[tid] = task_stack[tid] + 1;

          task_stack[tid][0] = (unsigned int)fn & 0xff;

          task_stack[tid][1] = (unsigned int)fn >> 8;

          }

          4.啟動任務調度器:

          將棧指針指向任意一個任務的私棧,執行RET指令.注意,這可很有學問的哦,沒玩過堆棧的人腦子有點轉不彎:這一RET,RET到哪去了?嘿嘿,別忘了在RET前已經將堆棧指針指向一個函數的入口了.你別把RET看成RET,你把它看成是另一種類型的JMP就好理解了.

          SP = task_sp[任務號];

          return;

          做完這4件事后,任務"并行"執行就開始了.你可以象寫普通函數一個寫任務函數,只需(目前可以這么說)注意在適當的時候(例如以前調延時的地方)調用一下task_switch(),以讓出CPU控制權給別的任務就行了.

          最后說下效率問題.

          這個多任務系統的開銷是每次切換消耗20個機器周期(CALL和RET都算在內了),貴嗎?不算貴,對于很多用狀態機方式實現的多任務系統來說,其實效率還沒這么高--- case switch和if()可不像你想像中那么便宜.

          關于內存的消耗我要說的是,當然不能否認這種多任務機制的確很占內存.但建議大家不要老盯著編譯器下面的那行字"DATA = XXXbyte".那個值沒意義,堆棧沒算進去.關于比較省內存多任務機制,我將來會說到.

          概括來說,這個多任務系統適用于實時性要求較高而內存需求不大的應用場合,我在運行于36M主頻的STC12C4052上實測了一把,切換一個任務不到3微秒.

          下回我們講講用KEIL寫多任務函數時要注意的事項.

          下下回我們講講如何增強這個多任務系統,跑步進入時代.

          四.用KEIL寫多任務系統的技巧與注意事項

          C編譯器很多,KEIL是其中比較流行的一種.我列出的所有例子都必須在KEIL中使用.為何?不是因為KEIL好所以用它(當然它的確很棒),而是因為這里面用到了KEIL的一些特性,如果換到其它編譯器下,通過編譯的倒不是問題,但運行起來可能是堆棧錯位,上下文丟失等各種要命的錯誤,因為每種編譯器的特性并不相同.所以在這里先說清楚這一點.

          但是,我開頭已經說了,這套帖子的主要目的是闡述原理,只要你能把這幾個例子消化掉,那么也能夠自已動手寫出適合其它編譯器的OS.

          好了,說說KEIL的特性吧,先看下面的函數:

          sbit sigl = P1^7;

          void func1()

          {

          register char data i;

          i = 5;

          do{

          sigl = !sigl;

          }while(--i);

          }

          你會說,這個函數沒什么特別的嘛!呵呵,別著急,你將它編譯了,然后展開匯編代碼再看看:

          193: void func1(){

          194: register char data i;

          195: i = 5;

          C:0x00C3 7F05 MOV R7,#0x05

          196: do{

          197: sigl = !sigl;

          C:0x00C5 B297 CPL sigl(0x90.7)

          198: }while(--i);

          C:0x00C7 DFFC DJNZ R7,C:00C5

          199: }

          C:0x00C9 22 RET

          看清楚了沒?這個函數里用到了R7,卻沒有對R7進行保護!

          有人會跳起來了:這有什么值得奇怪的,因為上層函數里沒用到R7啊.呵呵,你說的沒錯,但只說對了一半:事實上,KEIL編譯器里作了約定,在調子函數前會盡可能釋放掉所有寄存器.通常性況下,除了中斷函數外,其它函數里都可以任意修改所有寄存器而無需先壓棧保護(其實并不是這樣,但現在暫時這樣認為,飯要一口一口吃嘛,我很快會說到的).

          這個特性有什么用呢?有!當我們調用任務切換函數時,要保護的對象里可以把所有的寄存器排除掉了,就是說,只需要保護堆棧即可!

          現在我們回過頭來看看之前例子里的任務切換函數:

          void task_switch()

          {

          task_sp[task_id] = SP; //保存當前任務的棧指針

          if(++task_id == MAX_TASKS) //任務號切換到下一個任務

          task_id = 0;

          SP = task_sp[task_id]; //將系統的棧指針指向下個任務的私棧.

          }

          看到沒,一個寄存器也沒保護,展開匯編看看,的確沒保護寄存器.



        關鍵詞: 51 操作系統

        評論


        相關推薦

        技術專區

        關閉
        主站蜘蛛池模板: 鹤山市| 嘉义市| 台中市| 霸州市| 阿城市| 文成县| 宽城| 霍州市| 香格里拉县| 海宁市| 区。| 永泰县| 凌源市| 郧西县| 广东省| 海宁市| 浦江县| 邯郸县| 彭泽县| 涿鹿县| 侯马市| 施甸县| 乌鲁木齐县| 田东县| 宁国市| 抚宁县| 治县。| 奈曼旗| 琼海市| 南投县| 吴旗县| 长葛市| 林芝县| 谷城县| 南部县| 通渭县| 霍林郭勒市| 岳池县| 中山市| 冕宁县| 武定县|