Arduinoで簡単なPID制御のプログラムを作ってみる

 Arduinoで簡単なPID制御のサンプルスケッチ(ソース)を作ってみました。

⊿1(2019/09/15):少しソースの見直しと本文修正。

スポンサーリンク

概要

 制御するものですが、Arduino単体で手軽に試せるように、PWMのDuty比を「操作」して、発生する電圧を「制御」するというものです。Duty比をそのまま制御しても良かったのですが、面白味がなかったのであえて電圧にしてみました。(ほとんど変わりませんが・・・。)

 PWM出力用の3番pinとアナログ入力の0番pin(AN0)をブレッドボード用のワイヤー等で直差ししておくだけで準備はO.Kです。後はUSBでPCと繋いでおくだけ。

 3番ピンから出力されたPWM信号をAN0番ピンで読み取って電圧を測定。PWM信号を操作して目標となる電圧へ制御するものです。内容は、ArduinoのPIDライブラリにあるデモ内容とほぼ同様です。今回ライブラリは使用しません。PIDライブラリは非常に簡単で便利なのですが、痒い所に手が届かないというか・・・。ですので今回は使用しません。

 以下のソースや検証結果はUNOで行ってます。ProやMEGAなどでも動くかと思います。

 UNO以外で使用する場合は、ピンアサインや基準電圧、動作周波数の関係で、目標値への収束の仕方が変わってきます。またそれに伴い若干ソースを変更する必要があります。

Arduinoソース

 記事最下段にソース全体を掲載してます。ここでは切り取って要所の説明。

#define Kp      2
#define Ki      120
#define Kd      1
#define target  2.5

 まずは定数の定義、PID各項のゲインと目標となる電圧(ここでは2.5V)を定義しておきます。

  Serial.begin(115200);
  delay(1000);

 setup()内の記述です。シリアル通信を開始して若干待機しているだけ。

  for (int i = 0; i < 1000; i++) {
    vol += analogRead(0);
  }
  vol = 5.0 * (vol / 1000) / (1 << 10);

 ここからはloop()内の記述です。まずアナログpin(AN0)から電圧を読み取ってます。普通に読み取ってしまうと、PWMのON/OFFをそのまま読み取るだけとなります。ちょっと強引ですが、多めに1000回程サンプリングしてから電圧へ変換計算してます。

  dt = (micros() - preTime) / 1000000;
  preTime = micros();

 プログラムのループ時間(サンプリング時間)を取得しておきます。この「dt」をPID制御の積分項、微分項の計算に使用します。

  P  = target - vol;
  I += P * dt;
  D  = (P - preP) / dt;
  preP = P;

  U = Kp * P + Ki * I + Kd * D;

 PID制御の部分です。積分項は台形近似のほうが正確ですが、今回は単純な積算で数値積分してます。

  if (U > 255)U = 255;
  if (U < 0)  U = 0;
  duty += (U - duty) * 0.2;

 操作量が PWMのデューティー比(analogWriteの引数)0~255を外れないように簡単なマスクしてます。で操作量をそのままデューティ比にしてもよかったのですが、少しだけ反応が遅れるようにわざと遅らせるような処理を加えてます。ここの3行目はPID制御には関係ないです。

  analogWrite(3, duty);

 最後に計算した制御量でPWM出力します。

実際にArduinoに書き込んでシリアルモニタで電圧変化のログを取ってみました。

▼実測結果▼

 それらしくなるようにわざとオーバーシュートするようなゲイン設定にしてます。電圧変化が、ほぼ理論値と同様の推移で、目標の2.5Vへ収束してます。実測では多少ノイズ?のようなものが入ってしまってます。

 いろいろゲインを弄って傾向をみるのも楽しいかと思います。

スポンサーリンク

Arduinoスケッチ全体

// 2017/04/15 imo lab.
// 2021/09/15 imo lab. modify
//https://garchiving.com/

#define Kp      2
#define Ki      120
#define Kd      1
#define target  2.5

float duty = 0;
float dt, preTime;
float vol;
float P, I, D, U, preP;

void setup() {
  Serial.begin(115200);
  delay(1000);
}

void loop() {
  for (int i = 0; i < 1000; i++) {
    vol += analogRead(0);
  }
  vol = 5.0 * (vol / 1000) / (1 << 10);

  PID();
  analogWrite(3, duty);

  //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);
}

inline void PID() {
  dt = (micros() - preTime) / 1000000;
  preTime = micros();
  P  = target - vol;
  I += P * dt;
  D  = (P - preP) / dt;
  preP = P;

  U = Kp * P + Ki * I + Kd * D;
  if (U > 255)U = 255;
  if (U < 0)  U = 0;
  duty += (U - duty) * 0.2;
  //duty = U;
}

コメント

  1. あみだ より:

    初学者ですが、気になったのでコメントしました。

    duty = Kp * P + Ki * I + Kd * D;

    duty = Kp * P + Ki * I – Kd * D;
    ではないですか?

    間違っていたら申し訳ないです。

    • imo より:

      あみださん、はじめまして。
      ご指摘の内容は微分先行型の制御のことですかね。
      本投稿は通常のPID制御ですから微分項は「+」でいいかと思います。

  2. iiiik より:

    Arduinoのプログラミングを勉強中の初学者です。

    for (int i = 0; i < 1000; i++) {
    vol += analogRead(0);
    }
    vol = 3.3 * (vol / 1000) / 1024;

    この部分についてもう少し詳しく解説していただきたいです。

    また、なぜ
    vol = 3.3 * (vol / 1000) / 1024;
    のように計算したのですか?

    • imo より:

      iiiikさん、サイト拝見下さり有難うございます。
      さてご質問の該当ソース部ですが、

      電圧測定を1000回行ってその平均取って、
      測定値をビット値から電圧[V]に単位換算している部分となります。

      また計算式は、平均と単位換算を同時に行ってます。

      今後ともサイトを宜しくお願いします。

      • tenten より:

        実際にdutyに代入される値を表示してみると、かなり大きな数(255以上)が代入されているのですが、その値がそのままanalogwriteとして出力されるのでしょうか??

        • imo より:

          tentenさん、サイト拝見下さり有難う御座います。
          そのまま出力される?の意味が少しわかりませんが、255 で 100% 出力となってしまうためそれ以上は意味ないかと思います。どのような出力になるかは試したことないので不明です。記事内でも少し触れてますが255以上にはならないようソースで工夫したほうが良いかと思います。(サンプルソースを少し修正しておきました)