2017年5月25日 星期四

MicroPython on ESP8266 (八) : GPIO 測試

GPIO 是微控器與外部互動的窗口, 接感測器的為 Input, 用來偵測外部狀態或擷取外部資料, 可說是微控器的耳目; 而當 Output 時是作為致動器, 用來驅動 LED, 顯示器, 或繼電器等.

ESP8266 的 GPIO 埠有 16+1 個 (1 是指 ADC 輸入), 但因為 IC 接腳有限, 因此腳位大多作多工用途 (一腳多用), 例如 GPIO 1 被用作 REPL UART TX; GPIO 3 是 REPL UART RX, GPIO 16 被用來將晶片從深度睡眠中叫醒, 這三支腳不能做一般 IO 用, 而 GPIO 6~11 是用來連接 Flash 晶片等 SPI 介面, 也不能使用, 因此可用的 GPIO 埠只有 0, 2, 4, 5, 12, 13, 14, 15 共 8 個, 參考 :

# Technical specifications and SoC datasheets

不過深圳 AI Thinker 出品的 ESP8266 板有 12+ 種之多, 各板在接出 GPIO 腳數目上差異很大, 例如最簡單的 ESP-01 僅接出 GPIO 0 與 GPIO 2 兩個埠, 其中 GPIO 0 若在開機時被偵測到 Low, ESP8266 將進入韌體燒寫模式 (透過 UART), 否則 GPIO 0 即為一般的 GPIO 埠. ESP-12E 與 ESP-12F 模組有將全部 GPIO 接出 (ESP-12S 沒有), 而 ESP-05 與 ESP-10 則完全沒有接出 GPIO 埠, 參考 :

# ESP8266之GPIO功能、資源探索
# 認識UART、I2C、SPI三介面特性
# ESP8266板卡眾多,如何選擇?

正常使用時, GPIO 輸入腳最好用 2K~10K 上拉電阻接至 3.3V (使用 2K 對雜訊抑制力較佳), 不過 GPIO 腳內部都有內建上拉電阻, 可以透過軟體設定開啟, 參考 :

# Using ESP8266 GPIO0/GPIO2/GPIO15 pins
How to Choose Your ESP8266 Module

關於 GPIO 用法參考 MicroPython 教學文件 :

MicroPython Documentation (v1.8.6 PDF)
Quick reference for the ESP8266 : Pins and GPIO

本序列之前的測試文章參考 :

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

MicroPython 文件參考 :

MicroPython tutorial for ESP8266  (官方教學)
http://docs.micropython.org/en/latest/micropython-esp8266.pdf
http://docs.micropython.org/en/latest/pyboard/library/usocket.html#class-socket
http://docs.micropython.org/en/v1.8.7/esp8266/library/usocket.html#module-usocket
https://gist.github.com/xyb/9a4c8d7fba92e6e3761a (驅動程式)

常用的 ESP8266 開發板 D1 mini 的 GPIO 接腳如下 :


Source : DIYIOT



另一塊常用的 NodeMCU 接腳如下 :


Source : ESP8266 SHOP


控制 ESP8266 的 GPIO 腳需從 machine 模組匯入 Pin 類別 :

from machine import Pin  

Pin 類別定義了 Pin.OUT 與 Pin.IN 常數可用來設定 GPIO 之輸出入 :

p0=Pin(0, Pin.OUT)      #定義 GPIO 0 為輸出腳, 傳回 Pin 物件
p2=Pin(2, Pin.IN)          #定義 GPIO 2 為輸入腳, 傳回 Pin 物件

也可以匯入整個 machine 模組 :

import machine

但這樣 Pin 類別就要加上 machine 來存取了 :

p0=machine.Pin(0, machine.Pin.OUT)      #定義 GPIO 0 為輸出腳, 傳回 Pin 物件
p2=machine.Pin(2, machine.Pin.IN)          #定義 GPIO 2 為輸入腳, 傳回 Pin 物件

參考 :

# Adafruit : Digital Inputs

