2017年6月1日 星期四

MicroPython on ESP8266 (九) : PIR 紅外線移動偵測

做完 MicroPython on ESP8266 基本的 GPIO 實驗後, 接下來我想測試看看 PIR 紅外線移動偵測, 這也是我這波 MicroPython 熱的主軸 : 利用小巧又內建 WiFi 能力的 ESP8266 模組佈建人體移動監測終端. 我使用的是 ESP-01 (1MB Flash) 模組, 燒錄目前最新的 1.9.8 版韌體,  而 PIR 模組用的是 HC-SR501, 此模組雖然是吃 5V 電源, 但輸出信號是 3.3V 與 0V, 因此可直用於 ESP-01 模組的 GPIO 輸入腳. 關於 PIR 實驗, 參考之前在 Arduino 上的測試紀錄 :

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

HC-SR501 模組只有三支接腳, 左右兩邊是電源接地 GND 與 VCC (5V), 中間那支腳才是信號輸出 (3.3V), 參考 :


我是將其直接與 ESP-01 的 GPIO 0 相連, 然後在程式中將 GPIO 0 定義為輸入腳, GPIO 2 定義為輸出腳, GPIO 2 連接一個 LED 與 220 歐姆電阻後接地, 參考下列測試紀錄中的 GPIO 測試部分 :

MicroPython on ESP8266 (二) : 數值型別測試
MicroPython on ESP8266 (三) : 序列型別測試
MicroPython on ESP8266 (四) : 字典與集合型別測試
MicroPython on ESP8266 (五) : WiFi 連線與 WebREPL 測試
MicroPython on ESP8266 (六) : 檔案系統測試
MicroPython on ESP8266 (七) : 時間日期測試
# MicroPython on ESP8266 (八) : GPIO 測試

下列測試 1 是參考 "Arduino 測試 : PIR 紅外線移動偵測 (一)" 這篇的 Arduino 程式修改為 MicroPython 版本 :

測試 1 : 偵測到移動使 LED 閃爍  

from machine import Pin
import time
p0=Pin(0, Pin.IN)          #接 PIR 感測器信號輸出 (中間腳)
p2=Pin(2, Pin.OUT)      #接 LED + 220 歐姆電阻

def LED_blink():           #閃爍 LED 一次的函數
    p2.value(1)
    time.sleep_ms(50)     #暫停 50 ms
    p2.value(0)
    time.sleep_ms(50)      #暫停 50 ms

while True:
    if p0.value()==1:       #PIR 偵測到人體移動時輸出 High (3.3V)
        LED_blink()          #讓 LED 閃爍
    else:                           #移動停止時 PIR 輸出 Low
        p2.value(0)            #讓 LED 熄滅

將此程式存成 main.py 檔案後上傳 ESP-01, 參考下面這篇 :

MicroPython on ESP8266 (六) : 檔案系統測試

可利用 ampy, mpfshell, 或 webrepl 將 main.py 上傳到 ESP-01 模組 :

D:\test>ampy --port COM4 put main.py      #上傳檔案

然後將 ESP-01 重開機或在 REPL 按 Ctrl+D 軟開機, 當偵測到有人體移動時, LED 就會閃爍.


PIR 感測器偵測到人體移動輸出 High 的時間有多長? 我參考下列這篇測試 3 的 Arduino 程式, 將其改寫為 Python 版如下 :

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

測試 2 : 計算 PIR 輸出 High 的時間長度

from machine import Pin
import time
p0=Pin(0, Pin.IN)
p2=Pin(2, Pin.OUT)
waitForLow=False                                     #狀態旗標:用來計算輸出 High 的時間
timerStart=time.ticks_ms()                       #紀錄 High 時長的計時器

def LED_blink():                                        #閃爍 LED 一次的函數
    p2.value(1)
    time.sleep_ms(50)                                  #暫停 50 ms
    p2.value(0)
    time.sleep_ms(50)                                  #暫停 50 ms

