Ultradźwiękowy czujnik odległości HC-SR04

Ultradźwiękowy czujnik HC-SR04 jest jednym z najczęściej stosowanych przez hobbystów czujników odległości w urządzeniach i robotach. Swoją popularność zawdzięcza dość niskiej cenie przy stosunkowo dużym zasięgu. W artykule postaram się przedstawić budowę i zasadę działania czujnika oraz przykład programu odczytującego dane zaimplementowanego na zestawie Nucleo-L476RG.

Budowa i zasada działania

HC-SR04 to ultradźwiękowy czujnik odległości, czyli czujnik, który do pomiaru wykorzystuje falę dźwiękową o częstotliwości niesłyszalnej dla człowieka. Taką częstotliwość wykorzystują często zwierzęta np. pies, delfin czy nietoperz. Ultradźwięki dzięki małej długości fali wykorzystywane są przez człowieka m.in. w sonarach okrętów podwodnych (do detekcji obiektów) oraz medycynie.

Czujnik HC-SR04 wykorzystuje falę dźwiękową o częstotliwości 40 kHz. Zbudowany jest z nadajnika i odbiornika oraz zestawu układów elektronicznych, które wstępnie przetwarzają sygnał generowany przez nadajnik oraz przychodzący do odbiornika w taki sposób, aby wyprowadzić nam sygnał możliwie łatwy do odczytu. Czujnik standardowo ma wyprowadzone cztery piny: VCC (zasilanie), GND (masa), TRIG (wejście wyzwalające pomiar) oraz ECHO (wyjście zwracające wartość pomiaru).

Interfejs czujnika (wejście TRIG i wyjście ECHO) bazuje na sygnałach czasowych. Aby wywołać pomiar, należy podać na pin TRIG stan wysoki o czasie trwania minimum 10 us. Sygnał ten zamieniany jest na osiem impulsów o częstotliwości 40 kHz i generowany przez sondę nadawczą. Jeżeli fala dźwiękowa napotka przeszkodę na swojej drodze, odbija się od niej i wraca do odbiornika. Czujnik na wyjściu ECHO generuje impuls o szerokości proporcjonalnej do zmierzonej odległości.

Pomiar czasu trwania impulsu na wyjściu ECHO pozwala nam na odczytanie, w jakiej odległości od czujnika znajduje się przeszkoda. Zamiana czasu trwania impulsu na odległość podaną w centymetrach sprowadza się do prostych obliczeń matematycznych. Czas impulsu jest bowiem ściśle związany z czasem wędrowania fali dźwiękowej do przeszkody i z powrotem. Wiedząc, że fala dźwiękowa porusza się z prędkością 340 m/s, możemy obliczyć odległość, jaką przebyła. Ponieważ interesuje nas odległość do przedmiotu, musimy podzielić drogę przebytą przez falę dźwiękową przez 2.

odległość_m = (340 m/s * czas_impulsu_s) / 2

Możemy ten wzór teraz przekształcić do bardziej użytecznej formy w przypadku mikrokontrolerów, gdzie będziemy się raczej posługiwać centymetrami (ewentualnie milimetrami) oraz mikrosekundami.

odległość_cm = 100/1000000 * (340 m/s * czas_impulsu_us) / 2

Upraszczając wzór otrzymujemy postać:

odległość_cm = czas_impulsu_us * 0,017 = czas_impulsu_us / 58

Wzór ten odpowiada formule zamieszczonej w dokumentacji czujnika.

Konfiguracja mikrokontrolera

Odczyt informacji z czujnika zaimplementujemy dla mikrokontrolera STM32L476RG na zestawie Nucleo-L476RG w środowisku STM32CubeIDE.

