Kurs STM32L4 – #17 – termometry DS18B20 (1-wire, UART)

Kurs STM32L4 – #17 – termometry DS18B20 (1-wire, UART)

Protokół 1-wire spotykany jest chyba najczęściej podczas komunikacji z termometrami DS18B20. Dlatego w tej części kursu STM32L4 zajmiemy się właśnie tym zagadnieniem.

Podczas eksperymentów z DS18B20 utworzymy najpierw niskopoziomową bibliotekę do 1-wire, a później użyjemy jej do obsługi tych sensorów.

Czego dowiesz się z tej części kursu STM32L4?

Zaczniemy od szybkiego omówienia czujników DS18B20 oraz protokołu 1-wire. Następnie zbudujemy niskopoziomową bibliotekę, dzięki której nawiążemy komunikację z czujnikiem, odczytamy jego adres i zmierzoną temperaturę. Omówimy wady programowej implementacji 1-wire, którą można spotkać w sieci, a na koniec utworzymy lepszą wersję (z użyciem sprzętowego UART-a w trybie Half-Duplex).

DS18B20 – czujnik temperatury

Większość entuzjastów elektroniki spotkała się z czujnikami DS18B20, które mogą mierzyć temperaturę w zakresie od −55°C do 125°C. Główną zaletą tych czujników jest to, że korzystają z protokołu 1-wire. W praktyce oznacza to, że potrzebna jest tylko jedna linia danych, do której możemy podłączyć wiele czujników, a wyniki bez problemu przyporządkujemy później do konkretnych sensorów – dzięki temu, że mają one unikalne adresy. Więcej informacji o czujnikach znaleźć można w nocie katalogowej.

Opis wyprowadzeń termometru DS18B20

Opis wyprowadzeń termometru DS18B20

W przypadku komunikacji przez I2C mieliśmy dwie linie: SDA i SCL – pierwsza służyła do przesyłania danych, a druga do transmisji sygnału zegara. W przypadku 1-wire wystarczy jedna linia, bo brak tu sygnału zegarowego – jest to komunikacja asynchroniczna. Niestety nasz mikrokontroler STM32L4 nie posiada sprzętowego wsparcia dla 1-wire, nie oznacza to jednak, że nie możemy korzystać z takich czujników – tyle że musimy sami zadbać o obsługę tego protokołu (trzeba więc najpierw go poznać).

Termometry DS18B20 charakteryzują się m.in. szerokim zakresem pomiarowym

Termometry DS18B20 charakteryzują się m.in. szerokim zakresem pomiarowym

Protokół 1-wire

Zwięzły opis protokołu 1-wire znaleźć można na tej stronie. Oto najważniejsze informacje, które będą dla nas ważne: po pierwsze, w protokole 1-wire jedna linia służy zarówno do wysyłania, jak i odbierania danych. Podobnie działała linia SDA w protokole I2C, nie powinniśmy być zatem zaskoczeni podobnym rozwiązaniem elektrycznym – tu też piny ustawiane są w tryb otwartego drenu (ang. open-drain). Potrzebne jest więc podłączenie rezystora podciągającego (najczęściej 4,7 k).

Po drugie, domyślnym stanem linii jest stan wysoki, a układy podłączone przez 1-wire mogą wymuszać stan niski (zwieraniem linii do masy). Niestety, podobnie jak w przypadku I2C prędkość komunikacji jest ograniczona, ale w zupełności wystarcza ona do wielu zastosowań (takich jak odczyt temperatury). Dodatkowym utrudnieniem jest brak sygnału zegarowego, co w praktyce oznacza dla nas, że bardzo ważne będą tutaj czasy trwania sekwencji, które będziemy wystawiać na linii danych.

Schemat podłączenia dwóch urządzeń podrzędnych do wspólnej magistrali 1-wire

Schemat podłączenia dwóch urządzeń podrzędnych do wspólnej magistrali 1-wire

Kolejnym podobieństwem 1-wire do I2C jest podział na dwa rodzaje urządzeń. Mamy tutaj urządzenia nadrzędne (ang. master) i podrzędne (ang. slave). Urządzenie nadrzędne, czyli w naszym przypadku mikrokontroler STM32L4, kontroluje transmisję przez 1-wire i zawsze inicjalizuje komunikację. Układ podrzędny, czyli czujnik DS18B20, tylko odpowiada na żądania przesyłane przez mastera.

Sekwencja reset w 1-wire

Komunikacja rozpoczyna się od wygenerowania przez układ nadrzędny specjalnej sekwencji, nazywanej reset. Sekwencja ta informuje układy podrzędne o rozpoczęciu transmisji.

Sekwencja reset, informująca o początku transmisji 1-wire

Sekwencja reset, informująca o początku transmisji 1-wire

Reset rozpoczyna się od sygnału niskiego, który utrzymywany jest przez minimum 480 μs. Następnie master zwalnia linię i czeka na potwierdzenie ze strony układu podrzędnego. Po około 70 μs następuje sprawdzenie stanu linii – jeśli jakiś układ podrzędny jest obecny, to układ nadrzędny wykryje na linii stan niski. Przed dalszą komunikacją master musi odczekać jeszcze pewien czas – typowo 410 μs.

Odczyt danych przez 1-wire

Odczyt danych jest również inicjowany przez układ nadrzędny. W tym przypadku jest to wymuszenie stanu niskiego przez 6 μs, potem master czeka 9 μs, po czym następuje odczyt stanu linii. Stan niski wskazuje na odbiór bitu oznaczającego 0, a wysoki – 1. Po odczycie układ nadrzędny musi też odczekać 55 μs przed kolejną transmisją.

Sekwencja rozpoczynająca odczyt danych przez 1-wire

Sekwencja rozpoczynająca odczyt danych przez 1-wire

Wysyłanie danych (kodowanie bitów) przez 1-wire

