Kurs STM32 F1 HAL – #7 – liczniki (timery) w praktyce, PWM

Kurs STM32 F1 HAL – #7 – liczniki (timery) w praktyce, PWM

Przed nami kolejna część kursu programowania STM32 z użyciem HAL. Tym razem poznamy podstawy sprzętowych liczników (timerów).

Po krótkim wstępie teoretycznym przejdziemy do ćwiczeń praktycznych, podczas których przyda nam się umiejętność generowania sygnału PWM.

Mikrokontrolery z rodziny STM32 znane są z rozbudowanych możliwości timerów. Jest to niewątpliwie ich zaletą, jednak dla początkujących liczba dostępnych opcji może wydawać się nieco przytłaczająca. W ramach tego kursu nie będziemy omawiać wszystkich możliwości  jakie dają układy licznikowe, skupimy się na tym co najważniejsze na początek - czyli głównie PWM.

Efekt działania jednego z programów testowych.

Efekt działania jednego z programów testowych.

Kiedy mówimy o timerach, najczęściej mamy na myśli odmierzanie czasu. Zanim więc zaczniemy na dobre poznawać liczniki musimy przypomnieć sobie podstaw oraz wykonać trochę obliczeń.

Liczniki w STM32 - podstawy teoretyczne

W przypadku sygnału okresowego s(t), czas T > 0 mierzony w sekundach jest okresem, jeśli dla każdego momentu t: s(t) = s(t + T), czyli co T sekund sygnał się powtarza. W elektronice często sekunda to bardzo długi czas, więc popularne jest używanie takich jednostek jak:

1 ms = 10-3 s
1 us = 10-6 s
1 ns = 10-9 s

Częstotliwość jest odwrotnością okresu: f = 1/T, jednostką jest Herz (Hz). Dla przypomnienia:

1 kHz = 103 Hz
1 MHz = 106 Hz
1 GHz = 109 Hz

Pracując z zegarami często będziemy przechodzić, albo zamiennie używać okresu i częstotliwości, warto więc zapamiętać następującą zależność:

CzęstotliwośćOkres
1Hz1s
1kHz1ms
1MHz1us

Dlaczego to takie ważne? Po prostu w pewnych momentach znana jest jedna z tych wartości, a wygodniej jest używać drugiej. Przykład: mikrokontroler na płytce Nucleo pracuje z częstotliwością maksymalną 64MHz. Jaki należy zastosować dzielnik, aby uzyskać okres 1ms?

No właśnie - parametrem jest częstotliwość, a my chcemy uzyskać przerwanie co 1ms, jak to przeliczyć? Możemy okres, czyli 1ms zamienić na częstotliwość - 1kHz, a następnie podzielić. 64MHz  przez 1kHz. Wynik to 64000 i taki dzielnik będzie nam potrzebny (w przykładach).

Zestaw elementów do kursu

Gwarancja pomocy na forum Błyskawiczna wysyłka

Zestaw ponad 120 elementów do przeprowadzenia wszystkich ćwiczeń z kursu można nabyć u naszych dystrybutorów! Dostępne są wersje z płytką Nucleo lub bez niej!

Kup w Botland.com.pl

Podstawa czasu w STM32

Na początek upewnimy się, czy nasze obliczenia są poprawne. Ponieważ nie każdy ma pod ręką oscyloskop, spróbujemy skonfigurować licznik tak, aby dokładnie co 1s otrzymywać przerwanie. Za pomocą zegarka będziemy mogli się przekonać, czy nasz układ działa poprawnie.

Jak zwykle pierwszy krok to podłączenie zegara do naszego timera:

Układ STM32F103 posiada 4 moduły timerów, my wykorzystamy TIM2 jako przykład. Kolejny krok to zdefiniowanie zmiennej z konfiguracją. Na początek ustawiamy podstawę czasu:

Tryb pracy to zliczanie do góry, czyli wewnętrzny licznik będzie liczył od 0 do wartości TIM_Period. Licznik zlicza od 0, więc gdybyśmy ustawili ten parametr na 1000, okres wynosiłby 1001.

Ponieważ zegar systemowy działa z częstotliwością 8 MHz, to do 1000 doliczy za szybko (otrzymamy częstotliwość 8 kHz, a chcieliśmy 1 Hz). Wykorzystamy więc preskaler, czyli dzielnik zegara wejściowego. Podzielimy 8 MHz przez 8000, dzięki czemu nasz timer będzie pracował z częstotliwością 1 kHz, więc do 999 doliczy dokładnie po 1s.

