セグウェイを自作してみた ～プログラム編～

#シリーズ目次

- [セグウェイを自作してみた ～材料編～](http://qiita.com/sasaco/items/54a1faec2ddaa935dec1)
- [セグウェイを自作してみた ～電子回路組立編～](http://qiita.com/sasaco/items/b72819993790443877ec)
- セグウェイを自作してみた ～プログラム編～
- [セグウェイを自作してみた ～完成編～](http://qiita.com/sasaco/items/7f361b4fa2d1db572a6f)

---

この分野では門外漢の私が、「電子制御」をまったく勉強せずに
セグウェイを作ってみようと思います。
参考にした本はこちら
[Arduino Robotics (Technology in Action)](https://www.amazon.co.jp/gp/product/1430231831/ref=oh_aui_detailpage_o00_s00?ie=UTF8&psc=1)
[サンプルソース/Segbot](https://github.com/johndavid400/Segbot)

#全ソースコード
arduino は c言語で書きます

```c:balancer.ino
// 倒立振子、加速度/ジャイロセンサー(MPU-6050)版
// 2016/3/4

#include <Wire.h>
#include "KalmanFilter.h"
#include <SoftwareSerial.h>
#include <SabertoothSimplified.h>

// モータードライバの制御定数。
SoftwareSerial SWSerial(NOT_A_PIN, 13); // RX on no pin (unused), TX on pin 11 (to S1).
SabertoothSimplified ST(SWSerial); // Use SWSerial as the serial port.

#define SABER_MOTOR1_FULL_FORWARD  -127
#define SABER_MOTOR1_FULL_REVERSE  127
#define SABER_MOTOR1_FULL_STOP  0

#define SABER_MOTOR2_FULL_FORWARD  -127
#define SABER_MOTOR2_FULL_REVERSE  127
#define SABER_MOTOR2_FULL_STOP  0

// 加速度/ジャイロセンサーの制御定数。
#define MPU6050_ADDR         0x68 // MPU-6050 device address
#define MPU6050_SMPLRT_DIV   0x19 // MPU-6050 register address
#define MPU6050_CONFIG       0x1a
#define MPU6050_GYRO_CONFIG  0x1b
#define MPU6050_ACCEL_CONFIG 0x1c
#define MPU6050_ACCEL_XOUT_H 0x3b
#define MPU6050_ACCEL_XOUT_L 0x3c
#define MPU6050_ACCEL_YOUT_H 0x3d
#define MPU6050_ACCEL_YOUT_L 0x3e
#define MPU6050_ACCEL_ZOUT_H 0x3f
#define MPU6050_ACCEL_ZOUT_L 0x40
#define MPU6050_GYRO_XOUT_H  0x43
#define MPU6050_GYRO_XOUT_L  0x44
#define MPU6050_GYRO_YOUT_H  0x45
#define MPU6050_GYRO_YOUT_L  0x46
#define MPU6050_GYRO_ZOUT_H  0x47
#define MPU6050_GYRO_ZOUT_L  0x48
#define MPU6050_PWR_MGMT_1   0x6b
#define MPU6050_WHO_AM_I     0x75

// 加速度/ジャイロセンサーの制御変数。
KalmanFilter gKfx, gKfy; // カルマンフィルタ。
float gCalibrateX; // 初期化時の角度。（＝静止角とみなす）
float gCalibrateY; // 初期化時の角度。（＝静止角とみなす）
long gPrevMicros;  // loop()間隔の計測用。

// 倒立振子の制御変数。
float gPx, gIx, gDx; // 現在出力値とPID成分。
// この数字は試行錯誤で調整。
float gyro_weightX = 0.98;
float integra_weightX = 0;
float accel_weightX = 0.02;

float gPy, gIy, gDy; // 現在出力値とPID成分。
// この数字は試行錯誤で調整。
float gyro_weightY = 0.98;
float integra_weightY = 0;
float accel_weightY = 0.02;

// 加速度/ジャイロセンサーへのコマンド送信。
void writeMPU6050(byte reg, byte data) {
  Wire.beginTransmission(MPU6050_ADDR);
  Wire.write(reg);
  Wire.write(data);
  Wire.endTransmission();
}

// 加速度/ジャイロセンサーからのデータ読み込み。
byte readMPU6050(byte reg) {
  Wire.beginTransmission(MPU6050_ADDR);
  Wire.write(reg);
  Wire.endTransmission(false);
  Wire.requestFrom(MPU6050_ADDR, 1/*length*/, false);
  byte data =  Wire.read();
  Wire.endTransmission(true);
  return data;
}

void setup() {
  Serial.begin(115200);

  // モータードライバの初期化。
  SWSerial.begin(9600);
   
  // 加速度/ジャイロセンサーの初期化。
  Wire.begin();
  if (readMPU6050(MPU6050_WHO_AM_I) != 0x68) {
    Serial.println("\nWHO_AM_I error.");
    while (true) ;
  }
  writeMPU6050(MPU6050_SMPLRT_DIV, 0x07);   // sample rate: 8kHz/(7+1) = 1kHz
  writeMPU6050(MPU6050_CONFIG, 0x00);       // disable DLPF, gyro output rate = 8kHz
  writeMPU6050(MPU6050_GYRO_CONFIG, 0x00);  // gyro range: ±250dps
  writeMPU6050(MPU6050_ACCEL_CONFIG, 0x00); // accel range: ±2g
  writeMPU6050(MPU6050_PWR_MGMT_1, 0x01);   // disable sleep mode, PLL with X gyro
  delay(2000);

  // 重力加速度から求めた角度をカルマンフィルタの初期値とする。
  float ax = (readMPU6050(MPU6050_ACCEL_XOUT_H) << 8) | readMPU6050(MPU6050_ACCEL_XOUT_L);
  float ay = (readMPU6050(MPU6050_ACCEL_YOUT_H) << 8) | readMPU6050(MPU6050_ACCEL_YOUT_L);
  float az = (readMPU6050(MPU6050_ACCEL_ZOUT_H) << 8) | readMPU6050(MPU6050_ACCEL_ZOUT_L);
  float degRoll  = atan2(ay, az) * RAD_TO_DEG;
  float degPitch = atan(-ax / sqrt(ay * ay + az * az)) * RAD_TO_DEG;
  gKfx.setAngle(degRoll);
  gKfy.setAngle(degPitch);
  gCalibrateX = degRoll;
  gCalibrateY = degPitch;
  gPrevMicros = micros();
  
  //kill motors when first switched on
  setEngineSpeed(0, 0);
}

void loop() {

  // 重力加速度から角度を求める。
  float ax = (readMPU6050(MPU6050_ACCEL_XOUT_H) << 8) | readMPU6050(MPU6050_ACCEL_XOUT_L);
  float ay = (readMPU6050(MPU6050_ACCEL_YOUT_H) << 8) | readMPU6050(MPU6050_ACCEL_YOUT_L);
  float az = (readMPU6050(MPU6050_ACCEL_ZOUT_H) << 8) | readMPU6050(MPU6050_ACCEL_ZOUT_L);
  float degRoll  = atan2(ay, az) * RAD_TO_DEG;
  float degPitch = atan(-ax / sqrt(ay * ay + az * az)) * RAD_TO_DEG;

  // ジャイロで角速度を求める。
  float gx = (readMPU6050(MPU6050_GYRO_XOUT_H) << 8) | readMPU6050(MPU6050_GYRO_XOUT_L);
  float gy = (readMPU6050(MPU6050_GYRO_YOUT_H) << 8) | readMPU6050(MPU6050_GYRO_YOUT_L);
  float gz = (readMPU6050(MPU6050_GYRO_ZOUT_H) << 8) | readMPU6050(MPU6050_GYRO_ZOUT_L);
  float dpsX = gx / 131.0; // LSB sensitivity: 131 LSB/dps @ ±250dps
  float dpsY = gy / 131.0;
  float dpsZ = gz / 131.0;

  // カルマンフィルタで角度(x,y)を計算する。
  long curMicros = micros();
  float dt = (float)(curMicros - gPrevMicros) / 1000000; // μsec -> sec
  gPrevMicros = curMicros;
  float degX = gKfx.calcAngle(degRoll, dpsX, dt);
  float degY = gKfy.calcAngle(degPitch, dpsY, dt);
  degX -= gCalibrateX;
  degY -= gCalibrateY;

  // 倒れたらモーター停止。
  if (30 < abs(degX)) {
    setEngineSpeed(0, 0);
    Serial.print("**** stop ");
    Serial.print(degX);
    return;
  }

  // PID制御でモーター出力を計算。
  gPx = degX / 90;   // P成分：傾き-90～90度 → -1～1
  gIx += gPx;        // I成分：傾きの積算。
  gDx = dpsX / 250;  // D成分：角速度-250～250dps → -1～1
  
  gPy = degY / 90;   // P成分：傾き-90～90度 → -1～1
  gIy += gPy;        // I成分：傾きの積算。
  gDy = dpsY / 250;  // D成分：角速度-250～250dps → -1～1
  
  // 前後方向の出力を決定
  float level = (gPx * gyro_weightX + 
                gIx * integra_weightX +
                gDx * accel_weightX) * 400;
                
  // 左右方向の出力を決定
  float Steer = (gPy * gyro_weightY + 
                gIy * integra_weightY +
                gDy * accel_weightY) * 150;
 
  float Motor1percent = level - Steer;
  float Motor2percent = level + Steer;
  
  setEngineSpeed(Motor1percent, Motor2percent);


  // デバッグ用。
  static int ps;
  if (++ps % 1 == 0) {
    Serial.print("dt:"); Serial.print(dt);
    Serial.print("\tdegX:"); Serial.print(degX);
    Serial.print("\tdpsX:"); Serial.print(dpsX);
    Serial.print("\tdegY:"); Serial.print(degY);
    Serial.print("\tdpsY:"); Serial.print(dpsY);
    Serial.print("\tlevel:"); Serial.print(level);
    Serial.print("\tSteer:"); Serial.print(Steer);
    Serial.print("\tMotor1percent:"); Serial.print(Motor1percent);
    Serial.print("\tMotor2percent:"); Serial.print(Motor2percent);
    Serial.println("");
  }
  delay(1);
}

void setEngineSpeed( float Motor1percent, float Motor2percent ){
  
  Motor1percent = max(-100, min(100, Motor1percent)); // → -100～100
  Motor2percent = max(-100, min(100, Motor2percent)); // → -100～100

  float cSpeedVal_Motor1 = 0;
  float cSpeedVal_Motor2 = 0;

  cSpeedVal_Motor1 = map (Motor1percent,
                         -100,
                         100,
                         SABER_MOTOR1_FULL_REVERSE,
                         SABER_MOTOR1_FULL_FORWARD);
                         
  cSpeedVal_Motor2 = map (Motor2percent,
                         -100,
                          100,
                         SABER_MOTOR2_FULL_REVERSE,
                         SABER_MOTOR2_FULL_FORWARD);
                         
  ST.motor(1, cSpeedVal_Motor1);
  ST.motor(2, cSpeedVal_Motor2);
}
```

```c:KalmanFilter.h
// カルマンフィルタ。
// 2016/3/3

class KalmanFilter {
private:
    float angle;
    float bias;
    float P[2][2];
public:
    KalmanFilter();
    void setAngle(float newAngle);
    float calcAngle(float newAngle, float newRate, float dt);
};
```

```c:KalmanFilter.cpp
// カルマンフィルタ。
// 2016/3/3
//
// 計算式の参考資料。
// http://blog.tkjelectronics.dk/2012/09/a-practical-approach-to-kalman-filter-and-how-to-implement-it/

#include "KalmanFilter.h"

KalmanFilter::KalmanFilter() {
  angle = 0.0;
  bias = 0.0;
  P[0][0] = 0.0;
  P[0][1] = 0.0;
  P[1][0] = 0.0;
  P[1][1] = 0.0;
};

void KalmanFilter::setAngle(float newAngle) {
  angle = newAngle;
};

float KalmanFilter::calcAngle(float newAngle, float newRate, float dt) {

  // variances
  float Q_angle = 0.001;
  float Q_bias = 0.003;
  float R_measure = 0.03;

  // step 1
  float rate = newRate - bias;
  angle += dt * rate;

  // step 2
  P[0][0] += dt * (dt * P[1][1] - P[0][1] - P[1][0] + Q_angle);
  P[0][1] -= dt * P[1][1];
  P[1][0] -= dt * P[1][1];
  P[1][1] += Q_bias * dt;

  // step 3
  float y = newAngle - angle;

  // step 4
  float S = P[0][0] + R_measure;

  // step 5
  float K[2];
  K[0] = P[0][0] / S;
  K[1] = P[1][0] / S;

  // step 6
  angle += K[0] * y;
  bias += K[1] * y;

  // step 7
  float P00_temp = P[0][0];
  float P01_temp = P[0][1];
  P[0][0] -= K[0] * P00_temp;
  P[0][1] -= K[0] * P01_temp;
  P[1][0] -= K[1] * P00_temp;
  P[1][1] -= K[1] * P01_temp;

  return angle;
};
```

---

#解説
各セクションごとの解説をしていきます。

##外部参照モジュール

ジャイロセンサーの制御のために下記のモジュールを使います。
これは、arduino に標準で入っていないので、
下記の URL を参考に 各環境に モジュールを追加してください

[MPU-6050 Accelerometer + Gyro](http://playground.arduino.cc/Main/MPU-6050)

```c:
#include "I2Cdev.h"
#include "MPU6050.h"
```



##制御用変数の定義

フルパワーを 500 とします。
どうやら 1023 までいけるようです。

```c:
/*  Attitude Control Parameters */
#define FULL_SPEED 500//1023
```


ジャイロセンサーのアクセス用変数

```c:
/*  MPU-6050 Accelerometer + Gyro */
MPU6050 accelgyro;
```


モーター制御は
下記の pin 番号を通じて制御します。

```c:
/*  VNH2SP30 pin definitions */
int inApin[2] = {7, 4};  // INA: Clockwise input
int inBpin[2] = {8, 9}; // INB: Counter-clockwise input
int pwmpin[2] = {5, 6}; // PWM input
int cspin[2] = {2, 3}; // CS: Current sense ANALOG input
int enpin[2] = {0, 1}; // EN: Status of switches output (Analog pin)
```


モータの回転数を決定するのに
ジャイロセンサーの10回の検出値の平均を用いるようにします。

```c:
/*  gyro multiple times to get an average baseline value. */
#define average_times 10
float angular_rate_Y_log[average_times];
float angular_rate_Y_sum;
float angular_rate_Z_log[average_times];
float angular_rate_Z_sum;
```


ログは毎回出します。

```c:
int log_times = 0;
```



##初期化処理


定型分

```c:
void setup() {

    Wire.begin();
    Serial.begin(9600);

```


ジャイロセンサーの初期化

```c:
    // initialize device
    accelgyro.initialize();
    Serial.println(accelgyro.testConnection() ? "MPU6050 connection successful" : "MPU6050 connection failed");
```


モーター制御の初期化

```c:
    // Initialize digital pins as outputs
    Serial.println("Initializing Mortor Driver digital pins...");
    for (int i=0; i<2; i++){
      pinMode(inApin[i], OUTPUT);
      pinMode(inBpin[i], OUTPUT);
      pinMode(pwmpin[i], OUTPUT);
    }
    // Initialize braked
    for (int i=0; i<2; i++){
      digitalWrite(inApin[i], LOW);
      digitalWrite(inBpin[i], LOW);
    }
}
```



##制御処理


ジャイロセンサーの値を角度（°）単位に変換する

```c:
void loop() {
  
  int16_t ax, ay, az;
  int16_t gx, gy, gz;
  accelgyro.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);
  float acc_x = ax / 16384.0;
  float acc_y = ay / 16384.0; 
  float acc_z = az / 16384.0; 
  float rate_X = atan2(acc_x, acc_z) * 360 / 2.0 / PI; 
  float rate_Y = atan2(acc_y, acc_z) * 360 / 2.0 / PI;
  float rate_Z = atan2(acc_x, acc_y) * 180 / 2.0 / PI;
  
  Serial.print(rate_X);
  Serial.print("\t"); 
  Serial.print(rate_Y);
  Serial.print("\t"); 
  Serial.print(rate_Z);
  Serial.print("\t"); 
```


初回(average_times=10回)は値を保持するだけ
ジャイロセンサーの10回の検出値の平均を計算する

```c:
  if(log_times < average_times){
    angular_rate_Y_sum += rate_Y; 
    angular_rate_Y_log[log_times] = rate_Y;
    angular_rate_Z_sum += rate_Z; 
    angular_rate_Z_log[log_times] = rate_Z;
    log_times ++;
  }else{
    //傾きの平均値Xを計算する。
    angular_rate_Y_sum -= angular_rate_Y_log[0];
    angular_rate_Y_sum += rate_Y;
    for (int i=1; i<average_times; i++) 
      angular_rate_Y_log[i-1] = angular_rate_Y_log[i];
    angular_rate_Y_log[average_times-1]= rate_Y;
    float angular_rate_Y_average = angular_rate_Y_sum / average_times;

    //傾きの平均値Zを計算する。
    angular_rate_Z_sum -= angular_rate_Z_log[0];
    angular_rate_Z_sum += rate_Z;
    for (int i=1; i<average_times; i++) 
      angular_rate_Z_log[i-1] = angular_rate_Z_log[i];
    angular_rate_Z_log[average_times-1]= rate_Z;
    float angular_rate_Z_average = angular_rate_Z_sum / average_times;
```


モーターの回転数を決定する。
Yの角度(10回分の平均値)が 60° 以上 0 以下なら 前進（go_state = 2）

```c:
    //モーターの回転数を決定する。
    float pmw1 = 0; //0 to 1023
    float pmw2 = 0; //0 to 1023
    int go_state = 0;
    float delta = 0;

      if (angular_rate_Y_average <= 60 && angular_rate_Y_average >= 0 ){
        angular_rate_Y_average = abs(angular_rate_Y_average);
        float a2 = angular_rate_Y_average / 60;
        pmw1 =  FULL_SPEED * a2;
        pmw2 = pmw1;
        go_state = 2;
```


Yの角度(10回分の平均値)が 0 以上 -60° 以下なら 後進（go_state = 1）

```c:
      }
      if (angular_rate_Y_average >= -60 && angular_rate_Y_average <= 0 ){
        angular_rate_Y_average = abs(angular_rate_Y_average);
        float a1 = angular_rate_Y_average / 60;
        pmw1 =  FULL_SPEED * a1;
        pmw2 = pmw1;
       go_state = 1;
      }
```


ログと 実際にモーターに命令を出す関数 motorGo を呼ぶ

```c:
      Serial.print(go_state);
      Serial.print("\t"); 
      Serial.print(pmw1);
      Serial.print("\t"); 
      Serial.print(pmw2);
      Serial.print("\t"); 

      Serial.print(delta);
      Serial.print("\t"); 

      if ( go_state == 0 ){
        motorOff(0);
        motorOff(1);
      }else{
        motorGo(0, go_state, pmw1); 
        motorGo(1, go_state, pmw2);
      }
    }
    Serial.println("");
 }
```



##動作処理


実際にモーターに命令を出す関数

```c:
void motorOff(int motor){
  for (int i=0; i<2; i++){
    digitalWrite(inApin[i], LOW);
    digitalWrite(inBpin[i], LOW);
  }
  analogWrite(pwmpin[motor], 0);
}

void motorGo(uint8_t motor, uint8_t direct, uint8_t pwm)
{
  if (motor <= 1){
    if (direct <=4){
      // Set inA[motor]
      if (direct <=1)
        digitalWrite(inApin[motor], HIGH);
      else
        digitalWrite(inApin[motor], LOW);
      // Set inB[motor]
      if ((direct==0)||(direct==2))
        digitalWrite(inBpin[motor], HIGH);
      else
        digitalWrite(inBpin[motor], LOW);

      analogWrite(pwmpin[motor], pwm);
    }
  }
```



#改良の余地
ソースはここに
https://create.arduino.cc/editor/sasaco/feb5acb3-d17a-4cc3-b572-9ca4e98178e7/preview

まだ、前進・後進しかやっていません
そのうち z軸の傾きを使って 左右のモーターの回転数を変えて
曲がれるようにしたいと思います。

---

次回は[完成編](http://qiita.com/sasaco/items/7f361b4fa2d1db572a6f)についてお届けします。