超音波センサHC-SR04の使い方|ArduinoとRaspberry Piで解説

Raspberry Piと超音波センサで物体に追従させる
Raspberry Piと超音波センサで物体に追従させる

超音波センサの仕組みを解説

なんで超音波で距離が測定できるの?

こうもりは超音波で「見ている」
こうもりは超音波で「見ている」

超音波で距離が測定できる仕組みを説明しますね。

そもそも超音波とは、人間の耳には「聞こえない」または「聞こえにくい」音域の音のことです。とはいえ聞こえないだけであって音にはかわりありません。

おいらは聞こえてるよ
おいらは聞こえてるよ

超音波も音も、空気を振動させて遠くへ広がっていきます。そして、その音の速さは決まっているのです。(ココ大事)

音の速さは、気温や気圧によっても変化しますが、たとえば1気圧で20度のときでは「秒速343.7m/s」であることが知られてます。

よって、音の速さが分かっているのであれば「音を出してからその音が戻ってくるまでの時間」を測れば距離が測定できます。

つまり距離の計算式は次のようになります。

$$ 距離 = 音速 \times \frac{往復時間}{2} $$

音速の定義

また、音の速さを正確に知りたい場合は、次式で計算できます。ただし、この式は「1気圧で空気が乾燥している」という条件が付きます。また、tは摂氏温度、音速の単位はm/sです。

$$ 音速 = 331.5 + 0.61t$$

距離の計算方法

これらの式を元に、たとえば気温を20度と仮定して、超音波を発信してから受信するまでの往復時間をT秒としてみましょう。距離の計算は次のとおりになります。ただし、距離の単位はメートルです。

$$ 距離 = 331.5 + 0.61 \times 20 \times \frac{T}{2} $$

ところで、音は高域(周波数が高い)になるほど直進する性質を持ってます。これを「指向性」と呼んだりします。この「指向性」をもった音のほうが、狙った方向に向かって進んでくれるので、距離を測定したいときなどに便利なんですね。

ワタスは無指向性
ワタスは無指向性

ArduinoでHC-SR04を使う

ここからはArduinoと超音波センサ(HC-SR04)を使った距離測定のやり方を解説します。HC-SR04は、超音波の送信機と受信機が一体になったセンサモジュールです。信号の送受信はHC-SR04が自動でやってくれます。ですから、超音波センサの扱いはとても簡単なのです。Arduino初心者の方でも恐れずに超音波センサで遊んでみてください。

つかうもの

まずは、この記事で「つかうもの」をご紹介します。

超音波センサ(HC-SR04)

HC-SR04という超音波センサモジュールを使います。2つの超音波ユニットが付いていて、目のような形をしているのが特徴です。 片方で超音波を発振し、もう片方で跳ね返った超音波を受信します。

Arduino

この記事では「Seeeduino XIAO」を使いましたが、みなさんはお好きなArduinoを使ってください。

▼「Seeeduino XIAO」の使い方はこちらを御覧ください。

▼ どのArduinoを選んだら良いかわからない方はこちら。

アルディーノっていっぱいあるのねぇ..
アルディーノっていっぱいあるのねぇ..

ここまで理解できましたでしょうか?つぎに、超音波センサHC-SR04の使い方を解説します。

▼ HC-SR04の使い方は、次の3つのポイントを理解するだけです。

  1. Triggerピンを10μ秒だけHighにすると、超音波が発信される
  2. ❶の発振が終わったタイミングでEchoピンがHighになる
  3. 跳ね返ってきたパルスを受信するとEchoピンがLowになる

この手順を図で表現すると次のようになります。

HC-SR04のTriggerとEchoのフロー図
HC-SR04のTriggerとEchoのフロー図

距離を測るには、EchoピンがHighからLowに変わるまでの時間だけを知れればよいのです。具体的には、❷から❸の往復時間をArduinoなどで監視することになります。

ちなみに、超音波パルスの周波数は40kHzになります。HC-SR04の詳細はこちらをご覧ください。 HC-SR04データシート - 秋月電子通商

HC-SR04とArduinoの配線

ここからは実際に、HC-SR04とArduinoを使っていきます。まずは配線です。HC-SR04とArduinoの端子をそれぞれ表のように接続してください。

HC-SR04Arduino
Vcc5V
TrigD1
EchoD2
GNDGND

ただし、お使いのArduinoによっては、次のように端子の電圧に気をつけなければなりません。

Seeeduino XIAOで使う場合の注意点

Seeeduino XIAOのGPIOピンと、HC-SR04のEchoピンは直接接続してはダメです。なぜなら、HC-SR04のEchoピンからは5Vの信号が出力されるからです。

Seeeduino XIAOの場合、入力は3.3Vまでです。ですので5V信号を3.3V電圧に変換する必要があります。

