見出し画像

製品レビュー|電子機器12:距離センサ(GP2Y0A21YK)


1.概要

 購入した製品の使い方および感想用記事です。
 今回は「シャープ測距モジュール GP2Y0A21YK(550円/個(税込))」をレビューしました。

1-1.基本仕様

 赤外線を使用した測距モジュールです。赤外線LEDとPSD(position sensitive detector)を使用して、非接触で距離を検出することができます。

 1-1-1.仕様の概要

 基本の概要は下記参照。

  • 電源電圧:DC4.5-5.5V

  • 測定距離:10~80cm

  • 動作湿度:-10~60℃

  • 出力:アナログ出力(AO)

    • 出力電圧:最大が約3.14VでありRaspberry Pi PicoのADCでも計測可能

 構造として、光を放出する「Light Emitter」と受光(検出)する「Light detector」部があります。

 1-1-2.ピン配置/ブロック図

 下図の通り

1-2.詳細仕様

 1-2-1.電圧と距離の関係

 AO(アナログ出力)と距離の関係図です。下は測定レンジ10~80cmにおいて、距離の逆数を取ると線形になり検量線(予想線)を引きやすいことを示していると思います。
 この図は重要なので別章で説明しますが、ポイントとしては最大電圧が3.3V以下のためRaspberry Pi PicoのADCでも計測可能です。

2.製品原理

 製品の動作原理に関する部分を説明します。

2-1.PSD(位置検出素子)

 位置検出素子(PSD:Position Sensitive Device)とは半導体表面における"Lateral Photo Effect"を利用し、センサ上に照射された入射光の位置を検出する方式です。PSD上の光入射位置に対して、そのエネルギーに比例した起電力が発生し、抵抗層を経て電極に達します[ 1 ]。

 PSDは高抵抗半導体基板の片面または両面に均一 な抵抗層を形成し、抵抗層の両端に信号取り出し用の1 対の電極を設けた構造をもっています。受光面は抵抗層 であると同時にPN接合をも形成しており、光起電力効果 により光電流を生成します[ 2] 。

図 PSDの断面構造図

 LED などの発光器と組み合わせ、レンズなどの適切な光学系を用いるとPSD 上の光スポット重心位置 dと投光器とPSDの距離 B、レンズの焦点距離 f から三角測量で距離を測定できます[ 2 ]。

【出典】
[1]  特集◆光学的非接触方式による高精密測定技術一解説一 位置測定におけるPSDの応用 倉沢一男
[2] 技術資料 HAMAMATSU
[3] PSD距離センサからの� データを解釈するための方法 吉田 智章
[4] 技術編 変異センサ OMRON

2-2.半導体発光ダイオード(IRED)

 IRED(Infrared Emitting Diode)は赤外線LEDです。
 発光ダイオード(LED:Light Emission Diode)は、半導体レーザ(LD)と同じくp-n接合に電流を流して発光させる半導体発光素子で、半導体材料の違いで紫外、可視、赤外域のさまざまな波長の光を発光させることができます。 

 発光ダイオード(LED)の基本的な原理は、p型半導体(ホールが多い半導体)とn型半導体(電子が多い半導体)を接合したp-n接合が作り、この素子に順方向の電圧をかけるとホールと電子はp-n接合に向けて移動し双方が結合して消滅します。このとき電子がエネルギーの高い状態から低い状態に移るので余ったエネルギーが光として外部に放出されます。

https://www.fiberlabs.co.jp/tech-explan/about-led/

3.部材購入

3-1.購入品

 部品は本体のみ購入しました。

3-2.準備必須品

 その他必需品は下記の通りです。

  • マイコン/シングルボード(Raspberry Pi/Pico)

  • ブレッドボード

  • ジャンピングワイヤー

4.環境構築

4-1.マイコン準備

 センサを制御するためのシングルボードやマイコンの準備を行います。
Raspberry PiやPicoの準備は下記記事参照のこと

 Raspberry PiにGPIOを制御するためのライブラリが無い場合は”RPi.GPIO”を事前にインストールしておきます。
 Picoの場合はMicropythonを使用できるようにしておきます。

[Terminal]
pip install rpi.gpio

4-2.ライブラリのインストール

 4-2-1.Case1:Pico

 Raspberry Pi Picoは組み込み関数と標準ライブラリで対応できるため、追加の環境構築は不要です。

 4-2-2.Case2:Raspberry Pi

 未実装

5.使用前の準備

5-1.はんだ付け

 本製品の既にピンがつけられているため、はんだ付けは不要です。

