Termostat PID z wyjściem PWM

User avatar
veeroos
Posts: 538
Joined: Sun Mar 20, 2022 9:30 am
Location: Głogów

Post

Cześć. Zainspirowany tematem kolegi Rafała z forum DIY Supla na Facebooku https://www.facebook.com/groups/581640500087690/ poczyniłem termostat ze sterowaniem PID z wyjściem PWM.
334976037_1918868981781065_845433397551899541_n.jpg
334975996_172337718499105_3567886458572552895_n.jpg
Temperatura zadana jest ustawiana Online z Apki Supla, z Clouda lub offlinowo za pomocą przełącznika obrotowego z przyciskiem (tzw enkodera). Czujnik jaki zastosowałem jest to termopara z układem MAX6675, temperatura odczytana, oraz temperatura zadana wyświetlana jest za pomocą wyświetlacza Oled SSD1306.
Kod programu:

Code: Select all

/*
  Copyright (C) AC SOFTWARE SP. Z O.O.

  This program is free software; you can redistribute it and/or
  modify it under the terms of the GNU General Public License
  as published by the Free Software Foundation; either version 2
  of the License, or (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
*/

/*Podłączenie:

I2C:
SDA - 21
SCL - 22

Enkoder:
CLK_PIN - 25
DATA_PIN - 26
SW_PIN - 27


Przycisk konfiguracyjny - 0
Dioda led konfiguracja - 13

termopara MAX6675:
DO  - 19
CS  - 5
CLK - 18

Wyjście PWM - 14

*/


//================================================Includy========================
  #include "AiEsp32RotaryEncoder.h"
  #include "Arduino.h"
  #include <SPI.h>
  #include <Wire.h>
  #include <Adafruit_GFX.h>
  #include <Adafruit_SSD1306.h>
  #include <PIDController.h>
  #include "max6675.h"
  #include <EEPROM.h>
  #include <SuplaDevice.h>
  #include <supla/network/esp_wifi.h>
  #include <supla/control/relay.h>
  #include <supla/control/button.h>
  #include <supla/control/action_trigger.h>
  #include <supla/device/status_led.h>
  #include <supla/storage/littlefs_config.h>
  #include <supla/network/esp_web_server.h>
  #include <supla/device/supla_ca_cert.h>
  #include <supla/events.h>
  #include <supla/control/virtual_relay.h>
  #include <supla/network/html/device_info.h>
  #include <supla/network/html/protocol_parameters.h>
  #include <supla/network/html/status_led_parameters.h>
  #include <supla/network/html/wifi_parameters.h>
  #include <supla/network/html/custom_parameter.h>
  #include <supla/network/html/custom_text_parameter.h>
  #include <supla/sensor/thermometer.h>
//===============================================================================

//===========================Definicje zmiennych=================================
  #define STATUS_LED_GPIO 13
  #define GPIO_Konfiguracja 0
  int sda_pin = 21; // pin to connect to SDA pin on LCD (can be any data pin)
  int scl_pin = 22; // pin to connect to SCL pin on LCD (can be any data pin)
  double ThermostatTemperature;
  unsigned long czasowka;
  unsigned long czas_start;
  bool doSaveTermSetTemp = false;
  float Term_Step = 1;
  double ThermostatHistereza;
  float temperatura;
  int polaczenie_z_serwerem;
  int menu = 0;

  // Define Rotary Encoder Pins
  #define PIN_CLK 25
  #define PIN_DT 26
  #define PIN_SW 27
  #define ROTARY_ENCODER_VCC_PIN -1
  #define ROTARY_ENCODER_STEPS 4

  //instead of changing here, rather change numbers above
  AiEsp32RotaryEncoder rotaryEncoder = AiEsp32RotaryEncoder(PIN_CLK, PIN_DT, PIN_SW, ROTARY_ENCODER_VCC_PIN, ROTARY_ENCODER_STEPS);

  #define thermoDO  19
  #define thermoCS  5
  #define thermoCLK  18
  #define mosfet_pin 14
  #define __DEBUG__
  #define SCREEN_WIDTH 128 // OLED display width, in pixels
  #define SCREEN_HEIGHT 64 // OLED display height, in pixels
  #define OLED_RESET     -1 // Reset pin # (or -1 if sharing Arduino reset pin)
  /*In this section we have defined the gain values for the 
   * proportional, integral, and derivative controller I have set
   * the gain values with the help of trial and error methods.
  */ 
  #define __Kp 30 // Proportional constant
  #define __Ki 0.7 // Integral Constant
  #define __Kd 200 // Derivative Constant
  int clockPin; // Placeholder por pin status used by the rotary encoder
  int clockPinState; // Placeholder por pin status used by the rotary encoder
  long debounce = 0; // Debounce delay
  int encoder_btn_count = 0; // used to check encoder button press
  MAX6675 thermocouple(thermoCLK, thermoCS, thermoDO);
  Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);// Create an instance for the SSD1306 128X64 OLED "display"
  PIDController pid; // Create an instance of the PID controller class, called "pid"


