這幾天和羣裏小V同窗討論一個項目時,偶然發現了 ProtoThreads,簡稱PT,用其來實現arduino的多線程控制很方便。這裏摘錄幾篇介紹的文章。php
1、如下轉自http://www.arduino.cn/thread-5833-1-1.htmlhtml
1樓、背景——
想象一個這樣的狀況,請不要在乎這樣奇怪的情景——
一個四位的數碼管,因爲要「同時」顯示,所以每5ms刷新一次。(求別說MAX7219之類的IC……)
同時要處理一個矩陣鍵盤,設計是每10ms掃描一行,同時還有去抖處理,須要在檢測到按鍵後再延時40ms檢查一次。
檢測到有效按鍵,在數碼管上顯示某個值,好比1234。
同時還能從串口接收數據,若是有數據收到,立刻在數碼管上顯示某個值,好比5678,停留1s,期間按矩陣鍵盤不會有任何反應。
程序怎麼寫?暈了沒?
好比說,去抖的時候,若是直接用delay(40)的話,那數碼管的5ms刷新怎麼辦?串口收到的數據辦?
基於這種超煩的(劃掉)多任務處理,爲了編程方便,讓咱們祭出嵌入式操做系統這一個神器!!!
哎!別走!!!妹子等我說完,我不打算講高深理論哎!!(旁白:反正你也不是這個專業的,也講不出)
嵌入式操做系統是用來處理這類超煩的(劃掉)多任務處理的狀況,常見的有uCos、RT-Thread等等,有興趣的能夠去看看。
可是Arduino,編譯一個文件出來,若是你有留意的話,體積很大,而arduino自己的內存就很少,再移植一個就Orz了。因此,上面說的,不能用!!!哎!別走啊喂!!!
鑑於你們作些小做品,不須要用到如此高深的操做系統,只要簡單地處理一下這些多任務的問題,因此,讓咱們祭出Adam Dunkels大神的ProtoThreads!
二、ProtoThreads與嵌入式操做系統簡介
ProtoThreads是一個經過宏(#define)寫出來的神奇的模擬多線程(理解成多任務先)的庫,裏面全是頭文件,找不到.cpp等程序文件。它的核心利用了C語言switch語句的特性。說是嵌入式系統,但這其實還只算是一個調度器,因此,並不能說是一個完整的操做系統。
操做系統最核心的功能是:在等待某個事件發生的時候,好比說定時一段時間、有無按鍵、串口上有無數據等等,操做系統幫你將單片機從當前的任務中臨時切換到另外一個任務運行,直到指定事件發生了再回來接着運行,這樣就是變相實現了多任務處理,節省了CPU時間,還極大地減小開發難度(我會說我學過嵌入式系統後就不再想作流水線式的設計了嗎?)。
ProtoThreads在較大程度上實現出操做系統的核心功能,並且,每一個新建一個任務,只需額外增長16bit即兩個字節的空間(引入個人定時器宏則爲6個字節)。除了核心功能外,還增長了信號量、延時這兩個功能(僅限於我提供的庫),我大概想到消息隊列、標誌怎麼寫了,可是沒空寫(旁白:實際上是懶吧?)。
可是!!有缺點!!我說過了,它利用了switch語句的特性,因此,我很是不建議在任務中使用switch這個語句,除非你能保證在你的switch語句內不會切換任務。其次,請慎用內部變量,尤爲是循環變量,在切換任務時有必定的可能性發生不可預料的錯誤,要用,請必定加上static修飾。
講完,下面講講怎麼用。
三、任務準備工做
首先,每一個任務都必需要有一個記錄變量,記錄任務的狀態,便於返回。語句:
static struct pt xxx;
這個xxx大家本身取好了。下面的所有都是xxx。請必定要加上前面的static struct。
好,而後要初始化一個任務。在setup()函數裏面用這個語句:
PT_INIT(&xxx);
這樣就初始化成功啦~記得要加個&符號。(必定要哦~)
四、編寫任務
每一個任務在程序裏面,就算是一個獨立函數,在裏面處理你要作的東西就能夠啦。
函數格式以下:
static int 任務名(struct pt *pt)
{
PT_BEGIN(pt);
//你的處理過程
PT_END(pt);
}
若是不太懂ProtoThreads內部結構,就只改任務名好了,而後就這樣寫。PT_BEGIN(pt);必定要在開頭,PT_END(pt);必定要在結尾,別漏,不然編譯錯誤,運行到這裏的時候這個任務就完全結束了。
因爲處理過程通常是循環處理的,因此處理過程通常是while(1){處理內容},做用就像loop(){}函數啦。
下面是一些等待某種信號所用到的宏:(部分,我沒有所有講,只挑了一些經常使用的,有興趣的能夠本身看源代碼)
PT_WAIT_UNTIL(pt,條件);
這個語句的功能是,若是條件不成立,那麼暫時退出當前任務,先處理別的任務,再回來看看。若是條件成立了,那麼繼續往下執行。第一個變量pt我我的建議別改啦。
PT_WAIT_WHILE(pt,條件);
做用和上面的相反,條件成立則切換任務,條件不成立則繼續執行。
PT_WAIT_THREAD(pt,任務x名);
等到任務x完成了(任務x運行到PT_END了)才繼續執行。x應爲一次性任務而不是循環任務。
PT_RESTART(pt);
重啓當前任務
PT_EXIT(pt);
退出並註銷當前任務
PT_YIELD(pt);
把CPU讓給別的任務用一下下,用完了我再繼續用。
(下面的是定時器,該宏是我本身寫的,用以前請在#include "pt.h" 的前面,前面啊!加上一句#define PT_USE_TIMER)
先說明一下,下面的定時不必定徹底準確的,可能會有點點的偏差,可能偏後。若是趕上了很煩的任務,有可能會使延時延後。可是正常狀況下,直接用就行了。
若是要很精確的延時,請用delay語句或者計時器,可是,絕大多數狀況下,絕大多數狀況!絕大多數狀況!請用下面的語句代替delay延時!這樣才能把CPU讓給別的任務使用。
PT_TIMER_DELAY(pt,延時毫秒數);
字面上的意思,不用多說了吧?最大值約爲49.7天,估計沒人會延時辣麼久吧……
PT_TIMER_MICRODELAY(pt,延時微秒數);
字面上的意思,不用多說了吧?注意,最小精度與arduino的版本有關,與micros()有精度一致。
PT_TIMER_WAIT_TIMEOUT(pt,條件,毫秒數);
若是條件成立了,或者超時了,就繼續運行,不然切換任務。
又說完啦~就這些~
五、信號量(Semaphore)
妹子求別走T_T
我不得不解釋一個專業術語,由於這二貨頗有用(信號量:你才二)……
我不上定義了,直接用例子說,不是我發明的例子。
停車場。停車。停車場裏面的車位是固定的,假設沒有一輛車佔多個車位的狀況。在這種狀況下,剩餘車位數就是一個「信號量」了。進一量車,剩餘車位數就減一;出一量車,剩餘車位數就加一。若是剩餘車位數爲0,那麼想進來的車就只能在外面淋雨了。
對,信號量也是這樣用。獲得了一個信號量,任務繼續運行,得不到,一邊呆着去。
具體有什麼用呢?好比說,一樓寫着的,監控串口的任務讀到數據了,要佔用數碼管。那麼咱們命令一個信號量爲土豪,土豪只有一個。每次矩陣鍵盤要顯示數據,先申請一個土豪,寫數據,而後釋放土豪,若是申請不到就在牆角不斷畫圈圈。監控串口的任務一旦申請到土豪就劫持1s,不讓矩陣鍵盤用。這樣就能夠達到要求啦~
下面是用法:
要用的話,請在#include "pt.h"前面加上一句 #define PT_USE_SEM
首先要建立一個信號量,這個必定是全局變量:
static struct pt_sem 信號量名;
接着請在setup()函數裏面給它初始化:
PT_SEM_INIT(&信號量名,數量);
信號量名前面有個&,別忘了。數量就至關於停車場的總車位數。
而後要用啦。任務要停一輛車進去:
PT_SEM_WAIT(pt,&信號量名);
信號量名前面有個&,別忘了。一個語句只能停一輛車,土豪好多車就用屢次。
任務要開一輛車出來:
PT_SEM_SIGNAL(pt,&信號量名);
信號量名前面有個&,別忘了。用一次出一輛。
固然,對於一個任務來講,信號量沒上限,就是說,你能夠在停車場內再開闢新的車位,不斷用PT_SEM_SIGNAL()就行了。
其實信號量這貨解決的問題中,比較出名的是生產者與消費者問題。簡單地說,消費者要買,必需要生產者生產才能買到,沒生產出來,消費者只能等。
六、例子
你們翻了那麼久都累了伐……給個例子唄……
要求:板載LED以4秒一週期的速率閃爍。一旦收到串口發來的信息,無論信息量多少,快閃5次。用ProtoThreads寫。
//首先啓用定時器庫和信號量庫,下面會用到
#define PT_USE_TIMER
#define PT_USE_SEM
//引用庫
#include "pt.h"
static struct pt thread1,thread2; //建立兩個任務
static struct pt_sem sem_LED; //來個LED的信號量,同一時間只能一個任務佔用
unsigned char i; //循環變量,寫在這裏其實不合適
void setup() {
//初始化13口和串口
pinMode(13,OUTPUT);
Serial.begin(115200);
PT_SEM_INIT(&sem_LED,1); //初始化信號量爲1,即沒人用
//初始化任務記錄變量
PT_INIT(&thread1);
PT_INIT(&thread2);
}
//這是LED慢速閃爍的任務
static int thread1_entry(struct pt *pt)
{
PT_BEGIN(pt);
while (1)
{
PT_SEM_WAIT(pt,&sem_LED); //LED有在用嗎?
//沒有
digitalWrite(13,!digitalRead(13));
PT_TIMER_DELAY(pt,1000);//留一秒
PT_SEM_SIGNAL(pt,&sem_LED);//用完了。
PT_YIELD(pt); //看看別人要用麼?
}
PT_END(pt);
}
//這是LED快速閃爍的任務,若是有串口消息,快速閃5次
static int thread2_entry(struct pt *pt)
{
PT_BEGIN(pt);
while (1)
{
PT_WAIT_UNTIL(pt, Serial.available()); //等到有串口消息再繼續
PT_SEM_WAIT(pt,&sem_LED);//我要用LED啊!
//搶到使用權了,虐5次
for (i=0;i<5;i++)
{
digitalWrite(13,HIGH);
PT_TIMER_DELAY(pt,200);
digitalWrite(13,LOW);
PT_TIMER_DELAY(pt,200);
}
while (Serial.available())
Serial.read();
//清空串口數據,防止又來
PT_SEM_SIGNAL(pt,&sem_LED); //歸還LED使用權了
}
PT_END(pt);
}
void loop() {
//依次調用便可
thread1_entry(&thread1);
thread2_entry(&thread2);
}
七、後記
版權問題:ProtoThreads的基本代碼是由Adam Dunkels編寫了,詳情請看Readme.md,我我的只擴展了pt-timer.h這一個庫。轉載及使用ProtoThreads的基本代碼請遵循Adam Dunkels的聲明。轉載及使用我寫的pt-timer.h請署名「逍遙豬葛亮」。
歡迎轉載、使用、修改等等,提一提「逍遙豬葛亮」我會很高興的。百度Arduino吧裏面的是我寫的,因此不存在非法複製粘貼的問題吧?
git
2、如下轉自http://www.geek-workshop.com/forum.php?mod=viewthread&tid=610&extra=page%3D1編程
先上一段簡單的代碼look look多線程
#include <pt.h> static int counter1,counter2,state1=0,state2=0; static int protothread1(struct pt *pt) { PT_BEGIN(pt); while(1) { PT_WAIT_UNTIL(pt, counter1==1); digitalWrite(12,state1); state1=!state1; counter1=0; } PT_END(pt); } static int protothread2(struct pt *pt) { PT_BEGIN(pt); while(1) { PT_WAIT_UNTIL(pt, counter2==5); counter2=0; digitalWrite(13,state2); state2=!state2; } PT_END(pt); } static struct pt pt1, pt2; void setup() { pinMode(12,OUTPUT); pinMode(13,OUTPUT); PT_INIT(&pt1); PT_INIT(&pt2); } void loop () { protothread1(&pt1); protothread2(&pt2); delay(1000); counter1++; counter2++; }
此段代碼演示瞭如何使用PT庫來實現十二、13腳led分別隔1秒、5秒閃爍,已經在arduino09上測試經過
sorry,無註釋。。別急,這只是個演示
這篇文章會不斷更新,分別講述PT庫的原理和應用
讓你們能開發出更復雜的程序
好介紹開始了~
Protothread是專爲資源有限的系統設計的一種耗費資源特別少而且不使用堆棧的線程模型,其特色是:
◆ 以純C語言實現,無硬件依賴性;
◆ 極少的資源需求,每一個Protothread僅須要2個額外的字節;
◆ 能夠用於有操做系統或無操做系統的場合;
◆ 支持阻塞操做且沒有棧的切換。
使用Protothread實現多任務的最主要的好處在於它的輕量級。每一個Protothread不須要擁有自已的堆棧,全部的Protothread 共享同一個堆棧空間,這一點對於RAM資源有限的系統尤其有利。相對於操做系統下的多任務而言,每一個任務都有自已的堆棧空間,這將消耗大量的RAM資源, 而每一個Protothread僅使用一個整型值保存當前狀態。
我們來結合一個最簡單的例子來理解ProtoThreads的原理吧,就拿上面的閃爍燈代碼來講函數
#include <pt.h>//ProtoThreads必須包含的頭文件 static int counter1,counter2,state1=0,state2=0; //counter爲定時計數器,state爲每一個燈的狀態 static int protothread1(struct pt *pt) //線程1,控制燈1 { PT_BEGIN(pt); //線程開始 while(1) //每一個線程都不會死 { PT_WAIT_UNTIL(pt, counter1==1); //若是時間滿了1秒,則繼續執行,不然記錄運行點,退出線程1 digitalWrite(12,state1); state1=!state1;//燈狀態反轉 counter1=0; //計數器置零 } PT_END(pt); //線程結束 } static int protothread2(struct pt *pt) //線程2,控制燈2 { PT_BEGIN(pt); //線程開始 while(1) { //每一個線程都不會死 PT_WAIT_UNTIL(pt, counter2==5); //若是時間滿了5秒,則繼續執行,不然記錄運行點,退出線程2 counter2=0; //計數清零 digitalWrite(13,state2); state2=!state2; //燈狀態反轉 } PT_END(pt); //線程結束 } static struct pt pt1, pt2; void setup() { pinMode(12,OUTPUT); pinMode(13,OUTPUT); PT_INIT(&pt1); //線程1初始化 PT_INIT(&pt2); //線程2初始化 } void loop () //這就是進行線程調度的地方 { protothread1(&pt1); //執行線程1 protothread2(&pt2); //執行線程2 delay(1000); //時間片,每片1秒,可根據具體應用設置大小 counter1++; counter2++; }
看上面的代碼,你會發現不少大寫的函數,其實那些都是些宏定義(宏定義用大寫是約定俗成的..),若是把這些宏都展開你就能更好的理解他的原理了:oop
#include <pt.h>//ProtoThreads必須包含的頭文件 static int counter1,counter2,state1=0,state2=0; //counter爲定時計數器,state爲每一個燈的狀態 static int protothread1(struct pt *pt) //線程1,控制燈1 { { char PT_YIELD_FLAG = 1; switch((pt)->lc) {//用switch來選擇運行點 case 0: //此乃初始運行點,線程正常退出或剛開始都從這開始運行 while(1) //每一個線程都不會死 { do { (pt)->lc=12;//記錄運行點 case 12: if(!(counter1==1)) { return PT_WAITING; //return 0 } } while(0) digitalWrite(12,state1); state1=!state1;//燈狀態反轉 counter1=0; //計數器置零 } } PT_YIELD_FLAG = 0; pt->lc=0; return PT_ENDED; // return 1 } } static int protothread2(struct pt *pt) //線程2,控制燈2 { { char PT_YIELD_FLAG = 1; switch((pt)->lc) {//用switch來選擇運行點 case 0: //線程開始 while(1) //每一個線程都不會死 { do { (pt)->lc=39; case 39://記錄運行點 if(!(counter2==5)) { return PT_WAITING; //return 0 } } while(0) counter2=0; //計數清零 digitalWrite(13,state2); state2=!state2; //燈狀態反轉 } } PT_YIELD_FLAG = 0; pt->lc=0; return PT_ENDED; // return 1 } } static struct pt pt1, pt2; void setup() { pinMode(12,OUTPUT); pinMode(13,OUTPUT); pt1->lc=0; //線程1初始化 pt2->lc=0; //線程2初始化 } void loop () //這就是進行線程調度的地方 { protothread1(&pt1); //執行線程1 protothread2(&pt2); //執行線程2 delay(1000); //時間片,每片1秒,可根據具體應用設置大小 counter1++; counter2++; }
好了,終於擴展完了。。
分析一下上面的代碼,就知道其實ProtoThreads是利用switch case 來選擇運行點的,每一個線程中的堵塞,其實就是判斷條件是否成立,不成立則return,因此說每一個線程都頗有雷鋒精神,捨己爲人,呵呵。有一點要注意那就 是每一個線程只可以在咱們指定的地方堵塞,至於堵塞點,那就要看具體應用了。
因爲線程是反覆被調用的,所以,寫程序的時候不能像寫通常的函數同樣使用局部變量,由於每次從新調用都會把變量初始化了,若是要保持變量,能夠把它定義爲static的
在pt.h中定義了不少功能:
PT_INIT(pt) 初始化任務變量,只在初始化函數中執行一次就行
PT_BEGIN(pt) 啓動任務處理,放在函數開始處
PT_END(pt) 結束任務,放在函數的最後
PT_WAIT_UNTIL(pt, condition) 等待某個條件(條件能夠爲時鐘或其它變量,IO等)成立,不然直接退出本函數,下一次進入本 函數就直接跳到這個地方判斷
PT_WAIT_WHILE(pt, condition) 和上面一個同樣,只是條件取反了
PT_WAIT_THREAD(pt, thread) 等待一個子任務執行完成
PT_SPAWN(pt, child, thread) 新建一個子任務,並等待其執行完退出
PT_RESTART(pt) 從新啓動某個任務執行
PT_EXIT(pt) 任務後面的部分不執行,直接退出從新執行
PT_YIELD(pt) 鎖死任務
PT_YIELD_UNTIL(pt, cond) 鎖死任務並在等待條件成立,恢復執行
在pt中一共定義四種線程狀態,在任務函數退出到上一級函數時返回其狀態
PT_WAITING 等待
PT_EXITED 退出
PT_ENDED 結束
PT_YIELDED 鎖死
好比PT_WAIT_UNTIL(pt, condition) ,經過改變condition能夠運用的很是靈活,如結合定時器的庫,把condition改成定時器溢出,那就是個時間觸發系統了,再把condition改成其餘條件,就是事件觸發系統了
暫時寫這麼多吧測試
3、MALC還作了個帶定時器的庫http://www.geek-workshop.com/thread-666-1-1.htmlui
#include <PT_timer.h> #include <pt.h> int servopin=8;//定義舵機接口數字接口7 int myangle=100;//定義角度變量 int val=0; static struct pt pt1,pt2; PT_timer servotimer; static int servoMove(struct pt *pt) { PT_BEGIN(pt); while(1) { servotimer.setTimer(25); PT_WAIT_UNTIL(pt,servotimer.Expired()); int pulsewidth;//定義脈寬變量 myangle%=156;//視舵機而定,防止越界 pulsewidth=myangle*11+500;//將角度轉化爲500-2205的脈寬值 digitalWrite(servopin,HIGH);//將舵機接口電平至高 delayMicroseconds(pulsewidth);//延時脈寬值的微秒數 digitalWrite(servopin,LOW);//將舵機接口電平至低 } PT_END(pt); } String inString = ""; static int angleRead(struct pt *pt) { PT_BEGIN(pt); while(1) { PT_WAIT_UNTIL(pt, Serial.available()>0); int inChar = Serial.read(); if (isDigit(inChar)) inString += (char)inChar; if (inChar == ' '||inChar=='\n') { myangle=inString.toInt(); Serial.print("myangle="); Serial.println(myangle); inString = ""; } } PT_END(pt); } void setup() { pinMode(servopin,OUTPUT);//設定舵機接口爲輸出接口 PT_INIT(&pt1); PT_INIT(&pt2); Serial.begin(9600); } void loop()// { servoMove(&pt1); angleRead(&pt2); }
ServoMove呢就是本身模擬出來的舵機函數,爲何不用servo庫呢?由於arduino自身的servo庫有不少限制,第一PWM不能用了,第 二最小角度只有1度,固然這個程序裏舵機的精度也是1度,只要把相應的變量改爲float就能精確到小數了,不過最大的好處是舵機腳能夠任意設定
servomove中限定了最大角度爲155度,這個與舵機有關
angleRead中使用了stringtoInt,每次能夠輸入一個角度,角度能夠以 空格 或 回車 結尾(操蛋的VC2010 SerialMonitor發送數據不帶 回車的。。那就空格了)
程序中使用了定時器,本身用C++寫的,第一次寫庫,照葫蘆畫瓢了。。。
感謝czad的擴展庫翻譯http://www.geek-workshop.com/forum.php?mod=viewthread&tid=184
這個庫用起來很是簡單:
PT_Timer t;//定義一個定時器
t.setTimer(unsinged long time) //定時時間,單位ms
t.Expired()//判判定時器是否溢出,是返回值>0
在上面的代碼中,一個舵機週期是20ms,前面的約2.5毫秒是信號週期,剩下的10多ms全是無用的低電平,然而又必不可少,因此果斷用定時器取代delay
spa
4、庫的下載地址 http://pan.baidu.com/s/1qYRg9T6。
本文章只作備忘記錄使用,這幾天和V同窗作的項目中用到了這個庫,這裏受項目未公開緣由,就不貼出原代碼了。