Tworzymy zatem nowy projekt wybierając „File->New->STM32 Project”. Przechodzimy przez wstępną konfigurację projektu i zabieramy się za konfigurację wyjść mikrokontrolera. Ja wygenerowałem projekt z domyślną konfiguracją dla płytki Nucleo, dlatego część pinów mam już skonfigurowane. Do obsługi HC-SR04 będziemy potrzebowali dwóch pinów – wyjścia do wywoływania pomiaru (pin TRIG) oraz wejścia do odczytu impulsu z informacją o odległości. Ponieważ pomiar chcemy wykonywać okresowo, czyli co określony czas generować impuls o szerokości 10 us, jako wyjście TRIG skonfigurujemy PB10 w trybie PWM. Obsługę odczytu z czujnika można zrealizować natomiast na kilka sposobów. Do odczytu danych z pinu ECHO możemy wykorzystać:

  • wejście cyfrowe z przerwaniem i zliczać Timerem czas między zboczem narastającym a opadającym
  • tryb Input Capture Timera, który zareaguje na zbocze narastające, a następnie na opadające i obliczyć czas na podstawie różnicy odczytów z obu zdarzeń
  • tryb PWM Input, który jest swego rodzaju odmianą tryb Input Capture – wykorzystuje dwa sygnały Input Capture zmapowane na ten sam pin i pozwala sprzętowo obliczyć czas trwania impulsu oraz czas między kolejnymi impulsami (dedykowany do pomiaru sygnału PWM)

Najciekawszym rozwiązaniem wydaje się wykorzystanie trybu PWM Input. Co prawda w przypadku czujnika HC-SR04 nie mamy do czynienia z klasycznym sygnałem PWM, a jedynie z impulsem, ale tryb ten pozwoli nam na łatwy odczyt czasu trwania impulsu bez potrzeby zmiany zbocza, na jaki ma reagować. Wykorzystamy tylko jedną z informacji – wypełnienie – ponieważ czas między impulsami niezbędny do obliczenia okresu PWM nie będzie nam potrzebny.

Jak działa tryb PWM input? Informacji poszukajmy w dokumentacji „Reference Manual” na stronie 936 (wykorzystamy Timer 1). Zasadę działania tego trybu wyjaśnia nam poniższy diagram.

Jak wspomniałem, PWM Input jest odmianą tryb Input Capture, jednak wykorzystuje dwa sygnały Input Capture zmapowane na ten sam pin. Każdy z sygnałów jest aktywny na inne zbocze – pierwszy na zbocze narastające, a drugi na opadające. W momencie wywołania pierwszego zbocza licznik jest resetowany. Gdy nastąpi zbocze opadające pobierana jest informacja o liczniku i zapisywana w rejestrze CCR2 (kanał 2). W momencie wystąpienia kolejnego zbocza narastającego, w rejestrze CCR1 zapisywana jest informacja o okresie sygnału. Nam w przypadku pomiaru czasu trwania impulsu z HC-SR04 potrzebna będzie tylko informacja z kanału 2.

Przechodzimy zatem do konfiguratora i wybieramy z zakładki po lewej stronie TIM1. Zaznaczamy Clock Source jako Internal Clock, a tryb PWM Input aktywujemy w oknie Combined Channels -> PWM Input on CH1.

W ten sposób skonfigurujemy pin PA8. Ponieważ czujnik HC-SR04 pracuje z napięciem 5 V, powinniśmy w tym miejscu sprawdzić czy nasze wejście będzie współpracowało (czy jest 5 V tolerant). Informację o tym możemy uzyskać w Datasheet mikrokontrolera w tabeli 16. „STM32L476xx pin definitions” na stronie 84.

