自律型ロボットを作ろう!

〜 マインドストームを使ったロボット製作の体験教室 〜
松本成司
matsu@johnen.shinshu-u.ac.jp

初版 2012-08-01
最終更新 2012-08-13

印刷用のPDFファイルも用意しました。

目次

  1. この文書について
  2. マインドストームで簡単ロボット製作
  3. まずは簡単なロボットを組み立てよう
  4. プログラムを作ってロボットを動かそう
  5. プログラムをかっこよくスリムに
  6. ロボットに触覚をつけよう
  7. 時間を測ろう
  8. 音を鳴らそう
  9. ロボットに目をつけよう
  10. 一連の動作を部品化しよう
  11. 2台のロボットで通信しよう
  12. ディスプレイに文字を表示しよう
  13. さらにNXTではこんなこともできる
  14. オリジナルのロボットへ向けて
  15. 参考文献
カバー写真

この文書について

この文書は、2012年8月4〜6日に松本秀峰中等教育学校において開催された ロボティクス入門講座のための補助資料を加筆・修正したものです。

この講座では、LEGO社のマインドストームというロボットのキットを使用して ロボットやプログラムの製作体験を行います。 用意できるキット数の関係で、 旧モデルのマインドストーム・ロボティクス・インベンション・システム (RIS) を使うグループと、 新モデルのマインドストームNXT を使うグループに別れて作業しますが、 使用するキットやプログラミング言語が違っても ほぼ同じ内容で学習できるように配慮して作成されています。

クリエイティブ・コモンズ・ライセンス (CCライセンス) を利用することで、 作者は著作権を保持したまま作品を自由に流通させることができ、 受け手は許諾条件の範囲内で再配布やリミックスなどをすることができます。 この文書で採用しているライセンス(CC BY-SA 3.0) は、簡単に表現すると 『原著作者のクレジット表示』と『二次的な著作物は同一のライセンスで頒布』 という条件のもとで、商業利用も含めて自由に複製、頒布、改変することができます。

この講座で使用するプログラミング環境のNQCを開発して フリーソフトウェアとして配布してくれたDave Baumさん、 NQCの開発を引き継ぎ、 さらにはNBC/NXCを開発・公開してくれたJohn Hansenさんに感謝します。 また、この資料を作るにあたって非常に多くの書籍やウェブ上の資料を 参考にさせていただきました。 あらためてこれらの製作者の方々に感謝します。 そして、このような講座を担当する貴重な機会を与えてくださった 松本秀峰中等教育学校の瀬川伸先生に感謝します。

この文書は 『クリエイティブ・コモンズ 表示 - 継承 3.0 非移植 ライセンス』 のもとで提供しますので、自由にコピーや配布をしていただいてかまいません。 ただし、(おそらくたくさんあると思われる)間違いにお気づきの方は ご連絡いただけると幸いです。コメントも歓迎します。 また、3日間の集中講座で行う内容を優先して説明してありますので、 取り挙げられなかった内容の説明については、まだ作成途中の箇所が多いです。

LEGO (レゴ)、Mindstorms (マインドストーム) は LEGO Group (レゴグループ) の登録商標です。 この文書はレゴグループやその日本法人と一切関係はありません。

マインドストームで簡単ロボット製作

自律型のロボットって何?

自動的にネットを巡回するコンピュータ・プログラムなどのことも ロボットと呼びます。 略してボット(bot)とも言いますが、最近では自動的に迷惑メールを送信したり 個人情報を盗み出したりする悪意のあるプログラムのことをボットと 呼ぶことが多くなりました。

一般に、人間の代わりにいろいろな作業をしてくれる機械や さまざまな動物の形や動きを真似て動作する機械をロボットと呼びます。 後者は、昔から日本のアニメや特撮でよく登場しているので日本では 特に馴染み深いですね。 また実際に動くロボットとして、 ホンダのASIMOやソニーのAIBOを思い浮かべる人も多いでしょう。 今日では、産業用ロボット、医療ロボット、レスキューロボット、 水中探査ロボット、お掃除ロボット、軍事ロボットなどさまざまな種類のロボットが 世界中で開発されています。

これらのロボットの中には人間が遠隔で操作するものもありますが、 この講座で製作するのは、人間が操作しなくても 自分自身で状況を判断して動作することのできるロボットです。 このような、自分自身を律することのできるロボット、 自分自身をコントロールできるロボットのことを 『自律ロボット』と呼びます。 ちなみに『自立ロボット』と書く場合も、 単に自分で立てるという意味ではなく、 『自律ロボット』と同じ意味で使うことが多いようです。

このプログラムのことをソフトウェア(または簡単にソフト)と呼んでもかまいません。 が、この文書では、 パソコン用に開発され一般に配布されている、または販売されている コンピュータ・プログラムのことをソフトウェアと呼んで、 ロボットを動かすためのプログラムと一応区別しています。 しかし、これは正しい使い分けではありません。 実際、NXTの液晶メニューにはユーザが作ったプログラムも 「Software files」として表現されています。 一般には、 コンピュータ本体や機械本体をハードウェアと呼び、 これらを動かすためのコンピュータ・プログラムや関連文書、 さらにはユーザ教育などの無形物のことをソフトウェアと呼びます。

さて、そうは言っても「どういう場合にどういう動きをさせるか」 というのは、あらかじめ人間がロボットに教えてあげる必要があります。 間違ったことを教えれば間違った動きをするので、 ロボットを動かす手順をあらかじめよく練っておくことが大切です。 この手順、つまりロボットを動かす命令をまとめて 記述したものを『制御プログラム』、 または単に『プログラム』と呼びます。

マインドストームって何?

昔は、個人で行うロボット製作と言えば、 必要な部品を自分で選び、探しまわって集め、 売られていない部品は自分で作る、というのが一般的でした。 そしてそのための知識もそれなりに必要でした。 また学校で使う教材についても、簡単に組み立てができて プログラムも自作できるというキットはほとんどありませんでした。

RCXと呼ばれるのバッテリボックスを兼ねたRISのコントローラには、 日立(現ルネサステクノロジ)製のH8/300という16MHzで駆動 8ビットのマイクロプロセッサが搭載されています。 一方、NXTには 48MHzで駆動する32ビットのARM7マイクロプロセッサが搭載され、 モータもサーボモータになりました。

ところが、1998年に玩具のブロックで有名なレゴ社 (LEGO社) が Mindstorms Robotics Invention System (マインドストーム・ロボティクス・インベンション・システム、略してRIS) とよばれる教育用の安価なロボットのキットを発売しました。 これはLEGO社がアメリカのマサチューセッツ工科大学(MIT)と共同開発したもので、 キットにはロボットの頭脳に当たるコンピュータをはじめ、 明るさを測る光センサ、触れたかどうか判定するタッチセンサ、モータ、 各種ギヤ類、タイヤ、従来のブロックなど700個以上の部品が含まれています。 もちろんLEGOの他のキットとも組み合わせて使うこともできます。 この安価なキットだけでオリジナルのロボットを作ることができ、 自作プログラムで自由に動かせるという点で この製品は非常に画期的なものでした。 ちなみにインベンションとは創作とか発明という意味です。

4台のspybotics Robotics Invention System 2.0 教育用NXT

実際にこの講座で使うキットは2001年に発売された、RIS 2.0です。 1.0と書かれたRCXもありますが、ファームウェアはすべて 2.0用のもの にアップグレード済で、RCX2.0と変わりありません。

RIS以外にも スカウトと呼ばれるコントローラを含んだ Robotics Discovery Set (RDS)、 マイクロスカウトと呼ばれるコントローラを含んだ Droid Development Kit (DDK) や Dark Side Developer Kit (DSDK) なども発売され、 さらに2002年には モータやセンサが一体になったSPCと呼ばれるコントローラを含んだ 4種類の Spybotics (スパイボティクス) のキットも発売になりました。

そして2006年には、RISの後継となる Mindstorms NXTが発売になりました。 コントローラの性能やモータの性能が大幅に向上し、 超音波センサや音センサなども追加されました。

これらのキットに含まれる部品の組み合わせ方は事実上無限通りで、 工夫しながらさまざまな目的の独自ロボットを簡単に作ることができます。 ちなみに箱に記載されているキットの対象年齢はRISが12歳以上、 NXTが8歳以上となっています。

NXTでは、簡単なプログラムなら本体上でも作ることができます。

ところで、ロボットを動かすためのプログラムはパソコンを使って作ります。 そのためのソフトウェアもキットに付属していますが、 このLEGO社純正のソフトウェア以外にも多くのソフトウェアが 開発されていて、自由に(もちろん無料で)使用できるものも少なくありません。

いずれにしても、まったく同じロボットでもプログラムを変更することで さまざまな動かし方をすることができます。 もちろんロボット自体を変更・改良した場合には、 プログラムも変更しなければいけないことが多いでしょう。 このようにロボットの機械的な工夫とプログラムの連携が ロボット製作の重要なポイントです。

この講座の大まかな日程

一応の目標ということで次のような計画を立ててみました。

第1日

LEGOに慣れている人なら初めからオリジナル・ロボットを作ってもかまいませんが、 最初はインストラクションに従って作った方が簡単ですし、勉強にもなります。

まずキットに慣れるために、付属のインストラクション(説明書)に従って左右のタイヤを 独立に動かすことのできるロボットを作ります。 そして、そのロボットを前や後ろに動かしたり、 左右に曲がらせるための簡単なプログラムをパソコンで作成します。 プログラムをロボットに転送して、実際にロボットを動かしてみます。

少し慣れてきたら、よりシンプルで改良しやすいプログラムを書くための手法を学習します。 このステップを省略しないことで、のちのちのプログラミングが楽に(楽しく)なります。

次のステップではセンサを使って周りの状況に合わせて動くように ロボットを改良します。 タッチセンサを使って、障害物に当たったら向きを変えて進むロボットなどを作ってみましょう。

第2日

明るさを測定できる光センサを使って、 黒い線に沿って動くロボットを作ってみます。 せっかくなのでコースは自作しましょう。 といっても画用紙の上に太いペンで黒い線を描くだけなので、誰でも簡単にできます。 なんとかうまくできたら、次はスピードを上げる工夫をしてみましょう。

音を鳴らしたり、曲を演奏させるプログラムも作ってみます。

第3日

ひと通り、モータやセンサの使い方に慣れたら、次は二台のロボットで 通信をしてみます。 簡単な数字を送るだけですが、 これだけでも相当な連携動作ができるようになります。

ここまで学習すれば、ロボットにかなり複雑な動作をさせることができるはずです。 いよいよオリジナル・ロボットへ挑戦です。

この講座で使うソフトウェアについて

基本的には、ロボットを動かすためのプログラムを作成するツールと プログラムをロボットが解釈できる形式に変換して ロボットに転送するツールがあれば十分です。 もちろん個人でもこれらツールを自分のパソコンに用意することは可能ですが、 なるべく簡単に作業が始められるように これらのツールを全部まとめて収録したUSBメモリ用のデータを独自に用意しました (以下ではUSBメモリにまるごとコピーできるデータのことをイメージと呼びます)。 各自のUSBメモリにこのイメージをまだコピーしていない場合は、 担当の先生に相談してください。

このカスタマイズしたDebian Liveシステムには、 オフィス・スィートの LibreOffice、 高機能グラフィックス・ソフトウェアの Gimp や Inkscape なども収録されています。 また C、C++、Ruby, Python などのプログラミング環境も入っているので、 これらの言語でもすぐにプログラミング学習が始められます。 もちろん GNU/Linux の勉強にも適しています。

このUSBメモリ用のイメージは、 Debian Liveというシステムをマインドストームの 開発用にカスタマイズしたものです。 もともとパソコンに入っているWindowsなどの OS(オペーレーティング・システム)を起動せずに、 このUSBメモリからパソコンを起動させることで、 必要なツールを即使うことができます。 このイメージにはマインドストームのプログラム開発用のソフトだけでなく ホームページ閲覧ソフト(ブラウザ)をはじめ グラフィックスや表計算などの種々のソフトウェアも収録してありますので ぜひ活用してください。

準備するもの

この講座で使う物を以下にまとめてあります。

参考資料について

この文書で説明している内容は、初めてマインドストームに触る人を対象にした、ごく入門的なものです。 少しマインドストームに慣れてきたら、いろいろな文献を参考に さらに高度なロボット製作に挑戦してみてください。 マインドストームに関する書籍も豊富に出版されていますが、 インターネット上には優れた文献が非常に豊富にあります。 もしインターネットにアクセスすることができるなら、 ぜひ自分でも検索してみてください。

ここでは、NQCやNXCの入門的ガイドとして最も定番で日本語訳もある次の文書を 紹介しておきます。 いずれもインターネットからPDFファイルをダウンロードできます。 以下の入門ガイドを読めばこの文書を読む必要がないくらい とてもわかりやすく説明されてます。 その他の参考資料は、 この文書の最後にいくつか紹介してありますので参考にしてください。

『Programming Lego Robots using NQC』
Mark Overmars さんによるNQCの入門書 (Alberto Palacios Pawlovskyさんによる日本語訳)
http://www.staff.science.uu.nl/~overm101/lego/tutorial_j.pdf
『NXCを使ったLEGOのNXTロボットのプログラミング』
Daniele Benedettelliさん製作『Programming LEGO NXT Robots using NXC』 のAlberto Palacios Pawlovskyさんによる日本語訳
http://www.cc.toin.ac.jp/sc/palacios/courses/undergraduate/freshman/micro_intro/NXCtutorial_j.pdf
『NXCを使ったLEGOのNXTロボットのプログラミング』
上と同じDaniele Benedettelliさんのガイドの高本孝頼さんによる日本語訳(補足説明付き)
http://www2.ocn.ne.jp/~takamoto/NXCprogramingguide.pdf

まずは簡単なロボットを組み立てよう

まずRISやNXTに付属している説明書(インストラクション)に沿って ロボットを組み立てます。 が、その前に次の注意事項をよく読んでください

キットの取り扱い上の注意

基本的な注意

コントローラ本体やモータ、センサ類は精密な電子部品ですので、 特に慎重に扱いましょう。 衝撃を与えたり無理な力を加えたり、というのは厳禁です。 プラスティックの部品は比較的丈夫ですが、 それでも無理な力をかけると当然壊れるので注意深く扱ってください。 また細かな部品もたくさんありますので無くさないように注意しましょう。

無理な力を加えると写真のように壊れます。

壊れたホイール 壊れたリフトアーム 壊れたプレート 壊れたブッシュ

モータの扱いについて

モータに過度の負荷をかけてはいけません。 つまり力が足りなくてモータが回らないのに、無理に回そうとしてはいけません。 うまく回らない場合には、プログラムをすぐにストップさせましょう。 モータに負荷をかけ過ぎると、電池の消耗が激しくなったり、 モータそのものが壊れたりします。 力強く動かしたいときは、ギアを複数使って モータの回転数と最終出力(例えばタイヤ)の 回転数の比を調整してゆっくりと動かす必要があります。

コード類の扱いについて

コード類は絶対に強く引っ張ってはいけません。 またコードを抜くときは、断線を避けるために コード部分ではなく必ず端子部分をもってはずしましょう。 RIS付属の光センサも同様です。 線をもって引っ張ると写真のように壊れてしまいます。

壊れたライトセンサ

