超音波センサHC-SR04の使い方|Arduinoと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つのポイントを理解するだけです。
- Triggerピンを10μ秒だけHighにすると、超音波が発信される
- ❶の発振が終わったタイミングでEchoピンがHighになる
- 跳ね返ってきたパルスを受信するとEchoピンがLowになる
この手順を図で表現すると次のようになります。
距離を測るには、EchoピンがHighからLowに変わるまでの時間だけを知れればよいのです。具体的には、❷から❸の往復時間をArduinoなどで監視することになります。
ちなみに、超音波パルスの周波数は40kHzになります。HC-SR04の詳細はこちらをご覧ください。 HC-SR04データシート - 秋月電子通商
HC-SR04とArduinoの配線
ここからは実際に、HC-SR04とArduinoを使っていきます。まずは配線です。HC-SR04とArduinoの端子をそれぞれ表のように接続してください。
HC-SR04 | Arduino |
---|---|
Vcc | 5V |
Trig | D1 |
Echo | D2 |
GND | GND |
ただし、お使いの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信号へ安全に変換できます。
HC-SR04のTriggerピンは、入力になります。3.3V〜5Vの電圧を入力できますので、レベルシフトの必要はありません。Seeeduino XIAOのGPIOをそのまま接続できます。
ここら辺は、デジタルピンに使われている「MOSFETの仕組み」を理解すると良いです。余裕のある方は「MOSFETの使い方」をご覧ください。
距離を測定するソースコード
配線ができましたら、Arduinoをプログラミングしてみましょう。
Arduino IDEでSketchファイルを新規作成し、つぎのプログラムを書き込んでみましょう。Arduino IDEのシリアルモニターを開いてみてください。距離(cm)が表示されているはずです。超音波センサを動かして、障害物などに向けると距離の値が変化します。
/*
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で文字表示できるように改良してみました。
/*
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の配線
HC-SR04 | ラズパイ |
---|---|
Vcc | 5V |
Trig | GPIO20 |
Echo | 電圧レベル変換後GPIO21⭐︎ |
GND | GND |
Echoピンの電圧レベル変換⭐︎
HC-SR04のEchoからは5Vの信号が出力される。しかし、Raspberry PiのGPIOの入力電圧は3.3Vまで。そのため直接繋ぐことはできない。今回は、3.3Vのツェナーダイオードを使って5Vを3.3Vへ変換させることにした。
他にも抵抗で分圧したりレベルシフターを使うといった手段があります。
距離を測定するソースコード
Raspberry PiでHC-SR04を使って距離を測定するプログラムがこちら。
# -*- 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信号のデューティー比によって、角度が決められる。
サーボモータとRaspberry Piの配線
ラズパイ | サーボモータ |
---|---|
GND | GND |
GPIO26 | Signal |
ラズパイ以外の5V電源 | 5V |
サーボモータの電源はRaspberry Piと別にしよう
サーボモータの電源はラズパイから拾うのではなく、別電源を用意する。なぜならモータの消費電力は大きいので、動作が不安定になるからだ。
サーボモータについて詳しく解説したのでこちらも参考に
サーボモータの動作テストプログラム
これを元に、サーボモータの動作テストプログラムをPythonで書いてみた。
# -*- 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つの超音波センサモジュールだけで、物体の追跡をさせる方法は次の通り。
- 物体を見つけたら常にその右端または左端を狙うようにする
- ごくわずかの角度だけ常に首を振って「見つけた」「見つけていない」を高速で繰り返す
もし物体が移動して見失ってしまったら、すぐに見つけられるよう首を振るスピードを速める。つまりサーボモータの角速度を大きくする。こうすることにより、物体の端を捉えることができるはずだ。
ソースコード
これらの考えを元に、組んだPythonプログラムがこちらである。条件分岐が多く読みにくいプログラムとなってしまったが悪しからず。
# -*- 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()
改善点としては、動きが速すぎると物体を見失ってしまうので、首振りの部分を工夫する必要がある。