arduino 官網 arduino uno

Arduino 物聯網應用 - 上課教材

動畫互動網頁程式設計 > 課程內容 > 第 12 章 - 時間參數的應用

第 12 章 - 時間參數的應用

上次更新日期 2023/01/04

有玩過 arduino 搭配感測器的朋友大概會知道,許多的感測器讀取,其實是需要花費時間的!舉例來說,溫溼度計 (dht11) 的讀取, 就可能得要花費個數秒鐘。此時,如果使用 delay 的函數,那麼除了讀取的時間之外,還得要額外延遲數秒鐘,所以,跟我們預期的固定秒數顯示, 可能會有些許的落差。所以,比較好的程式寫法,可能需要用到實際 arduino 的規範時間較佳!另外,這組時間參數, 其實會過期 (overflow)...所以,定期將 arduino 重新開機 (reset),可能也是個不錯的思考方向!

學習目標:

  1. 了解 arduino 時間參數限制
  2. 使用 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: 參考資料