Arduinoを使って 1モータ・1軸のシンプルな倒立振子ロボットを作りました

この記事で学習できること

  • 倒立振子ロボット」や「自己バランスロボット」と呼ばれるロボットを単純な構成で作成し、作成に必要な技術知識を習得します。

倒立振子ロボットをシンプルな構成で作成します

  • 倒立振子ロボットは、「自己バランスロボット」とも呼ばれ、重心が支持点より上にある不安定な構造を持ち、センサと制御によって自律的にバランスを保つロボットです。傾き(傾斜角度)をセンサで検出し、モーターを制御して姿勢を維持するのが基本的な動作原理です。
  • このようなロボットを一台自作することで、電気回路の基礎、モーター制御、構造の設計など、幅広い知識と技術を実践的に学ぶことができます。
  • 今回は、少ない部品を使った単純な構成の倒立振子ロボットを製作しました。Arduino UNO R3、ジャイロセンサ、モータドライバ、DCモータ1個という単純な構成です。

倒立振子ロボットの動作映像

倒立振子ロボットの動作映像です。自己バランスの動作はぎこちないですが自立できました。(映像時間 25秒程度)

倒立振子ロボットの主要電気部品

倒立振子ロボットの動作に必要な部品を絞って、できるだけ単純な部品構成にしました。

主要部品個数機能
Arduino UNO R31個センサー情報を読み取り、モーターを制御
GY521(MPU6050)1 個ロボットの傾きと傾く速度を検出するセンサー
TB6612FNG (基板化)1 個DCモーターを駆動するIC
DCモータ1 個車輪の駆動
バッテリー (電源)2 式Arduino用およびDCモータ駆動用

1. 倒立振子ロボットの電気回路

  • 倒立振子ロボットの主要電気部品接続図です。
倒立振子ロボット回路図
— 倒立振子ロボット回路図 —

2. 部品接続のポイント

加速度・角速度センサ GY521(MPU6050)とArduinoの接続

  • SDA → D18/SDA
  • SCL → D19/SCL
  • VCC → 5V
  • GND → GND

モータ駆動用基板 (TB6612FNG内蔵)とArduino・DCモータ・電源の接続

  • PWMA → デジタルピン5ピン
  • AIN1 → デジタル2ピン
  • AIN2 → デジタル3ピン
  • STBY → Vcc (5V)
  • BIN1 → 未接続
  • BIN2 → 未接続
  • PWMB → 未接続
  • GND → GND
  • VM → モーター電源 (6V)
  • Vcc → Vcc (5V)
  • GND → GND
  • AO1 → DCモーター端子
  • AO2 → DCモーター端子
  • BO2 → 未接続
  • BO1 → 未接続
  • GND → GND

電源

  • 9V乾電池をArduino UNO R3用の電源として使用する。(DCジャックへ接続)
  • 6V(1.5V 単4電池 4本)をDCモータ駆動用電源として使用する。

倒立振子ロボットの構成

  • 倒立振子ロボットをどう構成するかを考えるのは一つの楽しみです。部品の取付方向は縦方向か? 横方向か? 組み立ては、ビスで止めるのか? 両面テープにするのか? 取付用の部品は、市販品を使うのか? 自作するのか? 試行錯誤しながら以下のような構成にまとめました。
    • 電気部品の配線を容易するためにブレッドーボードを使用する。
    • 車輪に対しての重量バランスが均等になるように部品は水平に配置する。
    • モーターギヤボックスとシャーシ間に3辺約50mmの立方体の自作スペーサを入れて電源(バッテリー)を配置する空間を作る。
    • 各部品は主に両面テープで固定する。
倒立振子ロボットの外観図
— 倒立振子ロボットの外観図 (斜視図) —
倒立振子ロボットの外観図 (下方図)
— 倒立振子ロボットの外観図 (下方からの図) —

倒立振子ロボットで使用した部品

1. ギヤボックス

— エコモーターギヤボックス(70203)—

