• Skip to main content
  • Skip to primary sidebar
  • Skip to footer
  • HOME
  • Arduino
    • Arduino初心者編
    • Arduino基礎編
    • Arduino応用編
    • Arduino実践編
  • Raspberry Pi
  • microbit
  • XBee
  • Scratch
  • Contact Us
  • Privacy Policy

STEMSHIP

ものづくりを通して、科学を学ぼう!

現在の場所:ホーム / Arduino / Arduino実践編 / ArduinoでSTEM教育​ 実践編:ライントレースカーをPID制御する

ArduinoでSTEM教育​ 実践編:ライントレースカーをPID制御する

2019年11月22日 by STEMSHIP コメントを書く

前回作成したライントレースカーはガタガタ走行でした。今回は、自動制御でよく用いられるPID制御で、ラインに対して滑らかな走行ができるようにプログラムします。難しそうに感じるかもしれませんが、実際にモノを作ってみると理解度が断然上がります。

使用する部品一覧

使用する車は、前回の記事(ArduinoでSTEM教育​ 実践編:ライントレースカー)で作成したものを使用します。 電子部品の説明、回路図、ブレッドボード図はこちらを参照してください。

PID制御とは

前回作成したライントレースカーは、左右のフォトリフレクタがラインを検出する/しないの2値で、車の進行方向を制御していました。単純な制御なので理解しやすいですが、車はガタガタ走行になってしまいます。滑らかに走行するためには、フォトリフレクタのライン検出をアナログ値で取得して、 車の進行方向を 細かく調整してあげれば良いのです。その手法というのが、PID制御です。

PID制御は、出力値が目標値に近づくように、入力値を制御する、フィードバック制御の一種です。PIDは、「Proportional-Integral-Differential」を略した表記です。 Proportional は比例、Integralは積分、Differentialは微分を意味します。 これを式で表すと、下記のとおりとなります。 ライントレースカーでは、出力値は左右のフォトリフレクタの検出値、目標値は左右のフォトリフレクタがラインを検出しない値、入力値は左右のタイヤ(モータ)の回転速度です。

t:時間、u:操作量、r:目標値、y:出力値、e:偏差
PID制御における、ゲイン調整による応答の変化

P制御

P制御(比例制御)は、ある時における操作量(タイヤの回転速度)を出力値(フォトリフレクタ検出値)と目標値(フォトリフレクタのライン未検出値)の差分の1次関数として制御します。すなわち、ラインから大きくハズレている場合は、左右のタイヤ回転速度差を大きくして、素早くラインに戻ります。ラインから少ししか外れていない場合は、 左右のタイヤ回転速度差を 小さくして、徐々にラインに戻ります。しかし、P制御には弱点が2つあります。

1つ目の弱点は、オフセットです。差分を操作量とするため、目標値近辺では操作量が非常に小さくなり、目標値から少しずれたところで安定してしまいます。これをオフセットまたは定常偏差と呼びます。

2つ目の弱点は、オーバーシュートです。目標値から大きくハズレているときは操作量が大きいので、目標値に到達してもすぐには反応できず、大きく通り過ぎてしまいます。これをオーバーシュートと呼びます。

P制御の応答性

I制御

I制御(積分制御)は、P制御の弱点であるオフセットを補います。直線を走っている状態であれば、P制御だけでも安定して走ることができます。しかし、カーブでは、オフセットによってカーブの外側で安定しようとします。そのため、曲がれきれずにコースアウトし易くなります。これを改善するために、 過去から現在までの偏差の累積をもとに制御します。 もう少し具体的に説明すると、小さなオフセットで安定していたとしても、そのオフセットを10ms毎に積み重ねていくと、100ms後には10倍の値になります。この値に応じてタイヤの回転速度を制御します。

PI制御の応答性

D制御​​

D制御(微分制御)は、P制御の弱点であるオーバーシュートを補います。直線から急カーブに突入すると、素早く対応できずにコースアウトしてしまいます。これに対応するためには、「過去の偏差」と「現在の偏差」の差分に応じて制御します。 PI制で 直線で安定走行している場合の偏差はゼロですが、カーブに突入すると偏差が急に大きくなります。カーブが急であればあるほど大きくなります。

PID制御の応答性

