/* ペルチェ素子の温度コントロール、温度プログラムパターン再生機能 PID制御、温度センサ:サーミスタ、温度設定:ロータリーエンコーダー 状態をOLEDに表示、最終形にまとめ 2019/12/21 ラジオペンチ http://radiopench.blog96.fc2.com/ */ #include #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 #define startPin 5 // スタートボタン // PIDパラメーター (要調整) #define Kp 4.2 // 4.0 増やすとなぜか感度低下 #define Ki 0.06 // 0.06 #define Kd 2.0 // 2.0 #define LockWindow 0.25 // 制御OK表示ウインド範囲 float Pv; // 現在温度 (Present Vlue) float Sv; // 温度設定値 (Set Value) float lastSv; float Mv; // 操作量 char buff[8]; // 文字列操作バッファ int lockCount = 0; // 制御ロック回数カウンタ volatile int X; // ロータリーエンコーダーの値 volatile int tCount = 0; unsigned long TTC = 0; // 開始からの経過時間(0.2秒ステップで表した経過時間) int opMode; // 動作モード 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.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相 pinMode(startPin, INPUT_PULLUP); // スタートボタン MsTimer2::set(5, timer2IRQ); // 5msでタイマー割り込み(時間管理とロータリーエンコーダー入力で使用) MsTimer2::start(); // 割り込み開始 modeSet(); // 動作モードの設定と運転開始 Serial.println(); Serial.print(F("Operetion Mode = ")); Serial.println(opMode); // シリアルに開始メッセージ Serial.println(F(", t, dP, dI, dD, Mv, Sv, Pv")); // データラベル oled.clearDisplay(); Sv = tempMeasure(); // 現在温度を設定値として開始 lastSv = Sv; X = Sv * 2; // ロータリーエンコーダーの位置を現在温度に合わせる } void loop() { while (tCount < 40) { // 0.2秒間待つ(5ms x 40 =200ms) } tCount = 0; switch (opMode) { // モード番号により温度設定 case 0: Sv = getSv(); // マニュアル設定 break; case 1: Sv = prog1(TTC); // プログラム1 (Step Up/Down) break; case 2: Sv = prog2(TTC); // プログラム2 (Ramp Up/Down) break; case 3: Sv = prog3(TTC); // プログラム3 (Fast High/Low) break; case 4: Sv = prog4(TTC); // プログラム3 (Fast High/Low) break; default: break; } Pv = tempMeasure(); // サーミスタから現在温度を読む Mv = pid(Sv, Pv); // PIDの制御量を決定 peltierDrive(Mv); // ペルチェをドライブ Serial.print(Mv / 10.0); Serial.print(F(", ")); Serial.print(Sv); Serial.print(F(", ")); Serial.println(Pv); 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表示 TTC++; // トータルタイムカウンタをインクリメント } void modeSet() { // モード選択(ロータリーエンコーダーで設定、スタートボタンで確定/運転開始 int lastEEP; oled.setTextSize(2); // 文字サイズ2倍角 opMode = EEPROM.read(0); // EEPROMから前回のモード番号を読む lastEEP = opMode; // 変更されたかチェックするために元の値を記録 if (opMode > 4) { // 無効な値だったら opMode = 0; // 0にしておく (負の値は無い) } X = opMode; // ロータリーエンコーダーの初期値設定 while (digitalRead(startPin) == HIGH) { // スタートボタンが押されていなければ opMode = X; // エンコーダーの現在値を読む if (opMode < 0) { // 値の範囲をチェック opMode = 0; } if (opMode > 4) { opMode = 4; } X = opMode; // 念のためにエンコーダーの値を補正 oled.clearDisplay(); oled.setCursor(0, 0); oled.print(F("Run ")); // 1行目に、 if (opMode == 0) { // モード番号がゼロなら oled.print(F("Manual")); // Manual } else { // ゼロ以外だったら、 oled.print(F("Prog ")); // prog と oled.println(opMode); // プログラム番号表示 if (opMode == 1) { oled.print(F("10-60 by10")); // 2行目にプログラム内容説明 } if (opMode == 2) { oled.print(F("10-60 rump")); } if (opMode == 3) { oled.print(F("10-60 fast")); } if (opMode == 4) { oled.print(F("20-50 fast")); } } oled.display(); // OLEDに表示 delay(100); } if (opMode != lastEEP) { // もし値が変更されていたら EEPROM.write(0, opMode); // 次回起動時用にopModeの値をEEPROMに保存 } } float getSv() { // 設定値を読み取る if (X < 0) { // (割り込みルーチンが持っているXの値を使い X = 0; // スケーリングと上下限を調整) } if (X > 60 * 2) { X = 60 * 2; } return X * 0.5; // 1ステップ0.5℃に補正 } float tempMeasure() { // サーミスタで温度を測る 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, dP, dI, dD; static float e1 = 0.0; static float e2 = 0.0; static float lastMv = 0.0; float dMv; // 操作修正量 if ((sv - lastSv) > 2.0) { // 1回の変化量の上限超えていたら sv = lastSv + 2.0; // 微分成分の飽和防止のために変化量を制限する(2℃/0.2秒) } if ((sv - lastSv) < -2.0) { sv = lastSv - 2.0; } lastSv = sv; // 次回監視用に値を保存 // 離散値に対するPID制御の式で制御量を決定 e = sv - pv; // 現在の誤差量 dP = Kp * (e - e1); // 比例部 dI = Ki * e; // 積分部 dD = Kd * ((e - e1) - (e1 - e2)); // 微分部 dMv = dP + dI + dD; // 補正量計算 Serial.print(F(", ")); Serial.print(TTC * 0.2, 1); // 時間 Serial.print(F(", ")); Serial.print(dP); // ΔKp Serial.print(F(", ")); Serial.print(dI); // ΔKi Serial.print(F(", ")); Serial.print(dD); // ΔKd Serial.print(F(", ")); x = lastMv + dMv; // 制御量を決定 e2 = e1; e1 = e; if (x > 100.0) { // 調節範囲の上限越していたら x = 100.0; // 100%でクランプ } if (x < -100.0) { // 下限以下なら x = -100.0; // -100%でクランプ } lastMv = x; // 次回の計算用に保存(制約付きで保存) return x; // 実際の制御量を返す } void panelDisp(float s, float p, float m) { // 運転中の画面表示 oled.clearDisplay(); oled.setTextSize(1); // 文字サイズを標準に設定 oled.setCursor(0, 0); // 1行目開始位置 if (opMode == 0) { // マニュアルモードだったら oled.print(F("Man")); // 画面左上に小さくMan と表示 } else { // プログラムモードだったら oled.print(F("PG ")); oled.print(opMode); // モード番号(1-3)表示 } oled.setCursor(91, 0); // 画面右上に開始からの累積時間(秒)を、 sprintf(buff, "%6lu", TTC / 5); // UL変数を6桁右詰めで表示 max=999999 oled.print(buff); dtostrf(p, 4, 1, buff); // 全体4桁、小数点以下1桁の文字列に変換 oled.setCursor(30, 0); // 温度表示開始位置 oled.setTextSize(2); // 文字サイズを倍角に設定 oled.print(buff); oled.print(F("C")); // 現在温度表示 oled.setCursor(0, 17); // 2行目開始位置 oled.setTextSize(1); // 文字サイズを標準サイズに設定 oled.print(F("set:")); dtostrf(s, 4, 1, buff); // 設定温度表示 oled.print(buff); oled.print(F("C pow:")); dtostrf(m, 6, 1, buff); // 出力パワー表示 oled.print(buff); oled.print(F("%")); oled.setCursor(61, 25); oled.print(F("Y")); // パワーバー中央目盛り表示 if (m > 0) { oled.fillRect(63, 30, m * 0.63, 31, WHITE); // パワーバーの正側表示(幅2画素) } else { oled.fillRect(64 + (m * 0.63), 30, -m * 0.63, 31, WHITE); // 負側表示 } oled.display(); } float prog1(unsigned long c) { // Program1 10℃から60℃まで10℃ステップで増減 float x; long t; int TS = 60; // 60秒刻みで動作指定(秒で指定) t = (c / 5) % (50 * TS); // 秒の値に換算し50TS(50分)周期で繰り返す if (t < 5 * TS) x = 10.0; // 10℃から開始 {} を省略しているので注意 else if (t < 10 * TS) x = 20.0; // 10℃ずつ上げる else if (t < 15 * TS) x = 30.0; else if (t < 20 * TS) x = 40.0; else if (t < 25 * TS) x = 50.0; else if (t < 30 * TS) x = 60.0; // 最高温度 else if (t < 35 * TS) x = 50.0; else if (t < 40 * TS) x = 40.0; else if (t < 45 * TS) x = 30.0; else if (t < 50 * TS) x = 20.0; else x = 10.0; // 最後に10℃に戻す return x; } float prog2(unsigned long t) { // prog2プログラム 10℃-60℃を2℃/分で滑らかに増減 float x; int TS = 60; // プログラムポイントの刻み秒数(標準は60秒) unsigned long T1 = 5 * TS * 5; // T1時刻 10℃ホールド終了、上昇開始(+2℃/分) unsigned long T2 = 30 * TS * 5; // T2時刻 60℃上昇完了、ホールド unsigned long T3 = 35 * TS * 5; // T3時刻 ホールド終了、降下開始(-2℃/分) unsigned long T4 = 60 * TS * 5; // T4時刻 10℃へ降下完了、ホールド t = t % (60 * TS * 5); // 60分周期で繰り返す if (t < T1) x = 10.0; else if (t < T2) x = 10.0 + 50.0 * (t - T1) / (T2 - T1); // 50℃までランプアップ else if (t < T3) x = 60.0; else if (t < T4) x = 60.0 - 50.0 * (t - T3) / (T4 - T3); // 10℃までランプダウン else x = 10.0; return x; } float prog3(unsigned long t) { // prog3プログラム 10/60℃の繰り返し float x; unsigned int T0 = 180; // 周期を秒で指定 t = (t / 5) % T0; // 秒に換算した後で制御周期の値に変換 if (t < (T0 / 2)) x = 10.0; // 周期の前半は10℃ else x = 60.0; //    後半は60℃ return x; } float prog4(unsigned long t) { // prog4プログラム 20/50℃の繰り返し float x; unsigned int T0 = 120; // 周期を秒で指定 t = (t / 5) % T0; // 秒に換算した後で制御周期の値に変換 if (t < (T0 / 2)) x = 20.0; // 周期の前半は10℃ else x = 50.0; //    後半は60℃ return x; } 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でペルチェのパワー調整 } } 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 --; // データーをデクリメント } }