NXT付属のコードの端子は爪の部分を軽く押さえて抜きますが、 この爪は折れやすのでコードを抜くときや 絡まってしまったコードを解く場合には特に注意してください。 USBなどの端子を抜き差しするときは斜めにせずにまっすぐ抜き差ししてください。 RIS付属のコードの中には端子部が汚れていて接触の悪いものもたまにありますので、 きちんとつないでいるのにモータが回らないときには申し出てください。

ゴム類について

ゴムの部品も何本かはいっていますが、とても切れやすいのでソフトに扱ってください。 無理に伸ばしたり、 ブロックの角に強く押し当ててしまうような使い方をしてはいけません。 プーリーで使う際にゴムをなるべく滑らないようにするには、 ゴムを強く引っ張るのではなく、大きなプーリーを使ったり、 プーリーを2連にしたりしてください。

使用する電池について

使用する電池は必ず、同一の種類で同じ時期に購入したものを使います。 違う種類の電池や新旧違う電池を混ぜて使うと、 発熱や液漏れの原因になり、 コンピュータ本体を壊してしまう可能性があります。

RCXの電池交換について

電池交換の際には、必ず電源を切ります。 RISのコントローラであるRCXには 作成したプログラムをコンピュータが直接解釈できるように翻訳してくれる 『ファームウェア』というプログラムが入っていますが、 電池の交換の際に、電池を抜いてしばらく放っておくとこのファームウェア が消えてしまいます。 再度ファームウェアをRCXに転送するには何分間かかりますので、 素早く(1分程度)で電池を交換するようにしましょう。 RCXの場合は、電池はすべて同じ向きです。 マイナス側(平な底面)から入れます。 プラスとマイナスを間違わないようにしましょう。 電池交換後、電源を入れて、0 が4つ表示されていれば大丈夫です。 0が1つだけ表示されている場合にはファームウェアを転送し直す必要があります。

NXTの電池交換について

NXTの場合も、電源を切って電池交換をします。 NXTの場合は電池を抜いてもファームウェアは消えません。 電池の向きは交互にいれます。 それぞれの電池をまず電池のマイナス側をバネのようなっている端子に 当ててから、少し押さえてプラス側(突起になっている方)をはめ込みます。

プレート類の扱いについて

通常のブロックの1/3の厚さで板状の部品をプレート呼びます。 このプレートは同じ大きさのものをピタッと重ねてしまうと外すのに少し苦労します。 使わないときには重ねずしまうようにしましょう。 どうしても重ねる場合には、ポッチを一つずらして重ねておきましょう。 重なったプレートはRISに入っているブロック外しを使うか、 大きなブロックに一度くっつけてからはずすと比較的用意にはずれます。

以上の注意を2回読みなおしてから次へ進みましょう。 特にコード類、ゴム部品の扱いには細心の注意を払ってください。

この講座で使用するRISとNXT

この講座では、用意できるキットの数の事情でRISとNXTの両方を使います。 ただし、RISは3804番のキットにモータを1つ、光センサを1つ追加した上で、 大小2個の市販ケースに収納してあります。 NXTの9797番については2006年発売の基本キット(v46)と 2011年発売の基本セット(v95)が混在していますが、それほど大きな違いはありません。 基本的にはバッテリがACからDCに変更になったのとL字の コネクタが6個増えたことです。 NXTのキットからは今回使用しないバッテリを抜いてあります。

NXTの教育用キット9797のv46とv95では実は本体の内部も変更になっています (いつ頃変更になったのかは不明)。 ウェブ上で故障が多数報告されている液晶の基盤 (おそらくハンダ付け不良) がCPU基盤に統合され、不具合が減ると期待されます。

また、コントローラにあらかじめ入れてあるファームウェアと呼ばれる ソフトウェアののバージョンは、 RIS用がRCX2.0 (配布時ファイル名 firm0328.lgo)、 NXT用が v1.28 (配布時ファイル名 LEGO MINDSTORMS NXT Firmware V1.28.rfw) となっています。 これ以外のファームウェアも使用可能ですが、その場合には バージョンをコンパイル時のオプションとして指定する必要があります。

新旧マインドストームの主な違い
RIS 2.0NXT
LEGOの型番38049797
部品点数718個431 (v95は 437個)
コントローラRCXNXT
モータ2+1個3個(サーボモータ)
光センサ1+1個1個
タッチセンサ2個2個
超音波センサなし1個
サウンドセンサなし1個
回転センサオプションで使用可モータが兼ねる
パソコンとの接続USB赤外線タワー直接USB接続
この講座で使うプログラミング言語NQCNXC

では、さっそくロボットを組み立てましょう。

最初のロボットの組立て

まずはインストラクション(説明書)に従って、 モータを2個使って前後に進んだり左右に曲がるロボットを作ります。 インストラクションの製作例には、 LEGOのパーツを上手に使う技もけっこう含まれています。 特に「軽量」「丈夫」「機能ごとの部品に分解しやすい」を実現するための 組み立て方を常に意識しながら作業を進めてください。

LEGO Mindstorms RISと組立例

インストラクションの10ページから25ページまでを参考にしてください。 ドライブベースには、タイヤ、足、キャタピラを装着することができます。 キャタピラは組み立てるのに少し時間がかかりますが、いろいろと試してみましょう。

軸 (黒い棒のような部品) やビーム (骨組みとして使われる穴の開いた部品) の長さはインストラクションの最後のページに実寸大の図がありますので、 これと比較して正しい長さのものを使ってください。

もともとRISにはモータが2個付属していますが、 1個追加しているのでモータは合計3個あります。 ところがこれらのモータには71427と43362という2つの型番があり、 重さや回転速度が違います。 ですので、左右のモータの型番が同じになるように2個のモータを選んでください。 ただし、たとえ同じ型番のモータでも回転速度にはバラつきがある、ということは 覚えておいてください。

RISの組立説明書に載っているロボット

LEGO Mindstorms NXTの組立例

前に触れたようにこの講座で使用する教育用キットにはv46 (431パーツ) と v95 (437パーツ) の2種類があります。

新しいv95を使用する場合には、 まず、インストラクションの2ページにある新旧の部品の対応表を確認します。 インストラクションは古い部品で説明されているの注意してください。 特に、インストラクションでは摩擦のある長さ3のピンの色が黒くなっていますが、 実際には青です。 これと似ている、黒いブッシュ付きの長さ3のピンと間違わないように。 この他、軸と軸をつなぐジョインター(黒)のデザインもイラストとは少し 変わっていますが、機能は同じです。

軸についてはそれぞれの組立図のそばに実寸大のものが描かれているので それを参考にしましょう。 RISと違って、偶数の長さのものは黒、奇数の長さのものは灰色、と色が 分かれているので間違いにくいはずです。

では、8ページから22ページまでを参考に組み立てましょう。

NXTの組立説明書に載っているロボット

部品の収納方法

RISの場合は、次の写真(別紙で配布)のような配置で部品を収納してください。 また、NXTの場合は、付属の図の通りに収納してください。

使っていない部品は、ケースのもとの位置にきちんと戻しておきましょう。 そうすることで、次にその部品を使うときにすぐに見つけることができます。 特にキットに慣れないうちは、組立て時間のうちのかなりの部分が 部品を探している時間であると気づくでしょう。

RISの収納ケース上段 RISの収納ケース下段 RISの小物収納ケース

プログラムを作ってロボットを動かそう

プログラムって何?

ロボットを動かすための一連の命令を記述したものも プログラムと呼びます (一般にはコンピュータに計算させるための一連の 命令を記述したものをプログラムと呼びます)。 またプログラムを作成する作業のことをプログラミングと呼びます。

NQCは1998年にDave Baumさんによって開発され、 現在はJohn Hansenさんによってメンテナンスされています。 一方、NXCは2006年に同じくJohn Hansenさんによって開発が始められ、 アセンブリ・ライクな言語であるNBCとセットで開発・配布されています。 NQCもNXCもオープンソースで開発・配布されているため、誰でも自由に使用したり改良することができます。もちろん無料です。 ちなみにNQCはRCXだけでなくSCOUTやスパイボティクスにも対応しています。

そしてプログラムを記述するための言葉がプログラミング言語 と呼ばれているもので、世の中には非常に多くのプログラミング言語があります。 この講座では、NQCNXCという マインドストーム専用に開発されたプログラミング言語を使ってプログラムを作成します。 これらの言語は、文法(プログラムを書く決まり)としてはC言語という プログラミング言語とよく似ていますが、 マインドストームのモータやセンサを扱う命令が豊富に用意されているので、 初心者でも簡単にプログラムを作成することができます。

今回使用するプログラミング言語
キットプログラミング言語
RISNQC
NXTNXC

プログラムを書いてみよう

プログラムを作成するためにはエディタと呼ばれるツール (ソフトウェア) を使います。 基本的には文字を入力することができて、いわゆる「テキスト形式のファイル」 として保存できるソフトであれば何でもかまいません。

この講座で使うライブUSBには「マウスパッド」というエディタが入っていますので これを起動します。 少しコンピュータに詳しい人向けに vi や emacs というエディタも使えるようになっています。

さっそくエディタを起動して、次のような簡単なプログラムを入力してみましょう。 これはロボットを3秒間前進させるためのプログラムです。 RISを使っているグループはNQCのプログラムを、 NXTを使っているグループはNXCのプログラムを入力してください。

ただし、『// モータAとCを前転』のように //から行末までの部分は、ロボットへの命令ではなく プログラムの説明なので、 書いても書かなくてもロボットの動きに違いはありません。 しかし、他人がプログラムを見た時に理解しやすいように、 また、自分でどんなプログラムなのか忘れないためにも、 必要な箇所にはちゃんと説明を書いておきましょう。 これらの説明箇所はコメント文と呼ばれています。 コメント文だけは日本語で書いても大丈夫です。 また、コメント文を書くには、この方法以外に/**/で 囲む方法もあります。 この方法を使えば//を使う場合と違って 複数行にまたがってコメントを書くことができます。

task main ()
{
  OnFwd(OUT_AC); // モータAとCを前転
  Wait(300);     /* 次の命令に進むまで3秒待つ
                      (単位は 1/100秒)     */
  Off(OUT_AC);   // モータAとCを停止
}
task main ()
{
  OnFwd(OUT_BC,75); // 75%のスピードで
  Wait(3000);       // 単位は 1/1000秒
  Off(OUT_BC);
}

3秒間前進するプログラム

NQCやNXCでは一連の動作(タスク)をtaskというキーワードで指定します。 タスクの名前は自分でつけることができますが、 必ず一つmainという名のタスクがなければいけません。 そして2つ以上のtaskがある場合でも プログラムはmainタスクからスタートして、 他のタスクを呼び出すようにします。

タスクの中身は {} で囲みます。 また、一つひとつの命令の後には ; を忘れないように。

少し命令を説明しておきましょう。 モータを前転(正回転)するにはOnFwdという命令をつかいます。 スイッチオンを意味する On と前へという意味の Forward を短縮してたし合わせた名前です。 実際、NQCではOnFwdという命令も使えます。

モータは力を出すので確かに出力ですが、 一般にコンピュータから出てくる情報のことを出力といいます。 出力は、モータを回す以外にも、画面への出力であったり、プリンタへの出力であったり、 スピーカーへの出力であったりさまざまです。 これに対しコンピュータに与える指示や情報のことを入力といいます。 キーボードやマウスは入力装置です。

また、この例からわかるように、制御したいモータの名前は接続したポート(端子)の 名前に合わせてOUT_ACのように表します(以下の表を参考に)。 OUTというのはこの場合、出力 (英語でアウトプット output) という意味です。 RISのインストラクションでは左のモータがA端子に、右のモータがC端子に接続され、 NXTのインストラクションでは右のモータがB端子に、左のモータがC端子に接続されているので、 これら例ではそれらにあわせて出力を指定しています。 もちろん違う端子を使う場合には適宜名前を変えてください。

さらにNXCのOnFwdでは、 モータの指定だけでなく回転スピードも, (コンマ) につづけて 指定しなければいけないことに注意してください。

出力名モータ
OUT_A端子Aにつないだモータ
OUT_B端子B   〃
OUT_C端子C   〃
OUT_AB端子Aと端子B   〃
OUT_BC端子Bと端子C   〃
OUT_AC端子Aと端子C   〃
OUT_ABC全部の端子   〃

NQCではOnFwd(OUT_A+OUT_C);のような足し算の形で モータ (出力ポート) を指定することができます。 これはOUT_A, OUT_B, OUT_C に2進数の 1, 10, 100 が割り当てられていて、 例えばOUT_A+OUT_Cのような足し算をすると OUT_ACに割り当てられている101に一致する (1+100=101) からです。 NXCではこの表記はできません。

さて次に出てきたWaitというのは、次の命令に進むまで待つ、 という命令です。英語のWaitそのままですね。 NQCでは時間を1/100秒単位で、NXCでは1/1000秒単位で指定します。

ところで上の例のように、どこからどこまでが { と } の間の部分なのかを 見やすくするために空白を入れて字下げすることがよくあります。 これはC言語などのプログラミング言語でも同じです (勝手に空白や改行を入れることが禁止されている言語もあります)。 命令と命令の間に空白や改行はいくつあってもかまいませんが、 全角の空白はダメなので気をつけましょう。

NQCやNXCでは改行せずに次のように一行で書くこともできますが、 プログラムを読みやすくするために適切に改行や字下げを行いましょう。

task main () { OnFwd(OUT_AC); Wait(300); Off(OUT_AC); }
一行で書いたプログラム

プログラムを入力できたら、ファイル名をつけて保存しましょう。 ファイル名は余計なトラブルを避けるために、myrobo1.nqcmyrobo1.nxc のように半角のアルファベットと数字、 マイナス記号(-)、アンダースコア(_)だけを使ってつけましょう。 この例のようにNQCのプログラムには後ろに.nqcを、 NXCのプログラムの後ろには、.nxcをつけておくと何かと便利です。 このようにファイル名の後ろにつけてファイルの種類を表す文字の並びは 拡張子と呼ばれています。

ところで上のプログラムは、 次のようにモータの命令を一つずつ書いても同じです。 というのは、コンピュータは非常に高速で命令を処理するので、 2つの命令に分けて書いても実質的に同時の命令となります。 命令を遅らせるには、意図的にWaitで時間を指定してしなければいけません。

task main ()
{
  OnFwd(OUT_A);
  OnFwd(OUT_C); // 上の命令とほとんど同時に実行
  Wait(300);    // 次の命令に進むまで3秒間待つ
  Off(OUT_A);
  Off(OUT_C);
}
task main ()
{
  OnFwd(OUT_B,75);
  OnFwd(OUT_C,75);
  Wait(3000);
  Off(OUT_B);
  Off(OUT_C);
}
3秒間前進する別のプログラム

プログラムをロボットに転送しよう

プログラムを保存することができたら、早速ロボットに転送してみましょう。

RISのRCXにプログラムを転送するには、キットに入っている赤外線タワーを パソコンに接続して、RCXの前の黒い部分に向けます。 かなり遠くまで赤外線が届くので、 赤外線タワーを他のグループのロボットに向けてはいけません。 NXTの場合は付属のUSBケーブルでロボットとパソコンを接続します。 RCXやNXTの電源が入っているか確認してください。

ライブUSBを使っている場合、基本的には2種類の転送方法があります。 一つ目は、ファイルマネージャ(ファイル一覧を表示するソフト)を起動して 作成したファイルのアイコンを右クリックして「RCXに転送」または「NXTに転送」を 選ぶ方法です。おそらくこれがもっとも簡単でしょう。