Widzimy w niej symbol FT, co oznacza „5V tolerant I/O”. Teraz możemy przejść do konfiguracji rejestrów Timera. W przypadku mikrokontrolera STM32L476RG (dla innych układów mogą nieznacznie się różnić) mamy możliwość skonfigurowania następujących ustawień TIM1 :

  • Counter Settings – ogólnie ustawienia timer-a:
    • Prescaler – dzielnik zegara, wartość 16-bitowa, należy pamiętać, że wpisujemy tutaj wartość, jaką chcemy osiągnąć pomniejszoną o 1, czyli np. dla 100 wpisujemy 99 (wynika to z faktu, że w momencie kasowania zawartości rejestru przeskakuje nam dodatkowy takt zegara, bo liczmy od 0
    • Counter Mode – tryb zliczania: w górę (Up) lub w dół (Down)
    • Counter Period (AutoReload Register) – wartość, do jakiej (w tybie Up) lub od jakiej (w trybie Down) nasz licznik będzie liczył
    • Internal Clock Division – dodatkowy dzielnik zegara używany przez filtry
    • Repetition Counter (RCR) – określa szybkość aktualizacji rejestrów porównawczych, gdy Auto-Reload Preload jest włączony
    • Auto-Reload Preload – bit decydujący, czy zawartość rejestru ARR, gdzie przechowywana jest wartość do której zlicza licznik, ma być buforowana (wpisana do rejestru na stałe, czy tylko w przypadku zdarzenia od zakończenia liczenia)
  • Trigger Output (TRGO) – konfiguracja wyjścia wyzwalającego dla innych liczników
  • PWM Input CH1
    • Input Trigger – wejście pomiarowe
    • Slave Mode Controller – tryb pracy slave
    • Parameters for Channel X – parametry dla kanału X
      • Polarity Selectrion – informacja, na jakie zbocze kanał powinien reagować
      • IC Selection – tryb pracy kanału (Direct, jeśli wejście jest na tym kanale, Indirect, jeśli na innym)
      • Prescaler Division Ratio – współczynnik podziału preskalera dla kanału
      • Input Filter –  określa długość próbkowania wejść sygnałów przed aktualizacją stanu licznika

Najpierw powinniśmy dobrać częstotliwość pracy Timera 1. Aby skonfigurować licznik, musimy odpowiednio ustawić wartości: Prescaler i Counter Period. Wartość w Polu Counter Period możemy ustawić na maksymalną, dzięki czemu zakres pracy licznika będzie szeroki i nie przepełni się nam tak łatwo. Wartość Prescaler-a dobierzemy natomiast w taki sposób, ale wartość w liczniku (CNT) była od razu zwracana w mikrosekundach.

Potrzebujemy zatem informacji o częstotliwości taktowania Timera 1. Zgodnie z dokumentacją (Datasheet) mikrokontrolera na stronie 17, TIM1 podłączony jest do szyny taktującej APB2 i zgodnie z konfiguracją zegara, taktowany będzie z częstotliwością 80 MHz.

Aby uzyskać częstotliwość 1 MHz (czyli okres 1 us) musimy podzielić zegar przez 80. W Counter Period ustawiamy wartość 65535 (max. dla 16 bitów). Tryb pracy Timera konfugurujemy jako zliczanie w górę (Up). Następnie Dla kanału pierwszego wybieramy Rising Edge i Direct, a dla kanału drugiego Falling Edge i Indirect. Resztę parametrów zostawiamy domyślnie.

Będziemy wykorzystywali przerwania, dlatego w zakładce NVIC Settings zaznaczamy TIM1 capture compare interrupt.

Teraz musimy skonfigurować jeszcze sygnał PWM na wyjściu PB10 (TRIG). Należy tutaj pamiętać, że pomiar odległości przy użyciu czujnika ultradźwiękowego może chwile trwać, dlatego zaleca się, aby w przypadku czujnika HC-SR04 o zasięgu do 200 cm nie wywoływać pomiaru częściej niż co 60 ms, ponieważ sygnał zwrotny nowego impulsu może nałożyć się z poprzednim, co zakłóci pomiar. Dlatego nasz sygnał powinien mieć okres 60 ms, czyli częstotliwość ok 16 Hz.

Aby skonfigurować nasz licznik, musimy odpowiednio ustawić wartości: Prescaler i Counter Period. Częstotliwość sygnału PWM na podstawie tych wartości oraz częstotliwości taktowania Timer-a oblicza się z poniższego wzoru:

PWM_Freq = Timer_Freq / (Prescaler * Counter Period)

Częstotliwość taktowania Timer-a 2, którego używamy w projekcie, możemy odczytać z zakładki Clock Configuration. Aby tego dokonać, potrzebujemy informacji o tym, do jakiej szyny podłączony jest Timer 2. Możemy to sprawdzić w dokumentacji (Datasheet) mikrokontrolera na stronie 17.

Jak możemy zauważyć, Timer 2 podłączony jest do szyny APB1. W zakładce Clock Configuration można odczytać, że jeżeli zegar główny (HCLK) ma ustawioną maksymalną częstotliwość dostępną dla tego mikrokontrolera, czyli 80 MHz, to szyna (a zatem również nasz licznik) jest taktowana z częstotliwością 80 MHz.

W polu Prescaler ustawimy wartość 80. Chcąc zatem, aby sygnał PWM miał częstotliwość 16 Hz, Counter Period powinien mieć wartość:

80 000 000/(16 * 80) = 62 500

Pozostały nam do skonfigurowania pozostałe istotne parametry licznika. Chcemy aby nasz licznik zliczał w górę (Counter Mode -> Up), wykorzystamy tryb 1 PWMbez trybu Fast oraz z polaryzacją High. Jak opisywałem wcześniej, wartość Pulse określa wypełnienie PWM. Chcemy aby generowany był sygnał prostokątny o czasie trwania 10 us. Ponieważ wartości Prescalera dobrałem tak, aby timer zliczał w mikrosekundach, w polu Pulse ustawiamy wartość 10. Pozostałe parametry nie są dla nas istotne i możemy je zostawić z wartością domyślną. Pełna konfiguracja Timer-a 2 oraz kanału 3 wygląda w następujący sposób.

Skonfigurowane piny będą wyglądały jak poniżej. Możemy też ustawić etykiety (prawy przycisk myszy + Enter User Label) – TRIG i ECHO.

Przy tak skonfigurowanych peryferiach możemy wygenerować projekt („Project->Generate Code” lub „Alt+K„) i przejść do napisania kodu programu.

Implementacja

Na początku stworzymy dwa pliki do obsługi czujnika: hc_sr_04.c (w folderze Core->Src) oraz hc_sr_04.h (w folderze Core->Inc), które będą stanowiły naszą bibliotekę.

W pliku hc_sr_04.h stworzymy strukturę, która będzie przechowywała informację o używanych timerach oraz zmierzoną odległość. Dodajemy również deklaracje funkcji i typdef-a.

typedef uint32_t TIM_Channel;

struct us_sensor_str
{
	TIM_HandleTypeDef *htim_echo;
	TIM_HandleTypeDef *htim_trig;
	TIM_Channel trig_channel;

	volatile uint32_t distance_cm;
};

void hc_sr04_init(struct us_sensor_str *us_sensor, TIM_HandleTypeDef *htim_echo, TIM_HandleTypeDef *htim_trig, TIM_Channel channel);
uint32_t hc_sr04_convert_us_to_cm(uint32_t distance_us);

W pliku hc_sr_04.c tworzymy funkcję inicjalizującą oraz przeliczającą dane z us na cm.

#define HC_SR04_US_TO_CM_CONVERTER	58

void hc_sr04_init(struct us_sensor_str *us_sensor, TIM_HandleTypeDef *htim_echo, TIM_HandleTypeDef *htim_trig, TIM_Channel trig_channel)
{
	us_sensor->htim_echo = htim_echo;
	us_sensor->htim_trig = htim_trig;
	us_sensor->trig_channel = trig_channel;

	HAL_TIM_IC_Start_IT(us_sensor->htim_echo, TIM_CHANNEL_1 | TIM_CHANNEL_2);
	HAL_TIM_PWM_Start(us_sensor->htim_trig, us_sensor->trig_channel);
}

uint32_t hc_sr04_convert_us_to_cm(uint32_t distance_us)
{
	return (distance_us / HC_SR04_US_TO_CM_CONVERTER);
}

W funkcji init() startujemy timer ECHO w trybie przerwań, ponieważ będziemy wykorzystywali zdarzenie od opadającego zbocza impulsu. W funkcji przeliczającej zwracamy na podstawie zmierzonego czasu trwania impulsu odległość w cm zgodnie z przeliczeniami przedstawionymi w pierwszej części artykułu.

W pliku main.c tworzymy zmienną struct us_sensor jako globalną.

/* USER CODE BEGIN PV */
struct us_sensor_str distance_sensor;
/* USER CODE END PV */

Następnie piszemy obsługę przerwania od zdarzenia Input Capture. Pobieramy w niej licznik z kanału 2 i przeliczamy czas na odległość.

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
	if(TIM1 == htim->Instance)
	{
		uint32_t echo_us;

		echo_us = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2);
		distance_sensor.distance_cm = hc_sr04_convert_us_to_cm(echo_us);
	}
}

