- konstrukcja oparta na mikrokontrolerze ATmega4808
- pomiar temperatury otoczenia: czujnik DS18B20
- dokładność pomiaru temperatury: ±0,5°C
- sterowanie: enkoder obrotowy
- wyświetlacz: LCD TFT 128×160 px
- wyjścia przekaźnikowe: 2×DPDT 250 V/8 A
- wbudowany zegar czasu rzeczywistego (DS3231) z podtrzymaniem (CR2032)
Uruchomienie i działanie układu
Uruchomienie układu zaczynamy od płytki zasilacza. Po podłączeniu sieci 230 V sprawdzamy obecność napięcia wyjściowego +5 V (wyprowadzenia 1 i 2 złącza P2) względem masy (wyprowadzenia 3 i 4 złącza P2). Jeżeli napięcie to jest prawidłowe, łączymy obie płytki złączami IDC. Napięcie na wyjściu stabilizatora U2 – umieszczonego na płytce sterownika – powinno wynosić +3,3 V. W następnym kroku do złącza J2 wpinamy programator umożliwiający zaprogramowanie mikrokontrolera Atmega4808 za pomocą jednej linii UPDI. Ja użyłem programatora MPLAB PICkit4 współpracującego ze środowiskiem projektowym MPLABX IDE. Po podłączeniu zewnętrznego czujnika DS18B20 do złącza P4 układ jest gotowy do weryfikacji działania oraz rozpoczęcia programowania ustawień.
Po włączeniu zasilania pojawia się ekran główny podzielony na dwie strefy. W pierwszej z nich wyświetla się temperatura mierzona przez czujnik DS18B20. Wynik jest aktualizowany co 1 sekundę (co wynika z czasu potrzebnego na dokonanie pomiaru). W drugiej strefie wyświetlane są natomiast: rzeczywisty czas w godzinach, minutach i sekundach, data, a także bieżący program termostatu.
Ten ostatni działa w dwóch przedziałach czasowych, umownie nazwanych: „dzień” i „noc”. Do każdego z tych przedziałów przypisana została wartość temperatury. Z założenia w czasie „dzień” ustawiana jest wyższa temperatura, właściwa dla aktywności domowników (może to być na przykład +21°C). W czasie „noc” ustawiamy temperaturę niższą, na przykład +18°C. Przekłada się to niższe zużycie gazu, a dodatkowo w niższej temperaturze lepiej się śpi.
Przedział czasowy „dzień” zaczyna się o godzinie określonej przez Czas1 i kończy się o godzinie określonej przez Czas2. W tym przedziale wykonywany jest program o nazwie „Program #1” z przypisaną temperaturą regulacji Temp1.
Przedział czasowy „noc” zaczyna się o godzinie Czas2, a kończy o godzinie Czas1. W tym przedziale z kolei wykonywany jest czas regulacji „Program #2” z przypisaną Temp2. Wszystkie parametry programu: Czas1, Czas2, Temp1 i Temp2 są programowane. Jedyne ograniczenie bieżącej wersji oprogramowania stanowi warunek, że Czas1 musi być mniejszy od Czas2. Na przykład Czas1=07:00, Czas2=22:15, ale Czas2 nie powinien mieć na przykład wartości 0:30. Wtedy program regulacji będzie działał, ale po zaniku zasilania może błędnie określić, w którym przedziale czasowym się znajduje, aż do momentu osiągnięcia jednego z czasów: Czas1, lub Czas2. Na rysunku 5 pokazano zasadę czasowego podziału doby na dwa programy Program #1 i Program #2. W czasie wykonywania programu #1 termostat utrzymuje temperaturę Temp1, a w czasie trwania programu #2 – temperaturę Temp2.
Temperatura jest regulowana przy zastosowaniu pętli histerezy. Załóżmy, że temperatura otoczenia okazuje się wyższa od temperatury Temp1. Termostat wyłącza wówczas grzanie pieca. Temperatura otoczenia spada, osiąga wartość Temp1. Termostat nadal nie włączy pieca – tak długo, aż temperatura otoczenia spadnie do wartości Temp1 – Hist, gdzie Hist jest wartością programowaną w zakresie od 0,1°C do 2°C z krokiem 0,1°C. Jeżeli temperatura otoczenia jest niższa od Temp1 – Hist, to termostat włącza grzanie. Temperatura w pomieszczeniu rośnie, aż osiągnie wartość wyższą od Temp1. Wtedy termostat wyłącza grzanie i cykl się powtarza. Takie rozwiązanie zapobiega częstym cyklom załącz-wyłącz, kiedy temperatura jest bliska wartości Temp1. Temperatura w pomieszczeniu będzie się w przybliżeniu wahać od wartości Temp1-Hist do Temp1. Wartość histerezy należy dobrać w zależności od warunków w ogrzewanych pomieszczeniach. Zbyt duża powoduje duże wahania temperatury, zbyt mała natomiast – częste cykle załącz-wyłącz. Prototyp termostatu pracował z histerezą 0,8°C. Histereza pozostaje wspólna dla obu programów.
Menu programowania jest dostępne po przyciśnięciu osi impulsatora. Można w nim ustawić czas, datę i termostat – fotografia 5.
Wybór jednej z funkcji w menu ustawień (czas, data, termostat) realizowany jest przez obrót gałki. Wybrana funkcja wyświetlana się na biało. Przyciśnięcie osi impulsatora sprawia, że rozpoczyna się wykonywanie wybranej funkcji.
Najbardziej rozbudowana jest funkcja ustawień termostatu. Można w niej zaprogramować:
- histerezę w zakresie od 0,1°C do 2,0°C z krokiem 0,1°C,
- temperaturę Temp1 w zakresie od 0°C do 31°C z krokiem 0,1°C,
- temperaturę Temp2 w zakresie od 0°C do 31°C z krokiem 0,1°C,
- czas 1 (godziny, minuty),
- czas 2 (godziny, minuty).
Nastawy zmienia się przez obrót osi impulsatora, a zatwierdza jej przyciśnięciem. Ustawiana wartość wyświetlana się na biało, a po zatwierdzeniu zmienia kolor na niebieski – fotografia 6.
Po wejściu w menu ustawień termostatu użytkownik nie ma możliwości wybrania tylko jednej z nastaw. Musi „przeklikać” wszystkie pozycje. Sterownik pamięta każdą nastawę i jeżeli jakiejś nie chcemy modyfikować, powinniśmy sekwencyjnie przyciskać oś impulsatora, żeby przejść do kolejnej nastawy bez wprowadzania zmian. To uproszczenie nie wpływa znacząco na komfort użytkowania, ponieważ programowania termostatu nie wykonuje się często. Wszystkie nastawy są zachowywane w pamięci nieulotnej mikrokontrolera i odtwarzane po każdym zaniku napięcia zasilania. Wraz z podtrzymaniem bateryjnym zegara RTC zapewnia to w miarę niezakłócony cykl sterowania przy chwilowych zanikach napięcia sieciowego. Sterownik – po ponownym włączeniu zasilania – na podstawie ustawień Czas1 i Czas2 oraz bieżącego wskazania zegara oblicza czy aktywny jest Program #1, czy Program #2 i zaczyna regulować nastawione temperatury właściwe aktywnemu programowi.
Ustawienia czasu oraz daty nie wymagają szerszego komentarza. W trybie ustawiania czasu programujemy kolejno: godziny i minuty. Po ustawieniu minut oraz przyciśnięciu osi impulsatora, zerowany jest licznik sekund – i wartości godzin, minut i sekund są wpisywane do rejestrów układu DS3231, a zegar zaczyna odliczać nowy czas. W funkcji ustawienia daty programowane są: dzień miesiąca, miesiąc, a także rok.
Jak już wspomniałem, oprogramowanie zostało napisane w środowisku MPLAB firmy Microchip. Oprócz oczywistych elementów – takich jak środowisko IDE i kompilator C – istotnym wsparciem dla programisty jest wtyczka konfiguracyjna MCC. Oprócz konfiguracji urządzeń peryferyjnych, dostarcza kody źródłowe procedur do ich obsługi, co znacznie ułatwia i skraca pisanie programu. Trzeba przy tym pamiętać, że większość procedur komunikacji sprawdza warunek zakończenia transmisji w nieskończonych pętlach (procedury blokujące). Warto je nieco zmodyfikować, tak by – po określonym czasie oczekiwania w pętli na spełnienie warunku – tę pętlę przerwać. Jeżeli tego nie zrobimy ryzykujemy, że program prędzej czy później trwale się zawiesi. Można również pozostawić go tak jak jest – i skonfigurować moduł watchdoga.
Mikrokontroler jest taktowany wewnętrznym oscylatorem o częstotliwości 20 MHz. Przy zasilaniu +3,3 V nominalna częstotliwość taktowania powinna być ograniczona do 10 MHz, ale w praktyce – w temperaturach pokojowych – mikrokontroler pracuje poprawnie przy 20 MHz (okazuje się to istotne dla w miarę płynnej pracy wyświetlacza). Układ taktowania jest konfigurowany w oknie Clock Control wtyczki konfiguratora MCC – rysunek 6.
Mikrokontroler ma wbudowane 256 bajtów nieulotnej pamięci EEPROM przeznaczonej do przechowywania danych systemowych. Dostęp do pamięci nieulotnej – w tym EEPROM (ale też i Flash) – zapewnia specjalny moduł NVMCTRL (Nonvolatile Memory Controller). Po zaznaczeniu Place
Flash Functions in Separate w oknie NVMCTRL (rysunek 7), wtyczka MCC wygeneruje funkcje inicjalizujące moduł NVMCTRL oraz funkcje zapisu i odczytu bajtu z pamięci EEPROM (funkcje te używane są do zapisywania i odczytywania ustawień termostatu – listing 1).
zapisanie bajtu do pamięci EEPROM
**************************************************/
uint8_t HMI_WriteEEPROM(uint8_t address,uint8_t data)
{
uint8_t status;
status = FLASH_WriteEepromByte(address, data);
return status;
}
/**************************************************
zapisanie bajtu do pamięci EEPROM
-procedura wygenerowana przez MMC
**************************************************/
/**
* \brief Write a byte to eeprom
*
* \param[in] eeprom_adr The byte-address in eeprom to write to
* \param[in] data The byte to write
*
* \return Status of write operation
*/
nvmctrl_status_t FLASH_WriteEepromByte(eeprom_adr_t eeprom_adr, uint8_t data)
{
/* Wait for completion of previous write */
while (NVMCTRL.STATUS & NVMCTRL_EEBUSY_bm)
;
/* Clear page buffer */
ccp_write_spm((void *)&NVMCTRL.CTRLA, NVMCTRL_CMD_PAGEBUFCLR_gc);
/* Write byte to page buffer */
*(uint8_t *)(EEPROM_START + eeprom_adr) = data;
/* Erase byte and program it with desired value */
ccp_write_spm((void *)&NVMCTRL.CTRLA, NVMCTRL_CMD_PAGEERASEWRITE_gc);
if (NVMCTRL.STATUS & NVMCTRL_WRERROR_bm)
{
return NVM_ERROR;
}
else
{
return NVM_OK;
}
}
Listing 1. Zapisanie danej do pamięci EEPROM
Zegar RTC DS3231 komunikuje się z mikrokontrolerem za pomocą magistrali I²C. Transmisję obsługuje sprzętowy moduł TWI konfigurowany w MCC – rysunek 8. Pracuje on jako master I²C z zegarem o częstotliwości 100 kHz.
W przypadku interfejsu TWI, MCC dostarcza plik twi0_master_example.c z działającymi funkcjami transferu danych po magistrali I²C. Na listingu 2 pokazano funkcję przesyłania danych do zegara RTC DS3231, korzystającą z „przykładowej” funkcji I2C0_example_write1ByteRegister.
zapisz danej do rejestru o adresie reg
*************************************************/
void RTC_SendData(uint8_t reg, uint8_t data)
{
I2C0_example_write1ByteRegister(RTC_I2C_ADD, reg, data);
}
void I2C0_example_write1ByteRegister(twi0_address_t address, uint8_t reg, uint8_t data)
{
while(!I2C0_Open(address)); //sit here until we get the bus.
I2C0_SetDataCompleteCallback(wr1RegCompleteHandler_example,&data);
I2C0_SetBuffer(®,1);
I2C0_SetAddressNackCallback(I2C0_SetRestartWriteCallback,NULL); //NACK polling?
I2C0_MasterWrite();
while(I2C0_BUSY == I2C0_Close()); //sit here until finished.
}
Listing 2. Funkcja przesłania danej do zegara RTC
Mikrokontroler przesyła dane do sterownika wyświetlacza poprzez magistralę SPI. Wyświetlacz ma rozmiar 160×128 pikseli – żeby określić jasność i kolor każdego z pikseli, mikrokontroler musi przesłać do sterownika 2 bajty. Zapisanie całego wyświetlacza wymaga zatem transferu 128×160×2=40960 bajtów.
Ponadto trzeba często wysłać sekwencje komend sterujących, na przykład do ustalenia adresu piksela. Wszystko to sprawia, że transfer danych powinien być szybki, co z kolei wymaga wydajnego mikrokontrolera oraz – oczywiście – szybkiego interfejsu SPI. W naszym przypadku zegar taktujący przesyłaniem bitów ma częstotliwość 5 MHz przy taktowaniu mikrokontrolera częstotliwością 20 MHz. Konfiguracja interfejsu SPI została pokazana na rysunku 9.
Procedury przesłania danych i komend wymagają również ustawienia linii CD wyboru interfejsu SPI oraz linii wyboru rodzaju przesyłanych danych DC (data/command). Na listingu 3 pokazano procedurę przesyłającą 2 bajty dotyczące jednego piksela do pamięci obrazu wyświetlacza.
{
uint32_t i;
PORTC.OUT |= (1 << DC); //zapis danej
PORTC.OUT &= ~(1 << CS); //SPI aktywny
for(i = 0; i < DataLen; i++){
SPI0_ExchangeByte( (uint8_t)(Data >> 8) );
SPI0_ExchangeByte( (uint8_t)(Data & 0XFF) );
}
PORTC.OUT |= (1 << CS); //SPI nie aktywny
}
Listing 3. Przesłanie dwu bajtów do pamięci wyświetlacza
MMC wygenerowała procedurę wysyłania jednego bajtu przez interfejs SPI – listing 4.
{
SPI0.DATA = data;
while (!(SPI0.INTFLAGS & SPI_RXCIF_bm));
return SPI0.DATA;
}
Listing 4. Funkcja wysyłająca jeden bajt przez SPI
Przy dużej ilości przesyłanych danych zdarza się, że procedura oczekiwania na wysłanie bajtów ulega zawieszeniu, choć teoretycznie przy zastosowaniu SPI nie powinna. Drobna modyfikacja powoduje, że problem ten praktycznie znika. W pętli oczekiwania inkrementowany jest licznik. Jeżeli nie zostanie sprzętowo ustawiona flaga SPI_RXCIF oznaczająca, że bajt został wysłany (i odebrany), to wykonywana jest ponowna inicjalizacja modułu SPI SPI0_Initialize () i pętla kończy działanie – listing 5.
uint8_t SPI0_ExchangeByte(uint8_t data)
{
uint16_t frame = 0;
SPI0.DATA = data;
while (1)
{
if!(SPI0.INTFLAGS & SPI_RXCIF_bm)
return SPI0.DATA; //dana prawidłowo wysłana
++ frame;
if (frame >= SPI_FRAME)
{
SPI SPI0_Initialize ();
return(0);
}
}
return SPI0.DATA;
}
Listing 5. Zmodyfikowana procedura wysyłania bajtu
Ostatni moduł sprzętowy używany przez program to timer TCB (rysunek 10). Został on skonfigurowany tak, by zgłaszać cykliczne przerwania co jedną milisekundę. W procedurze obsługi tego przerwania jest realizowana obsługa impulsatora.
Główne procedury regulacji nie są zbyt skomplikowane. Po każdym pomiarze temperatury – wykonywanym co sekundę – wywołaniu ulega procedura Termostat, której argumentem jest temperatura w formacie zmiennoprzecinkowym – listing 6.
{
double hist, temp1, temp2;
HMI_GetTermostat(); //pobierz dane ustawień
//termostatu
hist = (double)termo.hist/100;
temp1 = (double)termo.temp1/10;
temp2 = (double)termo.temp2/10;
Termostat_Check_Time (); //sprawdź czy nie zmienił
//się program
Termostat_Check_Temperature (temperature);
}
Listing 6. Funkcja termostatu
Funkcja HMI_GetTermostat() odczytuje z pamięci EEPROM wszystkie ustawienia termostatu: histerezę, temperatury programów temp1 i temp2 oraz oba czasy. Następnie wywoływana jest funkcja Termostat_Check_Time () – listing 7. Ma ona za zadanie porównać bieżący czas (odczytany z rejestrów zegara RTC) z nastawami Czas1 i Czas2, a na podstawie wyniku tego porównania – określić, który program czasowy ma się wykonywać.
//przy zmianie progów w czasowych
void Termostat_Check_Time (void)
{
uint8_t min, hour;
RTC_GetTime (); //pobierz czas z rejestrów DS3231
hour = HMI_ConvHexDec (time.hour);
min = HMI_ConvHexDec (time.min);
if (hour == termo.t1_hour && min == termo.t1_min)
{
termo.program = PR1;
return ;
}
else
if (hour >termo.t1_hour && hour < termo.t2_hour)
{
termo.program = PR1;
return ;
}
else
if (hour == termo.t2_hour && min < termo.t2_min)
{
termo.program = PR1;
return ;
termo.program = PR2
}
Listing 7. Identyfikacja aktywnego programu czasowego
Na podstawie wartości zmiennej termo.program funkcja Termostat_Check_Temperature (temperature) identyfikuje, która z temperatur ma być użyta w procesie regulacji. Procedura ta została pokazana na listingu 8. Opisana funkcja załącza lub wyłącza przekaźnik sterujący piecem, oraz wyświetla na ekranie głównym informację o aktywnym programie, a także wskazuje, czy grzanie jest włączone, czy wyłączone.
{
if(termo.program == PR1)
{
if(temperature < (temp1 – hist))
{
Heater = 1; //wlacz grzanie
if(termo.dpr1p)
{
LCD_DisplayString(20,70,"Prog#1",&Font20,COL6,WHITE);
LCD_DisplayString(108,70,"ON ",&Font20,COL6,BLACK);
termo.dpr1p = 0;
termo.dpr1np = 1;
}
}
if(temperature > temp1)
{
Heater = 0;
if(termo.dpr1np)
{
LCD_DisplayString(20,70,"Prog#1",&Font20,COL6,WHITE);
LCD_DisplayString(108,70,"OFF",&Font20,COL6,GREEN);
termo.dpr1np = 0;
termo.dpr1p = 1;
}
}
}
if (termo.program == PR2)
{
if(temperature < (temp2 – hist))
{
Heater = 1; //wlacz grzanie
if(termo.dpr2p)
{
LCD_DisplayString(20,70,"Prog#2",&Font20,COL6,WHITE);
LCD_DisplayString(108,70,"ON ",&Font20,COL6,BLACK);
termo.dpr2p = 0;
termo.dpr2np = 1;
}
}
if(temperature > temp2)
{
Heater = 0;
if(termo.dpr2np)
{
LCD_DisplayString(20,70,"Prog#2",&Font20,COL6,WHITE);
LCD_DisplayString(108,70,"OFF",&Font20,COL6,GREEN);
termo.dpr2np = 0;
termo.dpr2p = 1;
}
}
}
}
Listing 8. Funkcja termostatu
Program termostatu zajmuje 69% z 48 kB dostępnej pamięci Flash, zaś zdecydowana większość tej przestrzeni przypada na procedury związane z obsługą graficznego wyświetlacza, w tym także wzorce znaków alfanumerycznych. Również podczas pisania programu oprogramowanie interfejsu użytkownika HMI okazało się najbardziej pracochłonne. Można zadać sobie pytanie, czy warto poświęcać tyle czasu na bardziej lub mniej atrakcyjny interfejs użytkownika. Skoro jednak mamy dzisiaj możliwości używania tanich, dobrej jakości wyświetlaczy, wydajnych małych mikrokontrolerów, czy dobrych bezpłatnych narzędzi programistycznych, szkoda byłoby tego nie wykorzystać.
Tomasz Jabłoński, EP