コマンド入力では、特に指定しなくてもUSBポートを使うように設定してあります。 いろいろなオプションについては、man nqcなどで調べることができます。

2つ目は、シェルと呼ばれる、コマンドを入力するためのソフトを起動して、 次のように命令を直接入力する方法です。 シェルを起動すると、次のような表示になります。 パソコンの設定によっても違いますが、 表示されている文字はプロンプトとよばれ、 ユーザからの命令待ちの状態を示しています。

~$ 

この状態で、次のように入力します。NQCプログラムをRCXに転送するには、

~$ nqc -d myrobo1.nqc

myrobo1.nqc のところは各自のファイル名に変更してください。

また、NXCプログラムをNXTに転送するには、

~$ nbc -d myrobo1.nxc

のようにします。

ここでプログラムを「転送する」と書きましたが、 正確に言うと実際には我々が作成したプログラムを、 (1) コンピュータ (コントローラ) の中に入っているファームウェアが 解釈できる形に変換した上で、(2) 転送するという、2段階のことを 続けて行なってくれています。

このように人間が書いたプログラムをコンピュータがより理解しやすい形式に 翻訳(変換)することをコンパイルと呼びます。

うまく転送できない場合には

プログラムに文法的なエラーがあれば、エラーメッセージが表示されて プログラムは転送されません。 大文字、小文字を含めて命令のスペルがあっているか、 括弧を忘れていないか、セミコロンを忘れていないか、 などチェックしてみましょう。 プログラムを訂正したら保存して再度転送してみましょう。

以下によくありがちな間違いを挙げておきます。

また、シェルで転送しようとしている場合は、 作成したプログラムがちゃんと存在しているか

~$ ls

というコマンドで確認しましょう。

さあ!うまく動くかな?

無事エラーもなく転送できたら、ロボットを動かしてみましょう。

RCXの場合は、緑のスタートボタンを押すだけで動き始めるはずです。 NXTの場合は、横方向の矢印を動かして 「My Files」を選び(オレンジボタンで決定)、 さらに「Software files」を選びます。 そして自分が転送したファイル名を選んだ後、 Run を選べば動き始めるはずです。 一つ前のメニューに戻したり、プログラムを止める場合は下のボタンを押します。

うまく動かない場合には、モータがプログラムで指定した正しい端子に つながっているか、ケーブルがきちんとはまっているかなど、 確認してみましょう。

RCXを使っていてモータが反対に回る場合には、端子を逆に接続してみましょう。 NXTの場合は、逆にできないので、次の説明を参考にモータを逆転させるか、 スピードをマイナスにします (インストラクションの通りなら大丈夫なはずですが)。

ところで、これからいろいろな練習問題が出題されますが、 2問セットになっている問題の場合は、どちらか一方、 または両方挑戦してみてください。 2人1組で作業している場合にはお互いに別の問題を選びましょう。

2秒間前進して2秒間停止、さらに2秒間前進するようにプログラムを改造しましょう。

1秒間前進して2秒間停止、というのを3回繰り返すようにプログラムを改造しましょう。

バックや旋回もさせてみよう

モータを後転させるには、OnRevという命令を使います。

task main ()
{
  OnRev(OUT_AC); // モータAとCを後転
  Wait(300);
  Float(OUT_AC); // ゆっくり自然に止まる
}
task main ()
{
  OnRev(OUT_BC,75); // OnFwd(OUT_BC,-75)と同じ
  Wait(3000);
  Float(OUT_BC);
}
3秒間後進するプログラム

ここで、Floatという命令が出てきましたが、 これはモータに力をかけないようにする命令です。 つまり回す力も止める力もかかっていない、「浮いた」状態です。 これに対し、前に出てきたOffという命令は 実はモータを止めると同時に止まった状態に保つ、という命令でした。 もちろん無理やり回せば回ることは回りますが、 回すのにかなり抵抗があることがわかります。

さて、片方のモータだけを動かすことで、左右に曲がることもできます。 また、左右のモータを反対方向に動かすことで、その場で旋回させることもできます。

その場でロボットを時計回りに20秒間旋回させるプログラムを作って、 何周旋回するか数えましょう。

その場でロボットを半時計回りに20秒間旋回させるプログラムを作って、 何周旋回するか数えましょう。

上の練習問題の結果をもとにロボットが1周 (360度) 旋回するのに必要なだいたいの時間を 計算することができます。これを使って次の練習問題に進みましょう。

その場でロボットを時計回りに2回転させるプログラムを作ってみましょう。

その場でロボットを反時計回りに2回転させるプログラムを作ってみましょう。

前進と旋回ができれば、次の練習問題のようなプログラムも簡単に作ることができます。

50cm前進した後Uターンしてもとの場所に戻ってくるようプログラムを改造しましょう。

50cm前進した後、1回転半してもとの場所に戻ってくるようプログラムを改造しましょう。

さてここまでの練習で、実際にプログラムを作ってロボットを 動かす、という一連の作業に慣れたでしょうか?

プログラムをかっこよくスリムに

時間調整はプログラムの始めに

前の練習問題では、左右のモータを動かす時間をうまく調整する必要がありました。 もちろん毎回Waitの中の時間を編集して 調整することもできるのですが、 あらかじめプログラムの冒頭で名前をつけた『定数』 として指定しておくととても便利です。 特に次の例のように同じ時間が何度か登場するような場合には、 一箇所編集するだけで調整が可能になります。 また、たとえ何度も登場しなくても定数を使えば調整箇所をプログラムの 途中から探しだす必要がなく、 名前をつけることでどのような意味のある数なのか一目でわかります。

#defineで定義された定数は、コンパイル時に元の数に 置き換えられます。

以下は2秒間前進したあと2秒間後進するプログラム例です。 このように#defineに続けて『定数名』と『数値』を空白で 区切って書くだけで定数を定義することができます。 ちなみにdefineは英語で「定義する」といいう意味です。 NXTの例でもわかるように、時間以外でもプログラムが終了するまで 変更しない数値ならなんでも定義しておくことができます。

#define行は基本的に一行で書く、ということに注意してください。 長くなって2行以上で書きたい場合には次の行への継続を示す\ (バックスラッシュ) が行の終わりに必要です。 この\の後には何も書いては行けません。 この文書ではスペースの関係で\を使って行を継続していますが、 なるべく一行で書くようにしましょう。

#define MOVE_TIME 200 // 定数を定義

task main ()
{
  OnFwd(OUT_AC);
  Wait(MOVE_TIME); // MOVE_TIMEは自動的に
                   // 200と置き換えられる
  OnRev(OUT_AC);
  Wait(MOVE_TIME);
  Off(OUT_AC);
}
#define MOVE_TIME 2000  // 単位に注意
#define SPEED 75        // 別の定数を定義

task main ()
{
  OnFwd(OUT_BC,SPEED);
  Wait(MOVE_TIME);
  OnRev(OUT_BC,SPEED);
  Wait(MOVE_TIME);
  Off(OUT_BC);
}
定数を使ったプログラム(2秒前進の後、2秒後進)

定数名(定数の名前)は各自で決めることができますが、アルファベット と数字、_ (アンダースコア) だけを使ってつけましょう。 ただし数字は定数名の最初に来てはいけません。 つまりLE5のようは定数名はOKですが5LEはダメです。 空白や他の記号は使えないので注意しましょう。 定数名は慣習的にすべて大文字にすることが多いようです。 また、NQCやNXCであらかじめ定義されているコマンド名や 定数名などの、いわゆる『予約語』は使用できません。 例えばtaskOnFwdOUT_Astartstop などを定数名として使用することはできません。

今までに作ったプログラムを#defineをつかって改良しましょう。

決まった動作にも名前をつけておこう

実は数だけでなく、一連の命令も冒頭で定義しておくことができます。 例えば「左折する」というコマンドは、 もとのNQCやNXCでは用意されていませんが、定数と同様に #defineを使って自分で作ることができます。 この場合は、定数と呼ばずにマクロと呼びます。

#define TURN_TIME 80  // 左折の時間を定義
/* turn_left という名のマクロを定義 */
#define turn_left Off(OUT_A);OnFwd(OUT_C);\
  Wait(TURN_TIME);Off(OUT_C);  // 上の行の続き

task main ()
{
  OnFwd(OUT_AC); Wait(100);
  turn_left;
  OnFwd(OUT_AC); Wait(200);
  turn_left;
  OnFwd(OUT_AC); Wait(300);
  Off(OUT_AC);
}
#define TURN_TIME 800
#define SPEED 75
#define turn_left Off(OUT_C);\
  OnFwd(OUT_B,SPEED);Wait(TURN_TIME);Off(OUT_B);

task main ()
{
  OnFwd(OUT_BC,SPEED); Wait(1000);
  turn_left;
  OnFwd(OUT_BC,SPEED); Wait(2000);
  turn_left;
  OnFwd(OUT_BC,SPEED); Wait(3000);
  Off(OUT_BC);
}
マクロを使ったプログラム

マクロも原則一行で定義する必要がありますが、長くなる時には\ (バックスラッシュ) を使って行を継続させることができます。

このようにturn_leftという自分で定義した命令(マクロ)を 使うことよってtaskの中が簡潔になっているのがわかります。 さらにこのプログラムは「直進」の命令をマクロとして定義することで より簡潔になります。

直進用のマクロはいろいろな直進時間に対応できるようにしてみましょう。 そのためには、マクロ名の後ろに( )をつけて、その中に 直進時間を与えるための変数を書きます。 次の例ではtという変数が使われていますが、 これは定義内でWait(t)tとして使われます。 もちろん同じであればt以外の文字を使ってもかまいません。 数学で登場する f(t) = 2t + 4 ような関数の記法を思い出しましょう。

#define TURN_TIME 80
#define turn_left Off(OUT_A); OnFwd(OUT_C);\
  Wait(TURN_TIME);Off(OUT_C);
/* 引数を取るマクロ */
#define go_forward(t) OnFwd(OUT_AC); \
  Wait(t);Off(OUT_AC);

task main ()
{
  go_forward(100); // 1秒間前進
  turn_left;
  go_forward(200); // 2秒間前進
  turn_left;
  go_forward(300); // 3秒間前進
  Off(OUT_AC);
}

#define TURN_TIME 800
#define SPEED 75
#define turn_left Off(OUT_C);\
  OnFwd(OUT_B,SPEED);Wait(TURN_TIME);Off(OUT_B);
/* 時間tとスピードsの2つの引数を取るマクロ */
#define go_forward(t,s) \
  OnFwd(OUT_BC,s);Wait(t);Off(OUT_BC);

task main ()
{
  go_forward(1000,75); // 75%のスピードで1秒間前進
  turn_left;
  go_forward(2000,50); // 50%のスピードで2秒間前進
  turn_left;
  go_forward(3000,100);// 100%のスピードで3秒間前進
  Off(OUT_BC);
}
引数を取るマクロの例

このtのように、マクロなどを呼び出すときに指定する変数 (マクロなどに渡してあげる変数)のことを 『引数(ひきすう)』と呼びます。 ちなみに、「引(ひき)」は訓読み、「数(すう)」は音読みなので、 こういう読み方のことを湯桶(ゆとう)読みと呼びます。 重箱(じゅうばこ)読みの逆ですね。

上のNXCの例で定義されているように、 時間tとスピードsのように2つ以上の 引数を使いたい場合には、 go_forward(t,s)のように 引数を,(コンマ)で区切って定義します。 このマクロを呼び出す際には、時間tとスピード sの順序を間違えてはいけません。

上の例を参考に、 2秒前進、右折、1秒前進、左折、1秒前進、左折、1秒前進、右折、2秒前進、停止、 という動作をさせるプログラムを作りましょう。

上の例を参考に、 1秒前進、左折、2秒前進、右折、2秒前進、右折、2秒前進、左折、1秒前進、停止、 という動作をさせるプログラムを作りましょう。

マクロは、このように基本的に一行で書くことのできる比較的短い命令を 定義するのに便利です。一方、もう少し複雑な一連の動作の定義には、 後ほど説明する、関数やサブルーチンあるいはタスクを使います。

繰り返しはとても簡単

繰り返しはコンピュータが得意とするところです。 英語で繰り返しを意味するrepeatという命令を使えば、 例えば次のような「直進して左折」を4回繰り返すプログラムが 簡単にできます。

#define TURN_TIME 80
#define turn_left Off(OUT_A); OnFwd(OUT_C);\
  Wait(TURN_TIME);Off(OUT_C);
#define go_forward(t) OnFwd(OUT_AC); \
  Wait(t);Off(OUT_AC);

task main ()
{
  repeat(4) {  // {}内を4回繰り返す
    go_forward(100);
    turn_left;
  }
}

#define TURN_TIME 800
#define SPEED 75
#define turn_left Off(OUT_C);\
  OnFwd(OUT_B,SPEED);Wait(TURN_TIME);Off(OUT_B);
#define go_forward(t) \
  OnFwd(OUT_BC,SPEED);Wait(t);Off(OUT_BC);

task main ()
{
  repeat(4) {  // {}内を4回繰り返す
    go_forward(1000);
    turn_left;
  }
}
繰り返しは簡単に書ける

以上のように、マクロやrepeatを使うことで、 mainタスクの中のプログラムの流れが (コメント文で説明しなくてもよいくらい) 非常にシンプルでわかりやすいものになりました。 特にプログラムが長くなるときには、 同じ命令をダラダラと繰り返して書かないように常に注意しましょう。

一辺が40cmの三角形を描いて動くロボットのプログラムを作りましょう。

一辺が20cmの六角形を描いて動くロボットのプログラムを作りましょう。

次の例のようにrepeatのブロックを別のrepeatに入れる こともできます。 このように、ある構造の中に同じような構造がある構造を 入れ子構造と呼びます。

#define TURN_TIME 80
#define turn_left Off(OUT_A); OnFwd(OUT_C);\
  Wait(TURN_TIME);Off(OUT_C);
#define go_forward(t) OnFwd(OUT_AC); \
  Wait(t);Off(OUT_AC);

task main ()
{
  repeat(2) {
    repeat(4) {
      go_forward(100);
      turn_left;
    }
  }
}

#define TURN_TIME 800
#define SPEED 75
#define turn_left Off(OUT_C);\
  OnFwd(OUT_B,SPEED);Wait(TURN_TIME);Off(OUT_B);
#define go_forward(t) \
  OnFwd(OUT_BC,SPEED);Wait(t);Off(OUT_BC);

task main ()
{
  repeat(2) {
    repeat(4) {
      go_forward(1000);
      turn_left;
    }
  }
}
repeatを入れ子で使ったプログラム

一辺が40cmの三角形を3回描いて動くロボットのプログラムを作りましょう。

一辺が20cmの六角形を3回描いて動くロボットのプログラムを作りましょう。

少しずつ動かす時間を変化させるには

前の例では「決められた時間を動かすためには定数を使えば便利である」 ということを学習しました。 では、動かす時間を少しずつ変化させたい場合に何かいい方法はあるでしょうか? もちろんあります。 そういう場合には変数と呼ばれるものを使います。 数学で学習したxyといった変数と同じように、 プログラムで使う変数にもいろいろな値を代入することができます。

