トップページ > おもしろ科学実験室(工学のふしぎな世界) > 超音波速度センサを作ろう

おもしろ科学実験室(工学のふしぎな世界)

超音波速度センサを作ろう

2018年8月17日
熊本大学 工学部

必要なもの

  1. パソコン(WindowsやmacOS。ここではmacOS High Sierraを用いています)
  2. Arduino Uno
  3. 超音波距離計 US-015
  4. ブレッドボード
  5. リード線
  6. USBケーブル

今回は、超音波距離センサーモジュールを用いてターゲットまでの往復時間を測定し、ターゲットまでの距離とターゲットの移動速度を推定します。超音波距離センサーの制御にはArduinoを用います。得られた往復時間はArduinoからシリアル通信によってProcessingに転送され、距離と速度の情報を描画します。

図1:実験の概要

図2に示すようにUS-015のピン配列は右からGND(グランド)、Echo(受信)、Trig(送信)、VCC(電圧印加)となっており、Arduino Unoとは図3のように接続します。詳細は以下の通りです。

US-015 -> Arduino
GND -> GND
Echo -> 9
Trig -> 7
Vcc -> 5V

図2:超音波距離センサーモジュールUS-015
図3:接続の様子

Arduinoの設定

次にArduinoの設定です。Arduinoの開発環境は以下のサイトで無償で入手できます。
https://www.arduino.cc/

Arduino Unoとパソコンを接続したら、Arduinoアプリの画面でシリアルポートを設定します。「ツール」バーから「シリアルボード」をクリックして、「Arduino/Genuino Uno」となっているものを選びます。ここでは「/dev/cu.usbmodem14341」です。この情報は後ほどprocessingの設定でも用います。

Arduinoアプリに書くプログラムを説明します。最初にtrig(トリガー、送信)とecho(エコー、受信)のピンを設定します。実際に接続した状態に合わせて設定します。ここではtrigが7pin、echoが9pinです。
int trig = 7; // 出力ピン
int echo = 9; // 入力ピン
float dist = 0.;

setup部の設定

次に、setup部の設定です。ここでシリアル通信とpinのモード設定をします。この部分は最初に1度だけ実行されます。
void setup() {
Serial.begin(9600);
pinMode(trig,OUTPUT);
pinMode(echo,INPUT);
}

loop部の設定

次はloop部です。この部分はひたすら繰り返し実行されます。念のため Trig をいったん LOW、つまり0に設定して2μsec待った後に trig を HIGH(1) にして超音波照射をスタートします。そして20μsec経ったら再び trig を LOW に設定しすることで超音波照射がストップします。これで20μsec長のパルス波を照射したことになります。
void loop() {
// 超音波を照射
digitalWrite(trig, LOW);
delayMicroseconds(2);
digitalWrite(trig, HIGH);
delayMicroseconds(20);
digitalWrite(trig, LOW);

超音波の往復時間を超音波センサモジュールから出力させます。pulseIn関数を用いてターゲットから返って来た往復時間をduration変数に代入します。
float duration = pulseIn(echo, HIGH) ;

ArduinoからProcessingにシリアル通信によってdurationの値を転送します。この時、duration変数は32 bit(=4 byte)のfloat型ですが、シリアル通信では8 bit(=1 byte)しか転送できません。超音波センサから得られる往復時間[μs]の少数を切り捨てて整数を転送しようとした場合、1 byteであれば0から255(=28-1)まで表現できるので、最大距離は
255×10-6×340÷2=0.04352

となり、最大4.5cm程度しか検出できなくなります。しかし2byteあれば0から65535 (=216-1)まで表現できるので、距離センサから得られる往復時間[μs]の少数を切り捨ててしまえば、
65535×10-6×340÷2=11.14112

となり、11 mまで測定できることになります。超音波センサーモジュールの測定範囲は2〜400 cmなので、2 byteあれば十分ということがわかります。そこで今回は2 byteのint型に変換(キャストと言います)した上で、1 byteずつ転送することにします。Arduino側で2byte分のint型の値を2つに分割します。
 // Processing側とシリアル通信するための前処理
unsigned int value = int(duration);
byte val1 = (uint8_t)((value >> 8) & 0xFF);
byte val2 = (uint8_t)((value >> 0) & 0xFF);

分割した二つの情報の前にヘッダ情報を添えて、それぞれ転送します。ヘッダである’h’と分割されたval1もしくはval2の値がたまたま一致した場合にはエラーが生じる危険がありますが、今回は無視します。興味のある方は、回避方法を考えてみてください。
Serial.write('h');
Serial.write(val1);
Serial.write(val2);
delay(30);  // 30 msec待つ

}

