2023年3月11日 星期六

樹莓派學習筆記 : GPIO 測試 (一) RPi.GPIO 模組

我手邊的樹莓派除了Pi 2 與 Pi 4 外全系列都至少有一塊, 最近 Mapleboard 伺服主機即將架好, 打算好好地利用樹莓派, 拿來用在物聯網應用上, 但首先要把 GPIO 搞定才行, 我之前有整理過 GPIO 筆記. 參考 : 


本系列測試參考了如下書籍 : 

# Raspberry Pi 最佳入門與應用 (全華, 王玉樹, 母校第二版, 市圖第三版)
# AIoT 與 OpenCV 實戰應用 (碁峰, 朱克剛, 2020)


1. GPIO 接腳 : 

GPIO (General Purpose Input Output) 為 MCU 的數位輸出入腳, 是 MCU 與感測器 (sensor, 例如溫溼度, 紅外線與超音波等) 以及致動器 (actuator, 例如繼電器, 馬達等) 溝通的橋樑. GPIO 作為輸入時可讀不可寫; 作為輸出時可寫也可讀. GPIO 作為輸入時通常用來量測外界狀態 (ON/OFF) 或感測是否有中斷發生. 

樹莓派的 GPIO 最早為 26 腳, 但自 B+ 起增為 40 支腳 : 




GPIO 針腳的位置有兩種指涉方式, 一是使用 BCM (Broadcom) 的 MCU 的輸出入腳名稱編號 GPIO0~GPIO27, 範圍從 0~27; 二是使用板子上的位置編號, 範圍從 3~40 (中間有些是 GND 與 VCC), 其對映如下圖所示 :




位置與用途如下表 :


 功能 位置名稱 (GPIO.BCM) 位置編號 (GPIO.BOARD)
 5V 電源輸出 5V 2, 4
 3.3V 電源輸出 3.3V 1, 17
 接地 GND 6, 9, 14, 20, 25, 30, 34, 39
 硬體 PWM (脈波調變) GPIO12
 GPIO13
 GPIO18
 GPIO19
 32
 33
 12
 35
 SPI 通訊 0 GPIO10 (MOSI)
 GPIO9 (MISO)
 SCLK (GPIO11)
 CE0 (GPIO8)
 CE1 (GPIO7)
 19
 21
 23
 24
 26
 SPI 通訊 1 GPIO20 (MOSI)
 GPIO19 (MISO)
 SCLK (GPIO21)
 CE0 (GPIO18)
 CE1 (GPIO17)
 CE2 (GPIO16)
 38
 35
 40
 12
 11
 36
 I2C 通訊 GPIO2 (Data)
 GPIO3 (Clock)
 GPIO0 (EEPROM Data)
 GPIO1 (EEPROM Clock)
 3
 5
 27
 28
 UART 串列埠 GPIO14 (TX)
 GPIO15 (RX)
 8
 10


部分 GPIO 接腳有特定用途, 如下表所示  : 


 特定功能 說明 (BCM 編號)
 UART 14 (TX), 15 (RX)
 I2C 0 (EEPROM Data), 1 (EEPROM clock), 2 (SDA), 3 (SCL)
 SPI0 7 (CE1), 8 (CE0), 9 (MISO), 10 (MOSI), 11 (SCLK) 
 SPI1 16 (CE2), 17 (CE1), 18 (CE2), 19 (MISO), 20 (MOSI), 21 (SCLK)
 PWM  12, 13, 18, 19 (硬體 PWM), 全部 GPIO 均可軟體設定為 PWM 功能


注意, 除了四個硬體 PWM 腳外, 每一個 GPIO 腳都可以設定為軟體 PWM. 

SPI 是 Motolora 制定的全雙工主從式通訊界面, 一個主機可同時接多個從機, 利用選擇線 CE 來從機要通訊的從機, 傳輸速度可達 10 Mbps 以上, 資料用 MOSI (主機輸出, 從機輸入) 與 MISO (主機輸入, 從機輸出) 這兩條線做雙向傳輸. 

