Kurs STM32 LL cz. 10. Rodzaje i budowa Timerów, Timer w funkcji licznika

Timer to układ, którego zadaniem jest precyzyjne odmierzanie czasu. Podstawowym zadaniem jest zatem zliczanie impulsów – sygnału zegarowego lub z zewnętrznych wejść. Dodatkowo timery mają wbudowane funkcje związane z pomiarem czasu – pozwalają na określenie czasu trwania impulsów na wejściu oraz generowanie impulsów na wyjściu o określonej szerokości.

Rodzaje timerów w STM32G071RB

W mikrokontrolerach STM32 wyróżnia się kilka rodzajów układów czasowych. Zazwyczaj mamy do dyspozycji:

  • Advanced-control timer – zaawansowany układ czasowy
  • General-purpose timer – układ czasowy ogólnego przeznaczenia
  • Basic timer – podstawowy układ czasowy
  • Low-power timer – układ czasowy dedykowany do funkcji o niskim poborze mocy

W zależności od rodzaju timera, do dyspozycji mamy różne funkcjonalności, rodzaje kanałów oraz parametry takie, jak rozdzielczość, maksymalna częstotliwość taktowania, czy możliwość współpracy z DMA.

Advanced-control timer to najbardziej rozbudowane układy czasowe. Oprócz podstawowych funkcji, jak wejścia (Input Capture, PWM Input) i wyjścia (Compare Output, PWM), mają dodatkowe możliwości przeznaczone m.in. do sterowania silnikami trójfazowymi BLDC czy obsługi czujników obrotu. Zaawansowane timery mają rozdzielczość 32-bitową.

General-purpose timer to najczęściej używane układy czasowe. Mają do dyspozycji podstawowe funkcjonalności, czyli wejścia (Input Capture, PWM Input) i wyjścia (Compare Output, PWM). Mogą być 32-bitowe lub 16-bitowe i różnić się między sobą ilością kanałów czy funkcjami.

Basic timer to podstawowy układ czasowy. Nie mają kanałów wejściowych ani wyjściowych, a ich podstawowym zadaniem jest funkcja odliczania czasu. Stosowane jako podstawowe liczniki, mogą współpracować z innymi układami peryferyjnymi np. konwerterami DAC.

Low-power timer to układ czasowy dedykowany do funkcji o niskim poborze mocy. Mogą być taktowane za pomocą oscylatorów LSE lub LSI i pracować w trybach Low Power.

W przypadku STM32G071RBT6 mamy do dyspozycji 11 timerów. Dokładna specyfikacja przedstawiona została w tabeli poniżej.

W ramach kursu zajmiemy się podstawowymi funkcjami timer-ów. Omówimy możliwości pracy jako licznik, wejścia (Input Capture) i wyjścia (Output Compare, PWM). Do opracowania przykładów wykorzystamy układ TIM2, czyli timer ogólnego przeznaczenia.

Budowa układu czasowego

Poznaliśmy już rodzaje timerów w STM32. Możemy przejść do omówienia budowy układu czasowego ogólnego przeznaczenia (General-purpose timer), którym będziemy zajmować się w kursie.

Schemat timera ogólnego przeznaczenia można podzielić na 4 główne elementy: 

  • obwód wejścia sygnału taktującego
  • jednostkę podstawową
  • moduł wejść
  • moduł wyjść

Obwód wejścia sygnału taktującego odpowiada za prawidłowe dostarczenie źródła zegara do timera. 

Źródłem zegara może być sygnał wewnętrzny np. w przypadku TIM2 jest to sygnał z TIMPCLK, czyli sygnał taktujący szynę APB (PCLK) po przejściu przez blok mnożący. W przypadku innych timerów mogą to być sygnały bezpośrednio z oscylatorów HSI16, LSE, LSI lub bloku pętli PLL.

Układ timera może być również taktowany ze źródła zewnętrznego. W przypadku STM32G071RB wyróżnia się dwa tryby. W trybie 1 jako źródłem zegara jest sygnał podłączony do wejść (kanałów) timera. W trybie 2 źródłem zegara jest sygnał podłączony do wejścia ETR (External Trigger). Może nim być jeden z pinów mikrokontrolera. Dostępne możliwości dla poszczególnych timerów znajdziemy w tabeli 13. na stronie 48 w dokumentacji Datasheet, gdzie umieszczone są funkcje alternatywne każdego pinu.

Jednostka podstawowa układu czasowego realizuje funkcję liczenia. Składa się z licznika (CNT), preskalera (PSC) oraz rejestru autoprzeładowania (ARR). Za pomocą preskalera dostosowujemy “szybkość” liczenia. Przy preskalerze równym 1 licznik zwiększany jest przy każdym takcie zegara, przy większym odpowiednio co któryś takt. Rejestr ARR przechowuje wartość, do której chcemy aby zliczał licznik. Po osiągnięciu tej wartości licznik się przepełnia i generuje zdarzenie.

