前回作成したライントレースカーはガタガタ走行でした。今回は、自動制御でよく用いられるPID制御で、ラインに対して滑らかな走行ができるようにプログラムします。難しそうに感じるかもしれませんが、実際にモノを作ってみると理解度が断然上がります。
使用する部品一覧
使用する車は、前回の記事(ArduinoでSTEM教育 実践編:ライントレースカー)で作成したものを使用します。 電子部品の説明、回路図、ブレッドボード図はこちらを参照してください。
PID制御とは
前回作成したライントレースカーは、左右のフォトリフレクタがラインを検出する/しないの2値で、車の進行方向を制御していました。単純な制御なので理解しやすいですが、車はガタガタ走行になってしまいます。滑らかに走行するためには、フォトリフレクタのライン検出をアナログ値で取得して、 車の進行方向を 細かく調整してあげれば良いのです。その手法というのが、PID制御です。
PID制御は、出力値が目標値に近づくように、入力値を制御する、フィードバック制御の一種です。PIDは、「Proportional-Integral-Differential」を略した表記です。 Proportional は比例、Integralは積分、Differentialは微分を意味します。 これを式で表すと、下記のとおりとなります。 ライントレースカーでは、出力値は左右のフォトリフレクタの検出値、目標値は左右のフォトリフレクタがラインを検出しない値、入力値は左右のタイヤ(モータ)の回転速度です。
P制御
P制御(比例制御)は、ある時における操作量(タイヤの回転速度)を出力値(フォトリフレクタ検出値)と目標値(フォトリフレクタのライン未検出値)の差分の1次関数として制御します。すなわち、ラインから大きくハズレている場合は、左右のタイヤ回転速度差を大きくして、素早くラインに戻ります。ラインから少ししか外れていない場合は、 左右のタイヤ回転速度差を 小さくして、徐々にラインに戻ります。しかし、P制御には弱点が2つあります。
1つ目の弱点は、オフセットです。差分を操作量とするため、目標値近辺では操作量が非常に小さくなり、目標値から少しずれたところで安定してしまいます。これをオフセットまたは定常偏差と呼びます。
2つ目の弱点は、オーバーシュートです。目標値から大きくハズレているときは操作量が大きいので、目標値に到達してもすぐには反応できず、大きく通り過ぎてしまいます。これをオーバーシュートと呼びます。
I制御
I制御(積分制御)は、P制御の弱点であるオフセットを補います。直線を走っている状態であれば、P制御だけでも安定して走ることができます。しかし、カーブでは、オフセットによってカーブの外側で安定しようとします。そのため、曲がれきれずにコースアウトし易くなります。これを改善するために、 過去から現在までの偏差の累積をもとに制御します。 もう少し具体的に説明すると、小さなオフセットで安定していたとしても、そのオフセットを10ms毎に積み重ねていくと、100ms後には10倍の値になります。この値に応じてタイヤの回転速度を制御します。
D制御
D制御(微分制御)は、P制御の弱点であるオーバーシュートを補います。直線から急カーブに突入すると、素早く対応できずにコースアウトしてしまいます。これに対応するためには、「過去の偏差」と「現在の偏差」の差分に応じて制御します。 PI制で 直線で安定走行している場合の偏差はゼロですが、カーブに突入すると偏差が急に大きくなります。カーブが急であればあるほど大きくなります。
限界感度法によるPIDパラメータ調整
PIDの各パラメータ(Kp, Ki, Kd)は、シミュレーションを基に実物を動作させながら合わせ込むのが一般的です。とはいえ、全てのパラメータをランダムに調整していては、とてつもなく時間がかかってしまう。そこで、パラメータ調整の経験則として知られている「限界感度法」を用います。限界限度法の手順を以下に示します。
最初はP制御だけで調整します。Kpを0から少しずつ大きくしていき、制御量が安定して、それ以上大きくすると振動が発散してコースアウトする手前の状態を求めます。このときのKpを限界感度と呼び、Kuで表します。また、その時の制御量の振動周期をPuとします。これらの値は、シリアルプロッタでグラフを見ながら調整すると良いと思います。
次に、KpとKdを求めるために、 積分ゲインをKi = Kp / Ti、 微分ゲインをKd = Kp * Tdとして、最初のPID制御式に代入すると、下記のとおりになります。
さらに、伝達関数で表現すると、下記の式になります。
TiとTdは、限界限度法の経験則から下表のように算出できます。
制御の種類 | Kp | Ti | Td |
---|---|---|---|
P | 0.50Ku | – | – |
PI | 0.45Ku | 0.83Pu | – |
PID | 0.60Ku | 0.50Pu | 0.125Pu |
例えば、Ku=0.5, Pu=0.8secだった場合、PID制御パラメータは以下のように計算できます。
Kp = 0.60Ku = 0.6*0.5 = 0.3
Ki = Kp/Ti = 0.3/(0.50*0.8) = 0.75
Kd = Kp*Td = 0.3*(0.125*0.8) = 0.03
プログラム
I制御やD制御をするためには、フォトリフレクタの過去の検出値を保持しておく必要があります。フォトリフレクタは左右に2個あるため、それぞれ個別に保持しなければならないので、PIDクラスを作成します。
//www.stemship.com //2019.11.22 class PID { private: int _diff[2]; float _integral; int _deltaT; float _p, _i, _d, _pid; public: //コンストラクタ PID (int deltaT) { _deltaT = deltaT; } //PID値を返す int getPID(int now, int target, float KP, float KI, float KD) { _diff[0] = _diff[1]; _diff[1] = now - target; _integral += (_diff[0] + _diff[1]) * _deltaT; _p = KP * _diff[1]; _i = KI * _integral; _d = KD * (_diff[1] - _diff[0]) / _deltaT; _pid = _p + _i + _d; _pid = constrain(_pid, -255, 255); return _pid; } int getP() { return _p; } int getI() { return _i; } int getD() { return _d; } }
メインプログラムは、前回の記事(ArduinoでSTEM教育 実践編:ライントレースカー) をベースにPID制御を組み込んでいます。3行目~6行目のパラメータは、各々の環境で調整して決めてください。フォトリフレクタの目標値は「 const int valTarget = 940 」で設定しています。これはラインの無い床を検出した場合の値になります。PID制御では真ん中のセンサを使用しないため、プログラムから削除しても構いませんが、デバッグ用途で利用できるかもしれないため残しています。
//www.stemship.com //2019.11.22 #include "Motor.h" #include "PID.h" #define DELTA_T 50 //ループ時間[ms] #define KP 0 //各々の環境で調整して決める #define KI 0 //各々の環境で調整して決める #define KD 0 //各々の環境で調整して決める //右と左のモータ Motor motorR = Motor(11, 10); Motor motorL = Motor(6, 5); //PID制御 PID pidR = PID(DELTA_T); PID pidL = PID(DELTA_T); const int valTarget = 940; //トラッキング用センサのピン番号(Analog Input) const int pinSensorR = 1; //右側 const int pinSensorL = 5; //左側 const int pinSensorC = 3; //真ん中 //車の速度 const int carSpeed = 128; //車の動作関数 void runForward(); void runRight(); void runLeft(); void runBack(); void setup() { // put your setup code here, to run once: pinMode(pinSensorR, INPUT); pinMode(pinSensorL, INPUT); pinMode(pinSensorC, INPUT); //シリアル通信 Serial.begin(9600); } void loop() { unsigned long now = millis(); int valSensorR = analogRead(pinSensorR); int valSensorL = analogRead(pinSensorL); int valSensorC = analogRead(pinSensorC); int pidValR = pidR.getPID(valSensorR, valTarget, KP, KI, KD); int pidValL = pidL.getPID(valSensorL, valTarget, KP, KI, KD); Serial.print(valSensorR); Serial.print(","); Serial.print(valSensorL); Serial.print(","); Serial.print(valSensorC); Serial.print(","); Serial.print(pidValR); Serial.print(","); Serial.print(pidValL); Serial.print(","); Serial.print(pidR.getP()); Serial.print(","); Serial.print(pidR.getI()); Serial.print(","); Serial.print(pidR.getD()); Serial.print("\n"); motorR.forward(constrain(carSpeed + pidValR - pidValL, 0, 255)); motorL.forward(constrain(carSpeed + pidValL - pidValR, 0, 255)); //ループ時間[ms] delay(DELTA_T); } //WAITに移行するとき void enterWaiting() { motorR.stop(false); motorL.stop(false); } //RUNに移行するとき void enterRunning() { motorR.forward(255); motorL.forward(255); } //前進 void runForward(int val) { motorR.forward(val); motorL.forward(val); } //後退 void runBack(int val) { motorR.reverse(val); motorL.reverse(val); } //右旋回 void runRight(int val) { motorR.stop(false); motorL.forward(val); } //左旋回 void runLeft(int val) { motorR.forward(val); motorL.stop(false); }
<参考にしたサイト>
・ https://ja.wikipedia.org/wiki/PID%E5%88%B6%E5%BE%A1
・ https://monoist.atmarkit.co.jp/mn/articles/1007/26/news083.html
コメントを残す