Tak samo jak podczas omawiania dekodowania NEC na STM32L4, musimy też wiedzieć, jak w 1-wire kodowane są poszczególne bity, bo logiczna jedynka nie jest wcale reprezentowana przez ciągły stan wysoki itd. W przypadku 1-wire wysyłanie bitu reprezentującego logiczną jedynkę to 6 μs stanu niskiego oraz 64 μs stanu wysokiego. Z kolei zero realizowane jest przez wymuszenie przez 60 μs stanu stanu niskiego, a następnie stanu wysokiego przez kolejne 10 μs.

Sposób reprezentacji bitów w 1-wire

Sposób reprezentacji bitów w 1-wire

Te podstawowe informacje na temat 1-wire wystarczą do nawiązania komunikacji z naszym czujnikiem. Jedyne, czego jeszcze nam tutaj brakuje, to informacja o kolejności przesyłania bitów – w tym przypadku dane przekazywane są jako 8-bitowe bajty przesyłane w kolejności od najmniej znaczącego bitu.

Podłączenie DS18B20 do STM32L4

W związku z tym, że termometry DS18B20 korzystają tylko z jednej linii danych, ich podłączenie do mikrokontrolera jest banalnie proste. Wystarczy zasilanie (3,3 V), linia danych i rezystor podciągający.

Schemat ideowy i montażowy podłączenia DS18B20 do STM32L4

Schemat ideowy i montażowy podłączenia DS18B20 do STM32L4

Warto też wiedzieć, że istnieje możliwość zasilania układu DS18B20 wprost z linii danych (jest to tzw. zasilanie pasożytnicze). Wtedy do podłączenia czujnika zamiast trzech przewodów wystarczyłyby raptem dwa (linia danych i masa) – więcej informacji na ten temat znaleźć można w dokumentacji.

Gotowe zestawy do kursów Forbota

 Komplet elementów  Gwarancja pomocy  Wysyłka w 24h

Zamów zestaw elementów i wykonaj ćwiczenia z tego kursu! W komplecie płytka NUCLEO-L476RG oraz m.in. wyświetlacz graficzny, joystick, enkoder, czujniki (światła, temperatury, wysokości, odległości), pilot IR i wiele innych.

Zamów w Botland.com.pl »

Komunikacja STM32L4 z DS18B20

Mamy już podłączony układ, więc czas przystąpić do programowania. Zaczynamy tak jak zawsze, czyli tworzymy projekt z STM32L476RG, który pracuje z częstotliwością 80 MHz. Uruchamiamy debuggerUSART2 w trybie asynchronicznym. Zaznaczamy w opcjach projektu, że CubeMX ma wygenerować osobne pliki dla wszystkich modułów. Następnie dodajemy przekierowanie printf na UART – wystarczy dodanie pliku nagłówkowego:

oraz kodu zbliżonego do poniższego:

Na początek będziemy realizowali komunikację przez 1-wire w pełni programowo. Takie rozwiązanie określane jest jako bit-banging. Wracamy więc do CubeMX i zabieramy się za konfigurację peryferii.

Do komunikacji z czujnikiem potrzebna jest nam jedna linia GPIO – wybieramy pin PC4 i ustawiamy go w tryb GPIO_Output. Następnie nadajemy mu nazwę DS i zmieniamy tryb pracy na Output Open Drain, a domyślną wartość, czyli GPIO output level, ustawiamy na High.

Konfiguracja GPIO do komunikacji przez 1-wire

Konfiguracja GPIO do komunikacji przez 1-wire

Jak widzieliśmy wcześniej, komunikacja przez 1-wire wymaga dokładnego przestrzegania pewnych ram czasowych, dlatego przyda się tu licznik. Aktywujemy zatem TIM6 – jest to jeden z najprostszych timerów, więc tutaj sprawdzi się idealnie. Preskaler dla licznika ustawiamy na 79, dzięki czemu będziemy mogli za jego pomocą odliczać dokładnie czas z rozdzielczością 1 μs.

Programowy 1-wire na STM32L4 (bit-banging)

W pierwszej wersji programu skupimy się po prostu na uzyskaniu działającego kodu. Podobną wersję można zobaczyć w wielu innych poradnikach – ten kod będzie działać (do czasu), więc łatwo wpaść w zachwyt podczas pierwszych testów. Trzeba być jednak świadomym, że to rozwiązanie może sprawiać wiele problemów, a kod ten może być raczej przykładem z gatunku, jak nie należy tworzyć programów na mikrokontrolery (oczywiście pokażemy, jak zrobić to lepiej).

Na początek będziemy potrzebowali funkcji opóźniającej. Znamy już bardzo dobrze HAL_Delay, ale pozwala ona na tworzenie opóźnień będących wielokrotnością milisekundy. Tym razem interesują nas jednak mikrosekundy. Możemy więc łatwo napisać własną funkcję korzystającą z TIM6.

Za pomocą makra __HAL_TIM_SET_COUNTER zerujemy licznik timera. Następnie w pętli odczytujemy aktualny stan licznika przy użyciu __HAL_TIM_GET_COUNTER, który będzie zwiększany co 1 μs. Dzięki temu będziemy mogli generować opóźnienia z dokładnością wymaganą przez 1-wire.

Teraz możemy przystąpić do napisania funkcji generującej na linii 1-wire sekwencję reset. Nie ma tutaj nic trudnego – po prostu przepisujemy „na kod” to, co omówiliśmy wcześniej na diagramach:

Użyte czasy pochodzą dokładnie z omawianych wcześniej diagramów. Najpierw na 480 μs wystawiamy stan niski, następnie czekamy 70 μs i odczytujemy stan linii komunikacyjnej. Na koniec odczekujemy jeszcze 410 μs, po czym zwracamy odpowiednią wartość – HAL_OK, jeśli uzyskaliśmy odpowiedź od czujnika, lub HAL_ERROR, jeśli jej nie było.

W podobny sposób tworzymy funkcję wysyłającą pojedyncze bity (1 lub 0):

Odczyt bitu jest nieco bardziej skomplikowany, ale kod funkcji jest również dosłownym przeniesieniem wcześniejszego diagramu „na kod”:

