본문 바로가기

Programming/Python

[Python] 라즈베리 파이의 GPIO를 이용해 스위치/LED 제어하기 (2)

 지난 번 글에서 라즈베리 파이의 GPIO를 이용해 스위치/LED를 제어해보고 이를 토대로 여러 프로그램을 만들어봤었는데, bash로만 작성했던 게 왠지 마음에 걸려, 이제는 우리의 마음의 고향(?)같은 파이썬으로 작성하는 법을 적어보려고 한다. 각 언어별 라이브러리 벤치마킹 결과 표가 기억이 날 것이다.


 보다시피 파이썬에서는 GPIO 제어를 위해 두 가지 라이브러리를 이용할 수 있는데, 둘 중에서 가장 속도가 빠르고 무엇보다 라즈베리 파이에서 기본적으로 제공하고 있는 RPi.GPIO 라이브러리/모듈을 사용할 계획이다.





1. RPi.GPIO 라이브러리


 지난 번에 우리가 이용했던 bash 쉘 스크립트 언어는 그야말로 고통이다. 조건문의 대괄호와 안에 들은 내용끼리 붙어서는 안된다던가(ex) [$num -eq 1](X) [ $sw1 -eq $push ](O)) 변수를 참조할때는 꼭 $를 앞에 달아야한다던가 등등 깐깐함을 넘어 융통성마저 안 느껴지는 쉘 스크립트의 문법은 괴랄함 그 자체였다.


 파이썬은 우리가 여러 프로젝트를 진행해보면서 이미 어느정도 적응은 된 상태이고, RPi.GPIO 라이브러리의 사용법이 매우 쉽고 가독성이 좋다. 파이썬의 문법이 쉬운 편이기도 하니, 우리가 지난 번에 작성했던 프로그램들을 포팅(porting)하는 데 큰 어려움이 없을 것이다.


 …우선 사용하기 전에 업데이트 확인부터 먼저 해주도록 하자.


$ sudo apt-get update

$ sudo apt-get upgrade


※ 사실 필자도 이 둘을 쓰면서 둘의 차이가 뭐지? 싶었는데, update는 무엇을 업데이트해야 하는지 버전을 '확인'하는 과정이고, upgrade가 실제 업데이트 과정을 '진행'하는 명령어라고 한다.


 이제 RPi.GPIO 라이브러리의 기본 문법을 알아볼 차례다.



import RPi.GPIO as GPIO 



가장 기본적인 import 작업. RPi.GPIO 라이브러리를 불러온다. 일일히 RPi.GPIO라고 쳐주기엔 고된 일이니 GPIO로 퉁칠 수 있도록 define 해준다.



GPIO.setmode(GPIO.BCM)
GPIO.setmode(GPIO.BOARD)



GPIO의 핀/포트 번호를 어떤 모드로 참조할 것인지를 설정한다. 우리는 지난 번 쉘 스크립트를 작성할 때 BCM283x를 참조하는 wiringPi 라이브러리를 사용했었다. BCM 모드도 그것과 마찬가지로, GPIO 핀 번호를 사용하는 데 있어 쉽고 직관적이기 때문에 BCM을 사용하도록 하자.


BOARD 모드는 지난 글의 서문 부분에 붙여놨던 그 그림에 적힌 번호(=물리적인 핀 번호)를 사용하게 된다. GPIO18 핀을 사용하고자 할 때 BCM 모드는 18을, BOARD 모드는 12를 사용한다고 보면 되겠다.