while True:
    if p0.value()==1:                                     #PIR 偵測到人體移動時輸出 High (3.3V)
        if not waitForLow:                              #目前是 Low 狀態
            timerStart=time.ticks_ms()            #取得目前時戳更新計時器開始計時
            waitForLow=True                           #更新狀態旗標 : 等待 Low 到來
        LED_blink()
    else:                                                         #PIR 輸出 Low
        if waitForLow:                                    #原先狀態為 High
            print(time.ticks_ms()-timerStart)    #計算時長
            waitForLow=False                           #狀態旗標歸零
        p2.value(0)                                           #熄滅 LED

上面程式在 PIR 由 Low 變 High 時設定狀態旗標並使用 time.ticks_ms() 函數來取得以毫秒計算的時戳 (開機以來的毫秒數) 開始計時, 等到 PIR 由 High 變 Low 時再取一次時戳, 與開始時戳相減即得 PIR 為 High 之時長, 同時將狀態旗標歸零以備下次觸發. 計算時間差也可以使用 time_ticks(time_ticks_ms(), timerStart), 參考 :

https://docs.micropython.org/en/latest/esp8266/esp8266/quickref.html#delay-and-timing
# https://docs.micropython.org/en/latest/pyboard/library/utime.html#utime.ticks_diff

在 REPL 介面可看到 print() 函數所輸出的時間長度 (ms) :

MicroPython v1.9-8-gfcaadf92 on 2017-05-26; ESP module with ESP8266
Type "help()" for more information.
>>>
2622
4538
1513
2420
1816
1210
3429
2018
1211
4134
3732
2218
1412
807
605
1009
1109
605
2319
605
1311
908
2420

可見 PIR 的 High 輸出至少都持續 0.5 秒以上, 持續觸發的話 High 的時間會更久.

下列測試 3 程式用一個變數指定當 PIR 感測器被觸發後持續點亮 LED 的時間 :

測試 3 : 指定 PIR 觸發後 LED 點亮時間

from machine import Pin
import time
p0=Pin(0, Pin.IN)
p2=Pin(2, Pin.OUT)
highDelay=4.5                       #LED 持續點亮之秒數

while True:
    if p0.value()==1:
        p2.value(1)                     #點亮 LED
        time.sleep(highDelay)    #持續點亮指定之秒數
        p2.value(0)                      #時間到熄滅 LED


上面的範例適合用在感應燈的應用, 如果是要在 PIR 觸發後讓 LED 閃爍指定的時間長度該怎麼做? 這時就需要一個變數來記錄觸發後的狀態了, 如下列測試 4 所示 :

測試 4 : 指定 PIR 觸發後 LED 閃爍時間

from machine import Pin
import time
p0=Pin(0, Pin.IN)
p2=Pin(2, Pin.OUT)
duration=8                           #LED 持續閃爍之秒數

def LED_blink():
    p2.value(1)
    time.sleep_ms(50)
    p2.value(0)
    time.sleep_ms(50)

while True:
    if p0.value()==1:              #偵測到觸發訊號
        expire=duration*1000 + time.ticks_ms()    #計算結束時戳 (duration 換算成 ms)
        while time.ticks_ms() <= expire:            #若還未到達結束時戳就持續閃爍
            LED_blink()


上面的兩個範例在指定時間結束後即使偵測物體繼續移動, 雖然 PIR 感測器是在 repeat trigger 模式 (有些可用 Jumper 設定, 我的 PIR 沒有 Jumper, 固定為重複觸發), 但可能還是會稍停一下, 這對於移動照明而言不實用, 要如何才能在有持續移動情況下讓燈維持不滅呢? 這需要外部硬體中斷來協助才行, 參考下面這篇 Arduino 的測試 6 :

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

MicroPython 的硬體中斷用法請參考官網教學文件 :

# 6.1. External interrupts
http://docs.micropython.org/en/latest/wipy/library/machine.Pin.html#machine.Pin.irq
MicroPython on ESP8266 (八) : GPIO 測試


測試 5 : PIR 持續觸發使 LED 延遲熄滅

from machine import Pin
import time
p0=Pin(0, Pin.IN)
p2=Pin(2, Pin.OUT)
state=0
triggerCount=0       #觸發次數紀錄器 (最高次數為 triggerLimit 所規範)
triggerLimit=5        #觸發次數紀錄器上限值
highDelay=10.0      #點亮 LED 之秒數

