304 views
【1】やりたいこと
超音波距離センサー HC-SR04 を使い、身近な物との間の距離を測定してみたい。
https://akizukidenshi.com/catalog/g/g111009/
もし、誤差量が小さければ、色々と使ってみたい場面がある。
【2】情報収集
(1) データシート
上記の秋月電子さんの販売ページの中に、データシートが置いてある。
https://akizukidenshi.com/goodsaffix/hc-sr04_v20.pdf
データシートを見ると「I2Cで使える」と書かれているが、マイコン側から I2Cバス上をスキャンしても検出されなかった…
接続端子は間違っていないはずだが… (後日再確認)
■ピン配置
【3】回路の結線
Pin# | GPIO# | Signal |
1 | - | VCC 3.3V |
6 | - | GND |
8 | 14 | Echo |
10 | 15 | Trig |
なお、HC-SR04を 3.3Vで駆動するため、Echo線から出力される信号も 3.3Vだ。
よって、Echo線から出力される電圧を分圧するための抵抗は不要だ。
【4】プログラミング
Step 1: ミニマム実装
# Step 1 : ミニマム実装 # gpiozeroライブラリからDistanceSensorクラスを読み込む from gpiozero import DistanceSensor # プログラムの待機に使用するsleep関数をtimeライブラリから読み込む from time import sleep # プログラム終了用の関数を使うためにsysライブラリを読み込む import sys # トリガーピンとエコーピンの設定 trig_pin = 15 # GPIO 15番ピンにトリガーピンを設定 echo_pin = 14 # GPIO 14番ピンにエコーピンを設定 # DistanceSensor クラスのインスタンスを作成し、センサーを初期化する # echoピンとtriggerピンに指定したGPIOピンを接続し、最大距離を2メートルに設定 sensor = DistanceSensor(echo=echo_pin, trigger=trig_pin, max_distance=2.0) # max_distanceはメートル単位 # プログラムのメイン処理部分 try: while True: # 無限ループを開始し、常に距離を測定する sleep(0.2) # 測定結果の取得前に200ミリ秒(0.2秒)待機(センサーの仕様) distance = sensor.distance * 100 # センサーの測定値(メートル)をセンチメートルに変換 print("Distance: {:.1f}cm".format(distance)) # 小数点1桁で距離を表示する # キーボードでの中断(Ctrl+C)に対応する処理 except KeyboardInterrupt: print("プログラムを終了します。") # 中断メッセージを表示 sys.exit() # プログラムを終了する
max_distanceを指定する意味は?
最大値を指定すると分解能が向上する?
↓ No ↓
max_distance=2.0 は、センサーが計測する最大距離をメートル単位で指定しています。
このパラメータは解像度や精度の向上には直接関係しませんが、いくつかの役割があります。
■計測範囲の制限
センサーが応答する距離の範囲を制限します。
例えば、max_distance=2.0 に設定すると、2メートル以上の距離では測定結果が返されなくなります。
■応答速度の向上
センサーは超音波が反射して戻ってくるまでの時間を計測して距離を算出します。
設定した max_distance が短いほど、計測のタイムアウトも短くなるため、応答速度がわずかに速くなります。
■不要なエコーのフィルタリング
遠距離からの不要な反射(エコー)を除外できます。
これにより、指定距離以内の対象物に対する計測の安定性が向上する可能性がありますが、解像度や精度自体には影響を与えません。
しかし…
今回使用するセンサーのI/F仕様を確認すると、最大距離の指定はできない。
つまり…
gpiozeroライブラリのI/F仕様には存在するが、センサー制御には全く使われないパラメーターだ。
Step 2 : もう少し低水準な書き方に変更
DistanceSensor()がブラックボックスのためにデバイス制御の中身が見えない。
そこで、これを使わずに直接 echo端子の入力値を参照して測距するプログラムに書き換える。
# Step 2 : ミニマム実装(少し低水準な書き方に変更) from gpiozero import DigitalInputDevice, DigitalOutputDevice from time import time, sleep # TRIGとECHOのピンを設定 trig_pin = DigitalOutputDevice(15) # GPIO#15 echo_pin = DigitalInputDevice(14) # GPIO#14 # 超音波センサーで距離を測定する関数 def measure_distance(): # トリガーピンに10マイクロ秒のパルスを送信 trig_pin.on() sleep(0.00001) # 10μs待つ trig_pin.off() # エコー信号の立ち上がりを待つ while not echo_pin.is_active: pass start_time = time() # エコー信号の立ち下がりを待つ while echo_pin.is_active: pass end_time = time() # 時間差(秒)を計算 duration = end_time - start_time # 距離を計算(音速: 343 m/s) distance_cm = (duration * 34300) / 2 # 往復なので2で割る return distance_cm try: while True: # 距離を測定 distance = measure_distance() print(f"Distance: {distance:.2f} cm") sleep(1) except KeyboardInterrupt: print("Measurement stopped by user.")
Step 1 の実装よりも測定結果のばらつきが大きい…
同じ gpiozeroを使う方法でも、DistanceSensor() を使った方が測定結果が安定するようだ。
pigpio が使えればよいのだが、ラズパイ5では pigpioが使えないのだ…
参考: pigpio will not run on a Pi 5
Step 3 : 測定結果の収束判定を組み込む
本測定器を手持ちで測定を実施する場合、手の揺れなどにより測定値に微小な変動が生じる。
このため、当該環境下において、連続10回の測定で安定した結果が得られた場合に限り、その値を採用するものとする。
↓↓↓
Step 1 のプログラムに 測定結果の収束を判定する処理 を追加実装する。
# Step 3 : 乱れる測定結果を標準偏差を用いて収束判定する。 import gpiozero import time import sys import numpy as np # トリガーピンとエコーピンの設定 trig_pin = 15 # GPIO 15番ピンにトリガーピンを設定 echo_pin = 14 # GPIO 14番ピンにエコーピンを設定 # DistanceSensor クラスのインスタンスを作成し、センサーを初期化する # echoピンとtriggerピンに指定したGPIOピンを接続し、最大距離を2メートルに設定 sensor = gpiozero.DistanceSensor(echo=echo_pin, trigger=trig_pin, max_distance=2.0) # max_distanceはメートル単位 # 過去10回の距離測定値を格納するリストを初期化 distance_history = [] # メイン処理をtryブロックで囲み、キーボード割り込みを検出 try: while True: # 無限ループを開始し、常に距離を測定する time.sleep(0.2) # 次の測定まで200ミリ秒待機(センサーの仕様) distance = sensor.distance * 100 # センサーの測定値(メートル)をセンチメートルに変換 print("Distance: {:.1f}cm".format(distance)) # 小数点1桁で距離を表示 # 測定結果をdistance_historyリストに追加 distance_history.append(distance) if len(distance_history) > 10: # リストが10個のデータを超えた場合、最も古いデータを削除 distance_history.pop(0) # リストに10個のデータが揃ったら収束判定を実施 if len(distance_history) == 10: std_dev = np.std(distance_history) # 過去10回の測定結果の標準偏差を計算 print("std_dev = {:.3f}".format(std_dev)) # 標準偏差を小数点3桁で表示 if std_dev < 0.5: # 標準偏差が0.5未満の場合は測定値が安定したと判断 avg_distance = np.mean(distance_history) # 過去10回の測定値の平均を計算 print("Stable Distance Measurement: {:.1f}cm".format(avg_distance)) # 安定した平均値を表示 break # ループを終了 # キーボードでの中断(Ctrl+C)に対応する処理 except KeyboardInterrupt: print("プログラムを終了します。") # 中断メッセージを表示 sys.exit() # プログラムを終了する
収束判定に平均ではなく標準偏差を使う意義は?
標準偏差を使って収束判定を行う意義は、測定値の「ばらつきの大きさ」を確認できる点にあります。具体的には次のような利点があります:
■安定性の評価
測定値の平均値は、値が収束していない状態でも一定の範囲に収まってしまう可能性があります。しかし、測定値が安定していない場合は、ばらつきが大きく、標準偏差も大きくなります。標準偏差を用いることで、測定値が狭い範囲内に収束しているか(安定しているか)を確認できます。
■外れ値の影響を排除
測定値がばらつく場合や突発的な外れ値が発生した場合でも、標準偏差をチェックすることで安定かどうかを判断しやすくなります。外れ値が平均に与える影響を抑えつつ、測定値の変動が小さいかどうかを判定できます。
■安定状態の検出
標準偏差が小さいということは、連続した測定値が非常に似通った値であり、対象物との距離が安定していることを意味します。このため、標準偏差が閾値(例えば0.5cm)以下であれば「測定が安定した」と判断できます。
このように、標準偏差を使うことで、測定データの安定性を数値として評価でき、確実に収束した状態を確認することができます。
Step 4 : 実測値を基に校正値を決定する。
測定システムのキャリブレーション(Calibration)を以下の手順で進める。
1) 距離ごとに測定を実施し、理想値(I)と実測値(R)の差(誤差 E)を明らかにする。
2) 測定値をプログラム内で補正し、理想値に近づける。
■測定結果(Step 3 のプログラムを使用)
理想値(I) | 実測値(R) | 誤差(E) |
10cm | 9.6cm | -0.4cm |
20cm | 18.7cm | -1.3cm |
30cm | 28.2cm | -1.8cm |
40cm | 38.5cm | -1.5cm |
50cm | 48.0cm | -2.0cm |
■補正式の導出
この関係を正確にモデル化するには回帰直線を導出する必要があるが、ここでは近似的な補正式を導出する。
誤差 E を分析すると、以下の関係式で近似できることがわかる。
E = I x (-2/50) ←①
RとEの関係は次の式で表される。
R = I + E
I = R – E ←②
式①に②を代入すると、
E = (R – E) x (-1/25)
-25E = R – E
-24E = R
E = -R/24 ←③
式②, ③より、
I = R – (-R/24)
I = 25R/24
この補正式により、測定値 Rから理想値 Iを推定できる。
プログラム内でこの補正を実装し、測定システムの精度を向上させる。
# Step 4 : 測定結果を校正する。 import gpiozero import time import sys import numpy as np # トリガーピンとエコーピンの設定 trig_pin = 15 # GPIO 15番ピンにトリガーピンを設定 echo_pin = 14 # GPIO 14番ピンにエコーピンを設定 # DistanceSensor クラスのインスタンスを作成し、センサーを初期化する # echoピンとtriggerピンに指定したGPIOピンを接続し、最大距離を2メートルに設定 sensor = gpiozero.DistanceSensor(echo=echo_pin, trigger=trig_pin, max_distance=2.0) # max_distanceはメートル単位 # 過去10回の距離測定値を格納するリストを初期化 distance_history = [] # メイン処理をtryブロックで囲み、キーボード割り込みを検出 try: while True: # 無限ループを開始し、常に距離を測定する distance = sensor.distance * 100 # センサーの測定値(メートル)をセンチメートルに変換 print("Distance: {:.1f}cm".format(distance)) # 小数点1桁で距離を表示 # 測定結果をdistance_historyリストに追加 distance_history.append(distance) if len(distance_history) > 10: # リストが10個のデータを超えた場合、最も古いデータを削除 distance_history.pop(0) # リストに10個のデータが揃ったら収束判定を実施 if len(distance_history) == 10: std_dev = np.std(distance_history) # 過去10回の測定結果の標準偏差を計算 print("std_dev = {:.3f}".format(std_dev)) # 標準偏差を小数点3桁で表示 if std_dev < 0.5: # 標準偏差が0.5未満の場合は測定値が安定したと判断 avg_distance = np.mean(distance_history) # 過去10回の測定値の平均を計算 corr_distance = avg_distance * 25 / 24; print("Stable Distance Measurement: {:.1f}cm".format(corr_distance)) # 安定した平均値を表示 break # ループを終了 time.sleep(0.2) # 次の測定まで200ミリ秒待機(センサーの仕様) # キーボードでの中断(Ctrl+C)に対応する処理 except KeyboardInterrupt: print("プログラムを終了します。") # 中断メッセージを表示 sys.exit() # プログラムを終了する