Projekt zegara z termometrem, fotorezystorem i PIR

Projekt zrealizowany z użytkownikiem @Rafał

Rys. 1. Schemat podłączeń elementów sterownika wędzarni.

Link do symulacji:

https://wokwi.com/projects/402925149826920449

Opis projektu

Projekt ma na celu automatyczne sterowanie procesem wędzenia potraw w domowej wędzarni. Sterownik kontroluje temperaturę za pomocą czujnika DS18B20, reguluje grzanie przez przekaźnik oraz prędkość wentylatora PWM. Użytkownik może ręcznie wybierać różne etapy procesu (ustawienia temperatury, czasu i wentylatora) przyciskiem, a cały przebieg jest wizualizowany na wyświetlaczu LCD I²C.

Uwaga: Na schemacie zamiast wentylatora wykorzystano diodę LED (wentylator nie był dostępny w bibliotece elementów). W tym miejscu należy podłączyć sterownik lub tranzystor wentylatora — nie podłączaj wentylatora bezpośrednio do pinów Arduino, ponieważ grozi to ich uszkodzeniem!



Podłączenie do Arduino

Nazwa Pin Opis
czujnik_DS18B20 7 OneWire – pomiar temperatury
przekaźnik_grzanie 5 Wyjście do sterowania elementem grzewczym
wentylator_PWM 3 Wyjście PWM – regulacja prędkości wentylatora
przycisk_tryb 2 Wejście z przycisku wyboru trybu (INPUT_PULLUP, PCINT18)
LCD_SDA A4 I²C SDA – linia danych dla wyświetlacza LCD
LCD_SCL A5 I²C SCL – linia zegara dla wyświetlacza LCD


Kod programu:

Kod_000
//********************************************************************************************
//**********************PROGRAM STEROWNIKA WĘDZARNI*******************************************
//**************************WWW.ARDUINOWO.PL**************************************************
//********************************************************************************************

#include  // Dołączenie biblioteki LiquidCrystal_I2C dla wyświetlacza LCD
#include  // Dołączenie biblioteki OneWire dla komunikacji z czujnikiem temperatury
#include  // Dołączenie biblioteki DallasTemperature do obsługi czujnika temperatury

#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit)) // Makro do ustawiania bitów w rejestrach
volatile boolean flaga = 0; // Zmienna typu boolean do sterowania flagą

// Tworzenie obiektu dla wyświetlacza LCD z adresem I2C, liczba kolumn i wierszy
LiquidCrystal_I2C lcd(0x27, 20, 4);

volatile byte tryb = 0; // Zmienna do zliczania naciśnięć przycisku

const byte opoznienie = 25; // Opóźnienie odsprzęgania w milisekundach
unsigned long ostatni_czas1 = millis(); // Czas ostatniego zmienia stanu pierwszego przycisku
boolean poprzedni_stan1 = LOW; // Poprzedni stan pierwszego przycisku

int etap = 1; // Zmienna etapu
long czas; // Zmienna czasu
float temperatura_aktualna=0; // Zmienna aktualnej temperatury
byte wentylator_aktualny; // Zmienna aktualnej prędkości wentylatora

float temperatura_zadana; // Ustawienie żądanej temperatury
boolean grzanie=0; // Zmienna sterująca przekaźnikiem (0 - wyłączony, 1 - włączony)

//********************************************************************************************
//*******************************KONFIGURACJA*************************************************
//********************************************************************************************

// Deklaracja i inicjalizacja zmiennej globalnej - użycie pinu nr 7 dla czujnika temperatury
const byte nr_pinu = 7;

// Definicja numeru pinu, do którego podłączony jest przekaźnik
const int pin_przek = 5;

const int pwmPin = 3; // Definicja numeru pinu PWM

// Tablica z ustawieniami temperatury dla poszczególnych etapów w stopniach C
float temperatura[] = {40.00, 50.00, 60.00, 35.00}; 

// Tablica z czasami trwania poszczególnych etapów w sekundach
unsigned int sekundnik[] = {13, 14, 15, 16}; 

