ヒーター温度のPID制御 – スケッチ

ジャンク箱にあったサーミスタ。ヒーターの制御をするために、温度センサーとしてこのサーミスタを使いたいと考えています。
前回「ヒーター温度のPID制御 – 回路図」で、位相制御の電力調整回路にヒーターをつなぎ、サーミスタで温度をフィードバックさせる回路を作りましたので、今回はこれを動かすスケッチを作りました。

基本的な制御方法

ヒーター温度の制御方法ですが、ごく普通に「PID制御」を行ないます。毎度々々ですが、PID 制御に関してはググってください m(_ _;)m

じつはこの PID 制御、俺がその概要を習ったのは今から 50年近くも昔のことでした。
当時は実際に実験したりとか、関数式などの詳しいことはやらなかったのですが、その概念についてはとても印象に残っています。そんな古い技術がいまでも使われているなんて、素晴らしいことですね。
もちろん今後は、こうした制御にも AI が使われていくんでしょうけど、基本的な考え方はまだまだ生き続けるんじゃないかと思います。

閑話休題

Arduino を使って制御するため、今回は「速度型制御アルゴリズム」を利用します。
今回の操作量を MVn 、前回の操作量を MVn-1 、今回の変化量を dMVn とすると、

MVn = MVn-1 + dMVn

今回の偏差を en 、前回の偏差を en-1 、前々回の偏差を en-2 とすると、

dMVn = Kp*(en - en-1) + Ki*en + Kd*((en - en-1) - (en-1 - en-2))

Kp 、Ki 、Kd はそれぞれ比例ゲイン、積分ゲイン、微分ゲインといわれるパラメータです。

と、ここまではどこにでも書いてありますね。でも俺にとってはちんぷんかんぷんでした。”今回” はわかるけど、”前回” とか “前々回” とかっていつの話よ?
とりあえずその答えだけ。今回のスケッチでは、計測値 (サーミスタの出力値) を 5 秒ごとにサンプリングしています。モーターの回転数なんかと違って温度はゆっくり動きますから、まぁそんな程度でいいんじゃないかと。根拠は、ないです (^_^;)

つまり、”前回” とは 5 秒前、”前々回” とは 10 秒前の数値ということになります。

これ、どうでもいいっちゃどうでもいいんだけど、俺にとってはこの制御方法を理解する上でとっても重要なポイントでした。
詳しい話は、また今度。

スケッチ

全体のスケッチです。

位相制御部分は、若干の修正をしましたが基本的にこれまでと同じです。
PID 制御部分を追加しました。

loop() で 5 秒毎に目標値と計測値を読み取ります。その値を pidControl() に渡して操作量を計算。delayTimer() で操作量に応じた位相制御の点弧角 (ゼロクロスパルス割り込みからの遅延時間) を定めて、トライアックをトリガします。

  1. // AC-Dimmer-PID-Control v.1.01 2020.11.23 by meyon
  2. float Kp = 2.0; // Proportional
  3. float Ki = 0.22; // Integral
  4. float Kd = 1.4; // Differential
  5. int samplingCycle = 5000;
  6. int minimumSetValue = 311; // 55℃
  7. int maximumSetValue = 682; // 25℃
  8. int setValuePin = 0;
  9. int processValuePin = 1;
  10. int zerocrossInterruptPin = 2;
  11. int triggerOutputPin = 4;
  12. volatile bool zero = false;
  13. volatile bool timer = false;
  14. void zeroCross() {
  15.   digitalWrite(triggerOutputPin, LOW);
  16.   digitalWrite(LED_BUILTIN, LOW);
  17.   zero = true;
  18.   timer = true;
  19.   return;
  20. }
  21. void delayTimer(int level) {
  22.   static unsigned long previousTime = 0;
  23.   
  24.   noInterrupts();
  25.   
  26.   if(zero) {
  27.     previousTime = micros();
  28.     zero = false;
  29.   }
  30.   if(micros() - previousTime > level) {
  31.     digitalWrite(triggerOutputPin, HIGH);
  32.     digitalWrite(LED_BUILTIN, HIGH);
  33.     timer = false;
  34.   }
  35.   interrupts();
  36.   return;
  37. }
  38. int pidControl(int SV, int PV) {
  39.   static int en = 0;
  40.   static int en1 = 0;
  41.   static int en2 = 0;
  42.   static int MVn = SV;
  43.   static int MVn1 = 0;
  44.   en2 = en1;
  45.   en1 = en;
  46.   en = SV - PV;
  47.   MVn1 = MVn;
  48.   int dMVn = Kp*(en-en1) + Ki*en + Kd*((en-en1)-(en1-en2));
  49.   MVn = MVn1 + dMVn;
  50.   MVn = constrain(MVn, minimumSetValue, maximumSetValue);
  51.   Serial.print(SV);
  52.   Serial.print(",");
  53.   Serial.print(PV);
  54.   Serial.print(",");
  55.   Serial.print(MVn);
  56.   Serial.print("\n");
  57.   return MVn;
  58. }
  59. void setup() {
  60.   pinMode(triggerOutputPin, OUTPUT);
  61.   pinMode(LED_BUILTIN, OUTPUT);
  62.   attachInterrupt(digitalPinToInterrupt(zerocrossInterruptPin), zeroCross, RISING);
  63.   Serial.begin(9600);
  64. }
  65. void loop() {
  66.   static int dimmerLevel = 0;
  67.   static int manupilativeValue = 0;
  68.   static unsigned long interval = millis()-60000;
  69.   if(millis() - interval > samplingCycle) {
  70.     int setValue = analogRead(setValuePin);
  71.     setValue = map(setValue, 1023, 0, maximumSetValue, minimumSetValue);
  72.     int processValue = analogRead(processValuePin);
  73.     manupilativeValue = pidControl(setValue, processValue);
  74.     interval = millis();
  75.   }
  76.   
  77.   if(timer) {
  78.     if(zero) {
  79.       dimmerLevel = map(manupilativeValue, maximumSetValue, minimumSetValue, 8300, 100);
  80.       dimmerLevel = constrain(dimmerLevel, 100, 8300);
  81.     }
  82.     delayTimer(dimmerLevel);
  83.   }
  84. }

Serial.print() などは、制御動作のようすをシリアルプロッタに出力している部分です。プロッタに描かれるグラフを見ながら、比例ゲイン、積分ゲイン、微分ゲインの値を決めていきました。そのあたりのことはまた今度。

PID 制御

関数 pidControl() は、目標値 (ボリュームの設定値) と計測値 (サーミスタの出力値) を引数にして、戻り値として操作量を返します。
この関数は、サンプリング周期 samplingCycle の 5 秒ごとに呼び出されます。

まず、前回、前々回の偏差と前回の操作量をシフトしておきます。今回偏差 en は 目標値 SV と計測値 PV との差です。
それぞれの数値を計算式に代入して、今回操作量 MVn を算出。
操作量は、実際に制御する温度範囲の操作量、つまり 25℃ で 682 、55℃ で 311 の範囲に限定しています。そうすることで、現実離れした操作量になってしまうことを回避しています。

遅延時間 (位相) 制御

ゼロクロスパルスにより割り込みハンドラ zeroCross() を呼び出し、トリガ出力をオフにします。

割り込みがあったら delayTimer() により遅延タイマーをスタートさせ、操作量に応じた遅延時間を定めます。遅延時間は 8300μS から 100μS の範囲で、それぞれ約 0% 、約 100% の電力出力となります。
遅延時間が経過したら、トリガ出力をオンにし、トライアックを点弧させます。

目標値、計測値入力

loop() 内で、サンプリング周期の 5 秒ごとに目標値と計測値を読み込みます。
目標値は、ボリュームの出力電圧が 5V のとき 25℃ に相当する 682 、0V で 55℃ の 311 となります。 計測値も、サーミスタの特性により 25℃ で 682 、55℃ で 311 です。

読み込んだ数値は、pidControl() へ引数として渡します。

速度型制御アルゴリズムって…

速度型制御アルゴリズムについてですが、何度も何度もテストしているうちにだんだんその意味がわかってきましたよ (^_^;)
次回はそのあたりのことを、俺なりの理解で説明してみたいと思います。

タイトルとURLをコピーしました