Raspberry Pi + HC-SR04: Đo khoảng cách bằng GPIO
Cơ bản1/8/2026- Tác giả: IoTlabs Maker

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.

Raspberry PiGPIOHC-SR04UltrasonicPythonRPi.GPIOCảm biến khoảng cách

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 None / timeout

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

RuntimeError: This channel is already in use

Chương trình trước đó thoát không gọi GPIO.cleanup()

Luôn dùng khối try/finally gọi GPIO.cleanup() như trong code mẫu

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.