2011年5月22日日曜日

ドリトルV2.2のArduino対応がニクイ

もはや旧聞に属する話題ではありますが、去る4月11日、ドリトルの久々のバージョンアップ、2.2版が出ました。

あわせて第2版のテキストも出版されていますので、ぜひお求めになるとよいと思います。初版よりも、断然実用的な教科書になっていますし、自習書としての使いやすさも格段によくなっています。なお、お金のない学生さん、立ち読みしてみたい先生方は、V2.2の配布パッケージに本文まるごと入ったPDF(表紙を除く全て)が含まれていますので、そちらを見ていただくとよいのではないかと思います。ただし、B5版の書籍に合わせて作られていますので、そのままA4の用紙に印刷するととても大きな文字になってしまいますので、印刷するときにはご注意を。

ところで、V2.2では、外部接続できる機器として、従来のMYUロボ基板に加えて、Arduinoが扱えるようになりました。今回は、このArduino対応のしくみを覗いて、ドリトルの奥深さに触れてみようという趣旨です。

MYUロボ基板では、基板上のPICに書きこまれたファームウェアが実装した命令を、シリアルポート経由で送信したり、結果を受け取ったり、また自律動作させられるプログラムを書き込むことができました。

V2.2のArduino対応では、この、最後の「Arduinoへのプログラムの書き込み」は実現されていません。したがって、Arduinoとドリトルを動かすコンピュータとの間は、つねにUSBケーブルで接続されている必要があります。形態としては、ドリトルから、Arduinoに接続されたデバイスから値を読んだり、デバイスを制御したりするというものです。ScratchにおけるPicoBoard(他に、Sparkfun製互換品, 韓国のPinyチームが作った互換品Helloboard)のような位置づけです。

したがって、「Arduinoにつながる膨大に存在するデバイスを、Arduino IDEの言語を使わずに簡単に書きたい!」という期待には、いまのところModKitしかない(しかもまだ開発中)というところです。

それはともかく、ドリトルとArduinoをつなぐ、「ニクイ」しくみについてみていくことにしましょう。

Arduinoのスケッチ


ドリトルとArduinoがお話できるようにするために、Arduinoに専用のスケッチを書きこむようになっています。このスケッチを書き込むことで、ドリトルV2.2用Arduinoができあがるというわけです。もちろん別のスケッチを書き込んで、いつでも本来のArduinoに戻すこともできます。詳しくは、第2版テキストの105ページを参照してください。

スケッチは、ドリトルのフォルダ(Windowsの場合、dolittle.jarがあるフォルダ。Macの場合、~/Library/Dolittleの下)にある、arduino_dolittleというフォルダの下にある、"arduino_dolittle.pde"が本体です。なんと、たった41行しかありません。

Arduinoのスケッチは、2つのvoid型の関数からなります。ひとつは、リセット時に一度だけ実行されるsetup()関数、もうひとつは無限ループするloop()関数です。他に下請けとなる関数を自由に定義することもできますが、ドリトルV2.2では、この2つだけを使っています。

ではまず、setup()関数からみていきましょう。
void setup(){
  Serial.begin(9600);
  Serial.write(' ');
}
たった2行です。1行目は、標準のシリアルオブジェクトの初期設定をしています。このオブジェクトは、USBで接続されているPCと通信するほか、Arduino基板のデジタルポートD0, D1のRX, TXにも対応しています(並列接続です)。これを標準的な9600bpsの速度で設定しているというわけです。2行目では、このシリアルオブジェクトに半角空白(文字コード0x20)をひとつ送信しています。これは、ドリトル側に、「初期設定が終わったよ」ということを知らせていると考えられます(あとで確かめましょう)。

次に、loop()関数です。少々長いので、少しずつみていきます。まず最初の部分。

