Kurs STM32L4 – #8 – liczniki sprzętowe (PWM, enkoder)

Kurs STM32L4 – #8 – liczniki sprzętowe (PWM, enkoder)

Mikrokontrolery STM32L4 są wyposażane w wiele liczników sprzętowych, dzięki którym można bez problemu odmierzać czas, generować PWM lub obsługiwać różne czujniki.

W tej części kursu będziemy ćwiczyć stosowanie liczników w praktyce – od migania LED-em przez sterowanie diody RGB aż do obsługi enkoderów.

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

Wykonując ćwiczenia z tej części kursu, poznasz najważniejsze informacje na temat liczników, które dostępne są w układach z rodziny STM32L4. Zaczniemy od wywoływania przerwań co określony czas, następnie przejdziemy do generowania sygnału PWM. Sprawdzimy też, jak liczniki radzą sobie ze zliczaniem zewnętrznych impulsów i pomiarem sygnału zewnętrznego. Na koniec zajmiemy się również specjalnym trybem liczników, dzięki któremu można z łatwością obsłużyć enkodery.

Czym w mikrokontrolerach są liczniki?

Odliczanie czasu, generowanie sygnału PWM i pomiar czasu trwania impulsu to tylko przykłady zadań, z którymi świetnie radzą sobie liczniki. W języku angielskim moduły te określane są jako timers, trudno jednak o dobre tłumaczenie. Moglibyśmy mówić tu np. o układach czasowo-licznikowych, ale elektronicy najczęściej używają nazw „liczniki sprzętowe”, „liczniki” lub po prostu „timery”.

Jednym z zadań liczników jest precyzyjne odmierzanie czasu

Jednym z zadań liczników jest precyzyjne odmierzanie czasu

W dużym skrócie można powiedzieć, że timery to użyteczne moduły wewnątrz mikrokontrolerów, które zliczają „jakieś” impulsy i sygnalizują fakt doliczenia do ustalonej przez nas wartości. Mogą one zliczać takty sygnału zegarowego lub np. występowanie konkretnego zbocza na wejściu układu. W wyniku działania liczników mogą być generowane przerwania, ale mogą też dziać się inne rzeczy. Co ważne, te wszystkie operacje realizowane są sprzętowo, więc dzieją się niejako w tle i nie obciążają układu.

Liczniki dostępne w STM32L476RG

Mikrokontrolery STM32 znane są m.in. właśnie z rozbudowanych timerów. Model STM32L476RG nie jest pod tym względem wyjątkiem, bo posiada aż 16 takich takich modułów (to bardzo dużo). Liczniki zostały podzielone przez producenta na 6 kategorii (liczba bitów oznacza w tym przypadku, do ilu może zliczyć timer – do 65536 w przypadku 16-bitowych i do 4294967296 w przypadku 32-bitowych):

  • zaawansowane (ang. advanced control) – 2 liczniki (16-bitowe);
  • ogólnego zastosowania (ang. general purpose) – 5 liczników (16-bitowe), 2 liczniki (32-bitowe);
  • podstawowe (ang. basic) – 2 liczniki (16-bitowe);
  • energooszczędne (ang. low-power) – 2 liczniki (16-bitowe);
  • licznik SysTick – 1 licznik;
  • liczniki dla watchdoga – 2 liczniki.

Dwa liczniki z ostatniego punktu powyższej listy powiązane są z watchdogami. Podczas ćwiczeń z tym mechanizmem (w 5. części kursu) testowaliśmy działanie niezależnego watchdoga (ang. independent watchdog). Jednak dostępny jest też drugi moduł, tzw. okienkowy watchdog (ang. window watchdog).

Idąc dalej od dołu listy, mamy timer SysTick, z którego już korzystaliśmy. To on generuje przerwanie co 1 ms, dzięki któremu działają funkcje HAL_GetTick oraz HAL_Delay. Kolejne timery należą do grupy low-power – są to liczniki sprzętowe, które wyróżniają się tym, że mogą pracować nawet wówczas, gdy zegar wysokiej częstotliwości jest wyłączony (czyli np. podczas uśpienia mikrokontrolera).

Nas najbardziej interesują liczniki z grup basic, general purpose i advanced control. Do dyspozycji mamy łącznie aż 11 takich timerów. Różnią się one głównie rozdzielczością oraz liczbą dodatkowych funkcji, które mogą pełnić.

Specyfikacja liczników, z których będziemy korzystać

Specyfikacja liczników, z których będziemy korzystać

Poznawanie liczników zaczniemy od najprostszych typów, czyli tych z grupy basic. Później zajmiemy się układami z kolejnych grup, a do tematu liczników i tak wrócimy jeszcze w innej części kursu.

Liczniki podstawowe

W grupie timerów podstawowych znajdziemy 2 moduły: TIM6 i TIM7. Sygnałem, który trafia na wejście tych liczników, jest zawsze sygnał zegarowy, którego częstotliwość może zostać podzielona za pomocą 16-bitowego preskalera (czyli częstotliwość może być dzielona przez wartości od 1 do 65 536). Aktualna wartość licznika jest przechowywana w rejestrze CNT counter, który również jest 16-bitowy.

Schemat blokowy podstawowego licznika z STM32L4

Schemat blokowy podstawowego licznika z STM32L4

Wartość tego rejestru jest zwiększana podczas każdego cyklu (częstotliwość zegara podzielona przez preskaler), a następnie porównywana z zawartością rejestru auto-reload. Jeśli wartości te będą równe, to generowane jest przerwanie lub zdarzenie, a sam licznik rozpoczyna działanie od zera.

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 »

Generowanie cyklicznych przerwań

