第 12 章 - 時間參數的應用
上次更新日期 2023/01/04
有玩過 arduino 搭配感測器的朋友大概會知道,許多的感測器讀取,其實是需要花費時間的!舉例來說,溫溼度計 (dht11) 的讀取, 就可能得要花費個數秒鐘。此時,如果使用 delay 的函數,那麼除了讀取的時間之外,還得要額外延遲數秒鐘,所以,跟我們預期的固定秒數顯示, 可能會有些許的落差。所以,比較好的程式寫法,可能需要用到實際 arduino 的規範時間較佳!另外,這組時間參數, 其實會過期 (overflow)...所以,定期將 arduino 重新開機 (reset),可能也是個不錯的思考方向!
學習目標:
- 了解 arduino 時間參數限制
- 使用 arduino 實際時間做迴圈限制
12.1: arduino 時間參數與應用
過去因為簡單的應用,因此我們在設計迴圈時,通常會使用 delay(毫秒數) 來進行延遲的規範。不過,當程式碼進入 delay 時, 由於是一個停止的指令,所以此時 arduino 上面如果有其他資訊要處理,那麼這些動作就會被延遲到下一輪的迴圈內! 而沒有辦法立即動作!這就造成許多的困擾!因為程式會卡住啊!真討厭!
- arduino 紀錄的時間
實際上, arduino 因為小而美,所以大部分的記憶體與珍貴的儲存空間,都用在處理主要程式碼部份,因此,時間參數就只能使用到少少的資訊量。 即便如此,arduino 還是使用了 millis() 函數,紀錄了從 arduino 開機到當下的時間點!使用的單位是毫秒~回傳的格式為 unsigned long 整數, 數值會從 0~4294967295 之間!這是因為 long 格式使用了 32 位元,因此 2^32 就得到 4294967296,數值由 0 開始,就得到這個數據。 而這個數據轉成天數,計算: 4294967295/1000/(60*60*24) 就得到 49.71 左右,因此,這個值大概在 50 天內,就會溢位 (overflow)。 也因為如此,如果你的 arduino 強大到超過 50 天不用關機,那使用到 millis() 的程式碼,還是可能會掛掉...所以才需要防呆!
另外,除了毫秒的 millis 之外,其實還有個 32 位元的微秒函數,也就是 micros() 這個函數,這個函數數值也是從 0~4294967295, 只是到達 4294967296 時,會自動歸零~所以沒有溢位的問題!只是因為是微秒,因此需要 micros()/1000000 才會是秒鐘, 所以,計算之下 4294967295/10000/(60) 大概是 71.58 分鐘,亦即每隔 72 分鐘左右,這個數據會歸零的意思!如果短時間內要使用時間參數, 似乎這個數值比較準確一點點。
- 應用方式
基本上,在工作最後面,加入一個時間刻度紀錄,然後都用實時時間與剛剛紀錄的刻度時間做比較,如果超過一定時間時, 才進行實際工作。舉例來說,我們將 code-led-2 程式碼抓出來,先給改成如下的模樣,增加時間輸出,看看資料變化:
int led[] ={5, 6, 7, 8}; int i; int j = 0; void setup() { // put your setup code here, to run once: for (i=0; i<=3; i++) { pinMode(led[i], OUTPUT); } Serial.begin(9600); } void loop() { // put your main code here, to run repeatedly: for ( i=0; i<=3; i++ ) { digitalWrite(led[i], LOW); } digitalWrite(led[j],HIGH); j=j+1; if ( j>=4 ){ j = 0; } delay(1000); Serial.println(millis()); }
開始執行之後,你會發現到時間參數會漸漸改變,雖然不至於一口氣增加太多問題,這是因為我們的程式碼太過簡單, 所以時間延遲不太會有問題的緣故。鳥哥擷取剛開始的數據給大家參考一下:
11:15:28.108 -> 999 11:15:29.136 -> 1999 11:15:30.113 -> 3000 11:15:31.136 -> 3999 11:15:32.114 -> 5000 11:15:33.133 -> 6000 11:15:34.113 -> 7001 11:15:35.141 -> 8001 11:15:36.126 -> 9000 11:15:37.143 -> 10001 11:15:38.122 -> 11001 11:15:39.149 -> 12002 11:15:40.132 -> 13002 11:15:41.111 -> 14002 11:15:42.123 -> 15002
現在,將剛剛的檔案另存新檔成為 code-millis-1,之前的版本是每點亮一顆燈,就會延遲 1 秒鐘!現在,我們用底下的方式來改寫:
int led[] ={5, 6, 7, 8}; int i; int j = 0; long Time = millis(); void setup() { // put your setup code here, to run once: for (i=0; i<=3; i++) { pinMode(led[i], OUTPUT); } Serial.begin(9600); } void loop() { // put your main code here, to run repeatedly: if ( millis() - Time >= 1000 ) { for ( i=0; i<=3; i++ ) { digitalWrite(led[i], LOW); } digitalWrite(led[j],HIGH); j=j+1; if ( j>=4 ){ j = 0; } Time = millis(); Serial.println(millis()); } }
如前所述,我們在工作的結尾增加時間刻度紀錄,然後開頭使用 millis() 進行時間判斷而已,輸出的秒數就非常準確! 這樣才有特定迴圈的記憶功能感覺啊~而不是透過 delay 啊!
- 延遲比較大的 dht11 測試
上述的程式碼時間誤差你可能覺得沒什麼!那我們來看看 dht11 這個感測器的使用好了!先將 code-dht-1 程式碼拿出來, 增加幾個資訊成為如下:
#include <Adafruit_Sensor.h>
#include <DHT.h>
#include <DHT_U.h>
#define DHTPIN 7
#define DHTTYPE DHT11 // DHT 11
DHT_Unified dht(DHTPIN, DHTTYPE);
uint32_t delayMS;
void setup() {
Serial.begin(9600);
dht.begin();
Serial.println(F("DHTxx Unified Sensor Example"));
sensor_t sensor;
dht.temperature().getSensor(&sensor);
Serial.println(F("------------------------------------"));
Serial.println(F("Temperature Sensor"));
Serial.print (F("Sensor Type: ")); Serial.println(sensor.name);
Serial.print (F("Driver Ver: ")); Serial.println(sensor.version);
Serial.print (F("Unique ID: ")); Serial.println(sensor.sensor_id);
Serial.print (F("Max Value: ")); Serial.print(sensor.max_value); Serial.println(F("°C"));
Serial.print (F("Min Value: ")); Serial.print(sensor.min_value); Serial.println(F("°C"));
Serial.print (F("Resolution: ")); Serial.print(sensor.resolution); Serial.println(F("°C"));
Serial.println(F("------------------------------------"));
// Print humidity sensor details.
dht.humidity().getSensor(&sensor);
Serial.println(F("Humidity Sensor"));
Serial.print (F("Sensor Type: ")); Serial.println(sensor.name);
Serial.print (F("Driver Ver: ")); Serial.println(sensor.version);
Serial.print (F("Unique ID: ")); Serial.println(sensor.sensor_id);
Serial.print (F("Max Value: ")); Serial.print(sensor.max_value); Serial.println(F("%"));
Serial.print (F("Min Value: ")); Serial.print(sensor.min_value); Serial.println(F("%"));
Serial.print (F("Resolution: ")); Serial.print(sensor.resolution); Serial.println(F("%"));
Serial.println(F("------------------------------------"));
// Set delay between sensor readings based on sensor details.
delayMS = sensor.min_delay / 1000;
}
void loop() {
// Delay between measurements.
delay(delayMS);
Serial.print(delayMS);
Serial.print("; ");
Serial.println(millis());
// Get temperature event and print its value.
sensors_event_t event;
dht.temperature().getEvent(&event);
if (isnan(event.temperature)) {
Serial.println(F("Error reading temperature!"));
}
else {
Serial.print(F("Temperature: "));
Serial.print(event.temperature);
Serial.println(F("°C"));
}
// Get humidity event and print its value.
dht.humidity().getEvent(&event);
if (isnan(event.relative_humidity)) {
Serial.println(F("Error reading humidity!"));
}
else {
Serial.print(F("Humidity: "));
Serial.print(event.relative_humidity);
Serial.println(F("%"));
}
}
鳥哥測試的結果,跑了 70 秒時,時間誤差就增加 1 秒了!如果用剛剛的方法做設計!那麼時間延遲則幾乎沒有什麼變化! 時間資料相當準確呢!
#include <Adafruit_Sensor.h> #include <DHT.h> #include <DHT_U.h> #define DHTPIN 7 #define DHTTYPE DHT11 // DHT 11 DHT_Unified dht(DHTPIN, DHTTYPE); uint32_t delayMS; long Time = millis(); void setup() { Serial.begin(9600); dht.begin(); Serial.println(F("DHTxx Unified Sensor Example")); sensor_t sensor; dht.temperature().getSensor(&sensor); Serial.println(F("------------------------------------")); Serial.println(F("Temperature Sensor")); Serial.print (F("Sensor Type: ")); Serial.println(sensor.name); Serial.print (F("Driver Ver: ")); Serial.println(sensor.version); Serial.print (F("Unique ID: ")); Serial.println(sensor.sensor_id); Serial.print (F("Max Value: ")); Serial.print(sensor.max_value); Serial.println(F("°C")); Serial.print (F("Min Value: ")); Serial.print(sensor.min_value); Serial.println(F("°C")); Serial.print (F("Resolution: ")); Serial.print(sensor.resolution); Serial.println(F("°C")); Serial.println(F("------------------------------------")); // Print humidity sensor details. dht.humidity().getSensor(&sensor); Serial.println(F("Humidity Sensor")); Serial.print (F("Sensor Type: ")); Serial.println(sensor.name); Serial.print (F("Driver Ver: ")); Serial.println(sensor.version); Serial.print (F("Unique ID: ")); Serial.println(sensor.sensor_id); Serial.print (F("Max Value: ")); Serial.print(sensor.max_value); Serial.println(F("%")); Serial.print (F("Min Value: ")); Serial.print(sensor.min_value); Serial.println(F("%")); Serial.print (F("Resolution: ")); Serial.print(sensor.resolution); Serial.println(F("%")); Serial.println(F("------------------------------------")); // Set delay between sensor readings based on sensor details. delayMS = sensor.min_delay / 1000; } void loop() { // Delay between measurements. if ( millis() - Time >= delayMS ){ Serial.print(delayMS); Serial.print("; "); Serial.println(millis()); Time = millis(); // Get temperature event and print its value. sensors_event_t event; dht.temperature().getEvent(&event); if (isnan(event.temperature)) { Serial.println(F("Error reading temperature!")); } else { Serial.print(F("Temperature: ")); Serial.print(event.temperature); Serial.println(F("°C")); } // Get humidity event and print its value. dht.humidity().getEvent(&event); if (isnan(event.relative_humidity)) { Serial.println(F("Error reading humidity!")); } else { Serial.print(F("Humidity: ")); Serial.print(event.relative_humidity); Serial.println(F("%")); } } }
如此一來,時間幾乎不會有誤差!當然啦,還是得要依據你的工作流程會花費的時間來進行規劃才行!
12.2: 重新開機的方法
如前所述,基本上,arduino 會有時間溢位的問題。如果不處理,那麼超過 50 天之後,可能會發生一些小意外。 那,有沒有辦法直接對 arduino 強迫重新開機呢?如果 arduino 可以定時自動重新開機,那當然就沒有時間溢位的問題! 因為在溢位之前,arduino 自動重新開機,當然時間就歸零~也就沒有了溢位的問題。
那如何進行重新開機呢?基本上,有兩種方法,一種是透過 arduino 上面的 reset 腳位,另一種則是直接透過軟體程式撰寫來處理。
- 使用 reset (RST) 腳位處理
仔細看 arduino 的板子,你會發現到有個腳位會寫上 RST (Reset),明明開發板上面就有 reset 按鈕,為何還需要這個 Reset 腳位呢? 基本上,在程式碼當中,預設的情況系, Reset 腳會會是 HIGH,如果你傳個 LOW 過去,那麼板子就會重新開機呢! 只是,你得要將某個腳位與 Reset 連結在一起。
我們拿 code-millis-1 來修改,同時將 D10 與 RST 連結在一起,你沒看錯!可以使用雙母杜邦線直接連在一起就好! 如果沒有雙母杜邦線,也可以拿兩條杜邦線安插到同一組麵包板上面即可。然後將 code-millis-1 修改這樣:
int led[] ={5, 6, 7, 8}; int i; int j = 0; long Time = millis(); int rst = 10; long limitTime = 30 * 1000; void setup() { // put your setup code here, to run once: digitalWrite(rst, HIGH); pinMode(rst,OUTPUT); for (i=0; i<=3; i++) { pinMode(led[i], OUTPUT); } Serial.begin(9600); } void loop() { // put your main code here, to run repeatedly: if ( millis() - Time >= 1000 ) { for ( i=0; i<=3; i++ ) { digitalWrite(led[i], LOW); } digitalWrite(led[j],HIGH); j=j+1; if ( j>=4 ){ j = 0; } Time = millis(); Serial.println(millis()); } if ( millis() >= limitTime ) { digitalWrite(rst,LOW); } }
燒錄上傳之後,你打開終端機,可以確實發現,arduino 開機 30 秒之後,就會自動重新開機! 只是,這個範例有點奇怪,當第二次要重複燒錄這個程式時,系統會說燒錄失敗耶!我覺得很奇怪! 拔掉 reset 腳位,重新燒錄就可以成功!燒錄成功再將 reset 接回去就好。只是,這樣就很麻煩!超奇怪的! 雖然是真的可以用啦!
- 使用系統程式重新開機
arduino 有提供一個方式讓你重新啟動開發板!使用的方式相當簡單,我們同樣舉上面的範例來處理:
int led[] ={5, 6, 7, 8}; int i; int j = 0; long Time = millis(); long limitTime = 30 * 1000; void (* resetFunc) ( void ) = 0; void setup() { // put your setup code here, to run once: for (i=0; i<=3; i++) { pinMode(led[i], OUTPUT); } Serial.begin(9600); } void loop() { // put your main code here, to run repeatedly: if ( millis() - Time >= 1000 ) { for ( i=0; i<=3; i++ ) { digitalWrite(led[i], LOW); } digitalWrite(led[j],HIGH); j=j+1; if ( j>=4 ){ j = 0; } Time = millis(); Serial.println(millis()); } if ( millis() <= limitTime ) { resetFunc(); } }
這個方法也很炫~你不用重新處理硬體連線,只要在 IDE 程式碼裡面加上全域變數宣告的 resetFunc 函數, 然後同樣使用 millis() 的方式去計算出需要重新開機的時間,那麼 arduino 就會主動幫你進行重新開機的行為了!
12.3: 參考資料
- 蔡董 arduino 文件:時間控制
http://interact.vexp.idv.tw/int10/img28.html - 對你的 arduino 做 reset 動作:
https://www.theengineeringprojects.com/2015/11/reset-arduino-programmatically.html