Термостат для инкубатора или PID регулятор на arduino

Применение термостата с ПИД управлением не ограничено птицеводством, КО, проект может использоваться пивоварами для поддержания температуры сусла, винокурами в перегонных аппаратах, может просто греть воду в бойлере до приятной температуры, после небольших изменений, в самодельных паяльных станциях, муфельных печах, кароче везде где требуется контроль температуры с высокой точность. Принцип работы и отличие от банального термостата с гистерезисом показан в видео:

Ниже схемы подключения и исходные коды проекта.

О датчиках температуры:

Исходные коды проекта написаны нескольких типов датчиков:

  • TMP102 — датчик температуры с интерфейсом I2C, подробнее про работу с датчиком написано тут. Возможно датчик потребует корректировку температуры.
  • LM35 — не дорогой и распространенный, достаточно точный аналоговый датчик температуры, подробнее про него тут.
  • MCP9808 — высокоточный датчик температуры с интерфейсом I2C. Оптимальный выбор.

Имея навыки программирования ардуино, изменить код под другие датчики температуры труда не составит.

О ЖКИ индикаторе:

Решил не усложнять и взял стандартный текстовый экран WH1602A, про подключение подобных экранов к ардуино уже написано тут. Подключается напрямую, без переходников на I2C. Указанные в проекте номера выходов для подключения ЖКИ совпадают с китайским LCD Keypad Shield, я его использовал на стадии отладки.

Органы управления:

Настройка температуры терморегулятора осуществляется с помощью энкодера, удобно использовать модуль KY-040 по китайской номенклатуре 🙂

О реле:

В регуляторе крайне не рекомендуется использовать электромеханические реле, от частого переключения они выйдут из строя, особенно это относится к китайским релейным модулям за пару долларов.

Правильным решением будет использование твердотельного реле, либо модуль с симистором (что по сути является одним и тем же) на необходимый ток. Например SSR-25DA на 25А, если мощность нагревателя в районе нескольких киловатт, OMRON G3MB 202P держит до 2А или 440Вт.

У меня они в наличии не оказались, пришлось закупить в местном магазине радиодеталей оптосимистор MOC3063 для гальванической развязки и «детекции нуля» с симистором BT137x-800 на 8А и собирать твердотельное реле на макетке. Схема взята из даташита на MOC3063.

Схема подключения:

Структурная схема:

Возможно на этой будет понятней.

Схема подключения актуальная для датчиков с интерфейсом I2C (MCP9808, TMP102), в случаи использования аналогового LM35, его выход подключается к аналоговому входу А5.

 Исходные коды:

Датчик температуры TMP102, LCD1602
//Термостат для инкубатора или PID регулятор на arduino
//код из видео https://www.youtube.com/watch?v=9odcsuAquLU
//донат, http://www.donationalerts.ru/r/arduinolab

/// датчик температуры TMP102, LCD1602
 
 

#include <EEPROM.h>
#include <Wire.h>
#include <LiquidCrystal.h>
#include "SparkFunTMP102.h"
#include <PID_v1.h>

TMP102 tmp102(0x48); 
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);
//////////////////////////////////////////
#define ENCODER_A 2                          // вход энкодера
#define ENCODER_B 3
#define ENCODER_KEY 11

#define TEMP_MAX 60.0                        // Приделы настройки термостата
#define TEMP_MIN 25.0

#define RELAY_PIN 12                         // выход на реле

#define WindowSize 512                       // периуд, для симистора можно оставить, для
                                             // реле увеличить в ~10 раз
////////////////////////////////////////////// 

bool encoderPinALast = LOW;
bool n = LOW;
unsigned long windowStartTime;

double Setpoint, Input, Output;
double Kp=2, Ki=5, Kd=1.5;
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);
 
float temperature;
 