例として、一往復する度に往復の時間(距離)を伸ばしていくロボットを考えてみましょう。 一回目の往復は片道1秒、2回目は片道2秒、3回目は片道3秒、というふうに 進む時間を増やして行きます。 往復回数が少なければ次のプログラムのように命令を並べればよいだけなので簡単です。

#define go_and_back(t) \
  OnFwd(OUT_AC);Wait(t);\
  OnRev(OUT_AC);Wait(t);Off(OUT_AC);

task main ()
{
  go_and_back(100);
  go_and_back(200);
  go_and_back(300);
}

#define SPEED 75
#define go_and_back(t) \
  OnFwd(OUT_BC,SPEED);Wait(t);\
  OnRev(OUT_BC,SPEED);Wait(t);Off(OUT_BC);

task main ()
{
  go_and_back(100);
  go_and_back(200);
  go_and_back(300);
}
一往復ごとに往復の時間を伸ばしていくプログラム

ところが繰り返し回数が多くなると命令を並べて書くのは大変です。 そこで変数の登場です。

変数の宣言を task main の外で行った場合には、 その変数はグローバル変数(大域変数)と呼ばれプログラムの どこでも使用できます。 これに対し、例のように、あるタスクの中で宣言した場合は、 ローカル変数(局所変数)と呼ばれ、そのタスクの中だけ使用できます。

NXCでは、通常の整数型に加え、 桁数の多い (32bitの) 整数型long、 浮動小数点型float、 論理型bool、 文字型char、 文字列型string、 なども使えます。

まずは変数を使って上のプログラムを書きなおしてみましょう。 以下では、move_timeと名付けた変数を使います。 変数名は定数名と同じく、予約語以外で、 アルファベット、数字、_ (アンダースコア) を使って作ります。ただし、最初の文字に数字を使ってはいけません。

変数には、整数型、小数型、文字列型などいろいろありますが、 NQCで使用できるのは整数型のみです。 整数型の変数を宣言する際には、intというキーワードを 使います。ちなみにintは整数を表す英語、integerに由来します。

また変数の宣言と同時に最初の値(初期値)を代入しておくこともできます。

#define go_and_back(t) \
  OnFwd(OUT_AC); Wait(t); \
  OnRev(OUT_AC); Wait(t); Off(OUT_AC);

task main ()
{
  int move_time;  // 整数型の変数を宣言
  move_time = 0;  // move_time に 0 を代入

  move_time += 100; // move_timeに100を加える
  go_and_back(move_time);

  move_time += 100; //  move_timeの値は200になる
  go_and_back(move_time);

  move_time += 100; //  move_timeの値は300になる
  go_and_back(move_time);
}

#define SPEED 75
#define go_and_back(t) \
  OnFwd(OUT_BC,SPEED); Wait(t); \
  OnRev(OUT_BC,SPEED); Wait(t); Off(OUT_BC);

task main ()
{ /* 整数型の変数 move_time を使うことを
     宣言して、その初期値を0にする  */
  int move_time=0; 

  move_time += 1000; //  move_timeに1000を加える
  go_and_back(move_time);

  move_time += 1000; //  move_timeの値は2000になる
  go_and_back(move_time);

  move_time += 1000; //  move_timeの値は3000になる
  go_and_back(move_time);
}
変数を使ったプログラム

move_time = 0;左辺の変数に右辺の値を代入する (この場合 move_time0を代入する) という命令で、=という記号は代入演算子と呼ばれます。

次の命令に出てくる、+= は、左辺の変数を右辺の分だけ増やす、 という記号(演算子)です。 これと同じ意味で次のような表記も使うことができます。

move_time = move_time + 100;

この式を見て驚く人もいるかもしれません。 左辺と右辺は等しくないのに= (等号) で結びついている!、と。 でも上で説明したことを思い出してください。 この場合、= という記号は「等しい」という 意味ではなくて、右辺の式の値を左辺の変数に代入するという意味です。 つまりこの命令は、move_timeの値に100 を足したものをあらためて move_time に代入する、という意味です。 もし仮に move_time200という値が 入っていたとすると、 この命令を実行した後には move_time には 300という値が入っていることになります。

次の表は、数の計算でよく使う記号(算術演算子)をまとめたものです。 +- は数学の記号と同じですね。

整数の演算
表記意味
x + yxyを加えた値
x - yxからyを引いた値
x * yxyをかけた値
x / yxyで割った値
x % yxyで割った余り
x += yxyだけ増やす (x = x + y と同じ)
x -= yxyだけ減らす (x = x - y と同じ)
x *= yxy倍する (x = x * y と同じ)
x /= yx1/y倍する (x = x / y と同じ)
x++x1増やす (x += 1 と同じ)
x--x1減らす (x -= 1 と同じ)

さて、プログラムの書き換えに戻りましょう。 ここまでくれば、あとは簡単です。 すでに学習した repeatを使えば短いプログラムになります。

#define go_and_back(t) \
  OnFwd(OUT_AC); Wait(t); \
  OnRev(OUT_AC); Wait(t); Off(OUT_AC);

task main ()
{
  int move_time = 0; 

  repeat(3){
    move_time += 100;
    go_and_back(move_time);
  }
}
#define SPEED 75
#define go_and_back(t) \
  OnFwd(OUT_BC,SPEED); Wait(t); \
  OnRev(OUT_BC,SPEED); Wait(t); Off(OUT_BC);
task main ()
{
  int move_time = 0; 

  repeat(3){
    move_time += 1000;
    go_and_back(move_time);
  }
}
repeatを使って簡単にしたプログラム(繰り返し回数を増やすのは簡単)

プログラムがこういう形になれば繰り返しの回数を増やすのはとても簡単ですね。

上の例を参考に、最初は4秒間前進、その後3.5秒間後進、3秒間前進、2.5秒間後進、 ・・・0.5秒間後進、最後は停止、と、0.5秒づつ動く時間を減らしながら最後は止まる ロボットのプログラムを作りましょう。

最初は4秒間前進の後、4秒間後進、次はその半分の時間の2秒間前進・2秒間後進、 と動く時間を半分ずつに減らして、最終的に、0.25秒間前進ののち後進したあと 停止するロボットのプログラムを作りましょう。

変数を使って繰り返しを簡単に書く他の方法については、 後ほどまた触れます。

ロボットに触覚をつけよう

これまでの練習で、ロボットを動かすための基本的なプログラムの書き方が 理解できたでしょうか? さて、次はロボットに触覚(バンバー)をつけて、何かに触ったり当たったりしたら 動きを変えるようにしてみましょう。

タッチセンサの取り付け例 (RIS) タッチセンサの取り付け例 (NXT)

タッチセンサの取り付け

そのためにはタッチセンサと呼ばれるセンサを使います。 簡単な製作例がインストラクションにありますので、 まずはそのままバンパーを作ってみましょう。 RISの場合はインストラクションの 26ページ〜29ページ、NXTの場合は40ページから44ページです。

NXTのインストラクションでは、 ロボットの後ろにタッチセンサを装着するようになっていますが、 前にも装着できます。以下の説明では前に装着したことを前提にします。 後ろに装着した場合は、OnFwdOnRevに 置き換えるなどの変更をすれば、同じように動くはずです。

障害物にあたると止まるロボット

簡単な例として、障害物にあたると止まるプログラムを作ってみましょう。 NQCとNXCでは、センサの指定の仕方が多少違うので注意してください。

task main()
{
  /* 端子1にはタッチセンサ */
  SetSensor(SENSOR_1, SENSOR_TOUCH);

  OnFwd(OUT_AC);   // 前進

  /* センサが押されるまで待つ */
  until (SENSOR_1 == 1);

  Off(OUT_AC);  // 停止
}
task main()
{
  /* 端子1にはタッチセンサ */
  SetSensorTouch(S1);

  OnFwd(OUT_BC,75);   // 前進

  /* センサが押されるまで待つ */
  until (SENSOR_1 == 1);

  Off(OUT_BC);
}
障害物にあたると止まるプログラム

NXCでも、SetSensor(S1,SENSOR_TOUCH);という命令は使えますが、続けて ResetSensor(S1);という命令を実行する必要があります。

SENSOR_1は端子1のセンサの値を得るマクロで、 NQCではSensorValue(0)、NXCではSensor(S1) と定義されています。

センサを使うためには、まず、どのようなタイプのセンサがどの端子(ポート)に 接続されているのか指定する必要があります。 タッチセンサを使う場合、上の例のように、 NQCの場合はSetSensor、NXCの場合はSetSensorTouch という命令を使います。 1番目ではなく2番目のポートにタッチセンサをつないだときには、 SENSOR_1の代わりにSENSOR_2 (NQCの場合)、 S1の代わりにS2 (NXCの場合) を指定してください。

さて、上の例で登場したuntilというのが、センサの状況を 認識するための一つの命令です。 この命令は、( )の中の 条件が満たされるまでずっと待つ(次の命令に進まない)という意味です。 ちなみにuntilは英語で、時間的に「〜まで」という意味です。

この( )の中にあるSENSOR_1==1が 条件を表しているわけですが、 このような条件のことを命題と言ったり論理式と言ったりします。 論理式の特徴はtrue (真) とfalse (偽) の 2つの値のうちどちらか一方しか取らない、ということです。

上の例ではタッチセンサ自身の値は、SENSOR_1で得られ、 押されていれば1, 押されていなければ0です。 重要なのは=を2つ並べた==という記号で、 この記号の両辺(左右)の値を比較するためのものです。 そして==の両辺、この場合はSENSOR_11、 が等しければ論理式SENSOR_1==1の値はtrue、 等しくなければ論理式SENSOR_1==1の値はfalseになります。

この==のように値を比較するための記号を比較演算子 と呼びます。以下の表によく使う比較演算子をまとめてあります。

比較演算子
記号意味
== 等しい
< 小さい
<=小さいか等しい
> 大きい
>=大きいか等しい
!= 等しくない

条件式 1 + 2 == 3 の値は? (truefalseのどちら?)

条件式 2 / 3 == 0 の値は? (truefalseのどちら?)

タッチセンサ (SENSOR_1) が押されている時、 条件式 SENSOR_1==0 の値は?

タッチセンサ (SENSOR_1) が押されていない時、 条件式 SENSOR_1==1 の値は?

上のプログラムが理解できたら、次の練習問題に進みましょう。

障害物に当たるとUターンして進み、もう一度、障害物に当たると止まるようにプログラムを改良してみましょう。

障害物に当たると停止し、障害物を取り除くと2秒後にその場で180度旋回するようにプログラムを改良してみましょう。

障害物に何度当たっても動きつづけるように

上の例の、障害物に当たったら止まるプログラムを別の方法で書いてみましょう。 英語で「〜している間」を意味するwhileを使っても 簡単にかけます。while(論理式){命令}のように書くだけで、 論理式trueの間は{命令}を繰り返します。

task main()
{
  SetSensor(SENSOR_1, SENSOR_TOUCH);
  OnFwd(OUT_AC);
  while(SENSOR_1==0) { } // SENSOR_1が1の間繰り返す
  Off(OUT_AC);
}
task main()
{
  SetSensorTouch(S1);
  OnFwd(OUT_BC,75);
  while(SENSOR_1==0) { }
  Off(OUT_BC);
}
whileを使ったプログラム

ある論理式を値を反対にするためには!という記号を 論理式の前につけますが、この記号を使えば、 until(論理式);while(!(論理式)){} が同じであるということに気づいたでしょうか?

この例の場合は、OnFwd であらかじめ左右のモータを回しているので、 whileの命令で何もする必要がなく、 { }の中は空っぽになっています。 もちろん最初のOnFwdwhile の中に入れてwhile(SENSOR_1==0){OnFwd(OUT_AC);} のように書いても結果としては同じ動きをします。

このwhileには、とても便利な応用があります。 それは、論理式を常に真trueにして永遠に繰り返しをさせてしまう、 いわゆる無限ループです。

#define go_forward OnFwd(OUT_AC); // 前進
/* Uターン (少しバックして反転、時間は調整)*/
#define u_turn OnRev(OUT_AC); Wait(50); \
  OnFwd(OUT_A); Wait(100); Off(OUT_AC);

task main()
{
  SetSensor(SENSOR_1, SENSOR_TOUCH);

  while (true) {   // 永遠に繰り返し
    go_forward;
    until (SENSOR_1==1);  // 接触すると次に進む
    u_turn;
  }
}
#define SPEED 75
#define go_forward OnFwd(OUT_BC,SPEED);
#define u_turn OnRev(OUT_BC,SPEED); Wait(500);\
  OnFwd(OUT_C,SPEED); Wait(1000); Off(OUT_BC);

task main()
{
  SetSensorTouch(S1);

  while (true) {   // 永遠に繰り返し
    go_forward;
    until (SENSOR_1==1);  // 接触すると次に進む
    u_turn;
  }
}
障害物にあたるとUターンするプログラム

この例では、whileの中に 最初からtrueという論理式の値 (定数) を指定していますが、 もちろん while (1+2==3)while (4!=5) のように常にtrueになる 論理式であれば、どのように書いても同じことです。 が、最初からtrueを使うのが最も簡単ですね。

障害物に当たってUターンする回数を制限したい

上の例は、障害物に当たってもUターンして永遠に動き続けるプログラムでしたが、 一定の回数障害物に当たったら止まるように改良してみましょう。 そのためには、当たった回数を数えるための変数を使い、 当たる度に変数の値を増やしていきます。

今回はwhile (論理式) { 命令 ... }の代わりに do { 命令 ... } while (論理式);を使ってみましょう。 この方法は、先ほどのwhileの繰り返しと違って、 命令を実行した後で条件(論理式)を判定して、 trueならもとに戻って繰り返します。 つまり、最低1回は命令を実行するわけです。

#define go_forward OnFwd(OUT_AC);
#define u_turn OnRev(OUT_AC); Wait(50); \
  OnFwd(OUT_A); Wait(100); Off(OUT_AC);

task main()
{
  SetSensor(SENSOR_1, SENSOR_TOUCH);
  int n=0;  // 衝突回数を数える変数 (初期値を0に)

  do {   // { } 内の命令を実行
    go_forward;
    until (SENSOR_1==1);
    u_turn;
    n++;             // Uターンする度に1増やす
  } while (n < 5);  // nが5未満なら繰り返す
}

#define SPEED 75
#define go_forward OnFwd(OUT_BC,SPEED);
#define u_turn OnRev(OUT_BC,SPEED); Wait(500);\
  OnFwd(OUT_C,SPEED); Wait(1000); Off(OUT_BC);

task main()
{
  SetSensorTouch(S1);
  int n=0;  // 衝突回数を数える変数 (初期値を0に)

  do {
    go_forward;
    until (SENSOR_1==1);
    u_turn;
    n++;
  } while (n < 5);
}
5回Uターンしたら停止するプログラム

このように、回数などを数えるための変数を カウンタと呼びます。

上の例を、while (論理式) { 命令 ... } を使って書き直しましょう。

「もし〜なら」という条件で命令を実行

これまでの例で、条件をする方法としてuntilwhiledo〜whileを使うことを学習しました。 ここでもう一つ重要な条件指定の方法を学習しましょう。 それは、いろいろなプログラミング言語においてもよく使われるif を使う方法です。 英語の「if 〜」は「もし〜ならば」という意味です。

#define go_forward OnFwd(OUT_A);
#define u_turn OnRev(OUT_AC); Wait(50); \
  OnFwd(OUT_A); Wait(100); Off(OUT_AC);