Każdy kanał jest zbudowany wokół rejestru przechwytywania/porównywania (Capture/Compare). W skład modułu wejściowego dodatkowo wchodzi filtr cyfrowy z detektorem zboczy, multiplekser oraz preskaler. W momencie pojawienia się sygnału na wyjściu modułu (za preskalerem) zliczona przez licznik (Counter) wartość przepisywana jest do rejestru Capture/Compare i może być odczytana jako zmierzony czas.

Moduł wyjściowy składa się z rejestru Capture/Compare, komparatora oraz kontrolera wyjścia. Komparator odpowiada za porównywanie wartości umieszczonej w rejestrze Capture/Compare z wartością w liczniku (Counter) i w momencie zrównania się tych wartości generowany jest sygnał na wyjściu.

Timer w funkcji licznika

Licznik to podstawowa funkcjonalność każdego układu czasowego. Za zliczanie odpowiedzialna jest jednostka podstawowa, która składa się z rejestru licznika (CNT), preskalera (PSC) oraz rejestru autoprzeładowania (ARR).

Liczniki w STM32 mogą być 16-bitowe lub 32-bitowe. TIM2, którym zajmujemy się w kursie jest 32-bitowym licznikiem, dzięki czemu daje nam większe możliwości konfiguracji mierzonego czasu. Licznik może odliczać w górę, w dół lub jednocześnie w górę i w dół. 

Preskaler służy do dzielenia sygnału taktującego układ czasowy, dzięki czemu mamy możliwość elastycznej konfiguracji zliczania czasu. W przypadku 32-bitowego licznika i taktowania 64 MHz, bez użycia preskalera mielibyśmy możliwość zliczania maksymalnie około 4294967295 ÷ 64000000 = 67 s, a przy liczniku 16-bitowym (znacznie więcej takich liczników mamy do dyspozycji) już tylko około 65536 ÷ 64000000 = 1 ms. Preskaler powoduje, że licznik zwiększa (lub zmniejsza) swoją wartość “co któryś” takt zegara, dzięki czemu możemy wydłużyć i dostosować mierzony czas do naszych potrzeb. Przykład zachowania się rejestru licznika przy zmianie preskalera z 1 na 4 przedstawia grafika poniżej.

Rejestr przeładowania (Auto-Reload Register) to miejsce, gdzie wpisujemy wartość, przy której licznik powinien się przepełnić i wygenerować zdarzenie. Przepełnienie w zależności od konfiguracji powoduje, że wartość rejestru CNT albo jest resetowana, albo zaczyna odliczać w przeciwnym kierunku. Poniżej przedstawiony jest przykład, gdy w rejestrze Auto-Reload umieścimy wartość 36.

Tryby zliczania

Licznik ma możliwość odliczania w jednym z trzech trybów: w górę (upcounting), w dół (downcounting) lub jednocześnie w górę i w dół (center-aligned mode).

Tryb zliczania w górę (upcounting)

W trybie zliczania w górę licznik zaczyna od 0 i zwiększa ją aż do osiągnięcia wartości umieszczonej w rejestrze Auto-Reload. Po przepełnieniu (overflow) wartość licznika jest resetowana do 0 i ponownie następuje jej zwiększanie. Każde przepełnienie może generować zdarzenie, które wykorzystywane jest np. jako źródło przerwania.

Tryb zliczania w dół (downcounting)

Tryb zliczania w dół jest odwrotnością pierwszego trybu. Tutaj odliczanie zaczyna się od wartości umieszczonej w rejestrze Auto-Reload i jest zmniejszana aż do osiągnięcia 0. Wówczas następuje niedomiar (underflow) i generowane jest zdarzenie. Licznik po resecie przyjmuje ponownie wartość z rejestru Auto-Reload.

Tryb zliczania w górę i dół (center-aligned mode)

Tryb zliczania w górę i w dół jest połączeniem obu wcześniejszych trybów. Licznik po uruchomieniu startuje od wartości 0 i zlicza w sposób analogiczny jak w trybie upcounting. Po osiągnięciu wartości umieszczonej w rejestrze Auto-Reload, zaczyna zaczyna zliczać w dół tak jak w trybie downcounting. Następnie ponownie zlicza w górę. Każde osiągnięcie przez licznik wartości 0 i tej z rejestru ARR powoduje wygenerowanie zdarzenia.

Tryb licznika – podstawowe rejestry

Poznaliśmy podstawy działania timera w funkcji licznika, czyli najprostszym zastosowaniu układu czasowego. Teraz omówimy rejestry potrzebne do skonfigurowania jego parametrów.