2. 部品の一覧と購入先 (広告を含む)

  • 自作するときに便利と思いますので使用した部品と購入先を一覧にします。同じ部品であれば購入先は問いません。
No.部品名 など品番・メーカ・仕様購入先員数
1 CPUArduino UNO R3[Amazon]1
2 加速度・角速度センサGY-521 (MPU6050内蔵)[Amazon]1
3モータードライバIC (基板化済)TB6612FNG[Amazon]1
4ギヤボックス (タミヤ)70203[Amazon]1
5車輪 (タミヤ)70145[Amazon]2
6電池ボックス (9V電池用)電池ボックス (9V電池用)[Amazon]1
2電池ボックス (単4、4本用)電池ボックス (単4、4本用)[Amazon]1
3取付ボード (タミヤ)70157[Amazon]1
5ブレッドボード[Amazon]1
6 ブレッドボード用線材[Amazon]必要数
7ビス・ナット (ギヤボックス固定用)任意2
8両面テープ (部品固定用)任意必要数

の部品は「ELEGOO Arduino用のUNO R3 最終版スタータキット」に含まれています。

プログラム作成

主要部品の配線は終了し、機構部品の組み立ても終了しました。続いて倒立振子ロボットを制御するプログラムを以下の順番で作成します。

  • GY521(MPU6050)とのオフセット値の測定
    • 加速度・角速度センサGY521(MPU6050)で計測される角速度は、センサが静止状態では、”0″の値のはずです。しかし製造のバラツキからこの値は”0″にはならずオフセット値を持っています。角速度は角度計算に使用するので、センサが静止状態でオフセット値を測定し、この値を測定値から減算して使用します。
  • 自己バランス制御プログラミング
  • デバッグおよびチューニング

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

  • 加速度・角速度センサ―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初期化

  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軸: -441、Y軸 202、Z軸 28、でした。

倒立振子ロボットの制御用スケッチ

  • 倒立振子ロボットの制御用スイッチを作ります。電気部品は必要最小限に絞っています。まず、制御スケッチ完全版の動作を説明します。
  • 制御も単純化を目指してPD制御としています。(ロボットの作成記事を読むとPDI制御がよく用いられます。)
////////////////////////////////////////////////////////////////////////
// 倒立振子 バランス制御
////////////////////////////////////////////////////////////////////////

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

MPU6050 mpu;

// -------------------------
// 制御周期の設定
// -------------------------
unsigned long sampleTime = 5000; // 制御周期(マイクロ秒)。5ms = 200Hz
unsigned long prevMicros = 0;

// -------------------------
// デバッグ設定
// -------------------------
#define DEBUG0 0
#define DEBUG1 1
#define DEBUG2 2
#define DEBUG3 3
#define DEBUG4 4

#define DEBUG_MODE DEBUG1

// -------------------------
// 制御パラメータ
// -------------------------
float targetAngle = -0.75;
int MAX_PWM = 255;
float FALL_ANGLE = 60.0;

// PD制御ゲイン
float Kp = 55.0;
float Kd = 1.20;

// ジャイロオフセット
const int gyroOffsetX = -441;
const int gyroOffsetY = 202;
const int gyroOffsetZ = 28;

// TB6612FNG ピン設定
const int AIN1 = 2;
const int AIN2 = 3;
const int PWMA = 5;

// -------------------------
int16_t ax, ay, az;
int16_t gx, gy, gz;

float angle = 0;
unsigned long lastDebug = 0;

// -------------------------
// モーター制御(デッドゾーン補正入り)
// -------------------------
void motorDrive(int pwm)
{
    pwm = constrain(pwm, -MAX_PWM, MAX_PWM);

    // デッドゾーン補正
    const int DEADZONE = 60;

    if (pwm > 0 && pwm < DEADZONE) {
        pwm = DEADZONE;
    } else if (pwm < 0 && pwm > -DEADZONE) {
        pwm = -DEADZONE;
    }

    // 完全停止
    if (pwm == 0) {
        digitalWrite(AIN1, LOW);
        digitalWrite(AIN2, LOW);
        analogWrite(PWMA, 0);
        return;
    }

    // 通常駆動
    if (pwm > 0) {
        digitalWrite(AIN1, HIGH);
        digitalWrite(AIN2, LOW);
        analogWrite(PWMA, pwm);
    } else {
        digitalWrite(AIN1, LOW);
        digitalWrite(AIN2, HIGH);
        analogWrite(PWMA, -pwm);
    }
}