Mamy już prawie wszystko. Dla ułatwienia dodajmy jeszcze dwie funkcje, które będą potrafiły wysyłać oraz odbierać całe bajty. Może to wyglądać zawile, ale to nic strasznego – po prostu osiem razy wywoływana jest odpowiednia funkcja operująca na pojedynczych bitach, a wynik tej pętli jest łączony w bajt (wszystko za pomocą operatorów bitowych).

To, co napisaliśmy do tej pory, to niskopoziomowa obsługa protokołu 1-wire. Dopiero teraz możemy przejść do właściwej komunikacji z czujnikiem DS18B20.

Odczyt adresu czujnika DS18B20

Każdy czujnik posiada swój własny, niepowtarzalny identyfikator. Dzięki temu można podłączyć wiele czujników do jednej linii danych. Na razie podłączyliśmy tylko jeden czujnik, ale możemy zacząć od odczytania jego identyfikatora, który w dokumentacji określany jest jako ROM code. W efekcie uda nam się sprawdzić, czy komunikacja działa poprawnie.

W celu odczytania identyfikatora będziemy musieli najpierw wysłać przez 1-wire sekwencję reset, a następnie komendę Read ROM, której kod to 0x33 (wartość ta pochodzi oczywiście z dokumentacji czujnika). W odpowiedzi czujnik odeśle 8 bajtów – pierwszy powinien mieć kod 0x28, a ostatni będzie sumą kontrolną dla całego bloku danych. Program testowy może wyglądać następująco:

W pierwszej linii wywołujemy HAL_TIM_Base_Start, który uruchamia TIM6. Następnie wysyłamy kod 0x33, po czym od razu przechodzimy do pętli, której odbieramy kolejne bajty. Kod taki uruchamiamy za pomocą debuggera, a poprawność komunikacji zweryfikujemy, podglądając zmienne rc i rom_code.

Jeśli wartość zmiennych w debuggerze nie jest wyświetlana jako liczby w systemie szesnastkowym, to należy zaznaczyć interesujące nas zmienne w oknie Variables i wybrać odpowiednią opcję z menu, które znajduje się pod prawym przyciskiem myszy (Number Format > Hex).

Podgląd zawartości zmiennych w debuggerze

Podgląd zawartości zmiennych w debuggerze

Jeśli wszystko przebiegło poprawnie, to zmienna rc powinna mieć wartość HAL_OK – oznacza to, że czujnik odpowiedział na sygnał reset. Z kolei w zmiennej rom_code widoczny jest adres czujnika – każdy egzemplarz będzie miał inny, ale pierwszy bajt, czyli rom_code[0] powinien mieć wartość 0x28.

Przy okazji warto też sprawdzić, jakie dane pojawią się w przypadku błędu – można odłączyć DS18B20 od mikrokontrolera (np. odpinając linię danych) i ponowić test. W takiej sytuacji funkcja wire_reset powinna zwrócić HAL_ERROR, a odczyt danych powinien zwrócić samo 0xFF. Był to krótki program testowy, więc nie obsługuje on wszystkich błędów, ale warto pamiętać o tym na przyszłość.

Sprawdzanie sumy kontrolnej DS18B20

Ostatni bajt zwrócony przez nasz czujnik to tzw. suma kontrolna (CRC), dzięki której możemy sprawdzić, czy odebraliśmy poprawne dane. W dokumentacji DS18B20 znajdziemy opis tego, jak jest ona liczona.

Schemat obliczania sumy kontrolnej DS18B20 (fragment dokumentacji)

Schemat obliczania sumy kontrolnej DS18B20 (fragment dokumentacji)

Diagram może być mało czytelny, a całość może się wydać zawiła. Nie ma to jednak nic wspólnego z głównym tematem kursu, czyli STM32L4, więc nie będziemy dokładnie omawiać liczenia sumy CRC. Ograniczymy się do poniższego, gotowego programu, który oblicza sumę kontrolną zgodnie z tym algorytmem. Zainteresowani na pewno sami rozszyfrują działanie tego fragmentu kodu.

Funkcja byte_crc realizuje algorytm zaprezentowany na wcześniejszym diagramie, natomiast wire_crc pozwala na obliczenie sumy kontrolnej dla całego bloku danych. Dzięki temu do naszego testowego kodu możemy dodać obliczanie sumy kontrolnej. Powinno ono dotyczyć 7 bajtów zmiennej rom_code:

Po uruchomieniu tej wersji programu możemy porównać obliczoną sumę kontrolną, którą znajdziemy w zmiennej crc, z tym, co zostało odebrane, czyli z rom_code[7]:

Podgląd odebranych danych oraz obliczonej sumy kontrolnej

Podgląd odebranych danych oraz obliczonej sumy kontrolnej

Jeśli obie wartości są równe (tutaj 0xBA), możemy uznać, że nawiązaliśmy poprawnie komunikację z czujnikiem DS18B20, odebraliśmy dane i są one poprawne (trzeba jednak pamiętać, że każdy uzyska tutaj inną sumę kontrolną). Teraz możemy nareszcie przejść do pomiarów temperatury.

Odczyt zawartości pamięci DS18B20

Wiemy już, jak odczytać adres naszego egzemplarza DS18B20. Pora, aby spróbować odczytać kolejną bardzo ważną informację, która w dokumentacji określana jest jako brudnopis (ang. scratchpad). Pod tą oryginalną nazwą kryje się po prostu zawartość całej wewnętrznej pamięci czujnika (w tym pomiar).

Opis komórek pamięci DS18B20 (fragment dokumentacji)

Opis komórek pamięci DS18B20 (fragment dokumentacji)

Pierwsze dwa bajty są dla nas szczególnie interesujące – to właśnie w nich znajdziemy wynik pomiaru, czyli temperaturę. Niestety, aby upewnić się, że odczyt przebiegał bezbłędnie, musimy odczytać całe 9 bajtów, ponieważ dopiero ostatni zawiera sumę kontrolną.

