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