自己バランスロボットを作りました Vol #3 – プログラミング

自己バランスロボットとは

  • 自己バランスロボットとは、外部から支えられることなく、自らバランスを取り続けるロボットのことです。代表的な例として、二輪で直立しながら走行するロボットが挙げられます。このロボットは、センサーと制御アルゴリズムを用いて重心を常に調整します。

自己バランスロボットプログラミングの参考にした記事は?

  • Arduinoのホームページから、[Community] -> [Project Hub]と進むとArduinoを使った様々なプロジェクトが掲載されています。自己バランスロボットは何件かの投稿があり、[Arduino two weel self Balancing Robot]にはYoutubeの動画も掲載されています。GitHubにArduinoで自己バランスロボットを制作するのに使用するスケッチ一式が掲載されていました。悩んだら参考にしようと考えています。Arduinoのホームページの投稿ではDCモータが使われていますが、自作する自己バランスロボットは、連続回転サーボモータを使用するので、モータの制御方法は工夫が必要です。

自己バランスロボットのプログラムの作成手順

  • 自己バランスロボットに使用する連続回転サーボモータFS90Rと加速度・角速度センサMPU6050には部品の個体差そして取付時のガタがあります。最初に、それぞれの部品に係るバラツキを測定し、そのバラツキの値を、自己バランスロボットの制御プログラムに使用します。
  • それぞれの部品の測定には今までに学習で習得した知識を活用します。