Każdy czujnik posiada unikalny 64-bitowy adres, czyli tzw. ROM code, który już wcześniej odczytaliśmy. Istnieje możliwość odczytu danych z czujnika wybranego na podstawie adresu, ale jeśli do linii danych jest podłączony tylko jeden czujnik, to nie musimy używać adresu (trzymamy się teraz tej wersji).

W celu odczytania zawartości pamięci czujnika wyślemy komendę Skip ROM o kodzie 0xCC, oznaczającą pominięcie adresowania, a następnie Read Scratchpad o kodzie 0xBE – jest to polecenie, dzięki któremu dokonamy właściwego odczytu – w odpowiedzi czujnik odeśle 9 bajtów swojej pamięci.

Po uruchomieniu tej wersji kodu powinniśmy w debuggerze uzyskać efekt zbliżony do poniższego:

Podgląd zawartości pamięci czujnika DS18B20

Podgląd zawartości pamięci czujnika DS18B20

Jak widać, scratchpad[8] zawiera taką samą wartość jak obliczona przez nas suma kontrolna, więc odebrane wartości są poprawne. Pierwsze dwa bajty to 0x50 i 0x5 (czyli 0x05). To domyślne wartości, dokładnie takie jak we wcześniejszym fragmencie noty katalogowej DS18B20.

Dlaczego DS18B20 zwraca temperaturę 85°C?

Na razie nie rozpoczęliśmy pomiaru, dlatego odczytaliśmy takie wartości, warto jednak zrozumieć, co oznaczają. Jest to wartość 16-bitowa ze znakiem, zapisana w kolejności zgodnej z little-endian, więc sumaryczna wartość tych dwóch bajtów wynosi: 0x0550 = 1360. Wyniki zwracane przez nasze czujniki są wyrażane w 1/16 stopnia Celsjusza – wartość 1360 odpowiada więc 85°C. Jest to dla nas istotna informacja, bo jeśli kiedyś otrzymamy taką wartość, to powinniśmy traktować to jako błąd pomiaru.

Pomiar temperatury DS18B20

Dotychczas odczytywaliśmy tylko domyślne wartości, bo jeszcze nie uruchomiliśmy pomiaru. Czujnik rozpocznie pomiar temperatury dopiero, gdy wyślemy do niego znany nam sygnał reset, a następnie polecenie Skip ROM o kodzie 0xCC oraz polecenie Convert T o kodzie 0x44.

Następnie czujnik dokona pomiaru, co zajmie mu całkiem sporo czasu. Przy domyślnej rozdzielczości, która wynosi 12 bitów, pomiar może zająć nawet 750 ms. Nie pozostaje nam więc nic innego, jak dodać odpowiednie opóźnienie i odczytać później wynik:

Po uruchomieniu tej wersji kodu powinniśmy odebrać komplet nowych danych – tym razem pierwsze dwa bajty powinny zawierać wartości, które będą inne od domyślnych.

Pierwszy pomiar temperatury za pomocą DS18B20

Pierwszy pomiar temperatury za pomocą DS18B20

W naszym przykładzie uzyskaliśmy odpowiednio: 0x9D i 0x1 (0x01), co daje łącznie 0x019D, a gdy przeliczymy tę wartość na system dziesiętny, otrzymamy 413. Podzielenie tego wyniku przez 16 da nam temperaturę w wysokości 25,8125°C, co jest poprawną wartością.

Biblioteka do obsługi 1-wire

Pierwsze eksperymenty z DS18B20 za nami – napisaliśmy trochę kodu, niestety trudnego do ponownego użycia i pełnego magicznych liczb. Warto więc zrobić z tym porządek. Najpierw wydzielimy kod, który dotyczy samego 1-wire. Zaczynamy od pliku wire.h o następującej zawartości:

Włączamy plik stm32l4xx.h, aby mieć dostęp do typu HAL_StatusTypeDef, którego używamy do zwracania wartości HAL_OK lub HAL_ERROR. Dodaliśmy też nową funkcję – wire_init. W testowym kodzie musieliśmy pamiętać o wywoływaniu HAL_TIM_Base_Start, ale znacznie wygodniej będzie zdefiniować funkcję inicjalizującą komunikację przez 1-wire, która zrobi to „za nas”.

Dzięki temu niezależnie od wykorzystywanego modułu sprzętowego program główny musi po prostu wywołać wire_init na początku. Pozostałe funkcje już znamy, kopiujemy więc ich treść do pliku wire.c:

Wszystkie funkcje pomocnicze, które nie są wymienione w pliku wire.h, są oznaczone jako statyczne. Dzięki temu nie będą widoczne poza plikiem wire.c. To bardzo ważne – pozostały kod ma mieć dostęp tylko do tego, co zawiera wire.h; inne funkcje i zmienne powinny być niewidoczne poza plikami tej biblioteki, aby nie generowały problemów (np. konflikty nazw).

Biblioteka do obsługi DS18B20

Teraz ważniejsza część, czyli utworzenie plików ds18b20.h oraz ds18b20.c, a zatem biblioteki do obsługi czujników DS18B20, która będzie korzystała z biblioteki do 1-wire. Jej zawartość bazuje na naszych wcześniejszych eksperymentach, ale przy okazji wprowadzimy nieco poprawek do programu.

Zacznijmy od pliku ds18b20.h:

Podobnie jak w przypadku wire.h, dodajemy funkcję inicjalizującą czujnik, czyli ds18b20_init. Ta funkcja będzie wywoływała wire_init, więc program główny nie będzie musiał nawet wiedzieć, że używamy też biblioteki do 1-wire – patrząc z punktu widzenia końcowego użytkownika, który miałby korzystać z naszej biblioteki, to bardzo wygodne i eleganckie rozwiązanie.

Podczas pierwszych eksperymentów odczytywaliśmy adres naszego modułu. Teraz możemy utworzyć funkcję ds18b20_read_address, która będzie robiła dokładnie to samo. Funkcja zwraca wartość typu HAL_StatusTypeDef, czyli HAL_OK, jeśli odczyt adresu zakończy się sukcesem (wtedy adres zostanie zapisany w buforze przekazywanym jako parametr o nazwie rom_code). Jeśli nie uda się odczytać adresu czujnika, to funkcja zwróci HAL_ERROR.

