ライントレーサーをつくろう!

目次

ロボット紹介

今回の課題ではロボットは2つのセンサと2つのモーターを使い動きます。センサの位置はロボット前方、モーター2つで前進、後進、旋回をするため大体の形は決まってくると思います。

黒い線を辿る構造

今回のロボットは光センサ1つ、カラーセンサ1つを使用し黒い線の上を動くものです。
作られるプログラムによって、二つのセンサで黒い線を挟む方法と一つのセンサで動かす方法が考えられます。
挟む方法では次の図のようになります。

センサの図1

一つのセンサで黒い線をたどるには次の図のようになります。

センサの図2

今回二人ともこちらのやり方に近いですが微妙に違ったりします。
詳しくはプログラムを

ロボットの構造

基本的な構造は三輪車で中心に光センサ、少し右前にずれてカラーセンサがついています。今回は二人とも線を挟まないプログラムを作ったためこの様なセンサの配置となりました。

センサの写真

今回のロボットの特徴は本当にどうでもいいことなのですが見た目です。機能性よりはるかに見た目を大事にしました。
今回説明書を見ないで作ったのでキャスターも説明書のものと違います。部品数は少なくなりましたが少し車体が前に傾いてしまいます。

キャスターの写真

最初は説明書のロボットより軽量で強度のあるものを作りましたが線を探しながら進む姿に創作意欲が・・・
テーマは「生き物」です。

ロボットの外形

今回のロボットのテーマをクリアするために目を付けました。NETに入っていた発光ダイオードも活用し目が光る仕組みにしました。
特にこのような姿である必要はまったくありませんがせっかくなのでかっこいいものをと思いこのような姿で走らすことにしました。

このロボットの問題点と改善点

  • 重心が少し後ろに偏っていたため車輪が空転することがありました。この解決策としてNXT本体を少し前に出すことにしました。これにより空転することがなくなりました。
  • 今回のコースの中にありませんが進行方向右側への鋭角カーブは光センサがカーブにさしかかる前にカラーセンサが反応してしまいます。この解決策はプログラムにより鋭角カーブであることを判断するほかに、物理的解決策としてはカラーセンサの位置を変えることだと思います。しかし今回のコース内に鋭角カーブが存在しないため今回とくに対策を講じていません。
  • 目が光る分電池の減りが多少早くなるのではと思ったのですが、光らせるために使う電力は微々たるものなのであまり気にすることはないようです。今回は見た目をよくするために多少犠牲にしても仕方ないと思っています。
  • 実際に走らせて大きな問題となったのはコースの紙が何回も折ることにより所々盛り上がりセンサの値が大きくずれてしまうことです。解決策としてロボットで紙を抑えることにしました。
    紙を抑えるバーの写真
    これにより多少ではありますがセンサの誤認知を防げるようになりました。

コース解説

今回A4チームで作ったコースは次のコースです。
今回の課題は交差点は前進しロータリーは入ってから2つ目の出口を出るというものです。近道(点線)を行くか直進して直角に入るかです。

コースの写真

直線的なコースです。
形的には簡単なコースといえるのではないでしょうか。しかし、線が近いところがありカラーセンサが横に広いこのロボットには難しいところもあります。

プログラム紹介

今回の課題はロボットよりプログラムの勝負だと思います。中村と志村でお互い違う黒線の追い方をしています。

中村 (右回り、近道)

今回の課題で、最初はセンサ一つでライントレースに挑みました。しかしながら、主に時間の問題でロータリー内とその他センサ一つでは判定が困難な箇所で色センサを使うこととしました。

トレースの方法は単純で、光センサが感知している部分が黒ければ直進、白ければ前回曲がった方向と同方向に軌道を修正します。また、ロータリーや直角カーブを判定するため、若干逆回転のモーターの力を弱めるようにしています。

問題点としては、ロータリー・直角カーブ判定とヘアピンカーブ通過の兼ね合いが非常にシビアということが挙げられます。ロータリー判定では”左右方向一定距離のどちらにも黒線がない場合”ロータリーであると判定させているので、ヘアピンを曲がりきれなかった場合にロータリーだと判定してしまうのです。

また、他のコースを走らせる場合、そのままでは大抵のコースではうまく進行できません。比較的簡易な修正で済みますが、これも問題点の一つです。

/*
*  定数
* PP:回転ごとの増加値
* BORDER:明暗の閾値
* right90:右に90度回転する時間
* left90:左に90度回転する時間
*/
#define PP 0
#define BORDER 40
#define right90 500
#define left90 400
#define DEFPOW 50
#define ROT 410

long time;				//タイマーの値を保持する
int isblack=1;			//現在光センサーは黒か否か
int first_right=1;		//最初に右側旋回するか否か
int rottime;			//回転にかかった時間
int taskcnt=0;			//現在コースのどのパートにいるかを保持

sub r_force(int rot){	//右へ旋回(黒に戻っても停止しない)
	OnFwd(OUT_C,DEFPOW);
	OnRev(OUT_A,DEFPOW);
	Wait(rot);
	Off(OUT_AC);
	rottime=rot;
}
sub l_force(int rot){	//左へ旋回(黒に戻っても停止しない)
	OnFwd(OUT_A,DEFPOW);
	OnRev(OUT_C,DEFPOW);
	Wait(rot);
	Off(OUT_AC);
	rottime=rot;
}
sub straight(int t){	//光センサが黒になるか、t[ms]経つまで直進する
	time=CurrentTick();
	OnFwd(OUT_AC,DEFPOW);
	while(!isblack&&CurrentTick()-time<t){
		if(Sensor(IN_1)<BORDER){
			isblack=1;
		}
	}
	Off(OUT_AC);
}
sub left(int rot){		//右側旋回
	time=CurrentTick();
	OnFwd(OUT_A,DEFPOW);
	OnRev(OUT_C,DEFPOW*4/5);	//逆方向回転は若干弱める
	while(!isblack&&CurrentTick()-time<rot){	//光センサが黒になるか、タイマーがrot[ms]経つまで無限ループ
		if(Sensor(IN_1)<BORDER){
			first_right=0;
			isblack=1;
			rottime=CurrentTick()-time;
		}
	}
	Off(OUT_AC);
}
sub right(int rot){	//左側旋回(右側旋回の逆)
	time=CurrentTick();
	OnFwd(OUT_C,DEFPOW);
	OnRev(OUT_A,DEFPOW*4/5);
	while(!isblack&&CurrentTick()-time<rot){
		if(Sensor(IN_1)<BORDER){
			first_right=1;
			isblack=1;
			rottime=CurrentTick()-time;
		}
	}
	Off(OUT_AC);
}
sub short_path(){		//近道に差し掛かったときに呼ばれる。
	/*
	*直進+旋回で点線の破れた部分を通過し、近道を通行する。
	*/ 
	r_force(300);
	OnFwd(OUT_AC,50);
	Wait(500);
	Off(OUT_AC);
	while(true){
		OnFwd(OUT_AC,DEFPOW);
		if(Sensor(IN_1)>BORDER){
			isblack=0;
			right(ROT);
			straight(200);
			left(ROT*2);
		}
	
	}
	taskcnt++;
}
sub Rot(){		//ロータリーになったときに呼ばれる
	int i;

	/*
	*カラーセンサが白→黒になったことを判定するための変数。
	*/
	int bkcnt;
	int bkflg=0;
	bkcnt=0;
	switch(taskcnt){	//現在のコース内でのパートを判定
	case 1:
	case 2:
		/*ロータリーに侵入したことを示す音を鳴らす*/
		PlayTone(1000,30);
		Wait(30);
		for(i=0;i<50;i++){
			PlayTone(1000-i*20,5);
			Wait(5);
		}

		/*
		*ロータリーの線上へ動く
		*/
		r_force(200);
		isblack=0;
		right(2000);
		r_force(300);

		while(true){	//ゆっくりと回る。
			OnFwd(OUT_AC,DEFPOW*3/5);
			if(Sensor(IN_1)>BORDER){
				isblack=0;
				left(ROT*3);
				PlayTone(1000,10);
			}
			if(!bkflg&&Sensor(IN_2)<BORDER){
				bkflg=1;
				bkcnt++;
			}
			if(bkflg&&Sensor(IN_2)>BORDER){
				bkflg=0;
			}
			if(bkcnt==2){		//二回目の黒線通過でロータリーの通過を判定。
				OnFwd(OUT_AC,DEFPOW);
				Wait(400);
				Off(OUT_AC);
				r_force(right90);
				first_right=1;
				break;
			}
		}
		break;
	}
}
task main(){
	int i,j;
	time=CurrentTick();			//タイマーの初期をセット
	SetSensorLight(IN_1);		//光センサをセット
	SetSensorColorRed(IN_2);	//色センサをセット
	while(true){
		OnFwd(OUT_B,50);		//目を光らせる
		OnFwd(OUT_AC,DEFPOW);	//基本直進させる

		if(Sensor(IN_1)>BORDER){//黒線から外れた時
			isblack=0;			//外れたことを変数に格納
			for(j=1;j<=2;j++){	//二回旋回のチャンス
				if(isblack==0){	//未だに黒でければ
					if(first_right){
						right(ROT+PP*j);	//右旋回→左旋回
						left((ROT+PP*j)*2);
					}
					else{
						left(ROT+PP*j);		//左旋回→右旋回
						right((ROT+PP*j)*2);
					}
					if(j==2){				//二回旋回しても黒線に当たらないとき
						taskcnt++;
						if(taskcnt==1||taskcnt==2){		//ロータリーであると判定
							Rot();
						}
					}
					PlayTone(50,30);					//旋回するたびに音を鳴らす
				}
			}
		}
		if(taskcnt==2&&Sensor(IN_2)<BORDER){	//taskが2で色センサが感知したら近道である
			short_path();
		}
	}
}

志村(左回り、直角)

今回私は、通常光センサ1つを使い黒い線を辿るプログラムにしました。
黒の値と白の値の間を通るプログラムです。

志村のセンサの図

こうすることでより直線的に動くことができます。
カラーセンサは交差点とロータリーの認識に使いました。
カラーセンサが黒を認識することでロータリーや交差点に差し掛かったことを示します。カラーセンサが黒に反応後少し前に出て光センサが白ならロータリー、黒なら交差点となります。
変数を使用し交差点、ロータリーを何個過ぎたかを数えロボットの対応を変えています。
今回、黒 白の値に測定値を使うことでその場でプログラムの微調整をしなくて済むようにしました。task main()のはじめの長い命令はこの黒 白の値を測定した後スタートラインに移動させスタートを待つプログラムです。しかし、黒の値 白の値を測定するのにロボットを自分で動かす必要はあります。プログラムでロボットが測定場所に動くようにすることもできますが、測定範囲の誤差が大きいので断念しました。
今回のプログラムのフローチャートみたいなものを書きました。プログラムの流れがわかると思います。

フローチャートの図

今回の変数は
ro_ko (交差点とロータリーをあわせて何回目の交差かをロボットがカウントします。7回目の交差点はカーブ終わり直後でロボットがの交差点への入射角が傾いているため個別の指定をしています。この関数のおかげでコース特性を克服できます。)
ro_tari (ロータリー内にいる間はこの関数の値が増えます。このことで何回目の出口を出るかを指定できます。)
ro_deru (この変数は二回目のロータリー出口の直後の行く手をふさぐカーブの克服に使いました。ロータリーに出会うとカウントを1増やします。)
serach_l (どのくらい左に曲がったかを示す変数です。この変数が大きいと探すスピードをあげます。)
light_b light_w (光センサの測定値を入れる変数です。)
color_w color_b (カラーセンサの測定値を入れる変数です)
です。
音を鳴らすことによりロボットが今どのプログラムを実行しているかがわかります。目の光の強さを変えることでもロボットの状態を判断できます。見た目だけじゃないよ!

