[057] Mierzymy temperaturę

[057] Mierzymy temperaturę

Dawno, dawno temu, w roli czujników temperatury używano elementów, które włączone w obwód, w zależności od niej zmieniały jego parametry. Metoda była prosta, ale obarczona błędami i potrzebą kalibracji. Później pojawiły się układy, którym towarzyszyły podzespoły kalibracyjne i kształtujące wartości napięć wyjściowych liniowo względem mierzonej temperatury. Przykład takiego rozwiązania znajdziemy na płytce edukacyjnej TME. Były to jednak nadal układy analogowe i ich współpraca z mikrokontrolerami wymagała użycia przetworników analogowo-cyfrowych.


Rozwój elektroniki sprawił, że te czasy mamy już za sobą i dziś w roli takich czujników używa się układów, które można by nazwać małymi, specjalizowanym komputerami. Są dokładne, powtarzalne, nie wymagają żadnego skalowania i porozumiewają się ze światem za pomocą standardowych magistrali. Oto najbardziej znany przedstawiciel, DS18B20

Ten niepozorny element, który wygląda jak tranzystor małej mocy, zapożyczając zresztą z niego obudowę, stał się standardem, jak wyświetlacz pracujący z układem serii HD44780. Zatem jest doskonale znany, niezawodny i wyprodukowany zapewne w ilościach rzędu miliarda. Termometr ten mierzy temperaturę w zakresie od -55 do +125 stopni, przy czym w przedziale od -10 do +80 stopni zapewnia dokładność całkowitą rzędu jednego stopnia. Szybkość pomiaru wynosi ¾ sekundy, ale jeśli ograniczy się nieco dokładność, może wzrosnąć do jednej dziesiątej.

Aby wydobyć z termometru dane, należy rozmawiać z nim językiem interfejsu zwanego 1-wire. Nazwa wzięła się od sposobu podłączania urządzeń: używa się tutaj jednej linii (nie licząc wspólnej linii masy), która zarazem zasila urządzenia i przesyła dane. Robi się to zamiennie, a podczas komunikowania się urządzeń ze sobą, te korzystają z energii zgromadzonej w niewielkim kondensatorze znajdującym się w nich. Z tego powodu moce są nieduże, a szybkości – marne. Lecz termometr jest właśnie tym, co nie potrzebuje ani mocy, ani szybkości.

Termometr pracuje przy obu standardach napięć: 3,3 oraz 5 woltów i zawiera wszelkie elementy stabilizujące i kalibrujące. Posiada też dodatkowe cechy, jak pamięć progów, których przekroczenie, zarówno w górę jak i w dół, wywołuje alarm. I jak każde urządzenie kompatybilne z 1-wire, ma swój niepowtarzalny numer seryjny, zwany adresem. Jest on zapisany na 64 bitach, co daje ponad 18 trylionów kombinacji. Zatem szansa, że znajdziemy dwa takie jest żadna. Adres ten w zasadzie nie jest potrzebny do pracy, ale ułatwia wybór czujników w większych systemach. Skoro zapoznaliśmy się z teorią, podłączmy nasz termometr do Arduino.

Oczywiście możemy tutaj nic nie lutować, korzystając z płytki stykowej, ale tym razem postanowiłem zaprezentować rozwiązanie nietymczasowe, jako przykład takich sposobów konstrukcji urządzeń. Jeśli chodzi o wyprowadzenia, czarne to masa, zielone – linia danych, czerwone – zasilanie. Zatem by połączyć termometr z Arduino, wystarczy połączyć linie zasilające, a linię danych wysłać na dowolny z pinów.

Jedyny element dodatkowy, który jest wymagany, to rezystor o wartości 4,7 kΩ, który należy wpiąć pomiędzy zasilanie, a linię danych. Na zdjęciu wyżej przylutowałem bezpośrednio do wyprowadzeń rezystor powierzchniowy, na schemacie widzimy przewlekany.

Ale zaraz, mowa była o jednej linii i masie, a tu mamy w sumie trzy przewody. Gdybyśmy z jakichś powodów woleli podłączyć czujnik dwoma przewodami, nie ma problemu, podłącza się go wtedy nieco inaczej.

Trzeba tylko zewrzeć ze sobą obie skrajne nóżki, czyli zasilanie i masę. Zasilanie nadal będzie potrzebne po to, by podłączyć występujący wcześniej rezystor, ale on może znajdować się na początku przewodu. Ten sposób pracy jest jednak nieco mniej odporny na zakłócenia, acz każde z tych rozwiązań z punktu widzenia programu pracuje tak samo.

Do zilustrowania temperatury przyda się nam wyświetlacz. Ponieważ pisałem o nim już wiele razy, do tematu wracać nie będę. Czas ożywić urządzenie.

