라즈베리파이 PWM으로 LED·부저·서보 제어하기
목차
라즈베리파이 4에서 버튼을 내부 풀업으로 읽어 LED를 켜고, 누를 때마다 LED를 켜고 끄는 코드를 만듭니다. 이어서 PWM으로 LED 밝기를 바꾸고, 부저로 도레미를 연주하고, 서보모터를 원하는 각도로 움직입니다.
버튼 구조와 저항 계산은 라즈베리파이 전자 기초, 옴의 법칙과 LED 저항에서, LED 회로와 python3-rpi-lgpio 설치는 라즈베리파이로 LED제어하기에서 다뤘습니다. 보드는 같은 Raspberry Pi 4 Model B와 Raspberry Pi OS이고, 명령 예시의 student는 계정 이름입니다.
코드는 모두 GPIO.BCM 모드입니다. 이 글에서 쓰는 핀은 아래 넷입니다.
| 부품 | 핀 |
|---|---|
| LED | 물리 7번 = BCM 4 |
| 버튼 | 물리 11번 = BCM 17 |
| 부저 | 물리 12번 = BCM 18 |
| 서보 신호 | 물리 13번 = BCM 27 |
파일은 ~/webapps/ch05에 만듭니다.
cd ~/webapps/ch05
1. 버튼을 풀업으로 읽기

아무 데도 안 이은 입력은 흔들립니다
GPIO를 입력으로 쓰면 그 핀의 전압을 읽어 HIGH(1)나 LOW(0)로 알려 줍니다. 그런데 핀이 아무 데도 이어져 있지 않으면 값이 정해지지 않습니다. 손을 가까이 대기만 해도 1과 0 사이를 오갑니다. 이 상태를 플로팅이라고 합니다.
내부 풀업을 켭니다
pull_up_down=GPIO.PUD_UP을 주면 핀 안쪽의 저항이 핀을 3.3V 쪽으로 약하게 당겨 줍니다. 이것이 풀업입니다. 외부 저항은 따로 필요 없습니다.
- 버튼을 누르지 않으면 HIGH(1)입니다.
- 버튼을 누르면 핀이 GND와 이어져 LOW(0)가 됩니다.
연결

