Raspberry Pi + Button/LED: Nhập môn GPIO, ngắt và debounce
Cơ bản1/8/2026- Tác giả: IoTlabs Maker

Raspberry Pi + Button/LED: Nhập môn GPIO, ngắt và debounce

Nhập môn GPIO input/output trên Raspberry Pi với nút nhấn và LED: đọc/ghi GPIO cơ bản, xử lý ngắt (interrupt/event detection), và chống nảy phím (debounce) bằng phần mềm.

Raspberry PiGPIOButtonLEDInterruptDebouncePythonRPi.GPIO

Dự án nhập môn GPIO thứ tư và cũng là nền tảng nhất: nút nhấn điều khiển LED. Đây là bài học kinh điển để hiểu ba khái niệm cốt lõi của lập trình GPIO trên Raspberry Pi — đọc/ghi digital I/O.

Xử lý sự kiện bằng interrupt (GPIO.add_event_detect) thay vì polling tốn CPU, và quan trọng nhất là chống nảy phím (debounce): hiện tượng tiếp điểm cơ khí của nút nhấn "rung" trong vài mili-giây khi đóng/mở, khiến chương trình đọc nhầm thành nhiều lần nhấn liên tiếp nếu không xử lý đúng.

Hướng dẫn chi tiết

Nhập môn GPIO Raspberry Pi qua bài nút nhấn điều khiển LED: đọc/ghi digital I/O, xử lý bằng interrupt (GPIO.add_event_detect) và chống nảy phím (debounce) phần mềm, kèm ứng dụng nhấn ngắn/nhấn giữ.

1. Giới thiệu

Nút nhấn điều khiển LED là bài học "Hello World" của lập trình phần cứng GPIO — nhưng ẩn sau vẻ đơn giản là ba khái niệm nền tảng mà bất kỳ ai làm việc với vi điều khiển cũng phải nắm vững.

Thứ nhất, đọc/ghi digital I/O: cấu hình chân là INPUT hay OUTPUT, đọc mức HIGH/LOW, ghi tín hiệu điều khiển.

Thứ hai, xử lý sự kiện bằng interrupt (ngắt phần cứng, còn gọi là event detection trong RPi.GPIO) thay vì polling liên tục — giúp chương trình phản ứng tức thời mà không lãng phí CPU.

Thứ ba và quan trọng nhất cho nút nhấn cơ khí: debounce (chống nảy phím) — khi bạn nhấn hoặc thả một nút nhấn cơ khí, tiếp điểm kim loại bên trong không đóng/mở dứt khoát ngay lập tức mà "rung" qua lại trong vài mili-giây, khiến bộ vi xử lý (đọc tín hiệu ở tốc độ MHz) hiểu nhầm thành hàng chục lần nhấn/thả liên tiếp nếu không được xử lý đúng.

2. Linh kiện cần có

Linh kiện

Số lượng

Giá tham khảo

Raspberry Pi 4 Model B (đã cài Raspberry Pi OS)

1

~1.500.000đ

Nút nhấn tactile switch 6×6mm

1

~500đ

Module LED đơn (có điện trở hạn dòng onboard)

1

~5.000đ

Breadboard + dây jumper

1 bộ

~30.000đ

3. Sơ đồ kết nối

Chân module

Raspberry Pi 4

Nút nhấn – chân A

GPIO4 (Pin 7)

Nút nhấn – chân B

GND

LED VCC

3V3 (Pin 1)

LED GND

GND

LED IN

GPIO25 (Pin 22)

Về pull-up/pull-down: Nút nhấn chỉ có 2 chân (A-B), khi nhấn thì nối A với B (nối với GND); khi thả, chân A "thả nổi" (floating) nếu không có điện trở kéo — dẫn đến đọc giá trị ngẫu nhiên nhiễu.

Thay vì dùng điện trở pull-up vật lý bên ngoài, dự án này dùng pull-up nội bộ (internal pull-up) có sẵn trong chip Broadcom của Raspberry Pi, kích hoạt bằng GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_UP): khi không nhấn, chân được kéo lên HIGH (3.3V) qua điện trở nội bộ (~50kΩ); khi nhấn, chân nối thẳng xuống GND nên đọc được LOW.

4. Code ví dụ #1 — Đọc nút nhấn bằng polling + debounce phần mềm

Kỹ thuật debounce đơn giản nhất: đọc chân nhiều lần liên tiếp trong khoảng thời gian ngắn, chỉ chấp nhận kết quả khi tất cả các lần đọc đều cho cùng giá trị (ổn định).

#!/usr/bin/env python3
"""
Raspberry Pi + Button/LED - Nhap mon GPIO: doc nut nhan (polling) + debounce phan mem
Wiring:
  Button chan A -> GPIO4 (pull-up noi bo, nhan = LOW)
  Button chan B -> GND
  LED IN        -> GPIO25
"""
import RPi.GPIO as GPIO
import time