限界感度法によるPIDパラメータ調整

PIDの各パラメータ(Kp, Ki, Kd)は、シミュレーションを基に実物を動作させながら合わせ込むのが一般的です。とはいえ、全てのパラメータをランダムに調整していては、とてつもなく時間がかかってしまう。そこで、パラメータ調整の経験則として知られている「限界感度法」を用います。限界限度法の手順を以下に示します。

最初はP制御だけで調整します。Kpを0から少しずつ大きくしていき、制御量が安定して、それ以上大きくすると振動が発散してコースアウトする手前の状態を求めます。このときのKpを限界感度と呼び、Kuで表します。また、その時の制御量の振動周期をPuとします。これらの値は、シリアルプロッタでグラフを見ながら調整すると良いと思います。

次に、KpとKdを求めるために、 積分ゲインをKi = Kp / Ti、 微分ゲインをKd = Kp * Tdとして、最初のPID制御式に代入すると、下記のとおりになります。

さらに、伝達関数で表現すると、下記の式になります。

TiとTdは、限界限度法の経験則から下表のように算出できます。

制御の種類KpTiTd
P0.50Ku––
PI0.45Ku0.83Pu–
PID0.60Ku0.50Pu0.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

Filed Under: Arduino実践編 関連タグ:ライントレース

Reader Interactions

コメントを残す コメントをキャンセル

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください。

最初のサイドバー

CONTACT US

  • Facebook
  • Twitter
  • Youtube

More to See

Arduinoベースのセンサノード:XBeeデータ収集ノードの製作

2021年9月27日 By STEMSHIP

Arduinoベースのセンサノード:Arduinoを使った温度センサの作製

2021年9月21日 By STEMSHIP

XBeeモジュールとBMP280センサを使用してデータ収集する

2021年5月31日 By STEMSHIP

XBeeモジュールでMicroPythonを使ってセンサを読み取る

2021年5月24日 By STEMSHIP

XBeeモジュールでセンサを読み取る:センサノードの設定と測定

2021年5月17日 By STEMSHIP

カテゴリー

  • Arduino (44)
    • Arduino初心者編 (15)
    • Arduino基礎編 (11)
    • Arduino実践編 (4)
    • Arduino応用編 (11)
    • Arduino番外編 (3)
  • MakeyMakey (1)
  • microbit (11)
  • Raspberry Pi (13)
  • Scratch (6)
  • STEM教育 (5)
  • XBee (15)
  • ものづくり教育 (2)
  • 子供の教育 (2)
  • 科学 (6)
  • 算数 (5)

Tags

BLE DCモータ EEPROM ELEGOO ESP32 ESP8266 IoT LCD LED makeymakey MQTT Node-RED scratch STEM教育 WiFi XBee アニメーション オンラインゲーム カメラ クローン ゲーム コスチュームの変更 サーボモータ ステッピングモータ ステートマシン スピーカ タイマー ピンポン ブロック定義 ペン ライントレース ラジコン リスト 分数 割り込み 実験 工作 幼児 当たり判定 温度センサ 無線通信 物理 角度制御 重力 音楽

Footer

最近の投稿

  • Arduinoベースのセンサノード:スケッチ(プログラム)の作製
  • Arduinoベースのセンサノード:XBeeデータ収集ノードの製作
  • Arduinoベースのセンサノード:Arduinoを使った温度センサの作製
  • XBeeモジュールとBMP280センサを使用してデータ収集する
  • XBeeモジュールでMicroPythonを使ってセンサを読み取る

タグ

BLE DCモータ EEPROM ELEGOO ESP32 ESP8266 IoT LCD LED makeymakey MQTT Node-RED scratch STEM教育 WiFi XBee アニメーション オンラインゲーム カメラ クローン ゲーム コスチュームの変更 サーボモータ ステッピングモータ ステートマシン スピーカ タイマー ピンポン ブロック定義 ペン ライントレース ラジコン リスト 分数 割り込み 実験 工作 幼児 当たり判定 温度センサ 無線通信 物理 角度制御 重力 音楽

Search

2025年5月
月 火 水 木 金 土 日
 1234
567891011
12131415161718
19202122232425
262728293031  
« 10月    

Copyright © 2025 · STEMSHIP.COM