void loop(){
  int wait=5;
  if (Serial.available() > 0) {
    int in = Serial.read();
    int cmd = in & 0xf0;
    int port = in & 0xf;
整数型変数waitの値を5に設定しています。そのあと、シリアルオブジェクトが何か受信しているかどうかをif文で確かめています。このif文は、loop()関数の最後で閉じられているので、シリアルオブジェクトが何も受信していなければ、以下なにもせずにloopを繰り返します。

受信した場合、先頭の1文字(1バイト)を整数型の変数inに読み込みます(read())。そして、その結果を、1バイト(8bit)の上半分と下半分に分割しています。

「in & 0xf0」というのは、0xf0が二進数で0b11110000ですから、それと論理積をとることによって、上4bitだけをそのままに、下4bitは0b0000に置き換えて、変数cmdに記憶することになります。そして、「in & 0xf」で、つまり0b00001111ですから、下4bitだけをそのままに、上4bitを0b0000に置き換えて変数portに記憶しています。

これで、ドリトル側から送られてきた1バイトのデータを、cmdとportの2つに分割して得たことになります。では残り。
    switch (cmd) {
    case 0x20: // pinMode(Dout)
      pinMode(port ,OUTPUT);
      delay(wait);
      break;
    case 0x30: // pinMode(Din)
      pinMode(port ,INPUT);
      delay(wait);
      break;
    case 0x40: // Dout
      delay(wait);
      if (Serial.read() == 1) { digitalWrite(port, HIGH); } else { digitalWrite(port, LOW); }
      break;
    case 0x50: // Din
      Serial.write(digitalRead(port));
      break;
    case 0x60: // Aout
      delay(wait);
      analogWrite(port, Serial.read());
      break;
    case 0x70: // Ain
      Serial.write(analogRead(port)/4);
      break;
    default :
      break;
    }
  }
}
長くなりましたが、1行目のswitch文がこの部分のすべてです。

つまり、cmdに記憶された値によって、どんな動作をするかを分岐しているというわけです。cmdは「コマンド」の意味だと思われます。

そして、対応するcase文が6つありますから、6種類のコマンドが定義されている、ということがわかります。では、順にみていきましょう。

case 0x20ということですが、以下、下半分の4bitは省略して、「2番コマンド」などと書くことにします。

