감지하고 반응하는 장치 만들기
지난 장에서는 버튼을 누르면 LED가 켜지는 회로를 만들었다. 버튼은 사람이 직접 누르는 입력이고, LED는 눈으로 확인하는 출력이었다. 이번 장에서는 입력과 출력을 한 단계 확장한다.
센서는 주변 환경을 숫자로 읽는 부품이다. 온도, 습도, 거리, 밝기처럼 사람이 눈대중으로 보던 값을 라즈베리 파이가 읽을 수 있게 해 준다. 액추에이터는 전기 신호를 실제 움직임이나 동작으로 바꾸는 부품이다. 서보 모터, 릴레이, 팬, 펌프가 여기에 들어간다.
이번 장의 목표는 간단하다.
라즈베리 파이가 주변을 감지하고, 조건에 따라 스스로 반응하게 만든다.
실습은 모두 GPIO를 사용한다. 전원을 연결하기 전에는 항상 배선을 먼저 확인하고, 특히 5V 신호를 GPIO 입력 핀에 직접 넣지 않는다는 원칙을 지켜야 한다.
AI 생성 이미지
실습 폴더와 기본 도구 준비
먼저 이번 장에서 사용할 폴더를 만든다.
mkdir -p ~/pi-study/sensor-actuator
cd ~/pi-study/sensor-actuatorGPIO와 센서 라이브러리를 준비한다. 가상환경을 만들 때 필요한 python3-venv도 함께 설치한다.
sudo apt update
sudo apt install -y python3-venv python3-gpiozero python3-lgpio python3-spidev libgpiod2 python3-dev
python3 -m venv --system-site-packages .venv
source .venv/bin/activate
python -m pip install adafruit-circuitpython-dht가상환경 프롬프트가 보이면 준비가 된 것이다.
(.venv) pi@raspberrypi:~/pi-study/sensor-actuator $실행할 때마다 이 폴더에서 다음 명령으로 가상환경을 켠다.
cd ~/pi-study/sensor-actuator
source .venv/bin/activate디지털 입력, 아날로그 입력, PWM 출력
전자부품의 신호는 크게 세 가지로 나누어 생각하면 쉽다.
디지털 입력은 스위치처럼 0 또는 1로 읽는 신호다. 버튼이 눌렸는지, 센서가 감지했는지처럼 참과 거짓으로 판단할 수 있다. DHT22 온습도 센서도 데이터 핀 하나로 디지털 신호를 주고받는다.
아날로그 입력은 밝기나 소리처럼 연속적으로 변하는 값을 읽는 방식이다. 문제는 라즈베리 파이 GPIO에는 아날로그 입력 기능이 없다는 점이다. 그래서 가변저항, 조도 센서 같은 아날로그 센서를 읽으려면 MCP3008 같은 ADC가 필요하다. ADC는 Analog-to-Digital Converter의 약자로, 연속적인 전압을 숫자로 바꿔 주는 칩이다. MCP3008은 VREF에 걸어 준 전압 범위를 10비트, 즉 0~1023 단계로 읽을 수 있고 SPI 통신을 사용한다.1 이번 실습에서는 VREF와 VDD를 모두 3.3V에 연결하므로 입력 전압도 0~3.3V 범위 안에 있어야 한다.
SPI를 켠다.
sudo raspi-config nonint do_spi 0
ls /dev/spidev*다음처럼 보이면 SPI 장치가 활성화된 것이다.
/dev/spidev0.0 /dev/spidev0.1만약 아무것도 보이지 않으면 라즈베리 파이를 한 번 재부팅한 뒤 다시 확인한다.
sudo rebootMCP3008으로 가변저항 값을 읽는 기본 연결은 다음과 같다.2
| MCP3008 핀 | 연결 |
|---|---|
| 1번 CH0 | 가변저항 가운데 핀 |
| 9번 DGND | GND |
| 10번 CS/SHDN | GPIO8, CE0 |
| 11번 DIN | GPIO10, MOSI |
| 12번 DOUT | GPIO9, MISO |
| 13번 CLK | GPIO11, SCLK |
| 14번 AGND | GND |
| 15번 VREF | 3.3V |
| 16번 VDD | 3.3V |
가변저항의 양쪽 끝은 각각 3.3V와 GND에 연결한다. 가운데 핀이 MCP3008 CH0로 들어간다.
주의: MCP3008의 입력 전압은 VREF보다 높아지면 안 된다. 이번 회로에서는 VREF가 3.3V이므로 CH0~CH7 입력도 3.3V를 넘기지 않는다. 5V 센서 출력을 바로 넣으면 MCP3008과 라즈베리 파이가 손상될 수 있다.
테스트 파일을 만든다.
nano analog_read.pyfrom gpiozero import MCP3008
from time import sleep
pot = MCP3008(channel=0)
while True:
value = pot.value
print(f'analog value: {value:.3f}')
sleep(0.2)실행한다.
python analog_read.py가변저항을 돌렸을 때 0.000에서 1.000 사이 값이 변하면 성공이다. gpiozero는 MCP3008 값을 0과 1 사이의 소수로 바꿔 보여 준다. 이 값은 나중에 LED 밝기, 모터 속도, 임계값 조절에 사용할 수 있다.
PWM 출력은 아주 빠르게 켜고 끄는 비율을 조절해 중간값처럼 보이게 만드는 방식이다. LED 밝기 조절, 모터 속도 제어, 서보 모터 각도 제어에 사용한다. 예를 들어 50% 비율로 켜고 끄면 LED는 절반 밝기처럼 보인다. 서보 모터는 단순히 밝기를 조절하는 PWM과는 조금 다르게, 정해진 주기의 짧은 펄스 폭을 보고 각도를 판단한다. gpiozero가 이 부분을 대신 처리해 주기 때문에 처음에는 각도 값만 지정해도 된다.
온습도 센서 읽기: 실패하면 재시도하기
DHT22는 온도와 습도를 함께 읽을 수 있는 센서다. DHT11도 비슷하지만 정확도와 측정 범위는 DHT22가 더 낫다. 여기서는 DHT22를 기준으로 설명한다.
4핀 DHT22를 정면에서 봤을 때 왼쪽부터 1번이다.
| DHT22 핀 | 연결 |
|---|---|
| 1번 VCC | 3.3V |
| 2번 DATA | GPIO4 |
| 3번 NC | 연결하지 않음 |
| 4번 GND | GND |
일반 4핀 센서는 VCC와 DATA 사이에 10kΩ 정도의 풀업 저항을 넣는다. 풀업 저항은 데이터 선이 애매한 상태로 떠 있지 않게 잡아 주는 역할을 한다. 3핀 브레이크아웃 보드는 이미 저항이 들어 있는 경우가 많다.3
주의: 센서 전원은 3.3V에 연결한다. 5V로 동작하는 모듈을 쓰더라도 데이터 출력이 5V로 나오면 GPIO에 위험할 수 있다.
코드를 작성한다.
nano dht_retry.pyimport time
import board
import adafruit_dht
sensor = adafruit_dht.DHT22(board.D4)
def read_dht(retries=5):
for attempt in range(1, retries + 1):
try:
temperature = sensor.temperature
humidity = sensor.humidity
if temperature is None or humidity is None:
raise RuntimeError('empty reading')
if not (-20 <= temperature <= 80):
raise ValueError(f'temperature out of range: {temperature}')
if not (0 <= humidity <= 100):
raise ValueError(f'humidity out of range: {humidity}')
return temperature, humidity
except (RuntimeError, ValueError) as error:
print(f'읽기 실패 {attempt}/{retries}: {error}')
time.sleep(2.0)
raise RuntimeError('DHT 센서 값을 읽지 못했습니다. 배선과 핀 번호를 확인하세요.')
try:
while True:
try:
temp, hum = read_dht()
print(f'온도 {temp:.1f}°C / 습도 {hum:.1f}%')
except RuntimeError as error:
print(error)
time.sleep(3.0)
finally:
sensor.exit()실행한다.
python dht_retry.py정상이라면 다음처럼 출력된다.
온도 24.8°C / 습도 48.3%
온도 24.9°C / 습도 48.2%중간에 읽기 실패가 나와도 바로 고장이라고 판단하지 않는다. DHT 계열 센서는 읽기 오류가 비교적 자주 발생하므로, 예제처럼 오류를 잡고 잠시 기다린 뒤 다시 읽는 방식이 권장된다.4 또한 값이 센서의 상식적인 범위를 벗어나면 그대로 믿지 않고 버린다.
확인할 지점은 세 가지다.
board.D4와 실제 연결한 GPIO 번호가 같은가?DATA와 VCC 사이에 풀업 저항이 필요한 센서인가?
너무 빠르게 읽고 있지 않은가? DHT22는 2초 이상 간격을 두고 읽는 것이 안전하다.
초음파 거리 센서와 서보 모터 결합하기
이제 센서와 액추에이터를 함께 사용한다. 초음파 거리 센서가 가까운 물체를 감지하면 서보 모터가 움직이는 장치를 만든다. 작은 자동문이나 쓰레기통 뚜껑의 원리와 비슷하다.
준비물은 다음과 같다.
HC-SR04 초음파 거리 센서
SG90 같은 소형 서보 모터
1kΩ 저항 1개, 2kΩ 저항 1개
외부 5V 전원 또는 충분한 용량의 전원 모듈
점퍼선, 브레드보드
HC-SR04 연결은 다음과 같이 한다.
| HC-SR04 핀 | 연결 |
|---|---|
| VCC | 5V |
| TRIG | GPIO24 |
| ECHO | 전압 분배 후 GPIO23 |
| GND | GND |
중요한 부분은 ECHO다. HC-SR04의 ECHO 출력은 5V로 나올 수 있다. 라즈베리 파이 GPIO 입력은 3.3V 기준이므로 ECHO를 바로 연결하면 안 된다.
전압 분배는 이렇게 만든다.
HC-SR04 ECHO ── 1kΩ ── GPIO23 ── 2kΩ ── GND이렇게 하면 5V 신호가 약 3.3V 수준으로 낮아진다. 2kΩ 저항이 없다면 1kΩ 저항 두 개를 직렬로 연결해 2kΩ처럼 사용할 수도 있다.
서보 모터는 다음처럼 연결한다.
| 서보 선 | 연결 |
|---|---|
| 갈색/검정 GND | 외부 전원 GND, 라즈베리 파이 GND와 공통 연결 |
| 빨강 VCC | 외부 5V + |
| 주황/노랑 SIGNAL | GPIO17 |
주의: 서보 모터는 순간적으로 많은 전류를 사용한다. 라즈베리 파이의 5V 핀에서 직접 전원을 가져오면 재부팅이나 오동작이 생길 수 있다. 가능하면 별도 5V 전원을 사용하고, 외부 전원 GND와 라즈베리 파이 GND만 공통으로 연결한다.
코드를 작성한다. gpiozero는 DistanceSensor와 AngularServo를 제공하므로 거리 측정과 서보 제어를 짧은 코드로 연결할 수 있다.5
nano distance_servo.pyfrom gpiozero import DistanceSensor, AngularServo
from time import sleep
from statistics import mean
sensor = DistanceSensor(echo=23, trigger=24, max_distance=2.0)
servo = AngularServo(
17,
min_angle=-90,
max_angle=90,
min_pulse_width=0.0005,
max_pulse_width=0.0025,
)
def stable_distance(samples=5):
values = []
for _ in range(samples):
distance = sensor.distance # 미터 단위
if 0.02 <= distance <= 2.0:
values.append(distance)
sleep(0.05)
if not values:
return None
return mean(values)
try:
opened = False
servo.angle = -70
while True:
distance = stable_distance()
if distance is None:
print('거리 측정 실패')
sleep(0.5)
continue
print(f'거리: {distance * 100:.1f} cm')
if not opened and distance < 0.20:
servo.angle = 70
opened = True
print('가까운 물체 감지: 열림')
elif opened and distance > 0.25:
servo.angle = -70
opened = False
print('물체 멀어짐: 닫힘')
sleep(0.3)
finally:
servo.detach()
sensor.close()실행한다.
python distance_servo.py손을 센서 앞 20cm 안쪽으로 가져가면 서보가 한쪽으로 움직인다. 손을 치워 거리가 25cm보다 멀어지면 다시 반대 방향으로 돌아간다. 열리는 기준과 닫히는 기준을 조금 다르게 둔 이유는 20cm 근처에서 값이 흔들릴 때 서보가 계속 열렸다 닫혔다 하는 일을 줄이기 위해서다.
서보가 떨리면 다음을 확인한다.
서보 전원을 라즈베리 파이에서 직접 가져오고 있지 않은가?
외부 전원 GND와 라즈베리 파이 GND가 연결되어 있는가?
서보가 물리적으로 걸려 계속 힘을 쓰고 있지 않은가?
거리 센서 ECHO를 전압 분배 없이 GPIO에 넣고 있지 않은가?
서보 모델에 비해 각도 범위가 너무 넓지 않은가? 처음에는
-70과70보다 더 좁은 범위로 테스트해도 된다.
릴레이로 전원을 대신 켜고 끄기
릴레이는 전기적으로 작동하는 스위치다. 라즈베리 파이는 GPIO로 작은 신호만 낼 수 있지만, 릴레이 모듈을 이용하면 별도 전원을 쓰는 장치를 켜고 끌 수 있다.
여기서 말하는 것은 트랜지스터나 옵토커플러가 들어 있는 릴레이 모듈이다. 릴레이 부품의 코일을 GPIO에 직접 연결하면 안 된다. GPIO가 낼 수 있는 전류보다 훨씬 큰 전류가 필요하고, 코일에서 생기는 역전압도 라즈베리 파이에 위험하다.
릴레이 모듈의 입력 쪽은 보통 다음과 같다.
| 릴레이 입력 핀 | 연결 |
|---|---|
| VCC 또는 DC+ | 모듈이 요구하는 전원, 보통 5V |
| GND 또는 DC- | GND |
| IN | GPIO18 |
출력 쪽 단자는 보통 세 개다.
COM: 공통 단자NO: 평소에는 열려 있고, 릴레이가 켜지면 연결됨NC: 평소에는 연결되어 있고, 릴레이가 켜지면 끊어짐
처음에는 COM과 NO를 사용한다. 그러면 평소에는 장치가 꺼져 있다가 릴레이가 켜질 때만 전원이 연결된다. 예를 들어 낮은 전압의 DC 팬을 켠다면 부하 전원의 + 선을 릴레이가 끊고 이어 주도록 배선한다.
DC 전원 + ── COM NO ── 팬 +
DC 전원 - ───────────── 팬 -릴레이의 접점 쪽은 스위치처럼 동작하므로, 입력 쪽 GPIO 배선과는 별개의 회로라고 생각하면 된다. 다만 릴레이 모듈의 입력을 GPIO로 제어하려면 보통 라즈베리 파이 GND와 릴레이 모듈 GND가 서로 연결되어 있어야 한다. 제품마다 구조가 다르므로 모듈 설명서를 확인한다.
릴레이 모듈은 제품마다 LOW 신호에서 켜지는 것도 있고 HIGH 신호에서 켜지는 것도 있으므로 반드시 모듈 설명과 점퍼 설정을 확인해야 한다.6
매우 중요: 처음 실습에서는 절대 220V 가정용 전원을 연결하지 않는다. 5V LED 모듈, 12V 이하의 작은 팬처럼 낮은 전압의 DC 장치로만 테스트한다. 고전압 장비 제어는 감전과 화재 위험이 있으므로 전문가의 도움 없이 시도하지 않는다.
LOW 신호에서 켜지는 릴레이 모듈이 많다. 이 경우에는 active_high=False로 설정한다.
nano relay_test.pyfrom gpiozero import OutputDevice
from time import sleep
relay = OutputDevice(18, active_high=False, initial_value=False)
try:
while True:
print('릴레이 ON')
relay.on()
sleep(2)
print('릴레이 OFF')
relay.off()
sleep(2)
finally:
relay.off()실행한다.
python relay_test.py딸깍 소리가 2초마다 나면 릴레이가 동작하는 것이다. 연결한 낮은 전압 장치가 함께 켜졌다 꺼지면 출력 쪽 배선도 정상이다.
반대로 동작한다면 다음 줄을 바꿔 본다.
relay = OutputDevice(18, active_high=True, initial_value=False)단, 코드를 바꾸기 전에 모듈의 HIGH trigger, LOW trigger 표시나 점퍼 위치를 먼저 확인한다. 또 5V 릴레이 모듈 중에는 3.3V GPIO 신호를 안정적으로 인식하지 못하는 제품도 있다. 라즈베리 파이와 호환된다고 표시된 모듈을 고르면 시행착오를 줄일 수 있다.
센서값을 더 믿을 수 있게 만드는 방법
센서값은 항상 완벽하지 않다. 전원 노이즈, 긴 점퍼선, 주변 온도, 센서 자체의 한계 때문에 값이 튈 수 있다. 그래서 실제 프로젝트에서는 값을 한 번만 읽고 바로 판단하지 않는다.
가장 쉬운 방법은 평균이다.
from statistics import mean
values = [24.8, 24.9, 31.2, 24.7, 24.8]
filtered = [v for v in values if 0 <= v <= 50]
print(mean(filtered))하지만 이 예제에서는 31.2처럼 갑자기 튄 값도 정상 범위에 들어간다. 이런 경우에는 최근 값과 너무 차이 나는 값을 버릴 수 있다.
def remove_noise(values, max_jump=3.0):
result = []
for value in values:
if not result:
result.append(value)
continue
if abs(value - result[-1]) <= max_jump:
result.append(value)
return result센서 보정도 필요하다. 예를 들어 온도계와 DHT22를 나란히 두고 비교했더니 DHT22가 항상 1.5°C 높게 나온다면 보정값을 뺀다.
raw_temp = 25.6
calibrated_temp = raw_temp - 1.5보정은 센서를 속이는 것이 아니다. 센서마다 생기는 작은 오차를 프로젝트 기준에 맞게 조정하는 과정이다.
실용적인 판단 흐름은 다음과 같다.
센서값을 여러 번 읽는다.
말이 안 되는 값은 버린다.
남은 값의 평균을 낸다.
필요하면 보정값을 적용한다.
기준값과 비교해 액추에이터를 움직인다.
이 흐름을 기억하면 센서 종류가 바뀌어도 코드는 크게 달라지지 않는다. 온습도 센서든 거리 센서든 결국 구조는 같다.
측정한다 → 정리한다 → 판단한다 → 반응한다이번 장에서 라즈베리 파이는 단순히 LED를 켜는 장치를 넘어섰다. 온도와 습도를 읽고, 거리를 감지하고, 모터를 움직이고, 릴레이로 외부 장치를 제어했다. 다음 장에서는 이 장치들을 네트워크에 연결해 다른 컴퓨터에서 원격으로 확인하고 제어하는 방법으로 넘어간다.
참고
1Using MCP3008 ADC - Discover IoT
2Raspberry Pi: Read Analog Inputs with Python (MCP3008)
3Raspberry Pi Humidity Sensor using the DHT22
4DHT Humidity Sensing on Raspberry Pi or Beaglebone Black with GDocs Logging - Python Setup
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.