簡単な方法として、抵抗を使った分圧法があります。次の図のように、抵抗を2本つかって5V電圧を3.3Vに変換できます。

分圧抵抗で5V電圧を3.3Vに変換する回路図
分圧抵抗で5V電圧を3.3Vに変換する回路図

または、ロジックレベル変換モジュールを使うと、5Vを3.3V信号へ安全に変換できます。

HC-SR04のTriggerピンは、入力になります。3.3V〜5Vの電圧を入力できますので、レベルシフトの必要はありません。Seeeduino XIAOのGPIOをそのまま接続できます。

ここら辺は、デジタルピンに使われている「MOSFETの仕組み」を理解すると良いです。余裕のある方は「MOSFETの使い方」をご覧ください。

距離を測定するソースコード

配線ができましたら、Arduinoをプログラミングしてみましょう。

Arduino IDEでSketchファイルを新規作成し、つぎのプログラムを書き込んでみましょう。Arduino IDEのシリアルモニターを開いてみてください。距離(cm)が表示されているはずです。超音波センサを動かして、障害物などに向けると距離の値が変化します。

cpp
/*
  Created by Toshihiko Arai.
  https://101010.fun/arduino-hc-sr04.html
*/

#define TrigPin 1 // D1
#define EchoPin 2 // D2
 
double speedSound = 331.5 + 0.61 * 20; // 20は現在の気温
double distance = 0;

void setup() {
  Serial.begin(9600);
  pinMode(TrigPin, OUTPUT);
  pinMode(EchoPin, INPUT);
}

void loop() {
  trigger();

  double t = pulseIn(EchoPin, HIGH); // μS

  if (t > 0) {
    t = t / 2; //往復距離なので半分の時間
    distance = t * speedSound * 100 / 1000000; // 距離(cm)を計算
    Serial.println(distance);
  }
  delay(500);
}

void trigger() {
  digitalWrite(TrigPin, LOW); 
  delayMicroseconds(2);
  digitalWrite(TrigPin, HIGH );
  delayMicroseconds( 10 );
  digitalWrite(TrigPin, LOW );
}

ところで、プログラム中のpulseInは「デジタルピンがHigh状態になっている時間をマイクロ秒で返す」とても便利な関数です。Arduino言語では標準で使える関数になります。

【発展】ArduinoとOLEDで距離を表示させてみた

少し発展としてシリアルモニターではなく「OLEDディスプレイ」へ距離を表示してみました。

\液晶ディスプレイの使い方/

距離を表示するソースコード

さきほどのプログラムを元に、OLEDで文字表示できるように改良してみました。

cpp
/*
  Created by Toshihiko Arai.
  https://101010.fun/arduino-hc-sr04.html
*/

#include <U8g2lib.h>

U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);

#define TrigPin 1 // D1
#define EchoPin 2 // D2
 
double speedSound = 331.5 + 0.61 * 20; // 20は現在の気温

void setup() {
  Serial.begin(9600);
  pinMode(TrigPin, OUTPUT);
  pinMode(EchoPin, INPUT);

  u8g2.begin();
}

void loop() {
  trigger();

  double distance = 0;
  double t = pulseIn(EchoPin, HIGH); // μS

  if (t > 0) {
    t = t / 2; //往復距離なので半分の時間
    distance = t * speedSound * 100 / 1000000; // 距離(cm)を計算
    displayOLCD(distance);
  }
  delay(500);
}

void trigger() {
  digitalWrite(TrigPin, LOW); 
  delayMicroseconds(2);
  digitalWrite(TrigPin, HIGH );
  delayMicroseconds( 10 );
  digitalWrite(TrigPin, LOW );
}

void displayOLCD(double distance) {
    char buf[10];
    snprintf(buf, 10, "%.1fcm", distance);

    u8g2.clearBuffer();
    u8g2.setFont(u8g2_font_crox3hb_tf);
    u8g2.drawStr(0, 16, "Distance");
    u8g2.drawStr(0, 32, buf);
    u8g2.sendBuffer();
}

プログラム中のu8g2.drawStrは、char型で文字を渡さなければなりません。そのため、snprintf関数でdouble値をchar型へ変換させてます。

対象物との距離を5cm・10cm・20cm・30cmと変えて測定してみました。写真のように、かなり正確な距離を測定できて驚きです。

超音波センサで距離の測定
超音波センサで距離の測定

Raspberry PiでHC-SR04を使う

HC-SR04とRaspberry Piの配線

Raspberry PiとHC-SR04の配線図
Raspberry PiとHC-SR04の配線図

HC-SR04ラズパイ
Vcc5V
TrigGPIO20
Echo電圧レベル変換後GPIO21⭐︎
GNDGND

Echoピンの電圧レベル変換⭐︎