// -------------------------
// 角度計算(相補フィルタ)
// -------------------------
float getAngle(float dt)
{
    mpu.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);

    // ジャイロ値の補正
    float gyroRate = -((gy - gyroOffsetY) / 131.0);
    float accelAngle = atan2((float)ax, (float)az) * 180.0 / PI;

    // dt を用いた相補フィルタ
    angle = 0.98 * (angle + gyroRate * dt) + 0.02 * accelAngle;

    return angle;
}

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

    Wire.begin();
    mpu.initialize();

    pinMode(AIN1, OUTPUT);
    pinMode(AIN2, OUTPUT);
    pinMode(PWMA, OUTPUT);

    prevMicros = micros();
}

// -------------------------
void loop()
{
    unsigned long currentMicros = micros();

    // 指定した sampleTime ごとに実行
    if (currentMicros - prevMicros >= sampleTime) {
        float dt = (currentMicros - prevMicros) / 1000000.0;
        prevMicros = currentMicros;

        float ang = getAngle(dt);
        float angError = ang - targetAngle;

        // 転倒時は停止
        if (abs(angError) > FALL_ANGLE) {
            motorDrive(0);
            return;
        }

        // PD 制御
        // Kd項には応答性の良いジャイロ直値(角速度)を使用
        float gyroRate = -((gy - gyroOffsetY) / 131.0);
        float control = Kp * angError + Kd * gyroRate;

        int pwm = (int)control;
        motorDrive(pwm);

        // デバッグ出力(50ms周期)
        if (millis() - lastDebug >= 50) {
            lastDebug = millis();

            #if DEBUG_MODE == DEBUG1
                Serial.print("Angle: "); Serial.print(ang);
                Serial.print("  Control: "); Serial.print(control);
                Serial.print("  PWM: "); Serial.println(pwm);

            #elif DEBUG_MODE == DEBUG2
                Serial.print(ang); Serial.print(",");
                Serial.print(control); Serial.print(",");
                Serial.println(pwm);

            #elif DEBUG_MODE == DEBUG3
                Serial.print("Angle: "); Serial.print(ang);
                Serial.print("  Ax: "); Serial.print(ax);
                Serial.print("  Ay: "); Serial.print(ay);
                Serial.print("  Az: "); Serial.print(az);
                Serial.print("  Gx: "); Serial.print(gx);
                Serial.print("  Gy: "); Serial.print(gy);
                Serial.print("  Gz: "); Serial.println(gz);

            #elif DEBUG_MODE == DEBUG4
                Serial.print(ang); Serial.print(",");
                Serial.print(ax); Serial.print(",");
                Serial.print(ay); Serial.print(",");
                Serial.print(az); Serial.print(",");
                Serial.print(gx); Serial.print(",");
                Serial.print(gy); Serial.print(",");
                Serial.println(gz);
            #endif
        }
    }
}

1. 自己ライブラリと基本設定

#include <Wire.h>
#include <MPU6050.h>
  • Wire.h: Arduinoとセンサーの間で通信(I2C通信)を行うための標準ライブラリです。
  • MPU6050.h: 加速度・ジャイロセンサー「MPU6050」を簡単に扱うための専用ライブラリです。

2. 制御パラメータ

float targetAngle = -0.75; // 目標とする静止角度(重心のズレを補正)

float Kp = 55.0;           // 比例ゲイン:倒れそうになった時、戻そうとする力の強さ
float Kd = 1.20;           // 微分ゲイン:動きの速さを抑え、ガクガク振動するのを防ぐブレーキ

