地磁気センサー搭載(2014/12/23)

ロボット・インフィニティ(TypeA)の課題を解決する2つの対策をしました。

 

----------

1. ロボットカーの向きを正確に把握できるように地磁気センサーを搭載

以前はGPSモジュール(QZPOD)から出力される「$GPRMC」に含まれる「進行方向」を「ロボットカーの向き」として利用していた。$GPRMCの「進行方向」はある程度の速度がないと出力されないうえに、精度が低かった。そこで、地磁気センサーを搭載し、ロボットカーが静止していても正確な方位が取得できるようにした。

 

2. ロボットカーの状態を把握できるようにシリアル通信できるBluetoothモジュールを搭載

以前は離れた場所からロボットカーの状態が把握できず、デバッグしづらかった。(前回のブログでLCDシールドを搭載したが、ロボットカーと一緒に移動しないとLCDが確認できない上、ログが残らず不便だった。)そこで、ロボットカーにシリアル通信できるBluetoothモジュールを搭載し、PCやスマホにログを出力できるようにした。

----------

 

公園で試走をしたところ、位置情報の精度が低かったため、静止するポイントにばらつきはありましたが、ほぼ想定通りの走行ができました。


使用部材

部材 価格

 QZPOD

(QZS/GPS受信機)

事務局より借用

無料

Aeduino互換ボード [Romeo V2 All In One Controller]

(2個のDCモーターを制御できるArduino互換ボード)

秋月電子通商

3,990円

3軸 デジタル・コンパス モジュール [HMC5883L使用]

(ロボットカーの向きを$GPRMCより正確に取得するために搭載)

アマゾン

300円

Bluetoothモジュール [EGBT-046S-P]

(ロボットカーのログ出力用/シリアル通信なので扱いが簡単)

aitendo

750円

USBホストシールド [USB Host Shield 2.0 for Arduino]

(QZPODとUSB接続するためのシールド)

アマゾン

1,193円

モバイルバッテリー [DE-M01L-3015MX1]

(Arduino用電源)

ヤマダ電機

1,540円