I2C 是飛利浦公司制定的半雙工主從式通訊界面, 它只使用 SDA (傳資料用) 與 SCL (同步時脈) 兩條線進行通訊. 同樣一個主機可同時接多個從機, 但它不是像 SPI 那樣用 CE 線來選擇從機, 而是利用燒在晶片內的位址來選擇, 傳輸速度最高 3.2 Mbps. 由於 I2C 的 SDA 與 SCL 都是 Open Drain 結構, 故使用時須分別用一個上拉電阻接到 VCC 以免受到雜訊干擾. 

GPIO 的電氣特性摘要如下 :
  • GPIO 電壓位準為 3.3V, 不可直接與 5V 感測器相接 (須經位準轉換模組介接).
  • 每個 GPIO 腳最大輸出電流 16mA, 全部 GPIO 腳總輸出電流不可超過 100 mA.

2. 使用 RPi.GPIO 模組 : 

由於樹莓派廣受教育界與工業界使用, Python 將樹莓派的 GPIO 功能納入 Python 中, 此模組稱為RPi.GPIO, 參考 : 


教學文件參考 :


樹莓派作業系統內的 Python 已搭載此模組, 不須另外安裝, 直接用 import 即可, 匯入時通常取別名為 GPIO :

import RPi.GPIO as GPIO    

可用 VERSION 常數檢視此模組之版本 :

>>> import RPi.GPIO   
>>> GPIO.VERSION      
'0.7.0'

可用 pip3 install --upgrade 升版 :

pi@raspberrypi:~ $ pip install --upgrade RPi.GPIO    
Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
Collecting RPi.GPIO
  Downloading https://files.pythonhosted.org/packages/0c/c7/5f7aa692960e0eb187fa8206ab1f219757a259570c634fe15e2ca7d95aa2/RPi.GPIO-0.7.1-cp37-cp37m-linux_armv6l.whl (70kB)
Installing collected packages: RPi.GPIO
Successfully installed RPi.GPIO-0.7.1

這時關掉 Thonny 重開, 再檢查 VERSION 常數就變成 0.7.1 版了 : 

>>> GPIO.VERSION   
'0.7.1'

先來檢視 GPIO 模組的成員, 參考下面這篇文章中的做法 :


此文使用一個自訂模組 members 中的 list_members() 函式來顯示模組, 物件, 或類別之成員 :

# members.py
import inspect 
def varname(x): 
    return [k for k,v in inspect.currentframe().f_back.f_locals.items() if v is x][0]
def list_members(parent_obj):
    members=dir(parent_obj)
    parent_obj_name=varname(parent_obj)       
    for mbr in members:
        child_obj=eval(parent_obj_name + '.' + mbr) 
        if not mbr.startswith('_'):
            print(mbr, type(child_obj))

利用 Thonny 將上面程式存成 members.py, 然後用它來檢視 GPIO 模組成員 :

>>> import RPi.GPIO as GPIO   
>>> import members   
>>> members.list_members(GPIO)    
BCM <class 'int'>
BOARD <class 'int'>
BOTH <class 'int'>
FALLING <class 'int'>
HARD_PWM <class 'int'>
HIGH <class 'int'>
I2C <class 'int'>
IN <class 'int'>
LOW <class 'int'>
OUT <class 'int'>
PUD_DOWN <class 'int'>
PUD_OFF <class 'int'>
PUD_UP <class 'int'>
PWM <class 'type'>
RISING <class 'int'>
RPI_INFO <class 'dict'>
RPI_REVISION <class 'int'>
SERIAL <class 'int'>
SPI <class 'int'>
UNKNOWN <class 'int'>
VERSION <class 'str'>
add_event_callback <class 'builtin_function_or_method'>
add_event_detect <class 'builtin_function_or_method'>
cleanup <class 'builtin_function_or_method'>
event_detected <class 'builtin_function_or_method'>
getmode <class 'builtin_function_or_method'>
gpio_function <class 'builtin_function_or_method'>
input <class 'builtin_function_or_method'>
output <class 'builtin_function_or_method'>
remove_event_detect <class 'builtin_function_or_method'>
setmode <class 'builtin_function_or_method'>
setup <class 'builtin_function_or_method'>
setwarnings <class 'builtin_function_or_method'>
wait_for_edge <class 'builtin_function_or_method'>