아래 구멍 위치는 한 가지 예시입니다.
- 물리 6번 = GND → − 레일
- LED: 라즈베리파이로 LED제어하기와 같은 회로입니다. 물리 7번 = BCM 4 → a5, 저항(330Ω 또는 1kΩ) c5 – c9, LED 긴 다리 e9, 짧은 다리 e10, a10 → − 레일
- 버튼: e14, e16, f14, f16(가운데 홈에 걸쳐서)
- 물리 11번 = BCM 17 → a14
- a16 → − 레일
버튼 방향은 앞 글과 같습니다. 홈 같은 쪽의 두 다리(e14와 e16)가 누를 때만 이어집니다. 앞 글의 회로와 달리 3.3V 선이 없습니다. 3.3V 쪽은 내부 풀업이 맡습니다.
누르는 동안만 켜기
nano button_hold.py
import RPi.GPIO as GPIO
import time
led_pin = 4 # 물리 7번 = BCM 4
btn_pin = 17 # 물리 11번 = BCM 17
GPIO.setmode(GPIO.BCM)
GPIO.setup(led_pin, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(btn_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
try:
while True:
if GPIO.input(btn_pin) == GPIO.LOW: # 풀업이라 누르면 LOW
GPIO.output(led_pin, GPIO.HIGH)
else:
GPIO.output(led_pin, GPIO.LOW)
time.sleep(0.01)
finally:
GPIO.cleanup()
python3 button_hold.py
GPIO.input(btn_pin)이 버튼 핀의 현재 값을 읽습니다.- 값이 LOW면 버튼이 눌린 것이라 LED를 켭니다.
time.sleep(0.01)로 0.01초마다 한 번씩 읽습니다. 쉬는 시간이 있어서 CPU를 거의 쓰지 않습니다.- Ctrl+C로 끝내면
finally의GPIO.cleanup()이 핀을 정리합니다. - Ctrl+C 뒤에
KeyboardInterrupt글이 찍히는 것은 정상입니다. 이때GPIO.cleanup()은 이미 실행되었습니다.
헷갈리기 쉬운 점
- 풀업에서는 누르면 0(LOW)입니다. "누르면 1"로 생각하면 조건이 거꾸로 됩니다.
- 버튼의 다른 쪽 다리는 3.3V가 아니라 GND에 잇습니다.
2. 누를 때마다 켜고 끄기

배선은 1장 그대로입니다. 이번에는 한 번 누르면 켜지고, 다시 누르면 꺼지게 만듭니다.
떨어지는 순간을 잡습니다
풀업이라 버튼을 누르는 순간 값이 HIGH에서 LOW로 떨어집니다. 이 순간을 FALLING이라고 합니다. GPIO.add_event_detect()에 FALLING을 주면, 떨어질 때마다 지정한 함수가 한 번 실행됩니다.
버튼 떨림과 bouncetime
버튼 속 금속은 누르는 순간 아주 짧게 붙었다 떨어졌다를 반복합니다. 이것을 바운스라고 합니다. 그대로 두면 한 번 눌렀는데 여러 번 누른 것으로 읽힙니다.
bouncetime=50은 이 떨림을 걸러 줍니다. rpi-lgpio는 신호가 50ms 동안 그대로 있어야 누름으로 인정합니다.
코드
nano button_led.py
import RPi.GPIO as GPIO
import time
led_pin = 4 # 물리 7번 = BCM 4
btn_pin = 17 # 물리 11번 = BCM 17
GPIO.setmode(GPIO.BCM)
GPIO.setup(led_pin, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(btn_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
state = False
def toggle(channel):
global state
state = not state # True ↔ False 뒤집기
GPIO.output(led_pin, GPIO.HIGH if state else GPIO.LOW)
GPIO.add_event_detect(btn_pin, GPIO.FALLING, callback=toggle, bouncetime=50)
try:
while True:
time.sleep(1)
finally:
GPIO.cleanup()
python3 button_led.py
state는 LED가 켜져 있는지 기억하는 변수입니다. 처음에는 꺼짐(False)입니다.toggle()은 누를 때마다state를 뒤집고, 그 값대로 LED를 켜거나 끕니다. 함수 밖의state를 바꾸므로global state가 필요합니다.channel에는 눌린 핀의 번호 17(물리 11번 = BCM 17)이 들어옵니다. 이 코드에서는 쓰지 않습니다.- 아래쪽
while True는 프로그램이 끝나지 않게 기다리기만 합니다. 버튼 처리는toggle()이 따로 합니다.
앞의 button_hold.py가 아직 돌고 있으면 Ctrl+C로 먼저 끝냅니다. 한 핀은 한 프로그램만 쓸 수 있습니다.
헷갈리기 쉬운 점
- 한 번 눌렀는데 두 번 바뀌면 바운스가 남은 것입니다.
bouncetime을 100 정도로 조금만 올립니다. 200처럼 크게 잡으면 반응이 늦고, 짧게 톡 누른 것을 놓칩니다.- rpi-lgpio에는 "channel is already in use" 경고가 없습니다. 다른 프로그램이 아직 그 핀을 쓰고 있으면
lgpio.error: 'GPIO busy'오류가 나고 멈춥니다(출력 핀은'GPIO not allocated'로 나옵니다).gpioinfo(gpiod 패키지)를 실행해 GPIO17 줄이 사용 중으로 나오면 그 핀을 잡은 프로그램을 먼저 끝냅니다. rpi-lgpio로 만든 프로그램은 이름 자리에lg로 보입니다.Failed to add edge detection오류가 나면 원래 RPi.GPIO 패키지가 잡힌 것입니다.sudo apt remove python3-rpi.gpio후sudo apt install python3-rpi-lgpio를 실행합니다. 그래도 나오면 가상환경이나 pip로 설치한 다른 RPi.GPIO가 먼저 잡히는지 봅니다.
3. PWM으로 LED 밝기 바꾸기

GPIO는 HIGH와 LOW만 냅니다. 그 사이 밝기는 아주 빠르게 켰다 끄기를 반복해서 만듭니다. 이 방법이 PWM입니다.
듀티비와 주파수
- 주파수(Hz): 1초에 켜고 끄기를 몇 번 반복하는지입니다. 500Hz면 1초에 500번입니다.
- 듀티비(%): 한 번 반복하는 동안 켜져 있는 시간의 비율입니다. 0%면 늘 꺼짐, 100%면 늘 켜짐, 50%면 절반만 켜짐입니다.
반복이 아주 빨라서 눈에는 깜빡임 대신 밝기 차이로 보입니다. 밝기를 정하는 것은 듀티비입니다.
코드
배선은 1장의 LED 부분 그대로입니다(물리 7번 = BCM 4). 버튼은 꽂아 둔 채로 둬도 됩니다.
nano pwm_led.py
import RPi.GPIO as GPIO
import time
led_pin = 4 # 물리 7번 = BCM 4
GPIO.setmode(GPIO.BCM)
GPIO.setup(led_pin, GPIO.OUT)
pwm = GPIO.PWM(led_pin, 500) # 500Hz
pwm.start(0) # 듀티비 0%로 시작
try:
while True:
for d in range(0, 101, 5): # 점점 밝게
pwm.ChangeDutyCycle(d)
time.sleep(0.05)
for d in range(100, -1, -5): # 점점 어둡게
pwm.ChangeDutyCycle(d)
time.sleep(0.05)
finally:
pwm.stop()
del pwm
GPIO.cleanup()
python3 pwm_led.py
GPIO.PWM(led_pin, 500)은 출력으로 설정한 핀에 500Hz PWM을 만듭니다.setup다음에 와야 합니다.pwm.start(0)은 듀티비 0%, 곧 꺼진 상태로 시작합니다.ChangeDutyCycle(d)가 듀티비를 0에서 100까지 5씩 바꿉니다. 한 번 밝아졌다 어두워지는 데 약 2초 걸립니다.- 끝날 때는
pwm.stop()으로 PWM을 멈추고,del pwm으로 PWM 객체를 지운 뒤GPIO.cleanup()을 합니다.del pwm이 없으면 끝날 때Exception ignored in으로 시작하는TypeError글이 찍힐 수 있습니다. 동작에는 영향이 없지만 이 순서로 정리하면 깔끔하게 끝납니다.
이 PWM은 소프트웨어로 만든 신호라서 LED가 아주 약간 떨려 보일 수 있습니다.
헷갈리기 쉬운 점
- 주파수를 올린다고 밝아지지 않습니다. 밝기는 듀티비가 정합니다.
- 듀티비는 0~100 사이만 넣을 수 있습니다. 범위를 넘으면
ValueError가 납니다.- 1kΩ 회로는 100%에서도 어두워서 변화가 작게 보입니다. 330Ω으로 바꾸면 차이가 잘 보입니다.
4. 부저로 도레미 연주하기

수동 부저와 능동 부저
- 수동 부저: 안에 소리를 만드는 회로가 없습니다. 라즈베리파이가 보내는 신호가 1초에 몇 번 켜졌다 꺼지는지에 따라 높낮이가 정해집니다. 그래서 주파수를 바꾸면 음이 바뀝니다.
- 능동 부저: 안에 소리 회로가 들어 있습니다. 전기를 넣기만 하면 늘 같은 높이의 "삐" 소리가 납니다.
도레미를 연주하려면 수동 부저를 씁니다. LED는 듀티비를 바꿨지만, 부저는 듀티비를 50%로 두고 주파수를 바꿉니다.
연결
- 신호 핀 → 물리 12번 = BCM 18
- − 핀(GND) → GND
모듈에 적힌 핀 표기(S, +, −)는 키트마다 다릅니다. 내 모듈에 인쇄된 표기를 따릅니다.

코드
nano pwm_buzzer.py
import RPi.GPIO as GPIO
import time
buz_pin = 18 # 물리 12번 = BCM 18
GPIO.setmode(GPIO.BCM)
GPIO.setup(buz_pin, GPIO.OUT)
scale = [262, 294, 330, 349, 392, 440, 494, 523] # 도 레 미 파 솔 라 시 도
pwm = GPIO.PWM(buz_pin, 262)
pwm.start(50) # 듀티비 50%
try:
for f in scale:
pwm.ChangeFrequency(f) # 주파수를 바꾸면 음이 바뀜
time.sleep(0.5)
finally:
pwm.stop()
del pwm
GPIO.cleanup()
python3 pwm_buzzer.py
scale은 도(262Hz)부터 높은 도(523Hz)까지 여덟 음의 주파수입니다.ChangeFrequency(f)가 음을 바꾸고,time.sleep(0.5)가 한 음을 0.5초 냅니다. 약 4초 뒤에 끝납니다.
헷갈리기 쉬운 점
- 음이 바뀌지 않고 같은 소리만 나면 능동 부저입니다. 수동 부저로 바꿉니다.
- 부저에서 바꾸는 값은 듀티비가 아니라 주파수입니다. 듀티비는 50%로 둡니다.
- 낮은 음은 작게 들립니다. 피에조 부저는 보통 2kHz 근처에서 가장 크게 울립니다.
5. 서보모터를 각도로 움직이기

펄스 폭이 각도를 정합니다
실습용 SG90 서보는 20ms마다 들어오는 펄스를 봅니다. 1초에 50번이라 주파수는 50Hz입니다. 한 번의 펄스가 HIGH로 있는 시간, 곧 펄스 폭이 각도를 정합니다.
| 펄스 폭 | 위치 | 듀티비(펄스 폭 ÷ 20ms) |
|---|---|---|
| 1ms | 한쪽 끝 | 5% |
| 1.5ms | 가운데 | 7.5% |
| 2ms | 반대쪽 끝 | 10% |
처음에는 이 1~2ms 범위로 시작합니다.
전원은 외부 5V로

서보는 움직이는 순간 수백 mA에서 1A가 넘는 전류를 끌어갑니다. 라즈베리파이의 5V 핀(물리 2번 = 5V)에 이으면 전압이 떨어져 보드가 재부팅되거나 저전압이 생길 수 있습니다. 그래서 서보 전원은 외부 5V 전원에서 받습니다.
외부 5V 전원은 예를 들어 5V 어댑터를 브레드보드용 전원 모듈에 꽂아서 씁니다. 외부 전원의 +는 절대 라즈베리파이 핀에 잇지 않습니다.
- 서보 선: 갈색(−) f13, 빨강(+) f14, 주황(신호) f15
- j13 → − 레일, j14 → + 레일
- 외부 5V 전원의 + → + 레일, 외부 전원의 − → − 레일
- 물리 6번 = GND → − 레일(GND를 서로 이어 줍니다)
- 물리 13번 = BCM 27 → j15
신호선은 3.3V GPIO로 충분합니다. 서보 커넥터는 구멍이 난 암 타입이라, 수-수 점퍼선을 끼워 브레드보드로 옮겨 꽂습니다.
전원이 흔들린 적이 있는지는 아래 명령으로 봅니다. throttled=0x0이면 문제가 없었던 것입니다.
vcgencmd get_throttled
코드
nano pwm_servo.py
import RPi.GPIO as GPIO
import time
servo_pin = 27 # 물리 13번 = BCM 27
GPIO.setmode(GPIO.BCM)
GPIO.setup(servo_pin, GPIO.OUT)
MIN_DUTY = 5.0 # 1ms ÷ 20ms = 5%
MAX_DUTY = 10.0 # 2ms ÷ 20ms = 10%
pwm = GPIO.PWM(servo_pin, 50) # 50Hz = 20ms마다 한 번
pwm.start(0)
def set_angle(angle):
duty = MIN_DUTY + (MAX_DUTY - MIN_DUTY) * angle / 180
pwm.ChangeDutyCycle(duty)
time.sleep(0.5) # 움직일 시간
pwm.ChangeDutyCycle(0) # 신호 끊기 (떨림 방지)
try:
for a in [0, 90, 180, 90, 0]:
set_angle(a)
finally:
pwm.stop()
del pwm
GPIO.cleanup()
python3 pwm_servo.py
set_angle()은 0~180을 듀티비 5~10%로 바꿉니다. 0은 5%(1ms), 90은 7.5%(1.5ms), 180은 10%(2ms)입니다.- 0.5초 동안 움직일 시간을 준 뒤
ChangeDutyCycle(0)으로 신호를 끊습니다. 소프트웨어 PWM이라 신호를 계속 주면 서보가 미세하게 떨립니다. - 신호를 끊으면 서보는 그 자리에 있지만, 힘으로 버티지는 않습니다.
끝까지 돌지 않으면
서보마다 실제 범위가 조금씩 다릅니다. 양 끝까지 가지 않으면 범위를 조금씩 넓힙니다.
MIN_DUTY: 5 → 4 → 3 → 2.5(0.5ms)MAX_DUTY: 10 → 11 → 12(2.4ms)
끝에서 "드르륵" 소리가 나면 너무 넓힌 것입니다. 한 단계 되돌립니다.
헷갈리기 쉬운 점
- 빨강과 갈색을 바꿔 꽂지 않습니다. 전원을 넣기 전에 색을 다시 봅니다.
- 서보 전원을 3.3V 핀에 잇지 않습니다. 외부 전원을 쓰면 GND를 라즈베리파이와 꼭 이어 줍니다. GND가 빠지면 신호를 읽지 못해 움직이지 않습니다.
- 전원이 들어온 서보의 축을 손으로 억지로 돌리지 않습니다. 안쪽 기어가 상합니다.
마치며
버튼을 내부 풀업으로 읽었고, add_event_detect()와 bouncetime=50으로 누를 때마다 LED를 켜고 껐습니다. PWM에서는 듀티비로 LED 밝기를, 주파수로 부저의 음을, 펄스 폭으로 서보의 각도를 정했습니다. 서보는 외부 5V 전원에 잇고 GND를 라즈베리파이와 이어 주는 것까지 기억하면 됩니다.
참고 자료
- rpi-lgpio API(
PUD_UP,add_event_detect, 소프트웨어 PWM): https://rpi-lgpio.readthedocs.io/en/latest/api.html - rpi-lgpio와 원래 RPi.GPIO의 차이(bouncetime 방식, 사용 중인 핀 오류,
gpioinfo): https://rpi-lgpio.readthedocs.io/en/latest/differences.html - rpi-lgpio 설치(원래 패키지와 함께 설치하지 않음): https://rpi-lgpio.readthedocs.io/en/latest/install.html
- rpi-lgpio PWM 종료 메시지 이슈(
del pwm): https://github.com/waveform80/rpi-lgpio/issues/15 - 라즈베리파이 공식 GPIO 문서(내부 풀업, PWM): https://github.com/raspberrypi/documentation/blob/master/documentation/asciidoc/computers/raspberry-pi/gpio-on-raspberry-pi.adoc
- 라즈베리파이 공식 전원 문서(저전압): https://github.com/raspberrypi/documentation/blob/master/documentation/asciidoc/computers/raspberry-pi/power-supplies.adoc
- 라즈베리파이 공식
vcgencmd get_throttled: https://github.com/raspberrypi/documentation/blob/master/documentation/asciidoc/computers/os/graphics-utilities.adoc - 라즈베리파이 공식 Physical Computing(모터는 따로 전원): https://projects.raspberrypi.org/en/projects/physical-computing
- SG90 데이터시트(펄스 폭 1~2ms): https://www.electronicoscaldas.com/datasheet/SG90_Tower-Pro.pdf
- Tower Pro SG90 제품 페이지(외부 어댑터 전원): https://towerpro.com.tw/product/sg90-7/
'IoT' 카테고리의 다른 글
| 라즈베리파이 비밀번호 잠금 해제와 GPIO 오류 두 가지 해결기 (0) | 2026.10.13 |
|---|---|
| 라즈베리파이 LED 4개와 버튼으로 신호등·2진수·모스 부호 만들기 (0) | 2026.10.13 |
| 라즈베리파이 전자 기초, 옴의 법칙과 LED 저항 (0) | 2026.10.06 |
| 라즈베리파이로 LED제어하기 (1) | 2026.10.01 |
| 라즈베리파이로 파이썬 실행하기 (0) | 2026.10.01 |