task main()
{
  SetSensor(SENSOR_1, SENSOR_TOUCH);

  while (true) {
    go_forward;   // 前進

    if (SENSOR_1 == 1) {  // 接触すれば
      u_turn;             // Uターン
    }
  }
}

#define SPEED 75
#define go_forward OnFwd(OUT_BC,SPEED);
#define u_turn OnRev(OUT_BC,SPEED); Wait(500);\
  OnFwd(OUT_C,SPEED); Wait(1000); Off(OUT_C);

task main()
{
  SetSensorTouch(S1);

  while (true) {
    go_forward;

    if (SENSOR_1 == 1) {
      u_turn;
    }
  }
}
ifを使ったプログラム

ifの論理式がfalseの場合には、 何も実行せずに次に進みます。

左右のバンパーを使う

次に、左右に2個のタッチセンサをつけて両方のセンサを使ってみましょう。 RISの場合は、ダブルバンパーとしてインストラクションの30〜33ページに 紹介されています。 NXTの場合は、インストラクションに載っていないので タッチセンサを一つ使った場合を参考に自分で考えてみましょう。

RISの組み立て説明書に載っているダブルバンパー

まずはどちらか一方のセンサが押されたら ロボットが3秒間止まるようなプログラムを考えてみましょう。 これはifブロックを2つ並べることで簡単にできます。

#define wait3sec Off(OUT_AC);Wait(300); //3秒停止
  // stopは予約語なのでマクロ名として使えない
task main()
{
  SetSensor(SENSOR_1, SENSOR_TOUCH);
  SetSensor(SENSOR_3, SENSOR_TOUCH);

  while(true){
    OnFwd(OUT_AC);
    if (SENSOR_1==1) { wait3sec; }
    if (SENSOR_3==1) { wait3sec; }
  }
}
#define wait3sec Off(OUT_BC);Wait(3000);

task main()
{
  SetSensorTouch(S1);
  SetSensorTouch(S3);

  while(true){
    OnFwd(OUT_BC,75);
    if (SENSOR_1==1) { wait3sec; }
    if (SENSOR_3==1) { wait3sec; }
  }
}
どちらか一方のタッチセンサが押されたらロボット3秒間止まるプログラム

このように2つifブロックを並べて実現することも可能ですが、 実行する同じ命令 (この場合 wait3sec) を繰り返し記述するのは面倒です。 そこでもう少し便利な方法を紹介しましょう。 それは、 2つの論理式SENSOR_1==1SENSOR_3==1 のどちらかが成り立っていれば真(true) となるように論理式を組み合わせる方法です。 これは2つの論理式を||という記号でつなぐだけで実現できます。 このような論理式の演算を行う記号を論理演算子といいます。

#define wait3sec Off(OUT_AC); Wait(300);

task main()
{
  SetSensor(SENSOR_1, SENSOR_TOUCH);
  SetSensor(SENSOR_3, SENSOR_TOUCH);

  while(true){
    OnFwd(OUT_AC);
    /* 論理和を使った条件式 */
    if((SENSOR_1==1)||(SENSOR_3==1)) {
      wait3sec;
    }
  }
}
#define wait3sec Off(OUT_BC); Wait(3000);

task main()
{
  SetSensorTouch(S1);
  SetSensorTouch(S3);

  while(true){
    OnFwd(OUT_BC,75);
    /* 論理和を使った条件式 */
    if ((SENSOR_1==1)||(SENSOR_3==1)) {
      wait3sec;
    }
  }
}
2つの論理式を||で組み合わせた論理式

このように2つの論理式のどちらか一方でもtrueであれば、 組み合わせた論理式もtrueである、というような組み合わせ方を 論理和と呼びます。 逆に言えば、2つの論理式の論理和がfalseになるのは、 両方の論理式がfalseの時のみです。

下記の表は論理式ABが それぞれtrueまたはfalseのとき論理和 (A || B)の値がどうなる表した表です。 このようなtruefalseの値の表のことを 真理値表といいます。

論理式Aと論理式Bの論理和 A||B
ABA || B
falsefalsefalse
falsetruetrue
truefalsetrue
truetruetrue

ついでに、 同時に両方のセンサが押された時に限ってロボットが止まるようなプログラム を作ってみましょう。 この場合は、2つの論理式を&&でつなぎます。 そして、このように2つの論理式のどちらもtrueであるときに限り trueである、 というような組み合わせ方を論理積と呼びます。

#define wait3sec Off(OUT_AC); Wait(300);

task main()
{
  SetSensor(SENSOR_1, SENSOR_TOUCH);
  SetSensor(SENSOR_3, SENSOR_TOUCH);

  while(true){
    OnFwd(OUT_AC);
    /* 論理積を使った条件式 */
    if((SENSOR_1==1)&&(SENSOR_3==1)) {
      wait3sec;
    }
  }
}
#define wait3sec Off(OUT_BC); Wait(3000);

task main()
{
  SetSensorTouch(S1);
  SetSensorTouch(S3);

  while(true){
    OnFwd(OUT_BC,75);
    /* 論理積を使った条件式 */
    if ((SENSOR_1==1)&&(SENSOR_3==1)) {
      wait3sec;
    }
  }
}
2つの論理式を&&で組み合わせた論理式

論理積についても真理値表にまとめておきましょう。

論理式Aと論理式Bの論理積 A&&B
ABA && B
falsefalsefalse
falsetruefalse
truefalsefalse
truetruetrue

ちなみに&&を使った論理積を使わない場合、 次のようにifブロックのなかに別の ifブロックをいれる、つまりif入れ子 にすることで同じ動作をさせる、つまり論理積を組み立てることができます。

#define wait3sec Off(OUT_AC); Wait(300);

task main()
{
  SetSensor(SENSOR_1, SENSOR_TOUCH);
  SetSensor(SENSOR_3, SENSOR_TOUCH);

  while(true){
    OnFwd(OUT_AC);
    /* if の入れ子構造 */ 
    if (SENSOR_1==1) {
      if (SENSOR_3==1)) {
        wait3sec;
      }
    }
  }
}
#define wait3sec Off(OUT_BC); Wait(3000);

task main()
{
  SetSensorTouch(S1);
  SetSensorTouch(S3);

  while(true){
    OnFwd(OUT_BC,75);
    /* if の入れ子構造 */ 
    if (SENSOR_1==1) {
      if (SENSOR_3==1)) {
        wait3sec;
      }
    }
  }
}
ifブロックを入れ子にして2つの条件を組み合わせる

論理式 (1+2==3)||(3-1==1) の値は?

論理式 (4/3>1)&&(4/2==2) の値は?

SENSOR_1SENSOR_3がともに押されている時、 (SENSOR_1==0)||(SENSOR_3==1) の値は?

SENSOR_1が押されSENSOR_3が押されていない時、 (SENSOR_1!=0)&&(SENSOR_3!=1) の値は?

上の練習問題には、いじわる問題が混ざっているかもしれません。

左のバンパーが壁に接触したときは右折、右のバンパーが壁に接触したときは左折、 両方のバンパーが壁に接触したときやUターンするようなロボットを作りましょう。 (ヒント:両方のバンパーが接触する状況をつくるにはどうすればいい?)

机の上の落ちずに動きまわるロボットを作りましょう。 (ヒント:RISのインストラクションの90ページにテーブルバンパーの例があります。)

時間を測ろう

以前の例では、 障害物に当たったらUターンを繰り返す、というプログラムを作りました。 このプログラムでは while(true){ } を利用して、 永久的に動作を繰り返すようにしました。 では一定時間だけそのような動きをさせたい場合はどうすればよいでしょう?

これまで時間の指定というとWaitを使って次の命令までの時間を コントロールしていたわけですが、今回のような目的にはWaitは 使えません。なぜならWaitで待っている間は、触れたかどうかの 判断すら行えないからです。

こういう場合、便利なのがストップウォッチのように時間を測るタイマー機能です。 タイマーを使えば「一定の時間内に限って」という条件のもとで繰り返し動作を 簡単に行うことができます。 タイマー機能の使い方はNQCとNXCで多少違いますが、 基準となる時刻をまず決める、という点では同じです。

NQCではFastTimer(0〜3)だけでなく、 1/10秒単位で時間を測るTimer(0〜3)も用意されていますが、 これはRCX1.0が1/100秒のタイマーに対応していなかったためで、 RCX2.0を使うこの講座では必要ありません。 もちろんTimer(0〜3)を使うことはできますが、 Waitの引数と単位が違うので、その都度10倍したり 10で割ったりする必要があり面倒です。

NQCでは、FastTimer(0)FastTimer(3) の独立したタイマを4個使用することができます。 タイマーをリセットし再スタートするには ClearTimer(0)ClearTimer(3) という命令をつかいます。 このClearTimerは、 ストップウォッチで言えば、ストップ、リセット、スタートを 一度に行うような命令です。

NQCでもっと多くの時間を測りたい場合には、 NXCのように一旦ある時刻を変数に入れておき、 FastTimer(0)FastTimer(3) の値とこの変数との差を計算します。

NXCの場合はタイマーをリセットするコマンドがないので 常に基準の時刻との差を計算します。 ただし、1/1000秒の単位で計算すると非常に大きな値になるので 通常のint型でなくlong型を使います。

早速、例を見てみましょう。

#define MOVE_TIME 1500
#define go_forward OnFwd(OUT_AC);
#define u_turn OnRev(OUT_AC); Wait(50); \
  OnFwd(OUT_A); Wait(100); Off(OUT_AC);

task main()
{
  SetSensor(SENSOR_1, SENSOR_TOUCH);

  ClearTimer(0);  // 0番目のタイマをリセット

  /* タイマーの値 (1/100秒単位)が
       MOVE_TIME 以下の間だけ繰り返す */
  while (FastTimer(0) <= MOVE_TIME) {
    go_forward;
    until (SENSOR_1 == 1);
    u_turn;
  }
  Off(OUT_AC);  // 停止
}

#define MOVE_TIME 15000
#define SPEED 75
#define go_forward OnFwd(OUT_BC,SPEED);
#define u_turn OnRev(OUT_BC,SPEED); Wait(500);\
  OnFwd(OUT_C,SPEED); Wait(1000); Off(OUT_C);

task main()
{
  SetSensorTouch(S1);
   /* 測る直前の時刻をlong型変数 t0 に代入して
     今後はこの t0 を基準に時間測定 */
  long t0 = CurrentTick();
   /* CurrentTick()と t0 の差が MOVE_TIME 以下
       の時だけ繰り返し */
  while (CurrentTick()-t0 <= MOVE_TIME) {
    go_forward;
    until (SENSOR_1 == 1);
    u_turn;
  }
  Off(OUT_BC);
}
タイマーを使ったプログラム

NXCの例を見ればわかるように、基準となる時刻を変数で記憶させておくことが大切です。 いくつもの時間を同時に図りたいときにはt0, t1, t2, ..のように複数の 基準時刻を記憶しておくと良いでしょう(もちろん変数名はご自由に)。 ただしNQCの場合は、long型でなくint型で変数を定義します。

ある場所から前進し、障害物に触れたらそのまま後進してもとの位置にもどる プログラムを作成しましょう。 ヒントは「出発してから障害物に触れるまでの時間を測って記憶しておく」

障害物に触れたらUターンするプログラムをつくりましょう。 ただし、5秒以上障害物に触れないときには3秒間停止するようにしましょう。

音を鳴らそう

マインドストームにはサウンド機能が備わっているので、 いろいろな音を出すことができます。 基本的には、もともと用意されている(組み込まれている)音を出す PlaySoundという命令と、 音の高さ(周波数)と時間を指定して音を出すPlayToneという命令が あります。

もともとRCXやNXTに用意されている音を鳴らす

「ピッ」という音や「ブー」という音はもともと用意されていますので、 PlaySoundという命令に音の種類を指定するだけで 簡単に音を出すことができます。 指定できる音の種類は次の表の通りです。

SOUND_CLICK ピッ
SOUND_DOUBLE_BEEPピーッ、ピーッ
SOUND_DOWN ピロピロ〜と低くなる音
SOUND_UP ピロピロ〜と高く音
SOUND_LOW_BEEP ブーという低い音
SOUND_FAST_UPピロピロ〜と速く高くなる音

この表を参考にして PlaySound(SOUND_CLICK); のような命令を書くと 音が出ます。

task main()
{
  Wait(100);  // スタートの音と重ならないように
  PlaySound(SOUND_UP);  // ピロピロ〜と高く音
  Wait(200);            // 少し待つ
  PlaySound(SOUND_DOWN);// ピロピロ〜と低くなる音
}
task main()
{
  Wait(1000);
  PlaySound(SOUND_UP);
  Wait(2000);
  PlaySound(SOUND_DOWN);
}
組み込まれた音を鳴らすプログラム

ここで注意したいのは、次の音を鳴らす時には必ずWaitで 待ち時間を入れる、ということです。 実は PlaySound は、音を出すコマンド、というよりは サウンドバッファと呼ばれるサウンドデータの一時的な格納場所に音を送る命令です。 サウンドバッファにあるデータは次から次への演奏されるので、 この待ち時間がないと音がつながったりぐちゃぐちゃに聞こえたりします。 ですので続けて音を鳴らす場合には、 前の音の長さより少し長目の待ち時間を指定するようにしましょう。

障害物に接触すると、 「ピッ」という音を鳴らしてUターンするプログラムを作りましょう。

音の高さを指定して音を鳴らす

あらかじめ用意されている音(組み込まれた音)以外に、 音の高さや時間を指定して音を鳴らすこともできます。

ところで音というは空気の振動です。 この振動が人間の鼓膜に伝わり、小さな骨を経由した後、 電気信号に変換され、 さらに神経を通って脳へ伝わりようやく音として認識されます。 音の高い低いというのは一秒間にどれだけ空気が振動するかで決まります。 この一秒間に空気が振動する回数を(音の) 振動数または周波数と言い、 Hz (ヘルツ) という単位で表します。

音楽では、440Hzの音(1秒間に440回振動する音)を基準にして 他の音を調整していくことが一般的です (楽器などの音を調整することを調律と言います)。 この440Hzの音はピアノでいうと鍵盤の中央の『ラ』の音です。 この基準の『ラ』となる音が決まれば、 1オクターブ高い『ラ』の音(倍音と呼びます)は周波数を倍にすることで得られます。 逆に1オクターブ低い『ラ』の音は周波数を1/2にすることで得られます。

さて、楽譜の1オクターブの中には、ピアノの白鍵に相当するド、レ、ミ、ファ、ソ、ラ、シ、 と黒鍵に相当するド#、レ#、ファ#、ソ#、ラ#の合計12個の音があります。 1オクターブ上がると周波数が2倍になる、ということから 12個の音を『均等』に振り分けて音の高さを決めるのが 平均律と呼ばれる音の決め方です。

周波数の比が簡単な整数比になる音を組み合わせるとよく響く和音になります。 1オクターブ違う音は 1:2なので最もよく響きます。 平均律の場合には、オクターブ以外は完全な整数比にはなりませんが、 例えば、 「ド」と「ソ」は 2:3、 「ド」と「ファ」は 3:4、 「ド」と「ミ」と「ソ」は 4:5:6、 「ド」と「ファ」と「ラ」は 3:4:5 に近い比になっているので、これらの和音はよく響きます。

