Wracamy do tematu liczników w STM32L4. Tym razem wykorzystamy je do obsługi wyświetlaczy 7-segmentowych oraz do mierzenia odległości za pomocą czujnika ultradźwiękowego HC-SR04.
Przy okazji w ramach ciekawostki użyjemy też jednego ze wzmacniaczy, który wbudowany jest wewnątrz mikrokontrolera STM32L476RG.
Zaczniemy od wykorzystania liczników do mierzenia czasu trwania impulsów. Przykładem będzie tu HC-SR04, czyli popularny ultradźwiękowy czujnik odległości. Następnie skorzystamy z liczników do reprezentowania cyfr na wyświetlaczach 7-segmentowych, dzięki czemu stworzymy prosty dalmierz, który w całości będzie działał w przerwaniach. Na koniec dodamy do projektu obsługę analogowego czujnika temperatury, którego sygnał przepuścimy przez wzmacniacz, aby docelowo wykorzystać go do poprawy wskazań czujnika odległości (temperatura powietrza wpływa na prędkość dźwięku).
HC-SR04 – ultradźwiękowy czujnik odległości
Czujniki ultradźwiękowe spotkać można w wielu przemysłowych zastosowaniach (np. w roli czujników parkowania). Sensory takie są również popularne w hobbystycznych projektach – głównie ze względu na stosunkowo niską cenę gotowych modułów.
Niezależnie od ceny i zastosowania sposób obsługi tego typu czujników przeważnie jest taki sam, więc umiejętność opisana w tej części będzie bardzo uniwersalna. Na potrzeby tego kursu zdecydowaliśmy się wybrać moduł HC-SR04 – głównie ze względu na jego popularność wśród hobbystów i studentów.
Przykładowy moduł HC-SR04 (niektóre wersje modułu mogą wyglądać trochę inaczej)
Czujnik posiada cztery wyprowadzenia: masę, zasilanie (5 V), wejście wyzwalające pomiar (opisywane jako Trig, od angielskiego trigger) oraz pin, z którego odczytamy wynik (opisywany jako Echo). Działanie czujnika najłatwiej zrozumieć za pomocą poniższej ilustracji.
Przebiegi związane z pomiarem odległości za pomocą czujnika HC-SR04
Pomiar rozpoczynamy od wygenerowania na linii Trig impulsu o długości 10 μs. Czujnik wyśle wówczas paczkę kilku impulsów ultradźwiękowych i wystawi stan wysoki na wyjściu Echo. Następnie będzie on oczekiwał na odbiór sygnału, który odbił się od przeszkody. Po jego odebraniu stan wyjścia Echo znów zostanie zmieniony (z wysokiego na niski).
Czas trwania stanu wysokiego na wyjściu Echo odpowiada zatem czasowi potrzebnemu na wysłanie paczki ultradźwięków i odebranie odbitego echa.
Przyjmując, że prędkość dźwięku w powietrzu to 340 m/s, możemy policzyć, że w ciągu 1 µs dźwięk przebywa 0,034 cm, czyli około 1/29 cm. W związku z tym, że w tym przypadku dźwięk musi przebyć drogę dwa razy (od nadajnika do przeszkody i z powrotem), to aby „przeliczyć czas na odległość”, wystarczy podzielić go przez 58 (2 * 29) – uzyskamy wtedy odległość od przeszkody w centymetrach. Tym samym uda nam się precyzyjnie wykrywać otaczające nas obiekty – zupełnie tak samo, jak robią to nietoperze, korzystające z echolokacji.
Ultradźwiękowe czujniki odległości wykorzystują ten sam efekt co nietoperze
Podłączenie HC-SR04 do STM32L4
Krótki wstęp teoretyczny za nami, więc możemy przejść do praktyki. Zaczynamy od podłączenia czujnika do mikrokontrolera. Tym razem wejście wyzwalające pomiar, czyli Trig, łączymy z pinem PB10, a wyjście czujnika, czyli Echo – z pinem PA0. Oprócz tego podłączamy oczywiście czujnik do zasilania – tym razem wyjątkowo do 5 V.
Schemat ideowy i montażowy podłączenia HC-SR04 do STM32L4
Układ w obecnej wersji można podłączyć bezpośrednio do Nucleo, można również wykorzystać płytkę stykową – wtedy warto skorzystać z możliwości podłączenia dodatkowego kondensatora 100 nF w celu filtrowania zasilania (nie jest to jednak konieczne).
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.
Masz już zestaw? Zarejestruj go wykorzystując dołączony do niego kod. Szczegóły »
Współpraca STM32L4 z układami zasilanymi z 5 V
Nasz mikrokontroler jest zasilany 3,3 V. Niestety dość często okazuje się, że układy peryferyjne, które chcemy do niego podłączyć, wymagają zasilania z 5 V – tak właśnie jest w tym przypadku. Pojawiają się wówczas dwa problemy:
Na wyjściach naszego mikrokontrolera maksymalne napięcie wynosi 3,3 V, może się więc zdarzyć, że będzie to za mało, aby np. dany czujnik wykrył logiczną jedynkę.
Nie wszystkie piny mikrokontrolera (podczas pracy jako wejścia) tolerują 5 V, które może zostać podane właśnie np. przez taki czujnik.
To, czy punkt 1 jest dużym problemem, zależy od podłączanego układu. Niektóre elementy będą działać poprawnie, gdy zasilimy je z 3,3 V, a inne mogą wymagać dodatkowych zabiegów (lub innych sztuczek) – w dalszej części kursu jeszcze wrócimy do tego tematu.
Drugi problem dotyczy napięcia, które zostanie podane na wejście mikrokontrolera. W dokumentacji STM32L4, przy opisie wybranego przez nas pinu PA0, znajdziemy zapis FT_a, który oznacza, że dany pin toleruje napięcie 5 V – możemy więc spokojnie podłączyć sygnał Echo z czujnika wprost do PA0.
Informacja o tolerancji danego pinu dla 5 V na wejściu
Warto jednak pamiętać, że nie wszystkie piny mikrokontrolera STM32L476RG tolerują 5 V. Jeśli nie sprawdzimy, czy dany pin posiada dopisek FT_a, to możemy przypadkiem uszkodzić układ. Dokładny opis tego parametru znaleźć można w dokumentacji mikrokontrolera.
Lista parametrów, którymi opisywane są GPIO (fragment dokumentacji)
Obsługa czujnika HC-SR04 za pomocą liczników
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 debugger i USART2 w trybie asynchronicznym. Zaznaczamy też w opcjach projektu, że CubeMX ma wygenerować osobne pliki dla wszystkich modułów. Następnie (już standardowo) dodajemy przekierowanie printf na UART – wystarczy dodanie pliku nagłówkowego:
Następnie wracamy do perspektywy CubeMX i zabieramy się za konfigurację TIM2. Jest to 32-bitowy licznik – nie jest on niezbędny do tego konkretnego zadania, ale wybieramy go w ramach treningu, żeby poznać bardziej rozbudowane peryferie.
Licznik wykorzystamy do dwóch zadań. Pierwszy kanał timera będzie pracował w trybie wejścia, dzięki czemu uda nam się zmierzyć czas trwania impulsu zwracanego przez czujnik. Z kolei trzeci kanał licznika wykorzystamy do wygenerowania sygnału uruchamiającego pomiar – użyjemy trybu PWM, dzięki czemu licznik będzie mógł sprzętowo wygenerować odpowiedni impuls startowy.
W praktyce oznacza to, że musimy zacząć od poniższych ustawień TIM2:
Clock Source: Internal Clock
Channel 1: Input Capture direct mode
Channel 3: PWM Generation CH3
Następnie przechodzimy do edycji parametrów licznika. Zaczynamy od preskalera – w tym przypadku ustawiamy go na 79, ponieważ częstotliwość taktowania układu (80 MHz) zostanie wtedy podzielona przez 80, co da nam 1 MHz (albo inaczej 1 μs). Potem ustawiamy parametr Counter Period, który w tym przypadku będzie regulował częstotliwość naszych pomiarów – w tym przypadku wystarczy nam pomiar co 1 s, wpisujemy więc 999999. Na koniec potrzebna jest jeszcze konfiguracja czasu trwania impulsu generowanego przez PWM. Tutaj, zgodnie z dokumentacją czujnika, wpisujemy 10 μs.
W praktyce oznacza to, że musimy ustawić następujące parametry dla TIM2:
Prescaler: 79
Counter Period: 999999
PWM Generation Channel 3 > Pulse: 10
W przypadku pozostałych parametrów zostawiamy domyślne wartości. Zapisujemy projekt i od razu zabieramy się za pisanie kodu. Właściwie wszystko, co jest potrzebne do uruchomienia PWM oraz pomiaru sygnału z czujnika, już jest nam znane, bo temat ten omówiliśmy w części 8 kursu.
Opisaliśmy tam funkcje, które będą tu potrzebne, czyli HAL_TIM_PWM_Start oraz HAL_TIM_IC_Start. Nowością będzie to, że obie te funkcje wykorzystamy w kontekście tego samego licznika sprzętowego. Pierwsza wersja kodu może wyglądać następująco:
Jak widać, licznik bez problemu może pełnić jednocześnie funkcję „wejścia” i „wyjścia”. Niestety, po uruchomieniu programu uzyskamy jednak dość zaskakujący efekt.
Wartość odczytana przez pierwszą wersję programu
Wartość wyświetlana w terminalu będzie praktycznie stała – wskazania będą różne dla poszczególnych egzemplarzy czujnika, mogą się również wahać o kilka jednostek. Wartości te nie będą jednak zależały od tego, czy przed czujnikiem znajduje się jakaś przeszkoda. W rozwiązaniu tej zagadki znów pomocny będzie analizator stanów logicznych.
Podgląd komunikacji z czujnikiem za pomocą analizatora stanów logicznych
Jak widać, schemat „komunikacji” z czujnikiem się zgadza, jednak wcześniejsze przebiegi czasowe (które zostały przygotowane na bazie dokumentacji) zupełnie nie oddają skali czasu. Nasz układ działa poprawnie – na linii Trig pojawił się impuls wyzwalający, a na linii Echo otrzymaliśmy odpowiedź.
Dlaczego więc nasz pomiar nie jest poprawny? Pozostawiliśmy domyślne opcje licznika, więc pomiar dotyczy zbocza narastającego – liczony jest zatem czas od początku okresu, tj. od sygnału na linii Trig aż do pojawienia się zbocza narastającego, które oznacza dopiero początek odpowiedzi.
To, co uzyskaliśmy, też nam się przyda, bo czas między wyzwalaniem pomiaru a początkiem odpowiedzi jest praktycznie stały. Możemy więc zmienić konfigurację licznika tak, aby mierzyć moment pojawienia się zbocza opadającego, a czas, który właśnie zmierzyliśmy, po prostu odejmiemy od wyniku. Wracamy do CubeMX i zmieniamy parametr Polarity Selection dla Input Capture Channel 1 na Falling Edge. Dzięki temu będziemy zliczać czas do zbocza opadającego, czyli końca odpowiedzi.
Następnie ponownie uruchamiamy program (bez żadnej zmiany kodu). Teraz wyniki będą się zmieniały w miarę przybliżania/oddalania przeszkody do/od czujnika. Możemy już spokojnie zmienić program w taki sposób, aby odległość była wyświetlana w centymetrach. Wystarczy zmiana jednej linijki:
Oczywiście, jeśli wcześniej zbocze narastające pojawiało się po innym czasie niż 480, to zmieniamy wartość na taką, która odpowiada naszemu egzemplarzowi czujnika.
C
1
printf("%.1f cm\n",(value-480)/58.0f);
Teraz wynik będzie wyświetlany jako liczba zmiennoprzecinkowa, musimy tylko włączyć obsługę takich liczb za pomocą printf (Project > Properties > C/C++ Build > Settings > MCU Settings > Use float with printf...). Po uruchomieniu tej wersji programu otrzymamy już poprawną odległość.
Pomiar odległości i wyświetlanie wyników w centymetrach
Nasz program działa, ale jest daleki od ideału. Odejmowanie stałej wartości od wyniku to niewątpliwie zły pomysł, tym bardziej że ta wartość zależy od konkretnego egzemplarza tego czujnika. Na szczęście możemy rozwiązać ten problem znacznie lepiej (sprzętowo).
Sprzętowy pomiar obu zboczy sygnału
Wracamy do CubeMX i konfiguracji TIM2. Jeden kanał ma przypisany tylko jeden rejestr pamiętający wyniki pomiaru, więc nie możemy w nim zmieścić dwóch wartości. Możemy za to włączyć drugi kanał. Jeśli dla kanału drugiego wybierzemy opcję taką jak dla pierwszego (czyli Input Capture direct mode), to zobaczymy, że CubeMX automatycznie skonfiguruje pin PA1 jako wejście TIM2_CH2.
W związku z tym, że PA0 oraz PA1 to wejścia, moglibyśmy je połączyć i wykonać w taki sposób nasz pomiar, lecz istnieje nieco lepsze rozwiązanie.
Wystarczy, że zmienimy tryb działania drugiego kanału na Input Capture indirect mode (będzie od razu widać, że PA1 nie jest wtedy używany). W trybie direct każdy kanał ma przypisane wejście o takim samym numerze, czyli kanał 1 jest połączony z wejściem CH1, kanał 2 z CH2 itd. Natomiast w trybie indirect pary, czyli kanał 1 i 2 oraz 3 i 4, są zamienione. Dlatego kanał 2 jest połączony z CH1, a gdybyśmy wybrali ten tryb dla kanału 1, używałby on wejścia CH2.
Dzięki takiemu ustawieniu jeden pin, czyli PA0, będzie połączony z dwoma kanałami naszego timera jednocześnie. Teraz możemy dla pierwszego ustawić reakcję na zbocze narastające, a dla drugiego na opadające. Czyli dla formalności ustawiamy:
Dzięki temu będziemy dokładnie wiedzieli, kiedy rozpoczął i zakończył się sygnał, który zwracany jest przez czujnik, i na tej podstawie automatycznie obliczymy odległość (bez odejmowania stałej).
Nowy kod wykorzystuje aż trzy kanały licznika sprzętowego. Dzięki temu możemy generować sygnał wyzwalający pomiar oraz odczytywać oba zbocza odpowiedzi całkowicie sprzętowo, więc układ będzie mógł wyświetlać poprawne wyniki niezależnie od użytego egzemplarza czujnika.
Obsługa wyświetlaczy 7-segmentowych
Wysyłanie wyników przez port szeregowy jest świetnym rozwiązaniem, ale pora (dla treningu) utrudnić sobie zadanie i wykorzystać wyświetlacz 7-segmentowy. Elementy tego typu są powoli wypierane przez nowsze rozwiązania, np. wyświetlacze graficzne, ale nadal można je spotkać w wielu urządzeniach.
Budowa typowego wyświetlacza 7-segmentowego
Wyświetlacze 7-segmentowe to nic innego jak zbiór diod świecących, które włącza się w taki sposób, aby uzyskać pożądaną liczbę. Zadanie jest więc pozornie proste – miganie diodami już opisywaliśmy (na samym początku kursu). Obsługa tych wyświetlaczy jest jednak dla wielu osób problematyczna, dlatego pokażemy, jak obsłużyć je w pełni sprzętowo. Do wszystkiego dojdziemy stopniowo.
Każda dioda odpowiada jednemu segmentowi cyfry i jest oznaczona literą od A do G.
Wyświetlanie jednej cyfry
Zaczniemy od obsługi jednej cyfry wyświetlacza. W tym celu wystarczy go po prostu podłączyć tak, jak robiliśmy to na początku kursu – każda dioda do osobnego pinu, przez osobny rezystor. I taka wersja jest właściwie najlepsza. Możemy jednak zastosować pewne uproszczenie i dodać rezystor 330 R na wspólnej linii (katoda) – dzięki temu połączenie układu na płytce stykowej będzie znacznie łatwiejsze. Stosując takie uproszczenie, godzimy się jednak z tym, że jasność wyświetlacza będzie zależna od liczby segmentów, które są w danej chwili włączone (bo ten sam prąd będzie dzielony na więcej diod).
Najlepszym rozwiązaniem byłoby podłączenie każdego segmentu przez osobny rezystor. Bez problemu można to zrealizować na płytce stykowej, jednak w tym przypadku celowo godzimy się na pewne uproszczenie i stosujemy jeden rezystor (znając wady tego rozwiązania).
Schemat ideowy i montażowy do przykładu z tego ćwiczenia
Oczywiście wspólna katoda mogłaby być podłączona przez rezystor prosto do masy. Podłączamy ją jednak do kolejnego wyprowadzenia naszego mikrokontrolera, bo takie rozwiązanie będzie nam potrzebne podczas kolejnych testów.
Teraz wracamy do programowania (zostajemy w tym samym projekcie). Zaczynamy od ustawienia GPIO. Wszystkie piny, do których podłączyliśmy wyświetlacz, konfigurujemy jako wyjścia i nadajemy im odpowiednie etykiety (SEG_A, SEG_B, SEG_C, SEG_D, SEG_E, SEG_F, SEG_G oraz SEG_1 dla katody).
Konfiguracja GPIO dla wyświetlacza 7-segmentowego
Kod do obsługi wyświetlacza od razu wydzielimy do osobnych plików. Zaczynamy od utworzenia pliku seg7.h, do którego wstawiamy:
C
1
2
3
4
5
6
#pragma once
#include <stdint.h>
// Pokaż cyfrę na wyświetlaczu
// value - cyfra do wyświetlenia
voidseg7_show_digit(uint32_t value);
Funkcja seg7_show_digit będzie wyświetlała cyfrę podaną jako parametr na wyświetlaczu. Jej kod definiujemy oczywiście w kolejnym pliku, czyli seg7.c, który też musimy utworzyć. Pierwsza, wstępna wersja będzie długa, ale prosta do zrozumienia (zaraz ją skrócimy).
Wszystko powinno być chyba jasne – małego komentarza może wymagać jedynie zapis z dzieleniem modulo 10 (wewnątrz instrukcji sterującej switch). Jest to proste zabezpieczenie, bez którego podanie liczby dwucyfrowej sprawiłoby, że na wyświetlaczu nie pojawiłaby się żadna wartość. Dzięki modulo na wyświetlaczu pojawi się np. 1, gdy jako parametr podamy 21.
Teraz możemy przejść do programu głównego i wykorzystać naszą nową funkcję (na razie tymczasowo kasujemy lub zakomentowujemy poprzedni kod związany z obsługą czujnika):
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while(1)
{
for(inti=0;i<10;i++){
seg7_show_digit(i);
HAL_Delay(500);
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
Po uruchomieniu powinniśmy zobaczyć, że na wyświetlaczu pojawiają się kolejne cyfry. Jeśli wszystko działa poprawnie, to możemy przejść dalej, czyli do optymalizacji kodu. W przypadku problemów warto się teraz zatrzymać i sprawdzić np. poprawność połączeń.
Skracanie kodu obsługi wyświetlacza
Nasza dotychczasowa funkcja sprawdza cyfrę podaną jako argument i odpowiednio włącza segmenty wyświetlacza. Taki program oczywiście działa, ale jest strasznie długi i można go na wiele sposobów poprawić. Zacznijmy od eliminacji powtarzającego się kodu.
W aktualnej wersji dla każdej cyfry wywołujemy HAL_GPIO_WritePin aż siedem razy. Możemy więc wstawić ten kod do funkcji pomocniczej, która będzie ustawiała wyjścia zgodnie z wartościami bitów zmiennej przekazanej jako jej parametr. Do pliku seg7.c dodajemy więc funkcję statyczną:
Teraz możemy zmienić kod funkcji seg7_show_digit na nieco krótszą wersję:
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
voidseg7_show_digit(uint32_t value)
{
switch(value%10){
case0:
set_output(0b0111111);
break;
case1:
set_output(0b0000110);
break;
case2:
set_output(0b1011011);
break;
case3:
set_output(0b1001111);
break;
case4:
set_output(0b1100110);
break;
case5:
set_output(0b1101101);
break;
case6:
set_output(0b1111101);
break;
case7:
set_output(0b0000111);
break;
case8:
set_output(0b1111111);
break;
case9:
set_output(0b1101111);
break;
}
}
Jest nieco lepiej, ale to nie ostatnia możliwość skrócenia tego kodu, bo zamiast oddzielnej analizy każdego przypadku możemy utworzyć tablicę zawierającą informację o tym, jakie mają być stany pinów podczas włączania konkretnej cyfry.
Dzięki temu wystarczy, że wywołanie funkcji set_output pojawi się w kodzie tylko raz:
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
voidseg7_show_digit(uint32_t value)
{
constuint8_t digit[]={
0b0111111,
0b0000110,
0b1011011,
0b1001111,
0b1100110,
0b1101101,
0b1111101,
0b0000111,
0b1111111,
0b1101111,
};
set_output(digit[value%10]);
}
Takiej wersji kodu będziemy się teraz trzymać. Oczywiście każde podejście ma swoich zwolenników. Pierwsza wersja jest bardzo długa, ale najbardziej czytelna. Ostatnia wersja kodu jest znacznie krótsza, lecz może być trudniejsza w zrozumieniu – chcieliśmy jednak pokazać w kursie kolejną „sztuczkę”.
Wyświetlanie dwóch cyfr
Nasz wyświetlacz posiada dwie cyfry i moglibyśmy po prostu podłączyć drugi zestaw diod do innych GPIO, ale takie rozwiązanie byłoby nieefektywne. Wystarczy, że do osobnego pinu mikrokontrolera podłączymy wspólną katodę od drugiego wyświetlacza (oczywiście przez rezystor). Z kolei segmenty poszczególnych cyfr możemy śmiało ze sobą połączyć.
W tym przypadku (tak jak poprzednio) korzystamy z pewnego uproszczenia, więc konieczne jest dodanie odpowiedniego rezystora między mikrokontrolerem a wspólną katodą.
Schemat ideowy i montażowy drugiej wersji układu z wyświetlaczem 7-segmentowym
Przy takim połączeniu wykorzystujemy o wiele mniej pinów mikrokontrolera, ale nie jesteśmy w stanie wyświetlić jednocześnie różnych cyfr. Rozwiążemy ten problem, włączając tylko jedną cyfrę w danej chwili i bardzo szybko przełączając się między jedną a drugą cyfrą wyświetlacza. Dzięki temu będziemy mogli wyświetlać różne cyfry, używając tylko jednego pinu na każdą dodatkową cyfrę.
Taki tryb pracy nazywany jest multipleksowaniem i korzystamy tutaj z bezwładności ludzkiego oka, które nie zauważy, że wyświetlacze szybko migają.
Jak wiemy, diody świecą tylko wtedy, gdy są podłączone zgodnie z kierunkiem przewodzenia prądu. Zatem jeśli na wspólnej katodzie wystawimy stan wysoki, dana cyfra będzie zgaszona. Natomiast ustawienie stanu niskiego pozwoli włączać diody wybranej cyfry zgodnie ze stanem pinów podłączonych do wspólnych segmentów. Wracamy więc do CubeMX i dodajemy konfigurację pinu PA11 jako wyjścia SEG_2. Następnie konfigurujemy PA11 oraz PB12 jako wyjścia typu open-drain z domyślną wartością High.
Moglibyśmy zostawić domyślny tryb push-pull, ale wówczas diody wyłączonej cyfry byłyby podłączane do pełnego napięcia w kierunku zaporowym. Nie ma w tym nic złego, bo napięcie 3,3 V nie uszkodzi naszego wyświetlacza, ale lepszym rozwiązaniem jest użycie trybu open-drain. Tak jak pisaliśmy w części o I2C – w tym trybie tranzystor NMOS zwiera wyjście do masy, gdy wystawiany jest stan niski, a wysterowanie stanem wysokim pozostawia wyjście w stanie wysokiej impedancji.
Teraz uruchamiamy timer TIM6. Wykorzystamy go do naprzemiennego włączania cyfr wyświetlacza. Ustawiamy preskaler na 7999, dzięki czemu licznik będzie zliczał impulsy o częstotliwości 10 kHz. Od razu ustawiamy też okres, czyli parametr Counter Period, na 1999.
Przy takich ustawieniach przerwanie od tego licznika będzie pojawiało się pięć razy na sekundę, dzięki czemu dosłownie zobaczymy, jak działa ten kod. Na koniec w zakładce NVIC Settings włączamy przerwanie timera TIM6 oraz zmniejszamy przypisany mu priorytet (np. na 10). Teraz możemy zapisać zmiany i wrócić do naszego programu.
Zaczynamy od zmiany pliku seg7.h – dodajemy do niego informację o kolejnej funkcji:
C
1
2
3
4
5
6
7
// Pokaż liczbę na wyświetlaczu
// value - liczba do wyświetlenia
voidseg7_show(uint32_t value);
// Funkcja pomocnicza
// Zmiana aktywnego wyświetlacza
voidseg7_update(void);
Funkcja seg7_show będzie tym, czego oczekuje użytkownik naszej biblioteki – jej wywołanie będzie ustawiało wyświetlaną wartość, czyli obie cyfry. Natomiast seg7_update to funkcja pomocnicza, która wyłącza aktualnie włączoną cyfrę i włącza drugą. Częste wywoływanie tej funkcji będzie sprawiało, że obie cyfry będą dla człowieka widoczne jednocześnie.
Przechodzimy do pliku seg7.c i zaczynamy od dodania dwóch zmiennych:
C
1
2
staticuint32_t actual_value;
staticuint32_t active_digit;
Pierwsza to po prostu aktualnie wyświetlana wartość. Będziemy ustawiali ją w funkcji seg7_show:
C
1
2
3
4
voidseg7_show(uint32_t value)
{
actual_value=value;
}
Tym, co najważniejsze, jest funkcja seg7_update. Będzie ona wyświetlała cyfrę o numerze active_digit oraz zmieniała wartość zmiennej na kolejną:
Pierwsze dwie linijki ustawiają stan wysoki na pinach podłączonych do wspólnych katod obu cyfr, co powoduje wyłączenie wyświetlacza. Następnie na wyjściach ustawiamy stan odpowiadający temu, co ma być wyświetlone na danej pozycji wyświetlacza.
Jeśli aktywna jest pierwsza cyfra, to przekazujemy do seg7_show_digit wartość actual_value – funkcja i tak wyświetli tylko pierwszą cyfrę z dwucyfrowej liczby. Natomiast jeśli aktywna jest druga cyfra, to przekazujemy wartość actual_value podzieloną przez 10, co sprawi, że na wyświetlaczu zostanie pokazana właściwa wartość. W kolejnym kroku zmieniany jest stan linii połączonej z katodą aktywnej cyfry, co powoduje jej włączenie. Na koniec wartość active_digit zmieniana jest na przeciwną, więc kolejne wywołanie seg7_update włączy drugą cyfrę.
Teraz możemy przejść do pliku main i dodać do programu procedurę obsługi przerwania:
Jej treść to po prostu wywołanie funkcji seg7_update, która będzie włączała odpowiednie cyfry wyświetlacza. Na koniec edycja głównej części kodu. Po pierwsze uruchamiamy przerwanie od licznika TIM6, a po drugie dodajemy licznik, dzięki któremu sprawdzimy działanie tego układu.
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Infinite loop */
/* USER CODE BEGIN WHILE */
HAL_TIM_Base_Start_IT(&htim6);
intvalue=0;
while(1)
{
seg7_show(value);
value++;
HAL_Delay(500);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
Gdy teraz uruchomimy nasz program, zobaczymy, że wyświetla on kolejne liczby, ale w danej chwili włączana jest tylko jedna cyfra – był to jednak efekt zamierzony, aby można było łatwiej zrozumieć, jak działa ten mechanizm. W celu poprawienia uzyskanego efektu wystarczy, że zwiększymy częstotliwość, z jaką generowane jest przerwanie TIM6. Wracamy zatem do CubeMX i zmieniamy okres na 99. Nasz program będzie działał tak samo jak poprzednio – jedyna różnica będzie taka, że teraz przełączanie wyświetlaczy odbywa się sto razy na sekundę, więc ludzkie oko nie widzi migania.
Efekt działania programu – dwucyfrowy licznik
Wyświetlanie pomiarów na wyświetlaczu
Teraz możemy połączyć wcześniejszy pomiar odległości z wyświetlaczem – przy okazji pokażemy, jak napisać program, który będzie działał w pełni w przerwaniach. Cały czas wykorzystujemy takie samo podłączenie i te same liczniki, które zostały opisane na początku tego poradnika. Przechodzimy tylko do edycji parametrów licznika TIM2, który odpowiada za odpytywanie czujnika odległości. Zmniejszamy tam parametr Period z 999999 np. na 99999, dzięki czemu zwiększymy częstotliwość pomiarów.
Włączamy też obsługę przerwania od TIM2 i (tradycyjnie) zmniejszamy jego priorytet (np. na 10). Teraz możemy zapisać zmiany w projekcie i wrócić do edycji kodu. Następnie przechodzimy do pliku main i zaczynamy od edycji procedury obsługi przerwań:
Obsługujemy w niej oba liczniki. W przypadku TIM6 przekazujemy obsługę do funkcji seg7_update, a w przerwaniu od TIM2 pobieramy wynik pomiaru i przekazujemy go do seg7_show. Teraz możemy przystąpić do zmian głównej części kodu, która może być dość zaskakująca:
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
HAL_TIM_Base_Start_IT(&htim6);
HAL_TIM_Base_Start_IT(&htim2);
HAL_TIM_IC_Start(&htim2, TIM_CHANNEL_1);
HAL_TIM_IC_Start(&htim2, TIM_CHANNEL_2);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while(1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
Uruchamiamy liczniki bazowe dla TIM2 i TIM6, konfigurujemy również kanały TIM2. Po tej konfiguracji program główny może być już pusty, bo cały program działa w przerwaniach i zupełnie nie „zagraca” nam głównej pętli programu, w której moglibyśmy zająć się ważniejszymi tematami.
Dalmierz cyfrowy, który działa dzięki przerwaniom
Prędkość dźwięku w powietrzu?
Przy obliczaniu odległości mierzonej za pomocą ultradźwiękowego czujnika zastosowaliśmy popularną metodę polegającą na dzieleniu uzyskanego wyniku wyrażonego w mikrosekundach przez wartość 58. Otrzymywaliśmy wynik w centymetrach, a całość działa całkiem sprawnie.
Jednak prędkość dźwięku w powietrzu nie jest stała i zależy dość mocno od temperatury!
Użyty przez nas współczynnik 58 odpowiadał prędkości dźwięku 344,8 m/s, czyli temperaturze nieco ponad 20°C. Jednak gdybyśmy chcieli korzystać z dalmierza nie tylko w temperaturze pokojowej, warto byłoby zastanowić się, jaki błąd wprowadza taki stały współczynnik.
W temperaturze 0°C prędkość dźwięku wynosi 331,8 m/s, więc błąd to około 3,8%. Niby niewiele, ale jeśli będziemy mierzyli odległość 1 m, to „pomylimy się” o prawie 4 cm, a to dość dużo. Na szczęście możemy rozbudować układ o czujnik temperatury, a przy okazji poznamy kolejne tajniki programowania STM32.
LM35 – analogowy czujnik temperatury
Tym razem w roli czujnika temperatury wykorzystamy analogowy termometr LM35. W dokumentacji układu przeczytamy, że musi on być zasilany co najmniej z 4 V (w tym przypadku będzie to więc 5 V). Korzystanie z tego czujnika jest bardzo proste – podłączamy zasilanie, a na jego wyjściu otrzymujemy napięcie proporcjonalne do aktualnej temperatury. Do przetestowania tego czujnika nie potrzebujemy nawet mikrokontrolera – wynik możemy odczytać zwykłym multimetrem.
Schemat ideowy i montażowy dla pomiaru temperatury za pomocą LM35
Współczynnik zależności napięcia od temperatury dla układu LM35 to 10 mV/C. W naszym przypadku napięcie wynosiło około 0,258 V, co można przeliczyć na temperaturę 25,8°C. Warto zwrócić uwagę, że pomimo zasilania układu z 5 V na jego wyjściu otrzymujemy stosunkowo niskie napięcie – na tyle, że będziemy mogli podać je bezpiecznie na wejście przetwornika ADC.
Podłączenie i test czujnika
Teraz możemy odłączyć multimetr i podłączyć wyjście czujnika do pinu PA6 – oczywiście pozostawiamy na płytce stykowej wcześniejsze elementy, tj. wyświetlacz 7-segmentowy oraz czujnik ultradźwiękowy.
Schemat ideowy i montażowy ostatecznej wersji czujnika odległości
Wiemy już, że czujnik LM35 zasilany jest z 5 V, ale na wyjściu pojawia się napięcie 10 mV/C, więc nawet jeśli będziemy testować układ przy 50°C, na wyjściu pojawi się raptem 0,5 V. Dlatego możemy podłączyć czujnik bezpośrednio do wejścia ADC, nie martwiąc się, że uszkodzimy nasz mikrokontroler zbyt wysokim napięciem.
Wracamy więc do widoku CubeMX i przechodzimy do modułu ADC1. Uruchamiamy odczyt na kanale numer 11, czyli wybieramy dla niego opcję IN11 Single-ended. Następnie w parametrach musimy ustawić następujące opcje:
ADC_Settings > Continous Conversion Mode: Enabled
ADC_Settings > Overrun behaviour: Overrun data overwritten
Oprócz tego po aktywacji przetwornika ADC zostaniemy prawdopodobnie powiadomieni o tym, że konieczna jest zmiana ustawień zegarów. Warto zatem przejść do zakładki Clock Configuration, aby zatwierdzić automatycznie zaproponowane zmiany.
Konfiguracja przetwornika ADC (oraz czerwone ostrzeżenie w zakładce Clock Configuration)
Teraz możemy zapisać zmiany, wygenerować nowy kod i przystąpić do napisania programu. Do głównej funkcji main dodajemy kalibrację i uruchomienie przetwornika ADC, do tego wewnątrz pętli while dodajemy instrukcję, która będzie wypisywała w terminalu aktualny odczyt z ADC.
Po uruchomieniu tej wersji programu powinniśmy uzyskać ciąg wartości – w naszym przypadku było to około 324, co można łatwo przeliczyć: 324 * 3,3 V / 4096 = 0,26 V, co daje 26°C. Warto więc zmienić zawartość pętli while – możemy od razu wyświetlać temperaturę w stopniach Celsjusza oraz prędkość dźwięku dla danych warunków:
Tym razem na ekranie powinniśmy widzieć efekt zbliżony do poniższego – oczywiście wartości powinny się zmieniać np. po ogrzaniu termometru.
Pomiar napięcia przez ADC, przeliczanie odczytów na temperaturę i prędkość dźwięku
W związku z tym, że wszystko działa już poprawnie, możemy przenieść fragment kodu obliczający prędkość dźwięku dla aktualnych warunków atmosferycznych do osobnej funkcji, którą dodajemy do pliku main (jest to funkcja statyczna, więc dodajemy ją nad naszymi wcześniejszymi funkcjami):
C
1
2
3
4
5
6
7
/* USER CODE BEGIN 0 */
staticfloatcalc_sound_speed(void)
{
uint32_t adc_value=HAL_ADC_GetValue(&hadc1);
floattemp=adc_value*330.0f/4096.0f;
return331.8f+0.6f*temp;
}
Na koniec uwzględniamy prędkość dźwięku w trakcie obliczania odległości i usuwamy kod testowy z pętli głównej. Nowa wersja procedury obsługi przerwań będzie wyglądała następująco:
Dlaczego akurat tak obliczyliśmy odległość? Zacznijmy od wartości stop-start. Jest to wyrażony w mikrosekundach czas, w którym dźwięk przebył drogę do przeszkody i z powrotem. W związku z tym, aby wyrazić wynik w sekundach, dzielimy wartość przez 1 000 000, a ponieważ interesuje nas odległość (czyli droga dźwięku w jedną stronę), to dzielimy ją jeszcze przez 2. Chcemy mieć wynik nie w metrach, ale w centymetrach, więc powinniśmy pomnożyć obliczoną wartość przez 100. Skoro jednak dzielimy przez milion, następnie dzielimy przez 2, a później mnożymy przez 100, możemy to uprościć do dzielenia przez 20 000 – i tak właśnie zrealizowano to w programie.
Wzmacniacz operacyjny
Jak widać, nasz układ działał poprawnie, ale czujnik LM35 wykorzystuje bardzo małą część zakresu naszego przetwornika analogowo-cyfrowego. Dla temperatury od 0°C do 70°C na wyjściu czujnika pojawi się maksymalnie 0,7 V, czyli niewiele w porównaniu z 3,3 V. Oczywiście to nic złego i projekt ten mógłby zostać zakończony na tym etapie, ale (głównie w ramach treningu) jeszcze go rozbudujemy.
Podczas omawiania ADC w STM32L4 wspominaliśmy w ramach ciekawostki, że nasz mikrokontroler ma jeszcze inne moduły analogowe, w tym dwa wzmacniacze operacyjne. Możemy więc wykorzystać jeden z nich, aby wzmocnić napięcie odczytywane z czujnika.
Pozostawiamy układ na płytce stykowej w obecnej formie i wracamy do CubeMX. Zaczynamy od tego, że musimy wyłączyć pomiar z kanału 11 – wybieramy więc Disable dla kanału IN11. Następnie aktywujemy moduł OPAMP2 i ustawiamy następujące parametry:
Mode: PGA Not Connected
Power Supply Range: Power Supply Range High
PGA Gain: 4
Po zmianie trybu działania wzmacniacza CubeMX przypisał do pinu PA6 funkcję OPAMP_VINP, czyli wejście wzmacniacza – oczywiście nieprzypadkowo wcześniej podłączyliśmy czujnik właśnie do tego wyprowadzenia mikrokontrolera. Mamy więc już podłączony LM35 do wejścia wzmacniacza. Z kolei wyjście wzmacniacza zostało automatycznie przypisane do pinu PB0 opisanego jako OPAMP2_VOUT –pojawi się tam sygnał, który będzie czterokrotnie wzmocniony (zgodnie z wybranym parametrem).
Konfiguracja wzmacniacza OPAMP2 w STM32L4
Jeśli teraz zapiszemy projekt, to podczas kompilacji pojawią się błędy, bo wyłączyliśmy przetwornik analogowo-cyfrowy. Możemy jednak zakomentować cały kod odwołujący się do ADC i dodać zamiast tego uruchomienie naszego wzmacniacza:
Teraz po wgraniu programu możemy za pomocą zwykłego multimetru zmierzyć, jakie napięcie pojawia się na pinie PB0. Powinniśmy zmierzyć, że jest na nim napięcie 4 razy wyższe niż na wyjściu czujnika. Czyli np. jeśli zmierzymy tam 1,047 V, będzie to oznaczało, że temperatura to około 26°C (bo musimy pamiętać, że napięcie z czujnika jest mnożone razy 4). W ramach dodatkowych ćwiczeń można również sprawdzić, jak układ będzie działał przy wzmocnieniu razy 2 lub 8 (użycie wzmocnienia razy 8 pozwoliłoby na lepsze wykorzystanie zakresu ADC, ale ograniczyłoby zakres pomiaru do 41,25°C).
Pomiar napięcia wyjściowego wzmacniacza
Przechodzimy do CubeMX i wracamy do konfiguracji ADC1. Wybieramy kanał IN15 i ustawiamy w nim opcję OPAMP2 Output Single-ended – dzięki temu będziemy mogli zmierzyć przetwornikiem ADC napięcie, które pojawia się na wyjściu wzmacniacza operacyjnego. Pozostałe parametry ustawiamy tak jak poprzednio, czyli:
Continous Conversion Mode: Enabled
Overrun behaviour: Overrun data overwritten
Sampling Time: 640.5 Cycles
Musimy jeszcze zmienić konfigurację pinu PB0. Klikamy w niego lewym przyciskiem myszy i z menu wybieramy opcję ADC1_IN15 (z zielonym plusem po lewej stronie).
Konfiguracja przetwornika ADC do pomiaru napięcia z wyjścia wzmacniacza operacyjnego
Po zapisaniu projektu możemy już wrócić do kodu i usunąć komentarze przy funkcjach związanych z przetwornikiem. Dla testu możemy także wykorzystać wcześniejszy kod wyświetlający dane z ADC – musimy tylko edytować linijkę, w której obliczamy temperaturę (dodajemy dzielenie przez 4).
W wyniku działania tej wersji kodu zobaczymy, że wartości odczytywane przez przetwornik ADC są faktycznie czterokrotnie większe od tego, co było poprzednio – wzmacniacz działa więc poprawnie.
Działanie programu z włączonym wzmacniaczem operacyjnym
Na koniec, kiedy wiemy już, że wzmacniacz działa poprawnie, możemy usunąć treść pętli while. Trzeba jeszcze edytować funkcję obliczającą prędkość dźwięku, używaną podczas obsługi naszego czujnika (dodajemy do niej dzielenie przez 4).
C
1
2
3
4
5
6
staticfloatcalc_sound_speed(void)
{
uint32_t adc_value=HAL_ADC_GetValue(&hadc1);
floattemp=adc_value*330.0f/4096.0f/4.0f;
return331.8f+0.6f*temp;
}
Teraz nasz program powinien działać dokładnie tak samo jak poprzednio – tzn. wszystko powinno działać w tle dzięki przerwaniom i licznikom – od wykonania i interpretacji pomiaru, przez pomiar i wzmocnienie sygnału z czujnika temperatury, aż po pokazywanie wyników na wyświetlaczach 7-seg.
Zadanie domowe
Rozbuduj program czujnika w taki sposób, aby na wyświetlaczu były pokazywane dwie kreski: - -, gdy pomiar z czujnika wyjdzie poza zakres 0–99.
Dodaj do układu obsługę diody RGB, która za pomocą płynnych przejść kolorów powinna informować o tym, jak daleko znajdujemy się od przeszkody (kolor zielony oznacza, że daleko; kolor czerwony – blisko).
Wykorzystaj informacje z części 6 kursu, w której omówiliśmy oszczędzanie energii na STM32L4, i uśpij mikrokontroler, aby obniżyć pobór prądu zbudowanego dalmierza.
Podsumowanie – co powinieneś zapamiętać?
Najistotniejsze, aby z tej części zapamiętać trzy rzeczy, które przydają się w praktyce bardzo często. Po pierwsze dokładny pomiar czasu trwania impulsu za pomocą licznika. Po drugie obsługa wyświetlaczy 7-segmentowych – ciągle można je spotkać w praktyce, a nieumiejętne obsługiwanie tych elementów generuje sporo problemów. Po trzecie – i najważniejsze – warto zapamiętać, że wszystkie te operacje mogą odbywać się niejako „automatycznie” – wystarczy odpowiednio wykorzystać liczniki i przerwania.
Czy wpis był pomocny? Oceń go:
Średnia ocena 4.8 / 5. Głosów łącznie: 30
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 STM32L4 zajmiemy się popularnymi i lubianymi diodami RGB, którymi można sterować cyfrowo – mowa oczywiście o diodach WS2812. Będzie to dobra okazja do tego, aby w dość nietypowy sposób wykorzystać PWM.
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
Dołącz do 20 tysięcy osób, które otrzymują powiadomienia o nowych artykułach! Zapisz się, a otrzymasz PDF-y ze ściągami (m.in. na temat mocy, tranzystorów, diod i schematów) oraz listę inspirujących DIY na bazie Arduino i Raspberry Pi.
Dołącz do 20 tysięcy osób, które otrzymują powiadomienia o nowych artykułach! Zapisz się, a otrzymasz PDF-y ze ściągami (m.in. na temat mocy, tranzystorów, diod i schematów) oraz listę inspirujących DIY z Arduino i RPi.
Trwa ładowanie komentarzy...