1.SCoop
用戶指南 V1.2 (2013/01/10)
Arduino® ARM和AVR的簡單協作式調度器Scoop https://code.google.com/p/arduino?scoop?cooperative?scheduler?arm?avr/
示例:
#include “SCoop.h” // 創建調度器實例,名為mySCoop defineTask(Task1) // 標準任務定義方式,含setup和loop volatile long count; // 易改變量強製內存讀寫,確保主循環讀取最新值 void Task1::setup() { count=0; }; void Task1::loop() { sleepSync(1000); count++; }; //task1計數 defineTaskLoop(Task2) { // 快捷任務定義方式,無setup digitalWrite(13, HIGH); sleep(100); //任務中必須用sleep(ms),以便調度器介入 digitalWrite(13,LOW); sleep(100); } void setup() { Serial.begin(115200); //串口初始化為115.2Kbps mySCoop.start(); //啟動任務調度器 } void loop() { long oldcount=-1; yield(); //切換至任務調度器即運行task1/2 if (oldcount!=count) { Serial.print(“seconds spent :”); Serial.println(count); //串口輸出計數值 oldcount=count; } }
Cooperative multitasking即協作式多任務中,任務的優先級相同,沒有搶占打斷機製,適合Arduino大多數庫?接受重入調用的現實。yield()是 Arduino due Scheduler 庫的一部分,在版本>1.5 的標準 Arduino 庫中引入。基本上,當任務有空閑時調用yield()就會轉到調度程序,後者切換到下一個任務。每個任務的末尾都隱含yield()函數以便交回控製權。隻要微處?器有足夠的資源將它們保存在內存中並在您可接受的時間內執?它們,您就可以根據需要創建任意數?的任務。
1.1.任務及其堆棧
創建任務使用defineTask或defineTaskRun宏,可選第二個參數為此對象創建堆棧(字節數組),默認值AVR為 150,ARM為256。如果需要更多的局部變?,或者調用需要更多空間的函數,那麽您必須分配更多的堆棧。示例: defineTask(Task2,200)將為任務提供 200 字節的專用本地堆棧;或者在程序中用調用init(堆棧地址、堆棧的??、要使?的函數)來初始化代碼和堆棧,注意ARM程序需要8字節對齊:
SCoopTask myTask; defineStack(mystack,128) void mycode() { count+++; } void setup() { myTask.init(&myStack,sizeof(mystack),&mycode); … } 對init()方法的調用也可以像這樣與對象聲明組合在一起:
defineStack(mystack,128)
Void mycode() { count+++; }
SCoopTask myTask(&myStack,sizeof(mystack),&mycode); // implicit call to init()
Void setup() { … }
沒有簡單的方法來計算所需的最大堆棧大小,但是該庫包含一個名為stackLeft()的方法,返回堆棧中尚未使用的內存?。
一旦聲明了任務,就會創建一個對象,Arduino 環境會在進入主setup()或loop()之前在程序開頭自動調用對象“構造函數”。該對象會自動注冊在項目列表中,調度程序稍後將使用這些項目來逐個啟動和啟動每個任務(或事件或計時器)。
每個用defineTask宏定義的任務都應該有自己的setup()方法。調度程序將使用命令mySCoop.start()啟動列表中注冊的所有任務的所有setup ()。注冊的最新任務將首先啟動(與您的程序中的聲明相反的順序)。此命令應放在主 setup() 中。它還將初始化調度程序並重置用於計算時間的變?。此後,每個任務都被認為是“RUNNABLE”,以安全地進入其特定的loop()方法。
每個用defineTask或defineTaskLoop宏定義的任務也應該有自己的loop()方法。調度程序將定期啟動此方法,作為整個調度過程的一部分。如果需要,此loop()中的任務代碼可以永遠阻塞,隻要 yield()(或 sleep)方法在某個時間某處被調用。
進入和退出任務的機製由調度程序使用其yield()函數實現。從任務中調用yield()很可能會切換到列表中的下一個任務,並在到達最後一個任務時返回到調度程序。然後 Scheduler 將啟動所有掛起的計時器或事件(請參閱文檔後麵的內容)並將重新啟動一個接一個的任務。一旦執?了所有其他潛在任務(以及計時器或事件),它會將控製權交還給原始任務,就好像它是從對yield() 方法的調用的簡單返回一樣。執?隻是在之後繼續。
建議使用mySCoop.yield()從主Arduino sketch loop()係統地調用 yield(),這將強製循環在執?自己的 loop() 代碼之前首先啟動所有任務和未決事件/計時器。在SCoop.h文件開頭將其SCoopYIELDCYCLE值設為0可更改此?為,以便調度程序始終控製切換機製。使用這種方法,主循環()將被視為所有其他代碼段的控製塔,其中的代碼將具有更好的執?優先級,因為它將在每次任務切換或事件/定時器啟動時執?。
備注:SCoop庫在用戶的任務loop()結束時強製調用yield()。如果您實現自己的循環機製(例如,在任務 loop() 內的 while(1){ .. }),則程序必須在循環中的某處?時調用yield()或sleep()。SCoop庫用mySCoop.yield()覆蓋了原始的 Arduino yield()函數,所以可以在你的程序或包含的文件中的任何地方使用簡單的詞 yield();它也提供了標準 Arduino delay()的掛鉤機製,後者現在調用yield()函數以確保調度器能始終運行。
因為庫為堆棧分配了一定?的靜態內存,任務一旦開始就?應該結束!由用戶決定在任務loop()中編寫停止或啟動或等待事件的代碼,最終使用pause()和resume()。(有關動態任務,請參閱“Android 調度程序”)
1.1.1 任務中可以調用由SCoopTask基礎對象繼承的幾個方法
sleep(time) :與標準 delay() 函數相同的?為,但空閑時間用於立即將控製權交還給調度程序或執?其他掛起的任務。
sleepSync(time) :與睡眠功能相同,但睡眠時間與之前調用sleep()或sleepSync()函數同步,從而實現嚴格的周期性時間處理(“無抖動”)。
sleepUntil(Boolean) :隻需等待布爾變?變為真(然後將其設置為假),同時將控製權交還給調度程序。必須為此標誌使用volatile變?,因為狀態變更將來自另一個任務或來自主loop()本身。
sleepUntil(Boolean,timeOut) :與sleepUntil相同,但在給定的 timeOut 時間段後仍會返回。它可以用作表達式中的函數,並在超時的情況下返回 false。
stackLeft() :返回自任務啟動以來任務堆棧中從未使用過的字節數。這個函數也可以從Arduino sketch中的主循環()調用,在任務對象上下文之外,通過使用任務對象本身或全局指針引用它:
void printstack(){ SCoopEvent* ptr=SCoopFirstTask; // 全局庫類型和變? while (ptr) { Serial.println(reinterpret_cast(ptr)?>stackLeft()); ptr=ptr?>pNext; } } Void loop() { printstack(); … or … Serial.println(Task1.stackLeft()); }
1.1.2 跨任務對話和變量類型
編譯器優化經常將變?設為局部變?或使用臨時寄存器。為了跨任務傳遞信息或在多線程程序中使用變?(例如,一個任務正在寫入,另一個任務正在讀取),我們必須將公共變?聲明為volatile,這會強製編譯器每次都通過讀寫內存來訪問它。
為了簡化變?聲明,SCoop 庫為int8,16,32、uint8,16,32和Boolean預定義了一些類型,在原始 Arduino 類型名稱前麵加上 “v”並刪除“_t”,例如:
vui16 mycount; // exact same as volatile uint16_t mycount vbool OneSecond = false; exact same as volatile Boolean OneSecond 備注:末尾的“_t”已被自願刪除,但被認為是對傳統類型命名約定的偏離......
1.2.定時器
該庫提供了一個互補的SCoopTimer對象,可用於創建將由調度程序編排的周期性操作。您可以根據需要實例化任意多個計時器,甚至可以將它們臨時聲明為函數或任務中的本地對象。當您在單個原子yield()操作中進入和離開計時器時,計時器?需要堆棧上下文;他們使用Arduino的正常堆棧。計時器必須很快,因為它們在任何情況下都無法將控製權交還給調度程序(本地 yield() 方法已禁用)。定時器看起來像一個 MCU 定時器中斷,但可以從中調用任何現有函數或庫,而沒有係統崩潰的風險??
1.2.1 定時器的定義
使用宏defineTimerRun(),參數1是定時器對象的名稱,可選的參數2是周期,實例:
defineTimerRun(myTimer,1000){ ticSecond=true; countSecond++ }
此計時器的代碼隱式附加到myTimer::run()方法。計時器注冊在與任務相同的列表中,在調用yield()期間由調度程序觸發。一旦自上次調用以來經過的時間達到定義的時間段,將執?一次run()方法,它必須快速且無阻塞。
如果計時器需要時間來完成(比如幾毫秒),建議改用任務,在loop()方法的最開始使用sleepSync()函數,並在阻塞或緩慢的部分調用yield()或yield(0)。以下代碼將給出與 timer(1000) 定義完全相同的?為:
defineTaskLoop(myTimer,100) { sleepSync(1000); ticSecond=true; countSecond++ }
備注:在這個例子中,我們通過強製第二個參數的值為100來減少默認堆棧大小,因為任務loop()?需要那麽多,它?調用任何其他函數並且沒有局部變?。
1.2.2 啟動和監控定時器
調用mySCoop.start()將初始化所有已注冊的計時器,如果時間段作為defineTimerRun宏的第二個參數提供,則將啟用它們。如果?是,則由程序稍後通過調用方法schedule(time)來初始化它。以下是修改或監視計時器的方法:
schedule(time) :啟用計時器並準備每“time”毫秒啟動一次 schedule(time, count) :與schedule(time)相同,增加了最大執?次數。 getPeriod() :返回為該計時器的定義周期。 setPeriod() :設置定時器周期。 getTimeToRun() :返回下一次執?計時器 run() 方法之前的時間(以毫秒為單位)。
1.2.3 定義計時器的其他方法
defineTimer(T1,optional period) 可以為這個對象聲明一個 T1::setup() 和一個 T1::run() 方法,如果我們想在這個對象中添加一些設置代碼,而?是將它們寫在主 setup() 中,這很有用,但與V1.1不兼容,示例:
defineTimer(myTimer,1000) void myTimer::setup() { count=0; } void myTimer::run() { count++; }
defineTimer(myTimer,1000) void myTimer::setup() { count=0; } void myTimer::run()
另外2個宏是defineTimerBegin(event[,period]) 和defineTimerEnd(event)。請參閱庫中的示例2。
1.3.事件 Event
該庫提供了一個補充對象SCoopEvent ,可用於處?調度程序在外部事件或觸發器上執?的代碼。例如,可以從中斷 (isr, signal) 觸發事件,但相應的run()代碼將僅由調度程序在中斷上下文之外執?。這使得能夠編寫複雜的事件處理,調用庫函數,而無需編碼硬件中斷所需的關鍵性。
與SCoopTimer對象一樣,SCoopEvent沒有堆棧上下文,並且應該盡可能快以保持調度流暢。如果事件需要很長時間才能完成,則應將其聲明為永久任務,使用sleepUntil()方法等待易變標誌。事件聲明和使用示例:
defineEventRun(myevent){ Serial.println(“trigger received”); } isr(pin) { myevent.set(); } // or myevent=true;
事件對象隻有一個公共方法來設置觸發標誌:
set() :將觸發標誌設置為真。該事件將由調度程序通過調用 yield() 啟動。
set(value) :設置觸發值,如果為false,則?麽也?會發生。如果為真則相當於 set()
事件觸發標誌也可以通過賦值直接設置,如“myevent=true”,因為庫為該對象重載了標準“=”運算符。
defineEvent(event)可用於定義event::setup()和event::run(),就像定時器一樣。與V1.1?兼容!
另外2個宏是:defineEventBegin(event)和defineEventEnd(event)。請參閱庫中的示例2。
1.4.先進先出緩衝區 Fifo
SCoop庫的補充對象SCoopFifo和defineFifo宏用於管理先進先出緩衝區,適合於字節、整數、長整型或任何類型的 256字節以下的結構數據。這對於使用生產者?消費者或發送者?接收者模型以同步方式在任務、事件或計時器與另一個任務之間交換數據非常有用(任務之間?需要同步)。
下麵是一個示例,定義了一個包含 100 個整數的 fifo 緩衝區,並在執?模擬采樣的計時器和使用它們監視變化的任務之間使用它:
defineFifo(analogBuf,int16_t,100)
int16_t A,B;
defineTimer(anaRead,10) // 每10ms運行一次anaRead()
void anaRead::run() { A=analogRead(1); analogBuf.put(&A); } //每次讀取一個采樣值並存入Fifo
defineTaskLoop(task1) {
while(analogBuf<32) yield(0); // 等待Fifo緩衝區中有32個數據
int16_t avg=0;
for (int i=0; i<32; i++){
analogBuf.get(&B); avg += B; // 讀取Fifo中的32個數據並取其和
}
avg /= 32; yield(); // 算得32個數據的平均值
Serial.print “average analog value for 32 samples = “); Serial.println(avg);
}
SCoopFifo還提供了以下方法:
put(&var) :將變?的值添加到緩衝區。如果結果成功則返回 true,如果緩衝區已滿則返回 false。
putChar(val)、putInt(val)、putLong(val)可用於直接將給定值添加到緩衝區中,而?需要中間變?。
get(&var) :從緩衝區中取出舊值並將其存儲在傳遞的變?中。如果成功則返回 true,如果緩衝區為空則返回 false。
flush() :清空緩衝區並根據聲明返回緩衝區的大小(項目數)。
flushNoAtomic():與flush相同,但?涉及中斷。?應與中斷服務程序一起用。
count() :返回緩衝區中可用的樣本數?。也可在整數表達式中使用對象的名稱,該表達式將返回與方法count() 相同的值。
1.5.Virtual timer/delay: SCoopDelay和SCoopDelayus
類SCoopDelay用於延遲或超時測?,有點像 TimerDown (容後敘)。自動重新加載是選項,如果使用它則定時/延遲會自動啟動。典型用法:
SCoopDelay time; time = 10;
while (time) yield(); // this launch yield() during 10ms
SCoopDelay t10seconds(10000);
If (T10seconds.reloaded()) { /* do something every 10 seconds */ }
SCoopDelay對象的幾個方法:
set(time) :定義延遲的開始時間。然後延遲將開始倒計時到 0;
get() :返回延遲的值。如果延遲已過,則結果為 0。
add(time) : 給延遲增加一定的時間
sub(time) : 減去一定?的延遲時間
這4個方法也可以通過直接使用SCoopDelay對象,由運算符重載在x=delay或delay=x或delay+=x或delay?=x等表達式中透明的使用。
elapsed() :如果延遲結束則返回 true。
reloaded() :與 elapsed 相同,但如果已定義,則使用重新加載值自動重新啟動延遲。
setReload(time) :預定義並附加一個重載值到對象。與在對象聲明期間提供的一樣。
getReload() :返回附加到對象的重新加載參數的值。
reload() :將重新加載值添加到 SCoopDelay 對象。
initReload() :設置 SCoopDelay 對象及其重載值。
所有值都是int32類型,如SCoop.h文件開頭的SCDelay_t變?所定義。
通過擴展,該庫還提供了一個SCoopDelayus對象,它是相同的,但使用的參數以微秒為單位。這些值對於AVR是 int16,對於ARM是int32,由 SCoop.h 文件開頭的 micros_t 變?定義。
1.6.時間計數器:TimerUp和TimerDown
用於完全獨立於調度程序來處?時間的遞增和遞減計數。可將 tic 計數的時基指定為 1 毫秒到 30 秒 (int16)。可以聲明的定時器數?沒有限製。
1.6.1 TimerUp遞增計數器
從 0 到要在對象聲明中定義的最大值(可以是 0)。一旦達到最大值,計數器將返回 0,並且可以使用 rollOver()方法監視翻轉事件以啟動操作。例子:
#include TimerUp
counterMs(0); // ?麽都?做,需要進一步初始化
TimerUp myMillis(1000); // 每 1 毫秒從 0 計數到 999
TimerUp mySeconds(3600,1000); // 每 1000 毫秒從 0 計數到 3599
void loop() {
if (myMillis.rollOver())
Serial.print(“又過了一秒”)
if (mySeconds.rollOver())
Serial.print(“又過了一小時”)
}
該對象也可以直接用在表達式或賦值中:
TimerUp myTime(1000);
??
void loop() {
if (skip)&&(myTime > 500)
mytime = 800; // 將讀取並強製定時器
在定義rollover時的設定值可以在此後用setRollOver(timeMax)修改。
1.6.2 TimerDown遞減計數器
從最大值遞減到 0。一旦遞減計數到 0,計數器保持 0 值,直到程序強製它為另一個值。示例:
#include
#include
TimerUp tic(100,10) //每 10ms 從 0 遞增到 99
TimerDown msLeft(0); // 每 ms 倒計時一次
TimerDown secondsLeft(3600,1000); // 每 1000 毫秒從 3599 計數到 0
??
void loop() {
while (secondsLefts) {
if (tic.rollOver())
msLeft=1000; // 重新啟動這個計數器
Serial.print(secondsLeft); Serial.print(“ seconds and “);
Serial.print(msLeft); Serial.println(“ ms before end”);
}
}
1.6.3 TimerUp和TimerDown對象的相關方法
Init(time, timeBase):用給定的 timemax 和給定的時基初始化計數器。例如,counter.init(5,1000) 將使用對應於5秒的值初始化計數器(向上或向下)。若是TimerUp將從0計數到4;TimerDown將從5計數到 0。
set(time) :在計數器中強製一個新值。這等於給counter=100這樣的賦值;
reset() :隻是強製計數器為 0。同時清除 TimerUp 對象的翻轉標誌。
get() :返回定時器的值。計時器也可以直接在整數表達式中讀取,例如 x=counterMs;
pause(time) :計時器將保持其實際值直到恢複。
resume() :如果計時器暫停,則它會恢複並從該值開始重新計數。
1.7.庫編譯選項
7.1 數字輸入輸出和時間過濾
SCoop 庫提供了一個獨立的組件,通過使用對象的強大功能和運算符重載提供的可能性,以全麵的方式定義、使用和過濾數字輸入和輸出。IOFilter.h庫的提供主要是為了消除反彈或過濾和/或計算任何輸入的轉換,並具有使用調度程序編排和同步計時器的好處。但是像TimerUp和TimerDown一樣,這個庫也可以完全獨立於SCoop調度器使用,然後時間過濾就變成異步的並且依賴於用戶代碼。使用示例:
#include
#include
Output led(13);
Input switch1(1);
InputFilter button2(2,150,100); // 150ms before HIGH, 100ms before LOW
defineTimer(debounce,20) // will check all the inputs registered every 20ms
void debounce::setup() { }
void debounce::loop() { IOFilter::checkAll(0); }
…
void setup() { led = LOW; }
void loop() {
if (button1 || (button2.count==2)) {
button2.reset(); led=HIGH;
}
}
可以在沒有SCoop庫的情況下使用相同的示例,隻需刪除“scoop.h”文件和“debounce”計時器定義。每次讀取 button2輸入時,過濾和計數序列將異步發生。用戶代碼可以通過在程序的某個地方調用方法checkAll()來強製定期同步檢查過濾(參見下麵的解釋)。
1.7.2 IOFilter.h庫中可用的方法和類
Input myInput(pin) : 定義 myInput 為 INPUT 並準備使用 digitalRead(pin)
get() :使用 digitalRead(pin) 讀取引腳的即時值
myInput 可用於整數表達式 (x=myInput)
readAll() :V1.2此方法仍在開發中,但將用於強製庫讀取列表中聲明和注冊的所有輸入對象,以?在特定時間同步輸入值。其後使用get()或整數表達式讀取引腳時將返回最近readAll()方法存儲的值。這通常在工業過程中或在開發作為 PLC 循環工作的程序時很有用,從而實現類似於梯形圖或FBD語言的編程風格。
Output myOut(pin) : 將myOut定義為OUTPUT並準備使用 digitalWrite(pin)
set(value) :基本上執? digitalWrite(pin, value)。存儲該值以供進一步閱讀。
get() :返回通過 set(value) 命令寫入引腳的最後一個值。
myOut 也可用於整數表達式 (x=myOut) 或賦值 (myOut=1)
writeAll() :此方法是一個尚未實現的占位符,在此版本中仍處於開發階段,但將用於強製庫寫入列表中聲明和注冊的所有輸出對象,以?在特定時間同步輸出?新。然後使用 set() 或通過在表達式中分配對象來寫入輸出,隻會將新值寫入內存,直到用戶調用 writeAll() 為止。至於 readAll(),這通常在工業過程中或在開發作為 PLC 循環工作的程序時很有用,其中輸入在循環開始時讀取,輸出在循環結束時寫入一次。
InputFilter myInputF(pin, timeOn, timeOff) :將 myInputF 定義為 INPUT 並準備在輸入升至高 (timeOn) 和輸入降至低 (timeOff) 時使用帶時間過濾的 digitalRead(pin)。隻有當引腳在 timeOn 或 timeOff 定義的時間內保持此狀態時,該值才會設置為高或低
get() :一旦應用了過濾約束,就返回輸入的值。
check():啟動一個過程以根據時間過濾約束驗證輸入。如果自上次調用 check() 以來引腳值已從低變為高,則還增加附加計數器。
備注:check() 方法總是在用戶代碼調用 get() 方法或讀取整數表達式中的輸入時啟動一次。如果?使用調度程序庫,這保證了時間?改和計數事件的同步處?。
count() :返回輸入上的升起轉換數,由 check() 方法檢測到。
reset() :重置連接到輸入的計數器。 Des ?影響時序檢查約束。
checkAll() :為所有已聲明並自動注冊的對象(ptrInputFilter)啟動 check() 方法。此方法將首先檢查自上次調用以來花費的時間是否超過默認閾值 10 毫秒,以避免在?需要的地方浪費時間。該值可以在 IOFilter.h 文件的開頭?改。
checkAll(time) :提供與 checkAll() 相同的?為,但例程開始時的時間檢查器將使用給定的時間參數而?是默認時間參數。使用 0將強製執? checkAll();
備注:checkAll() 和 checkAll(time) 被聲明為靜態的,因此可以由用戶代碼調用而無需引用正式對象,隻需強製作用域 (IOFilter::checkAll())。
readAll() :占位符尚未實現。與 Input 對象的 readAll() ?為相同。
InputMem myBit(addr,bit) 通過從提供的內存地址中提取位,將 myBit 定義為返回 0 或 1 值(低/高)的輸入。這有助於定義附加到內存中某個位的綜合輸入變?,例如,由 Modbus 協議返回並存儲在 RAM 區域中給定內存地址的“線圈”。
get() :返回內存中位的值。
該對象還可以在整數表達式 (x=myBit) 中使用,因為庫重載了運算符。
OutputMem myBit(addr,bit) 將 myBit 定義為與提供的內存地址中的位相對應的輸出。這有助於定義中間輸出變?,或定義附加到存儲在 RAM 區域中的內存地址的輸出,如 Modbus 協議中使用的“線圈”對象。
set(value) :根據傳遞的值強製該位為 0 或 1。
get() :返回內存中位的值。
該對象還可以用於整數表達式(x=myBit 或 myBit=y),因為庫重載了運算符和賦值邏輯。
備注:打算在 SCoop 框架的?高版本中為模擬輸入和 PWM 輸出創建相同類型的庫,名稱為IOAnalog.h
1.8.更多技術性的東西
此圖片表示對象類和一些實例,以及它如何與Arduino程序中的標準setup()和loop()函數一起工作。
1.8.1 時序測量
通過在SCoop.h文件開頭定義的變?SCoopTIMEREPORT ,該庫將提供額外的變?來控製任務的進出時間。
如果預定義變?的值N設置為 0,則這些變?都?可用,並且庫的代碼被優化以減少調度程序的開銷時間和大小。如果值N設置為 1、2、 3 或 4(ARM 最多為 7),則以下4個變?將在 SCoop 和 SCoopTask 對象中可用:
SCoopTask.yieldMicros :給出調度程序最後N個周期中任務花費的總時間。
mySCoop.cycleMicros:整個周期的總時間,包括最後 N 個周期在主循環()中花費的時間。將這 2 個數字相除,就是cpu用於任務的比率。
cycleMicros:一旦除以N表示調度程序的平均響應能?。任何計時器或事件都將在此時間窗口內啟動(除非任務在同一時刻需要?多時間)。
SCoopTask.maxYieldMicros:提供自調度程序啟動以來 yieldMicros 達到的最大值。這可用於確定某個任務在某個時間點是否消耗了比預期更多的時間。該變?可在 R/W 中訪問,並可由主程序重置。
mySCoop.maxCycleMicros:提供自調度程序啟動以來cycleMicros達到的最大值。更頻繁地調用yield()肯定會降低cycleMicros和maxCycleMicros之間存在大差異的風險。
備注:通過在SCoop.h文件的開頭將SCoopTIMEREPORT的值從1更改為4,可以定義用於累積時間的周期數,從2(默認)、4、8到16。對於AVR程序,這些變?隻有16位,可能會溢出。對於ARM,可以通過將這些值擴展到5、6或7來累積多達128個周期,而?會對性能產生任何影響。
1.8.2 Quantum 強製執?每個任務花費的時間或CPU百分比的方法
每個任務對象都提供一個名為quantumMicros的變?,用於定義調度程序在切換到另一個任務之前應該在此任務中花費的時間?。這保證了任務獲得一定數?的 CPU 時間來繼續,相對於整個循環時間。初始化對象時,此變?根據程序設置為默認值(SCoopDefaultQuantum):ARM為200us,AVR為400us。如果需要,可以在程序中動態?改此值(例如myTask.quantumMicros=700;),最好是在任務setup()方法中。這將影響在此任務中花費的默認時間,因此也會影響整個周期的長度。這是一種將一定數?的CPU時間(“百分比”)強製用於任務的聰明方法。但是因為模型是Cooperative,代碼沒有提交。
備注:當任務正在使用sleep()或sleepUntil()或sleepSync()時,如果?滿足條件,它幾乎會立即切換回調度程序。因此,任務花費的時間在yieldMicros變?中被視為 0,這並?完全正確,因為任務至少花費了檢查其條件所需的時間。
用戶程序還可以在微秒內將一個參數強製傳遞給SCoopTask::yield(x),該參數將僅用於此yield(x)調用,而?是 quantumMicros。例如,寫入“yield(100)”將要求調度程序檢查我們是否已經在該任務中花費了100uS。如果是,那麽調度器將切換到下一個;如果沒有,它將立即從yield(100)返回。類似的用法,寫入“yield(0)”會強製調度程序立即切換到下一個任務。
mySCoop.start(cycleTime)也可用於將作為參數提供的cycleTime除以注冊的任務數(主循環 +1)來覆蓋默認任務quantumMicros。如果任務quantumMicros值為0,那麽SCoop庫正在優化代碼,以便係統地切換到下一個任務,而?控製其花費的時間。這減少了切換時間,因為yield()?會調用micros()函數來與quantumMicros進?比較。這可以通過在主程序setup()中編寫mySCoop.Start(0)或通過更改SCoop.h文件開頭的預定義變? SCoopDefaultQuantum 來實現。
mySCoop.start(cycleTime,mainLoop)可以將特定時間(甚至0)分配給mainloop。備注:如果mainLoop?為 0,則調度程序將始終調整主循環所花費的時間(加或減)以盡?保證恒定的cycleTime!
1.8.3 原子代碼
由於yield()函數現在嵌入到Arduino>1.5庫的許多函數中,因此可能需要強製一段代碼成為“原子的”並且?被調度程序中斷。想象一個示例,您使用庫寫入一個芯片寄存器,並且在發送新寄存器之前需要延遲3毫秒等待3毫秒。在這種情況下,該庫提供了一個名為mySCoop.Atomic的8位全局變?,您隻需在敏感代碼的開頭遞增 (mySCoop.Atomic++),然後在此代碼部分的末尾遞減 (mySCoop.Atomic??)。當mySCoop.Atomic變?包含非空值時,yield 函數就會返回。
從 V1.2 開始,可以使用預定義的宏SCoopATOMIC { ?}(與yieldATOMIC { ? } 相同)。這將在方括號內聲明的塊代碼的開頭和結尾插入正確的代碼。
1.8.4 重入保護
我們想要保護一段代碼的另一種情況是當一個函數調用 yield()(就像以太網庫中的一些函數)但該函數?可重入並且必須防止來自其他任務的多次調用。為此,引入了一個特殊的宏調用SCoopPROTECT(),與yieldPROTECT() 相同。隻需在函數的開頭使用此宏來保護它,這將自動插入一些靜態?失性標誌和一些代碼來檢查該標誌是否已由先前的調用設置。
如果是這樣,代碼將調用yield()直到標誌被重置。標誌重置自動插入到方括號中的bloc代碼末尾,其中使用了 SCoopPROTECT()。可以在塊代碼結束之前使用SCoopUNPROTECT()來提前重置標誌。
1.8.5 性能
對於搶占式操作係統,SCoop協作調度程序的性能取決於調度程序檢查時間和切換任務上下文所花費的時間。SCoop使用優化的代碼和例程,以在 yield()方法中花費盡可能少的時間。
如果自調度器啟動以來,在任務中花費的時間還沒有達到“quantumMicros”,那麽yield()將在任務中盡快返回。如果時間結束(AVR默認為200us,ARM為400us),那麽yield()將花費更多時間準備,然後切換到下一個任務。
SCoop使用標準的Arduino micros()函數來評估花費的時間。為了在Arduino UNO上獲得更好的性能,已經為 AVR 重寫了一個特定的16位微函數,利用了Teensy核心中使用的大多數想法。有關更多信息和版權,請參閱源代碼。
協作式調度的另一個關鍵區別是用戶代碼必須非常頻繁地且在有空閑時間或阻塞代碼段就調用yield(),這是保證任務、計時器和事件之間平滑切換的唯一方法。因此,在 yield() 中花費的時間相對比搶占式操作係統更重要,後者的任務調度由周期性中斷自上而下觸發,比如每毫秒一次。而SCoop在某些情況下,如果任務正在做非常簡單的事情(例如使用sleepUntil()),則yield()可能會非常頻繁,大約每10us一次,由此花費在yield()上的總時間將是以上搶占式操作係統的100倍!但這並?意味著SCoop?好,隻是您的任務?需要這麽多的CPU能?,因此將轉移到yield()。為此引入了時間片的概念,以確保我們在切換到另一個任務代碼之前繼續執?任務代碼。這樣一來,花在 yield() 上的相對時間就減少了,並且保證任務有一定數?的 CPU 資源,這對於需要像fft或采樣計算的應用程序很重要。
為了評估整體性能,SCoop庫提供了一個名為performance1的示例。它顯示了在沒有調度程序的情況下我們可以做多少“計數”,然後是3個具有大量度和0?度的任務。我們可以看到在yield()中花費的時間對10秒內完成的計數有多少影響。通過將此差異除以對yield()的調用次數(AVR為32或ARM為128),我們可以計算出每個yield() 花費的平均時間?
1.8.6 對象上下文與主要的Arduino程序
SCoop庫強製用戶將任務定義為具有相關setup()和loop()函數的對象,這看起來很酷,但是當程序變大時,這會帶來一些約束和限製。為了可讀性,任務可能分布在由task loop()調用的多個函數中:
void printMyStack() { … }
defineTaskRun(task1){ printMyStack(); x=x+1; sleep(100); }
在這種情況下,外部函數如printMyStack無法訪問 SCoopTask對象中的方法,如sleep()或stackLeft()(它們未聲明為靜態)。yield()例外,因為該庫定義了一個全局的yield(),它可以在任何地方使用。
為了解決這個問題,可以使用對象本身的名稱來引用該方法,例如task1.sleep(10),但是如果該函數對多個任務
是通用的,那麽代碼必須使用一個全局指針mySCoop.Task,它始終包含一個指向正在執?的任務對象的指針。所以在這種情況下,函數printMyStack()應該包含以下類型的代碼:
void printMyStack() { Serial.println(mySCoop.Task?>stackLeft()); mySCoop.Task?>sleep(100); }
1.8.7 “Android Scheduler” V1.2 New –> SchedulerARMAVR.h
基於Atmel ARM芯片的Arduino DUE板的Arduino 1.5庫提供了一個Scheduler.h庫,這是一個非常基本的調度程序,通過全局 yield() 方法進?任務上下文切換。原始源代碼來自 Android 團隊/項目,這就是為?麽我簡單地將這個庫稱為“Android Scheduler”。所以呢?
Scheduler.h的升級版SchedulerARMAVR.h隨SCoop庫一起提供,該版本現在與ARM和AVR都兼容!我通過使用AVR堆棧切換擴展原始的Scheduler.h庫將代碼移植到 AVR,盡管功能?佳,但結果非常好。因為每個用戶都會質疑使用哪種解決方案,所以我想透明地提供此文件作為您項目的可能替代方案。在本用戶指南中包括它就像提供本指南的硬拷貝和鍍金封麵,我?會考慮現在的金價,因為我把它綠色化了。
這個調度程序非常快,但?會檢查任何時間片或在任務中花費的時間,這可能會給您的應用程序帶來非常糟糕的結果。此外,該庫?為Fifo、時間測?或計時器和事件提供任何支持功能。沒關係,SCoop可以彌補這些。
這個調度程序的特點是可以在主程序中隨時啟用動態任務,隻需調用startLoop(myLoop,stackSize),如您在 multipleBlink 原始示例中所見。然後代碼將使用標準的malloc和free函數從堆中分配任務對象及其堆棧。這種方法通常被認為對於內存?足的小型係統來說是有風險的,並且對於非專家用戶來說非常敏感。該庫還提供了任務終止的可能性。然後內存被動態釋放,在堆heap中留下空?。
您可能會試用它並且會對這個庫感到滿意,所以將SCoop帶到幾乎相同的級別並使其兼容非常重要,這樣您就可以現在或以後移植到它。當然性能上是有差別的。使用SCoop在AVR上的最佳上下文切換時間是15us,而使用移植的“Android Scheduler”則降至 9us。但是,SCoop最後可能會?快,因為如果?花費時間片,基本的yield()調用將隻需要2.5us。
換句話說,我已經決定在 SCoop 庫中添加 2 個方法,這兩個方法提供了使用相同語法定義動態任務的完全相同的可能性:可以通過調用mySCoop.startLoop(myLoop, myStack)或Scheduler.startLoop(myLoop,myStack)並稍後通過調用SCoopTask::kill()終止此任務!為了保留指向此任務的指針,使用它的好方法是:
SCoopTask* ptrAndroid; ptrAndroid ptrAndroid = Scheduler.startLoop(myLoop,256); mySCoop.start(); // 或 Scheduler.start(); while (1) { yield(); if (something) ptrAndroid->killMe(); }
從技術上講,這是可?的而且您隻能使用這種方式將任務添加到SCoop。但我個人的選擇已經完成 。似乎也有可能(根據 multipleBlink 示例的成功)立即替換現有項目中Android Scheduler的使用,隻需替換:
#include 為 #include ,當然這個還有待進一步體驗。
一個先決條件是在主setup()結束時啟動Scheduler.start(),否則SCoop將?會啟動 (當 SCoopANDROIDMODE>=1時“Scheduler.”與 “mySCoop.”相同)。最後但同樣重要的是,僅當您在SCoop.h頭文件裏將預定義變? SCoopANDROIDMODE更改為2時,kill()才會起作用。原因是我?想用kill()帶來的可能約束,汙染主要“SCoop::yield()”中的一些關鍵代碼......
在閱讀了 20 頁長篇之後,是時候體驗所有這一切並在 Arduino 論壇中報告任何反饋或錯誤了!多謝你們。
C:Program Files (x86)Arduinohardwarearduinoavrplatform.txt
2. FreeRTOS
https://blog.csdn.net/jiyotin/article/details/118494109
3. TaskScheduler
https://blog.csdn.net/weixin_44035986/article/details/123412711
https://github.com/arkhipenko/TaskScheduler https://www.electrosoftcloud.com/en/arduino-taskscheduler-no-more-millis-or-delay 推薦,支持常見的多種uP如Uno/Nano/Attiny85/ESP32/nRF52/STM32/MSP43x。支持分層式優先級如兩級,每級中多個任務。
https://www.instructables.com/Simple-Multi-tasking-in-Arduino-on-Any-Board/
4. Blink Without Delay
https://docs.arduino.cc/built-in-examples/digital/BlinkWithoutDelay
unsigned long previousMillis = 0; // last time LED was updated
const long interval = 1000; // interval ms at which to blink
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
// save the last time you blinked the LED
previousMillis = currentMillis;
// if the LED is off turn it on and vice-versa:
if (ledState == LOW) {
ledState = HIGH;
} else {
ledState = LOW;
}
// set the LED with the ledState of the variable:
digitalWrite(ledPin, ledState);
}
void sos()
{ digitalWrite(LED, HIGH); // Turn on the LED
for (int i=0; i<3; i++){
tone(Buzzer, note, 100); // pin,freq,duration
delay(200);
noTone(Buzzer);
}
delay(200);
digitalWrite(LED, LOW); // Turn off the LED
for (int i=0; i<3; i++){
tone(Buzzer, note, 300);
delay(400);
noTone(Buzzer);
}
delay(200);
digitalWrite(LED, HIGH); // Turn on the LED
for (int i=0; i<3; i++){
tone(Buzzer, note, 100);
delay(200);
noTone(Buzzer);
}
delay(600);
}
5. Simulator
https://docs.wokwi.com/parts/wokwi-attiny85 https://docs.wokwi.com/parts/wokwi-arduino-uno