W funkcji main() wywołujemy inicjalizację struktury czujnika, która jednocześnie wywoła nam generowania impulsu na kanale 3 timera 2, czyli wywołanie pomiaru co 60 ms. Pętla while(1) będzie pusta.

/* USER CODE BEGIN 2 */
  hc_sr04_init(&distance_sensor, &htim1);
/* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */

Na koniec załączamy plik hc_sr04.h w pliku main.h.

/* USER CODE BEGIN Includes */
#include "hc_sr04.h"
/* USER CODE END Includes */

Przed uruchomieniem projektu powinniśmy jeszcze połączyć sterownik i enkoder z zestawem Nucleo i podłączyć silnik. Robimy to według poniższego schematu.

Teraz możemy skompilować kod (Project->Build Project) i go uruchomić (Run->Run). Aby podejrzeć odczyt z czujnika bez dodawania do projektu wyświetlacza, wykorzystamy narzędzie STM32CubeMonitor. Jest to dość rozubudowany program do akwizycji danych. Dzisiaj przedstawię tylko prostą konfigurację prowadzącą do ustawienia podglądu zmiennej w widżecie typu Gauge.

Po uruchomieniu programu otrzymujemy ekran startowy. Tworzymy schemat przepływu danych analogicznie to przedstawionego poniżej. Do prezentacji danych wybieramy obiekt „gauge”. Ponieważ widżet ten może przyjmować jako element wejściowy tylko jedną zmienną, a blok Processing przekazuje na wyjściu tablicę ze zmiennymi, pomiędzy Processing, a Gauge dodajemy blok Single value.