W celu uniknięcia niezrozumiałych zapisów i magicznych liczb zdefiniowaliśmy też stałą, która określa, ile bajtów zajmuje adres czujnika DS18B20. Następnie mamy dwie funkcje:

  • ds18b20_start_measure – rozpoczyna pomiar temperatury,
  • ds18b20_get_temp – zwraca wynik pomiaru.

W trakcie korzystania z biblioteki nie będziemy mieć dostępu do „brudnopisu”, zamiast tego funkcja ds18b20_get_temp zwróci od razu wynik przeliczony na stopnie Celsjusza. Dodatkowo jako parametr funkcji ds18b20_start_measure i ds18b20_get_temp można podać adres czujnika, czyli taką samą wartość jak odczytana przez ds18b20_read_address. Użyjemy tej opcji, aby odczytywać wyniki z dwóch czujników podłączonych do tego samego pinu. Jeśli jako rom_code podamy NULL, to funkcje powinny działać tak jak w dotychczasowych eksperymentach, czyli komunikując się z jedynym podłączonym układem za pomocą komendy Skip ROM.

Plik ds18b20.c zaczynamy od włączenia plików nagłówkowych oraz definicji używanych stałych:

Wielkość brudnopisu to 9 bajtów, tworzymy więc stałą DS18B20_SCRATCHPAD_SIZE o takiej wartości. Wcześniej do odczytu adresu czujnika używaliśmy kodu 0x33, który odpowiada poleceniu Read ROM, teraz dodajemy odpowiednią stałą DS18B20_READ_ROM.

Podobnie definiujemy kod 0xCC, którego używaliśmy wcześniej. Oznacza on SKIP_ROM, czyli pominięcie adresu i odwołanie do jedynego czujnika podłączonego do interfejsu 1-wire. Jeśli mamy podłączonych więcej czujników, to zamiast SKIP_ROM powinniśmy użyć MATCH_ROM, które pozwoli na adresowanie czujników. Po wysłaniu tego kodu należy przesłać 8 bajtów adresu czujnika, który nas interesuje. Na koniec mamy jeszcze dwa polecenia, z których już korzystaliśmy, czyli CONVERT_T, rozpoczynające pomiar, oraz READ_SCRATCHPAD, odczytujące zawartość pamięci czujnika DS18B20.

Pierwsza funkcja pliku ds18b20.c to ds18b20_init – wywołuje ona po prostu inicjalizację modułu wire:

Jako kolejną możemy napisać funkcję ds18b20_read_address. Jej kod wygląda następująco:

Poza użyciem stałych dodane zostało tutaj sprawdzanie błędów oraz sumy kontrolnej. Sam kod jest identyczny z naszymi wcześniejszymi eksperymentami.

Teraz dla ułatwienia zdefiniujmy funkcję pomocniczą send_cmd. Wcześniej wysyłaliśmy polecenia 0xCC i 0x44, aby rozpocząć pomiar, oraz 0xCC i 0xBE, aby odczytać brudnopis. Kod 0xCC oznacza pominięcie adresu czujnika, natomiast drugi bajt to polecenie dla czujnika. Nasza nowa funkcja będzie dodatkowo pozwalała na użycie adresowania układów:

Najpierw wysyłamy sygnał reset, następnie, jeśli jako rom_code podano NULL, postępujemy jak wcześniej, czyli wysyłamy SKIP_ROM, a jeśli podany został adres, to wysyłamy MATCH_ROM, a po nim wspomniany adres. Na koniec wysyłamy polecenie do wykonania przez czujnik, czyli wartość cmd.

Teraz możemy łatwo zdefiniować funkcję rozpoczynającą pomiar:

Odczyt temperatury składa się z dwóch etapów. Najpierw musimy odczytać zawartość brudnopisu, a potem wyciągnąć z niego temperaturę. Definiujemy więc funkcję pomocniczą:

Jej kod bazuje na naszych wcześniejszych eksperymentach, ale używamy tu send_cmd i sprawdzamy poprawność wyników. Na koniec wystarczy napisać treść ds18b20_get_temp, która odczyta zmierzoną temperaturę (wyciągając dane z brudnopisu) i dokona konwersji wyników na stopnie Celsjusza.

Jak pamiętamy, wynik 85 jest traktowany jako domyślna, czyli niepoprawna wartość, dlatego w razie błędów odczytu pamięci czujnika zwrócimy właśnie taką wartość. 

Wykorzystanie bibliotek do 1-wire i DS18B20

Pisanie bibliotek bywa męczące, ale teraz przechodzimy do najważniejszego, czyli do ich używania. Tu pojawia się ich ogromna zaleta – nie musimy pamiętać o szczegółach zawartych w bibliotece. Jeśli za jakiś czas będziemy po prostu chcieli odczytać temperaturę z DS18B20, to wystarczy, że użyjemy biblioteki – nie musimy od nowa przedzierać się przez dokumentację 1-wire czy samego czujnika.

Czyścimy nasz plik main z testowego kodu i dodajemy najpierw nagłówek nowej biblioteki (nie musimy dodawać nagłówka wire.h, bo wywołanie takie zostało zawarte wewnątrz biblioteki do DS18B20):

Następnie przechodzimy do testowania biblioteki – zacznijmy od nawiązania połączenia i pobrania adresu czujnika, który podłączony jest do mikrokontrolera:

Po uruchomieniu tej wersji kodu pod kontrolą debuggera w tablicy ds1 otrzymamy ten sam adres co odczytany wcześniej. U każdego adres ten powinien być inny – wartość tę należy skopiować, bo przyda nam się podczas dalszych testów. Warto również wyciągnąć aktualny czujnik, podłączyć w jego miejsce drugi czujnik z zestawu i odczytać także jego adres (oba będą później potrzebne). U nas było to:

  • Czujnik 1: 0x28, 0xFF, 0x26, 0xE9, 0x15, 0x20, 0x8A, 0xBA
  • Czujnik 2: 0x28, 0x3C, 0x55, 0xE9, 0x0C, 0x00, 0x00, 0xA8
