新入社員の中村です。本記事では、3週間に渡って行われたマイクロマウス新人研修について報告します。弊社では、すべての新入社員がPi:Coと呼ばれるロボットを製作するマイクロマウス新人研修を受けます。Pi:Coは、マイクロマウスという競技で使用される初心者向けロボットです。初心者向けに解説書やサンプルプログラムが付属しているため、誰でも簡単にロボットのプログラミングを学ぶことができます。マイクロマウス新人研修では、このPi:Coを使ったプログラミングを通して、ロボットの基礎技術を学び、ロボットへの愛着を深めます。今年度のマイクロマウス新人研修は例年と異なり、3週間という長い期間で実施されました。Pi:Coを製作して走行させるだけでなく、スラローム走行や斜め走行と呼ばれる滑らかな走行を追加するなど、充実した研修となりました。本記事では、マイクロマウス新人研修の実施内容と研修中に実装した斜め走行の技術について報告します。
Pi:Coとマイクロマウス競技
Pi:Coは、弊社が開発・販売する初心者向けの小型ロボットです。いくつかのバージョンが存在し、最新のものはPi:Co Classic 3として販売されています。Pi:Co Classic 3は組み立てキットで、組み立てを通してロボットの構造や電子回路について学ぶことができます。また、Pi:Coはプログラミング可能で、初心者向けの解説書やサンプルプログラムが付属しているため、ロボットプログラミングについても学ぶことができます。マイクロマウス競技の入門機として製作されたため、本製品を使用して実際に競技に参加することができます。競技参加を通して、より高度なロボット技術やプログラミング技術を習得することができます。
マイクロマウス競技は、手のひらサイズのロボットが迷路のゴールからスタートに到達するまでの時間を競う競技です。マイクロマウスは、人の操縦なしに自律的に迷路を走行します。ゴールに到達するためには、ハードウェアの知識や制御技術、アルゴリズムに関する知識など幅広い知識が必要となります。したがって、マイクロマウス競技に参加してマイクロマウスをゴールに到達させることを目指せば、そのような幅広い知識を一通り学ぶことができます。また、マイクロマウスをゴールに到達させることができたら、その知識が身についた証になります。
マイクロマウス新人研修
マイクロマウス新人研修では、Pi:Coの製作とプログラミングを通して、ロボットの基礎技術を習得します。第1週では、Pi:Coを組み立てて、サンプルプログラムを実行し、Pi:Coが正常に動作することを確認します。また、16×16の迷路を完走することを目指します。第2週では、角を止まらずに曲がるスラローム走行を実装します。第3週では、独自に設定した斜め走行の実装という課題に取り組みます。
第1週:Pi:Coの製作とプログラミング
第1週では、Pi:Coを組み立て、正常に動作することを確認します。
まずは、基板に部品をはんだ付けします。はんだ付けに慣れていない人は、焦らずゆっくりと丁寧に行いましょう。
次に、ベースに基板やモータを取り付けてPi:Coを組み立てます。
完成したら、サンプルプログラムを書き込んで実行します。サンプルプログラムは、機能ごとに複数のステップに分かれており、1つ1つの機能を確認しながら進めることができます。最後のステップでは、マイクロマウスが迷路を走行することができるようになります。
さて、サンプルプログラムの実行が終わったら、16×16の迷路を完走することを目指します。迷路を完走するためには、車輪直径やトレッド幅、壁センサの値など、プログラム内のパラメータを調整することが重要です。メジャーなどを使用して、どれだけ目標の位置からずれているか計測し、非常に高い精度で位置制御することができるように調整します。
本研修では、APEC2024という大会で使用された迷路を使用しました。調整がしっかり行われていれば、迷路を完走することができます。また、加速度や最高速度など内部の走行パラメータを調整することで、さらに走行を高速にすることができます。回転速度を増加させることと回転開始までの待ち時間を短縮することによって、角を曲がる時間が短縮され、全体として大きく時間を短縮することができます。
第2週:スラローム走行実装
第2週目は、スラローム走行を実装します。スラローム走行では、角を止まらずに曲がります。スラローム走行を実現するためには、並進速度を一定に保ちながら、回転速度を変化させる必要があります。
最も単純な実装は、等速円運動をするようにモータの回転数を瞬間的に切り替えるものです。並進速度と回転中心がわかれば、左右の車輪の速度が求まります。プログラムの実装では、求まった速度に切り替えるだけで済みます。しかし、瞬間的に車輪の速度が切り替わるため、脱調というモータの速度が設定どおりにならない現象が発生します。
そこで、機体の回転速度が段階的に切り替わるように実装します。ある一定角度曲がるまで回転速度を増加させ、目標の角度に近くなったら回転速度を減少させることを考えます。このときの軌道を計算することは簡単ではありません。したがって、シミュレータで軌道を計算する必要があります。本研修では、Googleスプレッドシートを用いてシミュレータを作成しました。1msごとに姿勢と速度がどのように変化するかを求めます。Googleスプレッドシートでの実装例を以下に示します。
位置をグラフにプロットすると次の図のようになります。
シミュレータを用いることで、角を曲がるために必要な角加速度と加減速の時間がわかります。この情報を用いたスラローム走行の実装例を以下に示します。
// 1ms毎の割り込み処理 void int_cmt0(void) { // 割り込み処理の回数を記録して、時間を計測する if (time_based_control) { control_time++; } // 車輪の速度を表す変数 float spd_r, spd_l; //加速処理 speed+=r_accel; angular_speed+=angular_accel*0.001; //最高速度を制限 if(speed > max_speed) { speed = max_speed; } //最低速度を制限(MTUの周期設定レジスタの値が~0xffffまでであるため) if(speed < min_speed){ speed = min_speed; } //センサ制御 if(con_wall.enable == true) { //壁制御が許可されている場合 //過去の偏差を保存 con_wall.p_error = con_wall.error; //姿勢制御の偏差を計算 if( ( sen_r.is_control == true ) && ( sen_l.is_control == true ) ) { //両方とも有効だった場合の偏差を計算 con_wall.error = sen_r.error - sen_l.error; } else { //片方もしくは両方のセンサが無効だった場合の偏差を計算 //片方しか使用しないので2倍する con_wall.error = 2.0 * (sen_r.error - sen_l.error); } //DI制御計算 con_wall.diff = (con_wall.error - con_wall.p_error) / 0.001; con_wall.sum += con_wall.error; //偏差の積分値を制限 if(con_wall.sum > con_wall.sum_max) { con_wall.sum = con_wall.sum_max; } else if(con_wall.sum < (-con_wall.sum_max)) { con_wall.sum = -con_wall.sum_max; } // 制御量を計算 con_wall.control = 0.001 * speed * (con_wall.kp * con_wall.error + con_wall.kd * con_wall.diff); spd_r = speed + con_wall.control; spd_l = speed - con_wall.control; }else{ // 旋回時、並進速度と角速度から車輪速度を求める spd_r = speed + (angular_speed * TREAD_WIDTH / 2) ; spd_l = speed - (angular_speed * TREAD_WIDTH / 2); } if (!physical_based) { if(spd_r < MIN_SPEED)spd_r = MIN_SPEED; if(spd_l < MIN_SPEED)spd_l = MIN_SPEED; } if (physical_based) { if (spd_r >= 0) { MOT_CWCCW_R = MOT_FORWARD; } else { MOT_CWCCW_R = MOT_BACK; } if (spd_l >= 0) { MOT_CWCCW_L = MOT_FORWARD; } else { MOT_CWCCW_L = MOT_BACK; } } MTU3.TGRC = SPEED2GREG(ABS(spd_r)); MTU4.TGRC = SPEED2GREG(ABS(spd_l)); } // スラローム走行 void slalom(float center_speed, float angular_acceleration, int time_acc, int time_total) { // グローバル変数を設定 physical_based = true; con_wall.enable = false; r_accel = 0; angular_speed = 0; angular_accel = angular_acceleration; max_speed = MAX_SPEED; min_speed = MIN_SPEED; speed = center_speed; step_r = step_l = 0; control_time = 0; time_based_control = true; MTU.TSTR.BIT.CST3 = MTU.TSTR.BIT.CST4 = 1; // 等速円運動開始まで待つ while(control_time < time_acc); angular_accel = 0; // 減速開始まで待つ while(control_time < time_total - time_acc); angular_accel = - angular_acceleration; // 旋回終了まで待つ while(control_time < time_total); angular_accel = 0; angular_speed = 0; physical_based = false; time_based_control = false; MTU.TSTR.BIT.CST3 = MTU.TSTR.BIT.CST4 = 0; }
実際の軌道はシミュレーションと異なる可能性があるため、回転角度に要する時間や前後の走行距離を調整する必要があります。調整をしっかり行うと、以下の動画のようなスラローム走行をすることができ、走行時間を大幅に削減することができます。
第3週:斜め走行実装
斜め走行実装では、45度・135度・大回りの90度・180度のスラロームを実装します。次のようなコースでは、90度のスラロームを繰り返すよりも、斜めに走行したほうが走行時間を短縮することができます。
上の図は45度のスラロームの組み合わせです。
上の図は135度のスラロームの組み合わせです。
上の図は135度と45度のスラロームの組み合わせです。
上の図は大回りの90度スラロームです。直進・旋回・直進となるコースでは、直進の区画からスラロームを開始することで、旋回半径を大きくすることができ、速度の上限を向上させることができます。
上の図は180度スラロームです。90度のスラロームを組み合わせるよりも、旋回半径を大きくとることができ、角速度の加速時間を長くとることができるため、早い速度で旋回することができます。
上の図はV90と呼ばれるスラロームです。斜め走行中に90度スラロームします。
図から分かるように、直進や180度スラロームでは区画の中心を端点にし、斜め走行では区画の境界を端点にします。
135度などのスラロームは、90度スラロームの関数の引数を変えるだけで実現することができます。第2週で作成したスラロームのシミュレータを用いて、次の値を求めます。
- 並進速度
- 角加速度
- 加減速時間
- 旋回終了時間
- スラローム旋回前後の直進距離
これらをパラメータをスラロームの関数に引数として渡せば、90度以外の角度のスラローム走行をすることができます。しかし、ここでは事前に決められた走行を行うだけで、迷路内を走行することはできません。迷路内を斜め走行するためには、ゴールまでのルートを求め、適切な角度のスラローム走行を組み合わせる必要があります。
そこで、最短経路を導出して、スラローム走行の組み合わせを求める機能を実装します。
まず、ゴールするまでの一連の動作を求める関数を作成します。サンプルプログラムのfast_run関数を参考にして、最短経路を求めます。fast_run関数で呼ばれるstraight関数やrotate関数を削除し、get_nextdir関数で取得される移動方向をグローバル変数である配列に保存します。配列には、front・left・rightという各区画での移動方向が保存されます。
t_local_dir dirs[256]; int dirs_size; // 最短経路を求める関数 void get_route(int gx, int gy) { t_position tmp = mypos; t_direction glob_nextdir; int count = 0; while ((mypos.x != gx) || (mypos.y != gy)) { switch (get_nextdir(gx, gy, MASK_SECOND, &glob_nextdir)) { // 移動方向を配列に保存する case front: dirs[count] = front; break; case right: dirs[count] = right; break; case left: dirs[count] = left; break; } // 位置姿勢を更新する mypos.dir = glob_nextdir; switch (mypos.dir) { case north: mypos.y++; break; case east: mypos.x++; break; case south: mypos.y--; break; case west: mypos.x--; break; } count++; } mypos = tmp; dirs_size = count; }
次に、一連の移動方向から斜め走行を含むスラローム走行の組み合わせを求める関数を作成します。次のような特定の移動の組み合わせがあれば、45度等のスラローム走行を行うことができます。
- 直進・右折・左折→右45度スラローム
- 直進・左折・右折→左45度スラローム
- 直進・右折・直進→大回りの右90度スラローム
- 直進・左折・直進→大回りの左90度スラローム
- 直進・右折・右折・左折→右135度スラローム
- 直進・左折・左折・右折→左135度スラローム
- 直進・右折・右折・直進→右180度スラローム
- 直進・左折・左折・直進→左180度スラローム
- 右折・直進→右45度スラローム
- 左折・直進→左45度スラローム
- 右折・右折・直進→右135度スラローム
- 左折・左折・直進→左135度スラローム
- 右折・右折・左折→右V90度スラローム
- 左折・左折・右折→左V90度スラローム
- 右折・左折→斜め直進
- 左折・右折→斜め直進
各組み合わせを条件文で検出して、スラロームと直進の順番をグローバル変数である配列に保存します。
void compress_diagonal_route() { int index = 0; float count = 0; dirs[dirs_size] = front; dirs_size++; for (int i = 0; i < dirs_size; i++) { switch (dirs[i]) { case front: // 右180度 if (i >= 1 && i + 3 < dirs_size && dirs[i + 1] == right && dirs[i + 2] == right && dirs[i + 3] == front) { compressed_dirs[index] = right180; i = i + 2; break; } // 左180度 if (i >= 1 && i + 3 < dirs_size && dirs[i + 1] == left && dirs[i + 2] == left && dirs[i + 3] == front) { compressed_dirs[index] = left180; i = i + 2; break; } // 右90度 if (i >= 1 && i + 2 < dirs_size && dirs[i + 1] == right && dirs[i + 2] == front) { compressed_dirs[index] = right90; i = i + 1; break; } // 左90度 if (i >= 1 && i + 2 < dirs_size && dirs[i + 1] == left && dirs[i + 2] == front) { compressed_dirs[index] = left90; i = i + 1; break; } // 左45度 if (i >= 1 && i + 2 < dirs_size && dirs[i + 1] == left && dirs[i + 2] == right) { compressed_dirs[index] = left45; i = i + 1; break; } // 右45度 if (i >= 1 && i + 2 < dirs_size && dirs[i + 1] == right && dirs[i + 2] == left) { compressed_dirs[index] = right45; i = i + 1; break; } // 左135度 if (i >= 1 && i + 3 < dirs_size && dirs[i + 1] == left && dirs[i + 2] == left && dirs[i + 3] == right ) { compressed_dirs[index] = left135; i = i + 2; break; } // 右135度 if (i >= 1 && i + 3 < dirs_size && dirs[i + 1] == right && dirs[i + 2] == right && dirs[i + 3] == left ) { compressed_dirs[index] = right135; i = i + 2; break; } // 直進 count = 0; while (i + 1 < dirs_size) { if (dirs[i + 1] == front) { count++; i++; } else{ i--; break; } } compressed_dirs[index] = front; compressed_dirs[++index] = count; break; case right: // 斜め走行時に直進 if (i > 1 && i + 1 < dirs_size && dirs[i + 1] == left) { int count = 0; while(1) { if (i + 1 < dirs_size && dirs[i] == right && dirs[i + 1] == left) { count++; i++; } else if( i + 1 < dirs_size && dirs[i] == left && dirs[i + 1] == right ) { count++; i++; } else { i--; break; } } compressed_dirs[index] = DIAGONAL; compressed_dirs[++index] = count; } // 斜め走行時に右45度 if (i > 1 && i + 1 < dirs_size && dirs[i + 1] == front ) { compressed_dirs[index] = right45; break; } // 斜め走行時に右135度 if (i > 1 && i + 2 < dirs_size && dirs[i + 1] == right && dirs[i + 2] == front) { compressed_dirs[index] = right135; i = i + 1; break; } // 斜め走行時に右V90度 if (i > 1 && i + 2 < dirs_size && dirs[i + 1] == right && dirs[i + 2] == left) { compressed_dirs[index] = rv90; i++; break; } break; case left: // 斜め走行時に直進 if (i > 1 && i + 1 < dirs_size && dirs[i + 1] == right) { int count = 0; while(1) { if (i + 1 < dirs_size && dirs[i] == right && dirs[i + 1] == left) { count++; i++; } else if( i + 1 < dirs_size && dirs[i] == left && dirs[i + 1] == right ) { count++; i++; } else { i--; break; } } compressed_dirs[index] = DIAGONAL; compressed_dirs[++index] = count; } // 斜め走行時に左45度 if (i > 1 && i + 1 < dirs_size && dirs[i + 1] == front ) { compressed_dirs[index] = left45; break; } // 斜め走行時に左135度 if (i > 1 && i + 2 < dirs_size && dirs[i + 1] == left && dirs[i + 2] == front) { compressed_dirs[index] = left135; i = i + 1; break; } // 斜め走行時に左V90度 if (i > 1 && i + 2 < dirs_size && dirs[i + 1] == left && dirs[i + 2] == right) { compressed_dirs[index] = lv90; i++; break; } break; } index++; } compressed_dirs_size = index; }
求まったスラロームと直進を順番に実行すれば、迷路を走行することができます。スラロームごとに並進速度が異なる場合は、スラローム前後の直進で加減速を行う必要があります。ここで、走行を表す列挙体の配列だけを使って前後の走行のパラメータを参照することは難しいです。なぜなら、45度などの斜めになるスラロームに関しては、斜めの直線に入る場合とそれから出る場合ではスラローム前後の直進距離が入れ替わるからです。複雑な条件分岐があるために最短走行の関数は300行になってしまい、関数全体を把握するのが難しくなりました。走行を表す列挙体の配列ではなく、走行パラメータをまとめた構造体の配列を作ると最短走行の実装が簡単になります。
斜め走行を実装した結果を下の動画に示します。斜め走行を行うことで、90度旋回を組み合わせるよりも速度を向上させることができ、走行時間を短縮することができます。また、斜め走行を含めた滑らかな走行が見られると、マイクロマウスがより知的に見えます。
斜め走行には、以下のような複数の難しい点が存在します。
- 角加速度・加減速時間・旋回前後の直進距離をすべての旋回について調整する必要があるため、時間がかかります。
- 各旋回で並進速度が異なる場合は、旋回後の直線で加減速を行う必要があります。
- 斜め走行の直線では壁がないため、柱を使って姿勢制御する必要があります。
これらに対応するためには、段階的な目標を設定すると良いです。例えば、スラローム走行の種類を限定したり、旋回での並進速度を同じにしたりすることで問題が簡単になります。このような段階的な目標を設定することは、マイクロマウスに限らず、ある目標を達成するためには有効な手段です。
成果報告会と16×16迷路の斜め走行
マイクロマウス新人研修の最終日前日に成果報告会を行いました。3週間の研修内容の発表と4×6迷路での斜め走行のデモを披露しました。成果報告会では以下のような質疑応答を行いました。
- ドキュメントを拡張するならどうしたいですか?
斜め走行に関するヒントを加えても良いかもしれません。サンプルプログラムのコードをもう少し整理したほうが読みやすいと思います。 - ハードウェアの違いで何か影響はありましたか?
タイヤのずれの影響は大きかったと思います。ホイールとタイヤの間に両面テープを貼ることで安定しました。 - スラロームを調整するときに角速度を指定するのか?
最大角速度の代わりに加減速する角度を指定します。例えば20度まで加速して、70度で減速するようにします。最大角速度は、シミュレーションの結果として得られます。 - DCモータを使用したマイクロマウスの製作経験がありますが、それと比べて今回の斜め実装で難しかった点は何ですか?
ステッピングモータの最大トルクが小さいためパラメータを下げないと脱調して動作しないことに気づくのが遅れました。DCモータとステッピングモータの感覚の違いを感じました。
研修最終日は、自由時間が設けられていました。そこで、16×16迷路の斜め走行に挑戦しました。その様子を以下の動画に示します。
16×16迷路で斜め走行を行うことができました。もう少し改良すれば、大会に出ることができると思います。
おわりに
本記事では、マイクロマウス新人研修の実施内容について報告しました。まず、第1週ではPi:Coを組み立て、動作確認を行いました。さらにサンプルプログラムを用いて、16×16迷路を走行させました。第2週では、スラローム走行を実装し、16×16迷路を走行させました。第3週では、第2週のスラローム走行を拡張し、斜め走行を実装しました。第1週と第2週を通して、ロボットの基礎技術を学習し、第3週では挑戦的な斜め走行の課題に取り組み自分の能力を高めることができました。非常に充実した研修となりました。
Pi:Co Classic 3は、初心者用のマイクロマウス入門機で、誰でもロボットの基礎技術を楽しく学ぶことができます。中級者は、斜め走行などの難しい課題に挑戦して、より技術を高めることができます。読者のみなさんも研修や独習にぜひ使ってみてください。