これでArduino側の設定は終わりです。「マイコンボードに書き込む」ボタンを押して完了です。

Processing側の設定

次にProcessing側の設定です。
Processingは以下のサイトで無償で入手できます。Examplesを眺めるだけでも楽しいです。
https://processing.org/

最初に、シリアル通信用ライブラリを読み込んで、ポートの設定をします。
import  processing.serial.*;
Serial  serial;

距離データを保存するdist_dataと速度データを保存するspd_dataというint型の配列を用意します。
int[] dist_data;
int[] spd_data;

速度を計算するために超音波を照射した時間間隔の情報が必要になります。そこで二つの変数next_timeとprv_timeを用意します。
float next_time = 0.;
float prv_time = 0.;

速度を表示する際の最大値を決めておくことで、見やすいグラフを目指します。
int spd_max = 400;

setup関数

ここまでがprocessingのプログラム全般に関わる設定でした。次はsetup関数です。ここはProcessingのプログラムを最初に実行した時に1回だけ実行するものを記述し、初期化関数とも呼ばれています。今回は、結果を表示するウィンドウの枠のサイズ、距離と速度データを保存する変数のサイズ、そして、シリアル通信を行うポートの設定を行います。ここで"/dev/cu.usbmodem14311"としている箇所は人によって異なります。Arduinoの画面に戻って、「ツール」バーから「シリアルポート」を選択し、どのポートが設定されているか確認してください。

void setup() {
size(800, 520);
dist_data = new int [width];
spd_data = new int [width];
serial = new Serial(this, "/dev/cu.usbmodem14311", 9600);
}

draw関数

次にdraw関数です。この関数はプログラムの実行中、常に繰り返し実行される部分です。今回はウィンドウの描画を行っています。Processingでは左上を座標の原点(0,0)として、xは右方向、yは下方向に与えられているので注意が必要です。

void draw() {
background(10);      //背景の設定
strokeWeight(2);     //枠の太さの設定
stroke(0,0,0);       //色の設定。(R,G,B)の順に設定する。(0,0,0)は黒色。

 // Upper: distance
rect(0, 0, 800, 256); //点(0,0)から横800px(ピクセル)、縦256pxの四角を描画。
// Bottom: speed
rect(0, 261, 800, 256); //点(0,261)から横800px(ピクセル)、縦256pxの四角を描画。

stroke(0, 255, 0); //色の設定。(0,255,0)は緑色。
dist_graph(dist_data); //dist_graph関数を呼び出し。詳細は後述。

stroke(255, 0, 0); //色の設定。(255,0,0)は赤色。
spd_graph(spd_data); //spd_graph関数を呼び出し。詳細は後述。

}

serialEvent関数

次に、serialEvent関数です。これはシリアル通信でデータを受信した際に実行されます。port.available()を用いると外部(今回の場合はArduino)から1 byte分のデータが何個送られて来ているかがわかります。If文を使って、3 byte以上のデータが溜まったら次の文に進みます。最初に受け取ったデータが’h’と一致した場合、次のデータは分割した往復時間データの1つ目のはずなので、val1として、その次はval2として受け取ります。この時、millis関数を用いてプログラムが実行されてからの経過時間をnext_timeに代入しておきます。