#include <OneWire.h>
#include <DallasTemperature.h>
#include <LiquidCrystal.h>

I znowu użyjemy bibliotek. Tutaj nie ma co być ambitnym, ręczne oprogramowanie interfejsu jest jak najbardziej możliwe, ale uciążliwe. Biblioteka OneWire pozwoli porozumiewać się z urządzeniami tego standardu w sposób prosty i niezawodny. Zaś biblioteka DallasTemperature obsłuży już nasz konkretny termometr. Przy dołączaniu nowych bibliotek, warto skompilować program zaraz na wstępie, kiedy jest jeszcze pusty.

Klikamy zatem w ikonkę Weryfikuj, po czym najprawdopodobniej dostaniemy komunikat o braku owych bibliotek. Zatem należy je pobrać, wpisując ich nazwy w wyszukiwarce Menedżera bibliotek.

const byte oneWireBus = 11;           // Tutaj jest określony port magistrali 1-wire
OneWire oneWire(oneWireBus);          // Tutaj definiuje się port magistrali dla biblioteki.
DallasTemperature sensors(&oneWire);  // Teraz można już przekazać bibliotece obsługi termometru namiary na bibliotekę obsługi magistrali.

Deklarujemy port, który będzie obsługiwał magistralę naszego interfejsu – wybrałem pin jedenasty – i powiadamiamy o tym bibliotekę jego obsługi oraz bibliotekę obsługi termometru. Pozostanie nam dopisanie znanego już fragmentu kodu deklarującego dane związane z wyświetlaczem oraz zadeklarowanie zmiennej temperatura, w której będziemy przechowywać wartość temperatury.

const byte lcdRS = 2;  // Tutaj są określone poszczególne adresy linii wyświetlacza.
const byte lcdEN = 3;
const byte lcdD4 = 4;
const byte lcdD5 = 5;
const byte lcdD6 = 6;
const byte lcdD7 = 7;
const byte lcdKolumny = 16;                                   // To jest ilość kolumn w naszym wyświetlaczu.
const byte lcdWiersze = 2;                                    // To jest ilość wierszy w naszym wyświetlaczu.
LiquidCrystal lcd(lcdRS, lcdEN, lcdD4, lcdD5, lcdD6, lcdD7);  // Tutaj dostarcza się bibliotece obsługi wyświetlacza wiedzy, gdzie co jest podłączone.

int temperatura;  // Tutaj przechowuje się wartość temperatury odczytaną z termometru, wymnożoną przez 10 i zaokrągloną.

Deklaracje mamy z głowy. Czas na działania wstępne. Termometr należy zainicjować. Inicjację wyświetlacza już znamy.

void setup() {
  sensors.begin();                    // Inicjuj termometr.
  lcd.begin(lcdKolumny, lcdWiersze);  // Inicjuj wyświetlacz, powiadamiając go o ilości kolumn i wierszy.
}

W tego typu projektach bardzo często będziemy się spotykać z problemem „latających napisów”. W naszym przykładzie będzie się to objawiać tym, że w przypadku wartości ujemnych wszystko przesunie się o kolumnę w prawo, bo na pierwszej pozycji wyskoczy minus. A gdy wartość będzie równa bądź większa od 10 albo równa bądź mniejsza od -10, dostaniemy kolejne przesunięcie. W przypadku takich urządzeń jak termometr czy zegarek, takie gonitwy napisów są irytujące. Oczywiście da się to naprawić i sposobów jest kilka, ale żaden nie jest taki prosty. Przedstawię tutaj sposób dość nietypowy, lecz pozwalający spojrzeć na problem formatowania liczb właśnie z takiej, nieco chytrej strony.

Przyda nam się zadeklarowana zmienna temperatura, która będzie przechowywać odczytaną wartość temperatury. Co ciekawe, nie będzie to zmienna typu zmiennoprzecinkowego, choć w tej postaci przychodzą dane z termometru, a raczej z biblioteki, która go obsługuje.

