Pomiar odległości jest wykorzystywany w codziennym życiu chociażby w czujnikach parkowania w samochodach. Robotom z kolei czujniki odległości pomagają przeżyć gąszczu przeszkód np. po to aby dron nie roztrzaskał sobie śmigieł o słup czy ścianę. Przyjżyjmy się bardzo taniemu dalmierzowi ultradźwiękowemu HC-SR04 – w jaki sposób działa oraz jak optymalnie go używać.
Dalmierz ultradźwiękowy HC-SR04
Czujnik ultradźwiękowy HC-SR04 zasilany jest napięciem 5V przy poborze prądu ok 15 mA w stanie aktywnym. Odległość jaką potrafi zmierzyć mieści się w zakresie 2÷400 cm. Stożek pomiarowy wynosi 15° stąd najlepsze rezultaty dla maksymalnej odległości uzyskuje się przy obiekcie większym niż 0,5 m². Im bliżej obiekt się znajduje tym może być oczywiście mniejszy.
Moduł do pomiaru odległości wykorzystuje ultradźwięki o częstotliwości 40 kHz które to są już daleko poza pasmem słyszalnym dla człowieka. Czujnik zbudowany jest z dwóch elementów ultradźwiękowych – nadajnika i odbiornika. Schemat działania każdego z pomiarów jest nastęujący:
- Na pin TRIG w celu rozpoczęcia pomiaru musi być podany stan wysoki trwający conajmniej 10 µs.
- Moduł startuje timer i wysyła 8 impulsów ultradźwiękowych.
- Czeka, aż odbita wiązka wróci do odbiornika, który jest tuż obok transmitera.
- Gdy wykryje sygnał odbity stopuje licznik.
- Łączny czas lotu impulsu ultradźwiękowego odzwierciedlony jest przez długość impulsu na pinie ECHO.
Tak to wygląda na analizatorze.
Jak wyliczyć odległość? Wzór jest dosyć prosty: Odległość = Prędkość propagacji fali * czas. Czas mamy zmierzony za pomocą licznika. Należy pamiętać, że jest to czas, który fala ultradźwiękowa potrzebuje, aby przebyć drogę tam i z powrotem. Prędkość propagacji dla powietrza wynosi 343 m/s, czyli 34300 cm/s chcąc uzyskać wynik w centymetrach. Czas podawany przez czujnik to µs. Aby uzyskać sekundy należy prosto podzielić przez 1000000. W ostatecznym rozrachunku Odległość[cm]= Czas[µs] / 2 * 0,0343[cm/µs]. Można też policzyć prościej wychodząc od tego w jakim czasie fala pokonuje jeden centymetr i jest to ok 29 µs/cm. Wzór będzie wtedy następujący: Odległość[cm]= Czas[µs] / 58[µs/cm]. Przy czym w tym wypadku nie będziemy znali części dziesiętnych centymetra, ale unikniemy float’a w obliczeniach bo Ile kosztuje używanie float i co daje FPU?. W bibliotece dałem możliwość wyboru sposobu obliczeń poprzez definicje #define HCSR04_HIGH_PRECISION która to odkomentowana wybiera obliczenia na float.
Schemat i Cube
Do testów wybrałem Nucleo STM32L476RG z jednego z moich zestawów. Jako, że czujnik działa pod napięciem 5 V należy uważać jak się go podłącza! Zwłaszcza pin Echo, który jest odpowiedzią z układu i będzie on miał poziom 5V w stanie wysokim. STM32 działa w domenie 3,3 V, ale na szczęście część z jego pinów jest 5V tolerant o czym pisałem we wpisie Dlaczego STM32?. Należy sprawdzić w dokumentacji do których można bezpiecznie się podłączyć. Ja wybrałem piny PA6(5 V tolerant) na ECHO oraz PB0(3,6 V tolerant) dla Trig. Napięcie 3,3 V podane z GPIO będzie wystarczające do wysterowania Trigger’a.
Na początku sprawdźmy proste, blokujące działanie. Do tego celu przyda się jakiś timer, z którym będziemy w stanie pracować na mikrosekundach. Maksymalna odpowiedź z HC-SR04 może wynosić około 4 ms, więc licznik 16-bitowy TIM3 będzie aż nadmiarowy, ale go wezmę. Częstotliwość główną HCLK ustawiłem na 48 MHz. W celu uzyskania mikrosekundowego ticku timer’a wystarczy podzielić tą częstotliwość przez 48. W tym celu ustawiam preskaler na 47. Zmieniam również maksymalną wartość do której może zliczyć licznik. Ustawiam na największą możliwą – 65535. Zliczanie zostawiam domyślne – w górę. Nie aktywuję żadnych przerwań.
Pomiar blokujący
W “bieda edyszyn” jak na większości przykładów dla popularnego Arduino mikrokontroler będzie oczekiwał na zakończenie nadawania sygnału ECHO z czujnika aby podać długość impulsu.
Funkcja inicjująca przyjmuje wskaźnik do timer’a skonfigurowanego wcześniej na mikrosekundowy tick .
HCSR04_STATUS HCSR04_Init(TIM_HandleTypeDef *htim);
Funkcja ta ma za zadanie wystartowanie licznika oraz ustawienie w stan niski pinu TRIG.
Drugą użytkową funkcją jest pomiar dystansu który zwraca zmierzoną odległość w centymetrach pod wskaźnik na float.
HCSR04_STATUS HCSR04_Read(float *Result);
Funkcja ta ustawia na 10 µs pin TRIG po czym czeka na pojawienie się stanu wysokiego na pinie ECHO. Gdy ten się pojawi, zeruje timer i w pętli czeka na stan niski na ECHO. Po zakończeniu sygnału echa dokonywane jest przeliczenie i wynik zostaje wpisany pod wskazaną zmienną. Jak widzisz działanie jest proste. Niestety mikrokontroler tak na prawdę niepotrzebnie czeka bezczynnie na zmiany stanu pinu ECHO. Może to nie jest spory czas, ale zależy on od mierzonej odległości. Dla małych dystansów łączny czas obsługi pomiaru będzie wynosił około milisekundy, ale dla większych będzie on sporo dłuższy co widać na poniższych przebiegach.
Jak zrobić to lepiej?
Nieblokująca obsługa HC-SR04
Do nieblokującej obsługi czujnika wykorzystam tryby, które w STM32 oferują nam timer’y. Do wyzwalania pomiaru posłuży mi PWM o częstotliwości ok 15 Hz(okres = 65535 mikrosekund) z czasem trwania stanu wysokiego 11 µs(0,022%). Niektóre źródła podają, że 20 Hz to maksymalny refreshrate dla tego czujnika. Nie mogłem doszukać się w oficjalnych dokumentach, więc przyjmuję taką wartość za prawidłową. Jednak dla ułatwienia skorzystania z jednego licznika(tick 1µs) do kompletnej obsługi pomiaru będzie wyżej wspomniane 15 Hz.
Do łapania sygnału ECHO posłuży mi tryb Input Capture, który złapie mi obydwa zbocza na pinie ECHO. Sygnał z tego pinu będzie doprowadzony ficzycznie do CH1 TIM3. Kanał ten będzie zatrzaskiwał wartość licznika gdy zauważy zbocze narastające na pinie PA6, czyli złapie wartość licznika w momencie startu impulsu pomiarowego. Zbocze opadające, czyli koniec pomiaru będzie wykrywał CH2 TIM i zatrzaskiwał wartość licznika w odpowiednim rejestrze. W konfiguracji STM32 będzie on wewnętrznie połączony z fizycznym pinem CH1 czyli PA6. Dzięki temu mogę rozróżnić i obsłużyć dwa różne zdarzenia na jednym pinie.
Przerwanie wywołuję jedynie po zatrzaśnięciu wartości przy zboczu opadającym. W obsłudze przerwania odejmuję wartość rejestru odpowiedzialnego za koniec pomiaru od rejestru trzymającego wartość z początku pomiaru. Wykorzystując pełen zakres 16 bitów timera nie muszę przejmować się ewentualnym przepełnieniem licznika między początkiem a końcem. Arytmetyka 16-bitowa elegancko obsługuje takie przypadki na liczbach uint16_t. Różnicę tę wstawiam do równania z początku postu i mam już obliczoną odległość przeszkody od czujnika.
Czas który potrzebuje teraz CPU na obsługę czujnika jest wręcz znikomy. Sam zobacz.
Pomiar jak mówiłem wykonywany jest z częstotliwością ok 15 Hz.
Dla zachowania kompatybilności z biblioteką blokującą odczyt po prostu zwraca zmienną biblioteczną do ktorej przerwanie wrzuca wynik swojego działania.
Do pliku nagłówkowego dorzuciłem definicje kanałów timer’a skonfigurowanych na potrzeby czujnika. Jeśli użyjesz innych – zmień je.
Pamiętaj również o dodaniu obsługi przerwania do HAL’owej funkcji _weak.
/* USER CODE BEGIN 4 */ void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { HCSR04_TIM_IC_CaptureCallback(htim); } /* USER CODE END 4 */
Czujnik stojąc przed ścianą ma pewne wahania. Można byłoby je wyeliminować za pomocą jakiegoś filtrowania. Wystarczyłaby prosta średnia krocząca z kilku ostatnich pomiarów których jak pamiętasz mamy ok 15 na sekundę.
Podsumowanie
Jak widać warto korzystać z możliwości sprzętowych jakie dają STM’y. Dzięki temu mikrokontroler może zajmować się czymś sensowniejszym niż puste oczekiwanie na zakończenie pomiaru. Możnaby się pokusić jeszcze o implementację filtru np. Kalmana, aby wyeliminować wahania pomiarów przy tak częstym ich wykonywaniu chociaż z moich obszerwacji na terminalu, nie było tak źle.
Jeżeli czujnik Ci się spodobał, możesz go nabyć u mnie w sklepie.
Projekty z wersją blokującą oraz nieblokującą znajdziesz na moim GitHubie: blokujący, nieblokujący
Jeśli zauważyłeś jakiś błąd, nie zgadzasz się z czymś, chciałbyś coś dodać istotnego lub po prostu uważasz, że chciałbyś podystkutować w tym temacie, napisz komentarz. Pamiętaj, że dyskusja ma być kulturalna i zgodna z zasadami języka polskiego.
8 komentarzy
Patryk · 21/03/2023 o 06:59
Cześć, świetna robota.
Mam też pytanie, skąd mamy pewność że “start” pomiaru nie zacznie się tuż przed przepełnieniem licznika, a “stop” po rozpoczęciu zliczania już od 0 (tj. stop < start). ?
Mateusz Salamon · 21/03/2023 o 09:00
Zapewniając kilka warunków nie musimy się tym martwić 🙂 Korzystamy z pełnego zakresu 2^n, oraz z liczb bez znaku. Wtedy nie ma znaczenia “przejście” przez zero – arytmetyka jest naszym sprzymierzeńcem.
patrgk · 13/04/2020 o 19:41
Mam pytanko odnośnie trybu nieblokującego, jak ustawić “wewnętrznie” w cube żeby jeden pin obsługiwał dwa kanały? Nie znalazłem nic takiego na internecie a ciekawość mnie zżera 😀
Mateusz Salamon · 13/04/2020 o 19:48
Musisz ustawić kanał jako Indirect. Wtedy kanał ten będzie obsługiwany z odpowiedniego pinu sąsiedniego.
SaS · 27/03/2020 o 19:51
Właśnie, nieblokujący, termin obcy w “Srajduino”. W przykładach użycia HC-SR04 na Srjduino używane jest “pulseIn”. Sposób napisania tej funkcji woła o pomstę do nieba więc wymiana srajduino z wersji AVR na ARM nic nie daje, para idzie w gwizdek.
Dobrze, ze pokazujesz jak używać sprzętu aby CPU nie kręcił się w delay-ach.
Mateusz Salamon · 27/03/2020 o 20:18
Ooooj jak widzę te implementacje na pulseln to mną trzepie 😀 Generalnie masa Arduino jest pisana na delayu… ale walczę z tym właśnie na moim blogu 🙂 Ja delaya uznaję tylko w testach na szybko i w inicjalizacji 😀
Michał · 10/01/2019 o 18:46
Git 🙂
Mateusz · 10/01/2019 o 19:08
Wiadomo, że git 🙂