2013年5月20日月曜日

LEGO Mindstorms NXT はじめました。

 巷では LEGO Mindstorms EV3 の9月リリースのニュースが出ています。自分はLEGO Mindstorms は殆どさわったことがなかったのですが、以前から興味はありました。EV3 を思い切って予約しようかとも思ったのですが、発売直後はまだ情報が少ないだろうと思い、敢えて、ネット上にも豊富に情報がある現行の NXT をはじめてみようと思いました。

 とりあえず、マニュアルに掲載されていたサンプルを組み立ててみました。マニュアルのサンプルに使用する部品はわかりやすいように「Start Here!」という袋に分けてくれてあるので、その袋の部品をトレーに取り出して、マニュアルのとおりに組み合わせていきます。






 説明書のとおりに組み合わせていくだけで、あっという間に、簡単に差動二輪の車輪ロボットベースが出来てしまいました。すごい・・・。でも、サンプルで指示通りに組み上げるだけなので、簡単ですが、自分で設計(デザイン)しようと思ったら、大変そう・・・。自分にとっては、このサンプルの組立例でもう十分目的を果たしそうな・・・。

 で、ギークなレゴビルダーの人たちは、これで色々なものを作っちゃうわけですが、自分には無理そうなので、違う路線で攻めてみようと思います。

 レゴの中でもこのMindstormsの最大の特徴はインテリジェントブロックでのプログラミングということになるなわけですが、標準ではLabVIEWベースと思われるグラフィカルプログラミング環境が付属してきます。自分はコードを書いてしまう方がなじむ気がするので、フリーで入手できる Bricx Command CenterというIDEで NXC というC言語ライク(Not eXactly Cだそうです)なプログラミングが出来る環境があったので、これを使ってみることにしました。このNXCでセンサの値を取得して、モータを動かして・・・というのは多分Googleで検索すれば出てくると思うので、もう少し違うことをしてみます。

 このNTXのインテリジェントブロックというかコントローラと各センサやモータは専用ケーブルで接続されます。このケーブルの両端、あるいはコントローラやセンサ・モータのコネクタ部分はISDNのような6芯のモジュラーに似ているんですが、爪の位置が真ん中からずれている特殊なものなので、自作が難しいです。なので、前回のロビのサーボケーブルのようにちょん切ります・・・。で、ちょん切ってどうするかというと、やっぱり、Arduinoにつなぎます。Wikipediaなどを見ると6本の信号線の情報なども掲載されていました。


 この中で気になるのが、5、6ピンです。I2C(SCL, SDA)やRS485(A, B)が機能として割り当てられています。そこでこれらの5、6ピンを使ったI2CとRS485について調べてみることにします。


 ところで、全然話が違うのですが、今回、(レゴブロックを整理するのも合わせて)、在庫部品の保管方法を少し見なおそうと思い、ホームセンターでプラスチック製の連結できる引き出しを買ってきました。今まではアマゾンの箱に分けて入れた部品を段積みにしていましたが、


下の方のものを取り出したりするときにハノイの塔みたいな状態になって、取り出して元に戻すのが結構面倒でした。引き出しに変えて、




引き出しの中に、今まで使っていた段ボール箱の紙を切って仕切りを作ったりして、部品類も値ごとに取り出しやすくなりました。ロビの置き場所も確保できたし、レゴもパーツ分けして整理する予定です。


 さて、レゴの話しに戻ると、まずは、I2CでのNXT - Arduino通信ですが、NXTがI2Cマスタなので、ArduinoをI2Cスレーブとして動かして上げることで、動作確認できました。ただ、プルアップ抵抗の値が微妙なところがあったような感じで、100kΩくらいが安定していました。2.2kとか10kだとエラー(送受信値が化ける)がありましたが、原因・詳細は調べていません。


 Arduino側が5VでNXT側の電圧が不明(4.4Vくらい?)だったのでI2Cレベルコンバータを間に入れたんですが、これが影響してるのかな?このレベルコンバータ自体にはプルアップ抵抗は入っていなかったはずだけど。
 で、ArduinoでI2C通信できたので、ストリナのI2Cコンパスをつないで値を取ろうとしたんですが、うまくいきませんでした。ロジアナで見ても、コンパスからの応答がない状態でした。ロジアナで見て気がついたんですが、I2C通信のクロックが1kHz前後くらいのように見えます。たしか、I2Cは100kHzとか400kHzとかじゃなかったっけ?クロック同期式なので、遅い分にはマスタが出すクロックに合わせて動く規格なんだろうけど、あんまり想定外の遅さでコンパスが反応しないのかな?う~ん、Mindstorms純正じゃない市販の安いI2C式のセンサを使えないかって思ったんだけど、難しいかな?アナログ入力の複数のセンサをArduinoで束ねて、Mindstormsと接続っていう感じでの使い方はありかもしれない。