5-2.部品の組付け

 部品の組付けはジャンパー線を使用して下記の通り繋ぎました。

 今回はつけていないですが電源を安定させるにはキャパシタを付けた方が良いらしいです。

【Raspberry Pi】
 今回は未実装

【Raspberry Pi Pico】

$$
\begin{array}{|c|c|c|} \hline \textbf{No.} &\textbf{センサー} & \textbf{Raspberry Pi Pico} \\
\hline \text{1} & \text{VCC} & \text{PIN40|VBUS(5V)}\\
\hline \text{2} & \text{GND} & \text{PIN38(GND)}\\
\hline \text{3} & \text{Vo} & \text{GPIO26(ADC0)}\\
\hline \end{array}
$$

5-3.検量線の作成

 結論としては「電圧と距離の関係式」を作成していきます。

 データシート(仕様書)とアプリケーションノートに下図があり、下記のことが確認できます。

  • 距離と電圧値は一定の関係がある。

  • 仕様書範囲の10~80cmは下に凸の曲線

  • 距離の逆数を取ると、”距離の逆数vs電圧”は比較的線形な値をとる

  • Exampleって書いてるということは「自分で検量線作成しろ」ってことか???白い紙とか使って作れってことか?

図  Example of distance measuring characteristics(output)(データシート記載)
図 4. Example of GP2Y0A Series distance  Characteristics (アプリケーションノート記載)

 とりあえずはデータシートの図を参照して、検量線を作成してみました。(本当は全部実測した方が良いのかもしれないですが)

 5-3-1.データ読み取り:WebPlotDigitizer

 グラフからデータを読み取るためにWebPlotDigitizerを使用しました。使い方に関しては下記記事に詳細が記載されています。

 まず事前にグラフ画像をスナップショットでコピーし画像ファイルとして保存しておきます。次に、下記サイトに移動しLaunch Onlineを開きます。

 次にWebPlotDigitizerの準備をしていきます。概要は下図の通りです。

 準備が出来たら、頑張ってポチポチクリックしながら点を取ります。十分取れたら、View Dataを押してDownload CSVを押します。
※なぜか全データ取っていますが、正確には10cm~80cm内データのみでよいです。

 クリック後にCSVファイルとしてプロットしたデータが得られました。プロットデータを確認すると「最大電圧3.14V@5.83cm」でした。

 Pythonでプロットが適切に取れていることを確認しました。なお、センサーの仕様測定距離は10~80cm内ですが、今回は学習用も含めて全範囲でコード作成していきます。
 結果として、参照グラフとほぼ同じ結果が得られました。

[IN]
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import japanize_matplotlib  

df = pd.read_csv('Default Dataset.csv', header=None)
df.columns = ['Distance[cm]', 'Volt[V]'] #カラム名修正
#距離<5.83cmのデータを抽出
df_short = df[df['Distance[cm]'] < 5.83]
#距離>=5.83cmのデータを抽出
df_long = df[df['Distance[cm]'] >= 5.83]

#可視化
fig, axs = plt.subplots(1,2,figsize=(12, 6)) 
##距離と出力電圧の関係図
axs[0].scatter(df_short['Distance[cm]'], df_short['Volt[V]'], color='blue', alpha=0.5, marker='x', label='<5.83cm')
axs[0].scatter(df_long['Distance[cm]'], df_long['Volt[V]'], color='red', alpha=0.5, marker='o',label='≧5.83cm')
axs[0].set(xlabel='Distance[cm]', ylabel='Volt[V]', xlim=(0, 80), ylim=(0, 3.5),
       xticks=np.arange(0, 81, 10), yticks=np.arange(0, 3.6, 0.5),
       title='計測距離[cm]と出力電圧[V]の関係図')
axs[0].legend(), axs[0].grid()
##距離の逆数をとったときの関係図
axs[1].scatter(1/df_short['Distance[cm]'], df_short['Volt[V]'], color='blue', alpha=0.5, marker='x', label='<5.83cm')
axs[1].scatter(1/df_long['Distance[cm]'], df_long['Volt[V]'], color='red', alpha=0.5, marker='o',label='≧5.83cm')
axs[1].set(xlabel='1/Distance[1/cm]', ylabel='Volt[V]', xlim=(0, 0.5), ylim=(0, 3.5),
         xticks=np.arange(0, 0.51, 0.05), yticks=np.arange(0, 3.6, 0.5),
         title='1/計測距離[1/cm]と出力電圧[V]の関係図')