注意, 這裡 Pin() 建構式的第一個參數是 ESP8266 本身 GPIO 的腳位編號, 例如要設定 GPIO 0 就傳入 0. 有些板子例如 NodeMCU 會將輸出入腳另外編號例如 D0, D1, D2 ... 等, 必須將其轉換為對應之 GPIO 編號才可以. 由於 ESP8266 板子很多, MicroPython 無法一一照顧到每一種板子, 因此採用 ESP8266 的 GPIO 編號是最大公約數, 教學文件原文如下 :

"Note that many end-user boards use their own adhoc pin numbering (marked e.g. D0, D1, ...). As MicroPython supports different boards and modules, physical pin numbering was chosen as the lowest common denominator. For mapping between board logical pins and physical chip pins, consult your board documentation."

要在輸出腳輸出 High/Low 位準可以直接呼叫 Pin 物件的 high() 或 low() 方法, 也可以呼叫 value() 方法並傳入參數 1 或 0 :

p0.high()       #在 GPIO 0 輸出 High
p0.low()        #在 GPIO 0 輸出 Low
p0.value(1)   #在 GPIO 0 輸出 High
p0.value(0)   #在 GPIO 0 輸出 Low

也可以在定義腳位時以參數 value 設定預設之輸出位準 :

p0=Pin(0, Pin.OUT, value=1)      #定義 GPIO 0 為輸出腳並預設輸出值為 High
p0=Pin(0, Pin.OUT, value=0)      #定義 GPIO 0 為輸出腳並預設輸出值為 Low

ESP8266 每一個 GPIO 腳都有內建上拉電阻, 可以在設定為輸入腳時以 Pin.PULL_UP 常數作為第三參數來啟用, 這樣該輸入腳預設值為 High, 否則會因為浮接而呈現隨機值 :

p2=Pin(2, Pin.IN, Pin.PULL_UP)    #定義 GPIO 2 為輸入腳, 並啟用上拉電阻
swPin=p2.value()       #讀取輸入腳 p2 目前的輸入值
ledPin=p0.value()      #讀取輸出腳 p0 目前的狀態

Pin 物件的 value() 函數是兩用的, 有傳參數時為 setter, 用來設定輸出腳的位準; 如果沒有傳參數進去的話為 getter, 用來讀取輸出入腳的值. 注意, 無參數的 value() 方法不只是用在讀取輸入腳的位準而已, 也可以用在輸出腳, 這時是讀取該輸出腳目前之狀態.

下面這個測試 1 的程式會持續不斷每隔一秒讓接在 GPIO 2 腳的 LED 燈閃爍.

測試 1 : 持續閃爍的 LED (flashing_LED.py)

此測試是要讓接在 GPIO 2 上的 LED 持續閃爍, 我找了一顆 LED 與 220 歐姆的限流電阻串接在 GPIO 2 後接地, 然後在 PC 上編寫了一個測試程式如下 :

from machine import Pin    #匯入 Pin 類別
pin=Pin(2, Pin.OUT)          #定義 GPIO 2 為輸出腳
import time                         #匯入時間模組
while True:                         #無窮迴圈
    pin.high()                        #點亮 LED
    time.sleep(1.0)                #暫停 1 秒
    pin.low()                         #熄滅 LED
    time.sleep(1.0)                #暫停 1 秒

將程式存檔為 main.py (一定要用此名稱, 因為它是 MicroPython 開機跑完 boot.py 後要執行的程式), 然後用 ampy 將此程式傳到 ESP8266 的根目錄下 (注意, 使用 ampy 時 Putty 連線須關閉) :

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

關於 MicroPython 的檔案系統管理, 參考 :

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

重開機就會看到 LED 不斷地閃爍.

間隔 1 秒不斷閃爍的 LED

這時若用 Putty 連線 ESP8266 將無反應, 因為目前程序是 main.py 裡的無窮迴圈, 若要中斷執行可按 Ctrl + C 就會送中鍵盤中斷, 控制權轉回 REPL 介面 :

Traceback (most recent call last):
  File "main.py", line 8, in <module>
KeyboardInterrupt:

MicroPython v1.8.7-7-gb5a1a20a3 on 2017-01-09; ESP module with ESP8266
Type "help()" for more information.
>>>
>>>

要再次執行 main.py 可按 Ctrl + D 軟體開機即可.

