本文作者:Gthgth
注意:小小調度器V2.0 作者: 兔子、smset
在作者和“兔子”大蝦的努力下,小小調度器迎來一個激動人心的新版本。(2.0正式版,為了大家方便學習才有V2.0簡易版)
在作者和兔子的幫助下,開始學習V2.0簡易版。
V1.1版本和V2.0 簡易版本不沖突,是兩個相對獨立的版本,各有各的優點,V1.1突出強調小,省資源。并不是v2.0 的存在就取代了V1.1;和V1.1版本
相比, 簡易版,在 的基礎上,支持任務重入;當然了 也是一如既往的小。
v2.0 V1.1 V2.0
在百度中查了一下:可重入代碼指可被多個函數或程序凋用的一段代碼(通常是一個函數),而且它保證在被任何一個函數調用時都以同樣的方式運行。
在小小調度器V2.0 中:
子任務可以被多個主任務調用,主任務可以給子任務傳遞參數。子任務也可以訪問主任務的數據。每個任務之間可以相互訪問數據。
具體反映在:
1.把每個任務函數的私有變量和行號、延時時間等都獨立出去,保存在自己的結構體變量里面了;
2.在運行任務函數時,有關數據不能直接傳給結構體,而是地址進去,進去后轉換回結構體。
一.主函數分析
voidmain(){
while(1){
delay_ms(1);//延時1毫秒
runtasks();
}
}
分析:很簡單,延時1ms執行runtasks()函數;這樣就相當于每隔1ms 掃描一次runtasks()函數。沒有用到定時器中斷,這個1ms 時基可以根據要求修改;
如果用定時器,時基寫的很小就會頻繁的打斷CPU。用延時感覺時基選擇小一點這樣更節省CPU資源,如果延時太長,就會占用太長CPU。(問:作者為
什么用延時作為時基沒用定時器?答:那種都行,看情況;在示例中用延時作為時基是考慮到調度器中統一沒有涉及到中斷。)
(smset補充:一是由于以arduino為例,arduino默認代碼沒有提供中斷,因此沒有采用中斷時基。
另一個原因是V2.0簡易版默認使用short類型的任務Timer變量,如果使用中斷進行UpdateTimer更新,是存在隱患的,所以
如果在中斷里進行UpdateTimer更新,則必須使用unsignedchar類型的任務Timer變量)。
1,展開 runtasks();函數
voidruntasks(){
//指定led1任務驅動的IO管腳
led1.pin=13;
//更新頂級任務的時間
UpdateTimer(led1);
UpdateTimer(breath1);
UpdateTimer(serial1);
//執行頂級任務
RunTask(LedTask, led1);
RunTask(BreathTask, breath1);
RunTask(SerialTask,serial1);
}
LED 的I/O 管腳初始化其實可以寫在專門的初始化函數里面,這里是為了更好的說明,寫在了runtasks()函數里。編程很靈活。
一般來說有幾個任務,就有幾個對應更新頂級任務的時間函數和對應的執行頂級任務。
2把UpdateTimer(led1); 函數展開。
宏#define UpdateTimer(TaskVar) do{ if((TaskVar.task.timer!=0)&&(TaskVar.task.timer!=END))TaskVar.task.timer--; } while(0)
帶入展開:{if((led1.task.timer!=0)&&(led1.task.timer!=END)) led1.task.timer--; }
延時時間unsignedshort timer; 變量值不等于0,也不等于END (65535),它的值就減一。等于0 就去執行對應的函數;等于65535 就掛起。這個和1.1
版本是一樣的。
led1是個結構體變量。在led1結構體里包含了一個task個結構體變量,要引用里面的元素,用.分隔開寫到里面的最小元素。
task結構體里面有兩個變量:unsignedshort timer; 和unsignedchar lc; (有關結構體變量展開看后面的第5部分 :有關結構體及其宏的展開。)
3把RunTask(LedTask, led1);函數展開
宏#define RunTask(TaskName,TaskVar) do{ if(TaskVar.task.timer==0)TaskVar.task.timer=TaskName(&(TaskVar)); } while(0)
展開帶入:{if( led1.task.timer==0) led1.task.timer=LedTask(&(led1)); }
假如延時時間到了timer==0,就執行后面的函數,執行LedTask(&(led1))函數,執行完把結果賦值給led1.task.timer這個變量。這個和1.0版本是一樣的。
就是延時時間到,就去執行任務函數,然后把新的延時時間賦值給自己的timer,開始下一輪的循環。
因為小小調度器是協作式的,假如某個任務的時間延時到了,并不意味著要馬上執行這個任務函數,要等上一個任務釋放掉CPU后才執行本任務函
數;這樣就意味著,我們在編制任務函數的時候對任務函數的執行,時間要求不是那么的嚴格,在一定范圍內執行就可以了;同時也意味著CPU 只有把
某個任務函數執行完,把本任務該做的事做完后,然后再做其他的事情;因為cpu運行速度是比較快的,一般情況下占用CPU資源比較多的是等待條件
滿足和延時(來個數學運算或者什么的占用cpu時間較長怎么破?查表??,這和cpu有關,和調度器沒關?);對于等待條件滿足的可以用宏#define
WaitUntil(A) ,對于延時的可以用宏#defineWaitX(ticks),這樣可以在本任務等待或者延時的時候,做其他事情,提高效率。
我們看一下這個函數:執行LedTask(&(led1))的結果是一個值,這個函數的原型是LedTask(C_LedTask*cp) 。
也就是取led1結構體變量的首地址(&( led1))傳遞到函數的原型中定義的結構體變量指針(C_LedTask*cp)。把它們對應起來,就是定義一個結構體指針,并指向led1 的首地址,這樣兩者對應起來 (結構體變量led1和結構體指針C_LedTask*cp類型都是一樣的;有關結構體變量展開看后面的第5 部分 :有關
結構體及其宏的展開。)。這里用結構體指針主要的目的就是把彼此剝離,為實現重入做好準備。實現任務函數多次調用,彼此沒有影響。
為了書寫方便,作者做了一個宏#defineTaskFun(TaskName) TimeDefTaskName(C_##TaskName*cp){switch(me.task.lc){default:
因為這個函數有返回值,所以函數前面加了類型限制符unsignedshort (宏為:#defineTimeDef unsignedshort)。
為了書寫或者閱讀方便作者就做了一個語法糖 (就是一個宏#define me (*cp),為了防止出錯,指針一定要加括號,涉及到優先級的問題)。
其實所用的宏定義都可以認為是語法糖,用糖把語法包裹著,就是為了方便書寫、理解等等。
4,把TaskFun(LedTask);函數展開
宏#defineTaskFun(TaskName) TimeDefTaskName(C_##TaskName*cp){switch(me.task.lc){default:
展開,替換后:
//TaskFun(LedTask){
unsignedshort LedTask(C_LedTask*cp){switch(me.task.lc){default:{ //編譯器初始化的時候給lc賦值為0。?
me.timelen=20;//LEDPWM總周期為20 毫秒。//一般在沒有進入循環前,可以對一些變量賦值。V1.1版本用到私有變量一般是在這里定義的,為局部
pinMode(me.pin, OUTPUT);//設置管腳輸出 // 靜態變量;2.0版本用到的變量在前面統一定義,變量的應用是通過指針進行的。當然了平
// 時怎么用就怎么寫。
while(1)
{
digitalWrite(me.pin,HIGH);//點亮LED
//WaitX(me.timeon);
//#defineWaitX(ticks) do{ me.task.lc=LINE; return(ticks);case LINE:;}while(0)
{ me.task.lc=LINE; return(me.timeon);caseLINE:;}
digitalWrite(me.pin,LOW);//關閉LED
WaitX(me.timelen-me.timeon);
}
}EndFun
展開后可以看到,里面用到的變量都是通過結構體指針,指向我們當初在“任務類及任務變量”那里定義的結構體變量。這樣有關任務函數的操作
其實所有的數據都是存在任務自己所定義的變量里面;這樣函數重入就不出現問題,多次調用任務函數彼此不影響;當然了運行任務函數前要對先對任
務用到的變量定義,全部都是全局變量。這個也是V2.0 簡易版和v1.1板的一個區別。
返回me.timeon 是個unsignedchar類型的。返回去的時候類型被轉換為unsignedshort 型。其余和1.0版本一樣,在記錄行號的時候沒有用到靜態局
部變量,用的是每個任務變量里 task結構體里面unsignedchar lc;。lc 默認為uchar 也就是說TaskFun(TaskName) 任務函數里面WaitX(ticks)的個數(不
包擴它調用的子任務)不能超過256 (0-255)個,這個和1.1版本是一樣的。每個任務函數里面所寫語句的行數是沒有限制的,每個任務函數里面只能
有256個WaitX(ticks),如果發現編譯錯誤,也是在前面增加空行。在V1.1版本的時候,有位網友在編制任務函數的時候,有一個里面用的WaitX(ticks)個
數比較多,編寫代碼的行數也比較多,他發現編譯錯誤,就在WaitX(ticks)前面加空行,可是又和其他的WaitX(ticks)沖突,到后面每個WaitX(ticks)前面都
有數量不等的空行;為了避免這種事情出現,一個是修改lc 變量的類型,這個在2.0版本是非常方便的,只要修改宏就行(#define LineDef unsignedchar)。
在V1.1也可以修改,只不過要修改兩三處地方。另外一個就是把函數優化或者拆分等等,讓任務函數里面不要出現這么多的WaitX(ticks)。
執行這個函數,返回一個延時的數值,下次執行的時候,通過SWITCH語句跳轉到上次執行的位置,繼續執行相關語句,并返回一個延時數值。這個
也是PT 的精華所在。如果不明白請參考1.0 版本的分解。
5.有關結構體及其宏的展開。
(1).原型:
#defineClass(type) typedefstructC_##typeC_##type;struct C_##type
Class(task)
{
TimeDeftimer;
LineDeflc;
};
(2).把宏替換掉
typedefstructC_taskC_task;structC_task{
TimeDeftimer;
LineDeflc;
};
把宏替換掉后對于typedefstructC_taskC_task;structC_task{ 這句的理解分兩部分
a.紅色部分,用C_task代替structC_task。用C_task可以定義結構體變量。
b.藍色部分 因為structC_task沒有定義,它的定義在下面,告訴上面不是沒定義嘛,在這定義了 。
c.一般來說類型定義typedefstructC_taskC_task;,應該放在它所重定義的類型的后面,就是應該在結構體定義后面,像u8,u16那樣。
d.先做typedef 類型定義,也就是說,這屬于事先聲明,之后才有具體定義,跟函數聲明一樣 。
(3).等價于
typedefstructC_taskC_task;
structC_task{
TimeDeftimer;
LineDeflc;
};
在這里要注意,宏展開后可以看到,可以用C_task 定義結構體,這個結構體變量里面只包含 unsignedshorttimer; 和unsignedcharlc;。
(4).任務類及任務變量展開:
//Class(LedTask)
typedefstructC_LedTaskC_LedTask;struct C_LedTask
{
C_tasktask;//每個任務類都必須有task變量,里面只包含timer和lc 變量
unsignedcharpin;//LED對應的管腳
unsignedchartimeon;//LED點亮的時長
unsignedchartimelen; //LED循環點亮的周期
}led1;
定義了一個名為led1 的結構體變量。
在這里需要注意一點:用C_LedTask可以定義結構體變量。用如果是C_LedTaskled2; 這是定義了一個名為led2 的結構體,里面的元素和led1里面的一樣;
要區分用C_LedTask和用C_task 定義結構體的區別。
在這個任務類里面定義了每個任務函數所用到的私有變量,及每個任務函數用到的記錄執行地址的lc變量和記錄需要延時的變量timer;是個完整的獨立的個體。用到子任務時,在父任務結構體中用 C_***task 定義一個子任務結構體變量 ,從某種意義上講任務重入也是需要代價的。
有時候感覺繞來繞去,其實就是這個任務類的問題,
1.因為任務類及任務變量定義中用Class 定義任務所用到的私有變量和一個獨立的C_tasktask,里面放著這個任務函數的行號和延時時間變量。用宏
C_***Task可以定義和本任務相關所有變量的結構體。
2.如果任務類里面包含了其他的子任務。一般包含一個或者幾個用 C_***task 定義的結構體變量;(其實就是相當于把子任務中用到的所有變量在這
個父任務中又重新定義了一下)。父任務調用這個子任務,會把數據放到這個子任務的結構體里;同理其他父任務調用同一個子任務也會把數據放到自
己的子任務結構體里。
3.父任務函數調用子任務函數時,用到的數據是通過指針傳遞的,把這些變量傳遞給子任務函數。當然了父任務函數變量的傳遞也是通過指針的。這樣結
合每個任務定義的結構體變量,就能解決任務重入的問題了。子任務可以被多個主任務調用,主任務可以給子任務傳遞參數。主任務彼此獨立互不影響。
V1.1版本,記錄行號的變量是局部靜態變量,涉及到跨任務的變量都是靜態局部變量或者靜態全局變量;延時變量是個全局變量。
V2.0版本,每個任務函數用到的變量都是自己的私有全局變量,在調用的時候通過指針傳遞。
二.呼吸燈
在上面LED控制的基礎上設計一個呼吸燈,指示燈從暗到亮變化,分20個階段;再從亮到暗變化,也分20個階段,每個階段保持100ms
分析:
作為一個獨立的頂級任務,設置的時候,就需要有自己的任務變量和任務函數。
把呼吸燈用到的變量統一放在一個結構體中,起名:breath1;呼吸燈對應的任務函數定義為BreathTask。編寫任務函數的時候,注意把他們定義的結構體
名和函數名對應起來,這樣就不容易弄混了。
1.呼吸燈任務類及任務變量
Class(BreathTask)//LED 呼吸燈控制任務
{
C_tasktask;//每個任務類都必須有task變量,里面存放著延時變量和行號。
unsignedchari;//呼吸燈變量,
}breath1; //呼吸燈的結構體變量名
2.呼吸燈任務函數 (呼吸燈的具體動作)
TaskFun(BreathTask){//實現呼吸燈效果
while(1)
{
//從暗到亮變化
for(me.i=0;me.i<20;me.i++){ //這個變量i就是我們在結構體breath1 中定義的unsignedchari;//呼吸燈變量。
WaitX(100); //和1.0版本原理一樣,釋放CPU,過100ms 再往下執行。
led1.timeon=me.i; //把呼吸燈的變量值,賦值給了LED任務函數里的變量了,因為定義的結構體都是全局變量的,可以相互調用,賦值。
}
//再從亮到暗變化
for(me.i=20;me.i>0;me.i--){
WaitX(100);
led1.timeon=me.i; //用到的本任務函數的變量是通過指針;在這里引用其他任務的變量,是直接引用的。執行到LED任務函數的時候值變了。
}
}
}EndFun
3.任務函數里的變量,都是通過指針傳遞的,和各自定義的結構體對應起來。由于2.0版本需要支持重入,任務函數值不能直接傳給結構體,
而是地址進去,進去后轉換回結構體。
4.在這個呼吸燈的任務函數中用到了其他任務函數中的變量:led1.timeon=me.i;,通過賦值,下次執行LED 函數的時候就會發生變化。也就是說
這個調度器支持任務之間的數據互訪。
5.如果這個呼吸燈任務函數里面不用數據互訪賦值,而用子任務調用,怎么寫?假如沒有這個呼吸燈的任務函數,執行led任務函數,led燈的狀態是什
么?
三.子任務分析
涉及到的宏#defineCallSub(SubTaskName,SubTaskVar) do{WaitX(0);SubTaskVar.task.timer=SubTaskName(&(SubTaskVar)); \
if(SubTaskVar.task.timer!=END)returnSubTaskVar.task.timer;}while(0)
看串口任務類及任務變量,
Class(SerialTask)
{
C_tasktask; //每個任務類都必須有task變量
C_WaitsecTaskwaitsec1;//串口任務擁有一個秒延時子任務
Stringcomdata;//串口任務自己用的變量
}serial1;
定義了一個結構體變量serial1,里面除了自己用的變量外,增加了一個C_WaitsecTaskwaitsec1; (定義了一個結構體,里面包含了WaitsecTask任務函數所
用到的全部變量)接下來我們看一下串口的任務函數。
TaskFun(SerialTask){//串口任務,定時輸出hello
Serial.begin(9600);
Serial.println("start");
while(1){
me.waitsec1.seconds=1;//總共延遲1+2=3秒
CallSub(WaitsecTask,me.waitsec1);
Serial.println("hello");
}
}EndFun
分解開來看一看
TaskFun(SerialTask){//串口任務,定時輸出hello
根據上面分析的經驗,執行完任務函數的有關指令,返回一個延時函數給timer。
我們看一下有關語句:
Serial.begin(9600);Serial.println("start");不用關心,串口的波特率和起始位什么的,(猜的)。
程序執行到me.waitsec1.seconds=1;很簡單,給自己里面子任務中的變量賦了一個值,看清楚是要求子任務延時1個單位。
接著繼續執行到CallSub(WaitsecTask,me.waitsec1);我們看一下它的宏
#defineCallSub(SubTaskName,SubTaskVar) do{WaitX(0);SubTaskVar.task.timer=SubTaskName(&(SubTaskVar)); \
if(SubTaskVar.task.timer!=END)returnSubTaskVar.task.timer;}while(0)
把有關參數帶進去。
{WaitX(0);me.waitsec1.task.timer=WaitsecTask(&(me.waitsec1)); if(me.waitsec1.task.timer!=END)returnme.waitsec1.task.timer;}
展開分析:
執行WaitX(0);在這里設置一個“斷點”,讓任務下次從這里執行。記錄當前LC 位置,這樣如果子任務有WAIT(X),出來以后下次能順利進去。(分析一下
如果沒有WaitX(0);會發生什么問題?)
執行自己的子任務WaitsecTask(&(me.waitsec1)把結果賦值給自己定義的子任務變量里的timer變量,
在程序中找找到WaitsecTask(),函數的原型:
#defineTaskFun(TaskName) TimeDefTaskName(C_##TaskName *cp){switch(me.task.lc){default:
TaskFun(WaitsecTask){//實現指定的秒數延遲 (me.waitsec1.seconds=1;在本例中賦值為1S),之后再加上2秒延遲
for(me.i=0;me.i<me.seconds;me.i++){
WaitX(1000);
}
CallSub(Wait2Task,me.wait2);//這里通過調用2秒固定延遲子任務,實現額外的2秒延遲。
}EndFun
執行完自己指定的延時后,繼續執行自己子任務里面子任務調用的它的子任務,CallSub(Wait2Task,me.wait2),再實現2S 的延時,展開略。
通過上面的分析,我們很清楚的看到用Class(task)定義結構體用起來是很方便的,除了考慮自己父任務函數里必須的變量外,對于子函數的調用只要
定義一個宏,(其實是把每一層的變量都放在了自己定義的宏里面了),用CallSub(SubTaskName,SubTaskVar)函數調用就可以了。只要你的內存大你可以無限的調用,無論子程序怎么調用,彼此互不影響。
定義了任務類(Class(task)),在函數變量應用和子程序變量定義的時候很靈活,減少我們的書寫量,每個任務函數用到的數據,都保持在自己獨立
定義的變量中;函數調用用指針;這樣,函數就可以實現重入。任務函數可以相互調用;只要你的內存足夠大,就可以無限調用。
以上展開后都在強調為任務重入做準備,其實如果不用到任務重入功能,把time變量改為uchar感覺V2.0 簡易版和V1.1所用的資源相差不多,V2.0
用到的變量全部是全局變量,V1.1用到的變量涉及到任務之間的切換都是局部靜態變量。其實v2.0 簡易版這種寫法感覺比V1.1 的更加清晰。
四.總結
通過上面的分解,我們再回頭看一下作者smset 對V2.0 的評價
主要改進:
1)徹底解決了任務重入問題
2)很好的解決了任務之間的通信問題
3)引入面向任務對象的概念
4)任務具有自己的變量,提高了程序封裝程度
0.png (42.16 KB, 下載次數: 85)
下載附件
2018-8-25 01:36 上傳
單片機源程序如下:
所有資料51hei提供下載:
小小調度器V2.0 簡化版.zip
(1.93 KB, 下載次數: 164)
2018-8-24 23:41 上傳
點擊文件名下載附件
下載積分: 黑幣 -5
小小調度器V2.0 Simple 整理說明2.pdf
(212.7 KB, 下載次數: 127)
2018-8-24 23:41 上傳
點擊文件名下載附件
下載積分: 黑幣 -5
|