
Raspberry Pi + HC-SR04: Đo khoảng cách bằng GPIO
Đọc khoảng cách 2–400cm bằng cảm biến siêu âm HC-SR04 qua GPIO Raspberry Pi, dùng cầu phân áp bảo vệ chân ECHO 5V và thư viện RPi.GPIO.
Dự án nhập môn GPIO trên Raspberry Pi: đo khoảng cách bằng cảm biến siêu âm HC-SR04.
Bạn sẽ học cách phát xung TRIG, đo độ rộng xung ECHO để tính khoảng cách, và quan trọng nhất là cách bảo vệ chân GPIO 3.3V của Raspberry Pi khỏi tín hiệu ECHO 5V bằng cầu phân áp điện trở, một kỹ năng nền tảng bắt buộc khi ghép cảm biến 5V với Raspberry Pi.
Hướng dẫn chi tiết
Hướng dẫn đo khoảng cách 2–400cm bằng HC-SR04 trên Raspberry Pi qua GPIO, dùng cầu phân áp điện trở bảo vệ chân ECHO 5V, kèm 3 mẫu code Python từ cơ bản đến dịch vụ giám sát telemetry.
1. Giới thiệu
Cảm biến siêu âm HC-SR04 là một trong những cảm biến nhập môn phổ biến nhất khi học GPIO trên Raspberry Pi: rẻ, dễ hiểu nguyên lý, và dạy được kỹ năng quan trọng nhất khi ghép cảm biến ngoại vi với Raspberry Pi là bảo vệ chân GPIO 3.3V khỏi tín hiệu 5V.
HC-SR04 hoạt động bằng cách phát một chùm sóng siêu âm qua chân TRIG, rồi đo thời gian sóng phản xạ về qua chân ECHO. Vì tốc độ âm thanh trong không khí xấp xỉ 343 m/s, ta tính được khoảng cách bằng công thức distance = (thời_gian_echo × 34300) / 2 (chia 2 vì sóng đi rồi về).
Vấn đề là chân ECHO của HC-SR04 xuất tín hiệu ở mức 5V, trong khi toàn bộ chân GPIO của Raspberry Pi chỉ chịu được tối đa 3.3V — cấp thẳng 5V vào GPIO có thể làm hỏng vĩnh viễn chân đó hoặc cả board. Bài viết này hướng dẫn dùng cầu phân áp bằng 2 điện trở 10kΩ để hạ áp ECHO xuống mức an toàn (~2.5V) trước khi đưa vào GPIO.
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đ |
Cảm biến siêu âm HC-SR04 | 1 | ~15.000đ |
Điện trở 10kΩ (cho cầu phân áp ECHO) | 2 | ~1.000đ |
Breadboard + dây jumper (male-female, male-male) | 1 bộ | ~30.000đ |
3. Sơ đồ kết nối
HC-SR04 | Raspberry Pi 4 |
|---|---|
VCC | 5V (Pin 2) |
GND | GND (Pin 6) |
TRIG | GPIO23 (Pin 16) — nối thẳng, không cần chia áp vì Pi cấp 3.3V > ngưỡng HIGH tối thiểu của TRIG |
ECHO | qua cầu phân áp R1(10kΩ nối tiếp) + R2(10kΩ xuống GND) → điểm giữa vào GPIO24 (Pin 18) |
Chi tiết cầu phân áp: ECHO (5V) → R1 (10kΩ) → điểm giữa → R2 (10kΩ) → GND. Điểm giữa nối vào GPIO24. Với R1 = R2 = 10kΩ, điện áp tại điểm giữa = 5V × R2/(R1+R2) = 5V × 0.5 = 2.5V — nằm an toàn dưới ngưỡng 3.3V tối đa của GPIO, và vẫn đủ cao hơn ngưỡng nhận biết mức HIGH (~2V trên logic 3.3V) để Raspberry Pi đọc đúng tín hiệu.
Cảnh báo an toàn: Tuyệt đối không nối trực tiếp chân ECHO vào GPIO khi chưa qua cầu phân áp, nguy cơ hỏng GPIO hoặc cả board Raspberry Pi.
4. Code ví dụ #1 — Đo khoảng cách cơ bản
Vòng lặp đơn giản: phát xung TRIG, đo độ rộng xung ECHO, in khoảng cách ra terminal. Có xử lý timeout để tránh treo chương trình khi không nhận được echo (vật thể ngoài phạm vi 2–400cm).
#!/usr/bin/env python3
"""
Raspberry Pi + HC-SR04 - Do khoang cach co ban qua GPIO
Board: Raspberry Pi 4 Model B
Thu vien: RPi.GPIO
Wiring:
HC-SR04 VCC -> RPi 5V (Pin 2)
HC-SR04 GND -> RPi GND
HC-SR04 TRIG -> RPi GPIO23 (Pin 16)
HC-SR04 ECHO -> cau phan ap 10k/10k -> RPi GPIO24 (Pin 18)
"""
import RPi.GPIO as GPIO
import time
TRIG = 23
ECHO = 24
def setup():
GPIO.setmode(GPIO.BCM)
GPIO.setup(TRIG, GPIO.OUT)
GPIO.setup(ECHO, GPIO.IN)
GPIO.output(TRIG, False)
print("[BOOT] HC-SR04 distance sensor ready. TRIG=GPIO23 ECHO=GPIO24")
time.sleep(0.5)
def read_distance_cm(timeout=0.04):
GPIO.output(TRIG, True)
time.sleep(0.00001)
GPIO.output(TRIG, False)
start_wait = time.time()
pulse_start = start_wait
while GPIO.input(ECHO) == 0:
pulse_start = time.time()
if pulse_start - start_wait > timeout:
return None # timeout - khong nhan duoc echo
pulse_end = time.time()
stop_wait = pulse_end
while GPIO.input(ECHO) == 1:
pulse_end = time.time()
if pulse_end - stop_wait > timeout:
return None
pulse_duration = pulse_end - pulse_start
distance = pulse_duration * 34300 / 2 # toc do am thanh 343 m/s
return round(distance, 1)
def main():
setup()
try:
while True:
dist = read_distance_cm()
if dist is None:
print("[WARN] Khong nhan duoc echo (ngoai pham vi 2-400cm)")
else:
print(f"Khoang cach: {dist} cm")
time.sleep(0.3)
except KeyboardInterrupt:
print("\n[EXIT] Dung boi nguoi dung")
finally:
GPIO.cleanup()
if __name__ == "__main__":
main()
5. Code ví dụ #2 — Lọc nhiễu bằng trung bình trượt + cảnh báo ngưỡng
HC-SR04 giá rẻ hay bị nhiễu số đọc. Bản này dùng cửa sổ trung bình trượt (moving average) 5 mẫu gần nhất để làm mượt kết quả, đồng thời lọc bỏ giá trị ngoài phạm vi vật lý hợp lệ (2–400cm) và in cảnh báo khi vật thể lại gần dưới ngưỡng 15cm.
#!/usr/bin/env python3
"""
Raspberry Pi + HC-SR04 - Loc nhieu bang trung binh truot + canh bao nguong
"""
import RPi.GPIO as GPIO
import time
from collections import deque
TRIG = 23
ECHO = 24
WINDOW_SIZE = 5
ALERT_THRESHOLD_CM = 15.0
def setup():
GPIO.setmode(GPIO.BCM)
GPIO.setup(TRIG, GPIO.OUT)
GPIO.setup(ECHO, GPIO.IN)
GPIO.output(TRIG, False)
print("[BOOT] HC-SR04 filtered reader ready")
time.sleep(0.5)
def read_raw_cm(timeout=0.04):
GPIO.output(TRIG, True)
time.sleep(0.00001)
GPIO.output(TRIG, False)
start_wait = time.time()
pulse_start = start_wait
while GPIO.input(ECHO) == 0:
pulse_start = time.time()
if pulse_start - start_wait > timeout:
return None
pulse_end = time.time()
stop_wait = pulse_end
while GPIO.input(ECHO) == 1:
pulse_end = time.time()
if pulse_end - stop_wait > timeout:
return None
distance = (pulse_end - pulse_start) * 34300 / 2
if distance < 2 or distance > 400:
return None
return distance
def main():
setup()
window = deque(maxlen=WINDOW_SIZE)
try:
while True:
raw = read_raw_cm()
if raw is not None:
window.append(raw)
if len(window) == 0:
print("[WARN] Chua co du lieu hop le")
else:
avg = sum(window) / len(window)
status = "CANH BAO - VAT THE QUA GAN!" if avg < ALERT_THRESHOLD_CM else "OK"
print(f"Khoang cach (loc): {avg:.1f} cm | {status}")
time.sleep(0.3)
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ế: dịch vụ giám sát liên tục (telemetry JSON)
Phiên bản gần với ứng dụng thực tế: chạy như một dịch vụ nền, in dữ liệu telemetry dạng JSON định kỳ (dễ ghép vào hệ thống log/dashboard), và tự báo trạng thái "degraded" khi cảm biến mất tín hiệu liên tục thay vì crash chương trình — nguyên tắc quan trọng khi triển khai cảm biến thực tế lâu dài.
#!/usr/bin/env python3
"""
Raspberry Pi + HC-SR04 - Dich vu giam sat khoang cach lien tuc (telemetry JSON)
Ung dung thuc te: log khoang cach dinh ky, tu bao trang thai "degraded"
khi cam bien loi tam thoi thay vi crash chuong trinh.
"""
import RPi.GPIO as GPIO
import time
import json
TRIG = 23
ECHO = 24
TELEMETRY_INTERVAL_S = 2.0
MAX_CONSECUTIVE_FAILURES = 5
def setup():
GPIO.setmode(GPIO.BCM)
GPIO.setup(TRIG, GPIO.OUT)
GPIO.setup(ECHO, GPIO.IN)
GPIO.output(TRIG, False)
print("=" * 50)
print("[BOOT] HC-SR04 Distance Monitoring Service")
print("[BOOT] TRIG=GPIO23 ECHO=GPIO24 (qua cau phan ap 10k/10k)")
print("=" * 50)
time.sleep(0.5)
def read_distance_cm(timeout=0.04):
GPIO.output(TRIG, True)
time.sleep(0.00001)
GPIO.output(TRIG, False)
start_wait = time.time()
pulse_start = start_wait
while GPIO.input(ECHO) == 0:
pulse_start = time.time()
if pulse_start - start_wait > timeout:
return None
pulse_end = time.time()
stop_wait = pulse_end
while GPIO.input(ECHO) == 1:
pulse_end = time.time()
if pulse_end - stop_wait > timeout:
return None
distance = (pulse_end - pulse_start) * 34300 / 2
if distance < 2 or distance > 400:
return None
return round(distance, 1)
def main():
setup()
consecutive_failures = 0
last_telemetry = 0.0
try:
while True:
distance = read_distance_cm()
if distance is None:
consecutive_failures += 1
else:
consecutive_failures = 0
now = time.time()
if now - last_telemetry >= TELEMETRY_INTERVAL_S:
degraded = consecutive_failures >= MAX_CONSECUTIVE_FAILURES
telemetry = {
"sensor": "hcsr04",
"distance_cm": None if degraded else distance,
"status": "degraded" if degraded else "ok",
"consecutive_failures": consecutive_failures,
}
print(json.dumps(telemetry))
last_telemetry = now
if degraded:
print("[WARN] Cam bien mat tin hieu lien tuc - kiem tra day noi/nguon")
time.sleep(0.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 |
|---|---|---|
Luôn nhận | Sai chân TRIG/ECHO, hoặc cảm biến không được cấp đủ 5V | Kiểm tra lại wiring theo bảng mục 3; đo điện áp VCC bằng đồng hồ vạn năng |
Số đọc nhảy lung tung, sai lệch lớn | Nhiễu điện, breadboard tiếp xúc kém, vật thể có bề mặt hấp thụ âm (vải, mút xốp) | Dùng bản lọc trung bình trượt (mục 5); kiểm tra lại các điểm nối trên breadboard |
GPIO24 luôn đọc HIGH bất kể có vật cản hay không | Thiếu điện trở R2 (kéo GND) trong cầu phân áp — chân đang thả nổi (floating) | Đảm bảo lắp đủ cả R1 và R2 đúng theo sơ đồ mục 3, không bỏ sót R2 |
| Chương trình trước đó thoát không gọi | Luôn dùng khối |
Board Raspberry Pi không nhận GPIO / GPIO chết vĩnh viễn | Đấu nhầm ECHO thẳng vào GPIO không qua cầu phân áp (5V vào chân 3.3V) | Không có cách sửa phần cứng đã hỏng — luôn kiểm tra kỹ cầu phân áp trước khi cấp nguồn lần đầu |
8. Tổng kết
Qua dự án này, bạn đã nắm được nguyên lý đo khoảng cách bằng cảm biến siêu âm (đo thời gian bay của sóng phản xạ), và quan trọng hơn là kỹ năng nền tảng khi làm việc với Raspberry Pi: luôn kiểm tra mức điện áp logic của mọi cảm biến/module trước khi nối vào GPIO, và dùng cầu phân áp điện trở khi cần hạ áp tín hiệu 5V xuống 3.3V.
Ba mẫu code đi từ đọc cơ bản → lọc nhiễu → dịch vụ giám sát telemetry cho thấy cách nâng cấp một đoạn code demo thành một thành phần đáng tin cậy hơn trong hệ thống thực tế. Bước tiếp theo bạn có thể thử: ghép cảm biến này với buzzer/LED để làm cảm biến đỗ xe, hoặc gửi dữ liệu qua MQTT lên dashboard IoT.