Jak sprawdzimy, czy mamy rację? Uruchomimy przerwanie wywoływane po przepełnieniu licznika:

Zdarzenie TIM_IT_Update jest generowane, gdy licznik modułu osiągnie wartość TIM_Period i zaczyna ponownie liczyć od zera. Nasz timer jednak jeszcze nie pracuje, uruchomimy go komendą:

Podczas obsługi przerwań od linii GPIO piasaliśmy kod łączący niskopoziomową obsługę przerwania z biblioteką HAL. Podobnie jest w przypadku timerów, po wystąpieniu przerwania sprzętowego, wywoływana jest funkcja TIM2_IRQHandler, która musi przekazać do biblioteki HAL informację o zgłoszonym przerwaniu. Niestety kod tej funkcji musimy napisać sami:

O ile przekierowywanie przerwań do biblioteki HAL jest niezbyt elegancko rozwiązane i w StdPeriph było nieco wygodniejsze, to dalsza obsługa przerwania jest w nowej bibliotece wykonywana na wyższym poziomie abstrakcji. Nie musimy sami sprawdzać bitów w odpowiednim rejestrze, aby dowiedzieć się które z przerwań związanych z timerem miało miejsce, ani zerować flag przerwania - to wszystko wykonuje za nas biblioteka.

Jedyne co musimy zrobić, to zaimplementować funkcje obsługi zdarzenia. Interesuje nas zdarzenie TIM_IT_Update, a do jego obsługi używana jest funkcja o nazwie HAL_TIM_PeriodElapsedCallback.

Wystarczy więc, że napiszemy jej kod:

Pełny kod programu:

Po uruchomieniu możemy sprawdzić, czy wszystko się zgadza. Dioda powinna świecić przez dokładnie jedną sekundę, po czym gasnąć na dokładnie taki sam czas. Jako ćwiczenie warto sprawdzić co stanie się, gdy zmienimy wartości parametrów TIM_Period i TIM_Prescaler.

Zadanie 7.1

Zmień program tak, aby dioda świeciła dokładnie 5 sekund, a następnie gasła na 5 sekund.

Zadanie 7.2

Wykonaj podobne ćwiczenie jak 7.1, ale ustaw czas na 100 ms.

Liczniki w STM32 - kanały

Układy licznikowe STM32 posiadają jeszcze dodatkowe 4 rejestry kanałów. Gdy główny licznik zrówna się z wartością rejestru danego kanału, można np.: wygenerować przerwanie (lub zmienić stan wyjścia).

Dzięki temu możemy otrzymać przerwanie co okres zegara oraz cztery dodatkowe przerwania po zadanym czasie. W poprzednim przykładzie ustawiliśmy okres zegara na 1s. Teraz spróbujemy za pomocą kanałów wygenerować przerwania po 100ms, 200ms, 500ms i 900ms.

Do czego to nam się może przydać? Na początku okresu zapalimy 4 diody. Po nadejściu każdego z przerwań będziemy gasić jedną z nich. Powinniśmy otrzymać 4 synchronicznie pracujące diody, świecące. Jest to duży krok w stronę sterowania silnikami poprzez PWM. Jednak dzięki diodom i długiemu okresowi pracy, możemy zobaczyć jak układ działa.

Na początek podłączamy diody podobnie jak w części kursu omawiającej GPIO, czyli do pinów PC0, PC1, PC2, PC3.

tim-leds_bb

Konfiguracja licznika przebiega tak jak poprzednio, trzeba tylko ustawić wartość rejestrów kanałów:

Następnie musimy włączyć przerwanie od zdarzenia zrównania licznika z rejestrem kanału (Compare Channel) - TIM_IT_CCx:

Najwięcej zmian musimy dokonać w procedurze obsługi przerwania. Sterowanie diodą na płytce Nucleo zostawimy jak poprzednio. Dodamy do tego sterowanie właśnie podłączonymi diodami.

Przy zdarzeniu Update zapalamy wszystkie 4 diody:

Natomiast po wystąpieniu zdarzenia TIM_IT_CCx, gasimy odpowiednią diodę. Do obsługi tego zdarzenia definiujemy funkcję o nazwie HAL_TIM_OC_DelayElapsedCallback:

Kod całego programu prezentuje się tak, jak poniżej:

Po uruchomieniu zobaczymy, że pierwsza dioda zapala się na bardzo krótko (100ms), kolejna nieco dłużej (200ms), itd.

Teraz spróbujmy zmienić preskaler, w tej chwili wynosi 8 000. Zmieńmy go przykładowo na 8:

Przy częstotliwości 1kHz nie widzimy już migania diod. Za to świecą one z różną jasnością – pierwsza najsłabiej, ostatnia najjaśniej (jeśli różnica jest mało widoczna, najlepiej zmniejszyć współczynniki wypełnienia).

Poprawiamy kod korzystający z kanałów licznika

Poprzedni program działał poprawnie, jednak w rzeczywistości trochę niechcący. Mianowicie konfiguracja kanałów naszego licznika była w dużym stopniu domyślna. Na nasze szczęście domyślne wartości w pełni nam odpowiadają, jednak dodajmy dla pewności do programu pełną konfigurację kanałów.

W tym celu potrzebujemy zmiennej typu TIM_OC_InitTypeDef. Nowy typ pozwoli nam skonfigurować kanały. Funkcję musimy wywołać cztery razy (dla każdego kanału osobno):

Ustawiamy tryb pracy kanału, na wartość TIM_OCMODE_TIMING. Pole Pulse, to wartość licznika naszego kanału (poprzednio jego wartość ustawialiśmy wywołaniem __HAL_TIM_SET_COMPARE). Pozostałe pola są dla nas w tej chwili nieistotne, ale powinniśmy przypisać wybrane wartości.

Gdy uruchomimy program, będzie on działał identycznie jak poprzednio. Możemy jednak mieć wpływ na konfigurację kanałów. Co wykorzystamy w kolejnym przykładzie.

Kod całego programu:

Zadanie 7.3

Wykonanie procedury obsługi przerwania w obecnej postaci zajmuje mikrokontrolerowi ok. 7.7us (zmierzona wartość). Przyjmując, że częstotliwość sterowania wynosi 20 kHz, oblicz ile procent czasu procesora zajmuje obsługa przerwań (dla 1 i dla 4 kanałów).

Generowanie PWM na STM32

Poprzednio do sterowania wyjściami procesora wykorzystywaliśmy przerwania. Takie rozwiązanie jest dość nieekonomiczne – procesor ma pełne ręce roboty obsługując przerwania w kółko zapalając i gasząc diody.

Na początek musimy wybrać wyprowadzenia, na których dostępny jest sprzętowy PWM. Pełny opis wyprowadzeń procesora jest dostępny w dokumentacji. Okazuje się, że wyprowadzenia TIM2, który używaliśmy wcześniej kolidują z konwerterem UART-USB. Można użyć remapowania i innych wyprowadzeń, ale my wykorzystamy łatwiejszą opcję i zmienimy timer.

Okazuje się, że timer TIM4 udostępnia wyjścia PWM na pinach PB6, PB7, PB8 oraz PB9, które są bez problemu dostępne. Przełączamy układ zgodnie z rysunkiem:

tim-pwm_bb

Jak zawsze pierwszym etapem jest podłączenie zegara, tym razem do TIM4.

Musimy również dodać konfigurację pinów PB6-9. Wykonamy to podobnie jak dotychczas, jednak użyjemy 2 nowych opcji. Ustawimy prędkość działania pinów na 50 MHz – domyślnie pracują z maksymalną prędkością 2MHz, jednak sprzętowy PWM może wymagać szybkich pinów.

Druga zmiana, to ustawienie trybu wyjścia jako GPIO_Mode_AF_PP, czyli funkcja alternatywna (PWM), w trybie push-pull. Kod wygląda następująco:

Konfiguracja podstawy czasu pozostaje praktycznie bez zmian (poza innym timerem):

Natomiast, to co nowe, to ustawienia kanałów. Tym razem mają pracować w trybie PWM:

Początkowe wartości PWM to 100, 200 i 500. Ponieważ pełny okres PWM wynosi 1000, więc odpowiada to wypełnieniu 10%, 20% i 50%. Pozostaje już tylko uruchomić timer:

Nasz program poza konfiguracją modułu już właściwie nic nie musi robić. Nie ma obsługi przerwań, program główny, to pusta pętla.

Cały kod programu:

Zadanie 7.4

Przygotuj program, który będzie płynnie zapalał i gasił kolejne diody.

Wykorzystanie PWM do sterowania diody RGB

