Projekt zrealizowany z użytkownikiem @Rafał

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:
//******************************************************************************************** //**********************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.
#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.
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.
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.
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).
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.
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.
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.
- Otwórz plik z kodem w Arduino IDE (lub innym edytorze).
-
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 }; -
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 }; -
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 }; - Zapisz zmiany i wgraj program na płytkę Arduino.
-
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.