void serialEvent(Serial port) { 
if ( port.available() >= 3 ) {   
if ( port.read() == 'h' ) { 
next_time = millis(); // [msec]
int val1 = port.read();
int val2  = port.read();

この2つのデータを合成して元の値である往復時間のデータを得ることができます。
int recv_data = val1 * 256 + val2;

このrecv_dataは単位がμsであることを考慮しつつ、空中音速を340 m/sとして、超音波センサーとターゲットの距離を計算します。往復の時間に音速をかければ往復伝搬距離が出るので、これをさらに2で割って、さらに今回は単位をmmに変換します。
int dist = int( recv_data * 340. / 2. * 1e-3)  ; // [mm]

伝搬時間から距離データを得ることができました。次は、距離データを逐次取得しつつ、過去のデータを保存していきます。こうすることで時系列データとして過去の距離を表示できる上に、過去の距離情報を利用して速度を算出します。
まず、最初に用意したdist_dataとspd_dataを1ステップずらし、0番目の配列に格納されていた最も古いデータが消去されます。

for (int i=0; i<dist_data.length-1; i++) {
dist_data[i] = dist_data[i+1];
spd_data[i] = spd_data[i+1];
      }

そして、dist_data配列の最後に、先ほど計算した最も新しい距離データを格納します。
dist_data[dist_data.length-1] = int(dist);

速度データ

次は速度データです。前回照射した時からターゲットが動いた場合、得られた距離データが変化しているはずです。そこで過去の距離データを用いることで、速度情報を算出します。速度情報には、下式に示すように、前回から今回照射するまでの間に経過した時間を利用します。

速度 = (今回の距離 – 前回の距離) ÷ (今回の時間 – 前回の時間)

この時注意したいのは、単純に1つ前に測定した距離のみを使って速度を算出しようとすると、得られる速度が大きくばらついてしまいます。これは、超音波センサの時間分解能の性能のみならず、ターゲットの形状によってはうまくエコーを取得できなかったり、あるいは今回の場合はシリアル通信がうまくいかなかったりと、様々な要因が考えられます。そこで今回は、最新の情報含めて過去8回分の速度情報を利用することで、極端な変化を抑制しました。

int tmp = 0;
int packet = 8;
for (int i=spd_data.length-packet; i<spd_data.length-1; i++) {
tmp = tmp + spd_data[i]/packet;
}
tmp = tmp + int( (dist_data[dist_data.length-1] - dist_data[dist_data.length-2])  / (next_time - prv_time) * 1000)/packet;  //[cm/s]

if(tmp >= spd_max){
tmp = spd_max;
}
spd_data[spd_data.length-1] = tmp ;

最後に

今回の経過時間をprv_timeに格納し、次の計算に備えます。
prv_time = next_time;

今度は、draw関数内で参照されているdist_graph関数とspd_graph関数です。dist_graph関数は距離の時系列データを描画します。距離は常に正の値をとるので、ウィンドウの下限を 0mm、上限をdist_max [mm]となるように設定しています。一方spd_graphは速度の時系列データを描画します。速度は近く方向と遠ざかる方向のプラスマイナスの値をとるので、ウィンドウの中心を0 [mm/s]、上限をspd_max [mm/s]、下限を-spd_max [mm/s]として描画します。

void dist_graph(int data[]){
for (int i=0; i<data.length-1; i++) {
line( i, 256-data[i]*256/dist_max, i+1, 256-data[i+1]*256/dist_max );
}
}

void spd_graph(int data[]){
for (int i=0; i<data.length-1; i++) {
line( i, height-128-data[i]*128/spd_max, i+1, height-128-data[i+1]*128/spd_max );
}
}

Processingを実行すると、下記のようなグラフが表示されます。上の緑色の波形が距離、下の赤色の波形が速度になります。

Arduinoプログラムの完成版

Arduinoプログラムの完成版はこちらになります。

// Arduino
int trig = 7; // 出力ピン
int echo = 9; // 入力ピン
float dist = 0.;

void setup() {
Serial.begin(9600);
pinMode(trig,OUTPUT);
pinMode(echo,INPUT);
}

void loop() {
// 超音波を照射
digitalWrite(trig, LOW);
delayMicroseconds(2);
digitalWrite(trig, HIGH);
delayMicroseconds(20);
digitalWrite(trig, LOW);

// US-015からエコーが返って来た時間を取得
float duration = pulseIn(echo, HIGH) ; //microsec  float: 32bit
// エコー時間からターゲットまでの距離を計算
unsigned int dist = int(duration * 340. / 2. * 1e-3)  ; // [mm]

// Processing側とシリアル通信するための前処理
unsigned int value = dist;
byte val1 = (uint8_t)((value >> 8) & 0xFF);
byte val2 = (uint8_t)((value >> 0) & 0xFF);

Serial.write('h');
Serial.write(val1);
Serial.write(val2);
delay(30);  // 30 msec待つ

}

Processingのプログラムの完成版

Processingのプログラムの完成版はこちらになります。

// Processing
import  processing.serial.*;

Serial  serial;
int[]   dist_data;
int[] spd_data;

float next_time = 0.;
float prv_time = 0.;      // time

int dist_max = 400;
int spd_max = 400;

void setup() {
size(800, 520);
dist_data = new int [width];
spd_data = new int [width];
serial = new Serial(this, "/dev/cu.usbmodem14311", 9600);
}

void draw() {
background(10);
strokeWeight(2);
stroke(0,0,0);
// Upper: distance
rect(0, 0, 800, 256);
// Bottom: speed
rect(0, 256+5, 800, 256);

stroke(0, 255, 0);
dist_graph(dist_data);

stroke(255, 0, 0);
spd_graph(spd_data);

}

void serialEvent(Serial port) { 

if ( port.available() >= 3 ) {   
if ( port.read() == 'h' ) { 
next_time = millis(); // [msec]
int val1 = port.read();
int val2  = port.read();
int recv_data = val1 * 256 + val2;

int dist = int( recv_data * 340. / 2. * 1e-3)  ; // [mm]

for (int i=0; i<dist_data.length-1; i++) {
dist_data[i] = dist_data[i+1];
spd_data[i] = spd_data[i+1];
}

dist_data[dist_data.length-1] = int(dist);

if(dist_data[dist_data.length-1] >= dist_max){
dist_data[dist_data.length-1] = dist_max;
}

int tmp = 0;
int packet = 8;
for (int i=spd_data.length-packet; i<spd_data.length-1; i++) {
tmp = tmp + spd_data[i]/packet;
}
tmp = tmp + int( (dist_data[dist_data.length-1] - dist_data[dist_data.length-2])  / (next_time - prv_time) * 1000)/packet;  //[cm/s]

if(tmp >= spd_max){
tmp = spd_max;
}

spd_data[spd_data.length-1] = tmp ;   
prv_time = next_time;
}

}
}

 

void dist_graph(int data[]){
for (int i=0; i<data.length-1; i++) {
line( i, 256-data[i]*256/dist_max, i+1, 256-data[i+1]*256/dist_max );
}
}

void spd_graph(int data[]){
for (int i=0; i<data.length-1; i++) {
line( i, height-128-data[i]*128/spd_max, i+1, height-128-data[i+1]*128/spd_max );
}
}

原理

今回は、Arduinoを介して超音波距離センサーを用いて速度を算出し、さらにProcessingを用いて描画を行いました。例えば人が秒速3 mで歩いた場合、1秒ごとに3 m移動することになります。今回はある時間あたりの距離の変化から速度を算出しましたが、他にも速度を測定することができます。
その一つが、みなさんも聞いたことのあるはずのドップラー効果と呼ばれるものです。救急車が目の前を通り過ぎる時、ピーポーピーポーの音程が変化するのを感じると思います。音を出す物体が動いていると、音の周波数が変わる現象をドップラー効果と呼びます。どの程度音程が変わるかは、救急車の速度によって変わり、猛スピードで通り過ぎるとかなりはっきりと変わりますし、徐行だと変化に気づかないはずです。

※このページに含まれる情報は、掲載時点のものになります。

関連記事

2019-09-13

生レポート!大学教授の声

私が受験勉強を乗り越えた方法、今も役立つ受験科目

長岡技術科学大学工学部

2023-10-17

生レポート!現役学生の声

研究と自己管理の重要性

福島大学共生システム理工学類

2015-12-21

生レポート!卒業生の声

大学で得る自信

山形大学工学部

2024-07-16

生レポート!卒業生の声

苦労して学んだ数学とプログラミング そこで得た忍耐力が社会人としての力に

電気通信大学情報理工学域

2019-01-18

生レポート!卒業生の声

あの時研究していなかったら?~研究活動は人生の岐路~

和歌山大学システム工学部

2010-02-24

生レポート!卒業生の声

思いっきり熱中すること

佐賀大学理工学部

熊本大学
工学部

  • 土木建築学科
  • 数理機械工学科
  • 情報電気電子工学科
  • 材料・応用化学科
  • 半導体デバイス工学課程

学校記事一覧

おもしろ科学実験室(工学のふしぎな世界)
バックナンバー

このサイトは、国立大学55工学系学部長会議が運営しています。
(>>会員用ページ)
私たちが考える未来/地球を救う科学技術の定義 現在、環境問題や枯渇資源問題など、さまざまな問題に直面しています。
これまでもわたしたちの生活を身近に支えてきた”工学” が、これから直面する問題を解決するために重要な役割を担っていると考えます。