2016年10月29日 星期六

Arduino 測試 : PIR 紅外線移動偵測 (二)

去年七月底時測試過被動式紅外線 (PIR) 移動偵測模組, 此模組不會發出紅外線, 而是透過內建的生物紅外線偵測晶片來感測是否有人或動物經過其有效距離之內 (特別是水平方向的移動), 有的話就輸出 HIGH, 參考 :

# Arduino 測試 : PIR 紅外線移動偵測 (一)

這幾天因為要安裝 20W 太陽能板進行初步綠能實驗, 所以將 PIR 模組拿出來做進一步測試. 下面測試 1 目的是想透過序列埠監控視窗觀察 PIR 輸出的 HIGH 是持續一段時間, 或者像按鈕一樣會有彈跳現象?

測試 1 :

 setup() {
  Serial.begin(9600);
  pinMode(pirPin,INPUT);
  }

void loop() {
  int val=digitalRead(pirPin);  //read PIR output
  char mark='-';  //default print-out mark
  if (val==HIGH) {mark='*';} //change mark to '*' if detected
  Serial.print(mark);  //print the mark
  ++counter;  //increment counter
  if (counter >= 100) {
    Serial.println(); //new line if over 100 marks
    counter=0; //reset counter
    }
  }

此程式中我們開啟序列埠以便輸出每一個迴圈偵測到的 PIR 訊號, 如果偵測到 PIR 輸出 HIGH (有生物移動), 就往串列埠輸出 '*', 否則輸出 '-' 符號, 為了在序列埠監控視窗方便觀察, 我用了一個迴圈計數器控制每輸出 100 個符號便跳行.

序列埠監控視窗擷取訊息如下 :

----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
---------------------------------------------------------------*************************************
****************************************************************************************************
****************************************************************************************************
****************************************************************************************************
****************************************************************************************************
*************************************************************************---------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------

可見當 PIR 模組輸出 HIGH 時是連續一段時間, 不會有彈跳現象 (即 HIGH-LOW 短期間內交替變化), 這是好事, 表示我們不用去處理彈跳問題.

其次, 由於我使用自製的 Arduino+ESP8266 IOT 模組, 上面內建一個無源蜂鳴器 (接在 D9 腳上), 所以我想在偵測到生物移動時也發出告警聲, 這可參考之前寫的 Arduino 聲音測試 :

Arduino 的聲音測試 (一)

測試 2 : 

const byte pirPin=2;
byte counter=0;
const byte buzzerPin=9; //read PIR output

void loop() {
  int val=digitalRead(pirPin);
  char mark='-';  //default print-out mark
  if (val==HIGH) { //change mark to '*' if detected
    mark='*';
    tone(buzzerPin, 1000);
    }
  else {
    noTone(buzzerPin);
    }
  Serial.print(mark);  //print the mark
  ++counter;  //increment counter
  if (counter >= 100) {
    Serial.println(); //new line if over 100 marks
    counter=0; //reset counter
    }
  }

這裡當 PIR 輸出 HIGH 時使用 tone() 函數去發出 1KHz 的告警聲, 當停止移動輸出為 LOW 時用 noTone() 函數終止發聲. 測試結果確實會在偵測到時 ㄅㄧ ㄅㄧ 叫.

接下來在下面測試 3 我想知道當 PIR 偵測到移動時, 它輸出的 HIGH 狀態持續了多少時間? 在 "Arduino 完全實戰手冊 (博碩出版, 王冠勛譯)" 這本書的 6.3.2 節的程式 6-4 有偵測移動時間的範例, 但是它的寫法可讀性不高, 邏輯不清晰, 我看完大致了解作法後自行寫了下面的測試程式, 其中我省略了校正程序, 我覺得那不需要.

測試 3 :

const byte pirPin=2; //PIR ouput connected to Arduino D2
boolean waitForLow=false; //flag for calculate HIGH time
unsigned long timerStart; //time stamp of start moving

void setup() {
  pinMode(pirPin, INPUT);
  Serial.begin(9600);
  }

void loop() {
  if (digitalRead(pirPin) == HIGH) { //still moving  
    if (!waitForLow) { //LOW to HIGH change : start timer
      timerStart=millis(); //record time stamp of start moving
      waitForLow=true; //set status : waiting for motion stop  
      }
    }
  else { //pirPin==LOW :no moving
    if (waitForLow) { //HIGH to LOW change
      Serial.print("motion time=");
      Serial.print(millis()-timerStart); //calculate HIGH time
      Serial.println(" ms");
      waitForLow=false; //reset status    
      }
    }
  }

此程式利用 waitForLow 變數來記錄 PIR 輸出 HIGH, 正等待移動停止輸出 LOW 的狀態, 另外利用 timerStart 紀錄這個 LOW to HIGH 變化的時戳, 以便計算 HIGH 狀態的持續時間. 注意, 時戳必須用 unsigned long 才夠紀錄其值. 當偵測到 HIGH 時, 先看看 waitForLow 是否為假, 式的話表示這是 LOW to HIGH 變化, 就將此時的時戳記錄在 timerStart 裡. 若偵測到 LOW, 也是要看看 waitForLow 是否為真, 是的話表示抓到一個 HIGH to LOW 變化, 這時用 millis() 減掉 startTimer 就可以得到 HIGH 狀態的持續時間了, 同時也要將 waitForLow 重置為 false.

序列埠監控視窗擷取訊息如下 :

motion time=543 ms
motion time=673 ms
motion time=544 ms
motion time=542 ms
motion time=541 ms
motion time=3015 ms
motion time=1865 ms
motion time=2787 ms

可見每次偵測到移動輸出 HIGH 時間都不一定, 但大約都在 0.5 秒以上. 這個 HIGH 持續時間可以用 PIR 模組上的一個可變電阻調整, 如下圖所示 :


但這很難精確地調整到我們需要的延遲秒數, 要精確的話必須用軟體的方式來解決, 如下列範例使用 delay() 函數來控制 :

測試 4 :

const byte pirPin=2; //PIR ouput connected to Arduino D2
const byte ledPin=13;
int highDelay=10000; //high delay time (ms)

void setup() {
  pinMode(pirPin, INPUT);
  pinMode(ledPin, OUTPUT);
  Serial.begin(9600);
  }

void loop() {
  if (digitalRead(pirPin) == HIGH) { //still moving
    digitalWrite(ledPin, HIGH);  //set output
    delay(highDelay); //lasting HIGH output
    digitalWrite(ledPin, LOW); //HIGH timeout
    }
  }

這裡的 highDelay 變數就是用來設定 HIGH 延遲的, 當 PIR 偵測到移動輸出 HIGH 時, 就將 D13 LED 點亮, 經過 highDelay 毫秒後再熄滅. 這個功能也可以用外部硬體中斷來完成, 但是 PIR 輸出只能接到 D2 或 D3 兩個中斷腳位, 因為 UNO/NANO/Pro Mini 這三種以 ATMEGA328P 為 MPU 的板子都只有兩個外部硬體中斷, 關於中斷參考 :

# Arduino 按鈕開關測試 (二) : 硬體中斷法 (Interrupt)

下列範例仍然將 PIR 輸出接到 D2 腳位 (中斷 0), 使用 PIR 輸出的上升緣來觸發中斷 :

測試 5 :

const byte pirPin=2; //PIR ouput connected to Arduino D2
const byte ledPin=13;
int highDelay=10000; //high delay time (ms)
volatile boolean state=LOW; //initial value of output

void setup() {
  pinMode(pirPin, INPUT);
  pinMode(ledPin, OUTPUT);
  Serial.begin(9600);
  attachInterrupt(0, int0, RISING); //assign handler int0()
  }

void loop() {
  if (state == HIGH) {
    digitalWrite(ledPin, HIGH); //set HIGH
    delay(highDelay); //lasting HIGH output
    digitalWrite(ledPin, LOW); //HIGH timeout
    state=LOW; //reset state
    }
  }

void int0() { //interrupt handler
  state=HIGH; //set state
  }

此例中當 PIR 偵測到移動時會從 LOW 變 HIGH, 此上升緣觸發中斷 0 去執行 int0() 函數, 將狀態變數 state 改為 HIGH; 而在 loop() 函數中判斷若 state 為 HIGH 就點亮 D13 LED, 經過 highDelay 毫秒後再熄滅, 同時將 state 重設為 LOW 狀態. 實際測試可知效果與上面測試 4 是一樣的.

不過上面不管是使用哪一種方式, 當設定的 HIGH 延遲時間結束時, 即使物體還在移動, 燈仍然會先熄滅, 然後再次感測到移動而重新點亮, 這樣對於感應照明控制之類的應用而言頗不自然, 因為通常我們期望的是, 即使我們設定了燈亮的持續時間, 但如果物體還在移動, 燈應該持續點亮, 直到物體停止一段時間確實不再移動後再熄滅.

這該怎麼做呢? 我修改了上面測試 5 的程式, 加入了一個 triggerCount 變數來累計中斷觸發的次數, 只要 PIR 一偵測到物體移動觸發中斷, 就將 state 設為 HIGH 以便點亮 D13 LED, 並將 triggerCount 增量 (除非碰頂到達 triggerLimit, 這是為了避免一直增量下去使得燈熄滅時間過度延長). 而在 loop() 中的每次迴圈中則依據 state 狀態來控制 LED 明滅, 當 HIGH 延遲時間結束時就將 triggerCount 減量, 當其到達 0 時表示一段時間內都沒有移動觸發, 就將 LED 熄滅.

測試 6 :

const byte pirPin=2; //PIR ouput connected to Arduino D2
const byte ledPin=13;
int highDelay=10000; //high delay time (ms)
volatile boolean state=LOW; //initial value of output
int triggerCount=0; //interrupt counter
int triggerLimit=5; //cieling of trigger Counter

void setup() {
  pinMode(pirPin, INPUT);
  pinMode(ledPin, OUTPUT);
  Serial.begin(9600);
  attachInterrupt(0, int0, RISING); //assign int0
  }

void loop() {
  Serial.println(triggerCount);
  if (state == HIGH) {
    digitalWrite(ledPin, HIGH); //set HIGH
    delay(highDelay); //lasting HIGH output
    --triggerCount;  //decrement counter
    if (triggerCount <= 0) {  //motion stop
      digitalWrite(ledPin, LOW); //HIGH timeout    
      state=LOW; //reset state
      }
    }
  }

void int0() { //interrupt handler
  state=HIGH; //set state  
  if (triggerCount <= triggerLimit) { 
    ++triggerCount;  //increment counter if not reaching uplimit
    }  
  }

實際測試發現若持續移動, triggerCount 會一直保持在 5 附近, 若停止移動後, triggerCount 每 10 秒會遞減, 直到 0 時就會將 LED 熄滅. 因此就上面的程式設定值而言, 只要連續 50 秒 (10*5) 都沒有偵測到移動, 燈就會熄滅. 序列埠監控視窗擷取訊息如下 :

0
0
0
2
2
1
2
3
4
5
5
4
3
2
2
1
1
0
0
0










沒有留言 :