void setup() {
//   Serial.begin(9600);
   
   tmp102.begin();
   
   lcd.begin(16, 2);
   lcd.clear();

   pinMode(RELAY_PIN, OUTPUT);
   pinMode (ENCODER_A, INPUT);
   pinMode (ENCODER_B, INPUT);
   pinMode (ENCODER_KEY, INPUT);
   
   tmp102.wakeup();

   myPID.SetOutputLimits(0, WindowSize);
   myPID.SetMode(AUTOMATIC); 

      //// проверка ЕЕПРОМ для первого включения 
   if (EEPROM.read(0) == 255 && EEPROM.read(4) == 255){
        EEPROM.write(0, 10);
        EEPROM.write(1, 0);
        EEPROM.write(2, 240);
        EEPROM.write(3, 65);
   }
 
   EEPROM.get(0, Setpoint);

 
}

void loop() {
  temperature = tmp102.readTempC();
  Input = temperature;
  
  myPID.Compute();  

   lcd.setCursor(0, 0);
   lcd.print(temperature);
   lcd.print("C  ");
   lcd.setCursor(8, 0);
   lcd.print(Setpoint, 1);
   lcd.print("C     ");
   lcd.setCursor(0, 1); 
   lcd.print(map(Output, 0, WindowSize, 0, 100)); 
   lcd.print("%  "); 
   lcd.setCursor(8, 1);  
   lcd.print(Output);
   lcd.print("mS "); 
     

///////////////////////


   if(!digitalRead(ENCODER_KEY)){                    // если надавили на кнопку, входим в настройки
     lcd.setCursor(13, 0); 
     lcd.print("<<");  
                                                     // управление энкодером
   n = digitalRead(ENCODER_A);
   if ((encoderPinALast == LOW) && (n == HIGH)) {
     if (digitalRead(ENCODER_B) == LOW) {
        Setpoint -= .1; 
        if (Setpoint < TEMP_MIN) Setpoint = TEMP_MIN;        // проверяем приделы настроек
     } else {
        Setpoint += .1; 
        if (Setpoint > TEMP_MAX) Setpoint = TEMP_MAX;
     }  
   } 
   EEPROM.put(0, Setpoint);                          // пишим в еепром настройку 
   encoderPinALast = n;
  delay(5);
//  return;
   }


      /************************************************   * turn the output pin on/off based on pid output   ************************************************/
  if (millis() - windowStartTime >= WindowSize) { //time to shift the Relay Window
    windowStartTime += WindowSize;
    if (windowStartTime > millis()) windowStartTime = 0;    // защита от переполнения
  }
  if (Output < millis() - windowStartTime) digitalWrite(RELAY_PIN, LOW);
  else digitalWrite(RELAY_PIN, HIGH);
 

}

Датчик температуры TMP102, LCD0802
//Термостат для инкубатора или PID регулятор на arduino
//код из видео https://www.youtube.com/watch?v=9odcsuAquLU
//донат, http://www.donationalerts.ru/r/arduinolab

/// датчик температуры TMP102, LCD0802
///  отличается только выводом информации на ЖК

#include <EEPROM.h>
#include <Wire.h>
#include <LiquidCrystal.h>
#include "SparkFunTMP102.h"                   // https://github.com/sparkfun/SparkFun_TMP102_Arduino_Library
#include <PID_v1.h>                           // https://playground.arduino.cc/Code/PIDLibrary

TMP102 tmp102(0x48);                          // адрес TMP102 на шине, может отличатся.
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);
//////////////////////////////////////////
#define ENCODER_A 2                          // вход энкодера
#define ENCODER_B 3
#define ENCODER_KEY 11

#define TEMP_MAX 60.0                        // Приделы настройки термостата
#define TEMP_MIN 25.0

#define RELAY_PIN 12                         // выход на реле

#define WindowSize 512                       // периуд, для симистора можно оставить, для
                                             //  реле увеличить в ~10 раз
////////////////////////////////////////////// 

bool encoderPinALast = LOW;
bool n = LOW;
unsigned long windowStartTime;