2番は、コメント(//記号以後、行末まで)にあるように、Arduinoの入出力ピンの方向(入力か、出力か)を決めているようです。実際に実行されているのは
pinMode(port, OUTPUT);
delay(wait);
 だけですから、さきほど記憶した、下4bitがポート番号(port)として使われ、対応する番号のピンを「出力」に設定していることがわかります。Arduinoのデジタル出力は13番までしかありませんから、4bitあれば足りるという設計だったわけですね。そして、設定したあと、変数waitに記憶した分だけちょっと待ちます。Arduino搭載のAVR CPUがきちんとポート設定をする時間をみているわけですね。waitの値は5だったので、5マイクロ秒待つことになります。

次に、3番コマンドを見てみましょう。
      pinMode(port ,INPUT);
      delay(wait);
今度は「入力」に設定していますね。

4番コマンドはどうでしょう。
      delay(wait);
      if (Serial.read() == 1) { digitalWrite(port, HIGH); } else { digitalWrite(port, LOW); }
ちょっと長くなっています。

先に5マイクロ秒待ってから、シリアルオブジェクトから1バイト値を取り出し、その値が1であれば、指定されたポートをHIGHに設定し、そうでなければLOWに設定する動作をしています。つまり、出力に設定したデジタルポートの状態を変更しているというわけです。1ならHIGHですから、普通なら0が指定されたときLOWにする、ということが予想されます(これは、ドリトル側がどうなっているかをみて、確かめましょう)。

5番コマンドは、
      Serial.write(digitalRead(port));
入力に設定されたデジタルポートの状態を読み取って、シリアルオブジェクトに送信しています。これは、HIGHなら1、LOWなら0がドリトル側に送信されるということになるでしょう。

6番コマンドは、
      delay(wait);
      analogWrite(port, Serial.read());
5マイクロ秒待ってから、ドリトルから送られてきた値をアナログ出力ポートに送信しています。Arduinoでは、アナログ値をポートに書きこむ命令は、PWM(パルス幅変調)を実行します。値が小さければ、1周期の間でHIGHになっている時間が少なく、大きければHIGHの時間が多くなります。これを使って、モータの速度調整をしたり、LEDの明るさをかえたりすることができるのです。先に待ち時間を入れているのは、ドリトルが値を送信する時間を待つためと考えていいでしょう。

最後の7番コマンドは、
      Serial.write(analogRead(port)/4);
こうなっています。アナログポートが入力した値を4で割って、それをシリアルオブジェクトに送信しています。これは、アナログポートに接続されたセンサーなどの値を読み取って、ドリトルに送信する役割を果たしていると考えられます。ちなみに、4で割っている理由ですが、ArduinoのA/Dコンバータ(電圧の高さをデジタル値の大きさに変換する回路)は、10bitの分解能をもっています。しかし、ドリトルとの通信は1バイト(8bit)でやりたい。そのために、10bitの分解能を8bitに減らす必要があります。そのために、10bitの数値を2bit分右シフトして8bitにするわけです。4で割るのは、2の2乗で割るということなので、要するに2bit右シフトすることと同じです。個人的には、「>>」という右シフト演算子を使って、「analogRead(port) >> 2」と書きたいところです。いずれにせよ、同じ意味です。

残るdefaultは、上記にあてはまらない場合、ということですから、0番、1番のコマンドということになります。なにもせずにbreak;していますから、未定義というわけです。

ドリトルのプログラム


ドリトルV2.1以後、外部機器に関する定義は、.iniの拡張子をもつファイルに記述するようになりました。いままではmyu.iniだけがあり、ミュウロボ基板に関する定義が書かれていました。V2.2では、arduino.iniというファイルがドリトルのフォルダにあります。

また、今後iniファイルの数が増えていく可能性がありますので、すべてを読み込むとドリトルが不必要にメモリを消費してしまうため、どのiniファイルを読み込むか、ということを最初に指定することになりました。これは、「システム」オブジェクトに「使う」という命令が定義されています。したがって、arduino.iniを読み込むためには、みなさんが作成するプログラムの最初に、
システム!"arduino" 使う。
 と書かなければなりません(テキスト106ページ参照)。

というわけで、arduino.iniファイルはドリトルの命令が書かれていて、それを読めば、上で説明した、Arduinoのスケッチに対応したどんな命令が使えるようになるのかがわかる、ということになります。

arduino.iniは、59行あります。以下、先頭から少しずつ読んでみましょう。

まず1行目です。
「シリアルポート == 未定義」!なら「システム!"エラー: Arduinoオブジェクトを作れません。" messagedialog」そうでなければ「Arduino=シリアルポート!作る。
 一般のドリトルのプログラムでは見慣れない記述で少々戸惑うかもしれません。しかし、テキストの付録をよく調べていただければ、意味はわかっていただけると思います。ここでは、「だいたいなにが書かれているか」の説明にとどめます。原理は、テキストの付録で勉強してください。

この条件文ですが、“「シリアルポート == 未定義」!なら”という条件になっています。ドリトルは、起動時にOSにシリアルポートがあるかどうかを問い合せて、あれば「シリアルポートオブジェクト」が作られます。なかった場合、つまり、Arduinoに対応したUSB-シリアル変換のデバイスドライバが正しくインストールされていないとか、そもそもUSBにArduinoが接続されていない場合には、シリアルポートオブジェクトが作られません。これを確かめている、というわけです。もし存在しなければ以後のプログラムは動かすだけ無駄なので、ユーザに警告を出します。“システム!... messagedialog.”という命令で、ダイアログに警告メッセージを出しています。そうでなければ、“Arduino=シリアルポート!作る。”命令で、Arduinoという名前で以後通信するシリアルポートにアクセスできるようにします。


空白行をひとつおいて、3行目以後は、このArduinoオブジェクトに追加するメソッド定義です。つまり、Arduinoオブジェクトの命令定義となります。まず、「ひらけごま」命令から。
Arduino:ひらけごま=「|_port|
  自分!(_port)開く。
  w=0.01。r=undef.
  「c=自分!データ数?。「(c)>0」!なら「w=0」実行。自分!(w)待つ」!20 繰り返す。
  「c>0」!なら「r=自分!(c)値?」実行。
  w=0.01。r=undef.
  「c=自分!データ数?。「(c)>0」!なら「w=0」実行。自分!(w)待つ」!100 繰り返す。
  「c>0」!なら「r=自分!(c)値?」実行。
  自分。
」。
「ひらけごま」命令は、引数をひとつとります。ポート名です。これは、ドリトルがインストールされているコンピュータにArduinoを接続しているとき、OSが認識しているポートの名前です。例えばWindowsなら「COM5」とか、MacにArduino Unoをつないでいる場合なら「/dev/cu.usbmodem26231」のようなものです。これを、_portという変数で受け取ります。

しばらくなにやら値を読むことを繰り返していますが、最終的に読んだ値を使っている形跡はありません。ここで、Arduinoのスケッチのsetup()関数のなかで、最後に空白文字をドリトル側に送信していたことを思い出してください。とにかくこれがやってくるまではArduino側の準備はできていませんから、それが届くまではゆっくりと待ち、届いたら待ちなしでさっさと繰り返しを終わるようなことをしているのではないかと思われます。

そして、肝心なのは、最後の“自分。”です。これを最後に書くことで、「ひらけごま」命令を実行したときに、Arduinoオブジェクトを返します。なんのことやらわからないと思いますが、ドリトルで「カスケード」と呼ぶ文法、つまり、同じオブジェクト(例えばカメ太)に連続して命令を書く記述方法がありますが、これを実現するために必要なことです。ドリトルは、カスケードしている場合、左からひとつ命令を実行して、その結果返ってきたオブジェクトに対して次の命令を与えることを文末まで繰り返すのです。
Arduino:とじろごま=「!とじる」。
これはあっさりと、シリアルポートオブジェクトが持っている「閉じる」命令をそのまま「とじろごま」命令に定義してます。

次から面倒な話が続きますが、ご容赦ください。どんどんドリトルの内部に入っていきます。
Arduino:newobj=「|a|
  obj=""。
  obj:_arduino=a。
  obj:getcmd=「|base port| obj:base=base。(base+port)!コード文字」。
  obj:待つ=「|t|_arduino!(t)待つ」。
  obj:書く=「|v|_arduino!(cmd) 出力。_arduino! (v) 出力。!0.001 待つ。」。
  obj:読む=「
    _arduino!(cmd) 出力。
    w=0.01。
    「c=_arduino!データ数?。「(c)>0」!なら「w=0」実行。_arduino!(w)待つ」!20 繰り返す。
    「c>0」!なら「(_arduino!(c)値?)!文字コード」そうでなければ「0」実行。
  」。
  obj。
」。
「newobj」命令を定義していますが、これは日本語では「作る」命令に対応しています。“a=Arduino!作る。”と書いたときの「作る」です。最初に“|a|”と、変数宣言されていますが、これは親オブジェクトを受け取るための引数だと思ってください。Arduinoオブジェクトの親オブジェクトはシリアルポートオブジェクトです。

次にわかりにくいのは、“obj=""。”でしょう。空の文字列をobjという変数に代入しています。これは実はドリトルで、全く新種のオブジェクトを定義するときの常套句で、「特にどれということはないが何か新しいオブジェクトを作ります」というときには、文字列オブジェクトの子どもとして作ることになっています。ドリトルはプロトタイプベースのオブジェクト指向言語なので、システムが用意しているオブジェクトの子ども以外のオブジェクトを作ることができません。そこで、「なんでもいいけど、印刷とかすぐできるし文字列の子どもにでもしておこうか」ということが多いのです。

というわけで、仮のオブジェクトであるobjに以下、属性(変数やメソッド)を定義しています。まず、親オブジェクト(シリアルポートオブジェクト)を_arduinoとして覚えることにして、以下使えるようにします。次にgetcmd命令を定義して、コマンドを表す上4bitとポート番号を表す下4bitを結合する作業を定義しています。引数baseがArduino側で解釈されるコマンド番号、portがArduinoの入出力ポートの番号です。足した値を「コード文字」で文字に変換することで、シリアルポートオブジェクトの「出力」命令に対応しています。次の「待つ」命令は、シリアルポートオブジェクトの「待つ」命令をそのまま使っています。次の「書く」命令ですが、cmdという変数が登場します。しかし、ここではこの変数の値は未定義です。別の命令で、この値を設定してから「書く」命令を呼び出す必要があるということです。次の「読む」命令は少し長いので、段落を改めることにします。

「読む」命令では、最初にcmdの値を出力しています。デジタル入力かアナログ入力か、どちらかわかりませんが、どちらかに対応したコマンド番号が送られるはずです。続く2行は「ひらけごま」命令にもありましたが、20回データが届くまで待ってから、届いた1つの値を「文字コード」命令で数値に変換し、もし値が返ってきていなければ0を値としています。そして、最後の“obj。”で、「作る」したオブジェクトを返します。

以下、Arduinoのポートを入力・出力のどちらかに設定する命令の定義になります。
Arduino:デジタル出力=「|portstr|
  obj=Arduino!(自分)newobj。
  obj:cmd=obj!0x40 (portstr) getcmd。
  自分!(obj!0x20 (portstr) getcmd) 出力。
  !0.01 待つ。
  obj。
」。
「デジタル出力」命令は、ポート番号を引数にとり、それと、デジタル出力への設定を行います。ただ、予め(通常、続けて実際に値を出力する「書く」命令が実行されるはずなので)デジタル値の出力に対応するコマンド番号4(0x40)をgetcmdで組み合わせてcmdに設定し、続けてデジタル出力設定のコマンド番号2(0x20)をArduinoに送っています。ここでも最後にobj。を書いて、カスケードに対応します。
Arduino:デジタル入力=「|portstr|
  obj=Arduino!(自分)newobj。
  obj:cmd=obj!0x50 (portstr) getcmd。
  自分!(obj!0x30 (portstr) getcmd) 出力。
  !0.01 待つ。
  obj。
」。
「デジタル入力」命令でも同様に、cmdに値を読むコマンド5(0x50)を設定してから、ポートをデジタル入力に設定するコマンド番号3(0x30)をArduinoに送っています。
Arduino:アナログ出力=「|portstr|
  obj=Arduino!(自分)newobj。
  obj:cmd=obj!0x60 (portstr) getcmd。
  obj。
」。
「アナログ出力」コマンドは、ArduinoでPWM出力できるピンは決まっていて、予め出力の設定が不要なので、単にコマンド番号6(0x60)をcmdに設定するだけです。値を出力するのは、newobj命令のなかで定義した「書く」命令が実行します。
Arduino:アナログ入力=「|portstr|
  obj=Arduino!(自分)newobj。
  obj:cmd=obj!0x70 (portstr) getcmd。
  obj。
」。
「アナログ入力」コマンドも、単にコマンド番号7(0x70)をcmdに設定しているだけです。実際に値を読むのは、「読む」命令が担当します。

最後の
」実行。
は、なんと1行目の条件分岐に対応しています。つまり、ここまでを、シリアルポートがOSにあった場合にやりますよ、というわけです。

これで、すべての解説が終わりました。実際にみなさんがArduinoでセンサーの値を調べたり、出力デバイスを制御したりするプログラムの書き方は、テキストの105ページから110ページまでに書かれています。ぜひ試してみてください。また、Arduino以外の別のデバイスをドリトルと通信させることも、ここで解説した内容の応用でできるかもしれません。

参考になれば幸いです。

やや蛇足


最後に、蛇足になりますが、Arduinoに接続するデバイスで、高級なもの(LCDや7seg LED、SDカードやGPSなど)は、いまのところ厳密には対応していません。特に、GPSのようにシリアルポートが必要なデバイスは、Arduino標準のSerialオブジェクトがドリトルとの双方向通信でふさがっているため、そこには接続できません。Arduinoのスケッチ側で、別のデジタル入出力ポートを指定したSerialオブジェクトを作成して、そこで通信しなければなりません。

これを実現するためには、「新しいシリアルポートを追加する」コマンドの追加と、「シリアルポートを指定して値を読み書きする」コマンド2つの、全部で3つのコマンドが必要です。

幸い、コマンドには4bitが割り当てられており、まだ6つしか使っていませんから、10個のコマンドが原理的には追加できるはずです。そのため、まずArduinoのスケッチで変数cmdをintではなく、unsigned intで定義しておくのが無難ではないかと思います。

Arduinoに接続するデバイスのうち、I2CやSPIプロトコルで接続するものも、ドリトル側でクロック生成するのは難があるかもしれません。その場合はまた、コマンドの追加ということになるかもしれません。

ただ、いずれにせよ、Arduino側にインタプリタを置いて、ドリトルとインテリジェントに通信するというアイディアは実にナイスだと思います。