Na zakończenie podłączymy diodę RGB i za pomocą PWM będziemy płynnie zmieniali kolor jej świecenia. Wykorzystamy TIM4, jak w poprzednim przykładzie, ale tym razem wystarczą nam 3 kanały.

Podłączamy układ zgodnie ze schematem montażowym:

tim-rgb_bb

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

Moglibyśmy wykorzystać poprzedni program do sterowania, ale wtedy wypełnienie sygnały byłoby również odwrócone. Wykorzystamy więc tryb PWM2, który działa dokładnie tak jak potrzebujemy - na początku okresu wystawia logiczne 0, a po zrównaniu licznika z rejestrem kanału wystawia 1. Działa więc jak odwrócony tryb PWM1.

Ponieważ będziemy chcieli płynnie zmieniać kolory, przyda nam się funkcja opóźniająca delay_ms, którą napisaliśmy w części o GPIO. Dodamy również obsługę przerwania SysTick. Do ustawiania jasności świecenia każdej składowej wykorzystamy makro __HAL_TIM_SET_COMPARE.

Okazuje się, że pozornie proste zadanie, jak sterowanie diodą RGB zawiera kilka niespodzianek. Pierwsza, to sama budowa diody. Po podłączeniu wszystkich 3 składowych zamiast białej barwy uzyskamy widoczne trzy punkty świecące odpowiednio na czerwono, zielono i niebiesko.

Dioda 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.

Najprościej poszukać odpowiedniego elementu z częściowo przezroczystego plastiku, można wydrukować coś ładnego w 3D, a jeśli nic nie znajdziemy, po prostu zasłonić diodę chusteczką.

Tymczasowe osłonięcie diody chusteczką (dla lepszego efektu).

Tymczasowe osłonięcie diody chusteczką (dla lepszego efektu).

Drugi problem, to znaczna nieliniowość jasności świecenia diody. Okazuje się, że zwiększając liniowo wypełnienie PWM, otrzymujemy bardzo nieliniową jasność świecenia diody. Poza bardzo małymi współczynnikami, dioda świeci właściwie z pełną jasnością.

Chcąc zlinearyzować jasność świecenia diody, musimy odpowiednio przeliczyć jasność na wypełnienie PWM. Możemy w tym celu wykorzystać funkcję:

Więcej na ten temat znaleźć można na Wikipedii.

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

Współczynniki 300, k oraz x0 są dobrane eksperymentalnie, zachęcam do ich zmiany i obserwacji wyników. Mając względnie liniową charakterystykę świecenia diody, możemy przygotować interesujący efekt wizualny. Z pomocą przychodzą nam funkcje trygonometryczne, np. sin().

Możemy napisać następujący program:

W efekcie dostaniemy całkiem ładnie wyglądającą lampkę RGB:

Kod programu:

Zadanie 7.5

Sprawdź, jak wygląda sterowanie jasnością świecenia diody bez funkcji calc_pwm. Najlepiej użyć jednego koloru i liniowo zmieniać wypełnienie - czy jasność świecenia diody też zmienia się liniowo? Wstaw w komentarzu film pokazujący rezultat.

Zadanie 7.6

Przygotuj inny sposób zmiany kolorów, wstaw w komentarzu filmik z rezultatem, który uzyskałeś.

Podsumowanie

Omówiliśmy tylko podstawowe możliwości układów licznikowych, w które wyposażony jest STM32F103RB. Warto chociaż wspomnieć o pozostałych funkcjach. Korzystając ze sprzętowych liczników mamy możliwość pomiaru szerokości impulsu. Pozwala to przykładowy na dokładny pomiar odległości popularnym czujnikiem HC-SR04. Co ważne dla osób budujących roboty, wśród dostępnych opcji, znajdziemy również sprzętowy interfejs do enkoderów oraz do obsługi czujników hall-a.

Dodatkowo pierwszy licznik ma dodatkowe możliwości, m.in. generowania PWM z wyjściem komplementarnym i programowanym czasem martwym (deadtime). Daje to możliwość bezpośredniego sterowania zarówno górnymi jak i dolnymi tranzystorami mostka H.

Nawigacja kursu

Autor kursu: Piotr Bugalski
Testy: Piotr Adamczyk
Redakcja: Damian Szymański

kursSTM32F1HAL, liczniki, PWM, stm32, timer

Komentarze

Komentarze do tego wpisu są dostępne na forum: