Arduinoでリフロー炉を作る(4) - PIDライブラリ とチューニング-

PIDライブラリ

リフロー炉は普通PID制御を行います。

Arduino用PIDライブラリが公開されていたので今回はこれを利用させてもらうことにしました。

Arduino Playground - PIDLibrary

このライブラリに添付されているExamples/RelayOutput.inoを改造したものを試してみました。 しかしRelayOutput.inoにはtypoがある様子。

たぶん54行目の

  if(Output < millis() - windowStartTime) digitalWrite(RelayPin,HIGH);

  if(Output > millis() - windowStartTime) digitalWrite(RelayPin,HIGH);

の間違いじゃないかな?

さて、パラメータとかをサンプルのまま試してみた結果こうなりました。 目標値は100℃なのですが180℃までオーバーシュートした上に発振してますね。

f:id:masahirosuzuka:20150506171736j:plain

パラメータのチューニング

PID制御 - Wikipedia

PID制御には3つのパラメータが存在します。wikiの受け売りなのですが簡単に説明するとこんな感じです。

  • Ki : 操作量に対する比例ゲイン

  • Pi : 偏差(目標値と現在の出力値との差分)の時間積分に比例して入力量を変化させるゲイン。つまり偏差が存在する状態が長時間いた場合入力量の変化がおおきくなります。

  • Di : 出力値の急激な変化に応じて入力量を変化させるゲイン。

さて、なんとかして最適なKi Pi Diを導き出さなくてはなりません。 今回はジーグラ・ニコルスのステップ応答法でパラメータの算出を試みることにしました。 なんとなくしか理解できてない状態のまま書いているのでツッコミ大歓迎。wikiに書いてる手順をそのままやってるだけです。

手順1

まず第一ステップとしてオーブンの素の状態の出力特性を出します。

今回は Arduinoでリフロー炉を作る(3) - 実験 - - masahirosuzuka's blog でやった実験の結果が利用できますね。

この出力特性のうち、最も勾配が急激な部分に接戦を引き、傾きを出します。 今回は300から800あたりが最も急勾配に見えるのでそこをグラフに取り出し、近似線を追加します。

f:id:masahirosuzuka:20150506181520j:plain

グラフが300から始まっているのでなんか不格好ですが

y = 0.3301x + 61.61

であることがわかりました。この傾き0.3301をRと置きます。

手順2

次にこの接線y = 0.3301x + 61.61と横軸が交わる時刻と入力を加えた時刻(今回は300と800の中点)の差分を出しこの時間をLとします。 計算したところ、L = 4.36sであることがわかりました。

手順3

手に入れたRとLを元に以下を適用します。

Ki Pi Di
P制御 1/RL
PI制御 0.9/RL 3.3L
PID制御 1.2/RL 2L 0.5L

今回の R=0.3301 L=4.36 を元に計算してみたところ、

Ki Pi Di
0.833775... 8.72 2.18

という結果になりました。

おお!!それっぽい値が出てきたではないか!?(空想科学読本風に)

今回試してみたコードはこれ

#include <SPI.h>
#include <PID_v1.h>
//#include <LiquidCrystal.h>

#define RELAYPIN 7
#define SLAVE 10

double SetPoint, Input, Output;
unsigned long time = 0;
int WindowSize = 5000;
unsigned long windowStartTime;

PID myPID(&Input, &Output, &SetPoint, 2, 5, 1, DIRECT);

void setup() {
  windowStartTime = millis();
  SetPoint = 100;
  myPID.SetOutputLimits(0, WindowSize);
  myPID.SetMode(AUTOMATIC);
  
  pinMode(RELAYPIN, OUTPUT);
  digitalWrite(RELAYPIN, LOW);
  pinMode(SLAVE, OUTPUT);
  digitalWrite(SLAVE, HIGH);
  Serial.begin(9600);
  SPI.begin();
  SPI.setBitOrder(MSBFIRST);
  SPI.setClockDivider(SPI_CLOCK_DIV4);
  SPI.setDataMode(SPI_MODE0);
}

void loop() {
  unsigned int thermocouple; // 14-Bit Thermocouple Temperature Data + 2-Bit
  unsigned int internal; // 12-Bit Internal Temperature Data + 4-Bit
  double disp; // display value

  //delay(100);
  
  digitalWrite(SLAVE, LOW);  //  Enable the chip
  thermocouple = (unsigned int)SPI.transfer(0x00) << 8;  //  Read high byte thermocouple
  thermocouple |= (unsigned int)SPI.transfer(0x00);  //  Read low byte thermocouple
  internal = (unsigned int)SPI.transfer(0x00) << 8;  //  Read high byte internal
  internal |= (unsigned int)SPI.transfer(0x00);  //  Read low byte internal
  digitalWrite(SLAVE, HIGH);  //  Disable the chip

  if ((thermocouple & 0x0001) != 0) {
    Serial.println("ERROR: ");
  } else {
    if ((thermocouple & 0x8000) == 0) { // above 0 Degrees Celsius
      disp = (thermocouple >> 2) * 0.25;
    } else {  // below zero
      disp = (0x3fff - (thermocouple >> 2) + 1)  * -0.25;
    }
    
    Input = disp;
    myPID.Compute();
    
    if (millis() - windowStartTime > WindowSize) {
      windowStartTime += WindowSize; 
    }
    
    int flag = 0;
    if (Output > millis() - windowStartTime) {
      flag = 1;
      digitalWrite(RELAYPIN, HIGH);
    } else {
      flag = 0;
      digitalWrite(RELAYPIN, LOW);
    }
    
    char str[16];
    char temp[16];
    char temp2[16];
    sprintf(str, "%ld, %s, %s, %d", time, dtostrf(disp, 5, 2, temp), dtostrf(Output, 5, 2, temp2), flag);
    Serial.println(str);
    
    time++;
  }
}