Supla::ESPWifi wifi;
Supla::EspWebServer suplaServer;
Supla::LittleFsConfig configSupla;
Supla::Device::StatusLed statusLed(STATUS_LED_GPIO, false);
Supla::Control::VirtualRelay *relay_0 = nullptr; //    Automat / Ręczny

//==============================================================================================================
//==================== Przypisanie ustawionej temperatury pod klasę termometru =================================
//==============================================================================================================

class TermostatTemp : public Supla::Sensor::Thermometer {
  public:
    TermostatTemp() {}

    void onInit() {
      channel.setNewValue(getValue() );
    }
    double getValue() {
      return ThermostatTemperature;
    }
    void iterateAlways() {
      channel.setNewValue(getValue() );
      if ( millis() - lastReadTime > 2000) {
        lastReadTime = millis();
    }
    }
}; TermostatTemp *TerTemp = nullptr;

//==============================================================================================================
//======================== U S T A W I E N i E    T E M P E R A T U R Y ========================================
//==============================================================================================================

class TermostatSet : public Supla::Control::Relay {
  public:
    TermostatSet() : Relay(-1, true, 32) {}

    void onInit() {
      // do nothing here
    }
    void turnOn(_supla_int_t duration) {
      //  increase "ThermostatTemperature" by Term_Step
      ThermostatTemperature += Term_Step;
      if (ThermostatTemperature > 125.0) ThermostatTemperature = 125.0;
      channel.setNewValue(false);
      doSaveTermSetTemp = true;
    }
    void turnOff(_supla_int_t duration) {
      //  decrease "ThermostatTemperature" by Term_Step
      ThermostatTemperature -= Term_Step;
      if (ThermostatTemperature < -20.0) ThermostatTemperature = -20.0;
      channel.setNewValue(false);
      doSaveTermSetTemp = true;
    }
    bool isOn() {
      return false;
    }
}; TermostatSet *TerSet = nullptr;


//==================Tutaj przekazujemy wartości z czujnika do Supli=====================================

class Temperatura : public Supla::Sensor::Thermometer {
  public:
    Temperatura() {}

    void onInit() {
      channel.setNewValue(getValue() );
    }
    double getValue() 
    {
      return temperatura;
    }
    void iterateAlways() {
      channel.setNewValue(getValue());
    }
}; 

void status_func(int status, const char *msg) {     //    ------------------------ Status polaczednia z serwerem--------------------------
 polaczenie_z_serwerem = status;                                       
}

//=========================================================

void rotary_loop()
{
	//dont print anything unless value changed
  if (menu ==2 ){
	if (rotaryEncoder.encoderChanged())
	{
    ThermostatTemperature = ThermostatTemperature + rotaryEncoder.readEncoder();
    rotaryEncoder.reset();
	}
  }
}

void IRAM_ATTR readEncoderISR()
{
	rotaryEncoder.readEncoder_ISR();
}
//=====================================================================


