利用 ProtoThreads實現Arduino多線程處理(1)

 轉載請註明:@小五義http://www.cnblogs.com/xiaowuyi  QQ羣:64770604


這幾天和羣裏小V同學討論一個項目時,偶然發現了 ProtoThreads,簡稱PT,用其來實現arduino的多線程控制很方便。這裏摘錄幾篇介紹的文章。

一、以下轉自http://www.arduino.cn/thread-5833-1-1.html

       1樓、背景——


想象一個這樣的情況,請不要在意這樣奇怪的情景——
一個四位的數碼管,由於要“同時”顯示,因此每5ms刷新一次。(求別說MAX7219之類的IC……)
同時要處理一個矩陣鍵盤,設計是每10ms掃描一行,同時還有去抖處理,需要在檢測到按鍵後再延時40ms檢查一次。
檢測到有效按鍵,在數碼管上顯示某個值,比如1234。
同時還能從串口接收數據,如果有數據收到,馬上在數碼管上顯示某個值,比如5678,停留1s,期間按矩陣鍵盤不會有任何反應。


程序怎麼寫?暈了沒?


比如說,去抖的時候,如果直接用delay(40)的話,那數碼管的5ms刷新怎麼辦?串口收到的數據辦?


基於這種超煩的(劃掉)多任務處理,爲了編程方便,讓我們祭出嵌入式操作系統這一個神器!!!


哎!別走!!!妹子等我說完,我不打算講高深理論哎!!(旁白:反正你也不是這個專業的,也講不出)


嵌入式操作系統是用來處理這類超煩的(劃掉)多任務處理的情況,常見的有uCos、RT-Thread等等,有興趣的可以去看看。


但是Arduino,編譯一個文件出來,如果你有留意的話,體積很大,而arduino本身的內存就不多,再移植一個就Orz了。所以,上面說的,不能用!!!哎!別走啊喂!!!


鑑於大家做些小作品,不需要用到如此高深的操作系統,只要簡單地處理一下這些多任務的問題,所以,讓我們祭出Adam Dunkels大神的ProtoThreads!
2、ProtoThreads與嵌入式操作系統簡介

ProtoThreads是一個通過宏(#define)寫出來的神奇的模擬多線程(理解成多任務先)的庫,裏面全是頭文件,找不到.cpp等程序文件。它的核心利用了C語言switch語句的特性。說是嵌入式系統,但這其實還只算是一個調度器,所以,並不能說是一個完整的操作系統。

操作系統最核心的功能是:在等待某個事件發生的時候,比如說定時一段時間、有無按鍵、串口上有無數據等等,操作系統幫你將單片機從當前的任務中臨時切換到另一個任務運行,直到指定事件發生了再回來接着運行,這樣就是變相實現了多任務處理,節省了CPU時間,還極大地減少開發難度(我會說我學過嵌入式系統後就再也不想做流水線式的設計了嗎?)。

ProtoThreads在較大程度上實現出操作系統的核心功能,而且,每個新建一個任務,只需額外增加16bit即兩個字節的空間(引入我的定時器宏則爲6個字節)。除了核心功能外,還增加了信號量、延時這兩個功能(僅限於我提供的庫),我大概想到消息隊列、標誌怎麼寫了,但是沒空寫(旁白:其實是懶吧?)。

但是!!有缺點!!我說過了,它利用了switch語句的特性,所以,我非常不建議在任務中使用switch這個語句,除非你能保證在你的switch語句內不會切換任務。其次,請慎用內部變量,尤其是循環變量,在切換任務時有一定的可能性發生不可預料的錯誤,要用,請一定加上static修飾。

講完,下面講講怎麼用。


3、任務準備工作

首先,每個任務都必須要有一個記錄變量,記錄任務的狀態,便於返回。語句:
static struct pt xxx;

這個xxx你們自己取好了。下面的全部都是xxx。請一定要加上前面的static struct。

好,然後要初始化一個任務。在setup()函數裏面用這個語句:
PT_INIT(&xxx);

這樣就初始化成功啦~記得要加個&符號。(一定要哦~)


4、編寫任務

每個任務在程序裏面,就算是一個獨立函數,在裏面處理你要做的東西就可以啦。

函數格式如下:
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,條件,毫秒數);
如果條件成立了,或者超時了,就繼續運行,否則切換任務。

又說完啦~就這些~


5、信號量(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()就好了。

其實信號量這貨解決的問題中,比較出名的是生產者與消費者問題。簡單地說,消費者要買,必須要生產者生產才能買到,沒生產出來,消費者只能等。


6、例子

大家翻了那麼久都累了伐……給個例子唄……
要求:板載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);
}


7、後記

版權問題:ProtoThreads的基本代碼是由Adam Dunkels編寫了,詳情請看Readme.md,我個人只擴展了pt-timer.h這一個庫。轉載及使用ProtoThreads的基本代碼請遵循Adam Dunkels的聲明。轉載及使用我寫的pt-timer.h請署名“逍遙豬葛亮”。

歡迎轉載、使用、修改等等,提一提“逍遙豬葛亮”我會很高興的。百度Arduino吧裏面的是我寫的,所以不存在非法複製粘貼的問題吧?


二、以下轉自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庫來實現12、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++;
      } 
複製代碼

看上面的代碼,你會發現很多大寫的函數,其實那些都是些宏定義(宏定義用大寫是約定俗成的..),如果把這些宏都展開你就能更好的理解他的原理了:

複製代碼
    #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改爲其他條件,就是事件觸發系統了
暫時寫這麼多吧

 

三、MALC還做了個帶定時器的庫http://www.geek-workshop.com/thread-666-1-1.html

複製代碼
#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

 

四、庫的下載地址   https://yunpan.cn/cPYWwT9y6rmDv  訪問密碼 4411。

        本文章只做備忘記錄使用,這幾天和V同學做的項目中用到了這個庫,這裏受項目未公開原因,就不貼出原代碼了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章