axs[1].legend(), axs[1].grid()
plt.show()

[OUT]

 5-3-2.多項式回帰の作成

 前提として、仕様書の計測範囲10~80cmにおいて距離の逆数と電圧は比較的線形の傾向があるため下記式でもよいと思います。

$$
出力電圧[V]=w_1\frac{1}{計測距離[cm]}+w_0=w_1x+w_0
$$

 今回は動作確認も含めて全距離で出力が見れるようにしてみます。機械学習まで使用するとMicropythonへの移動が手間のため多項式回帰を使用しました。なお、多項式でも最大電圧(曲線の山の部分)も含めて全距離を計算するのは難しかったため、最大電圧距離=5.83cmを境に式を2つに分けました。

 まずは練習で距離と電圧をフィッティングしました。PolynomialFeaturesの次数を変えながらフィッティングしたら6次がベストでした。

[IN]
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import japanize_matplotlib  
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression

df = pd.read_csv('Default Dataset.csv', header=None)
df.columns = ['Distance[cm]', 'Volt[V]'] #カラム名修正
#距離<5.83cmのデータを抽出
df_short = df[df['Distance[cm]'] < 5.83]
#距離>=5.83cmのデータを抽出
df_long = df[df['Distance[cm]'] >= 5.83]


#多項式回帰でフィッティング
#距離<5.83cm用
model = PolynomialFeatures(degree=6, include_bias=False)
model_poly = model.fit_transform(df_short['Distance[cm]'].values.reshape(-1, 1))

linear_short = LinearRegression()
linear_short.fit(model_poly, df_short['Volt[V]'])
print(f'係数:{linear_short.coef_}, 切片:{linear_short.intercept_}')

#距離>=5.83cm用
model_poly = model.fit_transform(df_long['Distance[cm]'].values.reshape(-1, 1))

linear_long = LinearRegression()
linear_long.fit(model_poly, df_long['Volt[V]'])
print(f'係数:{linear_long.coef_}, 切片:{linear_long.intercept_}')


#可視化
#可視化
x1 = np.linspace(0, 5.83, 100).reshape(-1, 1)
x2 = np.linspace(5.83, 80, 100).reshape(-1, 1)
y_pred1 = linear_short.predict(model.fit_transform(x1))
y_pred2 = linear_long.predict(model.fit_transform(x2))

fig, ax = plt.subplots(1,1,figsize=(6, 6)) 
##距離と出力電圧の関係図
ax.scatter(df_short['Distance[cm]'], df_short['Volt[V]'], color='blue', alpha=0.5, marker='x', label='<5.83cm')
ax.scatter(df_long['Distance[cm]'], df_long['Volt[V]'], color='red', alpha=0.5, marker='o',label='≧5.83cm')
ax.plot(x1, y_pred1, color='blue', linestyle='-', label='fitting curve(<5.83cm)')
ax.plot(x2, y_pred2, color='red', linestyle='-', label='fitting curve(≧5.83cm)')
ax.set(xlabel='Distance[cm]', ylabel='Volt[V]', xlim=(0, 80), ylim=(0, 3.5),
       xticks=np.arange(0, 81, 10), yticks=np.arange(0, 3.6, 0.5),
       title='計測距離[cm]と出力電圧[V]の関係図')
ax.legend(), ax.grid()
plt.show()
[OUT]
係数:[ 0.37762461  0.08851942  0.05079467 -0.03578782  0.00757885 -0.00055689], 切片:0.009885488793259345
係数:[-4.64240175e-01  2.18630919e-02 -5.77894500e-04  8.60860409e-06 -6.75364123e-08  2.17097708e-10], 切片:5.307920390299143

 本題として「電圧から距離を計測したい」ため、フィッティングを逆にしました。結果として下記数式でいけそうです。

$$
L_{Short} = 1.544V^5 + 2.443V^4 - 4.628V^3 + 3.306V^2 - 1.038V + 0.1201+ 0.0352\\
L_{Long} = -643.2V^5 + 861.6V^4 - 632.1V^3 + 257.8V^2 - 54.70V + 4.700 + 233.8
$$

[IN]
#多項式回帰でフィッティング
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression

#距離<5.83cm用
model = PolynomialFeatures(degree=6, include_bias=False)
model_poly = model.fit_transform(df_short['Volt[V]'].values.reshape(-1, 1))

linear_short = LinearRegression()
linear_short.fit(model_poly, df_short['Distance[cm]'])
print(f'Short係数:{linear_short.coef_}, 切片:{linear_short.intercept_}')