Następnie konfigurujemy zmienne, jakie chcemy wyświetlić. Klikamy dwa razy w pole „myVariables” i ustawiamy plik „Executable” klikając podając ścieżkę i plik „*.elf”. Wybieramy zmienną distance_sensor.distance_cm. Następnie w polu „my_Probe_Out” i „myProbe_In” ustawiamy nasz programator. W polu Variable_Filter jako varfilter wybieramy zmienną do wyświetlania. W konfiguracji myGauge możemy jeszcze dobrać jednostki do wyświetlania oraz progi dla kolorów, dzięki czemu nasz wykres będzie się zmieniał w zależności od zmierzonej odległości. Zatwierdzamy wszystko przyciskiem „Deploy” i otwieramy klikając w „Dashboard”. Na ekranie pojawi nam się okno z wykresem. Aby rozpocząć pomiary należy kliknąć przycisk START ACQUISITION.

Podsumowanie

Ultradźwiękowy czujnik HC-SR04 jest jednym z najczęściej stosowanych przez hobbystów czujników odległości. Dość duży zasięg sprawia, że chętnie stosujemy go w robotach mobilnych, ale także w takich urządzeniach, jak czujnik parkowania w garażu czy do aktywowania oświetlenia w przypadku przechodzącej osoby. Chociaż jego obsługa nie jest skomplikowana, ze względu na krótkie czasy trwania impulsów może sprawić problemy – zwłaszcza początkującym robotykom. Mam nadzieję, że artykuł przybliżył wam zasadę działania czujnika i sposób, w jaki można zaimplementować odczyt odległości.

Do pobrania

Repozytorium GitHub

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *