この記事では、ドローン制御の本質を理解するための第一歩として、以下の3つを扱います。 ドローンは「不安定な系を制御技術で安定化した飛行機械」であり、この制御の連鎖を理解することがすべての基盤になります。 この「StampFly制御システム」シリーズでは、具体的なC++コードを通して概念を学ぶことを重視しています。抽象的な説明だけでなく、実際に動作するコードを見ることで、「なんとなく分かった」で終わらずに確実な理解につなげたいと考えています。 記事中にコードが登場する際は、なぜそのコードが必要なのか(掲載の理由)、何を実現するコードなのか(目的と機能)、どのように読み解くべきか(理解のポイント)を併記します。 例えば、「センサデータの読み取り」について説明する際: このように、コードは説明を補強し、具体的な実装イメージを提供する役割を担います。 ドローン(UAV: Unmanned Aerial Vehicle)は、人が搭乗せずに飛行する航空機の総称です。技術的な観点から見ると、ドローンの本質は「制御システムによって安定化された不安定な機械システム」にあります。 従来の飛行機は、設計上ある程度の安定性を持っています。一方、マルチコプタ型ドローンは本質的に不安定で、制御システムが常に調整し続けなければ即座に墜落してしまいます。 手のひらの上で鉛筆を立てることを考えるとイメージしやすいと思います。 鉛筆を手のひらで立てるのは完全な静的不安定状態で、微小な傾きが増幅して倒れます。手を絶えず動かして調整することで、かろうじて立った状態を維持できます。マルチコプタも同様で、制御システムが毎秒400回の調整(手の動きに相当)を行うことで飛行を維持しています。 この頻繁な調整こそが、ドローン制御技術の核心です。 ドローンが飛ぶまでのプロセスは、以下の物理的連鎖で説明できます。 この連鎖の各要素を順に追っていくと: この循環が400Hz(0.0025秒間隔)で実行されることで、人間には不可能な高速制御が実現されています。 M5StampFlyは、ドローン制御の学習を意識して設計された教育プラットフォームです。主な仕様は以下のとおりです。 手のひらサイズで安全 室内での実験に適したサイズと重量です。36.8gと軽いので、万一ぶつかっても大きな事故にはなりにくく、動作も目視で確認しやすい大きさです。 制御システムは本格的 ESP32-S3による本格的な制御アルゴリズムの実装、BMI270をはじめとするセンサ群によるセンサフュージョン、FreeRTOSによるリアルタイムタスクスケジューリングと、小型ながら制御系としての構成はしっかりしています。 オープンな開発環境 ESP-IDFによる産業レベルの開発フレームワークが使え、設計とコードはすべてオープンソースで公開されています。 M5StampFlyの制御システムは、カスケード制御と呼ばれる二重ループ構造で実装されています。これは実際のドローンで広く採用されている制御方式です。 このコードは、実際のドローン制御で使われるカスケード制御の基本構造を示しています。 カスケード制御を採用する理由は大きく2つあります。まず、角速度制御(内側ループ)が高速に応答して外乱を素早く抑制してくれること。そして、各ループを独立して調整できるためパラメータ設定がやりやすいことです。ほぼすべての実用的なドローンがこの構造を採用しています。 このカスケード制御は、有人航空機の制御システムと同じ考え方です。 内側ループ(角速度制御)はSAS(Stability Augmentation System:安定性増大システム)に相当し、機体の揺れや振動を抑制して、パイロット(または外側ループ)の操作を安定化します。 外側ループ(姿勢制御)はCAS(Control Augmentation System:操縦性増大システム)に相当し、目標姿勢への追従や操縦入力に対する応答性の向上を担います。 直接姿勢制御だけでは、慣性の影響で振動しやすく安定飛行は困難です。カスケード制御にすることで、初心者でも比較的簡単にパラメータ調整ができるようになります。 「制御が複雑すぎて理解できない」 物理的連鎖を一つずつ追いかけるのが近道です。まずはPWM信号がモータを回すことを確認し、次にプロペラが推力を生む様子を観察し、最後に制御アルゴリズムに入る。ゲイン調整も、まず角速度制御(内側ループ)から始めると取り組みやすくなります。 「数学が難しくて分からない」 最初は直感的な理解から入れば十分です。三角関数は「傾き」を表現する道具、微分は「変化の速さ」、積分は「累積した量」。まずはこの程度の理解で進めて、必要になったら深掘りすればOKです。 「コードが動かない」 段階的にデバッグしましょう。まずシステムの基本状態を確認するところから。 基本的なPID制御を理解した先には、より高度な制御手法への展開があります。 モデル予測制御(MPC) は、未来の挙動を予測して最適な制御入力を計算する手法です。 機械学習ベースの制御 では、TensorFlow Liteなどを使ってニューラルネットワークによる制御出力を予測できます。 群制御 は、複数機体の協調飛行を実現する技術です。 このほか、非線形制御理論、ロバスト制御、最適制御、故障診断など、研究レベルの話題も今後のシリーズで触れていく予定です。 今回の要点をまとめます。 ドローンの本質は「制御システムによって安定化された不安定なシステム」であること。PWM→MOSFETドライバ→モータ→プロペラ→推力→運動→センサ→制御計算という物理的連鎖が400Hzで循環していること。そしてM5StampFlyは、この一連の仕組みを学ぶための教育プラットフォームとして設計されていること。 この物理的連鎖の理解が、ドローン制御のすべての基盤になります。どんなに高度な制御アルゴリズムも、最終的には「PWM信号でモータを回し、プロペラで推力を生成する」という物理的プロセスに帰結するからです。 次回の第2回では、この物理的連鎖を実現するM5StampFlyのハードウェアを詳しく解説します。ESP32-S3のデュアルコア構成、BMI270 IMUによる姿勢検出、小型ブラシモータの特性などを取り上げます。 シリーズ: 基礎・ハードウェア編
次回: 第2回「StampFlyハードウェア解説」 コード例について: 本記事のコード例は学習目的で簡略化しています。実際に使う際は、エラーハンドリングや安全機能、最適化を追加してください。 関連記事: 第2回: StampFlyハードウェア解説 参考資料: 本記事はドローンの誘導や制御についての話題を取り扱います。工学的に興味がある人、より深く勉強して実装してみたい人に向けて教育的観点や趣味の人たちを増やしたいと言う意味合いで執筆しています。これを読んでドローンやロボット制御に興味をもってもらって、実際に手を動かしてみる人が増えることを夢見ています。そのような意図以外に人を傷つけたりといった使い方や法令違反をすることなくご利用ください。 また、お約束事項ですが・・・・
本ブログに掲載する技術情報・解説・コード例は、教育・研究・学習目的で提供するものです。内容の正確性・安全性には十分配慮しておりますが、利用に伴う結果や損害について筆者は一切の責任を負いません。実装や運用は、各自の判断と責任において行ってください。 安全第一でドローン技術を学び、楽しんでください。
この記事の内容
この連載シリーズの特徴
理論と実装の一体化
コード掲載の方針
// バッテリー電圧を監視する実装例
float voltage = battery_monitor.getVoltage();
if (voltage < 3.3f) {
emergency_landing(); // 緊急着陸
}
基礎知識の整理
ドローンとは何か
マルチコプタが不安定な理由
物理的連鎖:PWMから飛行まで
PWM信号 → MOSFETドライバ → モータ回転 → プロペラ推力 → 機体運動 → センサ検出 → 制御計算 → PWM信号
↑ ↓
└─────────────────── フィードバック制御ループ ──────────────────────┘
M5StampFlyの設計思想
M5StampFlyの基本仕様
項目
仕様
MCU
ESP32-S3(デュアルコア 240MHz)
IMU
BMI270(6軸)
気圧センサ
BMP280
モータ
小型ブラシモータ
バッテリー
300mAh 1S HV LiPo
重量
36.8g
フレームサイズ
81.5mm
制御周波数
400Hz
最低バッテリー電圧
3.4V(これを下回ると緊急着陸)
教育プラットフォームとしての利点
実装詳解
基本的な制御ループの実装
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static const char* TAG = "STAMPFLY_INTRO";
// ドローンの基本状態を表現するクラス
class DroneBasicState {
public:
// 姿勢(外側ループで使用)
struct Attitude {
float roll = 0.0f; // ロール角 [ラジアン]
float pitch = 0.0f; // ピッチ角 [ラジアン]
float yaw = 0.0f; // ヨー角 [ラジアン]
} attitude;
// 角速度(内側ループで使用)
struct AngularRate {
float roll_rate = 0.0f; // ロール角速度 [rad/s]
float pitch_rate = 0.0f; // ピッチ角速度 [rad/s]
float yaw_rate = 0.0f; // ヨー角速度 [rad/s]
} angular_rate;
// モータコマンド
struct MotorCommands {
uint16_t motor1 = 1000; // 前右モータ [μs]
uint16_t motor2 = 1000; // 前左モータ [μs]
uint16_t motor3 = 1000; // 後左モータ [μs]
uint16_t motor4 = 1000; // 後右モータ [μs]
} motors;
// 状態の表示(デバッグ用)
void printState() const {
ESP_LOGI(TAG, "姿勢: Roll=%.2f°, Pitch=%.2f°, Yaw=%.2f°",
attitude.roll * 180.0f / M_PI,
attitude.pitch * 180.0f / M_PI,
attitude.yaw * 180.0f / M_PI);
ESP_LOGI(TAG, "角速度: Roll=%.2f°/s, Pitch=%.2f°/s, Yaw=%.2f°/s",
angular_rate.roll_rate * 180.0f / M_PI,
angular_rate.pitch_rate * 180.0f / M_PI,
angular_rate.yaw_rate * 180.0f / M_PI);
ESP_LOGI(TAG, "モータ: M1=%d, M2=%d, M3=%d, M4=%d",
motors.motor1, motors.motor2, motors.motor3, motors.motor4);
}
};
// カスケード制御による基本的な制御ループ
class BasicFlightController {
private:
DroneBasicState current_state_;
DroneBasicState target_state_;
// カスケード制御のゲイン
float kp_attitude_ = 5.0f; // 姿勢制御のPゲイン(外側ループ)
float kp_rate_ = 0.2f; // 角速度制御のPゲイン(内側ループ)
public:
// 制御ループ(400Hz で実行)
void controlLoop() {
// 1. センサからの状態読み取り(実際の実装は後の記事で詳解)
readSensors();
// 2. カスケード制御:外側ループ(姿勢制御)
// 目標姿勢角から目標角速度を計算
float target_roll_rate = kp_attitude_ *
(target_state_.attitude.roll - current_state_.attitude.roll);
float target_pitch_rate = kp_attitude_ *
(target_state_.attitude.pitch - current_state_.attitude.pitch);
float target_yaw_rate = kp_attitude_ *
(target_state_.attitude.yaw - current_state_.attitude.yaw);
// 3. カスケード制御:内側ループ(角速度制御)
// 目標角速度と現在の角速度の差分から制御量を計算
float roll_output = kp_rate_ *
(target_roll_rate - current_state_.angular_rate.roll_rate);
float pitch_output = kp_rate_ *
(target_pitch_rate - current_state_.angular_rate.pitch_rate);
float yaw_output = kp_rate_ *
(target_yaw_rate - current_state_.angular_rate.yaw_rate);
// 4. モータミキシング(X配置の場合)
uint16_t base_throttle = 1200; // ベースとなるスロットル値
current_state_.motors.motor1 = base_throttle + roll_output + pitch_output - yaw_output;
current_state_.motors.motor2 = base_throttle - roll_output + pitch_output + yaw_output;
current_state_.motors.motor3 = base_throttle - roll_output - pitch_output - yaw_output;
current_state_.motors.motor4 = base_throttle + roll_output - pitch_output + yaw_output;
// 5. 安全範囲内に制限
constrainMotorOutputs();
// 6. モータへの出力(実際のPWM出力は後の記事で詳解)
outputToMotors();
}
private:
void readSensors() {
// センサ読み取りの詳細実装は第4回で解説
ESP_LOGD(TAG, "センサデータを読み取り中...");
}
void constrainMotorOutputs() {
// モータ出力を安全範囲(1000-2000μs)に制限
current_state_.motors.motor1 = std::clamp(current_state_.motors.motor1,
(uint16_t)1000, (uint16_t)2000);
current_state_.motors.motor2 = std::clamp(current_state_.motors.motor2,
(uint16_t)1000, (uint16_t)2000);
current_state_.motors.motor3 = std::clamp(current_state_.motors.motor3,
(uint16_t)1000, (uint16_t)2000);
current_state_.motors.motor4 = std::clamp(current_state_.motors.motor4,
(uint16_t)1000, (uint16_t)2000);
}
void outputToMotors() {
// PWM出力の詳細実装は第3回で解説
ESP_LOGD(TAG, "モータ出力: %d, %d, %d, %d",
current_state_.motors.motor1, current_state_.motors.motor2,
current_state_.motors.motor3, current_state_.motors.motor4);
}
};
// FreeRTOSタスクとしての実装例
void flight_control_task(void* parameter) {
BasicFlightController controller;
TickType_t last_wake_time = xTaskGetTickCount();
const TickType_t task_period = pdMS_TO_TICKS(2.5); // 2.5ms = 400Hz
ESP_LOGI(TAG, "飛行制御タスクを開始します");
while (1) {
// 制御ループ実行
controller.controlLoop();
// 次回実行まで待機(正確な周期で実行)
vTaskDelayUntil(&last_wake_time, task_period);
}
}
カスケード制御の意味
航空機制御システムとの類似性
よくある問題と対処法
初学者がつまずきやすいポイント
// ステップ1:角速度制御のゲイン調整
void tune_rate_controller() {
float kp_rate = 0.1f; // 小さい値から開始
// 角速度応答を確認しながら徐々に増加
// ステップ2:姿勢制御のゲイン調整
float kp_attitude = 2.0f; // 角速度制御が安定してから調整
}
// デバッグ用のログ出力
void debug_system_state() {
ESP_LOGI(TAG, "=== システム状態 ===");
ESP_LOGI(TAG, "CPU使用率: %d%%", get_cpu_usage());
ESP_LOGI(TAG, "メモリ使用量: %d bytes", get_memory_usage());
ESP_LOGI(TAG, "制御周期: %.2f Hz", get_actual_control_frequency());
}
発展的な話題
高度な制御手法への展開
class ModelPredictiveController {
private:
static constexpr int HORIZON = 10; // 予測ホライズン
float state_prediction_[HORIZON];
public:
float computeOptimalControl(const DroneState& current_state) {
// 未来の状態を予測し、制約を満たしながら最適化
// 二次計画問題を解いて最適制御入力を計算
return solveQP(current_state, state_prediction_);
}
};
class MLController {
private:
TensorFlowLiteModel model_;
public:
MotorCommands predict(const SensorData& sensors) {
// ニューラルネットワークによる制御出力予測
return model_.inference(sensors);
}
};
class SwarmController {
public:
void coordinateWithNeighbors(const std::vector<DroneState>& neighbors) {
// フロッキングアルゴリズムによる群制御
}
};
まとめ
次回予告
おねがいと注意