Pora sprawdzić ten mechanizm w praktyce. Zaczynamy od standardowego projektu: STM32L476RG, który pracuje z częstotliwością 80 MHz, uruchamiamy debugger, do tego pin PA5 konfigurujemy jako wyjście LD2 i na razie tyle wystarczy; później dodamy kolejne GPIO.

Od razu aktywujemy moduł licznika TIM6, który znajdziemy w zakładce Timers. W tym celu wystarczy zaznaczyć opcję Activated. Po włączeniu modułu w dolnej części okna pojawią się parametry naszego licznika. Ustawiamy tam 2 wartości: Prescaler na 7999 oraz Counter Period na 9999.

Konfiguracja licznika TIM6 w STM32L4 za pomocą CubeMX

Konfiguracja licznika TIM6 w STM32L4 za pomocą CubeMX

Obliczenie preskalera i okresu

Dlaczego akurat takie wartości? Nasz układ jest skonfigurowany do pracy z 80 MHz. Oznacza to, że okres sygnału zegarowego to 1/80 MHz, czyli 0,0000000125 sekundy, czyli 12,5 nanosekundy. Timer TIM6 jest licznikiem 16-bitowym, a więc przy taktowaniu z 80 MHz swoją maksymalną wartość osiągnie po niecałej milisekundzie (12,5 ns × 65 536 = 819 200 ns).

To o wiele za mało, bo w ramach tego ćwiczenia chcieliśmy osiągnąć 1 sekundę. Dla pewności możemy jeszcze w CubeMX podejrzeć, czy nasz licznik jest faktycznie taktowany z 80 MHz – informację taką znajdziemy w prawym górnym rogu w zakładce Clock Configuration.

Informacja o taktowaniu timerów z 80 MHz w CubeMX

Informacja o taktowaniu timerów z 80 MHz w CubeMX

Są tam dwie pozycje powiązane z timerami. W dokumentacji mikrokontrolera znajdziemy informację, do której magistrali podłączony jest dany licznik – liczniki od TIM2 do TIM7 są podłączone do magistrali APB2, a pozostałe do APB1. W naszej konfiguracji wszystkie magistrale są taktowane tą samą częstotliwością, ale istnieje możliwość ustawienia różnych wartości.

Wiemy, że częstotliwość wejściowa to na pewno 80 MHz. Najwygodniej byłoby ją obniżyć do np. 1 kHz, uzyskując okres równy 1 ms. Wymagałoby to podzielenia częstotliwości przez 80 000, jednak niestety preskaler jest 16-bitowy, więc możemy podzielić częstotliwość najwyżej przez 65 536. Jeśli nie możemy uzyskać 1 kHz, to wybierzmy 10 kHz. W tym celu wystarczy podzielić częstotliwość wejściową przez 8000 – aby uzyskać ten efekt, wpisaliśmy do ustawień wartość o 1 mniejszą, czyli 7999.

Na koniec pozostaje nam policzyć, ile ma wynosić okres licznika (ang. conter period). Po podzieleniu częstotliwość to 10 kHz, więc chcemy, aby nasz licznik liczył do 10 000 (wtedy uzyskamy 1 s). Tak samo jak w przypadku preskalera, wstawiamy wartość mniejszą o 1, czyli 9999.

Sama konfiguracja licznika niewiele nam daje, bo konieczne jest jeszcze uruchomienie przerwania od tego timera. Zmieniamy więc zakładkę z ustawieniami na NVIC Settings i zaznaczamy jedyną dostępną tam opcję, czyli przerwanie od TIM6.

Aktywacja przerwania od licznika TIM6

Aktywacja przerwania od licznika TIM6

Zgodnie z poprzednią częścią kursu, w której omawialiśmy przerwania w STM32L4, dobrą praktyką jest ustawienie licznika systemowego SysTick tak, by miał on wyższy priorytet niż inne używane moduły. Przechodzimy więc do System Core > NVIC i obniżamy priorytet przerwania od licznika TIM6 – zamiast domyślnego 0 ustawiamy np. 10.

Zmiana priorytetu przerwania od TIM6

Zmiana priorytetu przerwania od TIM6

Wykorzystanie przerwania od licznika

Teraz możemy już zapisać nasz projekt, wygenerować szablon kodu i przystąpić do pisania programu. Tutaj sprawa jest bardzo prosta: przerwanie wywołane po przepełnieniu licznika będzie zgłaszane przez wywołanie funkcji HAL_TIM_PeriodElapsedCallback.

Możemy więc napisać funkcję o takiej właśnie nazwie, aby w jej wnętrzu zmieniać stan pinu sterującego diodą LD2. Trzeba jednak pamiętać, że ta sama funkcja jest wywoływana po pojawieniu się przerwania po przepełnieniu dowolnego licznika, musimy więc sprawdzić, który timer zgłosił to przerwanie.

Teraz pozostaje nam już ostatnia czynność, czyli samo uruchomienie timera. Użyjemy do tego funkcji HAL_TIM_Base_Start_IT. Wystarczy wywołać ją raz (przed pętlą główną):

Po skompilowaniu i wgraniu tego programu zobaczymy, że dioda włącza się i wyłącza dokładnie co sekundę. Co ważne, całość działa w oparciu o sprzętowy licznik TIM6. Kod ten nie korzysta z opóźnień blokujących wykonywanie programu, nie musimy też zajmować się diodą w pętli głównej – całość robi sprzętowy licznik i system przerwań.

Efekt działania programu

Efekt działania programu

Liczniki ogólnego zastosowania