double Setpoint, Input, Output;
double Kp=2, Ki=5, Kd=1.5;
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);
 
float temperature;
 
void setup() {
//   Serial.begin(9600);
   
   tmp102.begin();
   
   lcd.begin(8, 2);
   lcd.clear();

   pinMode(RELAY_PIN, OUTPUT);
   pinMode(ENCODER_A, INPUT);
   pinMode(ENCODER_B, INPUT);
   pinMode(ENCODER_KEY, INPUT);
   
   tmp102.wakeup();

   myPID.SetOutputLimits(0, WindowSize);
   myPID.SetMode(AUTOMATIC); 


   //// проверка ЕЕПРОМ для первого включения 
   if (EEPROM.read(0) == 255 && EEPROM.read(4) == 255){
        EEPROM.write(0, 10);
        EEPROM.write(1, 0);
        EEPROM.write(2, 240);
        EEPROM.write(3, 65);
   }
 
   EEPROM.get(0, Setpoint);

 
}

void loop() {
  temperature = tmp102.readTempC();
  Input = temperature;
  
  myPID.Compute(); 
    

/////////////////////// 

   if(!digitalRead(ENCODER_KEY)){                    // если надавили на кнопку, входим в настройки
                                         
         lcd.setCursor(0, 0);                        // отображаем страницу настроек
         lcd.print("settings"); 
         lcd.setCursor(1, 1); 
         lcd.print(Setpoint, 1);
         lcd.print("C   ");   
         
                                                     // управление энкодером
   n = digitalRead(ENCODER_A);
   if ((encoderPinALast == LOW) && (n == HIGH)) {
     if (digitalRead(ENCODER_B) == LOW) {
        Setpoint -= .1; 
        if (Setpoint < TEMP_MIN) Setpoint = TEMP_MIN;        // проверяем приделы настроек
     } else {
        Setpoint += .1; 
        if (Setpoint > TEMP_MAX) Setpoint = TEMP_MAX;
     }  
   } 
   EEPROM.put(0, Setpoint);                          // пишим в еепром настройку 
   encoderPinALast = n;
  delay(5);
 
   }
                                                     // иначе отобрадаем страницу с текущей 
   else{                                             //  температурой и мощьностью
     lcd.setCursor(0, 0);
     lcd.print(temperature);
     lcd.print("C  ");
   
     lcd.setCursor(1, 1); 
     lcd.print(map(Output, 0, WindowSize, 0, 100)); 
     lcd.print("%    "); 
 
   }


      /************************************************   * turn the output pin on/off based on pid output   ************************************************/
  if (millis() - windowStartTime >= WindowSize) {           // таймер на миллис
    windowStartTime += WindowSize;
    if (windowStartTime > millis()) windowStartTime = 0;    // защита от переполнения
  }
  if (Output < millis() - windowStartTime) digitalWrite(RELAY_PIN, LOW);
  else digitalWrite(RELAY_PIN, HIGH);
 

}

Датчик температуры LM35, LCD1602
//Термостат для инкубатора или PID регулятор на arduino
//код из видео https://www.youtube.com/watch?v=9odcsuAquLU
//донат, http://www.donationalerts.ru/r/arduinolab

/// датчик температуры LM35

#include <EEPROM.h>
#include <Wire.h>
#include <LiquidCrystal.h> 
#include <PID_v1.h>                         // https://playground.arduino.cc/Code/PIDLibrary

 
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);
//////////////////////////////////////////
#define ENCODER_A 2                          // вход энкодера
#define ENCODER_B 3
#define ENCODER_KEY 11
#define LM35_PIN A5                          // вход датчика LM35 (1-5V, 2-OUT, 3-GND)

#define TEMP_MAX 60.0                        // Приделы настройки термостата
#define TEMP_MIN 25.0

#define RELAY_PIN 12                         // выход на реле