HC-SR04のEchoからは5Vの信号が出力される。しかし、Raspberry PiのGPIOの入力電圧は3.3Vまで。そのため直接繋ぐことはできない。今回は、3.3Vのツェナーダイオードを使って5Vを3.3Vへ変換させることにした。

ツェナーダイオードで5V電圧を3.3Vへ変換
ツェナーダイオードで5V電圧を3.3Vへ変換

他にも抵抗で分圧したりレベルシフターを使うといった手段があります。

距離を測定するソースコード

Raspberry PiでHC-SR04を使って距離を測定するプログラムがこちら。

py
# -*- coding: utf-8 -*-
# Created by Toshihiko Arai.
# https://101010.fun/arduino-hc-sr04.html
# python2で実行すること

import RPi.GPIO as GPIO
import time
import os
import signal

GPIO.setmode(GPIO.BCM)
TRIG = 20
ECHO = 21
C = 343  # 気温20度の時の音速(m/s)

GPIO.setup(TRIG, GPIO.OUT)
GPIO.setup(ECHO, GPIO.IN)
GPIO.output(TRIG, 0)
time.sleep(0.3)

def readDistance():
    GPIO.output(TRIG, 1)
    time.sleep(0.00001)  # 10μs
    GPIO.output(TRIG, 0)

    while GPIO.input(ECHO) == 0:
        signaloff = time.time()
    while GPIO.input(ECHO) == 1:
        signalon = time.time()

    t = signalon - signaloff
    distance = t * C * 100 / 2
    return distance

def cleanup():
    print('cleanup')
    GPIO.cleanup()

try:
    while True:
        dist = readDistance()
        print(dist)
        time.sleep(0.1)

except KeyboardInterrupt:
    # SIGINTを監視していれば不要
    print('KeyboardInterrupt')
except:
    print('other')
finally:
    # 終了処理
    cleanup()

【発展】Raspberry Piと超音波センサで物体の追跡

ここからはRaspberry Piを使って超音波センサとサーボモータを組み合わせた物体の追跡方法を解説していく。超音波センサはHC-SR04、サーボモータはSM-S2309Sを使用した。

「超音波センサ1つで物体に追跡させる」ことを考えてみた。

超音波センサモジュール1つで、物体を追跡させるには少し工夫しなければならない。なぜなら、人間にたとえると「片耳だけ」で方向を探知するようなものだからだ。

超音波センサモジュールを見ると、2つの目があるように見えるが実は「1つ目」である。というか「口と耳」なのだ。1つは超音波を発する「口」、そしてもう1つは自分が発した超音波を受信する「耳」である。

サーボモータの動かし方

サーボモータと超音波センサを合体させたロボット
サーボモータと超音波センサを合体させたロボット

物体の動きに合わせて首を振るようにしたいので、ここではサーボモータの使い方を説明する。何かのキットに付いていたサーボモータSM-S2309Sを使ったが、他のサーボモータでも問題ないと思う。

サーボーモータの動かし方のポイントは次の2つだ。

  • PWM信号(50Hz程度)を、SIGピンへ送信する。
  • PWM信号のデューティー比によって、角度が決められる。
PWM信号について詳しくは、 オペアンプ1個でつくる!PWMジェネレータ をご覧ください。

サーボモータとRaspberry Piの配線

サーボモータとRaspberry Piの配線図
サーボモータとRaspberry Piの配線図

ラズパイサーボモータ
GNDGND
GPIO26Signal
ラズパイ以外の5V電源5V

サーボモータの電源はRaspberry Piと別にしよう

サーボモータの電源はラズパイから拾うのではなく、別電源を用意する。なぜならモータの消費電力は大きいので、動作が不安定になるからだ。

サーボモータについて詳しく解説したのでこちらも参考に

サーボモータの動作テストプログラム

サーボモータの動作テスト
サーボモータの動作テスト

これを元に、サーボモータの動作テストプログラムをPythonで書いてみた。

py
# -*- coding: utf-8 -*-
# Created by Toshihiko Arai.
# https://101010.fun/iot/raspberry-pi-sonic-radar.html
# python2で実行すること

import RPi.GPIO as GPIO
import time
import os
import signal

GPIO.setmode(GPIO.BCM)
SIG = 26
GPIO.setup(SIG, GPIO.OUT)

# PWMサイクル:20ms(=50Hz)
servo = GPIO.PWM(SIG, 50)
time.sleep(0.3)

servo.start(0)
servo.ChangeDutyCycle(6.3)  # 0°
time.sleep(1.0)

for i in range(5):
    servo.ChangeDutyCycle(2.2)  # 0°
    time.sleep(1.0)

    servo.ChangeDutyCycle(10.8)  # 180°
    time.sleep(1.0)

servo.ChangeDutyCycle(6.3)  # 90°
time.sleep(1.0)