#define run_sokudo1 40         //速度1の設定
#define run_sokudo2 75         //速度2の設定
#define run_sokudo3 75         //速度3の設定
#define run_sokudo4 40         //速度4の設定
#define run_sokudo5 100        //速度5の設定
int light_b;                   //光センサの黒の値
int light_w;                   //光センサの白の値
int color_w;                   //カラーセンサの白の値
int color_b;                   //カラーセンサの黒の値
#define VOL 4                  //音量4
#define oto_do4 262            //ドの音
#define oto_mi4 294            //ミの音
#define oto_so4 392            //ソの音
#define time_run 1             //一回に走る時間
int ro_ko;                     //交差点とロータリーの回数の変数を定義
int ro_tari;                   //ロータリーに入ったことを示す変数
int ro_deru;                   //何回目のロータリーか示す変数
int serach_l;                  //何回目の右ずれか示す変数
int serach_r;                  //何回目の左ずれか示す変数
sub serach_right()                 //ずれた時の修正サブルーチン(右)
{ 
       OnFwd(OUT_C,run_sokudo4);   //モーターCをrun_sokudo4で動かす。
	Wait(time_run);             //time_run/1000秒動かす
	Off(OUT_C);                 //Cを止める
}
sub serach_left()                 //ずれた時の修正サブルーチン(左)
{
	OnFwd(OUT_A,run_sokudo4);  //モーターAをrun_sokudo4で動かす。
	Wait(time_run);            //time_run/1000秒動かす
	Off(OUT_A);                //Aを止める
}
  