void loop() {
  lcd.home();                                              // Ustaw kursor na początku.
  sensors.requestTemperatures();                           // Wyślij rozkaz zmierzenia temperatury.
  temperatura = 10 * (sensors.getTempCByIndex(0) + 0.05);  // Przekształć wartość odczytaną w całkowitą (dokładność: jedno miejsce po przecinku)

Po ustawieniu kursora na początku wyświetlacza wykonamy polecenie zmierzenia temperatury. Następnie wartość ową przeniesiemy do naszej zmiennej temperatura, ale nie wprost, bo odczyty przychodzą w postaci liczby zmiennoprzecinkowej, czyli takiej z wartościami dziesiętnymi, a my tutaj zadeklarowaliśmy zmienną typu int, która może zapisywać jedynie wartości całkowite. Zrobiłem to w następującym celu. Otóż wartości odczytane mają sens do jednej dziesiątej stopnia. Gdybyśmy użyli formatu oryginalnego, wyświetlałyby nam się setne części stopnia, które sensu nie mają i pokazują przypadkowe wartości. Zatem mnożąc tę wartość przez 10 i osadzając w formacie całkowitym, dostaję zaokrągloną wartość oryginalną do jednego miejsca po przecinku. Reszta ułamka ginie bezpowrotnie.

Ale taki sposób zaokrąglania jest niezgodny z zasadami matematyki, albowiem jeśli wartość na kolejnej pozycji po przecinku była większa od pięciu, winniśmy pozycję poprzednią podnieść o jeden. Zatem tuż przed konwersją należy do odczytanej wartości dodać pięć setnych i uzyskamy zaokrąglenie zgodne z normami.

if (temperatura < 0) {  // Gdy wartość jest ujemna...
  temperatura--;        // Odejmij jeden ze względu na zaokrąglenia liczb ujemnych.
  lcd.print(F("-"));    // Rysuj znak minusa.
} else {
  lcd.print(F(" "));    // W przeciwnym razie rysuj spację.
}

Mając już zgrabną postać do analiz, postaramy się wykryć liczby ujemne. To akurat jest proste i jeśli wartość jest mniejsza od zera, narysujemy znak minusa. Ale gdy będzie równa lub większa, także należy coś narysować, by nam reszta wyniku nie przesunęła się. Tradycja nie przewiduje rysowania plusa, zatem wstawiamy tutaj spację, czyli odstęp. Lecz jeśli ktoś chce mieć plus – proszę bardzo.

temperatura = abs(temperatura);  // Wyciągnij wartość bezwzględną.
if (temperatura < 100) {         // Jeśli temperatura będzie większa od 9 bądź mniejsza od -9 stopni...
  lcd.print(F(" "));             // Rysuj dodatkową spację.
}

Teraz zajmiemy się wartościami jednocyfrowymi, czyli z przedziału od -9,9 do 9,9 stopnia. Jeśli temperatura będzie mieć wartość z tego przedziału, będziemy musieli najpierw wstawić dodatkową spację, by wynik nie przesunął się nam w lewo. Jeśli wartość będzie spoza tego przedziału, nie będzie trzeba nic wstawiać, bo w pustym miejscu pojawi się wartość dziesiątek.

Tylko co zrobić, gdy wartości będą ujemne? Przecież o minus już zadbaliśmy, zatem pojawiłby się drugi. Oczywiście, gdybyśmy najpierw nie przeprowadzili tej operacji, która tworzy wartość bezwzględną. Od tej pory nie będziemy mieć już liczb ujemnych.

Ktoś może zapytać: skoro identyfikujemy liczby dwucyfrowe, dlaczego tutaj widnieje sto, a nie dziesięć? Nie zapominajmy, że naszą wartość reprezentuje format int, czyli liczba całkowita. Tu na górze pomnożyliśmy ją przez 10, więc wszystkie rozważania musimy także mnożyć przez 10. Zatem identyfikacja liczb dwucyfrowych to identyfikacja wartości przekraczających setkę.

W tym momencie mamy już wszystko do prawidłowego przedstawienia wyniku na wyświetlaczu. Ale wciąż wynik to liczba całkowita. Gdybyśmy ją teraz wyświetlili wprost, zamiast 22 i 4 dziesiątych stopnia mielibyśmy 224 stopnie. Dlatego przekształcimy naszą wartość w postać ludzką, czyli zmiennoprzecinkową, o czym świadczy użycie formatu float.

lcd.print(float(temperatura) / 10, 1);  // Zamień wartość całkowitą w zmiennoprzecinkową i rysuj z dokładnością do jednego miejsca po przecinku.
lcd.print((char)223);                   // Rysuj znak stopni
lcd.print(F("C"));                      // Rysuj znak C

delay(500);                             // Pętla opóźniająca mająca na celu wyeliminowanie niestabilności odczytów.

Jest to format dla Arduino wyjątkowo niesympatyczny: po pierwsze nie przedstawia dokładnych wartości, a przybliżone, mniej więcej do szóstego miejsca po przecinku. Do tego wszelkie działania na tego typu liczbach są zasobożerne. Z tych powodów gdzie się tylko da, powinno się unikać tego formatu. Lecz w sytuacjach jak tutaj w niczym nam to nie będzie przeszkadzać, a pozwoli wyświetlić wynik jak należy.

Zwróćmy uwagę na dwie rzeczy: po pierwsze mamy tutaj dzielenie przez dziesięć, a to po to, by zejść z wartości wcześniej pomnożonej dziesięciokrotnie. Po drugie, zadeklarowaliśmy tutaj wyświetlanie wyłącznie jednej liczby po przecinku. Domyślnie wyświetlają się dwie, a przecież chcieliśmy się tego pozbyć i częściowo po to była cała ta komplikacja. Zresztą drugą pozycją byłoby teraz już zawsze zero.

Na koniec już dodajmy ozdobniki, wszak mamy na wyświetlaczu dużo miejsca. Tak się składa, że w zasobach znaków wyświetlacza znajdziemy symbol stopni. Jest trochę przesunięty, ale ujdzie. By się do niego dostać, nie możemy go wpisać wprost i musimy odwołać się do jego adresu, który wynosi dziesiętnie 223. Literkę C narysujemy już całkiem zwyczajnie.

Okazuje się, że pętla, ograniczona tylko opóźnieniem rysowania danych na wyświetlaczu, wykonuje się dużo szybciej niż dane przychodzące z termometru. Może się to objawiać w postaci zmieniających się nerwowo ostatnich pozycji wyniku. Po wstawieniu półsekundowej pętli opóźniającej całość się uspokaja. O ile nie popieram używania funkcji delay, tutaj niczemu to nie przeszkadza, ponieważ w tym programie nic więcej się nie dzieje.

Z termometrem tu prezentowanym możemy zrobić dużo więcej sztuczek. Możemy też podłączyć kilka czujników, robiąc bardziej użyteczne urządzenie, które mierzy na przykład temperaturę na zewnątrz i w mieszkaniu. Ale o tym napiszę innym razem.

#include <OneWire.h>
#include <DallasTemperature.h>
#include <LiquidCrystal.h>

const byte oneWireBus = 11;           // Tutaj jest określony port magistrali 1-wire
OneWire oneWire(oneWireBus);          // Tutaj definiuje się port magistrali dla biblioteki.
DallasTemperature sensors(&oneWire);  // Teraz można już przekazać bibliotece obsługi termometru namiary na bibliotekę obsługi magistrali.

const byte lcdRS = 2;  // Tutaj są określone poszczególne adresy linii wyświetlacza.
const byte lcdEN = 3;
const byte lcdD4 = 4;
const byte lcdD5 = 5;
const byte lcdD6 = 6;
const byte lcdD7 = 7;
const byte lcdKolumny = 16;                                   // To jest ilość kolumn w naszym wyświetlaczu.
const byte lcdWiersze = 2;                                    // To jest ilość wierszy w naszym wyświetlaczu.
LiquidCrystal lcd(lcdRS, lcdEN, lcdD4, lcdD5, lcdD6, lcdD7);  // Tutaj dostarcza się bibliotece obsługi wyświetlacza wiedzy, gdzie co jest podłączone.

int temperatura;  // Tutaj przechowuje się wartość temperatury odczytaną z termometru, wymnożoną przez 10 i zaokrągloną.

void setup() {
  sensors.begin();                    // Inicjuj termometr.
  lcd.begin(lcdKolumny, lcdWiersze);  // Inicjuj wyświetlacz, powiadamiając go o ilości kolumn i wierszy.
}

void loop() {
  lcd.home();                                              // Ustaw kursor na początku.
  sensors.requestTemperatures();                           // Wyślij rozkaz zmierzenia temperatury.
  temperatura = 10 * (sensors.getTempCByIndex(0) + 0.05);  // Przekształć wartość odczytaną w całkowitą (dokładność: jedno miejsce po przecinku)

  if (temperatura < 0) {  // Gdy wartość jest ujemna...
    temperatura--;        // Odejmij jeden ze względu na zaokrąglenia liczb ujemnych.
    lcd.print(F("-"));    // Rysuj znak minusa.
  } else {
    lcd.print(F(" "));  // W przeciwnym razie rysuj spację.
  }

  temperatura = abs(temperatura);  // Wyciągnij wartość bezwzględną.
  if (temperatura < 100) {         // Jeśli temperatura będzie większa od 9 bądź mniejsza od -9 stopni...
    lcd.print(F(" "));             // Rysuj dodatkową spację.
  }

  lcd.print(float(temperatura) / 10, 1);  // Zamień wartość całkowitą w zmiennoprzecinkową i rysuj z dokładnością do jednego miejsca po przecinku.
  lcd.print((char)223);                   // Rysuj znak stopni
  lcd.print(F("C"));                      // Rysuj znak C

  delay(500);  // Pętla opóźniająca mająca na celu wyeliminowanie niestabilności odczytów.
}
Płytka edukacyjna TME-EDU-ARD-2Płytka edukacyjna TME-EDU-ARD-2Sprawdź tutaj

Przeczytaj również

Nasi partnerzy

TMETech Master EventTME EducationPoweredby
Copyright © 2024 arduino.pl