Arduinoで簡単なPID制御のサンプルスケッチ(ソース)を作ってみました。
概要
制御するものですが、Arduino単体で手軽に試せるように、PWMのDuty比を「操作」して、発生する電圧を「制御」するというものです。Duty比をそのまま制御しても良かったのですが、面白味がなかったのであえて電圧にしてみました。(ほとんど変わりませんが・・・。)
PWM出力用の3番pinとアナログ入力の0番pin(AN0)をブレッドボード用のワイヤー等で直差ししておくだけで準備はO.Kです。後はUSBでPCと繋いでおくだけ。
3番ピンから出力されたPWM信号をAN0番ピンで読み取って電圧を測定。目標となる電圧へ制御するものです。内容は、ArduinoのPIDライブラリにあるデモ内容とほぼ同様です。今回ライブラリは使用しません。PIDライブラリは非常に簡単で便利なのですが、痒い所に手が届かないというか・・・。ですので今回は使用しません。
以下のソースや検証結果はArduino101で行ったものですが、UNOでも動くはずです。
UNOで使用する場合は基準電圧や動作周波数の関係で、目標値への収束の仕方が変わってきます。また基準電圧の違いで若干ソースを変更する必要があります。(そのままでも動きますが・・。)
Arduinoソース
記事最下段にソース全体を掲載してます。
切り取って要所のみの説明。
#define Kp 30 #define Ki 300 #define Kd 0.1 #define target 2.5 float duty = 0;
まずは定数の定義、PID各項のゲインと目標となる電圧(ここでは2.5V)を定義しておきます。初期のPWM出力は「0」としてます。0Vからスタートです。
Serial.begin(115200); // initialize Serial communication while (!Serial) { digitalWrite(13, LED); delay(200); LED = 1 - LED; } digitalWrite(13, LED);
シリアル通信が開始されるまで処理をループさせてます。ループ中はオンボードのLED(13番pin)を点滅。シリアル通信が開始されたら、ループを抜け出しLEDを点灯してます。101用の記述です。UNOでもこのままで動きますがシリアル通信開始直後のLEDの点き方が少しおかかったです(なぜか不明)。
for (int i = 0; i < 1000; i++) { vol += analogRead(0); } vol = 3.3 * (vol / 1000) / 1024;
アナログpin(AN0)から電圧を読み取ってます。普通に読み取ってしまうと、PWMのON/OFFをそのまま読み取るだけとなります。ちょっと強引ですが、多めに1000回程サンプリングしてから電圧へ変換計算してます。UNOの場合は基準電圧が5Vなので「3.3」のところを「5.0」とします。
dt = (micros() - preTime) / 1000000; preTime = micros();
ループ時間を取得しておきます。この[dt]を積分項、微分項の計算に使用します。
P = target - vol; I += P * dt; D = (P - preP) / dt; preP = P; duty += Kp * P + Ki * I + Kd * D;
PIDの各項を計算します。積分項は台形近似のほうが正確ですが、今回は単純に積算をしてます。最後に操作量を計算します。微分項はノイズに弱いのでマスクしても良いかも知れません。
実際にArduinoに書き込んでシリアルモニタで電圧変化のログを取ってみました。
▼電圧のPID制御▼
それらしくなるようにわざとオーバーシュートするようなゲイン設定にしてます。電圧変化が、ほぼ理論値と同様の推移で、目標の2.5Vへ収束してます。実測では多少ノイズ?のようなものが入ってしまってます。
いろいろゲインを弄って傾向をみるのも楽しいかと思います。ゲインを弄る場合は制御量が、PWMの0~255を超えないような値にしましょう。(本来はソース側で加味すべきことかもしれませんが・・。)
・ExcelでPID制御をシミュレーション(簡易)
・Arduinoでドローンの自作
Arduinoスケッチ全体
#define Kp 30 #define Ki 300 #define Kd 0.1 #define target 2.5 bool LED; float duty = 0; float dt, preTime; float vol; float P, I, D, preP; void setup() { pinMode(13, OUTPUT); Serial.begin(115200); // initialize Serial communication while (!Serial) { digitalWrite(13, LED); delay(200); LED = 1 - LED; } digitalWrite(13, LED); preTime = micros(); } void loop() { analogWrite(3, duty); for (int i = 0; i < 1000; i++) { vol += analogRead(0); } vol = 3.3 * (vol / 1000) / 1024; PID(); //if (Serial.available() > 0) { //int val = Serial.read(); //if (val == 's') { serialMonitor(); //} //} } inline void PID() { dt = (micros() - preTime) / 1000000; preTime = micros(); P = target - vol; I += P * dt; D = (P - preP) / dt; preP = P; duty += Kp * P + Ki * I + Kd * D; } inline void serialMonitor() { Serial.print(dt , 3); Serial.print(","); Serial.print(duty, 3); Serial.print(","); Serial.print(P, 3); Serial.print(","); Serial.print(I, 3); Serial.print(","); Serial.print(D, 3); Serial.print(","); Serial.println(vol, 3); }
コメント
初学者ですが、気になったのでコメントしました。
duty += Kp * P + Ki * I + Kd * D;
は
duty += Kp * P + Ki * I – Kd * D;
ではないですか?
間違っていたら申し訳ないです。
あみださん、はじめまして。
ご指摘の内容は微分先行型のPI-D制御のことですかね。
本投稿は通常のPID制御ですから微分項は「+」でいいかと思います。
コメント失礼します.
duty比の式についてなのですが,+=ではなく=ではないのですか?
もしよろしければご回答お願い致します.
micさん、はじめまして。
+=でも、=でもどちらでも制御としては機能すると思います。
その際はゲインの効き方が変わると思います。
Arduinoのプログラミングを勉強中の初学者です。
for (int i = 0; i < 1000; i++) {
vol += analogRead(0);
}
vol = 3.3 * (vol / 1000) / 1024;
この部分についてもう少し詳しく解説していただきたいです。
また、なぜ
vol = 3.3 * (vol / 1000) / 1024;
のように計算したのですか?
iiiikさん、サイト拝見下さり有難うございます。
さてご質問の該当ソース部ですが、
電圧測定を1000回行ってその平均取って、
測定値をビット値から電圧[V]に単位換算している部分となります。
また計算式は、平均と単位換算を同時に行ってます。
今後ともサイトを宜しくお願いします。