答えから言うと、一段階高い音(半音高い音)は元の音の周波数を 1.05946...倍することで得られます。 つまり『ラ#』の音は 440Hz×1.05946 = 446Hz、 さらにそれより半音高い『シ』の音は 446Hz×1.05946 = 494Hz、 ・・・と計算して、最終的に1.05946 を12回かければ(1.05946の12乗倍すれば) 最終的に1オクターブ高い『ラ』の音、つまり周波数が倍の音が得られる、 という理屈です。

以下は、このように計算したそれぞれの音の周波数です。 1オクターブ高い音や低い音は、これらを2倍したり1/2倍することで得られます。 PlayToneでは周波数を整数も時間も整数で指定する必要があります。

ソ# ラ# ド# レ# ファ ファ# ソ# ラ#
G G# A A# B C C# D D# E F F# G G# A A# B C
周波数 392 415 440 466 494 523 554 587 622 659 698 740 784 831 880 932 988 1047

次のプログラムは、 ドレミの歌の冒頭だけを演奏しながら前進し、 障害物に当たると音楽もロボットも止まるプログラムの例です。

#define DO  523  // ドの周波数
#define RE  587  // レの周波数
#define MI  659  // ミの周波数

task play_music()  // 音楽を演奏するタスク
{
  while (true) {   // ひたすら繰り返す
    /* 時間の単位は1/100秒
     Waitで音符の長さ決めて、それより少し短い
     時間をPlayToneで指定するときれいに聞こえる
    */
    PlayTone(Do,25); Wait(30);
    PlayTone(RE, 5); Wait(10);
    PlayTone(MI,25); Wait(30);
    PlayTone(DO, 5); Wait(10);
    PlayTone(MI,15); Wait(20);
    PlayTone(DO,15); Wait(20);
    PlayTone(MI,35); Wait(40);
  }
}

task main()
{
  SetSensor(SENSOR_1, SENSOR_TOUCH);

  start play_music ;  // 演奏を開始
  OnFwd(OUT_AC);      // 前進
  until (SENSOR_1 == 1);
  stop play_music ;   // 前進
  Off(OUT_AC);        // 停止
}
#define DO  523
#define RE  587
#define MI  659

task play_music()
{
  while (true) {
    /*
     時間の単位は1/1000秒
    */
    PlayTone(DO,250); Wait(300);
    PlayTone(RE, 50); Wait(100);
    PlayTone(MI,250); Wait(300);
    PlayTone(DO, 50); Wait(100);
    PlayTone(MI,150); Wait(200);
    PlayTone(DO,150); Wait(200);
    PlayTone(MI,350); Wait(400);
  }
}

task main()
{
  SetSensorTouch(S1);

  start play_music ;
  OnFwd(OUT_BC,75);
  until (SENSOR_1 == 1);
  /* 次の命令は firmware v1.28 ではエラーになる */
  // stop play_music ;
  Off(OUT_BC);
}
ドレミの歌(の冒頭だけ)を演奏しながら前進し、障害物に当たると止まるプログラム

この例では、音楽を演奏する部分をplay_musicという名をつけた 別のタスクとして定義しています。 別のタスクを開始するには、mainタスクの中で start play_music;のように 単に「start タスク名;」とするだけです。 逆にタスクを終了するため、 「stop タスク名;」します。

ただし、NXTのファームウェア1.28には不具合があり、あるタスクから他のタスクを止めるstopのような命令は使えないようです ("enhanced NBC/NXC firmware"を使えば大丈夫)。

注意してほしいのは、 別のタスクを開始すればそれは元のタスクと同時に実行される、ということです。 複数のタスクの命令が並行して処理されるので並列処理という言葉がよく使われます。 並列処理では、 モータをどのタスクがコントロールするか、ということがよく問題になります。 たまたまこの例では、play_musicは音の出力だけを扱い、 mainはモータ出力とセンサ入力だけを扱っているので、 モータをコントロールする権利を奪いあう、ということはありませんでした。 この問題については後ほど再び触れます。

上の「ドレミの歌」の冒頭を演奏するプログラムを、 次の2小節も演奏するように改良しましょう。

上の「ドレミの歌」の冒頭を演奏するプログラムを、 障害物を取り去れば演奏を再開するように改良しましょう。(RISのみ)

ロボットに目をつけよう

いよいよこの講座も佳境に入って来ました。

「目」と言っても、ここで使うのは明るさを感知できるだけの原始的な目ですが、 それでも周りの状況を感知してロボットの動きをコントロールすることができます。 RISやNXTには光センサが付属しています。 この光センサを使って黒線に沿って動く、 いわゆるライントレース・ロボットの製作を目標にしましょう。

光センサの取り付け

RISの場合はインストラクションの34〜35ページに、 NXTの場合は32〜34ページに光センサの使用例が載っています。 いずれもアタッチメントとして、 タッチセンサと交換して(あるいは前後に両方つけて)使うことができます。

光センサの取り付け例 (RIS) 光センサの取り付け例 (NXT)

付属の光センサは自分でも光を出すことができ、 近くの物体であれば、 この光の反射でその物体が白っぽいか黒っぽいか判断することができます。 ただし、黒い物体でも光沢のあるものであれば光をよく反射してしまい 「明るい」と判定されるので注意が必要です。 また、色については「黒」と「緑」のような違いであればある程度 見分けることができますが、 本格的に色を調べるにはNXT2.0付属のカラーセンサが必要です。

コースの作成

プログラムに取りかかる前に、 画用紙を使ってライントレースで使うコースを作りましょう。 黒い太いペンでコースを描きます。 単純過ぎず、複雑過ぎず、次のような箇所を採り入れた周回コースにしてみましょう。

最初の段階では、十字の交差点を完全に描かず、 一方の線が他方の線と交わる3cmくらい手前で止めておき、 向こう側も3cm離れたところから描くようにします (将来的にはつなぎます)。 またコースを描く際、次のような点に注意しましょう。

  ライントレースのコース作成例

黒い線にさしかかると止まるロボット

まずはタッチセンサの場合と同様に簡単な例として、 黒い線に差しかかったら止まる、という簡単なプログラムから作ってみましょう。 プログラムの構造は、タッチセンサの最初の例とほとんど同じです。

#define THRESHOLD 42 // しきい値

task main()
{
  /* 端子2には光センサ */
  SetSensor(SENSOR_2, SENSOR_LIGHT);

  OnFwd(OUT_AC);   // 前進
  /* THRESHOLDより暗くなるまで待つ */
  until (SENSOR_2 <= THRESHOLD);
  Off(OUT_AC);  // 停止
}
#define THRESHOLD 42 // しきい値
#define SPEED 50  // 少しゆっくり

task main()
{
  /* 端子3には光センサ */
  SetSensorLight(S3);

  OnFwd(OUT_BC,SPEED);
  until (SENSOR_3 <= THRESHOLD);
  Off(OUT_BC);
}
黒い線に差しかかると止まるプログラム

ある値よりも上がると(あるいは下がると) 条件の判定が変わるような境界の値のことを英語で、 thresholdと言います。 発音はちょっと難しく「スレシュホウルド」とか「スレショウルド」のように読みます。 日本語では閾値(いきち)とかしきい値と と言いますが、閾値を「しきい値」と読むこともあります。 ただし「敷居値」と書くことはあまりありません。 もともと敷居は、 上部の鴨居(かもい)とセットで襖や戸などの下部のレールとなる家の部材のことです。 「敷居をまたがせない」という慣用句のように空間を区切る意味でよく使われます。

もちろんTHRESHOLDという定数名でなくて IKICHISHIKIICHIという定数名をつけても 全く問題ありません。

この例ではとりあえず「しきい値」を42に設定しています。 RISのセンサでは、黒はだいたい30〜35、白は50〜60くらいなので、 そのだいたい中間の値を使っています。 NXTのセンサでは、黒は25〜30、白は55〜60くらいでしょうか。 光センサはモノによってかなりバラつきがありますので (特にRISの光センサ)、 あとで実際に明るさを測定して、より適切な値を選びます。

ところで上のプログラムではSetSensorSetSensorLight で光センサを使うための宣言を行なっています。 この宣言によってセンサの値(明るさ) が0から100までの整数で得られるようになります。 それをuntilの条件式で THRESHOLDと比較しているわけです。 白と黒の中間くらいの値をしきい値として選べば、 比較演算子として<を使うか <=を使うかの違いはそれほど大きくありません。

さて、プログラムを走らせると光センサが赤く光るはずです。 この赤い光の反射を見て黒い部分や白い部分の明るさを測定しているので、 あまりセンサが紙から離れてしまうと、 周りの光の影響で測定値が安定しません。 また、特にNXTの光センサの場合には構造上、紙に密着させると 光が受光部に届きにくくなり白い部分でも値が小さくなってしまいます。 ですのでセンサは紙から数ミリ〜1cmくらい離して設置するのがよいでしょう。 部屋の明るさなども考慮して調整してみてください。

黒い線にさしかかると1秒間停止した後、再び前進する、というのを繰り返す プログラムを作りましょう。

黒い線にさしかかるとUターンする、というのを繰り返すプログラムを作りましょう。

実際に明るさを測ってみよう

実際の明るさを測定してみましょう。 一度、光センサの設定が含まれるプログラム走らせた後は、 プログラムを止めた状態でも明るさを測定できます (NXTではいつでも測ることができます)。

RCXの場合は、黒い「view」ボタンを押すと、^というマークが 端子1を表す「1」の下の表示されます。 さらに、「view」ボタンを押すと^が「2」「3」と横に移動して さらに下の「A」「B」「C」の文字の上に移動します。 このように何回か「view」ボタンを押して、 光センサを接続している端子に^が来るようにすると ディスプレイのセンサの値が表示されます。

NXTの場合は、トップメニューから「View」を選び、 左右の矢印ボタンで「Reflected light」を選びます。 そしてさらに左右の矢印ボタンで光センサを接続している 端子 (port) 番号を選べばディスプレイにセンサの値が表示されます。

周りの明るさの違いやセンサの個体差もあるので、 周りのグループのセンサの値と違っていても不思議ではありません。 RISの方には光センサが2つ入っていますので、実際に比較してみましょう。

作ったコースのいろいろな部分の明るさを値を測ってみましょう。 最も明るい値、最も暗い値はいくらでしょうか?

黒い線の中央に沿って移動し、どのくらい値にバラつきがあるか 調べてみましょう。

さらに次の練習問題もやってみましょう。

黒線と垂直方向に、白い部分から黒線の中央まで光センサをゆっくりと移動して 値の変化を観察しましょう。 値が最大値から最低値まで変化していく幅(センサの移動距離)はどのくらい? RISの場合は2つの光センサの両方を調べてみましょう。

黒線の境界部分に沿って移動し、どのくらい値にバラつきがあるか調べてみましょう。

黒線に沿って走るロボット

さていよいよ黒線に沿って走るロボットを作ってみましょう。 このようなロボットのことを 英語では line follower (ライン・フォロアー) とか line tracer (ライン・トレーサー) と呼びます。 日本ではライントレース・ロボットやライントレーサーという言葉がよく使われます。

ライントレースの原理そのものは簡単です。 が、スピードや確実性をアップさせるにはいろいろな工夫が必要で とても奥深いテーマです。

光センサを1個使ってライントレースする主な方法としては 次の2つがあります。

  1. 基本的に光センサが黒線の上を動くようにする。 光センサが白い部分にさしかかったら左または右にカーブさせる。
  2. 基本的に光センサが黒線と白い部分の境界上を動くようにする。 光センサが白い部分にさしかかったら黒線側へ、 黒い部分にさしかかったら白い方へは向かうように、 カーブさせる。

   ライントレースの基本

1番目の方法では、白い部分にさしかかった時、 左に曲がべきか右に曲がるべきか、何らかの方法で決めなければなりません。 これに対し2番目の方法では、どちらに曲がるべきかは明らかです。 もちろん1番目の方法でも効率よく曲がる方向を決めていく良い方法がありますが、 ここでは簡単な2番目の方法で、 黒線の左側の境界をたどっていく、というプログラムを作ってみましょう。

#define THRESHOLD 42
#define turn_left Off(OUT_A); OnFwd(OUT_C);
#define turn_right OnFwd(OUT_A); Off(OUT_C);
#define STEP 1  // 一回の判定で進む時間

task main()
{
  SetSensor(SENSOR_2, SENSOR_LIGHT);

  while (true) {
    if (SENSOR_2 < THRESHOLD){  // 線上なら
      turn_left;                // 左へ
    } else {                    // 線上でなければ
      turn_right;               // 右へ
    }
    Wait(STEP);  // STEPの値をいろいろ変えてみよう
  }
}

#define THRESHOLD 42
#define SPEED 50
#define turn_left OnFwd(OUT_B,SPEED);Off(OUT_C);
#define turn_right Off(OUT_B);OnFwd(OUT_C,SPEED);
#define STEP 1  // 一回の判定で進む時間

task main()
{
  SetSensorLight(S3);

  while (true) {
    if (SENSOR_3 < THRESHOLD){  // 線上なら
      turn_left;                // 左へ
    } else {                    // 線上でなければ
      turn_right;               // 右へ
    }
    Wait(STEP);  // STEPの値をいろいろ変えてみよう
  }
}
黒い線(の境界)に沿って動くプログラム

この例のようにifの条件式はelse とセットで使用することで、条件が満たされた場合だけでなく、 条件が満たされない場合の動作も同時に記述できます。 ちなみに英語のelseは「〜でなければ」とか「他の」という意味です。

上の例で定数STEPの値をいろいろ変えた時にロボットの動きが どうなるか調べてみましょう。

上の例でどのくらい急なカーブまでなら、 光センサがちゃんと境界上を動いているか、 調べてみましょう。

上の例のように、 片方のタイヤを停止して左折・右折する場合には、 回転半径は左右のタイヤの間隔に等しくなるので、 この半径の円のカーブよりも急なカーブはうまく曲がることができません。 しかし、内側のタイヤを逆転して旋回に近い動作をさせれば、 もっと急なカーブにも対応させることができます。

内側のタイヤを外側のタイヤの半分くらいの回転スピードで逆転させることで、 急なカーブでの動きが改善されるか試してみましょう。 NQCの場合は、SetPowerという命令で出力パワーを 下げればスピードも遅くなります。パワーは0〜7 の値を指定することができ、 SetPower(OUT_A,3);のように使います。

直進もさせたい

前の例では、白か黒かの判定をして左折と右折だけで前進していくプログラムでした。 そこで少しプログラムを改良して、 明るさが中間くらいの場合には直進するようにしてみましょう。

そのためにはしきい値を2つ用意する必要があります。 例えば、明るさが37未満の場合は左へ、37以上50未満の場合は直進、 50以上の場合は右へ、 と動かしたい場合は37と50という2つのしきい値を使います。

実際には次の例のように elseのあとにさらにifブロックを続けて書くことで、 条件を順番に調べていくことができます。

#define BLACK 40  //  これ以下は黒
#define WHITE 50  //  これ以上は白
#define go_forward OnFwd(OUT_AC);  // 直進を追加
#define turn_left Off(OUT_A); OnFwd(OUT_C);
#define turn_right OnFwd(OUT_A); Off(OUT_C);
#define STEP 1

