ヴイストン「Beauto Chaser」で学ぶプログラミング入門講座
【第10回】BeautoChaserをラジコン化してみよう
みなさんお久しぶりです。大阪工業大学ものづくりセンターの近藤です。第4回以来ですね。今回は私が記事を担当させていただきます。最終回はちょっとおもしろい小ネタをいくつか紹介していきたいと思います。
ところで、本題に入る前に一つお知らせです。オーム社よりBeauto Chaserを使ったC言語の入門書「H8マイコンによる組込みプログラミング入門―ロボットで学ぶC言語」が発売されました。本連載記事の内容をやってみた後の復習やステップアップなどに役立ちそうですので、ぜひ一読をお勧めします。
●Beauto Chaserをラジコン化
ロボット専用無線コントローラ「V-コントローラ VS-C1」。4,000円ほどで販売されている |
今まで紹介してきたプログラムはすべてロボットが自分で動くタイプのものでしたが今回はVS-C1を使って操縦できるようにしてみました。VS-C1の信号を読み取る関数が用意されていますのでその関数を使うことで簡単に受信したデータを扱うことができます。また、VS-C1のサンプルプログラムもこちらで紹介されています。
(vs-wrc003.hの引用) BYTE getPAD(BYTE num); /* b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 B1: 左 | 下 | 右 | 上 |スタート| R3 | L3 |セレクト B2: □ | × | ○ | △ | R1 | L1 | R2 | L2 AN_*X: 左:00h,中心:80h,右:FFh AN_*Y: P上:00h,中心:80h,下:FFh */ enum PADDATA{ PAD_B1, //ボタン1バイト目 PAD_B2, //ボタン2バイト目 PAD_AN_RX, //右アナログスティック左右 PAD_AN_RY, //右アナログスティック上下 PAD_AN_LX, //左アナログスティック左右 PAD_AN_LY //左アナログスティック上下 }; (引用ここまで)
getPAD関数は引数で取得したいデータを指定するようになっています。取得する前に必ずupdatePAD()関数を使い最新の入力データを取得してから使用します。B1の上ボタンの値を取得したいときはgetPAD(PAD_B1)となります。例えば十字ボタンの上を押すとビット4(B4)に1が入り1バイトのデータとしては00010000(2進数表記、16進数表記では0x10)が取得できます。取得した値をif文などで条件に使うとその値に応じた処理を行なうことができます。
ボタン入力の例
十字ボタン右:PAD_B1は00100000(0x20)
R1ボタン:PAD_B2は00001000(0x08)
プログラム例
if(getPAD(PAD_B1)==0x10){処理1} //B1の入力が00010000(0x10)の場合{処理1}を実行 else{処理2} //B1の入力が00010000(0x10)でない場合{処理2}を実行
●Beauto Chaserのラジコン化の準備
コネクタを半田付けしたVS-WRC003。コネクタ中心の金具が見える向きにはんだづけを行ないます |
VS-C1を使ってBeauto Chaserをラジコン化するにはVS-C1の受信機をVS-WRC003に取り付けるためのコネクタが必要です。VS-WRC003のゲームパッド接続コネクタにこちらで入手できるコネクタを半田付けしてください。向きを間違えないように注意しましょう。Beauto Chaserに搭載した際、コネクタ中心の金具が見える向きにはんだづけを行ないます。
●十字キーを押すとLEDを点灯させるプログラム
getPAD関数を使い十字キーの上ボタンを押すとオレンジのLEDを点灯させるプログラムを作ってみました。
【お詫びと訂正】初出時、掲載プログラムの内容に誤りがありました。現在は正しいものに訂正されております。
(led.cの書き換え箇所) *メイン関数*********************************************/ void main(void) { int ontei =0; //変数onteiの宣言 int onryou =128; //変数onryouの宣言 変数onryouに128を代入 int B1date; //変数B1dateの宣言 int B2date; //変数B1dateの宣言 const BYTE MainCycle = 60; //制御周期の設定[単位:Hz 範囲:30.0~] Init((BYTE)MainCycle); //CPUの初期設定 //ループ while(1){ updatePAD(); //最新のゲームパッドの情報を取得 B1date=getPAD(PAD_B1); //PAD_B1のデータをB1dateに読み込む B2date=getPAD(PAD_B2); //PAD_B2のデータをB2dateに読み込む if(B2date==0x80){ //丸ボタン左が押された場合{}内を実行 ontei=220; //変数onteiに220を代入 } else if(B2date==0x40){ //丸ボタン下が押された場合{}内を実行 ontei=196; //変数onteiに196を代入 } else if(B2date==0x20){ //丸ボタン右が押された場合{}内を実行 ontei=175; //変数onteiに175を代入 } else if(B2date==0x10){ //丸ボタン上が押された場合{}内を実行 ontei=165; //変数onteiに165を代入 } else if(B1date==0x80){ //十字ボタン左が押された場合{}内を実行 ontei=147; //変数onteiに147を代入 } else if(B1date==0x40){ //十字ボタン下が押された場合{}内を実行 ontei=131; //変数onteiに131を代入 } else if(B1date==0x20){ //十字ボタン右が押された場合{}内を実行 ontei=115; //変数onteiに115を代入 } else if(B1date==0x10){ //十字ボタン上が押された場合{}内を実行 ontei=110; //変数onteiに110を代入 } else{ //どの条件も満たさない場合 ontei=0; //変数onteiに0を代入 } if(ontei==0){ //変数onteiが0であれば{}内を実行 BuzzerStop(); //ブザー出力を止める } else{ //変数onteiが0でなければ{}内を実行 BuzzerSet(ontei,onryou); //ブザーの音程と音量をセット BuzzerStart(); //ブザー出力 } } } (書き換え箇所終わり)
ボタンが押されたときは該当箇所の音階データを変数onteiに代入し、onteiが0でないかをif文で調べてから音程をセットしブザーを出力します。if文の中でいちいちgetPAD()関数を読み出すのもスマートでないため、またVS-C1の入力がif文を実行している途中で変わるかもしれないことを考慮し、getPAD()関数で取得したデータを変数B1date,B2dateに読み込み、その値をif文で見ています。
少々else ifの多いプログラムになってしまいましたね。たくさんのelse ifをコンパクトにまとめたい場合は三項演算子を使うとよいでしょう。場合によりますがif文で条件分岐した場合の処理がそんなに複雑でない場合は三項演算子により記述を簡略化することができます。if文とelse文を1行で記述することができると思えばよいでしょう。
・三項演算子記述方法その1
「条件」 ? 「値A」 : 「値B」;
条件が真の場合は値A、偽の場合は値Bとなる
・三項演算子記述方法その2
「条件1」 ? 「値A」
:「条件2」 ?「値B」
:「条件3」 ? 「値C」
:「条件4」 ? 「値D」
:「値E」;
条件が条件1の場合は値A、条件2の場合は値B、条件3の場合は値C、条件4の場合は値D、どれにも該当しない場合は値Eとなる。
三項演算子を用いて「8個のボタン入力を処理するプログラム」を記述した場合
(led.cの書き換え箇所) /*メイン関数***********************************************************/ void main(void) { int ontei =0; //変数onteiを宣言 onteiにoを代入 int onryou =128; //変数onryouを宣言 onryouに128を代入 int B1date; //変数B1dateの宣言 int B2date; //変数B2dateの宣言 const BYTE MainCycle = 60; //制御周期の設定[単位:Hz 範囲:30.0~] Init((BYTE)MainCycle); //CPUの初期設定 //ループ while(1){ updatePAD(); B1date=getPAD(PAD_B1); //PAD_B1のデータをB1dateに読み込む B2date=getPAD(PAD_B2); //PAD_B2のデータをB2dateに読み込む ontei = B2date==0x80 ? 220 //B2date==0x80の場合220をonteiに代入 : B2date==0x40 ? 196 //B2date==0x40の場合196をonteiに代入 : B2date==0x20 ? 175 //B2date==0x20の場合175をonteiに代入 : B2date==0x10 ? 165 //B2date==0x10の場合165をonteiに代入 : B1date==0x80 ? 147 //B1date==0x80の場合147をonteiに代入 : B1date==0x40 ? 131 //B1date==0x40の場合131をonteiに代入 : B1date==0x20 ? 115 //B1date==0x20の場合115をonteiに代入 : B1date==0x10 ? 110 //B1date==0x10の場合110をonteiに代入 : 0; //どれにも該当しない場合0をonteiに代入 if(ontei==0){ //変数onteiが0であれば{}内を実行 BuzzerStop(); //ブザー出力を止める } else{ //変数onteiが0でなければ{}内を実行 BuzzerSet(ontei,onryou); //ブザーの音程と音量をセット BuzzerStart(); //ブザー出力 } } } (書き換え箇所終わり)
【動画】8個のボタン入力を処理するプログラム |
●Beauto Chaserを十字ボタンでラジコン化するプログラム
十字ボタン4つ、丸ボタン4つの入力を処理するプログラムを改良し、モータを制御できるようにしました。ラジコンに近づいてきましたね。
(led.cの書き換え箇所) /*メイン関数***********************************************************/ void main(void) { int B1date; //変数B1dateの宣言 const BYTE MainCycle = 60; //制御周期の設定[単位:Hz 範囲:30.0~] Init((BYTE)MainCycle); //CPUの初期設定 //ループ while(1){ updatePAD(); //最新のゲームパッドの情報を取得 B1date=getPAD(PAD_B1); //PAD_B1のデータをB1dateに読み込む if( B1date==0x10){ //十字ボタン前の入力があった場合{}内を実行 Mtr_Run(-64,64,0,0); //前進 } else if(B1date==0x80){ //十字ボタン左の入力があった場合{}内を実行 Mtr_Run(-64,0,0,0); //左曲がり } else if( B1date==0x20){ //十字ボタン右の入力があった場合{}内を実行 Mtr_Run(0,64,0,0); //右曲がり } else if( B1date==0x40){ //十字ボタン下の入力があった場合{}内を実行 Mtr_Run(0,0,0,0); //モーター回転方向変更時過負荷防止 Wait(300); //300msec待つ Mtr_Run(64,-64,0,0); //後退 while(getPAD(PAD_B1)==0x40){ //十字ボタン後の入力が終了するまでwhile文の{}内を実行し続ける updatePAD(); //最新のゲームパッドの情報を取得 } Mtr_Run(0,0,0,0); //モーター回転方向変更時過負荷防止 Wait(300); //300msec待つ } else{ //どの条件も満たさない場合は{}内を実行 Mtr_Run(0,0,0,0); //停止 } } } (書き換え箇所終わり)
【動画】Beauto Chaserを十字ボタンでラジコン化するプログラム。このプログラムの欠点は少しでも斜め(例えば十字ボタンの前と右)の入力が入ってしまうと停止命令に以降してしまうところです。どのようにプログラムを改良すればよいかは考えてみてください(笑)。ギヤボックスのギアの組わわせをDタイプからBタイプに変更し、スピードを上げています |
先ほどの「十字ボタン4つ、丸ボタン4つの入力を処理するプログラム」とほとんど同じですね。ブザーを鳴らす処理をモータを動かす処理に変更しただけです。十字ボタン後だけはモータの回転方向が逆となるため正転→逆転、逆転→正転となる際、回転方向変更時の過負荷を防がなければいけません。このため else if( B1date==0x40)の{}の処理の最初に0.3秒のモータ停止処理を入れています。同様に過負荷防止のため、十字ボタン後だけはボタンが離されるまでは while(getPAD(PAD_B1)==0x40)の{}内を実行し続け、ボタンが離された(つまりwhile文のループを抜けた)直後に0.3秒のモータ停止処理を入れてから次の動作(前進やカーブ)を実行します。
●Beauto Chaserをラジコン化し、アナログスティックを使って方向修正するプログラム
ラジコンカーといえばやはりステアリングによる滑らかなカーブでしょう。Beauto Chaserには残念ながら車のハンドルのようなステアリングが搭載されていませんが、左右のモータの速度を調整することにより、滑らかにVS-C1のアナログスティックを倒した角度をカーブに反映させることができます。以下はそのプログラムです。
(led.cの書き換え箇所) /*メイン関数***********************************************************/ void main(void) { int Speed; //変数Speedの宣言 int Speed_R; //変数Speed_Rの宣言 int Speed_L; //変数Speed_Lの宣言 const BYTE MainCycle = 60; //制御周期の設定[単位:Hz 範囲:30.0~] Init((BYTE)MainCycle); //CPUの初期設定 //ループ while(1){ updatePAD(); //最新のゲームパッドの情報を取得 //////////////////////////////////////////////////////// //(1) //アナログスティックの値を取得(0~255で変動) //(int)は強制的に値をint型にするための処理 Speed= (int)(getPAD(PAD_AN_RX)-128)/2; //上記以外の場合は(getPAD(PAD_AN_RX)-128)/2を代入 /////////////////////////////////////////////////////// //(2) if(Speed < 5 && Speed > -5){ //アナログスティックがほとんど倒されていなければ Speed= 0; //誤差として切り捨てる } /////////////////////////////////////////////////////// //(3) if(Speed>=0){ //アナログスティックが右に倒された場合{}内を実行 Speed_R=-64+Speed; //右モータのスピードをアナログスティックが倒された分だけ遅くする Speed_L=64; //左モータのスピードはそのまま } else if(Speed<=0){ //アナログスティックが左に倒された場合 Speed_R=-64; //左モータのスピードをアナログスティックが倒された分だけ遅くする Speed_L=64+Speed; //右モータのスピードはそのまま } /////////////////////////////////////////////////////// //(4) if(getPAD(PAD_B2)==0x04){ //L1ボタンに入力があった場合{}内を実行 Mtr_Run(Speed_R,Speed_L,0,0); //モータへ現在のVS-C1の入力を反映 } else{ //L1ボタンに入力がない場合は{}内を実行 Mtr_Run(0,0,0,0); //モータを停止 } } } (書き換え箇所終わり)
【動画】Beauto Chaserをラジコン化し、アナログスティックを使って方向修正するプログラム |
少々ややこしくなってしまいましたね。プログラムを四段階に分けて説明していきたいと思います。
(1)はアナログスティックの値を読み込む処理です。アナログスティックの値は0~255の数値で変動します。今回は右のアナログスティックの左右のデータを使うため左いっぱいに倒すと0、真ん中で128、右いっぱいに倒して255の値が取得できます。しかし今回はスティックが真ん中からどれだけ倒れているかを測りたい(つまり真ん中の値である128からどれだけ差があるか)ので128で減算しています。さらにそのままの数値では値が大きすぎて、少しだけカーブしたいときに不便なため値を2で割っています。
(2)ではアナログスティックがほとんど倒されていない場合(つまりアナログスティックの値が118~138の場合)は強制的に変数Speedに読み込んだデータを捨てます。
またVS-C1の個体差や定年変化でアナログスティックを倒していない場合でも128にならない場合があります。例えばアナログスティックを倒していない場合の値が134だった場合、差分の6を反映してしまい、まっすぐ進みたいのに少しずつ片方に曲がってしまいます。誤差を読み込まないためにもこの処理が必要となります。
(3)では取得した値をモータ出力に反映させる準備を行なうための処理です。アナログスティックを左右どちらに倒したかについてはSpeedの値が0以上か0以下かを見ることにより判断できます。曲がりたい方向のモータのスピードをアナログスティックが倒された分だけ落とすことにより、倒した分だけ曲がることができます。
(4)車もハンドルを切っただけでは進みません。アクセルが必要です。VS-C1のL1ボタンが押されているときだけモータに(1)~(3)の過程で割り出したスピードのデータを反映させています。
●Beauto BalancerのC言語プログラム、はじめの一歩
さて、今度はBeatuo Balancerの小ネタを紹介したいと思います。Beauto Balancerは、Beauto Chaserと同じCPUボードVS-WRC003が使われていますので、C言語の開発環境もそのまま使うことができます。Beauto Chaserと同じくヴイストンから発売されている倒立振子制御学習キットです。本誌でも以前紹介しています。
Beauto Balancer | 【動画】Beauto Balancerの動作の様子 |
倒立させるのに高度な制御が必要なため、制御の仕組みや制御工学の勉強に役立ちそうです。Beauto Balancerの状態方程式を解いてそれをC言語で適用し、結果を考察するという大学の卒業研究のネタになりそうですね。
自分でプログラムをすべて作ってBeauto Balancerを立たせてみたいところですが、とても難しいので、今回はBeauto Balancerの隠しモードである赤外線センサー搭載モード、PSDセンサー搭載モードを紹介したいと思います。Beauto BalancerのC言語プログラムの解読や改造にチャレンジしてみたい方のはじめの一歩に役立てばいいなと思っています。
Beauto Balancerは出荷時の状態では、直立するためのプログラムが入っています。また、こちらの「Beauto プログラマ」を使えば前後に移動しながら直立するモードに切り替えることができます。しかしこちらの動画では赤外線センサーを使って黒いテープの間を往復したり、距離センサー(PSDセンサー)を使って障害物から離れるなどが紹介されていますが、2009年6月現在のところ「Beautoプログラマ」ではこれらのモードには変更できないようです。
●サンプルプログラムに隠された赤外線センサーモード、距離センサー(PSDセンサー)モードを探せ
こちらの動画で赤外線センサーモード、距離センサーモードが紹介されているので、サンプルプログラムの中にそれっぽいモードの記述がないかくまなく探してみると……ありました!
Beauto BalancerはC言語のサンプルプログラムがすでにこちらで公開されていますのでまずはこちらをダウンロードしてください。このサンプルプログラムをHEWから開き、2weels.cの193行目を見ると「//前後移動のための処理(zをずらす)」と書いてあります。続いて220行目には「//ライン間移動モード ※要拡張パーツ」、258行目には「//PSDモード ※要拡張パーツ」と書いてあります。まさしくこれなのではないでしょうか。
●プログラムを解読してみよう
解読といっても難しいことではありません、必要なところだけ見ていきましょう。if文の記述を見ると「//前後移動のための処理(zをずらす)」が実行されるための条件は<memmap[RUN_MODE]==1>です。つまりmemmap[RUN_MODE]という変数が1だと「//前後移動のための処理(zをずらす)」が実行されることになります。変数が2だと「//ライン間移動モード ※要拡張パーツ」、変数が3だと「//PSDモード ※要拡張パーツ」が実行されるようですね。ということはif文で条件分岐を行なう前の行(194行目)にmemmap[RUN_MODE]=0のように直接0から3の好きなモードの数字を代入してしまえば「/ライン間移動モード ※要拡張パーツ」「//PSDモード ※要拡張パーツ」モードを選択できるのではないでしょうか?
ためしに194行目にmemmap[RUN_MODE]=1;を追加し、ビルド後Beauto Balancerに書き込み実行してみると……見事「//前後移動のための処理(zをずらす)」モードを実行することができました!なんだかしてやったりという気分ですね(笑)。
memmap[RUN_MODE]=1を追加し、強制的に「//前後移動のための処理(zをずらす)」モードを選ばせる。 | 【動画】「//前後移動のための処理(zをずらす)」モードの様子。ブザーが鳴るたびに進む方向を変更しているのがわかります。ちなみにmemmap[RUN_MODE]=0にすると通常の直立モードになります |
●「/ライン間移動モード ※要拡張パーツ」モードにチャレンジ
それでは実際に赤外線センサを取り付けて「/ライン間移動モード ※要拡張パーツ」モードを試してみましょう。
226行目を見るとif(AdRead(1) > 750){という記述があります。AdRead関数については第8回の記事をご確認下さい。引数を1にしているということから、CN5/AN2のアナログ入力ポートから値を読み込んでいることがわかります。赤外線センサーのコネクタをCN5/AN2に差しこみ、赤外線センサー本体はBeauto Balancerの側面に厚めの両面テープで固定しましょう。「//ライン間移動モード ※要拡張パーツ」モードを実行するために2weels.cの194行目に先ほど追加したmemmap[RUN_MODE]=1をmemmap[RUN_MODE]=2に変更し、ビルド後書き込み、実行してみましょう。
赤外線センサー本体はBeauto Balancerの側面に厚めの両面テープで貼り付ける。 | 赤外線センサーのコネクタをCN5/AN2 |
.bmp AdRead関数の引数を手がかりに赤外線センサーの値を読み込む入力ポートを割り出します。 | .bmp memmap[RUN_MODE]を2に変更します。 |
【動画】「/ライン間移動モード ※要拡張パーツ」モードの動作テスト |
少々暴れていますが、モード変更と赤外線センサーの動作はうまくいったようですね。あとは各種ゲインの調整次第でうまく暴れずに動いてくれると思います。「//PSDモード ※要拡張パーツ」もぜひチャレンジしてみてください。
●最後に
全10回の本連載では、これからロボットやプログラミングに挑戦したいと考えている中高生、大学生の皆様の第一歩のきっかけになることをコンセプトに連載を行なってきました。みなさんいかがでしたか?「これなら自分でもできるっ!」と思いませんか?
実は私もC言語のプログラムについては初心者です。学生の頃はPICマイコンという少々特殊なマイコンしか扱ったことがなく、H8マイコン、C言語でのマイコン制御は今回のBeauto Chaserが初めてです。C言語でのマイコンプログラミングをやってみた私の率直な感想は、「こんな簡単な記述でロボットの制御できてしまうのか! とても楽しいっ!」です。
進路に悩んでいる中高生、大学生のみなさん、ものづくりやプログラミングに興味があれば、今回の連載をきっかけに、ちょっとだけ技術者への扉を開いてみてはいかがでしょうか? まずはチャレンジです! 習うより慣れろっ! です。
最後になりましたが、ここまでお付き合いいただき本当にありがとうございました。また、今回の記事を執筆するにあたりImpress Watch株式会社、ヴイストン株式会社、大阪工業大学の皆様に多大なご支援、ご協力をいただきました。この場を借りて心よりお礼申し上げます。
あと学生のimokenpi君おつかれ(笑)!
それでは皆さん、またどこかでお会いしましょう!
連載も終わったので、週末はBeauto Chaserで液晶ディスプレイの制御にチャレンジするぞ(笑)。
2009/7/1 00:00