task main()
{
light_w=0;        //変数の初期化
light_b=0;        //変数の初期化
color_b=0;        //変数の初期化
color_w=0;        //変数の初期化
ro_tari=0;        //変数の初期化
ro_ko=0;          //変数の初期化
ro_deru=0;        //変数の初期化
serach_l=0;       //変数の初期化
serach_r=0;       //変数の初期化
SetSensorLight(IN_1);                 // ポート1に光センサ
SetSensorColorRed(IN_2);              // ポート2にカラーセンサ
PlayToneEx(oto_mi4, 4000, VOL, FALSE);//ミの音を4000/1000秒鳴らす
Wait(6000);                           //6000/1000秒待つ
light_w=Sensor(IN_1)-4;               //光センサの白の値に測定値-4を代入
color_w=Sensor(IN_2)-10;              //カラーセンサの白の値に測定値-10を代入
PlayToneEx(oto_mi4, 4000, VOL, FALSE);//ミの音を4000/1000秒鳴らす
Wait(6000);                           //6000/1000秒待つ
light_b=Sensor(IN_1)+3;               //光センサの黒の値に測定値+3を代入
color_b=Sensor(IN_2)+7;               //カラーセンサの黒の値に測定値+7を代入
PlayToneEx(oto_mi4, 4000, VOL, FALSE);//ミの音を4000/1000秒鳴らす
Wait(6000);                           //6000/1000秒待つ
PlayToneEx(oto_do4, 1000, VOL, FALSE);//ドの音を1000/1000秒鳴らす(スタート合図開始)
OnFwd(OUT_B,20);                      //目を光らせる(暗い)
Wait(1100);                           //1100/1000秒待つ
PlayToneEx(oto_do4, 1000, VOL, FALSE);//ドの音を1000/1000秒鳴らす
OnFwd(OUT_B,60);                      //目を光らせる(明るい)
Wait(1100);                           //1100/1000秒待つ
PlayToneEx(oto_do4, 1000, VOL, FALSE);//ドの音を1000/1000秒鳴らす
OnFwd(OUT_B,20);                      //目を光らせる(暗い)
Wait(1100);                           //1100/1000秒待つ
PlayToneEx(oto_so4, 2000, VOL, FALSE);//スタート ソの音を2000/1000秒鳴らす
while(true)                                                //無限ループ
   {		
	if(Sensor(IN_2)<=color_b)                            //ロータリーor交差点(カラーセンサが黒を認識)
	{
		ro_ko=ro_ko+1;                                 //ロータリーと交差点をカウント
		PlayToneEx(oto_mi4, 100, VOL, FALSE);          //ミの音を100/1000秒鳴らす
		if( ro_tari!=1 && ro_tari!=2)                  //ロータリー内でない時にカラーセンサが反応
		{
			ro_tari=0;                               //ロータリー内であることを示す変数を0に戻す。
			if(ro_ko==7)                             //コース特性克服のためのif文(7回目の交差点)
			{
				OnFwd(OUT_A,run_sokudo3);          //モーターAをrun_sokudo3で動かす。
				Wait(200);                         //200/1000秒動かす
				Off(OUT_A);                        //Aを止める		
			}
			else                                      //通常の交差点での動き
			{
				OnFwd(OUT_A,run_sokudo1);           //モーターAをrun_sokudo1で動かす。
				Wait(85);                           //85/1000秒動かす
				Off(OUT_A);                         //Aを止める
				OnFwd(OUT_AC,run_sokudo2);          //モーターACをrun_sokudo2で動かす。
				Wait(180);                          //180/1000秒動かす
				Off(OUT_AC);                        //ACを止める
			}
			if(Sensor(IN_1)<light_b+20)                //交差点
			{
				PlayToneEx(oto_do4, 500, VOL, FALSE);//ドの音を500/1000秒鳴らす
			}
			else                                       //ロータリー
			{
				ro_tari=0;                           //ロータリー内であることを示す変数を0に戻す。
				ro_tari=ro_tari+1;                   //ロータリー内であることを示す変数に1をたす。
				ro_deru=ro_deru+1;                   //何回目のロータリーか示す変数に1をたす
				PlayToneEx(oto_so4, 500, VOL, FALSE);//ソの音を500/1000秒鳴らす
				OnRev(OUT_A,run_sokudo5);            //モーターAをrun_sokudo5で動かす。
				OnFwd(OUT_C,run_sokudo5);            //モーターCをrun_sokudo5で動かす。
				Wait(340);                           //340/1000秒動かす
				Off(OUT_AC);                         //ACを止める
			}
		}
		else                                              //ロータリー内で1回目と2回目のカラーセンサの反応
		{
			if(ro_tari==1)                             //ロータリー内の1回目のカラーセンサの反応(無視)
			{
				OnFwd(OUT_AC,run_sokudo5);          //モーターACをrun_sokudo5で動かす。
				Wait(100);                          //100/1000秒動かす
				Off(OUT_AC);                        //ACを止める
				ro_tari=ro_tari+1;                  //ロータリー内であることを示す変数に1をたす。
			}
			else                                      //ロータリー内の2回目のカラーセンサの反応(ロータリーから出る)
			{
				ro_tari=0;                          //ロータリー内であることを示す変数を0に戻す。
				if(ro_deru==1)                      //コース特性克服のためのif文(1回目のロータリーを出るとき。)
				{
					OnFwd(OUT_C,run_sokudo5);    //Cをrun_sokudo5で動かす。
					Wait(400);                   //400/1000秒動かす
					Off(OUT_C);                  //Cを止める
				}
				else                               //2回目のロータリーを出るとき
				{
					OnFwd(OUT_C,run_sokudo5);   //Cをrun_sokudo5で動かす。
					Wait(1300);                 //1300/1000秒動かす
					Off(OUT_C);                 //Cを止める
				}
			}
		}
	}
	else                                                 //カラーセンサが黒と反応していない時
	{
		if (Sensor(IN_1)>light_b && Sensor(IN_1)<light_w)//黒白
		{
			serach_l=0;                             //白のずれのカウントを0に戻す。
			OnFwd(OUT_B,80);                        //目を光らせる(明るい)
			OnFwd(OUT_AC,run_sokudo5);              //モーターACをrun_sokudo5で動かす。
			Wait(20);                               //20/1000秒動かす
			Off(OUT_AC);                            //ACを止める
		}
		else                                          //線からずれている
		 {
			if(Sensor(IN_1)<light_b)                //黒
			{
					OnFwd(OUT_B,20);           //目を光らせる(暗い)
					serach_right();            //サブルーチン
			}
			else                                    //白
			{
				serach_l=serach_l+1;              //何回目の白かカウントする
				if(serach_l>800)                  //大きくずれた時Aの出力をあげる。
				 {
					OnFwd(OUT_B,80);           //目を光らせる(明るい)
					OnFwd(OUT_A,run_sokudo5);  //Aのモーターをrun_sokudo5で動かす。
					Wait(time_run);            //time_run秒走らせる
					Off(OUT_A);                //Aを止める
				 }
				else                             //少しのずれ
				 {
					OnFwd(OUT_B,20);          //目を光らせる(暗い)
					serach_left();            //サブルーチン
				 }
			}
		 }
	}
    }
}