Rejestr TIM_CR1

Bity CMS[1:0] i DIR – konfiguracja trybu zliczania

Bit CEN – włączenie licznika

Rejestr TIM_CNT – stan licznika

Rejestr TIM_PSC – konfiguracja preskalera. Wartość wpisywana do rejestru powinna być o jeden mniejsza od tej, która wynika z obliczeń częstotliwości, ponieważ 0 w rejestrze oznacza dzielenie przez 1, 1 oznacza dzielenie przez 2, a 99 oznacza dzielenie przez 100.

Rejestr TIM_ARR – wartość, do której będzie zliczał licznik i generował zdarzenie

Rejestr TIM_EGR

Bit UG – programowe generowanie zdarzenia. Powoduje przeładowanie licznika

Rejestr TIM_SMCR

Bity SMS[2:0] i ECE – wybór źródła zegara taktującego timer

Rejestr TIM_SR

Bit UIF – flaga informująca o wystąpieniu zdarzenia przepełnienia/niedomiaru

Wszystkie projekty z kursu dostępne są w moim repozytorium GitHub.

[PROGRAM] Odliczanie czasu (polling)

Poznaliśmy podstawowe rejestry układu timer-a. Możemy napisać program, który będzie odliczał określony odcinek czasu i generował zdarzenia (na razie bez przerwania, dlatego w pętli głównej będziemy sprawdzali cyklicznie stan flagi).

Program będzie bazował na poprzednich rozdziałach. Konfigurację zegara wykonujemy w analogiczny sposób jak w dotychczasowych przykładach, ustawiając sygnał HCLK i PCLK z częstotliwości 64 MHz. Konfigurujemy też wyjście PA5, gdzie podłączona jest dioda.

Teraz przejdźmy do konfiguracji timer-a. W pierwszej kolejności włączamy taktowanie.

LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIM2);

Następnie ustawiamy źródło zegara na wewnętrzne, czyli bezpośrednio z PCLK (64 MHz).

LL_TIM_SetClockSource(TIM2, LL_TIM_CLOCKSOURCE_INTERNAL);

Ustawiamy tryb zliczania na zliczanie do góry (Upcounting).

LL_TIM_SetCounterMode(TIM2, LL_TIM_COUNTERMODE_UP);

Kolejno wpisujemy wartości do rejestrów preskalera i autoprzeładowania (Auto-Reload). Ponieważ częstotliwość taktowania timera to fCLK = 64 MHz, jeżeli chcemy uzyskać zdarzenie generowane co 1s musimy podzielić fCLK łącznie przez 64 000 000. Do rejestru preskalera możemy wpisać wartości od 0 do 65 535 (jest 16-bitowy). Przykładowo możemy tam umieścić wartość  64000-1.

LL_TIM_SetPrescaler(TIM2, 64000-1);

Teraz wartość rejestru ARR przyjmie ARR = fCLK / PSC czyli 1000. Do rejestru wpisujemy wartość o jeden mniejszą, ponieważ licznik zlicza od 0.

LL_TIM_SetAutoReload(TIM2, 1000-1);

Do obliczenia wartości ARR możemy też wykorzystać przydatne makro z bibliotek Low Layer, gdzie podajemy częstotliwość taktowania timera, wartość z preskalera i częstotliwość, jaką chcemy osiągnąć.

uint32_t timer_arr = __LL_TIM_CALC_ARR(64000000, 64000, 1);

Teraz możemy przeładować rejestr licznika, programowo wywołując zdarzenie “Update”.

LL_TIM_GenerateEvent_UPDATE(TIM2);

Następnie czyścimy flagę i uruchamiamy licznik.

LL_TIM_ClearFlag_UPDATE(TIM2);
LL_TIM_EnableCounter(TIM2);

W pętli głównej cyklicznie sprawdzamy flagę zdarzenia. jeżeli wystąpi, czyścimy ją i zmieniamy stan na LED.

while (1)
{
	if(LL_TIM_IsActiveFlag_UPDATE(TIM2) == 1)
	{
		LL_TIM_ClearFlag_UPDATE(TIM2);
		LL_GPIO_TogglePin(LED_GREEN_GPIO_Port, LED_GREEN_Pin);
	}
}

W ten sposób wykorzystaliśmy układ czasowy do odliczania czasu pomiędzy kolejnymi zmianami stanu na wyjściu. W efekcie dioda powinna zaświecać się i gasnać co 1 s.

Tryb licznika – rejestry przerwań

Rejestr TIM_DIER

Bit UIE – włączenie przerwania od zdarzenia “Update” (przepełnienie, niedomiar licznika)

Należy również pamiętać o skonfigurowaniu kontrolera przerwań NVIC.

[PROGRAM] Odliczanie czasu (interrupt)

Czas wykorzystać przerwania do obsługi zdarzenia od przepełnienia/niedomiaru licznika. Pozwolą one w lepszy sposób obsłużyć zdarzenie, a przede wszystkim bezpośrednio po jego wystąpieniu.

Program będzie bazował na poprzednich rozdziałach. Konfigurację zegara wykonujemy w analogiczny sposób jak w dotychczasowych przykładach, ustawiając sygnał HCLK i PCLK z częstotliwości 64 MHz. Podobnie jak dotychczas konfigurujemy też wyjście PA5, gdzie podłączona jest dioda.

Teraz przejdźmy do konfiguracji timer-a. W pierwszej kolejności włączamy taktowanie.

LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIM2);

Następnie ustawiamy źródło zegara na wewnętrzne, czyli bezpośrednio z PCLK (64 MHz).

LL_TIM_SetClockSource(TIM2, LL_TIM_CLOCKSOURCE_INTERNAL);

Ustawiamy tryb zliczania na zliczanie do góry (Upcounting).

LL_TIM_SetCounterMode(TIM2, LL_TIM_COUNTERMODE_UP);

Kolejno wpisujemy wartości do rejestrów preskalera i autoprzeładowania (Auto-Reload). Ponieważ częstotliwość taktowania timera to fCLK = 64 MHz, jeżeli chcemy uzyskać zdarzenie generowane co 1s musimy podzielić fCLK łącznie przez 64 000 000. Do rejestru preskalera możemy wpisać wartości od 0 do 65 535 (jest 16-bitowy). Przykładowo możemy tam umieścić wartość  64000-1.

LL_TIM_SetPrescaler(TIM2, 64000-1);

Teraz wartość rejestru ARR przyjmie ARR = fCLK / PSC czyli 1000. Do rejestru wpisujemy wartość o jeden mniejszą, ponieważ licznik zlicza od 0.

LL_TIM_SetAutoReload(TIM2, 1000-1);

Do obliczenia wartości ARR możemy też wykorzystać przydatne makro z bibliotek Low Layer, gdzie podajemy częstotliwość taktowania timera, wartość z preskalera i częstotliwość, jaką chcemy osiągnąć.

uint32_t timer_arr = __LL_TIM_CALC_ARR(64000000, 64000, 1);

Teraz możemy przeładować rejestr licznika, programowo wywołując zdarzenie “Update”.

LL_TIM_GenerateEvent_UPDATE(TIM2);

Następnie czyścimy flagę zdarzenia.

LL_TIM_ClearFlag_UPDATE(TIM2);

Czas na włączenie przerwań – zarówno w rejestrze timera, jak i w kontrolerze NVIC.

NVIC_SetPriority(TIM2_IRQn, 0);
NVIC_EnableIRQ(TIM2_IRQn);
LL_TIM_EnableIT_UPDATE(TIM2);

Mamy już wszystko przygotowane, zatem uruchamiamy licznik.

LL_TIM_EnableCounter(TIM2);

W przypadku przerwań sprawdzanie i czyszczenie flagi odbywać się będzie w obsłudze przerwania, a nie jak dotychczas w pętli głównej. Po sprawdzeniu flagi zmieniamy stan na LED.

void TIM2_IRQHandler(void)
{
	if(LL_TIM_IsActiveFlag_UPDATE(TIM2) == 1)
	{
		LL_TIM_ClearFlag_UPDATE(TIM2);
		LL_GPIO_TogglePin(LED_GREEN_GPIO_Port, LED_GREEN_Pin);
	}
}

W ten sposób wykorzystaliśmy układ czasowy do odliczania czasu pomiędzy kolejnymi zmianami stanu na wyjściu z obsługą przerwania. W efekcie dioda powinna zaświecać się i gasnać co 1 s. W ramach ćwiczeń warto poeksperymentować z wartościami preskalera i rejestru Auto-Reload oraz sprawdzić, czy wpływają one na częstotliwość migania diody zgodnie z oczekiwaniami.

Chciałbyś otrzymywać na bieżąco informacje o nowych artykułach z kursu? Zapisz się do newslettera!

TO NIE TYLKO MAIL Z INFORMACJĄ O NOWEJ LEKCJI, ALE TAKŻE DODATKOWE MATERIAŁY. NIE PRZEGAP NOWEJ TREŚCI I DODATKOWYCH BONUSÓW. PRZEJDŹ DO STRONY KURSU I PODAJ SWÓJ ADRES E-MAIL. NIE ZAPOMNIJ POTWIERDZIĆ CHĘCI DOŁĄCZENIA W PIERWSZEJ WIADOMOŚCI!
Repozytorium GitHub

Dodaj komentarz

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