본문 바로가기

라즈베리파이 PWM으로 LED·부저·서보 제어하기

IoT SorryP 2026. 10. 6. 15:07
반응형

라즈베리파이 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. 버튼을 풀업으로 읽기

내부 풀업을 켠 입력 핀에서 버튼을 누르지 않으면 HIGH(1), 누르면 GND와 이어져 LOW(0)가 되는 것을 나란히 보여 주는 두 칸 그림

아무 데도 안 이은 입력은 흔들립니다

GPIO를 입력으로 쓰면 그 핀의 전압을 읽어 HIGH(1)나 LOW(0)로 알려 줍니다. 그런데 핀이 아무 데도 이어져 있지 않으면 값이 정해지지 않습니다. 손을 가까이 대기만 해도 1과 0 사이를 오갑니다. 이 상태를 플로팅이라고 합니다.

내부 풀업을 켭니다

pull_up_down=GPIO.PUD_UP을 주면 핀 안쪽의 저항이 핀을 3.3V 쪽으로 약하게 당겨 줍니다. 이것이 풀업입니다. 외부 저항은 따로 필요 없습니다.

  • 버튼을 누르지 않으면 HIGH(1)입니다.
  • 버튼을 누르면 핀이 GND와 이어져 LOW(0)가 됩니다.

연결

버튼은 e14·e16·f14·f16에 가운데 홈을 걸쳐 꽂고 물리 11번 = BCM 17 → a14, a16 → − 레일, LED는 물리 7번 = BCM 4 → a5, 저항 c5–c9, 긴 다리 e9·짧은 다리 e10, a10 → − 레일, 물리 6번 = GND → − 레일로 연결한 3.3V 선 없는 브레드보드 배선도

아래 구멍 위치는 한 가지 예시입니다.

  1. 물리 6번 = GND → − 레일
  2. LED: 라즈베리파이로 LED제어하기와 같은 회로입니다. 물리 7번 = BCM 4 → a5, 저항(330Ω 또는 1kΩ) c5 – c9, LED 긴 다리 e9, 짧은 다리 e10, a10 → − 레일
  3. 버튼: e14, e16, f14, f16(가운데 홈에 걸쳐서)
  4. 물리 11번 = BCM 17 → a14
  5. 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. 누를 때마다 켜고 끄기

버튼을 누르는 순간 신호가 HIGH에서 LOW로 떨어지는 지점에서 함수가 한 번 실행되고, 그 직후의 짧은 바운스 톱니를 bouncetime 50ms가 걸러 내는 파형 그림

배선은 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 밝기 바꾸기

듀티비 25%, 50%, 75% 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. 부저로 도레미 연주하기

도 262Hz, 레 294Hz, 미 330Hz, 파 349Hz, 솔 392Hz, 라 440Hz, 시 494Hz, 높은 도 523Hz를 나타낸 음과 주파수 대응표

수동 부저와 능동 부저

  • 수동 부저: 안에 소리를 만드는 회로가 없습니다. 라즈베리파이가 보내는 신호가 1초에 몇 번 켜졌다 꺼지는지에 따라 높낮이가 정해집니다. 그래서 주파수를 바꾸면 음이 바뀝니다.
  • 능동 부저: 안에 소리 회로가 들어 있습니다. 전기를 넣기만 하면 늘 같은 높이의 "삐" 소리가 납니다.

도레미를 연주하려면 수동 부저를 씁니다. LED는 듀티비를 바꿨지만, 부저는 듀티비를 50%로 두고 주파수를 바꿉니다.

연결

  • 신호 핀 → 물리 12번 = BCM 18
  • − 핀(GND) → GND

모듈에 적힌 핀 표기(S, +, −)는 키트마다 다릅니다. 내 모듈에 인쇄된 표기를 따릅니다.

수동 부저 모듈 세 핀을 e5·e6·e7에 꽂고 물리 12번 = BCM 18 → a5(S 핀 줄), GND(−) 핀 줄 a7 → 위쪽 − 레일, 물리 6번 = GND → 위쪽 − 레일로 연결하고 가운데 핀은 비워 둔 브레드보드 배선도와, 핀 순서는 모듈에 인쇄된 표기를 따른다는 안내

코드

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. 서보모터를 각도로 움직이기

20ms(50Hz) 주기 안에서 펄스 폭 1ms는 한쪽 끝, 1.5ms는 가운데, 2ms는 반대쪽 끝으로 서보 축이 향하는 그림과 각 듀티비 5%, 7.5%, 10%

펄스 폭이 각도를 정합니다

실습용 SG90 서보는 20ms마다 들어오는 펄스를 봅니다. 1초에 50번이라 주파수는 50Hz입니다. 한 번의 펄스가 HIGH로 있는 시간, 곧 펄스 폭이 각도를 정합니다.

펄스 폭 위치 듀티비(펄스 폭 ÷ 20ms)
1ms 한쪽 끝 5%
1.5ms 가운데 7.5%
2ms 반대쪽 끝 10%

처음에는 이 1~2ms 범위로 시작합니다.

전원은 외부 5V로

SG90 서보 갈색 f13·빨강 f14·주황(신호) f15, j13 → − 레일, j14 → + 레일, 외부 5V 전원 + → + 레일·− → − 레일, 물리 6번 = GND → − 레일(공통 GND), 물리 13번 = BCM 27 → j15로 연결한 브레드보드 배선도와, 빨강·갈색을 바꾸지 말고 서보 전원을 3.3V 핀에, + 레일을 라즈베리파이 핀에 잇지 말라는 경고

서보는 움직이는 순간 수백 mA에서 1A가 넘는 전류를 끌어갑니다. 라즈베리파이의 5V 핀(물리 2번 = 5V)에 이으면 전압이 떨어져 보드가 재부팅되거나 저전압이 생길 수 있습니다. 그래서 서보 전원은 외부 5V 전원에서 받습니다.

외부 5V 전원은 예를 들어 5V 어댑터를 브레드보드용 전원 모듈에 꽂아서 씁니다. 외부 전원의 +는 절대 라즈베리파이 핀에 잇지 않습니다.

  1. 서보 선: 갈색(−) f13, 빨강(+) f14, 주황(신호) f15
  2. j13 → − 레일, j14 → + 레일
  3. 외부 5V 전원의 + → + 레일, 외부 전원의 − → − 레일
  4. 물리 6번 = GND → − 레일(GND를 서로 이어 줍니다)
  5. 물리 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를 라즈베리파이와 이어 주는 것까지 기억하면 됩니다.

참고 자료

다음 글: 라즈베리파이 LED 4개와 버튼으로 신호등·2진수·모스 부호 만들기

반응형