Odczytanie adresu DS18B20 czujnika za pomocą biblioteki

Odczytanie adresu DS18B20 czujnika za pomocą biblioteki

Teraz możemy przejść do programu odczytującego temperaturę. Jak pamiętamy, najpierw wywołujemy ds18b20_start_measure, aby rozpocząć pomiar, a po 750 ms wywołujemy ds18b20_get_temp, która zwróci nam wynik (lub 85 stopni jako błąd).

W przypadku liczb zmiennopozycyjnych dla bezpieczeństwa nie powinno się raczej używać operatora równości, dlatego podczas testów przyjmujemy, że każdy wynik powyżej 80 stopni jest błędny. Oczywiście to duże uproszczenie – lepszym rozwiązaniem byłoby rozbudowanie funkcji, tak aby zwracała np. kody błędów, gdy pomiar jest niepoprawny.

Musimy tylko włączyć jeszcze obsługę liczb zmiennoprzecinkowych przez printf (Project > Properties > C/C++ Build > Settings > MCU Settings > Use float with printf...). Po uruchomieniu tej wersji programu otrzymamy już poprawną temperaturę.

Efekt działania najnowszej wersji programu

Efekt działania najnowszej wersji programu

W ramach testu warto delikatnie dotknąć czujnik palcem, aby podnieść jego temperaturę i zobaczyć, czy wszystko działa poprawnie (efekt takiego podgrzania widoczny jest na powyższym zrzucie).

Odczyt z wielu DS18B20

Jedną z największych zalet protokołu 1-wire i czujników DS18B20 jest możliwość podłączenia wielu modułów równolegle. Dzięki temu możemy jednocześnie mierzyć temperaturę np. w pokoju oraz za oknem. Pora podłączyć teraz dwa czujniki naraz – wystarczy wpiąć je równolegle.

Schemat ideowy i blokowy połączenia dwóch czujników DS18B20

Schemat ideowy i blokowy połączenia dwóch czujników DS18B20

Pora wykorzystać spisane wcześniej adresy – należy dodać je do programu w formie dwóch tablic. Od razu usuwamy też fragment programu, który odczytywał adres podłączonego czujnika, i zostawiamy tylko inicjalizację biblioteki.

Teraz wystarczy w głównym programie podawać adres czujnika, do którego chcemy się odwołać:

Po uruchomieniu tej wersji programu w terminalu wyświetlane będą pomiary z obu czujników. Podczas testu warto dotknąć jednego z termometrów, aby delikatnie podgrzać go palcem – będzie wtedy od razu wiadomo, czy na pewno wyświetlana jest temperatura z dwóch różnych czujników.

Efekt działania programu odczytującego temperaturę z dwóch DS18B20

Efekt działania programu odczytującego temperaturę z dwóch DS18B20

Cały czas tworzymy programy, których głównym celem jest nauka – możemy więc pozwolić sobie na takie ustępstwa jak umieszczenie adresu czujników w pamięci flash mikrokontrolera. Podczas budowy docelowych urządzeń warto byłoby taki adres zapisać w pamięci EEPROM podłączonej do STM32L4. Można również pokusić się o wykorzystanie pól USER1 i USER2, które dostępne są w pamięci czujnika – więcej informacji na ten temat znaleźć można w dokumentacji DS18B20.

Opóźnienia, czyli kiedy pojawią się problemy

Poznaliśmy już nieco działanie czujników DS18B20, umiemy odczytywać z nich temperaturę i w sumie możemy być zadowoleni z naszych dotychczasowych dokonań, ale… niestety kod, który dotychczas napisaliśmy, zawiera bardzo poważny błąd – działa tylko czasami.

Pewnie wiele osób będzie zaskoczonych, w końcu testowaliśmy program wiele razy, wszystko działało poprawnie, więc skąd to narzekanie? Okazuje się, że faktycznie nasz kod działa poprawnie, ale tylko dopóki mikrokontroler nie robi nic innego – problem pojawi się, gdy rozbudujemy program.

W trakcie omawiania przerwań na STM32L4 udowodniliśmy, że poza głównym programem nasz układ musi obsługiwać właśnie przerwania. Czas na nie poświęcony jest niewielki (jeśli program jest napisany w odpowiedni sposób), ale nie jest on zerowy.

Protokół 1-wire wymaga dokładności na poziomie pojedynczych mikrosekund – w przypadku naszego mikrokontrolera, który taktowany jest zegarem o częstotliwości 80 MHz, bardzo szybkie procedury obsługi przerwań nie będą miały tu znaczenia, ale już wysłanie znaków przez printf, a nawet przerwania od modułów DMA mogą zupełnie uszkodzić program – nie pozostaje nam nic innego, jak ulepszyć bibliotekę i sprawić, aby była bardziej niezawodna.

Wyłączanie przerwań (styl Arduino)

Pierwsze, co możemy zrobić, to całkowite wyłączenie przerwań podczas komunikacji przez 1-wire. Takie rozwiązanie jest popularne chociażby na Arduino UNO. Efekty takiego „brutalnego” rozwiązywania problemów pojawiają się bardzo często w pytaniach na naszym forum o elektronice – okazuje się, że „z nieznanych przyczyn” nagle w programie przestało działać coś, co od zawsze działało poprawnie.

Wyłączanie przerwań wpływa na inne moduły i może być dużo gorsze w skutkach niż błędy, które sprawią, że nie uda nam się odczytać temperatury z DS18B20. Oczywiście, jeśli ktoś bardzo chce, to może na STM32L4 zastosować również taką metodę – potrzebna jest tylko zmiana w wire.c. Wystarczy, że dodamy wywołania __disable_irq oraz __enable_irq do funkcji wire_reset, read_bit i write_bit:

Na szczęście DS18B20 (i 1-wire) pozwala na dość długie przerwy między transmisją kolejnych bitów, więc czasy wyłączania przerwań byłyby krótkie. Nie licząc wire_reset, gdzie wyłączamy przerwania na prawie 1 ms. Takie podejście jest jednak niezbyt dobre.

Wyłączanie przerwań (styl ARM)

Użycie funkcji __disable_irq i __enable_irq jest dość drastycznym rozwiązaniem, bo wyłącza wszystkie przerwania o konfigurowalnym priorytecie, czyli prawie wszystkie. Takie rozwiązanie może spowodować nieprawidłowości w działaniu całego systemu. Moduł przerwań ma rozbudowany system priorytetów. Moglibyśmy więc podzielić przerwania na krytyczne, które działają szybko i nie powinny być wyłączane, oraz te mniej istotne, które wymagają więcej czasu, ale ich opóźnienie jest dopuszczalne.

W celu określenia poziomu przerwań, które mogą być w danej chwili obsługiwane, wykorzystywany jest rejestr o nazwie BASEPRI. Przerwania o przypisanej wartości priorytetu wyższej niż jego wartość (czyli o niższym priorytecie) nie będą obsługiwane.

Dzięki użyciu rejestru BASEPRI moglibyśmy przynajmniej zminimalizować wpływ wyłączania przerwań na działanie układu, a staranne planowanie priorytetów w wielu przypadkach pozwoliłoby uzyskać w pełni funkcjonalny system. My jednak użyjemy tym razem jeszcze innego rozwiązania i zamiast wyłączać przerwania, wykorzystamy peryferie sprzętowe.

Użycie modułu USART do komunikacji po 1-wire?

Dotychczas do komunikacji przez 1-wire używaliśmy zwykłego pinu GPIO i wszystko realizowaliśmy w pełni programowo. Takie rozwiązanie działa poprawnie, dopóki system nie musi wykonywać innych działań. Wykorzystanie dedykowanych modułów sprzętowych rozwiązuje problem, bo wszystkim zajmuje się sprzęt (niejako „w tle”).

Komunikacja po 1-wire może być w dość sprytny sposób realizowana za pomocą modułu sprzętowego, który normalnie odpowiada za realizowanie komunikacji przez UART. Metoda, którą teraz opiszemy, jest jednym z rozwiązań, które proponuje sam producentów czujników DS18B20.

Na czym polega ten „trik”? Nie będziemy korzystać z UART-a w tradycyjny sposób i nie będziemy do czujnika wysyłać tekstów za pomocą printf. Wykorzystamy tutaj fakt, że na liniach danych UART-a podczas transmisji danych generowane są „jakieś” przebiegi – możemy więc tak manipulować tym, co (i z jaką prędkością) wysyłamy przez UART, aby wygenerować przebiegi dla 1-wire.

Producent DS18B20 przykładowo pokazuje, jak wysyłanie sygnału reset ma się do komunikacji przez UART z prędkością 9600 bitów na sekundę. Jak widać, wysyłanie jednego bajtu przez UART z tą prędkością trwa dokładnie tyle, ile jest wymagane przez protokół 1-wire właśnie dla sekwencji reset. Wystarczy więc, że wykorzystamy moduł UART i wyślemy 0xF0 (niższe cztery bity mają wartość 0, wyższe – 1), a na wyjściu dostaniemy dokładnie to, co czujnik DS18B20 zinterpretuje jako sygnał reset.

Wysyłanie sekwencji reset dla 1-wire za pomocą UART-a (materiały producenta)

Wysyłanie sekwencji reset dla 1-wire za pomocą UART-a (materiały producenta)

Z kolei UART przy prędkości 115 200 bitów na sekundę jest w stanie wygenerować przebiegi pasujące do transmisji przez 1-wire bitów o wartości 0 i 1.

Wysyłanie pojedynczych bitów 1-wire za pomocą UART-a (materiały producenta)

Wysyłanie pojedynczych bitów 1-wire za pomocą UART-a (materiały producenta)

Analogicznie jest z odczytem – ten również jest możliwy za pomocą UART-a:

Odczytywanie pojedynczych bitów 1-wire za pomocą UART-a (materiały producenta)

Odczytywanie pojedynczych bitów 1-wire za pomocą UART-a (materiały producenta)

Jak widać, moduł UART może być bez problemu wykorzystywany do komunikacji przez 1-wire, trzeba tylko wykorzystać go w dość nieszablonowy sposób. Dzięki temu unikniemy problemów związanych z wyłączaniem przerwań. Co więcej, możliwe będzie użycie np. DMA do optymalizacji programu.

Implementacja 1-wire przez UART

Pora przetestować to w praktyce. Wracamy do CubeMX i resetujemy stan PC4 (klikamy lewym przyciskiem myszy i wybieramy opcję Reset_state). Następnie uruchamiamy kolejny moduł USART. Tym razem będzie to USART3, który konfigurujemy w tryb asynchroniczny, prędkość transmisji ustawiamy na 9600, do tego ustawiamy opcję Overrun na Disable.

Musimy jeszcze zmienić konfigurację pinu PC4. Domyślnie UART posiada wyjścia działające w trybie przeciwsobnym, czyli push-pull, natomiast 1-wire wymaga wyjść w trybie open-drain. Przechodzimy więc do kategorii System Core, wybieramy moduł GPIO, a w nim zakładkę USART. Zaznaczamy tam pin PC4 i jego tryb, czyli GPIO mode, zmieniamy na Alternate Function Open Drain.

Zmiana trybu pracy dla alternatywnej funkcji GPIO

Zmiana trybu pracy dla alternatywnej funkcji GPIO

UART to interfejs, który posiada jedną linię do nadawania i drugą do odbierania danych. Z kolei 1-wire (jak sama nazwa wskazuje) korzysta z jednej linii do transmisji danych w dwie strony. Musimy więc sprawić, aby sprzętowy UART korzystał z tej samej linii dla nadawania i odbierania. Jak? Najprościej połączyć PC4 oraz PC5 i tak utworzoną magistralę podłączyć do czujników.