Za nami szybki test podstawowych liczników, które sprawdzają się świetnie, gdy chcemy precyzyjnie odliczać czas. Teraz zajmiemy się bardziej skomplikowanymi timerami z grupy general purpose.

Już po schemacie, który przedstawia ich wnętrze, widać, że są one dość rozbudowane. Na pocieszenie warto zauważyć, że w samym środku tego schematu jest fragment, który już znamy, bo licznik bazowy działa prawie tak samo jak w opisanym wcześniej przykładzie – prawie tak samo, bo teraz taktowanie może pochodzić też z innych źródeł, a sam licznik może zliczać w górę lub w dół. 

Schemat blokowy liczników ogólnego zastosowania w STM32L4

Schemat blokowy liczników ogólnego zastosowania w STM32L4

Na szczęście nie musimy od razu wykorzystywać wszystkich możliwości, jakie oferują nam te liczniki. Zaczniemy od prostego przykładu, w którym wykorzystamy tylko wycinek możliwości danego timera. Teraz będzie interesował nas jedynie poniższy fragment schematu.

Podobieństwo do licznika podstawowego (fragment schematu z dokumentacji)

Podobieństwo do licznika podstawowego (fragment schematu z dokumentacji)

Mamy tutaj znany nam już moduł licznika, który nazywany jest licznikiem podstawowym/bazowym, dlatego w poprzednim przykładzie uruchamialiśmy go funkcją HAL_TIM_Base_Start_IT. Tym, co będzie tutaj nowe, są aż 4 tzw. kanały

Każdy kanał posiada własny rejestr „Capture/Compare n register”, gdzie „n” to numer kanału. Jeśli dany kanał działa jako wejście, to rejestr może przechowywać wynik pomiaru z danego kanału (przykładowo szerokość impulsu). Natomiast jeśli dany kanał jest wyjściem, wartość tego rejestru jest porównywana z licznikiem bazowym. Gdy wartości są równe, może być zgłaszane przerwanie lub może być zmieniana wartość pinu wyjściowego – za chwilę przećwiczymy kilka przykładów użycia tych rejestrów.

Generowanie cyklicznych przerwań

Bardziej rozbudowane moduły wymagają również bardziej skomplikowanej konfiguracji, zacznijmy więc od powtórzenia poprzedniego przykładu, czyli wygenerowania przerwania co 1 s, na razie bez używania kanałów. Dlatego przed przejściem dalej wyłączamy TIM6 (deaktywujemy go w CubeMX) i kasujemy kod, który dodaliśmy wcześniej do projektu.

Teraz z grupy Timers wybieramy moduł TIM3. Jako źródło taktowania, czyli Clock Source, wybieramy Internal Clock. Aktywuje się wtedy dolne okno z parametrami, w którym ustawiamy te same wartości co poprzednio, czyli Prescaler na 7999, a wartość dla Counter Period na 9999.

Konfiguracja licznika TIM3 w STM32L4

Konfiguracja licznika TIM3 w STM32L4

Na koniec włączamy przerwanie i zmieniamy jego priorytet na 10. Zapisujemy projekt i przechodzimy do pisania programu. Kod będzie podobny do poprzedniego. Czyli najpierw własna funkcja:

A później uruchamiamy licznik tak samo jak poprzednio (zmieniając oczywiście numer licznika):

Po przetestowaniu powinniśmy zobaczyć migającą diodę LD2. Widzimy więc, że timer TIM3, chociaż ma o wiele więcej opcji niż TIM6, w podstawowym zakresie oferuje praktycznie to samo.

Wykorzystanie wielu kanałów licznika

Podstawy za nami. Nadszedł czas, aby wykorzystać kanały. Wybrany przez nas licznik TIM3 ma 4 kanały i są one domyślnie wyłączone. Na potrzeby kolejnego ćwiczenia włączamy 3 pierwsze kanały. W tym celu w ustawieniach licznika zmieniamy ich tryby na „Output Compare No Output”.

Aktywacja 3 z 4 kanałów licznika TIM3

Aktywacja 3 z 4 kanałów licznika TIM3

Nazwa użytego trybu może być mało intuicyjna. Jej pierwsza część, czyli „Output Compare”, oznacza, że dany kanał będzie pracował jako wyjście. W takiej sytuacji wartość rejestru powiązanego z konkretnym kanałem jest porównywana z licznikiem głównym. Natomiast „No Output” oznacza, że kanał nie jest bezpośrednio (sprzętowo) połączony z żadnym pinem wyjściowym.

Po włączeniu kanałów w dolnej części okna CubeMX pojawią się dodatkowe grupy parametrów. Tym razem ustawiamy wartość parametru Pulse: dla pierwszego kanału na 2500, dla drugiego na 5000, a dla trzeciego na 7500.

Ustawienie parametrów Pulse dla kanałów licznika TIM3

Ustawienie parametrów Pulse dla kanałów licznika TIM3

Wartość Pulse będzie porównywana z wartością licznika bazowego. Okres tego timera to 10 000, więc licznik pierwszego kanału będzie zgłaszał przerwanie po 25% czasu, drugiego po 50%, a trzeciego po 75%. Potrzebujemy jeszcze metody na sprawdzenie, czy wszystko działa. W tym celu podłączamy do płytki Nucleo 3 diody – do wejść PA6, PA7 oraz PB0.

Schemat ideowy i montażowy układu do testowania kanałów TIM3

Schemat ideowy i montażowy układu do testowania kanałów TIM3

Następnie konfigurujemy te piny jako wyjścia i nadajemy im kolejno nazwy: LED1, LED2 i LED3.

Konfiguracja nowych diod w CubeMX

Konfiguracja nowych diod w CubeMX