可見 RPi.GPIO 模組中定義了一些常數, 常用者如下表 : 


 RPi.GPIO 常數 說明
 GPIO.BOARD 使用位置編號模式 
 GPIO.BCM 使用位置名稱模式 (Broadcom 編號)
 GPIO.IN 用於 setup() 函式的 type 參數, 用來設定腳位為輸入模式
 GPIO.OUT 用於 setup() 函式的 type 參數, 用來設定腳位為輸出模式
 GPIO.PUD_DOWN 用於 setup() 函式的 pull_up_down 參數用來啟用內建之下拉電阻
 GPIO.PUD_UP 用於 setup() 函式的 pull_up_down 參數用來啟用內建之上拉電阻
 GPIO.RISING 指定於上升緣發生中斷
 GPIO.FALLING 指定於下降緣發生中斷
 GPIO.BOTH 指定於上升緣或下降緣發生中斷


常用函式如下表 : 


 RPi.GPIO 函式 說明
 setmode(mode) 設定針腳指涉模式 : GPIO.BOARD (編號) 或 GPIO.BCM (名稱)
 setwarnings(bool) 設定是否要顯示警告訊息 (True=顯示, False=不顯示)
 setup(pin, type) 設定腳位 pin 的型態, type=GPIO.IN (輸入)/GPIO.OUT (輸出) 
 gpio_function(pin) 查詢腳位 pin 的型態, GPIO 腳傳回 1, 否則觸發例外 
 output(pin, state) 設定腳位 pin 的輸出位準, state=GPIO.LOW/GPIO.HIGH
 input(pin) 讀取腳位 pin 的輸入位準, 傳回 GPIO.HIGH/GPIO.LOW
 PWM(pin, freq) 設定針腳 pin 為軟體 PWM 輸出, 頻率為 freq (Hz), 傳回 PWM 物件 
 cleanup() 清除所有腳位之設定 
 wait_for_edge(pin, mode, timeout) 監視中斷腳位 pin 是否在 timeout 秒數內出現 mode 變化 (RISING/FALLING/BOTH), 是傳回中斷腳位之 GPIO 編號, 否則傳回 None
 add_event_detect(pin, mode, callback) 監視中斷腳位 pin 是否出現 mode 變化 (RISING/FALLING/BOTH), 是就呼叫中斷處理函式 callback.  


注意, output() 與 setup() 也可以同時對多個針腳進行設定, 只要將腳位編號組成一個 tuple 或 list 傳給第一參數 pin 即可. 呼叫 PWM() 建構函式會傳回 PWM 物件, 然後再呼叫其 start() 方法才會開始輸出 PWM. 

在使用 GPIO 之前必須用 GPIO.setmode() 函式設定使用哪種模式 :
  • GPIO.setmode(GPIO.BOARD) : 使用位置編號模式 (例如 23)
  • GPIO.setmode(GPIO.BCM) : 使用位置名稱模式 (例如 GPIO15) 
GPIO 程式末尾最好呼叫 GPIO.cleanup() 清除所有腳位之設定, 否則重新執行程式時會出現某個 GPIO 腳位正在使用中的警告訊息 (this channel is already in use), 雖然程式還是有被正常執行, 但看到這個 Warining 總是令人不舒服, 也可以在程式開始時就呼叫 setwarnings(Fasle) 關閉警告. 

呼叫 GPIO.gpio_function() 並傳入針腳編號, 若該腳為 GPIO 腳會傳回 1, 否則會觸發例外, 例如 :