void setup() {
  Serial.begin(115200);
  EEPROM.begin(1024);
  Wire.begin(sda_pin, scl_pin);

  rotaryEncoder.begin();
	rotaryEncoder.setup(readEncoderISR);
	//set boundaries and if values should cycle or not
	//in this example we will set possible values between 0 and 1000;
	bool circleValues = false;
	rotaryEncoder.setBoundaries(-1, 1, circleValues);
  rotaryEncoder.disableAcceleration();

    pinMode(mosfet_pin, OUTPUT); // MOSFET output PIN
    pinMode(PIN_SW, INPUT_PULLUP);// Encoder SW Pin
    pid.begin();          // initialize the PID instance
    pid.setpoint(150);    // The "goal" the PID controller tries to "reach"
    pid.tune(__Kp, __Ki,__Kd);    // Tune the PID, arguments: kP, kI, kD
    pid.limit(0, 255);    // Limit the PID output between 0 and 255, this is important to get rid of integral windup!
    if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
  #ifdef __DEBUG__
      Serial.println(F("SSD1306 allocation failed"));
  #endif
    for (;;); // Don't proceed, loop forever
  }
  auto przycisk_konfiguracja = new Supla::Control::Button(GPIO_Konfiguracja, false, false);
  przycisk_konfiguracja->configureAsConfigButton(&SuplaDevice);
  
  //================Supla przelacznik do ustawienia temperatury============================
  TerSet = new TermostatSet();
  TerSet->getChannel()->setDefault(SUPLA_CHANNELFNC_POWERSWITCH);
  TerSet->disableChannelState();

  new Temperatura();  // termometr
  new TermostatTemp(); // ustawiona temperatura

  EEPROM.get(0, ThermostatTemperature);
  if ((ThermostatTemperature > 125.0) || isnan(ThermostatTemperature))
  {
    ThermostatTemperature = 20.0;
    EEPROM.put(0, ThermostatTemperature);
    EEPROM.commit();
  }


  SuplaDevice.setSuplaCACert(suplaCACert);
  SuplaDevice.setSupla3rdPartyCACert(supla3rdCACert);


  //======================status polaczenia z serwerem===================================
  SuplaDevice.setStatusFuncImpl(&status_func);
  //=====================================================================================


  new Supla::Html::DeviceInfo(&SuplaDevice);
  new Supla::Html::WifiParameters;
  new Supla::Html::ProtocolParameters;
  SuplaDevice.setName("SUPLA-Termostat");
  SuplaDevice.begin();

  display.setRotation(2); //Rotate the Display
  display.display(); //Show initial display buffer contents on the screen -- the library initializes this with an Adafruit splash screen.
  display.clearDisplay(); // Cleear the Display
  display.setTextSize(2); // Set text Size
  display.setTextColor(WHITE); // set LCD Colour
  display.setCursor(48, 0); // Set Cursor Position
  display.println("PID"); // Print the this Text
  display.setCursor(0, 20);  // Set Cursor Position
  display.println("Regulator"); // Print the this Text
  display.setCursor(22, 40); // Set Cursor Position
  display.println("Temperatury"); // Print the this Text
  display.display(); // Update the Display
  delay(1000); // Delay of 200 ms
}


void loop()
{

   SuplaDevice.iterate();
  //read_encoder(); //Call The Read Encoder Function
  //set_temp(); // Call the Set Temperature Function
    if (doSaveTermSetTemp == true){
    EEPROM.put(0, ThermostatTemperature);
    EEPROM.commit();
    Serial.println("wartosc temperatury zapisana");
    doSaveTermSetTemp = false;
  }

   if (millis() > czasowka){
    temperatura = thermocouple.readCelsius();
    czasowka = millis() + 1000;
    } // Read the Temperature using the readCelsius methode from MAX6675 Library.
    int output = pid.compute(temperatura);    // Let the PID compute the value, returns the optimal output
    analogWrite(mosfet_pin, output);           // Write the output to the output pin
    pid.setpoint(ThermostatTemperature); // Use the setpoint methode of the PID library to
    display.clearDisplay(); // Clear the display
    display.setTextSize(2); // Set text Size
    display.setCursor(0, 0); // Set the Display Cursor
    display.print("Temp:"); //Print to the Display
    display.setCursor(0, 16);// Set the Display Cursor
    display.print(temperatura); // Print the Temperature value to the display in celcius
    display.setCursor(0,32); // Set the Display Cursor
    display.print("Ustaw:"); //Print to the Display
    display.setCursor(0, 48);// Set the Display Cursor
    
    display.print(ThermostatTemperature); // Print the Temperature value to the display in celcius
    if ((menu == 2)||(menu == 3)){
         display.setCursor(65,48); // Set the Display Cursor
         display.print("<--"); //Print to the Display
    }else{
      display.setCursor(65,48);
      display.print("   ");
    }
    display.display(); // Update the Display
#ifdef __DEBUG__
    Serial.print(temperatura); // Print the Temperature value in *C on serial monitor
    Serial.print("   "); // Print an Empty Space
    Serial.println(output); // Print the Calculate Output value in the serial monitor.
#endif
    rotary_loop();
    
 // }
 if (digitalRead(PIN_SW) == LOW) {
  if ( menu == 0){
  czas_start = (millis() + 3000);
    menu = 1;
  }
  if ((menu == 1) && (millis() > czas_start)){
    menu = 2;
  }
  if (menu == 2){
    czas_start = (millis() + 3000);
    menu = 3;
  }
  if ((menu == 3)&&(millis() > czas_start)){
    menu = 0;
  }
 }
 if((menu == 1) && (digitalRead(PIN_SW) == HIGH)){
  menu = 0;
  doSaveTermSetTemp = true;
}
 if((menu == 3) && (digitalRead(PIN_SW) == HIGH)){
  menu = 2;
}
 
}

Urządzenie zbudowane jest na ESP32. Pinologia i podłączenie są na początku programu. Kod programu, tak samo jak w większości moich kodów bazuje na przykładzie "Webinterface" z biblioteki SuplaDevice.
Ustawianie z poziomu urządzenia odbywa się następująco:
Wciskamy przycisk enkodera przez 3 sekundy pojawia się strzałka koło wartości zadanej i wtedy możemy ustawić zadaną wartość, wyjście z rybu ustawiania następuje tak samo, trzeba nacisnąc przez 3 sekundy przycisk enkodera.
Po każdorazowej zmianie wartości w trybie Online, oraz podczas wychodzenia z trybu ustawiania, aktualna wartość nastaw zapisywana jest w Epromie, tak aby po restarcie urządzenia pamiętało ono nastawy...
You do not have the required permissions to view the files attached to this post.
Zamel Mew-01, Wemos D1 mini Pro + Ikea vindriktning + BME280, 3x - SonOff mini, 3x - SonOff Basic, 3xGosund SP111, SonOff S55, 2x GOSUND WB4

https://github.com/v33r005
User avatar
klew
Posts: 9664
Joined: Thu Jun 27, 2019 12:16 pm
Location: Wrocław

Post

A ja nad termostatem siedzę już sporo czasu. 58 commitów, 59 zmienionych plików, 12 000 dodanych linii kodu (bez obaw, większość to testy ;P, chociaż sam termostat też już ponad 3 000 linii ma).
I nawet PID-a nie mam :D

Z ciekawości: jak chcesz używać tego termostatu? W sensie do czego? Jakim urządzeniem grzewczym da się sterować wyjściem PWM?
Przy PID moją największą wątpliwością jest dobór parametrów Kp, Ki, Kd.
Na forum HA znalazłem jakiś post, gdzie ktoś przez 2 miesiące dobiera parametry Kp, Ki, Kd do jakiegoś pomieszczenia w swoim domu...
Kiedy będzie Supla Offline Party / SOP#2025 ?
User avatar
Duch__
Posts: 1930
Joined: Wed Aug 24, 2016 7:26 pm
Location: Opole

Post

Mój domowy termostat zamknął się w prawie 4000 linii kodu ;) Pisany przez wcześniejsze dwa sezony grzewcze.
Image
User avatar
veeroos
Posts: 538
Joined: Sun Mar 20, 2022 9:30 am
Location: Głogów

Post

Jeżeli chodzi o mnie, to hobbystycznie 😉, zajmuję się warzeniem piwa, a tam temperatura ma bardzo duży wpływ na smak, huśtawka z histerezą źle może wpłynąć na powtarzalność 😉. Mam go zamiar do tego celu użyć. Jeśli chodzi o parametry P, I oraz D to wiem że to jest spora zabawa, muszę podpatrzyć jak autokalibracja tych parametrów jest zrobiona w Marlinie pod drukarki 3D.
Ogólnie co do termostatu z histerezą to też jakiś czas temu popełniłem kawałek kodu, ale miałem zaledwe 1400 linii kodu, działać to działało, ale do projektu jeszcze siądę, bo będę chciał dorobić offlinowe harmonogramy, bo dorzuciłem do konstrukcji DS3231 to niech się przyda do czegoś 😉
Zamel Mew-01, Wemos D1 mini Pro + Ikea vindriktning + BME280, 3x - SonOff mini, 3x - SonOff Basic, 3xGosund SP111, SonOff S55, 2x GOSUND WB4

https://github.com/v33r005
krycha88
Posts: 5417
Joined: Fri Nov 16, 2018 7:25 am

Post

klew wrote: Sat Mar 11, 2023 9:16 pm A ja nad termostatem siedzę już sporo czasu. 58 commitów, 59 zmienionych plików, 12 000 dodanych linii kodu (bez obaw, większość to testy ;P, chociaż sam termostat też już ponad 3 000 linii ma).
I nawet PID-a nie mam :D

Z ciekawości: jak chcesz używać tego termostatu? W sensie do czego? Jakim urządzeniem grzewczym da się sterować wyjściem PWM?
Przy PID moją największą wątpliwością jest dobór parametrów Kp, Ki, Kd.
Na forum HA znalazłem jakiś post, gdzie ktoś przez 2 miesiące dobiera parametry Kp, Ki, Kd do jakiegoś pomieszczenia w swoim domu...
Pobijemy kolejne granice w wielkości binarki ? :P
https://gui-generic-builder.supla.io/
User avatar
klew
Posts: 9664
Joined: Thu Jun 27, 2019 12:16 pm
Location: Wrocław

Post

krycha88 wrote: Sun Mar 12, 2023 9:53 am
klew wrote: Sat Mar 11, 2023 9:16 pm A ja nad termostatem siedzę już sporo czasu. 58 commitów, 59 zmienionych plików, 12 000 dodanych linii kodu (bez obaw, większość to testy ;P, chociaż sam termostat też już ponad 3 000 linii ma).
I nawet PID-a nie mam :D

Z ciekawości: jak chcesz używać tego termostatu? W sensie do czego? Jakim urządzeniem grzewczym da się sterować wyjściem PWM?
Przy PID moją największą wątpliwością jest dobór parametrów Kp, Ki, Kd.
Na forum HA znalazłem jakiś post, gdzie ktoś przez 2 miesiące dobiera parametry Kp, Ki, Kd do jakiegoś pomieszczenia w swoim domu...
Pobijemy kolejne granice w wielkości binarki ? :P
Niestety nie da się dodawać nowych funkcji poprzez redukcję ilości linii kodu ;)
Kiedy będzie Supla Offline Party / SOP#2025 ?
krycha88
Posts: 5417
Joined: Fri Nov 16, 2018 7:25 am

Post

klew wrote: Sun Mar 12, 2023 11:09 am Niestety nie da się dodawać nowych funkcji poprzez redukcję ilości linii kodu ;)
Racja ale jeżeli nie skończony termostat ma 3tys linii kodu to ile będzie miał skończony? :P
https://gui-generic-builder.supla.io/
User avatar
klew
Posts: 9664
Joined: Thu Jun 27, 2019 12:16 pm
Location: Wrocław

Post

krycha88 wrote: Sun Mar 12, 2023 1:37 pm
klew wrote: Sun Mar 12, 2023 11:09 am Niestety nie da się dodawać nowych funkcji poprzez redukcję ilości linii kodu ;)
Racja ale jeżeli nie skończony termostat ma 3tys linii kodu to ile będzie miał skończony? :P
Mógłbym wszystko w jedną linię zrobić, ale @pzygmunt kiedyś ustalił, że jedna linia nie ma przekraczać 80 znaków ;)
Kiedy będzie Supla Offline Party / SOP#2025 ?
User avatar
QLQ
Posts: 2342
Joined: Sun Sep 03, 2017 9:13 am
Location: Koszalin

Post

NIe chce mi się to kompilować. Burzył się wcześniej o esp_random.h - bo w Src\supla\storage\key_value.cpp było esp_random. a ja mam ESPRandom.h - poprawiłem / wyedytowałem (ew podeślij proszę Twoje esp_random w *.rar)
Pouzupełniałem biblioteki ale teraz dalej się burzy:

Code: Select all

In file included from C:\Program Files\Arduino\portable\packages\esp32\hardware\esp32\1.0.6\libraries\WiFi\src/WiFi.h:31:0,
                 from C:\Program Files\Arduino\libraries\ESPRandom/ESPRandom.h:9,
                 from C:\Users\RK\Desktop\termostat_esp32\termostat_esp32.ino:47:
C:\Program Files\Arduino\libraries\SuplaDevice\src/supla/network/esp_wifi.h: In member function 'virtual void Supla::ESPWifi::setup()':
C:\Program Files\Arduino\portable\packages\esp32\hardware\esp32\1.0.6\libraries\WiFi\src/WiFiType.h:35:22: error: 'ARDUINO_EVENT_WIFI_STA_GOT_IP' is not a member of 'system_event_id_t'
 #define WiFiEvent_t  system_event_id_t
                      ^
C:\Program Files\Arduino\libraries\SuplaDevice\src/supla/network/esp_wifi.h:85:11: note: in expansion of macro 'WiFiEvent_t'
           WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_GOT_IP);
           ^
C:\Program Files\Arduino\portable\packages\esp32\hardware\esp32\1.0.6\libraries\WiFi\src/WiFiType.h:35:22: error: 'ARDUINO_EVENT_WIFI_STA_DISCONNECTED' is not a member of 'system_event_id_t'
 #define WiFiEvent_t  system_event_id_t
                      ^
C:\Program Files\Arduino\libraries\SuplaDevice\src/supla/network/esp_wifi.h:95:11: note: in expansion of macro 'WiFiEvent_t'
           WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED);
           ^
C:\Users\RK\Desktop\termostat_esp32\termostat_esp32.ino: In function 'void loop()':
termostat_esp32:320:35: error: 'analogWrite' was not declared in this scope
     analogWrite(mosfet_pin, output);           // Write the output to the output pin
                                   ^
Znaleziono wiele bibliotek w "WiFi.h"
Wykorzystane: C:\Program Files\Arduino\portable\packages\esp32\hardware\esp32\1.0.6\libraries\WiFi
Niewykorzystane: C:\Program Files\Arduino\libraries\WiFi
exit status 1
'analogWrite' was not declared in this scope
jak coś nie działa to włącz zasilanie.....
User avatar
Robert Błaszczak
Posts: 4323
Joined: Sat Dec 22, 2018 8:55 pm
Location: Zielona Góra

Post

U mnie na ESP32 C3 kompiluje się bez problemu. Oczywiście po doinstalowaniu niezbędnych bibliotek.
Nie robiłem żadnych zmian w Supla Device.

A tak na marginesie, to prośba do wszystkich publikujących swoje programy w Arduino IDE, aby przy definicji bibliotek dodawali w komentarzu dokładną nazwę biblioteki, autora i wersję. Dla przykładu użyta tu termopara MAX6675 ma w AIDE 5 różnych bibliotek. A tak byłoby prościej:

Code: Select all

 #include "max6675.h" //MAX6675 library, Adafruit, 1.1.0
Pozdrawiam
Robert Błaszczak


Moja prywatna strona: www.blaszczak.pl

Return to “Projekty użytkowników”