#距離>=5.83cm用
model_poly = model.fit_transform(df_long['Volt[V]'].values.reshape(-1, 1))

linear_long = LinearRegression()
linear_long.fit(model_poly, df_long['Distance[cm]'])
print(f'Long係数:{linear_long.coef_}, 切片:{linear_long.intercept_}')


#可視化
x = np.linspace(0, 3.2, 100).reshape(-1, 1) #MAX:3.14V
y_pred1 = linear_short.predict(model.fit_transform(x))
y_pred2 = linear_long.predict(model.fit_transform(x))

fig, ax = plt.subplots(1,1,figsize=(6, 6)) 
##距離と出力電圧の関係図
ax.scatter(df_short['Volt[V]'], df_short['Distance[cm]'], color='blue', alpha=0.5, marker='x', label='<5.83cm')
ax.scatter(df_long['Volt[V]'], df_long['Distance[cm]'], color='red', alpha=0.5, marker='o',label='≧5.83cm')
ax.plot(x, y_pred1, color='blue', linestyle='-', label='fitting curve(<5.83cm)')
ax.plot(x, y_pred2, color='red', linestyle='-', label='fitting curve(≧5.83cm)')
ax.set(xlabel='Volt[V]', ylabel='Distance[cm]', xlim=(0, 3.5), ylim=(0, 80),
       xticks=np.arange(0, 3.6, 0.5), yticks=np.arange(0, 81, 10),
       title='計測距離[cm]と出力電圧[V]の関係図')
ax.legend(), ax.grid()
plt.show()
[OUT]
Short係数:[ 1.54381182  2.44304695 -4.62765743  3.30587764 -1.03752478  0.12008331], 切片:0.035225993927588295
Long係数:[-643.18179372  861.61285439 -632.13002196  257.78916872  -54.69732959 4.69926164], 切片:233.84807680573311

6.MicroPythonスクリプト(Pico)

 Micropythonでコードを作成しました。

6-1.任意:デバイス接続の確認

 通信はないため、外観チェックだけしておきます。

[IN]
-
[OUT]
-

6-2.コードの設計思想

 設計思想は下記の通りです。

  1. 検量線は事前に作成しておき、MicroPythonでも使用できる形にしておく。

    • 多項式回帰の係数は確実に動作するよう、関数内にベタ打ち

  2. 仕様の計測距離は10~80cmだが、(学習のため)動作確認できるように10cm以下も計測できるようにしておく。

    • Bool型でLong/Shortを選択できるようにしておく

    • 仕様測定範囲はLong(正確には10cm以上)のため、DefaultはLong

【コラム:Micropythonでの辞書/関数の挙動】
 もともとPythonの辞書型は「リストと異なり入力の順序は記憶しない」特性があります。PCを用いてJupyter Notebookで使用していると忘れがちですが、Micropythonでは影響が出ます。

 例として作成する多項式回帰の関数を**kwargsで取得しvalues()で取り出そうとした時、Pythonでは記載順で出力されますがMicropythonでは順序は保持されていません。普段は気にしていませんでしたが、辞書型は入力の順序は記憶しないので、最低でもKEYを用意する必要があります。
 本コードでは動作が分かりやすいよう、係数はすべてベタ打ちしました。

[IN]
x = [1, -2.0, 4.0, -3]

def f1(*args):
    return args

def f2(**kwargs):
    return kwargs

print(f1(*x))
print(f2(a=1, b=-2.0, c=4.0, d=-3))
print(f2(a=1, b=-2.0, c=4.0, d=-3).values())

6-3.スクリプト実行

 スクリプトを作成し、実行しました。問題なく値を取得できました。
 仕様書にもありますが動く物体の計測には縛りがあるため、一部の計測結果に外れ値が発生しています。

[IN]
from machine import Pin, ADC

def f_polynominal(x, a6, a5, a4, a3, a2, a1, b):
    y = a6*x**6 + a5*x**5 + a4*x**4 + a3*x**3 + a2*x**2 + a1*x + b
    return y