>>> import RPi.GPIO as GPIO   
>>> GPIO.setmode(GPIO.BOARD)    # 板子位置編號
>>> GPIO.gpio_function(1)               # 腳位 1 是 Vcc, 不是 GPIO 腳 -> 觸發例外
Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
ValueError: The channel sent is invalid on a Raspberry Pi 
>>> GPIO.gpio_function(3)               # 腳位 1 是 GPIO2
>>> for i in range(1, 41):   
    try:   
        f=GPIO.gpio_function(i)   
    except Exception:   
        f='invalid'   
    print(f'Pin {i} function : {f}')    
    
Pin 1 function : invalid
Pin 2 function : invalid
Pin 3 function : 1
Pin 4 function : invalid
Pin 5 function : 1
Pin 6 function : invalid
Pin 7 function : 1
Pin 8 function : 1
Pin 9 function : invalid
Pin 10 function : 1
Pin 11 function : 1
Pin 12 function : 1
Pin 13 function : 1
Pin 14 function : invalid
Pin 15 function : 1
Pin 16 function : 1
Pin 17 function : invalid
Pin 18 function : 1
Pin 19 function : 1
Pin 20 function : invalid
Pin 21 function : 1
Pin 22 function : 1
Pin 23 function : 1
Pin 24 function : 1
Pin 25 function : invalid
Pin 26 function : 1
Pin 27 function : invalid
Pin 28 function : invalid
Pin 29 function : 1
Pin 30 function : invalid
Pin 31 function : 1
Pin 32 function : 1
Pin 33 function : 1
Pin 34 function : invalid
Pin 35 function : 1
Pin 36 function : 1
Pin 37 function : 1
Pin 38 function : 1
Pin 39 function : invalid
Pin 40 function : 1

與上面的 GPIO 腳位置對照是一樣的 (傳回 1 表示為 GPIO 腳)


測試 2-1 : 控制 LED 燈明滅 [看原始碼]

# rpi-gpio-led-test-2-1.py
import RPi.GPIO as GPIO
import time

LED=3    # GPIO2
GPIO.setmode(GPIO.BOARD) 
GPIO.setup(LED, GPIO.OUT)

n=10
while n>0:
    print('HIGH')
    GPIO.output(LED, GPIO.HIGH)    # 也可用 1 取代 GPIO.HIGH
    time.sleep(0.5)
    print('LOW')
    GPIO.output(LED, GPIO.LOW)     # 也可用 0 取代 GPIO.LOW
    time.sleep(0.5)    
    n -= 1
GPIO.cleanup()

此程式設定使用 BOARD 模式來使用 GPIO 腳位, 指定編號 3 腳位 (GPIO2) 作為 LED 輸出腳, 然後在一個 10 次的迴圈中明滅 LED, 所以執行後 LED 會在閃 10 次后停止. 結果如下 :

>>> %Run rpi-gpio-led-test-1.py   
rpi-gpio-led-test-1.py:6: RuntimeWarning: This channel is already in use, continuing anyway.  Use GPIO.setwarnings(False) to disable warnings.
  GPIO.setup(LED, GPIO.OUT)
HIGH
LOW
HIGH
LOW
HIGH
LOW
HIGH
LOW
HIGH
LOW
HIGH
LOW
HIGH
LOW
HIGH
LOW
HIGH
LOW
HIGH
LOW




此例如果將腳位指定模式改為 GPIO.BCM, 亦即使用 GPIO 編號的話, 則 LED 要設為 2 (因為第三支腳為 GPIO2). 

下面的測試使用無限迴圈讓 LED 不斷地閃爍 : 


測試 2-2 : 利用無限迴圈讓 LED 燈不停閃爍 (1) [看原始碼]

# rpi-gpio-led-test-2-2.py
import RPi.GPIO as GPIO
import time

LED=3
GPIO.setmode(GPIO.BOARD) 
GPIO.setup(LED, GPIO.OUT)

while True:
    print('HIGH')
    GPIO.output(LED, 1)
    time.sleep(0.5)
    print('LOW')
    GPIO.output(LED, 0)
    time.sleep(0.5)  
GPIO.cleanup()

此例無限迴圈會讓 LED 不斷閃爍, 要讓程式停止執行是按 Ctrl + C, 或在 Thonny 點選 "執行/中斷程式執行" : 




>>> %Run rpi-gpio-led-test-2.py
HIGH
LOW
HIGH
LOW
HIGH
LOW
HIGH
LOW
HIGH
LOW
......
HIGH
LOW
HIGH
LOW
HIGH
LOW
Traceback (most recent call last):
  File "/home/pi/rpi-gpio-led-test-2.py", line 14, in <module>
    time.sleep(0.5)
KeyboardInterrupt

但這樣子最後一行的 GPIO.cleanup() 就不會被執行, 再度執行此程式時就會出現 "This channel is already in use" 的警告 :  

>>> %Run rpi-gpio-led-test-2.py
rpi-gpio-led-test-2.py:6: RuntimeWarning: This channel is already in use, continuing anyway.  Use GPIO.setwarnings(False) to disable warnings.
  GPIO.setup(LED, GPIO.OUT)
HIGH
LOW
HIGH
Traceback (most recent call last):
  File "/home/pi/rpi-gpio-led-test-2.py", line 11, in <module>
    time.sleep(0.5)
KeyboardInterrupt

根據 "AIoT 與 OpenCV 實戰應用" 這本書的解決辦法, 是使用 try except 捕捉鍵盤中斷, 當按下 Ctrl+C 中斷程式時讓它 pass, 這樣就會執行 cleanup() 了, 如下例所示 : 


測試 2-3 : 利用無限迴圈讓 LED 燈不停閃爍 (2) [看原始碼]

# rpi-gpio-led-test-2-3.py
import RPi.GPIO as GPIO
import time

LED=3
GPIO.setmode(GPIO.BOARD) 
GPIO.setup(LED, GPIO.OUT)

try:
    while True:
        print('HIGH')
        GPIO.output(LED, 1)
        time.sleep(0.5)
        print('LOW')
        GPIO.output(LED, 0)
        time.sleep(0.5)
except Exception:    # 中斷發生時不處理
    pass
     
GPIO.cleanup()

但我實測結果無效, 再度執行程式時仍然出現 "This channel is already in use" 的警告 :

>>> %Run rpi-gpio-led-test-3.py
rpi-gpio-led-test-3.py:6: RuntimeWarning: This channel is already in use, continuing anyway.  Use GPIO.setwarnings(False) to disable warnings.
  GPIO.setup(LED, GPIO.OUT)
HIGH
LOW
HIGH
Traceback (most recent call last):
  File "/home/pi/rpi-gpio-led-test-3.py", line 12, in <module>
    time.sleep(0.5)
KeyboardInterrupt
>>> KeyboardInterrupt

所以如果不想看到此警告, 還是在程式頭用 setwarnings(Flase) 關掉.  

下面是 4 個 LED 跑馬燈的範例 : 


測試 2-4 : 四個 LED 燈的跑馬燈 [看原始碼]

# rpi-gpio-led-test-2-4.py
import RPi.GPIO as GPIO
import time

led_pins=[2, 3, 4, 17]              # GPIO2/3/4/17=BOARD3/5/7/11
GPIO.setmode(GPIO.BCM)
for i in led_pins:
    GPIO.setup(i, GPIO.OUT)
    GPIO.output(i, GPIO.LOW)

n=10
while n>0:
    for i in led_pins:
        print(f'LED{i} is ON')
        GPIO.output(i, GPIO.HIGH)  
        time.sleep(0.5)
        print(f'LED{i} is OFF')
        GPIO.output(i, GPIO.LOW)    
        time.sleep(0.5)    
    n -= 1
GPIO.cleanup()

此例使用 BCM 模式來存取 GPIO 接腳, 將 GPIO2, GPIO3, GPIO4, 與 GPIO17 接到 LED 陽極, 然後經 4 個電阻接地, 故輸出 HIGH 燈亮, 輸出 LOW 燈滅, 結果如下 :




沒有留言 :