Zapisujemy projekt, aby został wygenerowany nowy kod, i przechodzimy do pisania programu. Poprzednio korzystaliśmy z przerwania zgłaszanego po przepełnieniu głównego licznika timera, czyli funkcji HAL_TIM_PeriodElapsedCallback. Teraz też z niej skorzystamy, ale poza zmianą stanu diody LD2 będziemy w niej włączać pozostałe diody. Możemy napisać więc następujący kod:

W następnym kroku dodajemy obsługę przerwań generowanych przez poszczególne kanały licznika. Dodajemy w tym celu kolejną funkcję o nazwie HAL_TIM_OC_DelayElapsedCallback.

Ta sama funkcja jest wywoływana dla każdego kanału i dla każdego timera, musimy więc sprawdzić, co było źródłem tego przerwania. W naszej procedurze obsługi tego przerwania sprawdzamy numer kanału, po czym wyłączamy jedną z diod świecących. Kod takiej funkcji może wyglądać następująco:

W programie głównym musimy jeszcze uruchomić licznik i skonfigurowane kanały:

Po skompilowaniu i uruchomieniu tego programu powinniśmy zobaczyć, że dioda LED1 świeci przez 25% czasu, LED2 przez 50%, a LED3 przez 75%. Działanie tego układu można przedstawić w poniższej formie.

Wykresy przedstawiające działanie programu

Wykresy przedstawiające działanie programu

Udało nam się więc uzyskać sterowanie PWM (ang. pulse-width modulation), z tą różnicą, że specjalnie dobraliśmy (na razie) niską częstotliwość sygnału, aby efekt działania tego kodu był wyraźnie widoczny bez użycia sprzętu pomiarowego.

Generowanie sygnału PWM

Nasze rozwiązanie działało bardzo dobrze, miało jednak dość poważny mankament – każde włączenie i wyłączenie diody wymagało od układu nieco pracy przy obsłudze przerwania. Przerwania w przypadku mikrokontrolerów działają szybko, ale sami wiemy, jak irytujące jest, kiedy ktoś nam co chwilę przerywa. To samo, co uzyskaliśmy za pomocą programu działającego w przerwaniach, możemy również osiągnąć za pomocą sprzętowego PWM.

Pora rozszerzyć fragment schematu blokowego, który przedstawia nasz licznik. Tym razem skorzystamy z jego „dodatkowego” kawałka. Teraz kanały będą same sterować przypisanymi do nich wyjściami, bo wyjścia TIMx_CH1, TIMx_CH2 i TIMX_CH3 mogą być kontrolowane przez moduł sprzętowy. 

Kolejny fragment schematu blokowego licznika – połączenie z wyjściami (fragment schematu z dokumentacji)

Kolejny fragment schematu blokowego licznika – połączenie z wyjściami (fragment schematu z dokumentacji)

Wracamy do perspektywy CubeMX, otwieramy konfigurację TIM3 i zmieniamy ustawienia kanałów na odpowiednio: PWM Generation CH1, PWM Generation CH2 i PWM Generation CH3. CubeMX po tej zmianie automatycznie wyzeruje ustawienia pól Pulse, więc musimy je przywrócić do poprzednich wartości – wpisujemy kolejno 2500, 5000 i 7500.

Zmiana konfiguracji kanałów TIM3

Zmiana konfiguracji kanałów TIM3

Po aktywacji tej funkcji CubeMX z automatu zaznaczył na zielono 3 piny GPIO, które są połączone z używanymi przez nas kanałami. Jednak to tylko propozycja – możemy zmienić piny na inne. Do tej pory pinom PA6, PA7 i PB0 przypisaliśmy funkcje GPIO_Output, ale chcemy żeby sterowanie przejął moduł TIM3. Zmieniamy więc ich funkcje na odpowiednio: TIM3_CH1, TIM3_CH2 i TIM3_CH3.

Aktualizacja GPIO przypisanych do licznika TIM3

Aktualizacja GPIO przypisanych do licznika TIM3

W wyniku tej zamiany CubeMX wykasuje informacje o przypisanych przez nas nazwach (LED1 itd.), ale nie będziemy już sami odwoływać się do tych pinów, więc możemy zostawić domyślne wartości. To już wszystkie zmiany w projekcie, pozostaje nam go zapisać i przystąpić do zmian w kodzie. 

W przerwaniu HAL_TIM_PeriodElapsedCallback zostawiamy tylko sterowanie diodą LD2:

Tym razem nie będziemy „ręcznie” obsługiwać przerwań od kanałów licznika, więc usuwamy funkcję HAL_TIM_OC_DelayElapsedCallback. Zmieniamy też nieco uruchamianie naszego timera – zamiast funkcji HAL_TIM_OC_Start_IT użyjemy HAL_TIM_PWM_Start. Jak łatwo się domyślić, PWM odwołuje się do trybu, który teraz wykorzystamy, a pominięcie "_IT" sprawi, że przerwania nie będą zgłaszane.

Program jest już gotowy, możemy go skompilować i przetestować. Powinien działać identycznie jak poprzednia wersja. Widać jednak, że jest prostszy, a co ważniejsze, nasz mikrokontroler nie musi obsługiwać tak wielu przerwań.

Zmiana częstotliwości sygnału PWM

Dotychczas częstotliwość działania naszego timera była mała (1 Hz), bo chcieliśmy, aby efekt działania pierwszego programu był łatwy do zweryfikowania „gołym okiem”. Oczywiście można używać timera do takiego migania, ale raczej nie jest to najlepsze wykorzystanie cennych zasobów sprzętowych.