以下は、お試しに使った、Mindstorms側のNXCのソース。
#include "NXCDefs.h"

#define I2C_PORT IN_1
#define READBUF_SIZE 2

byte writeBuf[2];
byte slaveAddrs1 = 0x21;
byte requestCmd = 0x41;
byte readBuf[READBUF_SIZE];

task main()
{
  int readCount = 0;
  int returnCount = 0;
  int i;
  byte n_read;
  
  // 受信バッファをクリア。
  for (i = 0; i < READBUF_SIZE; i++)
    readBuf[i] = 0;
  // 入力ポートをI2Cポートとして初期化。
  SetSensorLowspeed(I2C_PORT);
  
  // LCD画面クリア。
  ClearScreen();
  
  // メインループ。
  while(true)
  {
    if (ButtonPressed(BTNLEFT, false))
    { // 左ボタンを押されたら、
      TextOut(0, TEXTLINE_1, " LEFT");
      writeBuf[0] = 0x42;
      writeBuf[1] = 0x42;
      // I2C送信。
      I2CWrite(I2C_PORT, returnCount, writeBuf);
    }
    
    if (ButtonPressed(BTNRIGHT, false))
    { // 右ボタンを押されたら、
      TextOut(0, TEXTLINE_1, " RIGHT" );
      writeBuf[0] = 0x42;
      writeBuf[1] = 0x41;
      // I2C送受信。
      I2CBytes(I2C_PORT, writeBuf, readCount, readBuf);
    }
    
    // 時間待ち。
    Wait(10);
  }
}
以下、Arduino側のソース。
#include <Arduino.h>
#include <Wire.h>

byte myAddress = 0x21;
byte receiveData[10];
int receiveCount = 0;
byte sendData = 0x00;

// I2Cマスタからデータを受信した時のハンドラ。
void onI2cReceiveEvent(int getDataCount)
{
  int i;
  
  for (i = 0; i < getDataCount; i++)
  {
    receiveData[i] = Wire.read();
  }
  receiveCount = getDataCount;
}

// I2Cマスタからデータ要求を受信した時のハンドラ。
void onI2cRequestEvent()
{
  Wire.write(sendData);
}

// setup
void setup()
{
  // シリアルポート初期化。
  Serial.begin(115200);
  
  // I2Cスレーブとして初期化。
  Wire.begin(myAddress);
  // I2Cマスタからのデータ受信ハンドラを登録。
  Wire.onReceive(onI2cReceiveEvent);
  // I2Cマスタからのデータ要求受信ハンドラを登録。
  Wire.onRequest(onI2cRequestEvent);
}

// メインループ
void loop()
{
  int i;
  
  if (receiveCount > 0)
  {
    // ここに受信時の処理を記述。
    for (i = 0; i < receiveCount; i++)
    {
      Serial.print(receiveData[i], HEX);
      Serial.print(", ");
    }
    
    receiveCount = 0;
  }
}


 次に、もう一つのRS485での通信を試します。こちらが実現出来れば、I2Cも含めたセンサなどの処理をArduino側で分担させて、必要な指示なり結果をRS485でインタフェースするようなことができると思います。
 まずはRS485通信のためのArduino側のハードですが、今回は、シリアル-RS485変換モジュールがあったので、これを使うことにしました。(この変換モジュールはMAX485の互換チップが載っていて、~REとDEが結線された、よくある使い方のものです。)

Arduino側は今回はUnoを使ったので、SoftwareSerialで485の通信を行うことにしました。通信速度は38600bpsとしました。Mega(ADK)等を使うのであればPCとのUSB接続とは別にハードウェアのシリアルポートが利用できるので、通信速度も115200bpsまで使えると思います。Arduino側の(ソフトウェア)シリアル通信のRxD、つまりArduino側の受信ポートを485モジュールからの出力Tx-Oと接続、Arduinoの送信ポートTxDを485モジュールへの入力Rx-Iへ接続、Arduino側の送信制御用ポートを485モジュールのRTSへ接続します。Mindstormsの485通信のプロトコルがわからないので、とりあえず、Arduino側はRTSへの出力を通常はLowにしておき、いつでも受信できる状態としておき、Arduino側から送信したい時だけ、RTSをHighにして、送信を行い、また、RTSをLowにするような動作とします。RS485ライン側のMindstormsとの接続ですが、485モジュールのAをMindstormsのモジュラー端子6番のAで、Bを5のBでOKでした。485のA線B線表記はバラバラで統一がない(Aが負論理でBが正論理が正しいと聞いたことがあるが、ドライバICだと逆だったりもする)ので、とりあえず、試してみて、ひっくり返して付け替えるとか、ロジアナで覗いて確認してみるとかしないと、結局どっちなのかわからないことが多い気がします。


 当初、NXT側での受信動作がうまくいきませんでした。色々試したんですが、八方塞がりで諦めかけたんですが、結局、NXT側のファームウェアを別のものに書き換えたら動いてしまいました。ただし、受信したデータ(現段階では一文字のアルファベットを受信する動作)をNXT側の液晶に表示するところがうまく出来ません。使い方を間違っているのか?また、ファームの問題なのか?


 いずれにしても、このファームの問題で受信動作がうまく行かなかったことで、かなり、時間を食ってしまいました。
 以下、NXCのソース。