連続回転サーボモータの不感帯の測定

  • 連続回転サーボモータは、50HzのPWMで制御されており、writeMicroseconds()関数にパルス幅が1500usを代入した時に回転が止まり、パルス幅を1500Hzから上下に変化させると異なる方向に回転します。連続回転サーボモータの回転動作を[Arduinoの割込み関数を使ってサーボモーターの回転速度を測定しました。]で学習し、回転速度を測定しました。実際の連続回転サーボモータには個体差があり、パルス幅が1500us付近でモータが回転しない不感帯があり、その中心のパルス幅は1500usから少しずれていることが解ります。下図を参照ください。
  • 自己バランスロボットは、連続回転サーボモータを2個使います。2個のモータを[自己バランスロボットを作りました Vol #1]で作成した電気回路に接続して、パルス幅を1500usから変化させ、それぞれの連続回転サーボモータの不感帯を測定し、不感帯の中心値を求めます。
  • 「int speedPulse = 1500; // 回転速度を設定」の行のspeedPulseの数値を最初は10程度変化させながらモータが回転するかしないかを確認します。
////////////////////////////////////////////////////////////////////////
// 連続回転サーボモータをwriteMicroseconds()関数を使って回転させるスケッチ
////////////////////////////////////////////////////////////////////////

#include <Servo.h>

Servo servo1;  // サーボ1(ピン5)
Servo servo2;  // サーボ2(ピン6)

void setup() {

  Serial.begin(9600); //シリアル通信を9600bpsで開始

  servo1.attach(5);
  servo2.attach(6);
}

void loop() {
  // SG90-HVの基準値は1500µs(停止)

  int speedPulse = 1500; // 回転速度を設定

  servo1.writeMicroseconds(speedPulse);
  servo2.writeMicroseconds(speedPulse);

  // 回転速度設定値を表示
  Serial.print("SpeedPulse: ");
  Serial.println(speedPulse);

}
  • 2個の連続回転サーボモータの不感帯の測定が終わりました。以下の結果です。この値を自己バランスロボットの制御に使います。
連続回転サーボモータ #1不感帯(us) 1390 ~ 1453不感帯中心値(us) 1422
連続回転サーボモータ #2不感帯(us) 1410 ~ 1468不感帯中心値(us) 1439

加速度・角速度センサ―のオフセットの測定

  • 加速度・角速度センサ―GY-521 (MPU6050)は、加速度および角速度をX,Y,Z軸の3軸で測定します。角速度はGY-521が静止した状態では0のはずですが、製造のバラツキなどからある値を持ってしまいます。自己バランスロボットをバランスの取れた静止状態にして、GY-521の角速度の値を読み出し、この値を角速度のオフセットとして、自己バランスロボットの制御に使用します。
  • 自己バランスロボットが自立する位置を固定して、GY-521の出力を1000回測定し、オフセット値とします。この値は自己バランスロボットの制御用スケッチの中で使用します。
////////////////////////////////////////////////////////////////////////
//GY-521(加速度・角速度センサ)の加速度と角速度のオフセット値を計測するスケッチ
////////////////////////////////////////////////////////////////////////

#include <Wire.h>
#include <MPU6050.h>

MPU6050 mpu;

long accelX_sum = 0;
long accelY_sum = 0;
long accelZ_sum = 0;
long gyroX_sum = 0;
long gyroY_sum = 0;
long gyroZ_sum = 0;

void setup() {
  Serial.begin(9600);       // シリアルモニタを9600bpsで開始
  Wire.begin();             // I2C開始
  mpu.initialize();         // MPU6050初期化

  if (!mpu.testConnection()) {
    Serial.println("MPU6050接続エラー");
    while (1);
  }

  Serial.println("MPU6050接続成功");
  Serial.println("静止状態で1000回データ取得中...");
}

void loop() {
  // 1000回サンプリング
  for (int i = 0; i < 1000; i++) {
    int16_t ax, ay, az;
    int16_t gx, gy, gz;

    mpu.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);

    accelX_sum += ax;
    accelY_sum += ay;
    accelZ_sum += az;
    gyroX_sum += gx;
    gyroY_sum += gy;
    gyroZ_sum += gz;

    delay(2); // サンプリング間隔(約2ms)
  }

  // 平均値計算
  long accelX_avg = accelX_sum / 1000;
  long accelY_avg = accelY_sum / 1000;
  long accelZ_avg = accelZ_sum / 1000;
  long gyroX_avg = gyroX_sum / 1000;
  long gyroY_avg = gyroY_sum / 1000;
  long gyroZ_avg = gyroZ_sum / 1000;

  // 結果出力
  Serial.println("=== 平均値 ===");
  Serial.print("Accel X: "); Serial.println(accelX_avg);
  Serial.print("Accel Y: "); Serial.println(accelY_avg);
  Serial.print("Accel Z: "); Serial.println(accelZ_avg);
  Serial.print("Gyro X: "); Serial.println(gyroX_avg);
  Serial.print("Gyro Y: "); Serial.println(gyroY_avg);
  Serial.print("Gyro Z: "); Serial.println(gyroZ_avg);

  // 一度だけ計算して終了
  while (1);
}
  • 使用したGY-521のオフセットは、X軸: -704、Y軸 -301、Z軸 -130、でした。

自己バランスロボットの傾きの制御手順は?

  • 自己バランスロボットを組み立て、垂直に立ててバランスが取れる角度が約0度でした。この値を傾斜角度の目標値とします。自己バランスロボットの自立の制御方法は、ものすごく単純に書くと、[ロボットの傾斜角度を測定し]、傾斜角度の測定値と傾斜角度の目標値に差があれば、[測定値と目標値の差が減るようにモータを回転させる]です。

自己バランスロボットの傾きの測定方法は?

  • ロボットの傾きを測定するには、MPU6050のライブラリで提供されているgetMotion6()関数を使います。この関数で得られた加速度から倒立角度を計算します。得られた角速度は測定済みのオフセットを引いて使用します。[ArduinoでGY-521: MPU6050 (3軸 加速度・角速度センサ)から加速度と角速度を読み出す仕様を調べました ]で学習したようにMPU6050にはDLPF(デジタルローパスフィルター)が内蔵されています。測定誤差で加速度・角速度値が大きく変動するのを避けるため、この値を最大の6(帯域幅 約5Hz)に設定して使用します。

目標角度との差が減るようにモータを回転させる方法は?

  • 測定角度と目標値の差を減らす制御には、PID制御を用いました。Webで検索すると解説と実施例が多く見つかります。積分項が積みあがると前方若しくは後方に動くので、積分項はある大きさ以上で0に戻しています。MPU6050の角速度出力はドリフトします。ドリフトの補正に相補フィルタを使用しました。連続回転サーボモータは動作の中心を不感帯の中心値に設定します。
  • 作成した自己バランスロボットは倒れてしまうと、自分自身では立ち上がれません。倒れても車輪を回転させ続けるとモーター駆動ICや電源ICが壊れるかもしれません。傾斜角度が90° +/- 60°を超えるとバランスを回復することは難しいとし、その場合はモーターの回転を止めます。
  • 距離センサの測定値が5cm以下になると、自己バランスロボットを意図的に動かせます。そのために目標角度を0度から5度に変更します。
////////////////////////////////////////////////////////////////////////
// 自己バランスロボット バランス制御
////////////////////////////////////////////////////////////////////////

#include <Wire.h>
#include <MPU6050.h>
#include <Servo.h>

// --- ピン設定 ---
#define TRIG_PIN 2
#define ECHO_PIN 3
#define SERVO_LEFT_PIN 5
#define SERVO_RIGHT_PIN 6

// --- サーボのニュートラルポイント ---
const int SERVO_LEFT_NEUTRAL = 1461;  // マイクロ秒
const int SERVO_RIGHT_NEUTRAL = 1438; // マイクロ秒

// --- MPU6050のオフセット値 ---
const int16_t GYRO_X_OFFSET = -704;
const int16_t GYRO_Y_OFFSET = -301;
const int16_t GYRO_Z_OFFSET = -130;

// --- PIDパラメータ ---
float Kp = 20.0;  // 比例ゲイン
float Ki = 0.1;   // 積分ゲイン
float Kd = 0.5;   // 微分ゲイン

// --- 相補フィルタ ---
const float alpha = 0.98; // ジャイロ積分の重み
float filteredAngle = 0.0;

// --- 制御の制限値 ---
const int MAX_OUTPUT = 500;

// --- 変数 ---
MPU6050 mpu;
Servo servoLeft, servoRight;

float previousError = 0.0;
float integral = 0.0;

unsigned long lastTime = 0;

// --- セットアップ ---
void setup() {
  Serial.begin(115200);
  Wire.begin();

  // MPU6050を初期化
  mpu.initialize();
  if (!mpu.testConnection()) {
    Serial.println("MPU6050接続失敗!");
    while (1);
  }

  // DLPFを6に設定(帯域幅 約5Hz)
  mpu.setDLPFMode(6);

  // サーボを初期化
  servoLeft.attach(SERVO_LEFT_PIN);
  servoRight.attach(SERVO_RIGHT_PIN);

  // 超音波センサを初期化
  pinMode(TRIG_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);

  lastTime = millis();
}