def int0(p):                         #中斷處理函數  (必須傳入一個參數 : IRQ 腳位)
    global state                    #取用全域變數
    global triggerCount       #取用全域變數
    global triggerLimit        #取用全域變數
    state=1                           #LED 點亮狀態
    if triggerCount < triggerLimit:        #觸發次數紀錄器低於上限就增量
        triggerCount=triggerCount + 1
    print(p,"IRQ Trigger Count:",triggerCount)

p0.irq(trigger=Pin.IRQ_RISING, handler=int0)     #設定 GPIO 0 為上升緣觸發 IRQ 中斷

while True:
    if state==1:                         #LED 點亮狀態
        p2.value(1)                     #點亮 LED
        time.sleep(highDelay)    #暫停秒數
        triggerCount=triggerCount - 1        #觸發紀錄器減量
        print("Trigger Count:", triggerCount)
        if triggerCount <= 0:                       #觸發紀錄器歸零時熄滅 LED
            state=0
    else:
        p2.value(0)                      #熄滅 LED

上面的程式利用 state 變數紀錄 LED 明滅狀態, 並將 GPIO 0 設定為外部上升緣觸發 IRQ 中斷, 當中斷發生時, 中斷處理函數 int0() 會將 state 變更為 1, 點亮 LED, 同時若未達上限將觸發紀錄器增量. 在無限迴圈裡則依據 state 值來使 LED 明滅; 若要點亮 LED, 則在持續時間 highDelay 結束後將觸發紀錄器減量, 如果觸發紀錄器因此歸零, 則將 LED 熄滅.

設定 Pin 物件外部硬體中斷只要呼叫 irq() 方法, 並傳入 trigger 以及 callback 參數即可. 其中 trigger 可用選項如下 (可用 | 組合) :
  1. Pin.IRQ_LOW_LEVEL (LOW 觸發)
  2. Pin.IRQ_HIGH_LEVEL (HIGH觸發)
  3. Pin.IRQ_RISING  (上升緣都觸發)
  4. Pin.IRQ_FALLING (下降緣都觸發)
注意, 中斷發生時 MicroPython 會向中斷處理函數傳遞一個必要參數 p, 即產生中斷的 GPIO 腳位 Pin(0), 雖然此參數可能用不到, 但定義時必須有此參數, 否則會因傳不進去而產生錯誤. 例如上面程式的 def int0(p) 若改為 def int0() 將出現下列 TypeError :

MicroPython v1.9-8-gfcaadf92 on 2017-05-26; ESP module with ESP8266
Type "help()" for more information.
>>>
PYB: soft reboot
#9 ets_task(40100164, 3, 3fff829c, 4)
Traceback (most recent call last):
  File "boot.py", line 5, in   File "webrepl.py", line 70, in start
  File "webrepl.py", line 21, in setup_conn
OSError: [Errno 12] ENOMEM
TypeError: function takes 0 positional arguments but 1 were given
TypeError: function takes 0 positional arguments but 1 were given
TypeError: function takes 0 positional arguments but 1 were given

REPL 執行結果如下 :




可見即使在 LED 點亮狀態 (state=1) 程式暫停時, IRQ 仍持續觸發, triggerCount 繼續增值使得 LED 能持續點亮. 在停止移動後, triggerCount 計數器將從最高值 5 遞減至 0, 因此以上例之設定值 trggerLimit=5, highDelay=10 來說, 最後一次偵測到移動後, 若 5*10=50 秒內未再偵測到移動, 則 LED 就會熄滅. 調整 triggerLimit 與 highDelay 之值即可改變熄燈的延遲時間, 這樣就不會在有持續移動情況下, 發生 LED 一下子亮, 一下子又熄滅的情形了.

參考 :

update data to thingspeak
# TEST BOARD REVIEW WITH - EXERCISER / WEB SERVER / WEB CLIENT
IoT Tutorial for ESP8266
https://junkhack.wordpress.com/2015/08/25/初めてのiotデバイス完成/

沒有留言 :