// RS485 communication test.

#include "NXCDefs.h"

inline void WaitForMessageToBeSent()
{
  bool sending, avail;
  
  RS485Status(sending, avail);
  
  while(sending)
  {
    Wait(MS_1);
    RS485Status(sending, avail);
  }
}

task main()
{
  int i = 0;
  string buffer;
  string sendMsg;
  
  // Configure the S4 port as RS485
  //SetSensorType(IN_4, SENSOR_TYPE_HIGHSPEED);
  UseRS485();
  RS485Enable();
  //RS485Control(HS_CTRL_INIT, HS_BAUD_9600, HS_MODE_DEFAULT);
  RS485Uart(HS_BAUD_38400, HS_MODE_8_DATA|HS_MODE_N_PARITY|HS_MODE_10_STOP);
  
  Wait(MS_10);
  
  SendRS485String("Hello,RS485!");
  TextOut(0, LCD_LINE1, "SendRS485String()");
  
  Wait(MS_10);
  
  while(true)
  {
    if (ButtonPressed(BTNLEFT, false))
    { // 左ボタンを押されたら、
      ClearScreen();
      TextOut(0, LCD_LINE1, " LEFT");
      sendMsg = "Left button pushed.";
      SendRS485String(sendMsg);
      WaitForMessageToBeSent();
    }
    
    if (ButtonPressed(BTNRIGHT, false))
    { // 右ボタンを押されたら、
      ClearScreen();
      TextOut(0, LCD_LINE1, " RIGHT");
      sendMsg = "Right button pushed.";
      SendRS485String(sendMsg);
      WaitForMessageToBeSent();
    }
    
    // 受信処理。
    if (RS485DataAvailable())
    {
      ClearScreen();
      RS485Read(buffer);
      SendRS485String(buffer);
      TextOut(0, LCD_LINE1, buffer);
    }
  }
}
以下、Arduino側のソース。
#include <Arduino.h>
#include <SoftwareSerial.h>

// RS485ピン定義(SoftwareSerial使用)
const int rs485RtsPin = 9;
const int rs485RxPin = 10;
const int rs485TxPin = 11;

SoftwareSerial rs485(rs485RxPin, rs485TxPin);

void RS485_initialize(void)
{
  //  SoftwareSerial初期化。
  rs485.begin(38400);
  // DTS出力ピン設定。
  pinMode(rs485RtsPin, OUTPUT);
  // 受信(DTS=0)状態に設定。
  digitalWrite(rs485RtsPin, LOW);
}

void RS485_write(byte data)
{
  // DTS出力ピンを送信(DTS=1)に設定。
  digitalWrite(rs485RtsPin, HIGH);
  delayMicroseconds(5);
  // 送信。
  rs485.write(data);
  // 受信(DTS=0)状態に設定。
  digitalWrite(rs485RtsPin, LOW);
}

void RS486_write(byte data[], int count)
{ 
  // DTS出力ピンを送信(DTS=1)に設定。
  digitalWrite(rs485RtsPin, HIGH);
  // 送信。
  for (int i = 0; i < count; i++)
 { 
    rs485.write(data[i]);
 }
  // 受信(DTS=0)状態に設定。
  digitalWrite(rs485RtsPin, LOW);
}

// 初期化。
void setup()
{
  // シリアルポート初期化。
  Serial.begin(9600);
  
  // RS485初期化。
  RS485_initialize();
}

void loop()
{
  int count;
  
  // RS485受信処理。
  if (rs485.available())
  {
    Serial.write(rs485.read());
  }
  
  // シリアルポート受信処理。
  if (Serial.available())
  {
    RS485_write(Serial.read());
  }
}

現時点では、とりあえず、動いたような・・・って感じなので、もう少し調査が必要ですが、NXTとArduino等のマイコンのインタフェースの可能性が一応開けたかな?
 あっ、そうそう、このRS485に関してはNXT側は必ずポート4を使う必要があります。(I2CはIN_1からIN_4のどこでもプログラム内で指定して使えますが、RS485はIN_4固定です。)