PWM przydaje się natomiast do sterowania jasnością świecenia diody – oczywiście pod warunkiem, że zwiększymy częstotliwość tego sygnału. Możemy to zrobić bardzo łatwo: wystarczy zmiana wartości preskalera, który przypisany jest do TIM3.

Niestety różnica w jasności będzie prawie niewidoczna. Wynika to z tego, że zależność jasności LED-ów od wypełniania PWM nie jest liniowa. W celu poprawy efektu należy (oprócz częstotliwości) zmienić też wartości Pulse, które są powiązane z kolejnymi kanałami (np. 50, 400 i 2000).

Zmiana wartości Pulse dla kanałów TIM3

Zmiana wartości Pulse dla kanałów TIM3

Przy takich ustawieniach różnica w świeceniu diod będzie znacznie lepiej widoczna. Warto tutaj zwrócić uwagę, że wartość 2000 oznacza raptem 20% wypełnienia, a dioda i tak świeci bardzo jasno.

Sterowanie diodą RGB

Doszliśmy do momentu, gdy potrafimy generować sprzętowo sygnał PWM na 3 kanałach licznika TIM3. Oczywiście nie jest to przypadkowe, bo dążyliśmy do tego, aby sterować diodą RGB. Zostajemy więc przy tym samym liczniku, a 3 pojedyncze diody wymieniamy na jeden LED RGB.

Schemat ideowy i montażowy układu testowego z diodą RGB

Schemat ideowy i montażowy układu testowego z diodą RGB

Dioda RGB dostarczana z zestawem ma wspólną anodę – nasz układ nie ma więc wspólnej masy, co jest typowym rozwiązaniem. Zamiast tego mamy wspólne zasilanie 3,3 V, a sterowanie jest odwrócone.

Moglibyśmy wykorzystać poprzednią konfigurację do sterowania, ale wtedy wartości byłyby odwrócone – wypełnienie 0 dawałoby maksymalne świecenie, a zwiększanie wartości zmniejszałoby jasność. Aby uzyskać bardziej intuicyjne działanie, możemy tutaj wykorzystać tryb PWM mode 2 albo zmienić polaryzację wyjścia, czyli opcję CH Polarity zmienić na Low. Użyjemy drugiej opcji, czyli nie będziemy zmieniać trybu PWM, odwrócimy tylko polaryzację kanału.

Zmiana polaryzacji sygnału PWM dla wszystkich 3 kanałów

Zmiana polaryzacji sygnału PWM dla wszystkich 3 kanałów

Dotychczas wypełnienie PWM, czyli jasność świecenia, ustawialiśmy na stałe w konfiguracji modułu. Teraz będziemy programowo sterować jasnością, więc do ustawiania wypełnienia PWM użyjemy makra __HAL_TIM_SET_COMPARE. Przyjmuje ono trzy parametry: 1) wskaźnik do struktury opisującej licznik (czyli zmiennej htim3), 2) numer kanału (np. TIM_CHANNEL_1) i 3) nowa wartość parametru Pulse.

Pozornie proste zadanie, jak sterowanie diodą RGB, wiąże się z kilkoma trudnościami. Pierwszą jest sama budowa takiej diody – po podłączeniu wszystkich 3 składowych zamiast białej barwy uzyskamy trzy punkty świecące odpowiednio na czerwono, zielono i niebiesko. Dioda RGB w rzeczywistości zawiera trzy struktury, z których każda świeci innym kolorem. Niestety są one widoczne jako oddzielne źródła światła i nie mieszają się zgodnie z oczekiwaniami.

Drugi problem to nieliniowość diody. Liniowe zwiększanie wypełniania PWM nie sprawia, że jasność LED-ów rośnie liniowo. Właściwie struktury RGB świecą już całkiem jasno, gdy ustawi się stosunkowo niewielkie wypełnienie sygnału PWM, a jego dalsze zwiększanie nie wpływa znacząco na jasność. Aby zlinearyzować działanie układu, możemy przeliczyć jasność na wypełnienie PWM poniższą funkcją (opis parametrów i więcej informacji na jej temat znaleźć można na Wikipedii).

Wzór opisujący jasność LED

Wzór opisujący jasność LED

Przyjmując oczekiwaną jasność wyrażoną w procentach (0–100%), możemy napisać następujący kod:

Aby funkcja ta mogła działać poprawnie, musimy jeszcze dodać jeden plik nagłówkowy:

Współczynniki 10000, k i x0 zostały dobrane eksperymentalnie w taki sposób, aby jasność diod była bardziej zbliżona do liniowej. Dzięki temu można stosunkowo łatwo osiągnąć ciekawe efekty wizualne. Z pomocą przyjdzie nam funkcja sinus. Nad pętlą while dodajemy deklarację zmiennej counter (typu int), a wewnątrz pętli umieszczamy poniższy kod:

W efekcie dostaniemy całkiem ładnie wyglądającą lampkę RGB – wszystko dzięki trygonometrii.

Obsługa enkodera obrotowego

W kilku kolejnych przykładach będziemy korzystali z enkodera obrotowego. Element ten przydaje się bardzo często, gdy chcemy, aby użytkownik mógł wygodnie poruszać się po menu lub aby możliwe było ustawianie jakiejś wartości. Moduł enkodera ma 5 wyprowadzeń: zasilanie, przycisk (sygnał o tym, że wciśnięto pokrętło) oraz 2 kanały oznaczane jako A/B lub DT/CLK (zależnie od modułu).

Moduł enkodera obrotowego

Moduł enkodera obrotowego

Zanim przejdziemy do eksperymentów z STM32L4, można bardzo łatwo przetestować działanie tego modułu bez mikrokontrolera. Wystarczy podłączyć do niego zasilanie (3,3 V), a do obu wyjść można podłączyć diody z odpowiednimi rezystorami.

