Raspberry Piに距離センサーHC-SR04を付けて測定距離をLCDに表示してみる(Python)

 以前Raspberry Piに接続したLCDディスプレイに文字列を表示する記事を書きまして、機能をまとめたシンプルなクラスを作りました:

 こうなるとLCDに何か実用的な物を表示をしたくなります。そこで工具箱を散策して出てきたのが下の「距離センサー」:

「そうだ、ラズパイに距離センサーを付けて、測定した距離をLCDに表示してみよう」。今回の目標はこれです。

準備する物

距離センサー

 今回扱うのはHC-SR04という距離センサーです。元々はArduino用に購入した物でした。データシートは以下のPDFを参照下さい:

https://cdn.sparkfun.com/datasheets/Sensors/Proximity/HCSR04.pdf

互換品含め各所で割と格安で購入できます:

 この距離センサーはスピーカーから超音波を発信し、それが物に跳ね返って戻ってくると出力ピンに電気信号を発してくれます。音波が飛ぶ速度は環境が一定なら同じなので、発してからピンに電気信号が流れる時間差がわかれば距離を計算する事が出来ます。

接続端子は4つとシンプルです。Vccはプラス側の電源(5V)、GNDはマイナス側です。Trigには超音波を発信するシグナルをラズパイ側から送ります。すると跳ね返った超音波を受信機が受信しEchoに電流が流れます。

 ちなみにこの端子、間隔が2.54mmなのでブレッドボードにそのまんま刺せます(^-^)

抵抗

 1kΩの抵抗を2つ使います。 

 HC-SR04は5V駆動です。電源はラズパイの5V powerから供給出来ます。Trigに与える入力はラズパイのGPIOピンの規定電圧である3.3Vでも大丈夫なようなのですが、問題はEchoから返ってくる電圧。これは5Vになります。これをラズパイのGPIOピンに直接入力すると過電圧でラズパイが壊れる可能性があります。その為Echoから返ってくる電圧を下げる必要があり、それを抵抗で調整します。

 電圧を下げる方法は色々とあるようですが「抵抗分圧回路」が良く使われているようです:

抵抗分圧回路

 距離センサーのEchoピンの先にまず1kΩの抵抗を一つ付けます。その後で回路を2経路に分けて、GND側に同じく1kΩの抵抗を取り付けます。こうするとGND側とラズパイのGPIO側で電圧が2.5Vずつ分かれるため、GPIOに掛ける電圧が定格以内に収まります。

 抵抗値は1kΩで無くても同じ抵抗値なら良いのですが、抵抗が低すぎるとその分GPIOに流れる電流が大きくなってしまいます。GPIOは最大16mAしか流せないので注意です。

 後はLCDの回で用意したLCDディスプレイ等一式です。もちろん距離測定自体はLCDが無くても出来ます。

回路作成

 まずはHC-SR04とラズパイとを接続する回路をブレッドボード上に作成します。模式図は以下の通りです:

 HC-SR04はブレッドボードにぶすっと挿します。便利(^-^)
 電源はラズパイの5Vピン(4番)から得ます。GNDは9番へ。HC-SR04に距離測定を指示するTrigピンは13番に繋げます。

 発信した超音波を受け取ったタイミングで電流を出力するEchoピンは5Vなので、抵抗分圧回路を組んでラズパイのピンに2.5Vをかけるようにします。最初の1kΩの抵抗の後ろからケーブル(青)を伸ばし11番のピンに接続します。

 今回はLCDディスプレイに計測した距離を逐次表示するのが目標なので、LCDの回路も同時に組みます。これは以前作った物を流用です:

距離センサーだけを使いたい場合はLCDは無くても良いので、上の回路はオプション扱いです。

 これで回路は完成。続いてPythonに移りましょう~。

Echoの時間を測定して距離を算出

 Python側ですが、距離センサー専用のライブラリはRaspberry Piは提供していないので、GPIOを直に叩く方法で実装していきます。

 全体コードはこちらです:

import RPi.GPIO as GPIO
import lcdmng
import time

#GPIO番号
trig = 13    # Trigger(Out)
echo = 11    # Echo (In)

#GPIOを初期化
GPIO.setmode( GPIO.BOARD )   #GPIOピン番号を直接指定
GPIO.setup( trig, GPIO.OUT )
GPIO.setup( echo, GPIO.IN  )

 #LCD lcd = lcdmng.LCD( 1, 0x27 )

