/* ペルチェ素子の温度コントロール PID制御、温度センサ:サーミスタ、温度設定:ロータリーエンコーダー 状態をOLEDに表示 2019/11/11 ラジオペンチ http://radiopench.blog96.fc2.com/ */ #include // タイマー割り込み使用 #include // 128x32 OLED #define SCREEN_WIDTH 128 // OLED display width, in pixels #define SCREEN_HEIGHT 32 // OLED display height, in pixels #define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin) #define posiUpper 11 // パワーFETの接続ポート定義 #define posiLower 9 #define negaUpper 12 #define negaLower 10 #define lockLED 7 // ロック表示LED pin // PIDパラメーター #define Kp 3.0 // 4.0 #define Ki 0.06 // 0.06 #define Kd 10.0 // 10.0 #define LockWindow 0.25 // 制御OK表示 float Pv; // 現在温度 (Present Vlue) float Pv35; // LM35Zの測定温度 float Sv = 25.0; // 温度設定値 (Set Value) float Mv; // 操作量 char buff[8]; // 文字列操作バッファ int lockCount = 0; // 制御ロック回数カウンタ volatile int X = 20 * 2; // 温度(エンコーダー)の初期値 volatile int tCount = 0; Adafruit_SSD1306 oled(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); void setup() { analogReference(INTERNAL); // ADCは内部refを使用(フルスケール=1.1V) Serial.begin(115200); oled.begin(SSD1306_SWITCHCAPVCC, 0x3C); // OLED初期化 oled.clearDisplay(); oled.setTextSize(2); // 2 x pixel scale oled.setTextColor(WHITE); // 白文字で表示 pinMode(7, OUTPUT); // LED 表示 pinMode(posiLower, OUTPUT); digitalWrite(posiLower, LOW); // Hブリッジの初期化 pinMode(negaLower, OUTPUT); digitalWrite(negaLower, LOW); pinMode(posiUpper, OUTPUT); digitalWrite(posiUpper, HIGH); pinMode(negaUpper, OUTPUT); digitalWrite(negaUpper, HIGH); pinMode(2, INPUT_PULLUP); // ロータリーエンコーダーA相 pinMode(3, INPUT_PULLUP); //            B相 Sv = tempMeasure2(); // 現在温度を設定値として開始 X = Sv * 2; MsTimer2::set(5, timer2IRQ); // 割り込み周期設定 MsTimer2::start(); } void loop() { while (tCount < 40) { // 設定時間経過まで待つ(割り込み周期の倍数で時間指定) } tCount = 0; Sv = getSv(); // 設定値の読み取り // Pv35 = tempMeasure(); // 温度測定(LM35Z) Pv = tempMeasure2(); // サーミスタ Mv = pid2(Sv, Pv); // PIDの制御量を決定 peltierDrive(Mv); // 設定値でペルチェをドライブ Serial.print(Mv /10.0); Serial.print(", "); Serial.print(Sv); Serial.print(", "); Serial.println(Pv); // Serial.print(Mv / 10.0); Serial.print(", "); Serial.print(Sv); Serial.print(", "); Serial.print(Pv); Serial.print(", "); Serial.println(Pv35); if (abs(Sv - Pv) <= LockWindow) { // 誤差が規定範囲内かチェック lockCount++; if (lockCount > 5) { // 連続5回以上規定範囲内だったら digitalWrite(lockLED, HIGH); // LEDを点灯(温度ロックOK表示) lockCount = 5; } } else { // 範囲外なら lockCount = 0; // カウンタリセットして digitalWrite(lockLED, LOW); // LEDを消灯 } panelDisp(Sv, Pv, Mv); // OLED表示 } void peltierDrive(float p) { // ペルチェ駆動 digitalWrite(posiLower, LOW); // 貫通電流防止のために一旦全部OFF digitalWrite(negaLower, LOW); digitalWrite(posiUpper, HIGH); digitalWrite(negaUpper, HIGH); delayMicroseconds(20); // 念のためにちょっと待つ if (p >= 0) { // 順方向に通電(発熱) digitalWrite(posiUpper, LOW); analogWrite(negaLower, p * 2.55); // PWM } else { // 逆方向に通電(吸熱) p = -p; digitalWrite(negaUpper, LOW); analogWrite(posiLower, p * 2.55); // PWM } } float getSv() { // 設定値を読み取る if (X < 0) { // (割り込みルーチンが持っているXの値を使い X = 0; // スケーリングと上下限を調整) } if (X > 60 * 2) { X = 60 * 2; } return X * 0.5; // 1ステップ0.5℃に補正 } float tempMeasure() { // 温度測定 センサ:LM35ZD long d = 0; float t; analogRead(1); // ダミーリード for (int n = 0; n < 10 ; n++) { // 10回測定 d = d + analogRead(1); // } t = d * 1.1 / (102.3); // 平均値を温度の値で返す1mV=0.1℃ return t; } float tempMeasure2() { // サーミスタで温度を測る float V, R, T; // 電圧、抵抗、温度 long x; x = 0; analogRead(0); // ダミーリード for (int n = 0; n < 10; n++) { x += analogRead(0); // A0からサーミスタの電圧を読んで累積 } V = x * 1.1 / 10230.0; // 平均電圧の計算 (1.1V=1023)(10回ループで1.2ms) R = thermistorR(V, 3.3, 75000.0); // 電圧からサーミスタの抵抗値を計算(50μs) T = thermistorT(R, 3431.0, 10000.0, 25.0); // 抵抗値から温度を計算(180μs) return T; } float thermistorR(float Vx, float V0, float Rs) { // サーミスタの抵抗値計算 // Vx:サーミスタの電圧(V)、V0:元電圧(V)、Rs:直列抵抗の値(Ω)(上側に接続) return Vx * Rs / (V0 - Vx); } float thermistorT(float R, float B, float R0, float T0) { // 抵抗値から温度を計算 // R:サーミスタ抵抗(Ω)、B:B値、R0:基準温度の時の抵抗(Ω)、T0:基準温度(℃) return 1 / ((1.0 / B) * log(R / R0) + (1.0 / (T0 + 273.15))) - 273.15; // (logは自然対数で計算される) } float pid(float sv, float pv) { // 制御量を計算(pid制御) float x; float e; static float e1 = 0.0; static float e2 = 0.0; static float lastMv = 0.0; float dMv; // 操作修正量 e = sv - pv; // 現在の誤差量 // 離散値によるPID制御の式 dMv = Kp * (e - e1) + Ki * e + Kd * ((e - e1) - (e1 - e2)); // 補正量計算 x = lastMv + dMv; // 制御量を決定 lastMv = x; // 次回の計算用に値を保存 e2 = e1; e1 = e; if (x > 100.0) { // 調節範囲の上限越していたら x = 100.0; // 100%でクランプ } if (x < -100.0) { // 下限以下なら x = -100.0; // 0%でクランプ } return x; // 実際の制御量を返す } float pid2(float sv, float pv) { // 制御量を計算(pid制御)飽和対策 float x; float e; static float e1 = 0.0; static float e2 = 0.0; static float lastMv = 0.0; float dMv; // 操作修正量 e = sv - pv; // 現在の誤差量 // 離散値によるPID制御の式 dMv = Kp * (e - e1) + Ki * e + Kd * ((e - e1) - (e1 - e2)); // 補正量計算 x = lastMv + dMv; // 制御量を決定 e2 = e1; e1 = e; if (x > 100.0) { // 調節範囲の上限越していたら x = 100.0; // 100%でクランプ } if (x < -100.0) { // 下限以下なら x = -100.0; // 0%でクランプ } lastMv = x; // 次回の計算用に保存(制約付きで保存) return x; // 実際の制御量を返す } void panelDisp(float s, float p, float m) { // 画面表示 int barLength; oled.clearDisplay(); dtostrf(p, 4, 1, buff); // 全体4桁、小数点以下1桁の文字列に変換 oled.setCursor(30, 0); // 1行目開始位置 oled.setTextSize(2); // 文字サイズを倍角に設定 oled.print(buff); oled.print("C"); // 現在温度表示 if (lockCount >= 25) { // 設定値になっていたら(規定内カウンタが設定値以上なら) oled.print(" *"); // 温度ロック表示(*表示) } else { oled.print(" "); } oled.setCursor(8, 17); // 2行目開始位置 oled.setTextSize(1); // 文字サイズを標準サイズに設定 oled.print("set:"); dtostrf(s, 4, 1, buff); // 設定温度表示 oled.print(buff); oled.print("C pow:"); dtostrf(m, 6, 1, buff); // 出力パワー表示 oled.print(buff); oled.setCursor(61, 24); oled.print("v"); // パワーバー中央目盛り表示 if (m > 0) { oled.drawFastHLine(63, 31, m * 0.63, WHITE); // パワーバーの正側表示 } else { oled.drawFastHLine(64 + (m * 0.63), 31, -m * 0.63, WHITE); // 負側表示 } oled.display(); } void timer2IRQ() { // MsTimer2割込み処理(時間管理とロータリーエンコーダーの読み取り) static byte bp = 0; // ロータリーエンコーダー状態記録バッファ tCount++; // 時間間隔測定用タイムスロット数カウンタ bp = bp << 1; // バッファをずらして右端を開けておき、 if (digitalRead(2) == HIGH) { // A相の状態を bp |= 0x01; // bpの末尾に記録 } bp = bp << 1; if (digitalRead(3) == HIGH) { // B相の状態を bp |= 0x01; // bpの末尾に記録 } bp = bp & 0x0F; // 下位4ビット残して上位を消す // if (bp == 0b0111) { // このビットパターンと一致していたら if ((bp == 0b0111) | (bp == 0b1000)) { // 物によってはこちらを使う X ++; // データーをインクリメント } // if (bp == 0b1011) { // このビットパターンと一致していたら、 if ((bp == 0b1011) | (bp == 0b0100)) { // 物によってはこちら X --; // データーをデクリメント } }