Powrót

Kurs Arduino – jak zbudować termometr?

W drugim odcinku kursu Arduino budujemy różnego rodzaju termometry. Poznajemy też szereg przydatnych funkcji, które pozwolą na praktyczne wykorzystanie budowanych termometrów.

W artykule o numerze UR001, pierwszym odcinku kursu Arduino, nauczyliśmy się korzystać z programu Arduino IDE, by tworzyć, modyfikować, weryfikować i wgrywać do płytki programy – szkice. Omówiliśmy podstawowe polecenia wejścia/wyjścia: digitalRead(), digitalWrite(),   analogRead() i analogWrite(). Zaczęliśmy też korzystać z łącza szeregowego (serial) do komunikacji z konsolą na ekranie komputera.

Początki okazały się zaskakująco proste. W drugim odcinku kontynuujemy takie zachęcające, łatwe ćwiczenia. Cykl Arduino  przeznaczony jest głównie dla elektroników, dlatego w miarę możliwości ćwiczenia dotyczą zagadnień, które elektronik wykorzystuje lub może wykorzystać przy budowie różnych urządzeń. Dlatego zaczniemy od czegoś jak najbardziej elektronicznego i „obowiązkowego”, a mianowicie termometru.

Termometr „diodowy”

Zbuduj układ według rysunku 1 i fotografii 2.

Rysunek 1

Fotografia 2

Czujnikiem temperatury jest jakakolwiek zwyczajna dioda, na przykład 1N4148 albo 1N4001…7. Jak wiadomo, napięcie przewodzenia złącza półprzewodnikowego maleje ze wzrostem temperatury ze współczynnikiem około 2mV/°C. Włączymy rezystor podciągający w procesorze (około 30kΩ), aby przez diodę popłynął niewielki prąd (około 0,15mA) i za pomocą wejścia analogowego A0 będziemy mierzyć napięcie przewodzenia diody. Najprostszy program dostępny  jest tutaj  w plikuZIP AZIP A0201.ino i wszystkie A02 . Pokazuje on na konsoli ekranowej komputera odczyty z takiego czujnika za pomocą programu:

void setup() {
  Serial.begin(9600);
  pinMode(A0, INPUT_PULLUP);
}
void loop() {
  Serial.println(analogRead(A0));
  delay(1000);
}

Rysunek 3

Termometr działa, ale jak pokazuje rysunek 3, wskazania zmieniają się od wartości 110, co odpowiada temperaturze pokojowej 21°C, jaka wtedy panowała w pokoju, do wartości 104, co odpowiada nagrzaniu diody palcami do temperatury około 30 stopni. Po pierwsze rozdzielczość jest słaba, po drugie wskazania są dziwne i „odwrotne”.

Co do rozdzielczości, to jeden bit przetwornika o 1024 poziomach i napięciu odniesienia 5V odpowiada napięciu 4,88mV. A to przy współczynniku cieplnym diody 2mV/C daje kiepską rozdzielczość około 2,5 stopnia Celsjusza na jeden bit przetwornika.

Możemy to łatwo poprawić, wykorzystując wewnętrzne źródło odniesienia procesora (1,1V). Włączymy je, dodając w funkcji setup() polecenie wykorzystania go:

analogReference(INTERNAL);

Rysunek 4

Jak pokazuje rysunek 4, jest znacznie lepiej, bo różnicy temperatur około 10 stopni (od 21°C do 31°C) odpowiada zmiana wartości z przetwornika ADC o 26 (od 517 do 491). Daje to przyzwoity współczynnik 0,38 stopnia na jeden bit przetwornika i pozwala mierzyć temperaturę z rozdzielczością mniej więcej 1/3 stopnia Celsjusza.

Trzeba tylko jeszcze zmienić wynik przetwarzania na stopnie Celsjusza. Wykorzystamy do tego bardzo pożyteczną „arduinową” funkcję map(). Składnia jest następująca:

map (liczba, fromLow, fromHigh, toLow, toHigh);

Funkcja map() zamienia liczbę całkowitą (zwykle zawartą w zmiennej) na inną liczbę według liniowej zależności wyznaczonej przez dwa punkty określone czterema liczbami. Dwie pierwsze liczby (fromLow, fromHigh) pokazują zakres wartości wejściowych, a dwie następne (toLow, toHigh) – zakres wartości wyjściowych. Funkcja ta służy do skalowania, np. może posłużyć do zamiany liczb 0…1023 z przetwornika ADC na 8-bitowe liczby z zakresu 0…255:

map (zADC, 0, 1023, 0, 255);

Ale funkcja map() może też służyć do jednoczesnego „odwracania”. W naszym przypadku liczbie 517 z przetwornika ADC odpowiada „dolna” temperatura 21°C, a liczbie 491 – „górna” temperatura 31°C. Dlatego napiszemy:

map (zADC, 517, 491, 21, 31);

Odrobinę zmieniony program według rysunku 5 wyświetla temperaturę w pełnych stopniach Celsjusza.

Rysunek 5

Dobrze, tylko wcześniej stwierdziliśmy, że w ten sposób możemy mierzyć temperaturę z lepszą rozdzielczością, mniej więcej 1/3 stopnia Celsjusza. Można to zrobić na wiele sposobów. Problemem jest to, że funkcja map() działa na liczbach całkowitych, a nie zmiennoprzecinkowych (typu float) i w podanej postaci zwraca wartość temperatury w postaci liczb całkowitych. Aby nie było takiego zaokrąglenia, możemy pracować na liczbach całkowitych, ale 10 razy większych, a potem przejść na zmiennoprzecinkowe i podzielić przez 10. Program może wyglądać jak na:

int zADC;
float temp;
void setup() {
  Serial.begin(9600);
  pinMode(A0, INPUT_PULLUP);
  analogReference(INTERNAL);
}
void loop() {
  zADC = 10 * analogRead(A0);
  temp = map(zADC, 5170, 4910, 210, 310);
  Serial.print(temp / 10, 1);
  Serial.print(„\xC2\xB0”);
  Serial.println(‚C’);
  delay(1000);
}

Mamy tu dwie zmienne globalne: zmienną zADC typu int dla liczb całkowitych, drugą o nazwie temp typu float dla wyniku zmiennoprzecinkowego. Liczbę całkowitą odczytaną z przetwornika ADC mnożymy przez 10 i przeprowadzamy jej mapowanie ze współczynnikami też powiększonymi dziesięciokrotnie. Funkcja map() zwraca liczbę całkowitą, ale 10 razy większą niż temperatura w stopniach Celsjusza, więc nie tracimy dokładności. I tę całkowitą liczbę wpisujemy do zmiennej typu float.

Należy zauważyć, że dokonujemy tu „po cichu” konwersji typów, czyli rzutowania. Jest to tzw. rzutowanie niejawne – niezalecane, bo gdyby zostało przeprowadzone nieświadomie, może stać się przyczyną kłopotów. W naszym prościutkim szkicu kłopotów nie będzie, ale lepiej jest stosować tak zwane rzutowanie jawne, gdzie w programie wyraźnie widać, że świadomie i celowo dokonujemy rzutowania, czyli konwersji typów. Arduino jest „przykrywką” na kompilator języka C i jego ulepszenia C++, więc rzutowanie można zrobić zarówno w dość prosty sposób według reguł języka C

temp = (float) map(zADC, 5170, 4910, 210, 310);

jak też wykorzystując jeden z czterech sposobów dostępnych w języku C++:

temp = static_cast<float> (map(zADC, 5170, 4910, 210, 310));

Po dokonaniu rzutowania, w zmiennej temp, która jest typu float, nadal mamy liczbę całkowitą dziesięć razy za dużą. Ale gdy podzielimy ją przez 10, otrzymamy liczbę zmiennoprzecinkową z dużą liczbą miejsc po przecinku (w Polsce mówimy „miejsc po przecinku”, ale w komputerach część całkowitą od ułamkowej oddziela kropka). Standardowo polecenie

Serial.print(temp / 10);

wyświetli wynik dzielenia z dwoma miejscami po przecinku (kropce). Polecenie

Serial.print(temp / 10, 8);

wyświetli wynik z bezsensowną „dokładnością” ośmiu miejsc po przecinku (kropce). W naszym termometrze wystarczy jedno miejsce po przecinku, stąd mamy

Serial.print(temp / 10, 1);

Zauważ, że wcześniej pisaliśmy println, teraz piszemy tylko print. Polecenie println to skrót od print linedrukuj linię, czyli: napisz i przejdź do nowej linii. My po wypisaniu na ekranie temperatury nie przechodzimy do nowej linii, tylko dopisujemy w tej linii znak stopnia Celsjusza. Jest pewien kłopot z wypisaniem na konsoli komputera symbolu stopnia (°) – robimy to poleceniem