#距離を1パルス分測定
def mesureDist():
    #Triggerにトリガーとなるパルス信号を10us発信
    GPIO.output( trig, GPIO.HIGH )
    time.sleep( 0.00001 )
    GPIO.output( trig, GPIO.LOW )

    # EchoがHighになっている時間を計測
    # EchoがずっとLowになる場合は超音波の
    # 捕捉ミスなどのためエラー
    firstTime = time.time()
    echoStart = firstTime
    while GPIO.input( echo ) == GPIO.LOW:
        echoStart = time.time()
        if echoStart - firstTime > 1.0:
            return -1   #計測エラー

    # Highになりっぱなしになる場合はエラー        
    firstTime = time.time()
    echoEnd = firstTime
    while GPIO.input( echo ) == GPIO.HIGH:
        echoEnd = time.time()
        if echoEnd - firstTime > 1.0:
            return -1   #計測エラー

    # Highの時間を算出
    intervalSec = echoEnd - echoStart

    # 計測間隔として60msが必要
    time.sleep( 0.06 )

    # 対象までの距離(cm)を音速から計算
    #  音速:340.0 (m/s)
    return intervalSec * 100.0 * 340.0 /  2.0


#距離を測定し続けてLCDに出力
while True:
    try:
        distCm = mesureDist()
        if distCm > 0.0:
            lcd.print( 0, 0, "DIST:" + "{:.3f}".format(distCm) + " cm     " )

    except KeyboardInterrupt:
        GPIO.cleanup()

RPi.GPIOライブラリ

 インポートしているRPi.GPIOはGPIOを直接叩くライブラリです。クラスではなくて関数群になっています。詳細は以下で色々説明します。

lcdmng(オプション)

 こちらは以前作ったLCDに文字を表示するクラスです:

距離を測定する事には本質的には関わらないので、無くても距離センサーは動かせます。より良い物があればそれを使って下さい(^-^;

GPIOの初期化

 GPIOピンは設定しないとそのピンが入力用なのか出力用なのかを判断できません。それを行うのがGPIO.setup関数です:

#GPIOを初期化
GPIO.setmode( GPIO.BOARD )   #GPIOピン番号を直接指定
GPIO.setup( trig, GPIO.OUT )
GPIO.setup( echo, GPIO.IN  )

 まずGPIO.setmode関数にGPIO.BOARDを設定します。これによりsetup関数で指定するIDがピン番号指定になります。もしsetup関数で指定するIDをGPIO番号にしたいならここをGPIO.BCMにします。

 今回使うのは出力用にピン13番(trig)、入力用にピン11番(echo)の2つなので、setup関数にそれを指定します。引数は見たまんまなのでわかりますよね(^-^;

mesureDist関数

 この関数を呼び出すと測定を1回行います。詳しい実装については後述します。

Triggerパルスを発信

 今回の距離センサーHC-SR04は、Trigger端子に10usのパルス的な電流が流れるとスピーカーから測定用の超音波を極短時間発信します。この音波は40kHzあるので人の耳には聞こえません。関数の最初でそのパルスを発信するよう命令しています:

    #Triggerにトリガーとなるパルス信号を10us発信
    GPIO.output( trig, GPIO.HIGH )
    time.sleep( 0.00001 )
    GPIO.output( trig, GPIO.LOW )

 ピンに電流を流す命令がGPIO.output関数です。第1引数にピンを指定し、第2引数にGPIO.HIGHを渡すと直ちに電流を流します。逆にGPIO.LOWを指定すると電流を止めます。今回は10usだけHIGHになって欲しいのでtime.sleep関数に0.00001sec(=10us)指定しています。ただし、sleep関数の待ち時間は一般にバラツキがあり、実際に10usきっちりスリープできる保証はありませんのでご注意を。

超音波が飛んでいる時間を計測

 超音波を発した後、対象物に音波が当たり跳ね返ってセンサーに戻って来ます。その時間を測定する必要があります:

    # EchoがHighになっている時間を計測
    # EchoがずっとLowになる場合は超音波の
    # 捕捉ミスなどのためエラー
    firstTime = time.time()
    echoStart = firstTime
    while GPIO.input( echo ) == GPIO.LOW:
        echoStart = time.time()
        if echoStart - firstTime > 1.0:
            return -1   #計測エラー

    # Highになりっぱなしになる場合はエラー        
    firstTime = time.time()
    echoEnd = firstTime
    while GPIO.input( echo ) == GPIO.HIGH:
        echoEnd = time.time()
        if echoEnd - firstTime > 1.0:
            return -1   #計測エラー

    # Highの時間を算出
    intervalSec = echoEnd - echoStart
 
    # 計測間隔として60msが必要
    time.sleep( 0.06 )

 やっている事はストップウォッチと同じです。echoピンがLOWからHIGHになる瞬間を開始時刻(echoStart)として記録します。この時稀にですがいつまで経ってもechoピンがHIGHになってくれない事があります。おそらく最初の10usのパルスをセンサーが認識しなかったか、反射してきた超音波をうまく受け取れなかったなど事情があるのでしょう。そのため上のコードでは1秒間echoピンがLOWのままだったらエラーとしてマイナス値を返しています。

 センサーがトリガー信号を受信してechoピンがHIGHになってくれた場合、センサーは超音波を受信するまでechoピンをHIGHにし続けます。その時間が音波が行って帰ってくるまでの往復時間になるので、HIGHがLOWになる瞬間まで時刻(echoEnd)を記録し続けます。これにより超音波が発信して帰って来るまでの往復時間はechoEnd-echoStartで求まります。

 測定後次の測定をするまでには最低60ms開けないといけないとデータシートにありましたので、最後に60msだけsleepしています。

測定距離の算出

    # 対象までの距離(cm)を音速から計算
    #  音速:340.0 (m/s)
    return intervalSec * 100.0 * 340.0 /  2.0

 超音波が往復した時間から測定距離を算出します。音の伝わる速度は一般には340m/sと言われていますのでそれを採用します。「そんなざっくりでいいの?」と思うかもしれませんが、これはですね、これで良いと言えば良いんです。理由は簡単で「測定距離をそのまま使っても意味が無い」からなんです。この辺りも後程説明します。

 測定した時間は往復時間なので、2で割る事でターゲットまでの時間にし、そこに音速をかけて距離に変換しています。また上の速度はmなのに対して関数はcm単位で返したいので100倍しています。

LCDへ出力

 実際の測定はwhile文でぐるぐる回して連続的に測定し、逐一その結果をLCDに出力しています:

#距離を測定し続けてLCDに出力
while True:
    try:
        distCm = mesureDist()
        if distCm > 0.0:
            lcd.print( 0, 0, "DIST:" + "{:.3f}".format(distCm) + " cm     " )

    except KeyboardInterrupt:
        GPIO.cleanup()

 以前作成したLCDのクラスはprintメソッドだけで文字表示が出来るので便利です(^-^)

 測定距離は当然ながら誤差があります。HC-SR04の精度は対象が数十cm程度だと数ミリ~1cm程度にバラつき、1m以上になると数センチ程度のバラつきが出るようです。そのためLCDには小数点以下1~2桁を表示すれば十分です。それ以下の桁は表示しても意味がありません。format関数を使っているのはそのためです。

実際に表示してみたけど…

 ここまでですべてOK。早速距離センサーとLCDをラズパイに接続して、上のコードを実際に動かし距離を測定してみました。下の動画の左下のLCDに表示される数値に注目です:

 センサーの筒状の先端を下敷きのマス目の左端(原点)に合わせてあります。対象の箱を3cm、5cm、10cmと離して距離を測定していますが…あれれ、LCDの測定距離が大分ズレている事がわかりますよね。

距離センサーは原点を規定しない

 これ当たり前の事なんです。だってこの原点となる0cmラインは僕が勝手に決めた位置ですから。「いやいや、だったら距離センサーの原点に合わせなさいよ」と思うかもしれませんが、実は距離センサーのデータシートに「ここが原点ですよ」という個所は定められていません。

距離センサーがくれるのは「時間」だけ

 距離センサーがくれるのは、あくまでもTriggerにパルス信号を発信した瞬間から超音波を受信するまでの「時間」です。そこには距離の概念も座標の概念も無いんです。原点がスピーカーの根元の振動板の所とは限らなくて、振動させてからEchoをHIGHにするまでの間もあるわけです。その間に音波は幾分か飛んでいるわけでして、そうなると原点は振動版の先になりますが…その位置を規定する事なんてできませんよね(^-^;

 コードの説明で音速を340m/sとざっくり指定した理由も実はここにあります。センサーがある程度の精度でもたらしてくれるのは原点が不確かな時間だけなので、その時間に何らかの速度をかけて距離にしてももはや意味が無いんです。340m/sという粗い速度を「一応」掛けているのは、単に単位を揃えたいが為の事なんです。

キャリブレーションが必要

 という事で、距離センサーの時間に直接速度をかけて距離を求める事に意味はありません。そんじゃどうするかと言うと、距離センサーが返す時間に対して「その時間だったら測定距離は○○cm」という信頼できる答えを沢山用意するんです。そうすればある測定時間に対する欲しい距離を推定できるようになります。いわゆる「キャリブレーション(調整)」です。

 という事で、次回は今回の構成をベースにキャリブレーションを組み入れてみます。

ではまた(^-^)/

この記事が気に入ったらサポートをしてみませんか?