Schemat ideowy i montażowy prostego układu testującego enkoder

Schemat ideowy i montażowy prostego układu testującego enkoder

W czasie powolnego obracania gałką enkodera powinniśmy zauważyć, że najpierw gaśnie jedna z diod, później druga, a po chwili obie znów świecą (przed osiągnięciem stabilnego położenia). Zależnie od kierunku obracania zmieniać będzie się kolejność gaśnięcia diod.

Zasada działania enkodera obrotowego

Zasada działania enkodera obrotowego

Podczas testu trzeba kręcić enkoderem powolutku, aby naocznie obserwować działanie tego prostego czujnika. W praktyce chcielibyśmy obracać osią szybciej, więc takie naoczne obserwowanie zmian na wyjściu układu to świetne zadanie dla mikrokontrolera, który będzie mierzył to, z jaką częstotliwością i w którym kierunku kręcimy gałką.

Zliczanie impulsów za pomocą licznika

Zaczniemy od nietypowego przykładu, bo wykorzystamy enkoder do tego, aby zobaczyć, jak liczniki radzą sobie z pomiarem sygnału dostarczanego z zewnątrz. Dotychczas timer TIM3 zawsze zliczał impulsy pochodzące od zegara, działał więc jako „czasomierz”, a nie jako licznik. Zobaczmy, jak można wykorzystać ten moduł jako prawdziwy licznik. Wróćmy do schematu blokowego i spójrzmy na kolejną jego część – tym razem mowa o sygnale wejściowym.

Kolejny element schematu blokowego licznika TIM3 (fragment schematu z dokumentacji)

Kolejny element schematu blokowego licznika TIM3 (fragment schematu z dokumentacji)

Źródłem sygnału, który do tej pory zliczał nasz licznik, był wbudowany zegar. Możemy jednak zmienić konfigurację i zliczać sygnał pochodzący z innego źródła, które będzie podłączone do pinu TIMx_ETR. Spróbujmy przetestować taką konfigurację – podłączamy zasilanie modułu z enkoderem, a jeden jego kanał, np. CLK, podłączamy do pinu PD2, który może pełnić właśnie taką funkcję.

Schemat ideowy i montażowy do testowania licznika impulsów

Schemat ideowy i montażowy do testowania licznika impulsów

Zapisujemy wcześniejszy projekt z diodą RGB i tworzymy nowy: STM32L476RG (80 MHz), debugger i USART2 w trybie asynchronicznym. Od razu dodajemy też przekierowanie komunikatów wysyłanych przez printf – tak samo, jak robiliśmy to w części o komunikacji STM32L4 przez UART.

Dodajemy poniższy nagłówek:

oraz kod zbliżony do poniższego:

Przechodzimy teraz do konfiguracji licznika TIM3. W CubeMX wybieramy Timers > TIM3, a następnie w polu Clock Source wybieramy ETR2 – od razu odpowiednio opisane zostanie wejście PD2. Inne parametry zostawiamy w tym momencie bez zmian. Zapisujemy projekt i generujemy kod. 

Konfiguracja nowego projektu z licznikiem TIM3

Konfiguracja nowego projektu z licznikiem TIM3

Teraz możemy przystąpić do napisania właściwego programu. Timer uruchomimy za pomocą funkcji HAL_TIM_Base_Start. Wcześniej używaliśmy funkcji HAL_TIM_Base_Start_IT, ale nie potrzebujemy już przerwań od timera, więc zastosujemy wersję bez "_IT" na końcu.

Potrzebujemy jeszcze funkcji, która odczyta wartość zliczoną przez licznik sprzętowy TIM3. Tutaj przyda się __HAL_TIM_GET_COUNTER. Jest to co prawda makro, ale dla nas nie będzie to stanowiło różnicy. Jako parametr podamy wskaźnik do zmiennej htim3, a w wyniku otrzymamy wartość licznika TIM3.

Teraz możemy przystąpić do napisania programu:

Po uruchomieniu zobaczymy efekt zbliżony do poniższego. Obracanie osi enkodera będzie wpływało na wskazywane wartości.

Pierwsze odczyty z enkodera podłączonego do STM32L4

Pierwsze odczyty z enkodera podłączonego do STM32L4

Program działa, ale ma dwie istotne wady. Po pierwsze, te same wartości są wypisywane mnóstwo razy, po drugie, obrót o jedno położenie najczęściej zmienia wartość o kilka jednostek. Pierwszy problem możemy wyeliminować metodą, którą wykorzystywaliśmy podczas pracy z przerwaniami – będziemy zapamiętywać poprzednią wartość licznika, a komunikat wypiszemy tylko po zmianie.

Nasz poprawiony program może więc wyglądać następująco:

Teraz zobaczymy komunikaty, tylko gdy odczytana wartość ulegnie zmianie, ale nadal będą pojawiały się sytuacje, w których jeden przeskok gałki enkodera będzie powodował zwiększanie wskazań naszej zmiennej o kilka wartości.

Efekt działania kolejnej wersji programu

Efekt działania kolejnej wersji programu

Przyczyną tych problemów są znane nam już drgania styków. Możemy sobie z nimi łatwo poradzić, jeśli dodamy jakiś filtr analogowy lub cyfrowy. Wystarczy, więc dodać do układu filtr RC, czyli jeden rezystor 1 k i kondensator 100 nF w takiej konfiguracji jak poniższa.

Schemat ideowy i montażowy enkodera z filtrem RC

Schemat ideowy i montażowy enkodera z filtrem RC