const int gyroOffsetX = -441; // ジャイロオフセット
const int gyroOffsetY = 202;
const int gyroOffsetZ = 28;
  • targetAngle: ロボットが物理的に安定して立つ目標の角度です。
  • Kp / Kd: PD制御用のパラメータです。Kpを決め、次にKdを決めるという順番で調整します。
  • 測定した角速度のオフセット値を設定します。測定値からこの値を引いて計算します。

3. 角度計算(getAngle関数)

angle = 0.98 * (angle + gyroRate * dt) + 0.02 * accelAngle;
  • センサーから届くデータはそのままではノイズが多く、ロボットの制御には使えません。
  • 相補フィルタ: 「反応は早いが時間が経つとズレるジャイロセンサー」と「反応は遅いが正確な加速度センサー」のいいとこ取りをして、現在の正確な角度を算出します。

4. モーター駆動(motorDrive関数)

  • 計算された数値を、実際のモーターの回転に変換します。
  • デッドゾーン補正 (DEADZONE): モーターはごく弱い電力(PWM)では摩擦で回りません。ある程度の電圧を最低ラインとして設定し、スムーズに動き出すようにしています。
  • 正転・逆転: AIN1AIN2 ピンの HIGH/LOW を切り替えることで、ロボットの前進・後退をコントロールします。

5. メインループ

  • loop() 内では、一定の間隔(5ms = 1秒間に200回)で以下の処理を繰り返します。
  • 時間計測: micros() を使い、前回の計算からどれだけ時間が経ったか(dt)を正確に測ります。
  • 角度取得: 現在の傾きを計算します。
  • 転倒判定: FALL_ANGLE(60度)を超えたら、暴走を防ぐためにモーターを緊急停止させます。
  • PD制御計算:
    • Kp * angError: 傾きに比例した力で戻す。
    • Kd * gyroRate: 回転速度に抵抗する力(ブレーキ)をかける。
  • 出力: 計算結果(control)をモーターに送信します。

6. デバッグ出力

  • USBケーブルでPCに繋いだ際、現在の角度やモーターの出力をシリアルモニタで確認できるようにします。
  • デバッグ出力にもCPUが使われます。デバッグ出力を入れて制御の時間を圧迫するのを避けるため、完成時は出力されない設定ができるようにしています。

バランスロボットが期待通りに動かなかったら

  • 電気部品の接続が正しかどうかの確認
    • 電源用バッテリーは古くないか? (9V若しくは6Vが正常に出力されているか?)
    • デバッグのセンサー情報が出力されているか? ロボットを手で傾けたときにセンサー情報は追従しているか?
    • モータは倒れた角度を補正する方向に回転しているか?
  • 制御が適切かどうかの確認
    • 倒立振子ロボットが最も安定している角度を目標角度としているか?
    • 傾斜角度を計算する測定パラメータを正負を含めて正しく設定しているか?
    • Kp、Kdの数値は妥当か?
    • モータの不感帯の数値は妥当か?

色々な倒立振子ロボットを作りたくなったら

  • 作成した倒立振子ロボットは、使用する部品を必要最小限に絞っています。しかしどのようなロボットでも、加速度・角速度センサからのデータを読み取り変換して、その結果を使ってモータを制御する流れは変わりません。作成した倒立振子ロボットでは加速度・角速度センサの機能の一部しか使っていません。センサの機能を知れば知るほど制御が楽になると思います。加速度・角速度センサの仕様やドライバICの仕様を調べて記事にしています。
  • 倒立振子ロボットに使うモータはDCモータだけには限定されません。連続回転サーボモータを使用する倒立振子ロボット・自己バランスロボットの作成記事を回路設計、筐体設計、プログラミングに分けてまとめています。

まとめ

単純な部品構成で、また簡単な制御で倒立振子・自己バランスロボットが作れるかどうか試みました。最初は1個のモータで全体の重さを制御しきれるのかな? と不安がありましたがなんとか自立しました。この構成なら、少し興味があれば、誰でも作ってみようかなというレベルになったのではと思っています。自分自身もDCモータを使用する倒立振子ロボット・自己バランスロボットが完成していい経験と勉強になりました。


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