// --- ループ処理 ---
void loop() {
  // MPU6050のデータを読み取る
  int16_t ax, ay, az, gx, gy, gz;
  mpu.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);

  // ジャイロのオフセットを適用
  gx -= GYRO_X_OFFSET;
  gy -= GYRO_Y_OFFSET;
  gz -= GYRO_Z_OFFSET;

  // 加速度センサから傾斜角度を計算
  float accelAngle = atan2((float)ay, (float)az) * 180.0 / PI;

  // 時間差(Δt)
  unsigned long currentTime = millis();
  float dt = (currentTime - lastTime) / 1000.0;
  lastTime = currentTime;

  // ジャイロの回転速度(度/秒に変換)
  float gyroRate = gx / 131.0; // MPU6050感度: 131 LSB/(deg/s)

  // 相補フィルタ処理
  filteredAngle = alpha * (filteredAngle + gyroRate * dt) + (1 - alpha) * accelAngle;

  // 超音波距離を取得
  long distance = getDistance();

  // 目標角度を決定
  float targetAngle = 0.0; // デフォルトは0°
  if (distance < 5) {
    targetAngle = 5.0; // 距離が5cm未満なら目標角度を5°に設定
  }

  // 傾斜が±60°を超えたらモーターを停止
  if (abs(filteredAngle) > 60) {
    stopMotors();
    Serial.println("傾斜が60°を超えたため、モーターを停止しました。");
    return;
  }

  // PID制御
  float error = targetAngle - filteredAngle; // 目標角度を使用
  integral += error * dt;

  // integralが200を超えたらリセット
  if (abs(integral) > 200) {
    integral = 0.0;
    Serial.println("Integralが200を超えたためリセットしました。");
  }

  float derivative = (error - previousError) / dt;
  previousError = error;

  float output = Kp * error + Ki * integral + Kd * derivative;

  // 出力を±500に制限
  if (output > MAX_OUTPUT) output = MAX_OUTPUT;
  if (output < -MAX_OUTPUT) output = -MAX_OUTPUT;

  // モーターを制御
  controlMotors(output);

  // デバッグ情報
  Serial.print("Distance: "); Serial.print(distance);
  Serial.print(" cm | AccelAngle: "); Serial.print(accelAngle);
  Serial.print(" | FilteredAngle: "); Serial.print(filteredAngle);
  Serial.print(" | TargetAngle: "); Serial.print(targetAngle);
  Serial.print(" | Integral: "); Serial.print(integral);
  Serial.print(" | Output: "); Serial.println(output);
}

// --- モーター制御 ---
void controlMotors(float output) {
  int leftSpeed = SERVO_LEFT_NEUTRAL - (int)output;
  int rightSpeed = SERVO_RIGHT_NEUTRAL + (int)output;

  servoLeft.writeMicroseconds(leftSpeed);
  servoRight.writeMicroseconds(rightSpeed);
}

// --- モーター停止 ---
void stopMotors() {
  servoLeft.writeMicroseconds(SERVO_LEFT_NEUTRAL);
  servoRight.writeMicroseconds(SERVO_RIGHT_NEUTRAL);
}

// --- 超音波距離測定 ---
long getDistance() {
  digitalWrite(TRIG_PIN, LOW);
  delayMicroseconds(2);
  digitalWrite(TRIG_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIG_PIN, LOW);

  long duration = pulseIn(ECHO_PIN, HIGH);
  long distance = duration * 0.034 / 2;
  return distance;
}

自己バランスロボットが自立しました!

  • 自立できるようになるまで、PID制御係数を調整します。根気の必要な作業です。映像はPID制御係数調整後にバランスが取れているロボットの動きです。

実際は色々苦労していました

  • 回路を設計し、自作シールドを作り、筐体を作って、組み立てて、後はプログラムを作れば完成と思って、プログラムを作りましたが、全く自立できませんでした。色々とWebを調べました。角度ドリフトの補正は、カルマンフィルターかMadgwickフィルターの精度が良いとの記事もあり、試しましたが効果がありません。PID制御係数を色々と変化させましたが効果がありません。PID制御をスケッチ内の計算から、PID_V1.hを使ったライブラリに変更しても効果がありません。
  • 連続回転サーボモータを使って自己バランスロボットを作っている例はあまりなく、触れてはいけない構成だったのか?などと思い、また回路の作成時間と筐体の作成時間を思い出して、無駄になったのか?と悲観に暮れていました。連続回転サーボモータは[Arduinoの割込み関数を使ってサーボモーターの回転速度を測定しました。]のブログで、簡易的なタコメータを作って回転動作を調べていたので、自己バランスができるのではないかと思っていたのですが…。このブログの学習で連続回転サーボモータに回転の不感帯があることを知りました。そうか、連続回転サーボモータをwriteMicroseconds()関数で動かすときは、不感帯の中心値から制御しないといけないと思い付き、それをスケッチに加え、PIDの係数を根気よく調整するとやっと自立しました。
  • 現時点では長時間自立させていると急に動いて倒れることがあり、まだPIDの係数の微調整が必要ですが、通常は安定して自立できているので、まずは一安心です。

御質問、誤植の指摘、記事の改善点の気づきなどありましたら「問い合わせ 」のページからお願いします。