Taka zmiana sprawi, że nasz układ będzie działał stabilniej. Jednak nie zawsze dodawanie elementów jest tak proste… Możemy więc poprawić działanie programu przez włączenie cyfrowej filtracji danych. Wracamy do poprzedniej wersji układu, czyli odłączamy rezystor 1 k i kondensator 100 nF, a pin PD2 łączymy bezpośrednio z wyjściem CLK enkodera.

Następnie wracamy do perspektywy CubeMX i w konfiguracji modułu TIM3 spoglądamy na sekcję z parametrami. W grupie Clock odnajdujemy pole Clock filter (4 bits value) i wpisujemy tam 15 – jest to maksymalna wartość dla tego pola.

Włączenie cyfrowej filtracji dla wejścia licznika

Włączenie cyfrowej filtracji dla wejścia licznika

Teraz po uruchomieniu programu powinniśmy otrzymać wyniki podobne do poprzednich. Przeskoki mogą się pojawiać, jednak błędów jest o wiele mniej niż bez filtracji. W tym przykładzie zobaczyliśmy, że układ czasowo-licznikowy może działać faktycznie jako licznik. Warto więc zapamiętać, że sygnałem wejściowym dla licznika może być sygnał zegarowy, ale wcale nie musi (przydaje się to bardzo często).

Pomiar szerokości impulsu za pomocą licznika

Wykonamy teraz bardzo ważne ćwiczenie, do którego będziemy jeszcze w przyszłości wracali. Jak już mówiliśmy, podczas obracania osi enkodera diody podłączane do jego wyjść na chwilę gasły, a pomiar tego czasu pozwala m.in. na obliczenie prędkości obrotowej. Zmierzmy ten czas! Schemat jest prawie identyczny jak poprzedni, zmieniamy tylko pin, do którego podłączamy enkoder PA6 zamiast PD2.

Schemat ideowy i montażowy do pomiaru szerokości impulsu

Schemat ideowy i montażowy do pomiaru szerokości impulsu

Poprzednio licznik używał sygnału z zewnątrz, tym razem chcemy znowu mierzyć czas, więc wrócimy do taktowania licznika sygnałem zegarowym. Wykorzystamy jednak dodatkowo kanał nr 1 do zmierzenia czasu wystąpienia stanu niskiego w sygnale wejściowym. Pora zapoznać się z kolejnym fragmentem schematu blokowego naszego licznika.

Kolejny element schematu blokowego licznika (fragment schematu z dokumentacji)

Kolejny element schematu blokowego licznika (fragment schematu z dokumentacji)

Tym razem enkoder podłączymy do wejścia TIMx_CH1. Sygnał z wejścia będzie podawany na układ filtrujący i wykrywający, dzięki temu wykrycie dowolnego zbocza wywoła (jako sygnał TI1F_ED) reset licznika, czyli rozpoczęcie liczenia od zera. Po wykryciu zbocza narastającego wartość licznika zostanie zapamiętana w rejestrze kanału 1.

Wracamy do CubeMX, wybieramy licznik TIM3 i następujące ustawienia:

  • Slave Mode: Reset Mode – po wykryciu zmiany sygnału chcemy resetować licznik bazowy;
  • Trigger Source: TI1_ED – informacja o zmianie pochodzi z linii TI1F_ED;
  • Clock Source: Internal Clock – źródłem taktowania dla licznika bazowego jest zegar;
  • Channel 1: Input Capture direct mode – kanał 1 będzie zapamiętywał moment zwolnienia przycisku.

Musimy jeszcze odpowiednio zmienić parametry widoczne w dolnej części okna:

  • Prescaler: 7999 – dzielimy częstotliwość zegara przez 8000;
  • Polarity Selection: Rising Edge – zapamiętujemy zbocze narastające;
  • Input Filter (4 bits value): 15 – włączamy filtrowanie sygnału z 16 impulsów.
Nowe ustawienia dla licznika TIM3

Nowe ustawienia dla licznika TIM3

Na koniec przechodzimy do zakładki NVIC Settings i aktywujemy przerwanie od licznika TIM3. Do tego zmieniamy priorytet przerwania od tego timera na 10 (lub dowolną inną wartość różną od 0). Gdy konfiguracja jest już gotowa, możemy zapisać projekt i przystąpić do napisania programu.

Po pomiarze zostanie zgłoszone przerwanie i będzie wywołana funkcja HAL_TIM_IC_CaptureCallback. Możemy w niej odczytać wynik, używając funkcji HAL_TIM_ReadCapturedValue.

Nasz kod wygląda więc teraz następująco:

W programie głównym musimy uruchomić sam licznik. Użyjemy do tego funkcji HAL_TIM_Base_Start, ponieważ nie potrzebujemy przerwania po przepełnieniu licznika. Natomiast do uruchomienia pomiaru na kanale 1 wykorzystamy funkcję HAL_TIM_IC_Start_IT. Dodajemy też wyświetlanie wyników:

Po uruchomieniu tej wersji kodu widoczny będzie efekt zbliżony do poniższego. Im wolniej będziemy kręcić osią enkodera, tym większe wartości zobaczymy na ekranie. Czasami mogą pojawić się bardzo krótkie impulsy, których filtr cyfrowy nie dał rady wyeliminować.

Efekt działania programu mierzącego czas impulsu

Efekt działania programu mierzącego czas impulsu

Ten przykład jest ważny, ponieważ bardzo często chcemy zmierzyć czas trwania impulsu wejściowego pochodzącego np. z czujnika. W przypadku Arduino wystarczy w tym celu użyć funkcji pulseIn, ale jej działanie blokuje wykonywanie programu oraz przerwań, a wynik może być mało dokładny. Dzięki użyciu modułu sprzętowego mamy możliwość wykonania precyzyjnego pomiaru, który w dodatku odbywa się całkowicie sprzętowo.