BUTTON_PIN = 4
LED_PIN = 25
DEBOUNCE_S = 0.05


def setup():
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(BUTTON_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
    GPIO.setup(LED_PIN, GPIO.OUT)
    GPIO.output(LED_PIN, False)
    print("[BOOT] Button/LED polling demo ready (GPIO4=button, GPIO25=led)")


def read_debounced(pin, stable_reads=3, sample_delay=DEBOUNCE_S / 3):
    """Doc pin nhieu lan lien tiep, chi tin ket qua khi on dinh."""
    last = GPIO.input(pin)
    stable_count = 1
    for _ in range(stable_reads - 1):
        time.sleep(sample_delay)
        current = GPIO.input(pin)
        if current == last:
            stable_count += 1
        else:
            last = current
            stable_count = 1
    return last if stable_count >= stable_reads else None


def main():
    setup()
    led_state = False
    last_button_state = GPIO.HIGH  # nha (khong nhan) = HIGH voi pull-up

    try:
        while True:
            stable = read_debounced(BUTTON_PIN)
            if stable is not None and stable != last_button_state:
                if stable == GPIO.LOW:  # canh xuong = vua nhan
                    led_state = not led_state
                    GPIO.output(LED_PIN, led_state)
                    print(f"[BUTTON] Nhan -> LED {'BAT' if led_state else 'TAT'}")
                last_button_state = stable
            time.sleep(0.02)
    except KeyboardInterrupt:
        print("\n[EXIT] Dung boi nguoi dung")
    finally:
        GPIO.cleanup()


if __name__ == "__main__":
    main()

5. Code ví dụ #2 — Xử lý bằng interrupt (GPIO.add_event_detect)

Thay vì polling liên tục (vòng lặp while True luôn kiểm tra), bản này đăng ký callback chỉ chạy khi có cạnh xuống (FALLING edge) trên chân nút nhấn — hiệu quả hơn, đúng chuẩn lập trình sự kiện. Tham số bouncetime của RPi.GPIO tự động chặn các sự kiện lặp lại trong X mili-giây (debounce phần cứng cấp thư viện), nhưng để chắc chắn tuyệt đối, code vẫn đọc lại mức pin sau một khoảng trễ ngắn để xác nhận.

#!/usr/bin/env python3
"""
Raspberry Pi + Button/LED - Xu ly nut nhan bang ngat (interrupt/event detection)
Su dung GPIO.add_event_detect + bouncetime + xac nhan lai muc pin de chong nay chinh xac hon.
"""
import RPi.GPIO as GPIO
import time

BUTTON_PIN = 4
LED_PIN = 25
CONFIRM_DELAY_S = 0.02  # doi ngan roi doc lai de xac nhan muc on dinh (chong nay phan cung)

led_state = False


def button_pressed(channel):
    global led_state
    time.sleep(CONFIRM_DELAY_S)
    if GPIO.input(channel) != GPIO.LOW:
        return  # nhieu/nay - khong phai nhan that
    led_state = not led_state
    GPIO.output(LED_PIN, led_state)
    print(f"[IRQ] Nut nhan (interrupt) -> LED {'BAT' if led_state else 'TAT'}")


def setup():
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(BUTTON_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
    GPIO.setup(LED_PIN, GPIO.OUT)
    GPIO.output(LED_PIN, False)
    GPIO.add_event_detect(BUTTON_PIN, GPIO.FALLING, callback=button_pressed, bouncetime=200)
    print("[BOOT] Interrupt-driven button/LED demo ready")


def main():
    setup()
    try:
        while True:
            time.sleep(1)  # main loop ranh - xu ly hoan toan qua callback ngat
    except KeyboardInterrupt:
        print("\n[EXIT] Dung boi nguoi dung")
    finally:
        GPIO.cleanup()


if __name__ == "__main__":
    main()

6. Code ví dụ #3 — Ứng dụng thực tế: nút nhấn đa chức năng (nhấn ngắn/nhấn giữ)

Mẫu ứng dụng thực tế thường gặp trên các thiết bị IoT: một nút nhấn duy nhất nhưng phân biệt được nhấn ngắn (dưới 0.6 giây → bật/tắt) và nhấn giữ (từ 0.6 giây trở lên → nhấp nháy báo hiệu 3 lần, ví dụ để reset cấu hình). Kỹ thuật này dùng GPIO.BOTH để bắt cả cạnh lên lẫn cạnh xuống, đo khoảng thời gian giữa hai sự kiện để phân loại hành động.

#!/usr/bin/env python3
"""
Raspberry Pi + Button/LED - Ung dung thuc te: nut nhan da chuc nang
Nhan ngan (<0.6s) -> bat/tat LED
Nhan giu (>=0.6s) -> LED nhap nhay bao hieu 3 lan
Ket hop interrupt GPIO.BOTH de do thoi gian giu nut chinh xac.
"""
import RPi.GPIO as GPIO
import time

BUTTON_PIN = 4
LED_PIN = 25
LONG_PRESS_S = 0.6
DEBOUNCE_MS = 200

press_start = None
led_state = False


def blink(times=3, on_time=0.15, off_time=0.15):
    original = led_state
    for _ in range(times):
        GPIO.output(LED_PIN, True)
        time.sleep(on_time)
        GPIO.output(LED_PIN, False)
        time.sleep(off_time)
    GPIO.output(LED_PIN, original)


def on_edge(channel):
    global press_start, led_state
    time.sleep(0.02)  # xac nhan muc on dinh (chong nay)
    level = GPIO.input(channel)

    if level == GPIO.LOW:  # canh xuong = bat dau nhan
        press_start = time.time()
        return

    # canh len = tha nut
    if press_start is None:
        return
    held = time.time() - press_start
    press_start = None

    if held >= LONG_PRESS_S:
        print(f"[BUTTON] Nhan giu {held:.2f}s -> bao hieu nhap nhay")
        blink()
    else:
        led_state = not led_state
        GPIO.output(LED_PIN, led_state)
        print(f"[BUTTON] Nhan ngan {held:.2f}s -> LED {'BAT' if led_state else 'TAT'}")


def setup():
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(BUTTON_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
    GPIO.setup(LED_PIN, GPIO.OUT)
    GPIO.output(LED_PIN, False)
    GPIO.add_event_detect(BUTTON_PIN, GPIO.BOTH, callback=on_edge, bouncetime=DEBOUNCE_MS)
    print("[BOOT] Multi-mode button ready: short=toggle, long(>=0.6s)=blink x3")


def main():
    setup()
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        print("\n[EXIT] Dung boi nguoi dung")
    finally:
        GPIO.cleanup()


if __name__ == "__main__":
    main()

7. Lỗi thường gặp

Lỗi

Nguyên nhân

Cách sửa

LED bật/tắt "nhảy" nhiều lần chỉ với 1 lần nhấn

Chưa xử lý debounce — tiếp điểm cơ khí rung khi đóng/mở khiến chương trình đọc nhiều cạnh tín hiệu giả

Dùng bouncetime trong add_event_detect() và/hoặc xác nhận lại mức pin sau một khoảng trễ ngắn như code ví dụ #2, #3

Chân nút nhấn đọc giá trị ngẫu nhiên khi không nhấn

Chân GPIO đang "thả nổi" (floating) — thiếu pull-up/pull-down

Bật pull-up nội bộ bằng pull_up_down=GPIO.PUD_UP trong GPIO.setup()

RuntimeError: Conflicting edge detection already enabled

Gọi add_event_detect() nhiều lần trên cùng 1 chân mà chưa gọi remove_event_detect() hoặc GPIO.cleanup() trước đó

Đảm bảo chỉ đăng ký event detect một lần trong setup(), và luôn GPIO.cleanup() khi thoát

Callback không được gọi dù đã nhấn nút

Đăng ký sai loại cạnh (ví dụ dùng GPIO.RISING nhưng logic là active-low nên sự kiện thực tế là FALLING)

Xác nhận logic pull-up/active-low của mạch trước khi chọn RISING/FALLING/BOTH

Code nhấn giữ luôn báo "nhấn ngắn" dù giữ lâu

Biến press_start bị reset sai do đọc nhầm callback, hoặc bouncetime quá lớn làm mất sự kiện cạnh lên khi thả nút

Giảm bouncetime xuống hợp lý (100–200ms) và kiểm tra log in ra ở từng bước để debug

8. Tổng kết

Dự án nút nhấn + LED tưởng đơn giản nhưng dạy đủ ba kỹ năng GPIO nền tảng: đọc/ghi digital I/O, xử lý sự kiện hiệu quả bằng interrupt thay vì polling, và debounce đúng cách — kỹ năng sẽ theo bạn xuyên suốt mọi dự án phần cứng sau này, từ cảm biến chuyển động, encoder xoay, đến bàn phím ma trận.

Ba mẫu code từ polling cơ bản → interrupt → nhấn ngắn/nhấn giữ đa chức năng cho thấy cách nâng cấp dần độ phức tạp và độ tin cậy. Hướng mở rộng tiếp theo: kết hợp nhiều nút nhấn để làm menu điều khiển, hoặc dùng interrupt với encoder xoay (rotary encoder) để đọc giá trị analog dạng số.