Применение термостата с ПИД управлением не ограничено птицеводством, КО, проект может использоваться пивоварами для поддержания температуры сусла, винокурами в перегонных аппаратах, может просто греть воду в бойлере до приятной температуры, после небольших изменений, в самодельных паяльных станциях, муфельных печах, кароче везде где требуется контроль температуры с высокой точность. Принцип работы и отличие от банального термостата с гистерезисом показан в видео:
https://youtu.be/9odcsuAquLU
Ниже схемы подключения и исходные коды проекта.
О датчиках температуры:
Исходные коды проекта написаны нескольких типов датчиков:
- 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);
}
Возможные проблемы и пути решения:
пока всё.