Serial.print(„\xC2\xB0”);

Zamiast Serial.print możemy wykorzystać Serial.write – polecenie wysłania jednego znaku. Ten sam symbol stopnia możemy uzyskać na kilka pokrewnych sposobów:

Serial.write(„\xC2\xB0”);
Serial.write(0xC2); Serial.write(0xB0);
Serial.print(char(194)); Serial.print(char(176));
Serial.write(char(194)); Serial.write(char(176));

ale nie zadziała polecenie:

Serial.print(0xC2); Serial.print(0xB0);

które wypisze na ekranie liczbę 194176. Te niełatwe zagadnienia będziemy omawiać szerzej, w kontekście wyświetlania napisów na ekranie konsoli komputera oraz za pomocą wyświetlacza znakowego LCD.

Na koniec wypisujemy na ekranie dużą literkę C poleceniem println, powodującym przejście do nowej linii.

Rysunek 6

I oto, jak pokazuje rysunek 6, stworzyliśmy najprawdziwszy termometr z najprostszym czujnikiem diodowym. Oczywiście jego prymitywna kalibracja pozostawia wiele do życzenia. Gdybyśmy jednak z pomocą jakiegoś termometru wzorcowego w dwóch znacznie różniących się temperaturach zmierzyli dane z przetwornika ADC, to uzyskalibyśmy naprawdę przyzwoity termometr, uwzględniający zarówno parametry użytej diody, jak też rozrzuty napięcia odniesienia przetwornika ADC. Może on znaleźć praktyczne zastosowanie, zwłaszcza z czujnikiem w postaci tranzystora (baza zwarta z kolektorem), np. tranzystora PNP mocy, którego wkładka radiatorowa będzie na potencjale masy.

Dokładniejszy termometr możemy zrealizować z wykorzystaniem popularnych układów scalonych.

„Scalony” termometr analogowy

Do budowy dokładnego termometru możemy wykorzystać jakąś popularną kostkę, która wytwarza napięcie proporcjonalne do temperatury. Jest wiele tego rodzaju analogowych układów scalonych, między innymi LM35, LM335, TMP35…TMP37, MCP9700 czy TCP1047.

Zacznijmy od LM335 – czujnika temperatury bezwzględnej, który daje napięcie wprost proporcjonalne do temperatury bezwzględnej (w kelwinach) ze współczynnikiem 10mV/°C. Jak wiadomo, temperaturze 0°C odpowiada 273,15K. Jeżeli więc przykładowo napięcie czujnika wyniesie 2,946V (2946mV), daje to temperaturę 294,6 kelwina, czyli 21,45°C, w przybliżeniu 21,5°C.

Duże napięcie z czujnika uniemożliwia wykorzystanie wewnętrznego źródła napięcia odniesienia 1,1V. Ale w tej roli można wykorzystać obecny na płytce Arduino Uno stabilizator napięcia 3,3V, co umożliwi pomiar napięć do 3300mV, czyli temperatur do około 330 kelwinów (+57°C). Przy napięciu odniesienia 3300mV jednemu bitowi 10-bitowego przetwornika ADC odpowiada napięcie 3,2mV, co daje rozdzielczość około 1/3 stopnia, podobnie jak termometru z diodą.

Teraz kalibracja będzie dużo łatwiejsza – wystarczy jednopunktowa.

Jeśli masz kostkę LM335, zbuduj układ według rysunku 7 i fotografii 8.

Rysunek 7

Fotografia 8

Według katalogu czujniki te mogą pracować przy prądach od 0,45mA do 5mA. Dlatego tu nie wystarczy włączenie wewnętrznego podciągania i potrzebny jest dodatkowy rezystor 2,2kΩ.

Napięcie 3,3V przez zworę – mostek podajemy na wejście zewnętrznego napięcia odniesienia AREF i w programie ustawimy zewnętrzne napięcie AREF jako napięcie odniesienia dla przetwornika ADC. Szkice tej wersji termometru dostępne są w Elportalu w pliku A0202.ino.

Aby skalibrować termometr, wystarczy zmierzyć wskazanie z przetwornika ADC w jednej znanej temperaturze. U mnie w temperaturze 22,8°C (296 kelwinów) przetwornik dał wskazanie 929 – rysunek 9, co daje współczynnik korekcyjny 3,1385/°C.

Rysunek 9