GPIO.setup(pin/port number, GPIO.IN) 
GPIO.setup(pin/port number, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.setup(pin/port number, GPIO.IN, pull_up_down=GPIO.PUD_UP) 




GPIO.setup(pin/port number, GPIO.OUT)  
GPIO.setup(pin/port number, GPIO.OUT, initial=1)



GPIO.setup은 어떤 GPIO 핀의 입력/출력 방향(direction)을 설정하는 기능을 한다. 입/출력을 설정할 대상 핀/포트 번호를 첫 번째 인자로 받고, 입력(INput)으로 설정할 것인지 출력(OUTput)으로 할 것인지를 두 번째 인자로 받는다.


입력으로 설정됐을 때, 세 번째 인자로 풀업/풀다운 저항을 설정할 것인지 여부를 추가로 받을 수 있다. 지난 번 쉘 스크립트에서도 풀업 저항을 사용했기에, 파이썬 프로그래밍에서도 풀업으로 설정할 것이다.


출력으로 설정됐을 때는 초기값을 얼마로 줄 것인지를 세 번째 인자로 추가적으로 받을 수 있다. initial에는 0 또는 1 값을 입력할 수 있다.



GPIO.output(pin/port number, 1
GPIO.output(pin/port number, 0)



GPIO.output은 출력 모드로 설정된 핀에 0 또는 1값을 할당하는 역할을 한다. 예를 들어 LED를 켜고자 할 때는 1값을, 끄고자 할 때는 0값을 주는 것이다. 숫자값들은 다음과 같이 치환할 수 있다. 본인에게 편한 걸 골라서 써도 된다:


    • 1 = GPIO.HIGH = True
    • 0 = GPIO.LOW = False



= GPIO.input(pin/port number) 



GPIO.input은 입력 모드로 설정된 핀으로부터 값을 읽어온다. 예를 들어 스위치가 눌렸을 때는 0값을, 떼졌을 때 1값을 받아온다. 받아온 값은 위 예시처럼 변수에 할당할 수 있다. 아니면 다음과 같이 바로 조건문에 사용될 수 있다:


1
2
3
4
if GPIO.input(25):  # if pin 25 == 1  
    print "Pin 25 is 1/GPIO.HIGH/True"  
else:   # if pin 25 == 0
    print "Pin 25 is 0/GPIO.LOW/False"  



이제 엣지 검출(edge detection)과 관련된 메소드들을 알아보자.



= GPIO.wait_for_edge(pin/port number, GPIO.RISING, timeout=5000)



GPIO.wait_for_edge는 어떤 입력 핀으로부터(첫 번째 인자) 특정 엣지가(두 번째 인자) 발생할 때까지 기다린다. 위 예시처럼 변수에 할당할 수도 있다. 세 번째 인자는 엣지 검출까지 기다릴 시간을 마이크로초(ms; ex: 5000ms = 5s) 단위로 받을 수 있다.


두 번째 인자에 들어갈 수 있는 엣지의 종류로는 라이징 엣지(GPIO.RISING), 폴링 엣지(GPIO.FALLING), 혹은 둘 다(GPIO.BOTH)가 있다. 보통 스위치를 예로 들 때, 스위치를 누를 경우 폴링 엣지, 스위치가 떼졌을 때 라이징 엣지가 발생한다. 다음과 같이 조건문에 이용될 수 있다:


1
2
3
4
5
pin = GPIO.wait_for_edge(pin/port number, GPIO.FALLING, timeout=5000)
if pin is None:
    print('Timeout occurred')
else:
    print('Edge detected on the pin/port', pin)






GPIO.add_event_detect(pin/port number, GPIO.RISING, callback=my_callback, bouncetime=300)



GPIO.add_event_detect는 어떤 입력 핀으로부터(첫 번째 인자) 특정 엣지가(두 번째 인자) 발생하는지는 감지하고, 특정 엣지가 발생했을 경우 callback에 할당한 함수(세 번째 인자)를 실행하는 'event handler'이다. 이름 그대로 어떤 핀을 특정 입력 이벤트가 발생하는지 감시 대상에 추가한다는 것이다.


네 번째 인자는 바운싱/채터링을 방지하기 위해 잠시 delay하고자 하는 시간을 마이크로초(ms) 단위로 받는다.




GPIO.remove_event_detect(pin/port number)



GPIO.remove_event_detect는 add_event_detect로 감지 대상에 추가했던 특정 핀을 감지 대상에서 다시 제외시키는 기능을 한다. add와 remove라는 단어만으로 어떤 역할들을 하는지 바로 감이 잡힐 것이다. 



GPIO.cleanup() 



마지막으로 GPIO.cleanup()은 파이썬 프로그램 안에서 초기화했던 핀 설정들을 모두 '청소'(clean up)해주는 기능, 즉 GPIO 라이브러리/모듈이 점유한 리소스를 해제하는 기능을 한다. 


GPIO의 사용이 모두 끝날 때(=프로그램이 종료될 때) GPIO.cleanup() 함수를 호출하도록 하는 것이 좋고, 라즈베리 파이 개발자 측에서도 강력히 권장하는 사항이다.


 그밖에도 더 궁금한 점이 있다면 매뉴얼을 참고하자. 다만, 공식적으로 지원하는 라이브러리/모듈임에도 매뉴얼의 완성도가 미흡한 게 아쉬울 따름이다: https://sourceforge.net/p/raspberry-gpio-python/wiki/Examples/




2. 파이썬을 이용해 LED369 프로그램 작성하기


 기본적인 RPi.GPIO 라이브러리 사용법을 익혔으니, 이제 우리가 이전에 쉘 스크립트로 작성했었던 LED 369[각주:1] 프로그램을 파이썬으로 다시 작성해보도록 하자.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import RPi.GPIO as GPIO 
from time import sleep
 
GPIO.setmode(GPIO.BCM)
 
GPIO.setup(18, GPIO.OUT)
GPIO.setup(23, GPIO.OUT)
GPIO.setup(24, GPIO.OUT)
GPIO.setup(25, GPIO.OUT)
 
GPIO.setup(21, GPIO.IN, pull_up_down=GPIO.PUD_UP)
 
GPIO.output(18, False)
GPIO.output(23, False)
GPIO.output(24, False)
GPIO.output(25, False)
 
LED_list=[18232425



GPIO 핀을 초기화하는 부분이다. 추후 sleep기능을 사용하기 위해 sleep 또한 같이 import 해준다.


GPIO를 BCM 모드로 설정하고, LED용으로 사용한 18, 23, 24, 25번 핀을 출력 모드로 설정하고 꺼진 상태(0)로 초기화한다. 스위치용으로 사용한 21번 핀을 출력 모드로 설정하고, 풀업으로 설정한다. LED용 핀 번호를 led_list 리스트에 저장한다. 


참고로 setup과 output, input 메소드는 핀/포트 번호를 받는 첫 번째 인자가 리스트 자료형도 받을 수 있다. 그렇다면 다음과 같이 소스가 줄어들 수 있겠다:


1
2
3
4
5
6
7
8
9
10
11
import RPi.GPIO as GPIO 
from time import sleep

GPIO.setmode(GPIO.BCM)

LED_list=[18232425]

GPIO.setup(21, GPIO.IN, pull_up_down=GPIO.PUD_UP)

GPIO.setup(LED_list, GPIO.OUT)
GPIO.output(LED_list, False)



1
2
3
4
5
6
7
8
9
10
GPIO.add_event_detect(21, GPIO.FALLING, callback=LED369, bouncetime=300)
print("Wait for the switch event.")
 
while True:
    try:
        sleep(5)
    except KeyboardInterrupt:
        print("Au revoir!".center(20))
        GPIO.cleanup()
        break



main문에 해당한다고 볼 수 있는 부분이다. 스위치용으로 설정된 21번 핀이 눌릴 때(GPIO.FALLING)를 감지해, 폴링 엣지가 발생할 경우 LED369 함수를 호출한다. 바운싱/채터링 방지 시간은 300ms로 설정했다.


add_event_detect 메소드는 한 줄만으로 프로그램 내에서 계속해서 엣지 이벤트를 감지하도록 설정되기 때문에, while문 등 루프문에 넣지 않도록 한다.


while문 안에서는 아무 일도 하지 않으면서 엣지를 계속해서 감지할 수 있도록 sleep 함수를 집어넣는다. 만약에 KeyboardInterrupt(= Ctrl+C와 같은 키보드 이용 인터럽트)가 발생할 경우, cleanup 메소드를 통해 GPIO 라이브러리/모듈이 점유한 리소스를 모두 해제하고 while문을 나옴으로써 프로그램을 종료시킨다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def LED369(channel):
    print("LED369 activated")
 
    for i in range(1101):
        num = (i-1) % 4
        num_str = str(i)
        flag_369 = 0
 
        if (num_str.find('3'!= -or num_str.find('6'!= -or num_str.find('9'!= -1):
            print("x(%s)" % num_str)
            flag_369 = 1
        else:
            print(num_str)
 
        if (flag_369 == 1):
            GPIO.output(LED_list[num], False)
        else:
            GPIO.output(LED_list[num], True)
 
        sleep(0.5)
        GPIO.output(LED_list[num], False)



스위치가 눌렸을 때 호출되는 LED369 함수이다. 호출됐을 때 "LED369 activated"라는 문자열을 출력한다. 그 후, i=1부터 100까지 루프를 돌며 LED 369를 수행하는 연산에 들어간다. (※ 파이썬의 for문에서 range는 첫 번째 인자 이상-두 번째 인자 '미만'이기 때문에 101은 포함되지 않는다)


LED_list 리스트의 인덱스 값으로 사용하기 위해, 모듈로 연산(%)으로 i-1 값을 4(=LED_list 리스트의 길이)로 나눴을 때 나머지값이 저장되는 num 변수를 생성한다. 그리고 현재 숫자값을 가지는 i를 문자열로 받는 num_str 변수도 생성해준다. 그리고 현재 숫자에 3, 6, 9가 들어갔는지 여부를 체크하기 위한 flag_369 플래그 변수를 생성한다.


문자열 메소드 중 find는 특정 문자열에서 찾고자하는 문자열의 오프셋(위치) 값을 리턴하는 기능을 하는데, 찾는 문자열이 없을 경우 -1을 리턴하는 게 포인트다. 현재 숫자를 문자열로 저장 중인 num_str에서 문자열 3, 6, 9를 find하고, 만약에 find가 위치 값을 리턴한다면(= -1을 리턴하지 않는다면) 3, 6, 9가 들어간 것으로 간주하고 flag_369를 활성화시켜준다.


그래서 flag_369가 활성화된 숫자/순서의 LED는 켜지지 않고, 그 외의 경우엔 LED가 켜진 후, 0.5초 뒤에 꺼지도록 작동한다.



…아니면 flag_369와 같은 플래그 변수를 사용하지 않고 다음과 같이 바로 조건문 안에 LED 출력 설정 기능을 넣을 수도 있겠다:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def LED369(channel):
    print("LED369 activated")
 
    for i in range(1101):
        num = (i-1) % 4
        num_str = str(i)
 
        if (num_str.find('3'!= -or num_str.find('6'!= -or num_str.find('9'!= -1):
            print("x(%s)" % num_str)
        else
            GPIO.output(LED_list[num], True)
            print(num_str)
 
        sleep(0.5)
        GPIO.output(LED_list[num], False)



 이를 합쳐서 다음과 같은 최종 소스가 나온다. 파이썬/RPi.GPIO로 작성하니 더욱 간단하고 가독성이 좋게 작성이 되는 것을 체감할 수 있다:







3. 수행 결과



 LED 369 프로그램을 수행한 결과를 촬영한 영상이다.

  1. 스위치를 눌렀을 때 LED가 1부터 100번째까지 순차적으로 켜지지만, 숫자 3, 6, 9가 들어가는 순서의 LED는 켜지지 않음. [본문으로]