// Tablica z ustawieniami prędkości wentylatora dla poszczególnych etapów w procentach
byte wentylator[] = {10, 20, 100, 23}; 

float histereza = 2; // Ustawienie wartości histerezy dla temperatury


// Tworzenie obiektu dla protokołu OneWire
OneWire oneWire(nr_pinu);
// Tworzenie obiektu dla czujnika temperatury
DallasTemperature sensor(&oneWire);

//********************************************************************************************
//*******************************FUNKCJA SETUP************************************************
//********************************************************************************************

void setup()
{
  //Ustawienie wartości początkowych
  czas = sekundnik[0];
  wentylator_aktualny = wentylator[0];
  temperatura_zadana = temperatura[0];

  // TIMER 1 for interrupt frequency 1 Hz:
  cli(); // stop interrupts
  TCCR1A = 0; // set entire TCCR1A register to 0
  TCCR1B = 0; // same for TCCR1B
  TCNT1  = 0; // initialize counter value to 0
  // set compare match register for 1 Hz increments
  OCR1A = 62499; // = 16000000 / (256 * 1) - 1 (must be <65536)
  // turn on CTC mode
  TCCR1B |= (1 << WGM12);
  // Set CS12, CS11 and CS10 bits for 256 prescaler
  TCCR1B |= (1 << CS12) | (0 << CS11) | (0 << CS10);
  // enable timer compare interrupt
  TIMSK1 |= (1 << OCIE1A);
  sei(); // allow interrupts

  lcd.init(); // Inicjalizacja wyświetlacza LCD
  lcd.backlight(); // Włączenie podświetlenia wyświetlacza
  sensor.begin(); // Inicjalizacja czujnika temperatury
  Serial.begin(9600); // Rozpoczęcie komunikacji szeregowej z prędkością 9600 bps

  pinMode(2, INPUT_PULLUP); // Ustawienie pinu 2 jako wejście z podciągnięciem do VCC
  sbi(PCICR, PCIE2); // Włączenie przerwań pin-change dla grupy PCINT2
  sbi(PCMSK2, PCINT18); // Włączenie przerwania dla pinu PCINT18 (pin 2)

  pinMode(pin_przek, OUTPUT); // Ustawienie pinu przekaźnika jako wyjście
  digitalWrite(pin_przek, grzanie); // Ustawienie początkowego stanu przekaźnika

  pinMode(pwmPin, OUTPUT); // Ustawienie pinu PWM jako wyjście
}

//********************************************************************************************
//*******************************FUNKCJA LOOP*************************************************
//********************************************************************************************

void loop()
{
  sensor.requestTemperatures(); // Żądanie odczytu temperatury z czujnika
  temperatura_aktualna = sensor.getTempCByIndex(0); // Pobranie odczytu temperatury

  // Wyświetlanie trybu pracy na wyświetlaczu LCD
  if (tryb == 0) {
    wyswietlanie_trybu(tryb, temperatura_aktualna, czas, wentylator_aktualny);
  } else {
    wyswietlanie_trybu(tryb, temperatura[tryb - 1], sekundnik[tryb - 1], wentylator[tryb - 1]);
  }

  // Implementacja logiki histerezy dla grzania
  if (temperatura_aktualna < temperatura_zadana - histereza && grzanie == 0) {
    grzanie = 1; // Włączenie grzania, gdy temperatura jest znacznie poniżej żądanej
  } else if (temperatura_aktualna > temperatura_zadana + histereza && grzanie == 1) {
    grzanie = 0; // Wyłączenie grzania, gdy temperatura przekracza żądaną wartość
  }
  digitalWrite(pin_przek, grzanie); // Sterowanie przekaźnikiem

  // Sterowanie prędkością wentylatora za pomocą PWM
  analogWrite(pwmPin, (wentylator_aktualny * 255) / 100);
}

//********************************************************************************************
//*******************************PRZERWANIE PIN***********************************************
//********************************************************************************************