task main()
{
  SetSensor(SENSOR_2, SENSOR_LIGHT);

  while (true) {
    if (SENSOR_2 < BLACK) {        // 線上なら
      turn_left;                   // 左へ
    } else if (SENSOR_2 < WHITE) { // 境界付近なら
      go_forward;                  // 直進
    } else {            // 完全に線からはずれれば
      turn_right;                  // 右へ
    }
    Wait(STEP);
  }
}

#define BLACK 30  //  調整してください
#define WHITE 50  //  調整してください
#define SPEED 50
#define go_forward OnFwd(OUT_BC,SPEED);
#define turn_left OnFwd(OUT_B,SPEED);Off(OUT_C);
#define turn_right Off(OUT_B);OnFwd(OUT_C,SPEED);
#define STEP 1

task main()
{
  SetSensorLight(S3);

  while (true) {
    if (SENSOR_3 < BLACK) {
      turn_left;
    } else if (SENSOR_3 < WHITE) { // 境界付近なら
      go_forward;                  // 直進
    } else {
      turn_right;
    }
    Wait(STEP);
  }
}
黒線(の境界)に沿って動くプログラム(直進あり)

この例で大切なことは、条件を判断する順番です。 最初の SENSOR_2 < BLACKtrueならば、 それ以降の条件は判断されません。 つまり、もし最初にif (SENSOR_2 < WHITE)というような 条件式を書いてしまうと、そのあと else if (SENSOR_2 < BLACK)のような条件式を書いても 判断されないので意味がありません。 このように

if ( ... ){          // 条件その1
   ...
} else if ( ... ) {  // 条件その2
   ...
} else if ( ... ) {  // 条件その3
   ...
} else {    // どの条件に合わない場合
   ...
}

と条件式をつなげていくifブロックでは、 条件式の順序に注意しましょう。

上の例で、しきい値を変えて境界領域の範囲をうまく調整しましょう。

上の例で、左右に曲がる際に内側のタイヤを逆転させるとどのような動きになるか調べましょう。

これまでの練習で、どのような値を調整すればどのように動きが変化するか、 多少はつかめたでしょうか? 上の方法で更に細かく状況を場合分けするのは簡単です。

上の例を改良して、 「完全に白」「白と境界付近の間」「境界付近」「黒と境界付近の間」「完全に黒」 の5段階で動きを変えてみましょう。

もっと滑らかに、もっと確実に、もっと速く

これまでのライントレースのプログラミングで、どういう場合うまく行って、 どういう場合に失敗するか、というのが徐々に理解できてきたと思います。

おそらくまず最初に改善したいのは、光センサが左右に振られすぎる、 という点ではないでしょうか。 左右に振られすぎることで、ぎこちない動きになったり、 光センサが黒い線を通り越して反対側に移動したりしてしまいます。 実はこの問題は、左右に曲がるスピードやパワーを減らすことで 以外と簡単に改善できます。 つまり、直線部分と曲線部分でスピードを変えるわけです。

また、光センサを左右の駆動輪の真ん中からあまり遠くない場所に持ってくる、 というロボット本体の改良も有効です。 そうすることで、 ロボットが左右に急激に振られた時の光センサの振られる距離を減らすことができ、 スピードがある程度あってもセンサが過敏に反応しなくなるので、 結果としてぎこちない動きが抑えられます。

この他、急なカーブで光センサが黒い線(の境界)に追従できていない場合には、 前の練習問題であったように、内側のタイヤを逆転させて その場で旋回させてみましょう。 ただ、旋回だと前に進まないので、よほど線からは外れた時や よほど黒線の中央部に寄ってしまったときだけ旋回させて、 その他のカーブは内側のタイヤを停止するだけにしておく、 あるいはゆっくりと回転させる、 という方法が考えられます。 もちろん境界付近は直線で動かすようにすれば、これも 前の練習でやったように5段階の場合分けになります。

以下は、これらの点の改善を試みたサンプル・プログラムです。

#define THRESHOLD 45
#define HIPOWER 7  // 直線用のパワー
#define LOWPOWER 2   // カーブのパワー
#define set_power_H SetPower(OUT_AC,HIPOWER);
#define set_power_L SetPower(OUT_AC,LOWPOWER);
#define go_forward set_power_H; OnFwd(OUT_AC);
#define turn_left1 set_power_L;\
  OnFwd(OUT_C); OnRev(OUT_A);
#define turn_left0 set_power_L;\
  OnFwd(OUT_C); Off(OUT_A);
#define turn_right0 set_power_L;\
  OnFwd(OUT_A); Off(OUT_C);
#define turn_right1 set_power_L;\
  OnFwd(OUT_A); OnRev(OUT_C);
#define STEP 1

task main()
{
  SetSensor(SENSOR_2,SENSOR_LIGHT);

  while (true) {
    if (SENSOR_2 < THRESHOLD -8) {
      turn_left1;   // 左旋回
    } else if (SENSOR_2 < THRESHOLD -3) {
      turn_left0;   // 左折
    } else if (SENSOR_2 < THRESHOLD +3) {
      go_forward;   // 直進
    } else if (SENSOR_2 < THRESHOLD +8) {
      turn_right0;  // 右折
    } else {
      turn_right1;  // 右旋回
    }
    Wait(STEP);
  }
}
#define THRESHOLD 45
#define SPEED_H 50  // 直線用のスピード
#define SPEED_L 25  // カーブのスピード
#define OnRL(speedR,speedL) \
  OnFwd(OUT_B,speedR);OnFwd(OUT_C,speedL);
#define go_forward OnRL(SPEED_H,SPEED_H); // 直進
#define turn_left1 OnRL(SPEED_L,-SPEED_L);// 左旋回
#define turn_left0 OnRL(SPEED_L,0);       // 左折
#define turn_right0 OnRL(0,SPEED_L);      // 右折
#define turn_right1 OnRL(-SPEED_L,SPEED_L);// 右旋回
#define STEP 1 // 1回の判断で動く時間

task main()
{
  SetSensorLight(S3);

  while (true) {
    if (SENSOR_3 < THRESHOLD -15) {
      turn_left1;
    } else if (SENSOR_3 < THRESHOLD -7) {
      turn_left0;
    } else if (SENSOR_3 < THRESHOLD +7) {
      go_forward;
    } else if (SENSOR_3 < THRESHOLD +15) {
      turn_right0;
    } else {
      turn_right1;
    }
    Wait(STEP);
  }
}




滑らかに確実にラインをトレースするように少し改善したプログラム

上の例のような5段階で明るさを判断するプログラムを作り、 それぞれのしきい値を適切に調整しましょう。

上の例のような5段階で明るさを判断するプログラムを作り、 旋回時や右折左折時のスピードやパワーを適切に調整しましょう。

これまでの例では、光センサの値を何段階かに分けて動きを区別してきました。 しかし、現在の(あるいは少し前の)光センサの値から、 左右のモータの適切な回転数の差を割り出すことができれば、 より連続的でスムーズな動きが期待できます。 おそらく最初に思いつくのは、 光センサの中間値からずれた分に比例して回転数の差をつけ、 内側のタイヤの回転を遅くしたり、逆転したりする方法でしょう。

さらに少し進んだ方法として、 条件判断する時にその直前(あるいは少し前の)の判断を参考にする、 ということも考えられます。 例えば上のプログラムでは、 完全に黒くなった時に左旋回で方向を修正するわけですが、 その次の条件判断で少しだけ黒くなった場合には、 まだ少し黒いからといってさらに左に曲げるのではなく、 そのまま直進させてみる、ということも考えられます。 そうすることで方向が修正されすぎてしまうことを防ぎます。 このように、明るさの変化の割合を考慮することで、 よりスムーズな動作が期待できます。

また、直前の条件判断だけでなく、 少し前からの状態を考慮に入れることもできます。 例えば、ずっと黒が続いた場合にはもっと左に曲がる、など 変化の積算を考慮する方法もあります。

結局のところ、 ライントレースというのは光センサの値やその時間変化に応じて 左右のモータをどのようなスピード(あるいはパワー)で回すとよいか、 という問題なのですが、実際に少し試してみるとその奥の深さを 実感できるでしょう。 この先は宿題です。

交差点はどうする?

交差点に差しかかった場合、これまでのプログラムではロボットは どのように動くでしょうか? 黒線の左側の境界をトレースしている場合には、そのまま左折しようと するはずです。

そこで、交差点を非常に急なカーブと考えて、 左に何度も何度も繰り返して向きを修正しようとする場合には、 それを交差点とみなすことにしましょう。 つまり明るさ判断のループを繰り返す際、一定の回数連続して 黒になった場合には、進行方向を修正して、交差点を横断するように プログラムを書き直しましょう。 そのために、連続して黒になる回数を数える変数(カウンタ)を用意します。 ただし、黒にならなかった場合はすぐにリセットします。

以下はNXC用のプログラム例ですが、NQC用のプログラムも ほぼ同じように作ることができます。

#define THRESHOLD 45
#define SPEED_H 50
#define SPEED_L 25
#define OnRL(speedR,speedL) OnFwd(OUT_B,speedR);OnFwd(OUT_C,speedL);
#define go_forward  OnRL(SPEED_H, SPEED_H);
#define turn_left1  onRL(SPEED_L, -SPEED_L);  // 左旋回
#define turn_left0  onRL(SPEED_L, 0);         // 左折
#define turn_right0 onRL(0, SPEED_L);         // 右折
#define turn_right1 OnRL(-SPEED_L, SPEED_L);  // 右旋回
#define STEP 1  // 1回の判断で動作させる時間
#define nMAX 300  // 通常のカーブとして許容できる繰り返しの最大値 (調整する)
#define short_break Off(OUT_BC); Wait(1000); // 小休止
#define CROSS_TIME 200  // 交差点通過にかかる時間
/* 交差点を渡る */
#define cross_line OnRL(SPEED_L,SPEED_L);Wait(CROSS_TIME);short_break;

task main()
{
  SetSensorLight(S3);
  int nOnline=0; // 続けて黒になった回数 (カウンタ)

  while (true) {
    /* 黒を続けてnMAX回繰り返さない間、通常のライントレースをする */
    while (nOnline < nMAX) {
      if (SENSOR_3 < THRESHOLD-15) {
        turn_left1;
        nOnline++;  // カウンタを増やす
      } else {
        if (SENSOR_3 < THRESHOLD-7) {
          turn_left0;
        } else if (SENSOR_3 < THRESHOLD+7) {
          go_forward;
        } else if (SENSOR_3 < THRESHOLD+15) {
          turn_right0;
        } else {
          turn_right1;
        }
        nOnline=0;  // カウンタをリセット
      }
      Wait(STEP);
    }
    short_break; // 小休憩
    turn_right1; Wait(nMAX*STEP);  // 進行方向修正
    cross_line;  // 交差点を渡る
    nOnline=0; //カウンタをリセット
  }
}
交差点を渡るプログラム

もちろんこのプログラムではヘアピンのような急なカーブと交差点を見分けるのは 少し難しいでしょう。 しかし、例えば、コースをいくつかのゾーンに分けた上で、 ゾーンごとに違う方法で交差点や急カーブを判別し、 タイマを使って(時間条件も加味して) おおよその交差点通過時刻を予想することで、 かなり精度よく交差点と認識できるはずです。

急なカーブを交差点と間違って認識した場合、 「交差点」を渡り終わった時点では黒線上にいないはずです。 このことを利用して、ご認識した場合には再度もとのコースに戻るよう 上のプログラムを改良しましょう。

カウンタで回数を数える代わりに、 タイマーを使って一定の時間黒い部分が続いたら交差点とみなす、 というプログラムにしてみましょう。

光センサを2個使う (RIS)

光センサを2個使えば交差点を見分けるのもとても簡単になります。 2個の光センサを黒線をまたぐように設置しておくと、 共に白ならば直進、片方だけ黒ければ黒い方へ曲がる、 共に黒ければ交差点、というような方法が考えられます。

この講座では2個光センサを使う方法は時間の関係で省略しますが、 興味のある人はタッチセンサ2個使った場合と同じく うまく論理和や論理積を使ってプログラムを作ってみましょう。

一連の動作を部品化しよう

これまでもマクロを使って、再利用可能な一連の動作は部品化してきました。 一行で書くマクロは比較的単純な一連の命令に適していますが、 複雑になってくると使い勝手がよくありません。 ここでは、サブルーチンや関数といったプログラムの部品を使う方法を 説明します。

サブルーチン

ルーチンというのは決まりきった手順や一連の仕事を意味します。 プログラムでも、一連の命令をまとめたものを意味し、 一番最初に実行されるものをメインルーチン、 そのメインルーチンから呼び出させるものをサブルーチンと呼びます。

マクロがコンパイル時にもとの命令の置き換えるのに対し、 サブルーチンは実際にプログラムが走っている時に 呼び出されます。ですので、サブルーチンを何回呼び出しても プログラムのサイズは増えません。

以前マクロを使って書いたプログラムを サブルーチンを使って書き直してみましょう。

#define TURN_TIME 80  // 左折の時間を定義

/* turn_leftという名のサブルーチンを定義 */
sub turn_left()
{
  Off(OUT_A); OnFwd(OUT_C);
  Wait(TURN_TIME);
  Off(OUT_B);
}

task main ()
{
  OnFwd(OUT_AC); Wait(100);
  turn_left();
  OnFwd(OUT_AC); Wait(200);
  turn_left();
  OnFwd(OUT_AC); Wait(300);
  Off(OUT_AC);
}
#define TURN_TIME 800
#define SPEED 75

sub turn_left()  // NXCでは sub も void も同じ
{
  OnFwd(OUT_B,SPEED); Off(OUT_C); 
  Wait(TURN_TIME);
  Off(OUT_B);
}

task main ()
{
  OnFwd(OUT_BC,SPEED); Wait(1000);
  turn_left();
  OnFwd(OUT_BC,SPEED); Wait(2000);
  turn_left();
  OnFwd(OUT_BC,SPEED); Wait(3000);
  Off(OUT_BC);
}
サブルーチンを使ったプログラム

残念ながらNQCではサブルーチンは引数を取ることができませんが、 NXCのサブルーチンは引数を取ることができます。 この引数を取るサブルーチンを使えば、 上記の直進部分もサブルーチンとして定義できます。 NQCの場合は、迂回方法としてグローバル変数と呼ばれる、 メインルーチンでもサブルーチンでも使える変数を利用します。 グローバル変数は、 タスクやサブルーチンの外側で定義されていることに注意してください。

#define TURN_TIME 80
int move_time=0;  // グローバル変数を定義

sub turn_left()
{
  Off(OUT_A); OnFwd(OUT_C);
  Wait(TURN_TIME);
  Off(OUT_B);
}

sub go_forward()
{
  OnFwd(OUT_AC);
  Wait(move_time);  // 外で move_time の値を与える
  Off(OUT_AC);
}

task main ()
{
  move_time=100; go_forward();
  turn_left();
  move_time=200; go_forward();
  turn_left();
  move_time=300; go_forward();
}
#define TURN_TIME 800
#define SPEED 75

sub turn_left()
{
  OnFwd(OUT_B,SPEED); Off(OUT_C); 
  Wait(TURN_TIME);
  Off(OUT_B);
}

sub go_forward(int s, int t)
{
  OnFwd(OUT_BC,s);
  Wait(t);
  Off(OUT_BC);
}

task main ()
{
  go_forward(75,1000);
  turn_left();
  go_forward(75,2000);
  turn_left();
  go_forward(75,3000);
}
直進サブルーチンを使ったプログラム

インライン関数