servo.stop()
GPIO.cleanup()

超音波センサ1つで物体を追跡する仕組み

それでは本題の「超音波センサ1つで物体を追跡」をやってみよう。

1つの超音波センサモジュールだけで、物体の追跡をさせる方法は次の通り。

  1. 物体を見つけたら常にその右端または左端を狙うようにする
  2. ごくわずかの角度だけ常に首を振って「見つけた」「見つけていない」を高速で繰り返す

超音波で常に物体の端を狙う
超音波で常に物体の端を狙う
超音波1つでは物体を追いかけるのは難しい
超音波1つでは物体を追いかけるのは難しい

もし物体が移動して見失ってしまったら、すぐに見つけられるよう首を振るスピードを速める。つまりサーボモータの角速度を大きくする。こうすることにより、物体の端を捉えることができるはずだ。

ソースコード

これらの考えを元に、組んだPythonプログラムがこちらである。条件分岐が多く読みにくいプログラムとなってしまったが悪しからず。

py
# -*- coding: utf-8 -*-
# Created by Toshihiko Arai.
# https://101010.fun/arduino-hc-sr04.html
# このプログラムはpython2で実行する

import RPi.GPIO as GPIO
import time
import os
import signal

GPIO.setmode(GPIO.BCM)  # 役割ピン番号で命名
TRIG = 20
ECHO = 21
SIG = 26
SERVO_PWM_MIN = 2.2  # 0° 時計の針で9時を0°とする
SERVO_PWM_MAX = 10.8  # 180°
SERVO_PWM_1DEGREE = (SERVO_PWM_MAX - SERVO_PWM_MIN) / 180
MONITORING_DIST = 20  # 最短距離

C = 343  # 気温20度での音速(m/s)

GPIO.setup(TRIG, GPIO.OUT)
GPIO.setup(ECHO, GPIO.IN)
GPIO.setup(SIG, GPIO.OUT)

GPIO.output(TRIG, 0)
servo = GPIO.PWM(SIG, 50)
time.sleep(0.3)

servo.start(0)

def readDistance():
    GPIO.output(TRIG, 1)
    time.sleep(0.00001)  # 10μs
    GPIO.output(TRIG, 0)

    while GPIO.input(ECHO) == 0:
        signaloff = time.time()  # 秒
    while GPIO.input(ECHO) == 1:
        signalon = time.time()  # 秒

    t = signalon - signaloff  # 秒
    distance = t * C * 100 / 2
    return distance

def rotate(angle):  # 0°から180°
    pwm = angle * SERVO_PWM_1DEGREE + SERVO_PWM_MIN
    if pwm > SERVO_PWM_MAX:
        pwm = SERVO_PWM_MAX
    elif pwm < SERVO_PWM_MIN:
        pwm = SERVO_PWM_MIN

    servo.ChangeDutyCycle(pwm)
    return

def cleanup():
    print('cleanup')
    rotate(90)
    time.sleep(1)
    servo.stop()
    GPIO.cleanup()

initAngle = 0
currentAngle = 0
clockwise = True
isCatched = False
isMissing = False  # 物体を見失う
isWondering = False  # 不安フラグ
missingCount = 0

try:
    rotate(initAngle)
    time.sleep(1)
    while True:

        dist = readDistance()
        # print('角度: {0}, 距離: {1}'.format(currentAngle, dist))

        isExistObject = dist < MONITORING_DIST

        if isExistObject:
            isMissing = False
            missingCount = 0
            isWondering = False
            if isCatched == False:  # 新規発見!
                isCatched = True
                clockwise = False if clockwise == True else True
        else:
            if isCatched:
                isCatched = False
                clockwise = False if clockwise == True else True
                # print("物体を見逃しますた!")
                isMissing = True

            if isMissing:
                missingCount += 1

        if missingCount > 100:  # 物体が存在しなかったのでリセットする
            isCatched = False
            isWondering = False
            isMissing = False
        elif missingCount > 10:  # 不安フラグを立てる
            isWondering = True

        if isCatched:
            gain = 0.7
        elif isWondering:
            gain = 4
        else:
            gain = 1

        if clockwise:
            gain *= 1
        else:
            gain *= -1

        currentAngle += 1 * gain

        if currentAngle > 180:
            clockwise = False
        elif currentAngle < 0:
            clockwise = True

        rotate(currentAngle)
        time.sleep(0.005)

except KeyboardInterrupt:
    print('KeyboardInterrupt')
except:
    print('other')
finally:
    cleanup()

改善点としては、動きが速すぎると物体を見失ってしまうので、首振りの部分を工夫する必要がある。

関連記事

 
最後までご覧いただきありがとうございます!

▼ 記事に関するご質問やお仕事のご相談は以下よりお願いいたします。
お問い合わせフォーム

関連記事