#define WindowSize 500                       // периуд, для симистора можно оставить, для
                                             // реле увеличить в ~10 раз
////////////////////////////////////////////// 

bool encoderPinALast = LOW;
bool n = LOW;
unsigned long windowStartTime;

double Setpoint, Input, Output;
double Kp=2, Ki=5, Kd=1.5;                    // коэффициенты настройки PID
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);
 
float temperature;
 
 
void setup() {
//   Serial.begin(9600); 
   
   lcd.begin(16, 2);
   lcd.clear();

   pinMode(RELAY_PIN, OUTPUT);
   pinMode (ENCODER_A, INPUT);
   pinMode (ENCODER_B, INPUT);
   pinMode (ENCODER_KEY, INPUT);
   
   analogReference(INTERNAL);                   // включаем внутрений источник опорного 1,1 вольт

   myPID.SetOutputLimits(0, WindowSize);
   myPID.SetMode(AUTOMATIC); 

      //// проверка ЕЕПРОМ для первого включения 
   if (EEPROM.read(0) == 255 && EEPROM.read(4) == 255){
        EEPROM.write(0, 10);
        EEPROM.write(1, 0);
        EEPROM.write(2, 240);
        EEPROM.write(3, 65);
   }
 
   EEPROM.get(0, Setpoint);

 
}

void loop() {
 
  temperature = analogRead(LM35_PIN) / 9.31;         // забераем температуру 
  Input = temperature;
  
  myPID.Compute();  
   

/////////////////////// 

   if(!digitalRead(ENCODER_KEY)){                    // если надавили на кнопку, входим в настройки
       digitalWrite(RELAY_PIN, LOW);                 // и выключаем нагреватель.     
         lcd.clear();                                // очищаем экран и выводим параметр 
         lcd.setCursor(8, 0);
         lcd.print(Setpoint, 1);
         lcd.print("C  "); 
                                                     // управление энкодером
   n = digitalRead(ENCODER_A);
   if ((encoderPinALast == LOW) && (n == HIGH)) {
     if (digitalRead(ENCODER_B) == LOW) {
        Setpoint -= .1; 
        if (Setpoint < TEMP_MIN) Setpoint = TEMP_MIN;        // проверяем приделы настроек
     } else {
        Setpoint += .1; 
        if (Setpoint > TEMP_MAX) Setpoint = TEMP_MAX;
     }  
     EEPROM.put(0, Setpoint);                          // пишим в еепром настройку 
   }  
   encoderPinALast = n; 
  delay(5);
  return;
   }

 //
  if (millis() - windowStartTime >= WindowSize) {             // таймер на миллис
    windowStartTime += WindowSize;
     if (windowStartTime > millis()) windowStartTime = 0;    // защита от переполнения

       lcd.setCursor(0, 0);                                  
       lcd.print(temperature);                               // вывод на экран перенес в таймер
       lcd.print("C  ");                                     //  чтение с LM35 происходит быстрее. 
       lcd.setCursor(8, 0);                                  //  отсутствует необходимая LCD 
       lcd.print(Setpoint, 1);                               //  экрану зарежка
       lcd.print("C  ");
       lcd.setCursor(0, 1); 
       lcd.print(map(Output, 0, WindowSize, 0, 100)); 
       lcd.print("%  "); 
       lcd.setCursor(8, 1);  
       lcd.print(Output);
       lcd.print("mS "); 
  }
  if (Output < millis() - windowStartTime) digitalWrite(RELAY_PIN, LOW);
  else digitalWrite(RELAY_PIN, HIGH);
 

}

Датчик температуры MCP9808, LCD1602
//Термостат для инкубатора или PID регулятор на arduino
//код из видео https://www.youtube.com/watch?v=9odcsuAquLU
//донат, http://www.donationalerts.ru/r/arduinolab

/// датчик температуры MCP9808, LCD1602
 
 