[単3型充電池 [SONY BCG34HH4K]

(モーター駆動用電源)

ヨドバシカメラ

1,270円×2個

ラジコンカー

(前輪の方向制御モーター・後輪の推進用モーターともにDCモーター)

自宅にあったもの

おそらく数千円


構成


ソースコード

// ★は当日変更するパラメーター

 

#include <cdcacm.h>

#include <usbhub.h>

#include "pgmstrings.h"

#include <SoftwareSerial.h>

#include <Wire.h> //I2C Arduino Library

#ifdef dobogusinclude

#include <spi4teensy3.h>

#endif


// QZPODからのデータを格納するバッファーのサイズ

#define READBUFFERSIZE (256)


// 区切り記号

#define DELIMITER (",")


// π

#define pai 3.141592


// デジタルコンパス5883

#define HMC5883_WriteAddress 0x1E

#define HMC5883_ModeRegisterAddress 0x02

#define HMC5883_ContinuousModeCommand 0x00

#define HMC5883_DataOutputXMSBAddress  0x03

int regb=0x01;

int regbdata=0x40;

int outputData[6];


// bluetoothモジュールとのシリアル通信(デバック用)

SoftwareSerial mySerial(11, 12); // Rx, Tx


//Standard PWM DC control

int E1 = 5; //M1 Speed Control

int E2 = 6; //M2 Speed Control

int M1 = 4; //M1 Direction Control

int M2 = 7; //M1 Direction Control


// 目的地緯度経度

float g_wpLong = 0;

float g_wpLat = 0;


// NMEAデータ保存用バッファ

char g_szNmea[READBUFFERSIZE] = "";


// ターゲットユニット保存バッファ

char g_szTarget[16];


// Arduinoボード上のキーNo.

int g_iKey=-1;

int adc_key_val[5] ={ 30, 150, 360, 535, 760 };

int NUM_KEYS = 5;


// ステータス

int g_iStatus = 0; // 0:スタートできる状態 1~3:n番目の目的地に向かって走る 4:初期状態or走行完了


// RMCを受診した回数

int g_iCounter = 0;


// ★待機フラグ(RMCを指定された回数受けたら走行制御する)

int g_iWait =1;


// ★車No.

char g_szCarNo[] = "02";


// ★基準点の設定(日本科学未来館)

float X11 = 13946.5445;  // スタートポイント(経度)

float Y11 = 3537.1497;  // スタートポイント(緯度)

float X71 = 13946.53437;  // A7(経度)

float Y71 = 3537.1445;  // A7(緯度)

float X17 = 13946.53803;  // B7(経度)

float Y17 = 3537.1580;  // B7(緯度)


// ユニットの緯度経度成分

float Dax = (X71 - X11) / 6;

float Dbx = (X17 - X11) / 6;

float Day = (Y71 - Y11) / 6;

float Dby = (Y17 - Y11) / 6;


class ACMAsyncOper : public CDCAsyncOper

{

public:

    virtual uint8_t OnInit(ACM *pacm);

};


uint8_t ACMAsyncOper::OnInit(ACM *pacm)

{

  uint8_t rcode;

  // Set DTR = 1 RTS=1

  rcode = pacm->SetControlLineState(3);


  if (rcode)

  {

    ErrorMessage<uint8_t>(PSTR("SetControlLineState"), rcode);

    return rcode;

  }


  LINE_CODING lc;

  lc.dwDTERate = 115200;

  lc.bCharFormat = 0;

  lc.bParityType = 0;

  lc.bDataBits = 8;


  rcode = pacm->SetLineCoding(&lc);


  if (rcode)

    ErrorMessage<uint8_t>(PSTR("SetLineCoding"), rcode);


  return rcode;

}


USB     Usb;

//USBHub     Hub(&Usb);

ACMAsyncOper  AsyncOper;

ACM           Acm(&Usb, &AsyncOper);


void setup()

{

  // PWM DC control

  int i;

  for(i=4;i<=7;i++)

    pinMode(i, OUTPUT);

  

  // LED

  pinMode(13, OUTPUT);


  // bluetooth Serial

  mySerial.begin(9600);


  // I2C(デジタルコンパス)

  Wire.begin();


  // USBのイニシャライズ

  Usb.Init();


  delay( 200 );

}


void loop()

{

  // ボタンが押されたかを確認する

  int adc_key_in = analogRead(0);

  int iKey = get_key(adc_key_in);


  // キーが押された

  if(g_iKey != iKey && iKey != -1){

    g_iKey = iKey;

    g_iStatus = 0;

  }


  // 目的地に着いた場合はボタンが押されるまで何もしない  

  if(g_iStatus >= 4)

    return;

  

  // S2、又は、S3キーが押された以降はロボットカープログラムを実行(S3キーはテストモード)

  if(g_iKey == 1 || g_iKey == 2){

    ReadQZPOD();

  }


  // S1キーが押されたら走行を停止

  else if(g_iKey == 0)

    stop();


}


// QZPODからデータを読み取る

void ReadQZPOD()

{

  Usb.Task();

  delay(30);


  if(Acm.isReady()) {

    uint8_t rcode;

    uint8_t buf[64];

    uint16_t rcvd = 64;


    //USBから64バイトデータを読み込む

    rcode = Acm.RcvData(&rcvd, buf);


    // 0バイト以上受信した

    if( rcvd ) {

      char szReadBuffer[READBUFFERSIZE] = "";

      char szLineString[READBUFFERSIZE] = "";


      // 読み込んだデータをszReadBufferにコピーする

      int len = strlen(g_szNmea);

      if(len > 1){

        strcpy(szReadBuffer, g_szNmea);

        memcpy(&szReadBuffer[len], buf, rcvd );

      }

      else{

        memcpy( szReadBuffer, buf, rcvd );

      }

      g_szNmea[0] = '\0';


      while(1){

        // 改行データごとにデータを区切る

        if( !ReadLineString( szReadBuffer, rcvd, szLineString, READBUFFERSIZE ) ){

          // 改行データがなかったら前のデータにつなげて更にUSBからデータを読み取るためreturnする

          strcat( g_szNmea, szLineString );

          return;

        }

      

        // szLineStringの文字列長が0だったら何もしないでreturnする

        if( strlen(szLineString) == 0){

          return;

        }

        

        // 区切りがバッファの中にあったので前半のデータをg_szNmeaに付け足す

        strcat( g_szNmea, szLineString );


        float fLat = 0.0;

        float fLong = 0.0;

        float fDeg = 0.0;


        // S3キーが押された場合はテストモード(QZQSMを無理やり入力)

        if(g_iKey == 2){

          strcpy(g_szNmea, "$QZQSM,55,C6C407DE0207030703030101050706020205000000000000000000001BAD358*08");

          g_iKey = 1;

        }


        // NMEAデータを解析する($QZQSMと$GNRMCのみが対象)

        int i = AnalyzeLineString( g_szNmea, fLat, fLong, fDeg );


        // 災危通報を受信済みの場合のみ以下の処理を実行

        if(g_iStatus >= 1){


          // 目標地点が初期化されている場合は目標ポイントを算出する

          if(g_wpLat == 0){

            char c1[2], c2[2];

            int i0 = 0;

            int Na = 0;

            int Nb = 0;


            c1[0] = g_szTarget[1+(g_iStatus-1)*4];

            c1[1] = '\0';

            c2[0] = g_szTarget[3+(g_iStatus-1)*4];

            c2[1] = '\0';

            Na = atoi(c1);

            Nb = atoi(c2);

      

            // ターゲットユニットの位置情報を算出する

            g_wpLat = Y11 + Day * (Na-1) + Dby * (Nb-1);

            g_wpLong = X11 + Dax * (Na-1) + Dbx * (Nb-1);

          }


          // 受信データが$GNRMCだった

          else if(i == 1){


            // ★$GNRMCを指定回もしくはg_iWaitで指定された回数受信したら車体の制御を実行

            if(g_iCounter >= g_iWait){

              // 測位結果に基づいて車体を制御

              ControlRobotCar(fLat, fLong, fDeg);

              g_iCounter = 1;

              mySerial.println(fLat);

              mySerial.println(fLong);

              mySerial.println(fDeg);

            }

            else

              g_iCounter++;


            mySerial.println(g_iCounter);

          }

        }

        // 1行の読み取りが終わったのでg_szNmeaのデータをクリアする

        g_szNmea[0] = '\0';

      }

    }

    delay(10);

  }

}


// QZPODから出力されたNMEAデータを1行ずつに切り分ける

// 0 : 読み取り途中 / 1 : 読み取り完了

int ReadLineString(char szReadBuffer[], const int ciReadBufferSize, char szLineString[], const int ciLineStringSize )

{

  int iIndexChar = 1;

 

 // 1バイトずつ読み込んでいく 

  while( 1 )

  {

    char c = szReadBuffer[iIndexChar];

    

    // 区切り文字の場合

    if( '$' == c){// && iIndexChar >= 1){

      char buf[ciLineStringSize];

      

      // szLineStringに区切りの前半のデータををコピーする

      memcpy(szLineString, szReadBuffer, iIndexChar-1);

      szLineString[iIndexChar-1] = '\0';

      

      // bufに区切りの後半のデータをコピーする

      memcpy(buf, &szReadBuffer[iIndexChar], ciLineStringSize - iIndexChar);

      buf[ciLineStringSize - iIndexChar] = '\0';


      // szReadBufferにbufをコピーする

      memcpy(szReadBuffer, buf, ciLineStringSize);

      

      return 1;

    }

    // 区切り文字ではない場合

    else{

      // 最後のデータまで区切り文字がなかった場合

      int ibuf = strlen(szReadBuffer);

      if( ibuf - 1 <= iIndexChar ){

        // szLineStringにszReadBufferをそのままコピーする

        memcpy( szLineString, szReadBuffer, ibuf);

        szLineString[ibuf] = '\0';

        szReadBuffer[0] = '\0';

        break;

      }

      iIndexChar++;

    }

  }

  return 0;

}


// センテンスの解析。

// $GPRMCの場合、引数変数に、緯度、経度を入れ、戻り値 1 を返す。

// $QZQSMの場合、引数変数に、ターゲット位置の緯度、経度をいれ、戻り値2を返す。

// それ以外の場合、戻り値は 0 を返す。

// $QZQSM,55,C6C407DE0207030703030101050706020205000000000000000000001BAD358*08

// $QZQSM,55,C6C407DEまでは共通、その後の02以降がロボットの番号やメッシュデータと行き先を表す

// 解析例は、次の通り

// 02:指令をうけとるロボットの番号

// 07:緯度方向のユニット分割数、

// 03:ユニット間隔[m]

// 07:経度方向のユニットの分割数、

// 03:ユニット間隔[m]

// 03:目標ターゲット数

// 0101:スタートユニット位置 (1,1)

// 0507: 第一ターゲットの場所(スタート位置から北に5マス目と東に7マス目)

// 0602: 第2ターゲットの場所(北に6マス目と東に2マス目)

// 0205: 第3ターゲットの場所(北に2マス目と東に5マス目)

// 

// 提供されるデータはすべてASCIIのテキストデータ

int AnalyzeLineString( char szLineString[], float& rfLat, float& rfLong, float& rfDeg )

{

  char c0[3];


  // 初めて対象となる$QZQSMを受信した場合

  // Sample $QZQSM,55,C6C407DE0207030703030101050706020205000000000000000000001BAD358*08

  if( 0 == strncmp( "$QZQSM", szLineString, 6 ) && g_iStatus == 0){

    mySerial.println( szLineString );


    //  車No.を読み取る

    c0[0] = szLineString[18];

    c0[1] = szLineString[19];

    c0[2] = '\0';

   

    // 指定された車No.の場合

    if(strcmp(c0, g_szCarNo) == 0){

      // フラグをたて、ランプを点灯し、とりあえず方位取得のために走り出す


      // ターゲットユニットをメモリにコピー

      memcpy(g_szTarget, &szLineString[34], 12);

      g_szTarget[12] = '\0';

        

      // 1番目のターゲットユニットに向かう

      g_iStatus = 1;

        

      // LEDを点灯する

      digitalWrite(13, HIGH);

      delay(3000);

      digitalWrite(13, LOW);

    }

    // 指定された車No.以外の場合は動かない、もしくは、止まる

    else{

      stop();

      g_iStatus = 0;

      g_wpLat = 0;

      g_wpLong = 0;

      return 0;

    }

    return 2;

  }


  // $GPRMCの場合

  // Sample $GNRMC,085120.307,A,3541.1493,N,13945.3994,E,000.0,240.3,181211,,,A*6A

  if( 0 == strncmp( "$GNRMC", szLineString, 6 ) || 0 == strncmp( "$GPRMC", szLineString, 6 ) ){

    mySerial.println( szLineString );


    // データを分解する

    strtok( szLineString, DELIMITER ); // $GPRMC

    strtok( NULL, DELIMITER ); // UTC時刻

    strtok( NULL, DELIMITER ); // ステータス

    char* pszLat = strtok( NULL, DELIMITER ); // 緯度(dddmm.mmmm)

    strtok( NULL, DELIMITER ); // 北緯/南緯

    char* pszLong = strtok( NULL, DELIMITER ); // 経度(dddmm.mmmm)


    // 位置情報を取れなかった場合

    if( NULL == pszLong )

      return 0;


    // 文字列を数値に変換

    rfLat = atof(pszLat);

    rfLong = atof(pszLong);

    

    // 車体の向きをデジタルコンパスより取得

    GetAngle(rfDeg);

    

    return 1;    

  }

  

  // その他のNMEAデータの場合

  return 0;

}


// 方位偏差を計算してロボットカーを制御する

// float rbfLat;    ロボットの緯度

// float rbLong;    ロボットの経度

// float d_dir;     ロボットの進行方向(単位:度)

// 戻り値:1 目的地についていない/2 目的地についた

int ControlRobotCar(float& rbLat, float& rbLong, float& d_dir)

{

  float jLat = (g_wpLat - rbLat);  // way point到達判定x値

  float jLong = (g_wpLong - rbLong);  // way point到達判定y値

  float r_wp;  // way pointの北からの方位

  float r_dir;  // dirのラジアン値

  float d_dev;  // 方位偏差


  // ターゲットユニットに着いた(ユニットサイズの2倍以内にいる)

  if(jLat*jLat + jLong*jLong <= 4*Dax*Dax + 4*Dbx*Dbx){

    // 止まる

    stop();

    

    // ステータスの値を一つ上げる

    g_iStatus++;

    

    // データの初期化

    g_wpLong = 0;

    g_wpLat = 0;

    

    int i = 0;    

    // LEDを光らせる

    while(i < 5){

      digitalWrite(13, HIGH);

      delay(500);

      digitalWrite(13, LOW);

      delay(500);

      i++;

    }

    // まだ3番目のターゲットに到着していない場合はまた走り始める

    if(g_iStatus < 4){

      advance(255,255);

    }

    return 2;

  }


  // 方位の計算

  r_wp = atan((g_wpLong - rbLong) / (g_wpLat - rbLat));

  if((g_wpLong - rbLong) > 0 && (g_wpLat - rbLat) < 0)

    r_wp = pai + r_wp;

  else if((g_wpLong - rbLong) < 0 && (g_wpLat -rbLat) < 0)

    r_wp = pai + r_wp;

  else if((g_wpLong - rbLong) < 0 && (g_wpLat -rbLat) > 0)

    r_wp = 2 * pai + r_wp;

  else;


  // 進行方向のラジアン変換

  r_dir = (pai / 180) * d_dir;


  // 方位偏差の計算

  d_dev = r_wp - r_dir;

  if(d_dev <= -pai)

    d_dev = 2 * pai + d_dev;


  // ウェイポイントが進行方向より右側の場合

  if(d_dev > pai / 8){ // ★閾値は要変更

    turn_R(255,255);

  }

  // ウェイポイントが進行方向より左側の場合

  else if(d_dev < (-1) * (pai / 8)){ // ★閾値は要変更

    turn_L(255,255);

  }

  else{

    advance(255,255);

  }

  return 1;

}


// デジタルコンパスから車体の向きを取得(北からの向き)

void GetAngle(float& angle) {

 

    int i,x,y,z;


    Wire.beginTransmission(HMC5883_WriteAddress);

    Wire.write(regb);

    Wire.write(regbdata);

    Wire.endTransmission();

 

//    delay(1000);

    Wire.beginTransmission(HMC5883_WriteAddress); //Initiate a transmission with HMC5883 (Write address).

    Wire.write(HMC5883_ModeRegisterAddress);       //Place the Mode Register Address in send-buffer.

    Wire.write(HMC5883_ContinuousModeCommand);     //Place the command for Continuous operation Mode in send-buffer.

    Wire.endTransmission();                       //Send the send-buffer to HMC5883 and end the I2C transmission.

//    delay(100);

 

 

    Wire.beginTransmission(HMC5883_WriteAddress);  //Initiate a transmission with HMC5883 (Write address).

    Wire.requestFrom(HMC5883_WriteAddress,6);      //Request 6 bytes of data from the address specified.

 

//    delay(500);

 

     //Read the value of magnetic components X,Y and Z

 

    if(6 <= Wire.available()) // If the number of bytes available for reading be <=6.

    {

        for(i=0;i<6;i++)

        {

            outputData[i]=Wire.read();  //Store the data in outputData buffer

        }

    }

 

    x=outputData[0] << 8 | outputData[1]; //Combine MSB and LSB of X Data output register

    z=outputData[2] << 8 | outputData[3]; //Combine MSB and LSB of Z Data output register

    y=outputData[4] << 8 | outputData[5]; //Combine MSB and LSB of Y Data output register

 

    // offset

    x = x+18;

    y = y+266;


    // angle in degrees

    angle= atan2((double)y,(double)x) * (180 / 3.14159265) +180;


    // ロボットカーの向きと、センサーの取り付け方向の関係から角度を変換

    if(angle >= 0 && angle < 90 )

        angle = angle + 270;

    else

        angle = angle - 90;

 

    //Print the approximate direction

    mySerial.print(angle,2);

    mySerial.print(" Deg / ");

    

    if((angle < 22.5) || (angle > 337.5 ))

        mySerial.println("North");

    if((angle > 22.5) && (angle < 67.5 ))

        mySerial.println("North-East");

    if((angle > 67.5) && (angle < 112.5 ))

        mySerial.println("East");

    if((angle > 112.5) && (angle < 157.5 ))

        mySerial.println("South-East");

    if((angle > 157.5) && (angle < 202.5 ))

        mySerial.println("South");

    if((angle > 202.5) && (angle < 247.5 ))

        mySerial.println("South-West");

    if((angle > 247.5) && (angle < 292.5 ))

        mySerial.println("West");

    if((angle > 292.5) && (angle < 337.5 ))

        mySerial.println("North-West"); 

    

//    delay(100);

}


// 止まる

void stop(void)

{

  digitalWrite(E1,LOW);

  digitalWrite(E2,LOW);

//  mySerial.println("stop");

}

// 前進する

void advance(char a,char b)

{

  digitalWrite(E1,LOW);

  analogWrite (E2,b);

  digitalWrite(M2,HIGH);

  mySerial.println("advance");

}

// 後進する

void back_off (char a,char b)

{

  digitalWrite(E1,LOW);

  analogWrite (E2,b);

  digitalWrite(M2,LOW);

  mySerial.println("back");

}

// 左に曲がる

void turn_L (char a,char b)

{

  analogWrite (E1,a);

  digitalWrite(M1,HIGH);

  analogWrite (E2,b);

  digitalWrite(M2,HIGH);

  mySerial.println("turn_L");

}

// 右に曲がる

void turn_R (char a,char b)

{

  analogWrite (E1,a);

  digitalWrite(M1,LOW);

  analogWrite (E2,b);

  digitalWrite(M2,HIGH);

  mySerial.println("turn_R");

}

// Arduinoボード上のキーが押されたらキーNo.を取得する

int get_key(unsigned int input)

{   

  int k;

  for (k = 0; k < NUM_KEYS; k++)

  {

    if (input < adc_key_val[k])

    {  

      return k;  

    }

  }

  // ボタンは押されなかった

  if (k >= NUM_KEYS)

    k = -1;

  return k;

}