Jeżeli więc aktualne wskazanie z przetwornika ADC podzielimy przez 3,1385, to otrzymamy temperaturę w kelwinach. Następnie od wyniku trzeba odjąć stałą 273,15, co da temperaturę w stopniach Celsjusza. W tym przypadku nie ma tu potrzeby stosowania funkcji mapującej, więc obliczenia można przeprowadzić na liczbach zmiennoprzecinkowych (typu float), na przykład według rysunku 10.

Rysunek 10

Jeśli wystarczy tylko skala Celsjusza, można wykorzystać dość przejrzysty szkic z jedną zmienną zmiennoprzecinkową term:

float term;
void setup() {
 Serial.begin(9600);
  analogReference(EXTERNAL);
}
void loop() {
  term = analogRead(A0);
  term = term / 3.1385;
  term = term – 273.15;
  Serial.print(term, 1);
  Serial.print(„\xC2\xB0”);
  Serial.println(‚C’);
  delay(1000);   }

Można zupełnie zrezygnować ze zmiennej typu float, ale na liczbie całkowitej z przetwornika ADC trzeba przeprowadzić operacje zmiennoprzecinkowe, więc nie można zapomnieć o rzutowaniu (float), na przykład jak na zwięzłym szkicu:

void setup() {
  Serial.begin(9600);   analogReference(EXTERNAL); }
void loop() {
  Serial.print(((float)analogRead(A0) / 3.1385 – 273.15), 1);
  Serial.print(„\xC2\xB0”);Serial.println(‚C’);delay(1000); }

Tutaj argumentem przekazywanym do polecenia drukowania Serial.print() jest wyrażenie

((float)analogRead(A0) / 3.1385 – 273.15)

którego zmienoprzecinkowy wynik zostanie wydrukowany z jednym miejscem po przecinku. To z zapasem wystarczy, bo i tak realna rozdzielczość termometru z kostką LM335 nie będzie lepsza, niż obliczona wcześniej jedna trzecia stopnia.

Lepsze właściwości zapewni nowocześniejszy układ LM35.

Precyzyjny termometr pokojowy

Jeśli masz kostkę LM35, zestaw układ według rysunku 11 i fotografii 12.

Rysunek 11

Fotografia 12

Szkice tej wersji termometru dostępne są tutaj w szkicu A0203.ino. w pliku ZIP A02.

Układ LM35 daje na wyjściu napięcie wprost proporcjonalne do temperatury wyrażonej w stopniach Celsjusza ze współczynnikiem 10mV/°C, co przy wewnętrznym napięciu odniesienia 1,1V pozwoli osiągnąć rozdzielczość około 0,1 stopnia.

Pięknie, tylko nie można w ten najprostszy sposób mierzyć napięć ujemnych – właśnie dlatego będzie to tylko termometr pokojowy, a nie zaokienny. Z uwagi na tolerancję czujnika oraz tolerancję napięcia odniesienia, też potrzebna jest kalibracja – całkowicie wystarczy jednopunktowa. Znów przy użyciu dokładnego termometru wzorcowego najpierw sprawdzamy, jaki jest odczyt z ADC przy znanej temperaturze. U mnie w temperaturze 21,7°C odczyt z ADC wynosił 228 (rysunek 13), co daje współczynnik kalibracyjny 10,507.

Rysunek 13

Po uwzględnieniu go możemy wykorzystać banalnie prosty szkic:

void setup() {
  Serial.begin(9600); analogReference(INTERNAL); }
void loop() {
  Serial.print(((float)analogRead(A0) / 10.507), 1);
  Serial.print(„\xC2\xB0”);  Serial.println(‚C’);
  delay(1000);     }

pokazany też na rysunku 14 (zawarty także w pliku A0203.ino).

Rysunek 14

I oto zbudowałeś naprawdę czuły i dokładny termometr, który możesz wykorzystać na wiele sposobów.

Jestem przekonany, że w razie potrzeby bez kłopotów dostosujesz programy do jeszcze innych podobnych czujników.

Jeśli zbudujesz czułą wersję o realnej rozdzielczości 0,1 stopnia z kostką LM35, koniecznie zwróć uwagę na fluktuacje wskazań. Prawdopodobnie takie obserwacje znacząco wpłyną na Twoje oczekiwania i wyobrażenia dotyczące precyzyjnych pomiarów temperatury.

Zachęcam do eksperymentów! A w następnym odcinku UR003 spróbujemy inaczej zobrazować wyniki pomiarów.

Piotr Górecki