このプログラムの問題点

  • 速さを追及するがゆえに正確さがおろそかになりがちなプログラムです。run_sokudo5を小さい値にすることで正確さは補えますが、100であることにこだわりたいと思います。1周34秒は魅力的
  • 黒 白の値を測定値にしたことで測る場所により多少の誤差が出てしまいます。スイッチを押した後に測定のためにロボットを動かす必要も出てきました。
  • コース特性を克服するためのif文のせいでこのコース以外ではうまく動きませんがif文をなくすことで直進して十字路に進入でき進行方向を妨げるカーブが存在しないコースであればクリアできると思います。
    ダメな物の図

感想・考察

中村

この課題では、苦労したことが多数ありました。その中で最たるものは、ロータリー部分です。センサ一つでロータリー侵入を感知する場合、ヘアピンカーブとの兼ね合いが問題となります。この問題はとても重く、少しの旋回範囲の差でうまく行かなくなるので、条件の差があるとすぐに失敗してしまいます。例えば、場所が変わっただけで大きく旋回範囲がずれてしまうのです。

しかしながら、この試行錯誤に於いて組み込み機器の制御の難しさとNXTの特性がより理解できたものと思います。

志村

今回の課題で私が苦労したところはロータリーと交差点交差点をどう判別するか、ロータリーに進入した後にどうやって1つ目の出口を無視して2つ目の出口で抜け出すかでした。今回私は変数を使い交差点とロータリーの数を数えることでこの問題を解消しました。一回成功しても次に失敗することもありプログラム上での修正に苦労しました。今回の課題で思ったことはNXTらしさがなかなか出すことができなかったと思います。1つ挙げるならスピードはNXTだからできたことなのかなぁとも思います。

コメント

気になったこと等ありましたらお気軽にコメントして下さい。



添付ファイル: filefuro-tya-t_s.gif 509件 [詳細] filehyousiki.gif 283件 [詳細] fileko_su.jpg 405件 [詳細] filekyasuta_r.jpg 337件 [詳細] filesensa_1.gif 318件 [詳細] filesensa_2.gif 419件 [詳細] filesensa_s.gif 345件 [詳細] fileba_r.jpg 344件 [詳細] filer_r.jpg 373件 [詳細] filesensa_r.jpg 352件 [詳細]

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2011-12-20 (火) 13:14:35 (2805d)