라즈베리파이(영어: Raspberry Pi)는 영국 잉글랜드의 라즈베리 파이 재단이 학교와 개발도상국에서 기초 컴퓨터 과학의 교육을 증진시키기 위해 개발한 신용카드 크기의 싱글 보드 컴퓨터이다. 아두이노와 함께 IoT 분야의 주역으로 성장하였다.
다음과 같이 싱글 보드 형태의 컴퓨터로 출시하였지만, 다양한 주변기기와 부속품을 통해 보다 많은 분야에서 활용되고 있다.
GPIO는 General Purpose Input Output의 약자로 라즈베리파이와 전자적으로 통신하기위한 표준포트이다. GPIO를 통해 쉽게 전자회로를 통제하고 원하는 사물인터넷 기기를 만들 수 있다.
이번에 사용한 라즈베리 파이 모델 3의 경우는 총 40개의 핀이 있다.
그 중 GPIO는 입력용과 출력용으로 사용할 수 있다. GPIO가 사용하는 표준 전압은 3.3V이고 이보다 높을 경우 보드가 손상을 입을 수 있다. GPIO를 통해 입력 또는 출력되는 전압이 대략 1.7V 이하이면 0, 이상이면 1로 인식한다. 이를 이용하여 주변 전자장치와 신호를 주고 받을 수 있다.
GPIO핀에는 5밀리암페어의 적은 전류만 공급되는데, 이를 직접 이용해서는 조그만 LED정도만 켜고 끌수 있다. 전원은 3.3V와 5V 두종류가 있고, GND는 접지용도로 사용된다. GPIO핀중 뒤에 수식어가 붙어서 특수용도로 사용되는 핀은 SDA, SCL이다. 시계와 데이터 회선용도로 사용가능하다. 블루투스 전자모듈처럼 직렬통신을 할 경우 수신용 RX와 송신용 TX포트가 있다. MOSI, MISO, SCK 또한 직렬 통신에 사용된다.
이와 같은 직렬 인터페이스를 직렬주변기기인터페이스버스(SPI)라고 부른다.
PWM핀은 모터와 LED를 제어할 떄 단순히 디지털로 껐다, 켰다로만 사용하는 것이 아니라 조금씩 강하게, 약하게 신호를 주고받을 수 있게 해준다. 자동차의 속도를 조절할때 PWM을 사용할 것이다.
라즈비안은 라즈베리파이에서 사용하는 OS이다. 라즈비안은 Linux를 기반으로 구성된 OS이다. 이번 프로젝트는 라즈비안을 이용하여 진행할 것이다. www.raspberrypi.org 홈페이지에서 라즈비안 img파일을 다운로드 받는다.
포맷된 SD카드를 준비하고 Etcher 프로그램을 다운받는다.
다음과 같이 Etcher프로그램을 통해 micro SD카드에 라즈비안 img파일을 설치한다.
다음은 라즈비안을 라즈베리파이에 설치를 완료한 모습이다.
라즈베리파이에 모니터와 마우스, 키보드를 연결하기 번거로우니 원격으로 컴퓨터를 제어할 수 있는 VNC를 사용하여 사용중인 PC로 제어한다.
VNC Server란에 라즈베리파이가 사용중인 IP주소를 입력하고 이름을 입력한다.
위와 같이 다른 PC를 이용하여 원격으로 라즈베리파이를 제어할 수 있다.
파이썬의 RPi.GPIO 모듈을 사용하여 GPIO 포트를 제어할 것이다.
라즈베리파이는 브로드컴사의 단일칩 시스템 SOC를 이용하고 있으므로 BCM 모드를 이용하여 핀 번호를 GPIO모듈 번호로 사용한다.
사용한 핀번호를 setup함수에 인자로 사용하여 그 번호를 출력용으로 사용하겠다고 선언한다.
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setup(18, GPIO.OUT)
모터를 제어하기 위해서는 L293D라는 모터 드라이브가 필요하다. 이 L293D 드라이브를 이용하여 모터의 방향과 세기를 조절할 것이다. 모터의 방향은 디지털 신호로 제어하고, 힘의 세기는 앞에서 설명한 PWM의 아날로그 값을 이용하여 조절한다.
다음은 모터를 제어하기위한 회로 구성도이다.
두개의 DC 모터에 각 두개씩 output선이 연결되어 있다. DC모터는 중심축의 코일에 전기를 보내서 주변의 영구자석간의 반발력을 이용하여 회전을 만드는 가장 일반적인 모터이다. 그러므로 전류의 방향과 세기를 다르게 하면 모터의 회전방향과 세기도 조정할 수 있다. 방향을 조절하기 위한 신호 두개와 세기를 조정하는 pwm이 최소하나 연결되어 있어야 한다.
L293D는 4.5V ~ 36V까지 전원을 공급받을 수 있다. 그러므로 라즈베리파이에서 제공하는 5V전원을 사용해야 한다. L293D IC칩의 8번과 16번에 전원을 공급하고 4번, 5번, 12번, 13번 네개의 핀은 접지에 연결한다.
각 모터마다 3개씩 총 6개의 핀을 L293D 칩에 연결해야 한다. 라즈베리파이의 GPIO 16번, 20번, 21번 포트와 13번, 19번, 26번 포트를 사용한다. 그 중 13번, 16번, 19번핀은 PWM핀으로 두 모터의 힘의 세기를 조절하는 PWM 용도로 사용한다. 이제 모터를 회로에 연결해야한다.. 하나의 모터를 L293D칩의 3번, 6번핀에 연결하고, 나머지 모터를 11번, 14번 핀에 연결한다.
HC-SR04 초음파 센서에는 총 4개의 핀이 있다. 한 쪽 끝의 VCC핀에는 라즈베리파이에 5V 전원을 공급하고,
GND핀은 접지용, TRIG핀은 라즈베리파이로부터 신호를 받는 역할, 신호를 받으면 초음파를 발사하고 다시 수신하여 이 값을 ECHO핀에 출력한다. 이 값을 라즈베리파이가 받아서 거리를 계산한다.
ECHO핀은 5V 전압을 사용하는데 라즈베리파이는 3.3V 전압을 사용하므로 1K옴 이상의 저항을 연결해주어야 한다.
다음은 초음파 센서를 연결하기 위한 회로도이다.
VCC와 GND는 브레드보드에서 모터에서 사용하는 입력값을 같이 사용하고 TRIG핀은 GPIO 23번핀에 연결한다. GPIO의 24번핀은 브레드보드의 저항을 거쳐서 ECHO핀에 연결한다.
코드를 살펴보자.
import RPi.GPIO as GPIO
import time
GPIO.setmode(GPIO.BCM)
먼저 GPIO라이브러리와 time라이브러리를 import하고, setmode함수를 이용하여 핀번호를 참조할 수 있도록 한다.
TRIG = 23
ECHO = 24
GPIO.setup(TRIG,GPIO.OUT)
GPIO.setup(ECHO,GPIO.IN)
TRIG와 ECHO핀에 사용한 GPIO핀번호를 변수를 통해 초기화하고, 초음파 신호를 주고 받을 수 있도록 setup함수를 이용하여 설정한다.
def getDistance():
GPIO.output(TRIG, False)
time.sleep(1)
GPIO.output(TRIG, True)
time.sleep(0.00001)
GPIO.output(TRIG, False)
while GPIO.input(ECHO)==0: #Check whether the ECHO is LOW
pulse_start = time.time() #Saves the last known time of LOW pulse
while GPIO.input(ECHO)==1: #Check whether the ECHO is HIGH
pulse_end = time.time() #Saves the last known time of HIGH pulse
pulse_duration = pulse_end - pulse_start #Get pulse duration to a variable
distance = pulse_duration * 17150 #Multiply pulse duration by 17150 to get distance
distance = round(distance, 2) #Round to two decimal points
return distance
getDistance()함수를 이용하여 장애물간의 거리를 구한다. 1초간격으로 초음파 센서를 껐다 켜면서 초음파 센서를 작동하도록 한다. 껐다 켤때의 시간을 start 와 end 변수에 나눠 담고, 둘의 차를 계산한뒤 17150을 곱하여 거리를 측정한다.
if __name__ == '__main__':
try:
while True:
distance_value = getDistance()
if distance_value > 2 and distance_value < 400:
print ("Distance is %.2f cm" %distance_value) #Print distance with 0.5 cm calibration
else:
print ("Out Of Range") #display out of range
except KeyboardInterrupt:
print ("Terminate program by Keyboard Interrupt")
GPIO.cleanup()
거리값이 2cm이상이고 400cm이하 일동안 거리를 출력한다. Ctrl + C를 입력하면 종료한다.
이제 모터를 제어하고 장애물이 있으면 회피하는 기능을 구현하자.
import RPi.GPIO as GPIO
import time
#Set GPIO BCM(Broadcom SoC) pin number
GPIO.setmode(GPIO.BCM)
TRIG = 23
ECHO = 24
GPIO.setup(TRIG,GPIO.OUT)
GPIO.setup(ECHO,GPIO.IN)
RIGHT_FORWARD = 26
RIGHT_BACKWARD = 19
RIGHT_PWM = 13
LEFT_FORWARD = 21
LEFT_BACKWARD = 20
LEFT_PWM = 16
왼쪽과 오른쪽 모터를 제어하기 위한 칩번호를 선언한다. 왼쪽과 오른쪽의 전진, 후진 그리고 PWM으로 구성되어 있다.
GPIO.setup(RIGHT_FORWARD,GPIO.OUT)
GPIO.setup(RIGHT_BACKWARD,GPIO.OUT)
GPIO.setup(RIGHT_PWM,GPIO.OUT)
GPIO.output(RIGHT_PWM, 0)
RIGHT_MOTOR = GPIO.PWM(RIGHT_PWM, 100)
RIGHT_MOTOR.start(0)
RIGHT_MOTOR.ChangeDutyCycle(0)
GPIO.setup(LEFT_FORWARD,GPIO.OUT)
GPIO.setup(LEFT_BACKWARD,GPIO.OUT)
GPIO.setup(LEFT_PWM,GPIO.OUT)
GPIO.output(LEFT_PWM, 0)
LEFT_MOTOR = GPIO.PWM(LEFT_PWM, 100)
LEFT_MOTOR.start(0)
LEFT_MOTOR.ChangeDutyCycle(0)
전진, 후진, PWM 핀번호를 출력용으로 설정하고 PWM은 모터의 주파수를 100으로 설정한다. start()와 ChangeDutyCycle()함수를 통하여 모터의 출력값을 0 ~ 100 %로 설정할 수 있다. 우선 0으로 초기화한다.
#Get distance from HC-SR04
def getDistance():
GPIO.output(TRIG, GPIO.LOW)
time.sleep(1)
GPIO.output(TRIG, GPIO.HIGH)
time.sleep(0.00001)
GPIO.output(TRIG, GPIO.LOW)
#When the ECHO is LOW, get the purse start time
while GPIO.input(ECHO)==0:
pulse_start = time.time()
#When the ECHO is HIGN, get the purse end time
while GPIO.input(ECHO)==1:
pulse_end = time.time()
#Get pulse duration time
pulse_duration = pulse_end - pulse_start
#Multiply pulse duration by 17150 to get distance and round
distance = pulse_duration * 17150
distance = round(distance, 2)
return distance
거리를 구하는 함수는 위의 설명을 참고하자.
#Right Motor Control
def rightMotor(forward, backward, pwm):
GPIO.output(RIGHT_FORWARD,forward)
GPIO.output(RIGHT_BACKWARD,backward)
RIGHT_MOTOR.ChangeDutyCycle(pwm)
#Left Motor Control
def leftMotor(forward, backward, pwm):
GPIO.output(LEFT_FORWARD,forward)
GPIO.output(LEFT_BACKWARD,backward)
LEFT_MOTOR.ChangeDutyCycle(pwm)
forward, backward, pwm을 파라미터로 받아 모터를 제어하는 함수를 선언한다.
if __name__ == '__main__':
try:
while True:
distance_value = getDistance()
#Check whether the distance is 50 cm
if distance_value > 50:
#Forward 1 seconds
print ("Forward " + str(distance_value))
rightMotor(1, 0, 70)
leftMotor(1, 0, 70)
time.sleep(1)
else:
#Left 1 seconds
print ("Left " + str(distance_value))
rightMotor(0, 0, 0)
leftMotor(1, 0, 70)
time.sleep(1)
except KeyboardInterrupt:
print ("Terminate program by Keyboard Interrupt")
GPIO.cleanup()
장애물과의 거리가 50cm 이상이면 1초동안 앞으로 전진한다. 여기서 rightMotor와 leftMotor함수에 forward, backward, pwm 값을 인수로 주어서 모터를 제어한다. 1이면 작동하고 0이면 정지한다.
다음은 조립을 완성한 미니카의 모습이다.
작동하는 모습을 영상으로 찍어보았다.
https://youtu.be/oLcthl1rbQk
이제 파이썬 플라스크 웹서버를 이용하여 원격으로 라즈베리파이와 통신하고 제어할 수 있는 기능을 구현해 볼 것이다. 또한 라즈베리파이의 파이카메라를 연결하여 원격으로 실시간 영상을 볼 수 있는 기능 또한 구현 할 것이다.
플라스크는 파이썬을 기반으로 작성된 웹 프레임워크의 하나로, Werkzeug 툴킷과 Jinja2 템플릿 엔진을 기반으로 하고 있다.
우선 플라스크를 이용하여 원격지에서 라즈베리파이와 통신하려면 DDNS를 이용해야한다.
DDNS는 실시간으로 DNS를 갱신하는 방식이다. 인터넷 상에서 서버를 찾아가는 방법은 고유의 숫자인 IP이고 이것은 우리가 사용하는 www.google.com과 같은 이름을 호스트의 고유 네트워크 주소로 바꿔주는 서비스가 DNS이다. 우리가 집에서 사용하는 무선 공유기는 고정된 IP값을 사용하는 것이 아니라 인터넷 서비스 업체에서 제공하는 한정된 네트워크 주소 자원을 돌려서 사용하고 있는 것이다.
이렇게 바뀌는 주소를 유동 IP라고 한다.
이렇게 IP가 바뀌어도 도메인 이름과 연결시켜서 사용할 수 있게 해주는 것이 DDNS서비스이다.
이 서비스를 이용하여 고유의 도메인 이름을 사용하면 집에있는 무선 공유기의 내부 IP가 아무리 많이 바뀌어도 그 이름으로 찾아갈 수 있다.
이번 프로젝트에 사용할 도메인 이름으로 aga1000.asuscomm.com으로 설정하였다.
보통 하나의 무선 공유기에 여러대의 기기를 접속하여 사용한다. 공유기는 외부에서는 하나의 IP를 받아서 인터넷에 접속하지만 내부에서는 내부 자체의 IP망을 구성하여 사용하고 있다. 이렇게 여러대의 장비가 하나의 외부 공용 IP를 같이 써서 외부 인터넷에 접속하여 사용한다.
무선공유기에 연결된 여러대의 기기들 중 특정한 기기에만 접속하려면 그 기기만의 내부 고유 IP를 알아야 한다. DDNS를 사용하여 외부에서 무선공유기 IP로 접속하여 내부의 여러 장비의 고유 IP중 어디로 접근해야할까. 공유기 안의 여러 장치중 라즈베리파이가 사용하는 내부 고유 IP로 안내하고 싶은데 어덯게 해야할까.
특정 내부 IP로 연결해주는 기능이 포트 포워딩(Port Forwading)이다.
라즈베리 파이의 고유 Port로 5561번을 설정해 주었다.
코드 :
from flask import Flask, render_template, request, Response
import RPi.GPIO as GPIO
import time
import io
import threading
import picamera
flask 라이브러리와 render_template, request, Response를 import하고, 스트리밍 영상 처리를 위한 io, 별도 프로세스 thread를 구현하기 위한 threading, 파이카메라를 사용하기 위한 picamera 라이브러리를 import 한다.
class Camera:
thread = None # background thread that reads frames from camera
frame = None # current frame is stored here by background thread
start_time = 0 # time of last client access to the camera
스레드는 프로세스를 여러개로 나눈 독립적인 실행단위이다. 이번 원격 스트리밍 기능에서는 비디오도 송출하고 미니카도 조종해야 한다. 그런데 비디오를 송출하다가 미니카를 제어하는 명령을 늦게 처리한다면 사고가 날 수 있다. 따라서 별도의 독립적인 프로세스인 스레드 개념을 도입하여 비디오 송출과 미니카 조종 기능을 분리 하였다.
def getStreaming(self):
Camera.start_time = time.time()
#self.initialize()
if Camera.thread is None:
# start background frame thread
Camera.thread = threading.Thread(target=self.streaming)
Camera.thread.start()
# wait until frames start to be available
while self.frame is None:
time.sleep(0)
return self.frame
@classmethod
streaming 함수를 관리해주고 화면을 보내주는 함수
def streaming(c):
with picamera.PiCamera() as camera:
# camera setup
camera.resolution = (320, 240)
camera.hflip = True
camera.vflip = True
# let camera warm up
camera.start_preview()
time.sleep(2)
stream = io.BytesIO()
for f in camera.capture_continuous(stream, 'jpeg',
use_video_port=True):
# store frame
stream.seek(0)
c.frame = stream.read()
# reset stream for next frame
stream.seek(0)
stream.truncate()
# if there hasn't been any clients asking for frames in
# the last 10 seconds stop the thread
if time.time() - c.start_time > 10:
break
c.thread = None
독립적인 스레드로 파이카메라에서 프레임 단위로 계속 영상을 보내주는 함수
app = Flask(__name__)
#Set GPIO BCM(Broadcom SoC) pin number
GPIO.setmode(GPIO.BCM)
TRIG = 23
ECHO = 24
GPIO.setup(TRIG,GPIO.OUT)
GPIO.setup(ECHO,GPIO.IN)
RIGHT_FORWARD = 26
RIGHT_BACKWARD = 19
RIGHT_PWM = 13
LEFT_FORWARD = 21
LEFT_BACKWARD = 20
LEFT_PWM = 16
GPIO.setup(RIGHT_FORWARD,GPIO.OUT)
GPIO.setup(RIGHT_BACKWARD,GPIO.OUT)
GPIO.setup(RIGHT_PWM,GPIO.OUT)
GPIO.output(RIGHT_PWM, 0)
RIGHT_MOTOR = GPIO.PWM(RIGHT_PWM, 100)
RIGHT_MOTOR.start(0)
RIGHT_MOTOR.ChangeDutyCycle(0)
GPIO.setup(LEFT_FORWARD,GPIO.OUT)
GPIO.setup(LEFT_BACKWARD,GPIO.OUT)
GPIO.setup(LEFT_PWM,GPIO.OUT)
GPIO.output(LEFT_PWM, 0)
LEFT_MOTOR = GPIO.PWM(LEFT_PWM, 100)
LEFT_MOTOR.start(0)
LEFT_MOTOR.ChangeDutyCycle(0)
#Get distance from HC-SR04
def getDistance():
GPIO.output(TRIG, GPIO.LOW)
time.sleep(1)
GPIO.output(TRIG, GPIO.HIGH)
time.sleep(0.00001)
GPIO.output(TRIG, GPIO.LOW)
#When the ECHO is LOW, get the purse start time
while GPIO.input(ECHO)==0:
pulse_start = time.time()
#When the ECHO is HIGN, get the purse end time
while GPIO.input(ECHO)==1:
pulse_end = time.time()
#Get pulse duration time
pulse_duration = pulse_end - pulse_start
#Multiply pulse duration by 17150 to get distance and round
distance = pulse_duration * 17150
distance = round(distance, 2)
return distance
#Right Motor Control
def rightMotor(forward, backward, pwm):
GPIO.output(RIGHT_FORWARD,forward)
GPIO.output(RIGHT_BACKWARD,backward)
RIGHT_MOTOR.ChangeDutyCycle(pwm)
#Left Motor Control
def leftMotor(forward, backward, pwm):
GPIO.output(LEFT_FORWARD,forward)
GPIO.output(LEFT_BACKWARD,backward)
LEFT_MOTOR.ChangeDutyCycle(pwm)
#Forward Car
def forward():
rightMotor(1, 0, 70)
leftMotor(1, 0, 70)
time.sleep(1)
#Left Car
def left():
rightMotor(0, 0, 0)
leftMotor(1, 0, 70)
time.sleep(0.3)
#Right Car
def right():
rightMotor(1, 0, 70)
leftMotor(0, 0, 0)
time.sleep(0.3)
#Stop Car
def stop():
rightMotor(0, 0, 0)
leftMotor(0, 0, 0)#Forward Car
def forward():
rightMotor(1, 0, 70)
leftMotor(1, 0, 70)
time.sleep(1)
#Left Car
def left():
rightMotor(1, 0, 70)
leftMotor(0, 0, 70)
time.sleep(0.3)
#Right Car
def right():
rightMotor(0, 0, 70)
leftMotor(1, 0, 70)
time.sleep(0.3)
#Backward Car
def backward():
rightMotor(0, 1, 70)
leftMotor(0, 1, 70)
time.sleep(0.3)
#Stop Car
def stop():
rightMotor(0, 0, 0)
leftMotor(0, 0, 0)
@app.route("/<command>")
def action(command):
distance_value = getDistance()
if command == "F":
forward()
message = "Moving Forward"
elif command == "L":
left()
message = "Turn Left"
elif command == "R":
right()
message = "Turn Right"
elif command == "S":
stop()
message = "Stop"
elif command == "B":
backward()
message = "Moving Backward"
else:
stop()
message = "Unknown Command [" + command + "] "
msg = {
'message' : message,
'distance': str(distance_value)
}
return render_template('video.html', **msg)
route함수의 파라미터로 command를 사용하여 웹 서버를 통해 html 페이지로 명령을 보낼 수 있다. getDistance()함수를 이용하여 장애물과의 거리를 측정한 후 각 command에 맞는 함수를 실행하여 미니카를 제어한다.
msg라는 배열에 메시지와 거리값을 render_template함수를 이용하여 html파일에 전달한다. render_template함수를 사용하면 html 문서를 렌더링 할 수 있다.
def show(camera):
"""Video streaming generator function."""
while True:
frame = camera.getStreaming()
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
Camera 클래스에서 프레임을 계속 보내주는 getStreaming함수의 결과화면을 받아서 html에서 표시가능한 형식으로 표현하는 역할을 하는 함수
@app.route('/show')
def showVideo():
"""Video streaming route. Put this in the src attribute of an img tag."""
return Response(show(Camera()),
mimetype='multipart/x-mixed-replace; boundary=frame')
show함수에서 받아온 화면을 html파일에 response하는 기능을 한다.
if __name__ == "__main__":
try:
app.run(host='0.0.0.0', port=5561, debug=True, threaded=True)
except KeyboardInterrupt:
print ("Terminate program by Keyboard Interrupt")
GPIO.cleanup()
port를 포트 포워딩한 5561로 설정하고 실행하였다.
나의 라즈베리파이의 고유 내부 IP주소인 aga1000.asuscomm.com:5561/S로 연결하여 RC카를 작동해본 모습이다.
작동 영상 :
https://youtu.be/qo87FHvM9Cw
이번 프로젝트를 통해 소프트웨어와 하드웨어를 결합하는 경험을 해볼 수 있었고, 너무 흥미로운 시간이었다. 그동안 이번 프로젝트만큼 결과물을 직접 눈으로 확인할 수 있는 프로젝트는 적었어서 더 재미있었던 프로젝트였다. 이번 프로젝트에서는 장애물을 만나면 1초동안 회피하는 동작에서 그치지만, 앞으로 더 나아가 영상인식, 머신러닝 기술을 도입한 진정한 자율주행 자동차를 만들어 보고 싶어졌다. 자율주행 분야로 진출하고 싶은 나의 의지를 확인할 수 있었고, 아직 많이 부족하지만 나의 꿈인 자율주행 소프트웨어 개발자가 되기위해 피나는 노력을 할 것이다!
본 게시물을 creapple사이트의 파이썬 라즈베리파이 IoT프로젝트-원격모니터링 자동차 강의를 수강하고 작성하였습니다.
https://www.creapple.com/item
혹시 외부배터리는 어떤거 사용하셨는지 알 수 있을까요?