上の例を、今度は「インライン関数」を使って書き直して見ましょう。 ここで言う「関数」には、中学校で習うようなある独立変数(入力)に対して従属変数(出力)が決まる y=f(x) のような形の場合もあれば、ただ動作をするだけで何の値も返さない場合もあります。

値を返す関数の場合には、変数の型と同じく、整数型、小数型、文字型などいろいろな型があります。 一方、値を返さない関数にはvoidという特別な型を指定します。 ちなみに英語のvoidというのは「空っぽの」という意味です。

インライン関数は、マクロと同様、 コンパイルする際に実際の命令に置き換えられます。 つまりサブルーチンと違って、プログラムの実行中にわざわざ呼び出して使う、 ということがないかわりに多用すればプログラムのサイズが大きくなります。 残念ならがNQCでは、このインライン関数しか用意されていません。

#define TURN_TIME 80

void turn_left()  // 関数を定義
{
  Off(OUT_A); OnFwd(OUT_C);
  Wait(TURN_TIME);
  Off(OUT_B);
}

void go_forward (int t)  // 引数を取る関数
{
  OnFwd(OUT_AC);
  Wait(t);
  Off(OUT_AC);
}

task main ()
{
  go_forward(100);
  turn_left();
  go_forward(200);
  turn_left();
  go_forward(300);
}

#define TURN_TIME 800
#define SPEED 75

inline void turn_left()  // inline に注意
{
  OnFwd(OUT_B,SPEED); Off(OUT_C); 
  Wait(TURN_TIME);
  Off(OUT_B);
}

inline void go_forward(int s, int t)
{
  OnFwd(OUT_BC,s);
  Wait(t);
  Off(OUT_BC);
}

task main ()
{
  go_forward(75,1000);
  turn_left();
  go_forward(75,2000);
  turn_left();
  go_forward(75,3000);
}
インライン関数を使ったプログラム

関数

(準備中)

タスク

(準備中)

2台のロボットで通信しよう

(準備中)

RCXは赤外線を使用した通信機能を持っています。 この通信機能を使うことで2台以上のRCXを連携させることができます。 実際には、SendMessageという命令を使って 0〜255の数を「メッセージ」として相手に送ることができます。 受信側のロボットは、最後に受信したメッセージをMessage() で得ることができます。Message()の値をクリアして0に戻すには、 ClearMessage()を使います (従って0は間際らしいの送らないようにしましょう) 。

一方NXTはBluetooth (ブルートゥース) と呼ばれる近距離の無線を 使って4台まで通信することができます。 4台のうち一台がマスター(主)と呼ばれる役割を果たし、 他の3台はスレーブ(従)という役割になります。 Bluetoothを使用するためにはまず設定をしなければいけません。

まず、メインメニューから「Bluetooth」を選び、 「On/Off」で「On」にします(マスター、スレーブ共)。 そうすると次に「Bluetooth」を選んだ時にサブメニューが現れるようになります。 マスター側で、「Search」を選び、対象のスレーブを選びます。

例としてロボットAのバンパーを押している間、 ロボットBが前進するプログラムを作ってみましょう。 基本的には、あらかじめ決めておいた「数」を メッセージとして送受信することでモータを回転または停止させます。

//  送信側プログラム
//
#define SIGNALON 11   // モータ回転のためのメッセージ
#define SIGNALOFF 12  // モータ停止のためのメッセージ

task main ()
{
  SetSensor(SENSOR_1,SENSOR_TOUCH);

  while (true) {
   /* センサー1が押されていれば SIGNALON を送信
      センサー1が押されていなければ SIGNALOFF を送信 */
    if (SENSOR_1 == 1) {
      SendMessage(SIGNALON);
    } else {
      SendMessage(SIGNALOFF);
    }
    Wait(10);  // 少し間を入れて
  }
} 

//  送信側プログラム(マスター)
//
#define CONN 1   // スレーブの接続番号
#define SIGNALON 11   // モータ回転のためのメッセージ
#define SIGNALOFF 12  // モータ停止のためのメッセージ

task main ()
{
  SetSensorTouch(S1);

  while (true) {
   /* センサー1が押されていれば SIGNALON を送信
      センサー1が押されていなければ SIGNALOFF を送信 */
    if (SENSOR_1 == 1) {
      SendRemoteNumber(CONN,MAILBOX1,SIGNALON);
    } else {
      SendRemoteNumber(CONN,MAILBOX1,SIGNALOFF);
    }
    Wait(100);
  }
} 
ロボットAのタッチセンサを押しているときだけロボットBが前進するプログラム (送信側)

NXTのスレーブのメールボックスはMAILBOX1〜MAILBOX10まで使用できます。

// 受信側のプログラム
//
#define SIGNALON 11
#define SIGNALOFF 12

task main ()
{
  ClearMessage(); // Message()の値を0に初期化
  while (true) {
    until (Message() == SIGNALON);
    OnFwd(OUT_AC);
    until (Message() == SIGNALOFF);
    Off(OUT_AC);
  }
}



// 受信側のプログラム(スレーブ)
//
#define SIGNALON 11
#define SIGNALOFF 12

task main ()
{
  int msg;  // 受け取った値を格納する変数
  while (true) {
    /* MAILBOX1から値を受け取りmsgに格納 */
    ReceiveRemoteNumber(MAILBOX1, true ,msg);
    if (msg == SIGNALON) {
      OnFwd(OUT_AC);
    } else {
      Off(OUT_AC);
    }
  }
}
受信側のプログラム

ReceiveRemoteNumber(MAILBOX1, true ,msg); の中のtrueはメッセージを受け取ったあと メールボックスをクリアするためのものです。

ディスプレイに文字を表示しよう

(準備中)

さらにNXTではこんなこともできる

NXTの組み立て説明書に従って超音波センサとサウンドセンサを取り付けたところ

超音波で距離を測ろう

人間の耳はだいたい 20〜20,000Hz (ヘルツ) くらいの周波数の音を 認識できると言われています。 人間の耳では聞こえない高い周波数の音を超音波とといいます。 超音波センサは、 発信した超音波が物体に反射して戻ってくるまでの時間を 測ることで物体までの距離を計算します。 NXTに付属の超音波センサでは40kHz(=40,000Hz)の超音波が使われています。

音速が約340mとして、40kHzの超音波センサが出す超音波が一回振動する間に 進む距離を求めてみましょう。一秒間に40,000回振動して340m進むということは?

さて、 超音波センサを使った例を見てみましょう。 センサの指定方法とセンサの値を得る方法に注意してください。 光センサやタッチセンサと違います。 それ以外は特に難しくないですね。

#define SPEED 50
#define go_forward OnFwd(OUT_BC,SPEED);
#define u_turn OnRev(OUT_B,SPEED); OnFwd(OUT_C,SPEED);\
 Wait(1000); Float(OUT_C);

task main()
{
  SetSensorLowspeed(S4); // 端子4には超音波センサ

  while (true) {
    go_forward;

    /* 25cm以内に障害物があるとUターン */
    if (SensorUS(S4) <= 25) { u_turn; }
  }
}
15cm以内に障害物があるとUターンするプログラム

付属の超音波センサの特性を少し理解するために、 プログラムを作る前に次の練習問題をやっておきましょう。

光センサの値を表示したのと同じ方法で、超音波センサで測った距離を表示させてみましょう。 ロボットの向きをいろいろ変えてみて、値がどう変化するか調べてみましょう。

どのくらい遠くまでまで測ることができるでしょうか? どのくらい近くまで測ることができるでしょうか?

ある程度、超音波センサの特性を理解できたら、次の練習問題に進みましょう。

ロボットに手を近づけたとき距離25センチ以内に近づくと離れ、40センチ以上離れると 近づく、「つかず離れず」ロボットのプログラムを作りましょう。

手を近づけたときなるべく30センチの距離を保つようなプログラムを 作りましょう。ただし、少し近づいた時や少し離れた時はゆっくり 30センチの距離に近づき、 近づきすぎた時や離れすぎたときは全速力で30センチに戻るような プログラムにしてみましょう (30センチからのずれに比例した速さで30センチの距離に戻るような プログラム)。

音に反応するロボットを作ろう

NXTには音の大きさを測るサウンドセンサが付属しています。 使い方は光センサと同様です。 ある一定の大きさ以上の音がしたらUターンするプログラムを作ってみましょう。

#define SPEED 50
#define THRESHOLD 40
#define go_forward OnFwd(OUT_BC,SPEED);
#define u_turn OnRev(OUT_B,SPEED); OnFwd(OUT_C,SPEED);\
 Wait(1000); Float(OUT_C);

task main()
{
  SetSensorSound(S2); // 端子2にはサウンドセンサ

  while (true) {
    go_forward;

    /* 音がしたらUターン */
    if (Sensor(S2) <= THRESHOLD) { u_turn; }
  }
}
音がしたらUターンするプログラム

音がしたら、ピクンと動くロボットのプログラムをつくりましょう。

音の大きさに比例して、より遠くにバックするロボットの プログラムをつくりましょう。

多彩なモータ制御

(準備中)

NXTに付属のモータはいわゆるサーボ・モータと呼ばれるモータで、 回転数(回転角度)を指定して回転させたり、 左右のモータを正確に同期させたりすることが容易にできます。 また、角度センサとして角度を測ることもできます。

命令説明
RotateMotor(OUT_A, 75, 45); 端子Aのモータを75%のスピードで45度前転
RotateMotor(OUT_A, -75, 45);端子Aのモータを75%のスピードで45度後転
OnFwdSync(OUT_AB, 75, 0);端子Aと端子Bのモータを75%のスピードで同期させる

オリジナルのロボットへ向けて

(準備中)

参考文献

ここでは主に、NQCやNXCを使ってマインドストームを動かす上で、参考になるウェブサイトや書籍を紹介します。 最近ではNXCについて取り上げる日本語のウェブサイトも増えていて、 ここで紹介する以外にも素晴らしいサイトがたくさんあるはずです。 ぜひ自分でも探してみましょう。

ウェブ

検索キーワードとしてNQCやNXCのコマンド名を含めると関連ページが見つかりやすいです。

LEGO.com Mindstorms Home (英語) http://mindstorms.lego.com/
レゴ社のマインドストーム公式サイト。 NXTのファームウェアもここからダウンロードできます。
レゴ マインドストーム公式サイト (日本語) http://www.legoeducation.jp/mindstorms/
レゴジャパン(株)による教育用レゴマインドストームの紹介。
NQC Web Site (英語) http://bricxcc.sourceforge.net/nqc/
Dave Baumさんの作成した Mindstorm用コンパイラ NQC のホームページ。 現在は John Hansen さによってメンテナンスされています。 NQCのソースコードおよび、Mac用、 Windows用のバイナリーがダウンロード可。 ソースをコンパイルすれば GNU/Linux でも使えます。
NQC Programmer's Guide (英語PDFファイル) http://bricxcc.sourceforge.net/nqc/doc/NQC_Guide.pdf
NQCの開発者Dave BaumさんとJohn HansenさんによるNQCマニュアル。 まさにNQCの仕様書とも呼べる文書で、 NQCの正確な仕様を調べる時には非常に便利です。
Programming Lego Robots using NQC (日本語PDFファイル) http://www.staff.science.uu.nl/~overm101/lego/tutorial_j.pdf
Mark OvermarsさんによるNQCの入門書 (Alberto Palacios Pawlovskyさんによる日本語訳)
NXCを使ったLEGOのNXTロボットのプログラミング (PDFファイル)
http://www.cc.toin.ac.jp/sc/palacios/courses/undergraduate/freshman/micro_intro/NXCtutorial_j.pdf
Daniele Benedettelliさん製作『Programming LEGO NXT Robots using NXC』 のAlberto Palacios Pawlovskyさんによる日本語訳。
NXCを使ったLEGOのNXTロボットのプログラミング (PDFファイル)
http://www2.ocn.ne.jp/~takamoto/NXCprogramingguide.pdf
上と同じDaniele Benedettelliさんのガイドの高本孝頼さんによる日本語訳。 補足説明付き。
NXC Programmer's Guide (英語) http://bricxcc.sourceforge.net/nbc/nxcdoc/nxcapi/main.html
NXCのすべての命令や定数などが載っているNXCの仕様書のオンライン版。 NQCに比べて桁違いに増えた膨大なコマンドや定数などが説明されています。
こうしろうのMindStorms日記 (日本語) http://itpro.nikkeibp.co.jp/article/MAG/20061214/256937/
2001年当時中学生のお子さんをお持ちの金宏和實さんによるブログ形式の特集記事。 前世紀から続いている長期連載記事で、 初期の頃はNQC、数年前はNXC、最近はAndronid、などの話題が中心ですが、 C言語やJava、PHP、SQLなど他の言語の話題もあります。
LEt'sGOstudio (日本語,英語) http://www.isogawastudio.co.jp/legostudio/
五十川芳仁さんのサイト。LEGOのコミュニティではとても有名な方で、 こんなものまでレゴで作れるの?というようなすごい作品をたくさん製作されています。 RCXやNXTを使った作品も豊富です。
LUGNET - Robotics (英語) http://news.lugnet.com/robotics/
LUGNET (LEGO User Group NETwork) は、LEGOに関する膨大な情報を提供している レゴファンによるコミュニティ・サイトです。 その中のロボティクス関連のページもとても充実していて、 さまざまなプログラミング環境についてのリンク集にもなっています。
MindStormsの洞窟 - はじめてのBricxCC (日本語) http://line.to/mac/MindStorms/BricxCC/
MacさんによるBrixCCの解説。BrixCCはNQCやNXCなどマインドストーム用開発環境の 統合ソフトです。エディタやコンパイラの他サウンド入力用の鍵盤などの 支援ツールが一緒になっています。Windows上でしか動かないのが残念。
ロボティクス入門ゼミ@信州大学 (日本語) http://yakushi.shinshu-u.ac.jp/robotics/
マインドストームを使った信州大学の一年生向けの授業のページ。 最近はRISだけでなくNXTも使っています。 この講座で使用したDebian Liveシステムのダウンロードもできます。

書籍

(作成中)

Jin Sato,『Jin Sato の LEGO MindStorms 鉄人テクニック』, ISBN-13: 978-4274086823
Dave Baum et al.,『Definitive Guide to LEGO Mindstorms』, ISBN-13: 978-1590590638
Dave Baum et al.,『Extreme Mindstorms』, ISBN-13: 978-1893115842
John C. Hansen,『LEGO Mindstorms NXT Power Programming』, ISBN-13: 978-0973864922
Mario Ferrari et al.,『Building Robots With Lego Mindstorms』, ISBN-13: 978-1928994671
Yoshihito Isogawa,『The LEGO Technic Idea Book: Wheeled Wonders』, ISBN-13: 978-1593272784
Yoshihito Isogawa,『The LEGO Technic Idea Book: Simple Machines』, ISBN-13: 978-1593272777
Yoshihito Isogawa,『The LEGO Technic Idea Book: Fantastic Contraptions』, ISBN-13: 978-1593272791
Mario Ferrari et al.,『Building Robots with LEGO Mindstorms NXT』, ISBN-13: 978-1597491525
Daniele Benedettelli,『Creating Cool MINDSTORMS NXT Robots』, ISBN-13: 978-1590599662
David J. Perdue,『The Unofficial Lego Mindstorms Nxt Inventor's Guide』, ISBN-13: 978-1593272159