Schemat ideowy i montażowy do testu komunikacji 1-wire przez UART

Schemat ideowy i montażowy do testu komunikacji 1-wire przez UART

W programie będziemy musieli zmieniać prędkość działania modułu USART3 w trakcie transmisji, więc dotychczasowa metoda zmieniania prędkości (klikanie w CubeMX) odpada. Na szczęście nie jest to nic skomplikowanego. Zacznijmy od odszukania funkcji MX_USART3_UART_Init w pliku usart.c:

Jak widzimy, prędkość 9600 jest przypisywana do pola huart3.Init.BaudRate – możemy zatem powielić ten fragment kodu i podmieniać „w locie” prędkość, z jaką ma pracować USART3. Teraz możemy już przystąpić do zmian w naszym pliku wire.c. Zaczynamy od włączenia nagłówka usart.h:

Następnie tworzymy funkcję pomocniczą set_baudrate:

Kod tej funkcji to właściwie kopia z MX_USART3_UART_Init – dodaliśmy tylko możliwość ustawiania prędkości komunikacji. Jest to funkcja statyczna, więc powinna znaleźć się w pliku wire.c nad funkcjami, które będą z niej korzystać, czyli wire_reset, write_bit oraz read_bit.

Teraz możemy edytować funkcję wire_reset. Zmiana jest stosunkowo prosta – zamiast „ręcznego” zmieniania stanów GPIO musimy wysyłać lub odbierać „coś” przez UART z odpowiednią prędkością.

Nowe funkcje do wysyłania i odbierania bitów będą wyglądały tak:

I to koniec zmian! Nie musimy robić nic więcej – wystarczy skompilować i wgrać program, a całość powinna zadziałać dokładnie tak samo jak poprzednio. W terminalu powinniśmy widzieć odczyty:

Efekt działania programu wykorzystującego UART do komunikacji przez 1-wire

Efekt działania programu wykorzystującego UART do komunikacji przez 1-wire

Jak widać, moduł UART może służyć nie tylko do przesyłania „Hello world!” – może być wykorzystywany do komunikacji z modułami, które pozornie wcale nie są wspierane przez nasz mikrokontroler. Takie same sztuczki można również robić np. z użyciem SPI i innych modułów.

Udoskonalona obsługa 1-wire przez UART (Half-Duplex)

To jednak nie koniec, bo trochę szkoda marnować aż dwa piny mikrokontrolera na komunikację 1-wire. Producent przewidział tryb pracy UART-a, w którym komunikacja automatycznie przebiega tylko przez jeden pin. W celu przetestowania tej wersji odłączamy przewód od pinu PC5 i wracamy do CubeMX. Idziemy do konfiguracji modułu USART3, wybieramy tryb Single Wire (Half-Duplex), a po tej zmianie upewniamy się, że parametr Overrun jest ustawiony na Disable.

Nowa konfiguracja USART w trybie Half-Duplex

Nowa konfiguracja USART w trybie Half-Duplex

Teraz w pliku wire.c aktualizujemy funkcję zmieniającą prędkość UART-a na poniższą. Oprócz zmiany parametrów kluczowe jest tutaj wywołanie funkcji HAL_HalfDuplex_Init zamiast HAL_UART_Init.

To koniec zmian. Nie trzeba edytować już nic więcej – wystarczy wgrać nową wersję kodu. Wszystko powinno działać dokładnie tak samo jak wcześniej, z tą różnicą, że zaoszczędziliśmy jeden pin. Nasza biblioteka była edytowana i poprawiana wiele razy, więc na sam koniec (zadanie dla chętnych) warto oczyścić ją z nieużywanych funkcji – nie potrzebujemy już np. inicjalizacji TIM6 ani delay_us.

Zadanie domowe

  1. Dodaj do programu obsługę pilota IR. Wciśnięcie klawisza 1 powinno wysyłać do PC temperaturę z pierwszego DS18B20, a naciskanie klawisza 2 powinno wysyłać odczyt z drugiego czujnika.
  2. Dodaj do programu obsługę analogowego czujnika LM35. Temperatura zmierzona przez ten układ powinna być wysyłana do PC po naciśnięciu na pilocie klawisza 3.
  3. Dodaj do programu obsługę LPS25HB. Temperatura zmierzona przez ten układ powinna być wysyłana do PC po naciśnięciu na pilocie klawisza 4. Porównaj działanie wszystkich czujników.

Podsumowanie – co powinieneś zapamiętać?

Termometry DS18B20 przydają się podczas realizacji różnych projektów – warto więc wiedzieć, jak się z nimi obchodzić. Trzeba jednak pamiętać, że podstawowa wersja programu jest podatna na „zakłócenia” od przerwań. Dlatego praktycznie zawsze warto jednak wybierać lepszą, sprzętową obsługę 1-wire, co można bez problemu zrealizować za pomocą UART-a.

Czy wpis był pomocny? Oceń go:

Średnia ocena 5 / 5. Głosów łącznie: 6

Nikt jeszcze nie głosował, bądź pierwszy!

Artykuł nie był pomocny? Jak możemy go poprawić? Wpisz swoje sugestie poniżej. Jeśli masz pytanie to zadaj je w komentarzu - ten formularz jest anonimowy, nie będziemy mogli Ci odpowiedzieć!

W kolejnej części kursu zajmiemy się krótkim podsumowaniem, będzie to również pora na ostatni quiz, dzięki któremu sprawdzisz, ile zapamiętałeś z tego kursu STM32L4. Będzie to także dobre miejsce na wszystkie komentarze na temat całego kursu oraz sugestie dotyczące zagadnień, które powinniśmy opisać w przyszłości.

Nawigacja kursu

Główny autor kursu: Piotr Bugalski
Współautor: Damian Szymański, ilustracje: Piotr Adamczyk
Oficjalnym partnerem tego kursu jest firma STMicroelectronics
Zakaz kopiowania treści kursów oraz grafik bez zgody FORBOT.pl

1-wire, DS18B20, kursSTM32L4, stm32l4

Trwa ładowanie komentarzy...