>>>
>>>
PYB: soft reboot    (按了 Ctrl + D)
#6 ets_task(40100164, 3, 3fff8398, 4)
Traceback (most recent call last):
  File "boot.py", line 5, in <module>
  File "webrepl.py", line 70, in start
  File "webrepl.py", line 21, in setup_conn
OSError: [Errno 12] ENOMEM

下面測試 2 程式是從之前的 Arduino 程式改寫而來, 參考下列文章中的測試 1 :

Arduino 的按鈕開關測試 (一) : 輪詢法 (Polling)

測試 2 : 沒有處理按鈕的彈跳現象 (push_button_no_debouncing.py)

from machine import Pin
swPin=Pin(0, Pin.IN, Pin.PULL_UP)    #GPIO 0 設為輸入並開啟上拉電阻
ledPin=Pin(2, Pin.OUT)       #GPIO 2 設為輸出
ledState=0;                            #LED 初始狀態:暗的
while True:                            #無窮迴圈
    swState=swPin.value()     #讀取按鈕狀態
    if swState==0:                   #按鈕被按下 (接地)
        if ledState==1:              #若 LED 是亮的, 則熄滅之
            ledState=0
        else:                               #若 LED 是滅的, 則亮之
            ledState=1
        print(ledState)               #顯示 LED 狀態
        ledPin.value(ledState)   #更改 LED 狀態

在這個測試中我在 GPIO 0 上接一個按鈕開關, 開關的另一端接地, 然後將 GPIO 0 定義為輸入腳, 並開啟其上拉電阻, 這樣當按鈕沒按下時 GPIO 0 腳預設就是 High 而不是浮接的雜訊. 由於這個程式沒有處理按鈕的彈跳問題, 所以實際按按鈕並不會像期望中那樣按一下燈亮, 再按一下燈滅那樣交替明滅.

未處理彈跳-結果未達預期

處理按鈕彈跳問題可參考下列 Wemos D1 mini 教學文件 "2.4 Button" (page 10) :

Micropython on ESP8266 Workshop v1.0 (pdf)

我把程式改編如下 :

測試 3 : 處理按鈕彈跳現象之方法 (1) (push_button_with_debouncing_1.py)

import time  
from machine import Pin
swPin=Pin(0, Pin.IN, Pin.PULL_UP)
ledPin=Pin(2, Pin.OUT)
while True:
    if not swPin.value():      #若按鈕被按下 (接地)
        ledPin.value(not ledPin.value())     #反轉 LED 狀態
        time.sleep_ms(300)                        #暫停 0.3 秒
        while not swPin.value():                 #按鈕若還在按下狀態就在迴圈繞, 否則跳出去
            pass

此程式借助 time 模組的 sleep_ms() 函數來讓機器暫停執行 300 毫秒, 關於 sleep_ms() 參考 :

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

一般而言機械按鈕的不穩定彈跳時間約在 50ms~100ms 就會結束, 因此 300ms 過後應該已經到達 ON 或 OFF 的穩定狀態. 暫停 300ms 後用第二層無窮迴圈再次檢查按鈕狀態, 如果還在按下狀態就在迴圈裡繞維持現狀; 否則就跳出迴圈.

有處理彈跳-功能正常

注意, 上面的程式使用了 value() 函數讀取 GPIO 2 輸出腳以取得目前 LED 之明滅狀態 (傳回 1 或 0), 將狀態反轉後再傳給 value() 去設定 LED, 因此按一下明亮, 再按一次即熄滅. 由此可見 Python 語言之簡潔.

另外我在 MicroPython 官網找到一個給 Pyboard 用的消除彈跳程式, 參考 :

Debouncing a pin input (for Pyboard)

我將其改寫為 ESP8266 用的版本如下 :