def calc_distance(volt:float, Long_mode:bool=True):
    #係数(距離<5.83cm用)
    coeff_S = {'a1': 1.54381182, 
                   'a2': 2.44304695, 
                   'a3': -4.62765743, 
                   'a4': 3.30587764, 
                   'a5': -1.03752478, 
                   'a6': 0.12008331,
                   'b': 0.0}
    #係数(距離>=5.83cm用)
    coeff_L = {'a1': -643.18179372,
                  'a2': 861.61285439,
                  'a3': -632.13002196,
                  'a4': 257.78916872,
                  'a5': -54.69732959,
                  'a6': 4.69926164,
                  'b': 233.84807680573311}    

    if Long_mode:
        a1, a2, a3, a4, a5, a6, b = coeff_L['a1'], coeff_L['a2'], coeff_L['a3'], coeff_L['a4'], coeff_L['a5'], coeff_L['a6'], coeff_L['b']
        return f_polynominal(volt, a6=a6, a5=a5, a4=a4, a3=a3, a2=a2, a1=a1, b=b)
    else:
        a1, a2, a3, a4, a5, a6, b = coeff_S['a1'], coeff_S['a2'], coeff_S['a3'], coeff_S['a4'], coeff_S['a5'], coeff_S['a6'], coeff_S['b']
        return f_polynominal(volt, a6=a6, a5=a5, a4=a4, a3=a3, a2=a2, a1=a1, b=b)
    

#ADCから電圧値取得
adc = ADC(Pin(26)) #GPIO26 ADC0
val_raw = adc.read_u16() #デジタル値(0-65535)取得
volt = val_raw * 3.3 / 65535 #電圧値(0-3.3V)に変換
print(f'Raw Value: {val_raw}, Voltage: {volt}V')

#距離計算
dist = calc_distance(volt, Long_mode=True) #Long_mode=Trueで5.83cm以上の距離計算
print(f'Distance: {dist}cm')
[OUT]
Raw Value: 35560, Voltage: 1.79V
Distance: 13.72cm

Raw Value: 53020, Voltage: 2.67V
Distance: 8.51cm

Raw Value: 49516, Voltage: 2.49V
Distance: 9.61cm

Raw Value: 43722, Voltage: 2.20V
Distance: 10.90cm

Raw Value: 38985, Voltage: 1.96V
Distance: 12.21cm

Raw Value: 27174, Voltage: 1.37V
Distance: 19.67cm

Raw Value: 32199, Voltage: 1.62V
Distance: 15.73cm

6-4.おまけ:実測

 実際に距離を見ながら計測してみました。(本当は白い紙とかの方が良いと思います)

 結果は下記の通りです。10cm以下は除外しても、コスパでいうならそこそこ良い結果に感じます。原理的に入射光の角度が重要なため、たぶん壁っぽいやつの方が精度は高いはずです。
(※データシートにも境目があるものに対するNoteがあったので)

$$
\begin{array}{|c|c|c|c|c|}
\hline\textbf{No.} & \textbf{実距離[cm]} & \textbf{デジタル値} & \textbf{電圧[V]} & \textbf{計測距離[cm]} \\
\hline1 & 3.5 & 27686 & 1.39 & 19.22 \\
\hline2 & 7.0 & 62191 & 3.13 & 7.24 \\
\hline3 & 9.0 & 57598 & 2.90 & 6.88 \\
\hline4 & 11.0 & 51004 & 2.57 & 9.19 \\
\hline5 & 13.0 & 39721 & 2.00 & 11.96 \\
\hline6 & 15.0 & 34248 & 1.72 & 14.44 \\
\hline7 & 15.0 & 36120 & 1.82 & 13.43 \\
\hline8 & 18.0 & 29223 & 1.47 & 17.94 \\
\hline9 & 21.0 & 26758 & 1.35 & 20.04 \\
\hline10 & 24.0 & 23749 & 1.20 & 23.00 \\
\hline11 & 27.0 & 22021 & 1.11 & 24.98 \\
\hline12 & 30.0 & 20020 & 1.01 & 27.70 \\
\hline\end{array}
$$

7.Pythonスクリプト(Raspberry Pi)

 Raspberry PiはADCがないため外付けADCが必要となります。私は実装していませんが、下記記事にRaspberry PiにA/DコンバータMCP3002を外付けして測定した結果がありますのでご参考までに。

8.所感

 簡単な所感は下記の通り

  • 思った以上に精度がよい。さすがは天下のシャープ!

  • 規格はないのだけど、ケーブルの色が気持ち悪いのは自分だけ?

  • ようやくアナログ出力(AI/AO)が分かってきた。

  • 電子機器は距離が短いのもあり大体が電圧で測定する物が多い。そろそろDC4-20mAも扱ってみたい(抵抗器250Ω付けたら1-5Vと同じだろうけど)


参考資料

別添1 Python関係

別添2 技術関係

あとがき

 今までのセンサよりすんなりいったのは良かった。やっぱ日本製は相性がよい


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