#include <EEPROM.h>
#include <Wire.h>
#include <LiquidCrystal.h>
#include "Adafruit_MCP9808.h"                // https://codeload.github.com/adafruit/Adafruit_MCP9808_Library/zip/master
#include <PID_v1.h>                          // https://playground.arduino.cc/Code/PIDLibrary 

Adafruit_MCP9808 tempsensor = Adafruit_MCP9808();
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);
//////////////////////////////////////////
#define ENCODER_A 2                          // вход энкодера
#define ENCODER_B 3
#define ENCODER_KEY 11

#define TEMP_MAX 60.0                        // Приделы настройки термостата
#define TEMP_MIN 25.0

#define RELAY_PIN 12                         // выход на реле

#define WindowSize 512                       // периуд, для симистора можно оставить, для
                                             // реле увеличить в ~10 раз
////////////////////////////////////////////// 

bool encoderPinALast = LOW;
bool n = LOW;
unsigned long windowStartTime;

double Setpoint, Input, Output;
double Kp=2, Ki=5, Kd=1.5;
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);
 
float temperature;
 
void setup() {
//   Serial.begin(9600);
    
   lcd.begin(16, 2);
   lcd.clear();

   if (!tempsensor.begin()) {
    lcd.print("not find MCP9808!");
    while (1);
  }

   pinMode(RELAY_PIN, OUTPUT);
   pinMode (ENCODER_A, INPUT);
   pinMode (ENCODER_B, INPUT);
   pinMode (ENCODER_KEY, INPUT); 

   myPID.SetOutputLimits(0, WindowSize);
   myPID.SetMode(AUTOMATIC); 

      //// проверка ЕЕПРОМ для первого включения 
   if (EEPROM.read(0) == 255 && EEPROM.read(4) == 255){
        EEPROM.write(0, 10);
        EEPROM.write(1, 0);
        EEPROM.write(2, 240);
        EEPROM.write(3, 65);
   }
 
   EEPROM.get(0, Setpoint);

 
}

void loop() {
  temperature = tempsensor.readTempC();
  Input = temperature;
  
  myPID.Compute();  

   lcd.setCursor(0, 0);
   lcd.print(temperature);
   lcd.print("C  ");
   lcd.setCursor(8, 0);
   lcd.print(Setpoint, 1);
   lcd.print("C     ");
   lcd.setCursor(0, 1); 
   lcd.print(map(Output, 0, WindowSize, 0, 100)); 
   lcd.print("%  "); 
   lcd.setCursor(8, 1);  
   lcd.print(Output);
   lcd.print("mS "); 
     

///////////////////////


   if(!digitalRead(ENCODER_KEY)){                    // если надавили на кнопку, входим в настройки
     lcd.setCursor(13, 0); 
     lcd.print("<<");  
                                                     // управление энкодером
   n = digitalRead(ENCODER_A);
   if ((encoderPinALast == LOW) && (n == HIGH)) {
     if (digitalRead(ENCODER_B) == LOW) {
        Setpoint -= .1; 
        if (Setpoint < TEMP_MIN) Setpoint = TEMP_MIN;        // проверяем приделы настроек
     } else {
        Setpoint += .1; 
        if (Setpoint > TEMP_MAX) Setpoint = TEMP_MAX;
     }  
   } 
   EEPROM.put(0, Setpoint);                          // пишим в еепром настройку 
   encoderPinALast = n;
  delay(5);
//  return;
   }


      /************************************************
   * turn the output pin on/off based on pid output
   ************************************************/
  if (millis() - windowStartTime >= WindowSize) { //time to shift the Relay Window
    windowStartTime += WindowSize;
    if (windowStartTime > millis()) windowStartTime = 0;    // защита от переполнения
  }
  if (Output < millis() - windowStartTime) digitalWrite(RELAY_PIN, LOW);
  else digitalWrite(RELAY_PIN, HIGH);
 

}

Возможные проблемы и пути решения:

 


пока всё.