測試 4 : 處理按鈕彈跳現象之方法 (2 (push_button_with_debouncing_2.py)

import time
from machine import Pin

def wait_pin_change(pin):              #等待位準變化趨於穩定的函數
    cur_value=pin.value()                 #進入函數時的目前腳位狀態
    counter=0                                    #計數器初始值 0
    while counter < 20:                 #到達穩定狀態後延遲 20ms 再離開函數的迴圈
        if pin.value() != cur_value:     #腳位狀態已改變 : 增量計數器
            counter += 1                        
        else:                                         #腳位狀態不變 : 計數器歸零
            counter = 0
        time.sleep_ms(1)                     #延遲 1ms

swPin=Pin(0, Pin.IN, Pin.PULL_UP)
ledPin=Pin(2, Pin.OUT)
while True:
    wait_pin_change(swPin)             #先至少等 20ms 開關趨於穩定
    if not swPin.value():                    #如果按鈕按下就反轉 LED 狀態
        ledPin.value(not ledPin.value())

此程式是利用自訂函數 wait_pin_change() 去偵測是否按鈕狀態有變化, 由於 GPIO 0 被上拉電阻預設為 High, 進入此函數後 cur_value 會記住 1, 如果按鈕沒被按下程序就會在無窮迴圈中不斷循環 (因 counter 不斷被 reset 為 0), 直到按鈕被按下, 這時 counter 就會增量, 但在下一迴圈若彈跳到 1, counter 又會被 reset 重來, 所以最快的話就是在預定地 20 次迴圈裡, 每次都讀取到彈跳時的 0, 讓 counter 每次都增量, 這樣 20ms 後就會跳出迴圈, 回到主程序, 但通常這不太可能發生. 總之, 在按鈕達到穩定狀態的 20ms 後就會離開 wait_pin_change() 函數, 這樣就可以反轉 LED 狀態了.

接下來要測試 ESP8266 的脈寬調變 PWM (Pulse Width Modulation) 功能, 這是利用數位方波的頻率與脈寬變化來模擬類比訊號如蜂鳴器聲音或 LED 亮度的方法, 參考 :

# Arduino 的聲音測試 (一)

PWM 主要的參數是方波的頻率 (frequency) 與工作週期 (duty cycle), 以 PWM 發出聲音來說, 頻率會決定聲音的音高 (pitch); 而工作週期則決定聲音的音色. 而以 PWM 訊號控制 LED 的話, 頻率會決定閃爍的程度; 而工作週期則決定亮度, 這亮度與 PWM 方波的平均電壓成正比 :

Vavg=Vcc * Th/T

此處 Th 為方波為 High 的時間, 而 T 為方波之週期. Th/T 即工作週期.

ESP8266 的每一個 GPIO 除了 GPIO 16 外都具有 PWM 功能, MicroPython 在 machine 模組中提供了 PWM 類別來處理脈寬調變, 通常會與 Pin 類別一起匯入 :

from machine import Pin, PWM

呼叫 PWM 類別的建構式 PWM() 並傳入 Pin 物件即可建立一個 PWM 物件, 例如 :

pwm2=PWM(Pin(2))  

注意, 不必傳入第二參數 Pin.OUT, 因 PWM 本來就是要做輸出用的.

這樣就可以呼叫 PWM 物件的 freq() 與 duty() 方法, 分別傳回目前方波的頻率與工作週期 :

MicroPython v1.8.7-7-gb5a1a20a3 on 2017-01-09; ESP module with ESP8266
Type "help()" for more information.
>>> from machine import Pin, PWM
>>> pwm2=PWM(Pin(2))   #設定 GPIO 2 為 PWM 輸出
>>> pwm2.freq()                 #傳回目前頻率
500  
>>> pwm2.duty()                #傳回目前工作週期
0

可見方波預設頻率是 500 Hz, 而工作週期預設為 0. 注意, freq() 與 duty() 沒有傳參數時為 getter方法, 有傳參數時為 setter 方法, 因此如果要更改頻率與工作週期就傳入參數, 其範圍為 :
  1. freq() : 0~1000 Hz
  2. duty() : 0~1023  (1023 為 100% 工作週期)
例如下面這兩個設定指令會讓 LED 以一半的亮度與 20Hz 頻率閃爍, 一般來說頻率在 50Hz 以上幾乎感覺不出 LED 有在閃爍 :

>>> pwm2.duty(512)     #工作週期為 512/1024=50%
>>> pwm2.freq(20)        #20 Hz

但是如果把工作週期設為 1023, 則即使頻率為 20 Hz 也不會閃爍, 因為此時工作週期為 100%, 相當於一直輸出 High, 因此 LED 會恆亮.

事實上建構式 PWM() 可接受三個參數, 除了第一參數固定為 Pin 物件外, 可以將頻率與工作週期以 freq 與 duty 具名傳入, 例如 :

pwm2=PWM(Pin(2), freq=20, duty=512)

上面偵測按鈕被按下的方法稱為輪循法 (Polling), 偵測動作的程式碼擺在主迴圈裡, 時時刻刻都要去偵測, 比較耗費時間. 另外一種偵測外部狀態變化的方法稱為硬體中斷 (IRQ), ESP8266 的每一個 GPIO 腳都可以啟動 IRQ 功能, 讓 GPIO 腳位自己去監視狀態變化, 當觸發 IRQ 時就去執行中斷處理函數, 這樣主迴圈就不用花時間去關注輸入腳的狀態了. MicroPython on ESP8266 的 IRQ 功能是透過 Machine.Pin 物件的 irq() 方法來設定, 參考 :

http://docs.micropython.org/en/latest/wipy/library/machine.Pin.html#machine.Pin.irq
Arduino 按鈕開關測試 (二) : 硬體中斷法 (Interrupt)

呼叫 irq() 方法時必須傳入兩個必要參數 trigger (觸發方式) 與 callback (中斷處理函數名稱), 其中 trigger 可用的方式有如下常數 :
  1. Pin.IRQ_LOW_LEVEL (LOW 觸發)
  2. Pin.IRQ_HIGH_LEVEL (HIGH觸發)
  3. Pin.IRQ_RISING  (上升緣都觸發)
  4. Pin.IRQ_FALLING (下降緣都觸發)
這四種觸發方式可以用 | (或) 運算子組合, 例如 :

Pin.IRQ_RISING | Pin.IRQ_FALLING (上升或下降緣都觸發)
Pin.IRQ_LOW_LEVEL | Pin.IRQ_FALLING (LOW 或下降緣都觸發)

2017-09-05 註 :

IRQ_LOW_LEVEL 與 IRQ_HIGH_LEVEL 在 ESP8266 版的 MicroPython 尚未支援, 僅 RISING 與 FALLING 可用.


另外, 定義中斷處理函數時必須傳入一個參數例如 :

def int0(p):

因為當中斷發生時, MicroPython 會向中斷處理函數傳遞一個必要參數, 即發生中斷的腳位編號例如 Pin(0), 若定義時沒有宣告此參數, 則中斷發生時會因傳不進去而產生錯誤.


測試 5 : 處理按鈕彈跳現象之方法 (3 (push_button_with_debouncing_3.py)

from machine import Pin
import time

p0=Pin(0, Pin.IN, Pin.PULL_UP)
p2=Pin(2, Pin.OUT)
state=0               #LED 狀態旗標

def int0(p):
    global state     #取用全域變數
    if state:            #反轉 LED 狀態
        state=0
    else:
        state=1

p0.irq(trigger=Pin.IRQ_FALLING, handler=int0)   #設定 GPIO 0 外部硬體中斷 (下降緣)

while True:
    p2.value(state)  #依據狀態旗標改變 LED 狀態

實際測試結果功能大致正常, 但爾而會失常, 可見 IRQ 不會處理彈跳. 下列測試 6 程式是我參考以前 Arduino 的去彈跳程式改寫而來 :

測試 6 : 處理按鈕彈跳現象之方法 (4 (push_button_with_debouncing_4.py)

from machine import Pin
import time

p0=Pin(0, Pin.IN, Pin.PULL_UP)
p2=Pin(2, Pin.OUT)
state=0
debounceDelay=300                      #彈跳期
lastMillis=0                                    #紀錄最近時戳

def debounced():                             #去彈跳函數
    global debounceDelay                #取用全域變數
    global lastMillis                          #取用全域變數 (最近時戳)
    currentMillis=time.ticks_ms()    #取得目前時戳
    if (currentMillis-lastMillis) > debounceDelay:     #目前時戳與最近時戳差已超過彈跳期
        lastMillis=currentMillis          #將最近時戳更新為目前時戳
        return True                            
    else:
        return False

def int0(p):
    global state
    if debounced():       #已經過了彈跳期
        if state:                #反轉 LED 狀態
            state=0
        else:
            state=1
    print(p,"IRQ triggered ",state)    #印出

p0.irq(trigger=Pin.IRQ_FALLING, handler=int0)

while True:
    p2.value(state)

實測確實能完美地去彈跳, REPL 輸出畫面如下 :


去彈跳處理方法大致如上, 下面測試 7 是一個 PWM 有趣的應用-呼吸燈, 這是我從下面這個網站的 Breathing LED 範例改編而來的, 此程式使用了 machine 模組中 計時器類別 Timer, 利用它週期性呼叫回呼函數來遞增與遞減 PWM 的工作週期, 讓 LED 看起來像是在呼吸一樣.

https://www.dfrobot.com/blog-606.html

測試 7 : 會呼吸的 LED 燈 (breathing_LED.py)

from machine import Pin, Timer, PWM
pwm=PWM(Pin(2),100)   #GPIO 2 PWM 頻率 100 Hz
polar=0                              #極性 : 0=由暗漸亮 1=由亮漸暗
duty=0                               #工作週期 : 0~1023

def setLed(t):                     #計時器的回呼函數 (callback)
   global duty, polar            #存取全域變數
   if polar == 0:                   #極性 : 0=由暗漸亮
       duty += 16                   #工作週期遞減
       if duty >= 1008:     #不碰頂才不會轉折時頓一下 (1008=1024-16)
           polar=1                    #將碰頂時極性反轉
   else:                                 #極性 : 1=由亮漸暗
       duty -= 16                    #工作週期遞增
       if duty <= 0:            #到底時極性反轉
           polar=0
   pwm.duty(duty)               #更新工作週期

tim=Timer(-1)                     #使用 ID=-1 的虛擬計時器
tim.init(period=10, mode=Timer.PERIODIC, callback=setLed)    #啟動計時器
#後面 try~except 部分不用亦可
try:
   while True:
       pass
except:
   tim.deinit()                       #關閉計時器
   pwm.deinit()                    #關閉 PWM


上面程式中傳入 Timer() 建構式的參數 -1 是虛擬計時器之編號, 可以任意編定一個例如 -2 或 -3 的 ID. Timer 的 mode 有 Timer.ONE_SHOT 與 Timer.PERIODIC 兩種, 參考 :

# ESP8266 micro python - Tutorial 3: Micropython Timer I2C
# class Timer – control hardware timers
class Timer – control internal timers
http://docs.micropython.org/en/v1.8.7/esp8266/esp8266/quickref.html#timers

這個程式可以加上網路功能, 透過雲端主機偵測遠端的設備是否運作正常 (Hearbeat). 

參考 :

ESP8266 Tutorial: Programming the Onboard GPIO Pins
How to Use the ESP8266-01 Pins
How to Unbrick an ESP8266 – Using ESP-03 As Example
# ESP8266: Controlling a buzzer

2017-08-03 補充 :

關於 PWM 用法在下面這篇中也有用到 :

# MicroPython on ESP8266 (十六) : 蜂鳴器測試


2017-09-05 補充 :

在新版韌體中 Pin 物件已無 high() 與 low() 方法, 必須用 value(1) 與 value(0) 才行. 另外, 在 ESP-12 模組, WeMOS D1 Mini, 或 NodeMCU 板子上有內建一個接在 GPIO 2 的 LED 可用來顯示程式執行狀態, 例如尚未連線成功快閃, 連線成功後改為慢閃等. 注意, GPIO2 在 ESP8266 晶片內有 10K 歐姆上拉電阻, 因此要輸出 value(0) 才是亮燈, 不是 value(1) :

from machine import Pin
p2=Pin(2, Pin.OUT)
p2.value(1)    #輸出 High 位準 (LED 燈滅)
p2.value(0)    #輸出 Low 位準 (LED 燈亮)

1 則留言 :

Unknown 提到...

老師您好 我是參考這個影片去做測試
https://youtu.be/hrjtAYMrxF4?t=504
但我的Pin物件似乎找不到high或low
請問有可能是缺少套件安裝嗎?
AttributeError: 'Pin' object has no attribute 'high'