
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.
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 |
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 |
| Gọi | Đảm bảo chỉ đăng ký event detect một lần trong |
Callback không được gọi dù đã nhấn nút | Đăng ký sai loại cạnh (ví dụ dùng | Xác nhận logic pull-up/active-low của mạch trước khi chọn |
Code nhấn giữ luôn báo "nhấn ngắn" dù giữ lâu | Biến | Giảm |
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ố.