今回はPID制御の学習のためDCモーターの回転スピードが一定になるように制御をしてみた。
制御とは
言葉のイメージどおり、対象物を思い通りに操ることを制御と呼ぶ。
DCモーターの場合は電圧に応じて回転スピードが変化するので、回転スピードを計測して遅すぎる場合は電圧を上げ、速すぎる場合は電圧を下げれば良さそうだ。ところが実際にやってみるとこのアイデアはうまくいかない。
実は電圧を上げてもその電圧に応じた回転スピードに達するまでにはタイムラグがあるため、センサーで判定するとまだ出力不足と判断して電圧を上げすぎてしまう。逆に電圧を下げてもすぐに回転スピードは落ちないので、今度は電圧を下げすぎてしまう。これを繰り返すのでモーターの速度はフラフラと上がったり下がったりを繰り返すことになる。
実際に試してみたのがこちら。
想定どおり、電圧変動から少し遅れて回転速度が変わっている。
また、モーターは何かしらの仕事をさせる目的で使う為、普通は負荷がかかる。負荷が変動しても一定スピードで動かそうと思うと工夫が必要になる。
PID制御とは
PID制御とは、Proportional(比例)-Integral(積分)-Differential(微分)の頭文字をとった制御方法である。
数学嫌いにとってはうげっとなるワードかもしれないが概念を理解するだけならシンプルなので安心して欲しい。
P制御(比例)
P制御は基準値からの誤差に比例して操作量を増減させる手法である。
要するに、HIGH(下げる) or LOW(上げる)という単純処理じゃなくて、ちゃんと程度問題として扱いましょうということだ。
ちょっと頑張れば誰でも思いつきそうなアイデアなのでここはそれほど難しい話ではない。
ただP制御は基準値に近づけば近づくほど操作量を減らしていくので、実はいつまでたっても基準に到達しない可能性があるという弱点があるらしい。
※曖昧な書き方なのは、今回私が行った実験では特にその弱点が露呈することが無かったためである。
I制御(積分)
I制御は基準値からの誤差の累積に比例して操作量を増減させる手法である。
累積なんてしたらすぐに基準値を飛び越えてしまいそうだけど、うまく調整しないと実際にその通りになる。
ただ飛び越えた分はマイナスの累積となるため段々波が小さくなるように安定してくる。
I制御は安定するまでに時間がかかるというデメリットがあるが、どんなに小さな誤差でもずっと続くと累積によって大きな力を生むため、P制御と組み合わせるとちょうどお互いの弱点を補完しあうことができる。
PIを組み合わせる場合は基本的にはP制御をメインに据えて、誤差のところだけI制御による累積の力をほんの少し借りるというイメージ。
D制御(微分)
D制御は基準値からの誤差の急激な変化を打ち消すような力を働かせる手法である。
そもそも変化が無ければ働かないのでこれ単体では制御手法として成立せず、PIと組み合わせる安定剤に近い役割。
例えば急激に負荷が増えた時などは通常のP制御ではそのまま失速してしまうことがある。どかっと負荷が増えた際には、瞬間的に電圧を特盛にして早急にリカバリーしたい。そこで、変化の傾きを測定して操作量を柔軟に決めるのがこのD制御だ。
PID制御
それぞれの制御をどれくらい取り入れるかは、それぞれゲインと呼ばれる定数を掛け合わせることで行う。
ゲインの調整は以下のサイトに詳しく書いかれているのでとご参考までに。
controlabo.com
私は今回適当なサンプルをベースに試行錯誤するという方法をとったけど、本当はちゃんとやらないとハードを壊しかねないので貴重な機材や危険な機材でやるときはきちんと理解して適切な値の設定が必要なようだ。
今回は模型用モーターを適切な保護機構がついたモータードライバーで制御してるのでまぁ多少間違えてもそんな大層なことにならない。
またPWMで制御してるのでは最小0、最大255までしか入らない。10,000とかPWMで指定してしまったところで255と解釈されるのでせいぜいモーターが100%出力になって煩い程度である。
実験のセットアップ
セットアップはこんな感じ。
準備物
部品取り用ギア―ドモーター
速度検出センサーのスリット付き円盤に合わせるシャフトアダプタが無かったので購入。バラしてアダプタ部分だけ取り外し。ネジザウルスとかラジオペンチでねじりながら引っ張る感じだった。
最初は3Dプリンターでの印刷を試したんだけど、今はある程度大きなものを短時間で印刷する為にノズル径を0.8mmに変更してて、スピード設定なんかも最適化してるので小さいパーツの精密な印刷は出来なかった。毎回米粒みたいになる。小物の精密印刷用に調整しなおすのも面倒なので。。
タミヤのダブルシャフトモーター
その他
- Arduino入りブレッドボード
- モーター駆動用の直流安定化電源
コード
#define AIN1 3 #define AIN2 5 #define ROTARY_ENCODER 2 #define SPAN 250 // milliseconds #define REFERENCE 20 // rolls per SPAN const double Kp = 2; const double Ki = 0.1; const double Kd = 0.5; long rolls = 0; // エンコード用の円盤のスリット数が20なので20カウントしたら1回転。 void detect_turn() { static int cnt = 0; if(cnt++ >= 20){ rolls++; cnt = 0; } } void setup() { pinMode(AIN1,OUTPUT); pinMode(AIN2,OUTPUT); pinMode(ROTARY_ENCODER, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(ROTARY_ENCODER), detect_turn, RISING); Serial.begin(9600); analogWrite(AIN1,0); analogWrite(AIN2,0); } void loop() { static double pwm = 0; static double e = 0; static double e1 = 0; static double ie = 0; double M; static unsigned long last_millis = millis(); if(millis() - last_millis >= SPAN) { e1 = e; e = REFERENCE - rolls; ie = ie + (e + e1)*0.25/2; M = Kp*e + Ki*ie + Kd*(e-e1)/0.25; // 回転数が2未満になると、止まっているか、速すぎて測定できていないかどちらかなので // PWM値で状態を判定して停止中なら始動用に300ミリ秒間255(100%)を出力し、そのあと2秒間出力を20にして安定させる。 // 単に速すぎる場合はそのまま2秒間出力を20にして安定させたのち、pwm値30から再開。 // 回転数が2以上の場合は通常のPID計算に基づいてpwmを決定。 if(rolls<2){ if(pwm<60){ analogWrite(AIN1,255); delay(300); } analogWrite(AIN1,20); delay(2000); pwm = 30; } else { pwm += M; } analogWrite(AIN1,int(pwm)); Serial.print("ROLLS:"); Serial.print(rolls); Serial.print(","); Serial.print("PWM:"); Serial.println(pwm); rolls = 0; last_millis = millis(); } }
実行結果
実行の結果は以下のようになった。
負荷をかけた瞬間にPWM出力が大きく変動することで逆に回転数の変動は最小限に抑えられ、負荷が無くなった瞬間にPWMも急激に下げることで回転数の急激な上昇を防いでいる。しっかりとD操作が効いている。
苦労した点
今回一番苦労したのはPIDとは関係なくモーターの始動用コードの調整である。
モーターは始動時に大きな電力を必要とするため、一時的に最大出力で始動させる必要があるがそうすると回転数もMAXで始動してしまう。
ただ今回使った速度検出センサーは8,000rpm程度の計測が限界で、それを越えると出力が0になって停止状態と判別できなくなってしまう。
待機秒数を間違えるとうまく始動しなかったり、暴走して延々とpwmが上がり続けたりとなかなかうまくいかない。ここでまる2日ハマった。
所感
私がPID制御に興味を持ったのが2022年6月。以下の記事が初めてである。
thom.hateblo.jp
数学を真面目にやってこなかったので微分も積分もできない状態から1年経過し、ようやくモータースピードの調整までたどり着いた。
結局、微分も積分もできないのは変わっていないけど、言ってることは理解できるようになってきたので実際のコードでやってる近似処理もなんとなく頭の中で結びついてきたといったところ。
やはり何かやりたいことを胸に抱きつづけて少しずつでも、途切れながらでも挑戦をしつづければやがては到達できるんだなと思った。
以上