Tryb obsługi enkoderów w STM32L4

Potrafimy już sprawdzać liczbę sygnałów z enkodera, mamy też informacje o prędkości. Moglibyśmy teraz zebrać te dane i stworzyć funkcję, która obsługiwałaby porządnie cały enkoder. Okazuje się jednak, że nie ma takiej potrzeby, bo liczniki wbudowane w mikrokontrolery STM32L4 posiadają tryb, który jest dedykowany właśnie do tego zadania.

Liczniki są wyposażone w tzw. tryby dla enkoderów. Praca w takim trybie oznacza, że licznik zbiera dane z dwóch kanałów i na tej podstawie oblicza pozycję enkodera. Zaczynamy od zmiany podłączenia: sygnał CLK z enkodera łączymy z pinem PA6, a drugi sygnał enkodera łączymy z PA7.

Schemat ideowy i montażowy dla przykładu z enkoderem

Schemat ideowy i montażowy dla przykładu z enkoderem

Wracamy do perspektywy CubeMX. Wyłączamy dotychczasowe ustawienia dla TIM3 (ustawiając opcję Diable w każdym polu), a następnie opcję Combined Channels ustawiamy na Enoder Mode.

Zmiana ustawień TIM3 i aktywacja trybu dla enkoderów

Zmiana ustawień TIM3 i aktywacja trybu dla enkoderów

Zapisujemy projekt i przechodzimy do edycji kodu. Najpierw usuwamy fragmenty związane z TIM3, które dodaliśmy podczas poprzednich ćwiczeń – nie będą nam już potrzebne. Do uruchomienia licznika użyjemy funkcji HAL_TIM_Encoder_Start, natomiast to, co najważniejsze, czyli pozycję enkodera, będziemy odczytywać za pomocą znanego nam już makra __HAL_TIM_GET_COUNTER.

W poprzednich przykładach odczytywane wartości były zawsze dodatnie, więc wybór typu zmiennych miał mniejsze znaczenie. Teraz jednak wygodniej nam będzie użyć typu int16_t – tak aby 16-bitowa wartość odczytana z rejestru licznika była reprezentowana w postaci liczby ze znakiem (obroty w lewo i prawo). Przykładowy kod może wyglądać następująco:

Po skompilowaniu i uruchomieniu tej wersji programu powinniśmy zobaczyć efekt zbliżony do tego, który widoczny jest na poniższym zrzucie ekranu. Wartości będą rosły lub malały – wszystko zależnie od kierunku obrotu (i sposobu podłączenia kanałów enkodera do mikrokontrolera).

Efekt działania trybu dedykowanego do obsługi enkoderów

Efekt działania trybu dedykowanego do obsługi enkoderów

Warto zwrócić uwagę, że odczyt działa poprawnie bez włączania filtracji. Dzieje się tak, ponieważ użycie dwóch sygnałów sprawiło, że drgania przestały mieć negatywny wpływ na działanie układu.

Testując działanie enkodera, zauważymy, że pełny obrót zmienia wynik o wartość 40. Czasem wygodnie jest tak skonfigurować moduł, że dane położenie zawsze zwraca tę samą wartość. Możemy wrócić do konfiguracji TIM3 i zmienić wartość parametru Counter Period na 39 (jak zwykle o 1 mniejszą).

Działanie programu po zmianie ustawień TIM3

Działanie programu po zmianie ustawień TIM3

Gdy teraz przetestujemy program, zobaczymy, że wartości zmieniają się od 0 do 39, nie otrzymujemy już wartości ujemnych, a wykonanie pełnego obrotu powoduje powrót do tej samej wartości. Dzięki temu jesteśmy w stanie łatwo wyznaczyć dokładną pozycję gałki enkodera.

Zadanie domowe

  1. Utwórz nowy projekt i uruchom w nim dwa podstawowe liczniki. Pierwszy z nich powinien migać zieloną diodą co 2 sekundy, a drugi żółtą diodą co 50 ms. Sterowanie diodami powinno być realizowane w całości za pomocą przerwań.
  2. Dodaj do projektu z punktu pierwszego kolejny licznik, który będzie sterował jasnością diody RGB. Napisz program, który będzie symulował pracę koguta policyjnego (płynne miganie czerwoną i niebieską strukturą diody RGB).
  3. Dodaj do projektu z punktu drugiego kolejny licznik i wykorzystaj go do obsługi enkodera. Edytuj program w taki sposób, aby kręcenie gałką enkodera wpływało na częstotliwość pracy koguta.

Podsumowanie – co powinieneś zapamiętać?

Omówiliśmy przydatne podstawy pracy z licznikami sprzętowymi. Po tej części kursu powinieneś już wiedzieć, jak generować przerwania co określony czas, jak generować PWM oraz jak za pomocą specjalnego trybu obsłużyć enkoder. W jednej z kolejnych części kursu wrócimy jeszcze do liczników i wykorzystamy je w trochę bardziej zaawansowanych przykładach.

Czy wpis był pomocny? Oceń go:

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

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 następnej części kursu zajmiemy się przetwornikami analogowo-cyfrowymi (ADC), dzięki którym do naszego mikrokontrolera będziemy mogli podłączyć przeróżne czujniki analogowe. Zajmiemy się m.in. obsługą joysticka, a do wizualizacji danych wykorzystamy oprogramowanie STM Studio.

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

enkoder, kurs, kursSTM32L4, liczniki, programowanie, stm32l4

Trwa ładowanie komentarzy...