ISR(PCINT2_vect)
{
  boolean stan1 = !bitRead(PIND, 2); // Odczytanie bieżącego stanu trzeciego przycisku
  if (stan1 != poprzedni_stan1) // Sprawdzenie, czy stan trzeciego przycisku się zmienił
  { // Sprawdzenie, czy przycisk jest wciśnięty i czy upłynęło odpowiednio
    // dużo czasu od ostatniego wciśnięcia
    if (stan1 == LOW && millis() - ostatni_czas1 > opoznienie)
    {
      tryb++; // Inkrementacja zmiennej "tryb"
      if (tryb > 4) {
        tryb = 0;  // Reset wartości "tryb" po przekroczeniu 2
      }
      // lcd.clear();
    }
    // Aktualizacja czasu ostatniego wciśnięcia, gdy przycisk jest zwolniony
    if (stan1 == HIGH) ostatni_czas1 = millis();
  }
  poprzedni_stan1 = stan1; // Aktualizacja poprzedniego stanu trzeciego przycisku

}

//********************************************************************************************
//*******************************PRZERWANIE ZEGARA********************************************
//********************************************************************************************

ISR(TIMER1_COMPA_vect)
{
  czas--; // Dekrementacja zmiennej czasu
  if (czas < 0) { // Sprawdzenie, czy czas jest mniejszy niż 0
    if (etap != 5) { // Sprawdzenie, czy etap nie jest równy 5
      etap++; // Inkrementacja etapu
    }
    if (etap == 2) { // Sprawdzenie, czy etap jest równy 2
      czas = sekundnik[1]; // Ustawienie czasu na wartość z tablicy sekundnik dla etapu 2
      wentylator_aktualny = wentylator[1]; // Ustawienie prędkości wentylatora dla etapu 2
      temperatura_zadana = temperatura[1]; // Ustawienie temperatury zadanej dla etapu 2
    }
    if (etap == 3) { // Sprawdzenie, czy etap jest równy 3
      czas = sekundnik[2]; // Ustawienie czasu na wartość z tablicy sekundnik dla etapu 3
      wentylator_aktualny = wentylator[2]; // Ustawienie prędkości wentylatora dla etapu 3
      temperatura_zadana = temperatura[2]; // Ustawienie temperatury zadanej dla etapu 3
    }
    if (etap == 4) { // Sprawdzenie, czy etap jest równy 4
      czas = sekundnik[3]; // Ustawienie czasu na wartość z tablicy sekundnik dla etapu 4
      wentylator_aktualny = wentylator[3]; // Ustawienie prędkości wentylatora dla etapu 4
      temperatura_zadana = temperatura[3]; // Ustawienie temperatury zadanej dla etapu 4
    }
    if (etap == 5) { // Sprawdzenie, czy etap jest równy 5
      czas = 0; // Ustawienie czasu na 0
      wentylator_aktualny = 0; // Wyłączenie wentylatora
      temperatura_zadana = 0; // Ustawienie temperatury zadanej na 0
    }
  }
}

//********************************************************************************************
//*******************************FUNKCJE******************************************************
//********************************************************************************************

void wyswietlanie_trybu(byte tryb_f, float temp, unsigned long sekundy, byte went)
{
  lcd.setCursor(0, 0); // Ustawienie kursora na początku pierwszego wiersza
  if (tryb_f != 0) { // Sprawdzenie, czy tryb jest różny od 0
    lcd.print("   Ust. Etap_"); // Wyświetlenie tekstu "Ust. Etap_"
    lcd.print(tryb); // Wyświetlenie numeru aktualnego trybu
    lcd.print("  "); // Wyświetlenie dwóch spacji
  } else { // Jeśli tryb jest równy 0
    lcd.print("Proces: "); // Wyświetlenie tekstu "Proces: "
    if (etap != 5) { // Sprawdzenie, czy etap jest różny od 5
      lcd.print("Etap_"); // Wyświetlenie tekstu "Etap_"
      lcd.print(etap); // Wyświetlenie numeru aktualnego etapu
    } else { // Jeśli etap jest równy 5
      lcd.print("KONIEC"); // Wyświetlenie tekstu "KONIEC"
    }
  }

  lcd.setCursor(2, 1); // Ustawienie kursora
  lcd.print("Temp: "); // Wyświetlenie tekstu "Temp:"
  lcd.print(temp + 0.05, 1); // Wyświetlenie temperatury z dokładnością do 1 miejsca po przecinku
  lcd.write(0b11011111); // Symbol stopnia
  lcd.print("C"); // Symbol stopnia Celsjusza
  lcd.print("   "); // Czyszczenie niepotrzebnych znaków

  lcd.setCursor(2, 2); // Ustawienie kursora
  lcd.print("Czas: "); // Wyświetlenie tekstu "Czas:"
  lcd.print(sekundy); // Wyświetlenie pozostałego czasu
  lcd.print("s"); // Wyświetlenie symbolu sekund
  lcd.print("   "); // Czyszczenie niepotrzebnych znaków

  lcd.setCursor(2, 3); // Ustawienie kursora
  lcd.print("Went: "); // Wyświetlenie tekstu "Went:"
  lcd.print(went); // Wyświetlenie aktualnej prędkości wentylatora
  lcd.print("%"); // Wyświetlenie symbolu procenta
  lcd.print("  "); // Czyszczenie niepotrzebnych znaków

}





Wytłumaczenie kodu programu:

1. Nagłówki bibliotek i makra

W tej sekcji dołączamy niezbędne biblioteki oraz definiujemy makro do ustawiania bitów w rejestrach AVR.

Kod_002
#include    // Sterowanie LCD przez I²C
#include              // Protokół 1-Wire dla DS18B20
#include    // Obsługa DS18B20

#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit)) // Ustawianie bitów
volatile boolean flaga = 0;                       // Flaga do przerwań
    

2. Zmienne globalne i obiekty sprzętowe

Tu deklarujemy wszystkie zmienne sterujące trybem pracy, tablice z parametrami etapów oraz obiekty dla wyświetlacza i sensora.

Kod_003
volatile byte tryb = 0;               // 0=auto, 1–4=ręczne etapy
const byte opoznienie = 25;           // Debounce przycisku (ms)
unsigned long ostatni_czas1 = millis();
boolean poprzedni_stan1 = LOW;

int etap = 1;                         // Numer bieżącego etapu
long czas;                            // Licznik sekund
float temperatura_aktualna = 0;       // Odczyt z DS18B20
float temperatura_zadana;             // Cel histerezy
boolean grzanie = 0;                  // Stan przekaźnika
byte wentylator_aktualny;             // PWM wentylatora

const byte nr_pinu = 7;               // Pin DS18B20
const int pin_przek = 5;              // Przekaźnik grzałki
const int pwmPin = 3;                 // Wyjście PWM

float temperatura[]   = {40.0,50.0,60.0,35.0};
unsigned int sekundnik[] = {13,14,15,16};
byte wentylator[]    = {10,20,100,23};
float histereza = 2.0;                // Bufor ±2°C

LiquidCrystal_I2C lcd(0x27, 20, 4);   // LCD 20×4, adres 0x27
OneWire oneWire(nr_pinu);
DallasTemperature sensor(&oneWire);
    

3. Funkcja setup()

Konfiguracja początkowa: Timer1 co sekundę, LCD, sensor, przycisk z przerwaniami oraz wyjścia.

Kod_004
void setup() {
  // 1) Pierwsze wartości
  czas                = sekundnik[0];
  wentylator_aktualny = wentylator[0];
  temperatura_zadana  = temperatura[0];

  // 2) Timer1 – CTC 1 Hz
  cli();
  TCCR1A = 0; TCCR1B = 0; TCNT1 = 0;
  OCR1A = 62499;                  // 16MHz/256/1Hz – 1
  TCCR1B |= (1 << WGM12) | (1 << CS12);
  TIMSK1 |= (1 << OCIE1A);
  sei();

  // 3) LCD i sensor
  lcd.init(); lcd.backlight();
  sensor.begin();
  Serial.begin(9600);

  // 4) Przycisk PCINT na pinie 2
  pinMode(2, INPUT_PULLUP);
  sbi(PCICR, PCIE2);
  sbi(PCMSK2, PCINT18);

  // 5) Wyjścia
  pinMode(pin_przek, OUTPUT);
  digitalWrite(pin_przek, grzanie);
  pinMode(pwmPin, OUTPUT);
}
    

4. Główna pętla loop()

Odczyt temperatury, wyświetlanie trybu, algorytm histerezy i sterowanie wentylatorem przez PWM.

Kod_005
void loop() {
  // A) Odczyt temperatury
  sensor.requestTemperatures();
  temperatura_aktualna = sensor.getTempCByIndex(0);

  // B) Wyświetlanie (auto vs ręczne)
  if (tryb == 0)
    wyswietlanie_trybu(0, temperatura_aktualna, czas, wentylator_aktualny);
  else
    wyswietlanie_trybu(tryb,
      temperatura[tryb-1],
      sekundnik[tryb-1],
      wentylator[tryb-1]);

  // C) Histereza grzałki
  if (temperatura_aktualna < temperatura_zadana - histereza && !grzanie)
    grzanie = 1;
  else if (temperatura_aktualna > temperatura_zadana + histereza && grzanie)
    grzanie = 0;
  digitalWrite(pin_przek, grzanie);

  // D) PWM wentylatora
  analogWrite(pwmPin, (wentylator_aktualny * 255) / 100);
}
    

5. Przerwanie Pin-Change (PCINT2_vect)

Obsługa przycisku na pinie 2 z filtrem drgań styków (debounce).

Kod_006
ISR(PCINT2_vect) {
  boolean stan1 = !bitRead(PIND, 2);
  if (stan1 != poprzedni_stan1) {
    if (stan1 == LOW && millis() - ostatni_czas1 > opoznienie) {
      tryb++;
      if (tryb > 4) tryb = 0;
    }
    if (stan1 == HIGH) ostatni_czas1 = millis();
  }
  poprzedni_stan1 = stan1;
}
    

6. Przerwanie Timer1 (TIMER1_COMPA_vect)

Wywoływane co sekundę – dekrementacja licznika i przejście do kolejnych etapów.

Kod_007
ISR(TIMER1_COMPA_vect) {
  czas--;
  if (czas < 0) {
    etap++;
    if (etap <= 4) {
      int i = etap - 1;
      czas                = sekundnik[i];
      wentylator_aktualny = wentylator[i];
      temperatura_zadana  = temperatura[i];
    } else { // etap 5 = KONIEC
      czas                = 0;
      wentylator_aktualny = 0;
      temperatura_zadana  = 0;
    }
  }
}
    

7. Funkcja wyświetlania wyswietlanie_trybu()

Rysuje na LCD aktualny proces lub ręczne ustawienia etapu.

Kod_008
void wyswietlanie_trybu(byte tryb_f, float temp, unsigned long sekundy, byte went) {
  lcd.setCursor(0, 0);
  if (tryb_f) {
    lcd.print("   Ust. Etap_"); lcd.print(tryb_f); lcd.print("  ");
  } else {
    lcd.print("Proces: ");
    if (etap < 5) {
      lcd.print("Etap_"); lcd.print(etap);
    } else {
      lcd.print("KONIEC");
    }
  }

  lcd.setCursor(2, 1);
  lcd.print("Temp: ");
  lcd.print(temp + 0.05, 1); lcd.write(0b11011111); lcd.print("C   ");

  lcd.setCursor(2, 2);
  lcd.print("Czas: "); lcd.print(sekundy); lcd.print("s   ");

  lcd.setCursor(2, 3);
  lcd.print("Went: "); lcd.print(went); lcd.print("%  ");
}
    

Instrukcja zmiany parametrów wędzenia

Poniżej instrukcja krok po kroku wraz z przykładami, jak zmienić ustawienia etapów w oryginalnym kodzie sterownika.

  1. Otwórz plik z kodem w Arduino IDE (lub innym edytorze).
  2. Zlokalizuj definicje tablic zawierających parametry etapów:
    Kod_009 – oryginał
    float temperatura[]     = {40.00, 50.00, 60.00, 35.00};
    unsigned int sekundnik[] = { 13,    14,    15,    16   };
    byte wentylator[]        = {10,    20,    100,   23  };
            
  3. Przykład 1: zmiana parametrów etapu 3
    Chcemy, aby etap 3 pracował w 55 °C przez 600 s z wentylatorem 40 %:
    Kod_010 – przykład dla etapu 3
    float temperatura[]     = {40.00, 50.00, 55.00, 35.00};   
    unsigned int sekundnik[] = { 13,    14,    600,    16   };   
    byte wentylator[]        = {10,    20,    40,    23   };   
            
  4. Przykład 2: zmiana parametrów etapów 1 i 2
    Chcemy, aby etap 1 trwał 20 s przy 45 °C i wentylator 15 %, a etap 2 – 18 s przy 55 °C i wentylator 30 %:
    Kod_011 – przykład dla etapów 1 i 2
    float temperatura[]     = {45.00, 55.00, 60.00, 35.00};   
    unsigned int sekundnik[] = { 20,    18,    15,    16   };   
    byte wentylator[]        = {15,    30,    100,   23   };   
            
  5. Zapisz zmiany i wgraj program na płytkę Arduino.
  6. Weryfikacja w trakcie pracy:
    • Uruchom sterownik.
    • Przejdź przyciskiem „Tryb” do „Ust. Etap_X” odpowiadającego zmienionemu etapowi.
    • Sprawdź na LCD nowe wartości temperatury, czasu i wentylatora.
    • Wciśnij „Tryb” ponownie, aby wrócić do automatycznego cyklu („Proces: Etap_X”).

W ten sposób możesz precyzyjnie dostosować dowolny etap wędzenia, zmieniając tylko odpowiednie elementy w tablicach bez ingerencji w inne fragmenty kodu.



Dalsze kroki i możliwe usprawnienia

Poniżej kilka pomysłów, jak rozbudować i udoskonalić sterownik wędzarni:

  • Dodanie przycisków – umożliwi nawigację po menu i ręczne zmienianie parametrów bez potrzeby podłączania Arduino do komputera.
  • Dodanie pamięci EEPROM – pozwoli zachować wprowadzone ustawienia (temperatury, czasy etapów, prędkość wentylatora) nawet po wyłączeniu zasilania.
  • Regulacja PID zamiast prostej histerezy – pozwoli na płynniejsze i bardziej precyzyjne utrzymanie zadanej temperatury z mniejszymi odchyleniami.
  • Logowanie danych na kartę SD lub do wewnętrznej pamięci – archiwizacja przebiegu temperatury, czasu i ustawień wentylatora do późniejszej analizy.
  • Rozbudowane menu na LCD – stworzenie hierarchicznego interfejsu, w którym użytkownik może definiować i zapisywać własne profile wędzenia.
  • Łączność bezprzewodowa (Bluetooth/Wi-Fi) – zdalne monitorowanie i sterowanie z poziomu smartfona lub komputera.
  • Alarmy i powiadomienia – wizualne i dźwiękowe sygnały ostrzegawcze lub wysyłanie powiadomień SMS/e-mail po zakończeniu cyklu lub w razie krytycznego odchylenia temperatury.
  • Kalibracja czujnika – możliwość ręcznego lub automatycznego dostrajania czujnika DS18B20 w celu zwiększenia dokładności pomiarów.
  • Funkcje bezpieczeństwa – automatyczne wyłączenie grzałki w razie awarii wentylatora, zwarcia lub braku detekcji temperatury.
  • Dynamiczne profile etapów – możliwość definiowania dowolnej liczby etapów oraz zapisywania ich do późniejszego użycia.

Wprowadzenie tych usprawnień pozwoli na bardziej precyzyjną i komfortową obsługę wędzarni oraz na zbieranie wartościowych danych do dalszej optymalizacji procesu.