Kurs STM32 LL cz. 11. Timer w trybie Input Capture

Tryb Input Capture to funkcja licznika, która pozwala reagować na sygnały wpływające na wejście timera i mierzyć czas pomiędzy nimi. Dzięki temu jesteśmy w stanie w precyzyjny sposób dokonać pomiaru częstotliwości sygnałów, czasu trwania impulsów, czy pomiar parametrów sygnałów PWM.

Schemat blokowy wejścia Input Capture

Budowę bloku wejścia timera przedstawia poniższa grafika.

Wejściami timera mogą być różne sygnały – zarówno fizyczne wejście na porcie GPIO, jak i wejścia z wewnętrznych układów peryferyjnych np. komparatorów.

Sygnał z wejścia przekazywany jest do filtru. Mamy możliwość konfiguracji czasu trwania filtracji sygnału, dzięki czemu łatwo wyeliminujemy zakłócenia. Czas filtrowania ustawiamy w stosunku do sygnału podłączonego do timera. Na przykład, gdy przy przełączaniu sygnał wejściowy nie jest stabilny przez 5 wewnętrznych cykli zegara, musimy zaprogramować czas trwania filtra dłuższy niż te 5 cykli, czyli minimum 8.

Przefiltrowany sygnał wchodzi do detektora zboczy. Timer może reagować na zbocza narastające, opadające lub na oba zbocza jednocześnie. Należy pamiętać, że timer generuje jedno wspólne zdarzenia dla każdego zbocza – na zbocze narastające i opadające flaga jest ta sama, co nie pozwala ich rozróżnić z poziomu timera.

Sygnał na wyjściu detektora zboczy jest przekazywany do preskalera, gdzie mamy możliwość konfiguracji co które zbocze będzie powodowało zdarzenie. Sygnał z detektora może być również przekazany do innego timera ustawionego w trybie slave.

Każde zdarzenie na wejściu timera po przejściu przez cały blok wchodzi do bloku głównego kanału Capture/Compare. Tam wystąpienie zdarzenia sprawia, że wartość z licznika (Counter) przekazywana jest do rejestru Capture/Compare, z którego może zostać odczytany i wykorzystany do dalszych obliczeń.

Procedura konfiguracji trybu Input Capture

Konfiguracja trybu Input Capture wymaga zachowania pewnych reguł i kolejności ustawiania rejestrów w odpowiedni sposób, aby wywołać wymagane przez nas zachowanie. Procedura została dość szczegółowo opisana w dokumentacji Reference Manual. Pokrótce przedstawię ją poniżej.

  1. Idąc zgodnie z kierunkiem przepływu sygnałów w bloku Input Capture, w pierwszej kolejności powinniśmy wybrać źródło wejścia. Decydujemy, czy timer będzie reagował na sygnał zewnętrznych podany na pin GPIO, czy na sygnał wewnętrzny np. z komparatora.
  2. Następnie wybieramy aktywne wejście. Kanał timera (CCRx) może być połączony z wejściem TI1 lub TI2. Dzięki takiej elastyczności możemy podłączyć jedno wejście (np. TI1) do dwóch kanałów i otrzymać oddzielną reakcję na każde ze zboczy.
  3. Potem konfigurujemy czas filtracji w odniesieniu do sygnału podłączonego do timera.
  4. Kolejno wybieramy rodzaj zbocza dla danego kanału: narastające, opadające albo reakcję na oba jednocześnie.
  5. Teraz czas na ustawienie preskalera dla wejścia.
  6. Na koniec uruchamiamy kanał.

W przypadku, gdy korzystamy z przerwań lub kontrolera DMA, dodatkowo powinniśmy włączyć ich obsługę.

Każde zdarzenie na wejściu timera będzie generowało pojawienie się wartości licznika w rejestrze CCR (Capture Compare Register) danego kanału. Dodatkowo ustawiana będzie flaga zdarzenia CCxIF (Capture Compare Interrupt Flag) oraz w zależności od konfiguracji przerwanie lub żądanie DMA.

Rejestry w trybie Input Capture

Rejestr TIM2_TISEL – wybór rodzaju wejścia timera

Rejestr TIMx_CCMR1 – konfiguracja trybu pracy kanału Capture/Compare

Bity CC1S[1:0] – wybór kierunku pracy (wejście/wyjście) oraz konfiguracja aktywnego wejścia na kanale

Bity IC1PSC[1:0] – wybór preskalera na wejściu

Bity IC1F[3:0] – konfiguracja filtru wejścia

Rejestr TIMx_CCER – rejestr do włączania i wyłączania kanałów timera

Bity CC1NP i CC1P – wybór zbocza wykrywanego na wejściu

Bit CC1E – włączenie wyjścia/wejścia kanału

Rejestr TIMx_CCR1 – rejestr, do którego kopiowana jest wartość licznika w momencie wystąpienia zdarzenia na wejściu

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

[PROGRAM] Pomiar czasu między zboczami (polling)

Czas zastosować tryb Input Capture w praktyce. W programie wykorzystamy go do odliczania czasu pomiędzy kolejnymi naciśnięciami przycisku, a konkretnie między kolejnymi zboczami narastającymi.

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.

Będziemy korzystali z fizycznego wejścia PA0 na porcie GPIOA, dlatego musimy skonfigurować pin GPIO w trybie Alternate Function. Zgodnie z tabelą poniżej, TIM2_CH1 jest pinie PA0 dostępny w trybie AF2.

Uruchamiamy zatem taktowanie na GPIOA i konfigurujemy wyprowadzenie GPIO.

#define TIM2_CH1_IC_Pin LL_GPIO_PIN_0
#define TIM2_CH1_IC_Port GPIOA
 
LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOA);
LL_GPIO_SetPinPull(TIM2_CH1_IC_Port, TIM2_CH1_IC_Pin, LL_GPIO_PULL_NO);
LL_GPIO_SetPinSpeed(TIM2_CH1_IC_Port, TIM2_CH1_IC_Pin, LL_GPIO_SPEED_FREQ_LOW);
LL_GPIO_SetAFPin_0_7(TIM2_CH1_IC_Port, TIM2_CH1_IC_Pin, LL_GPIO_AF_2);
LL_GPIO_SetPinMode(TIM2_CH1_IC_Port, TIM2_CH1_IC_Pin, LL_GPIO_MODE_ALTERNATE);

Jednostkę bazową timera ustawimy w analogiczny sposób jak w poprzednim rozdziale. Preskaler będzie w tym przypadku decydował, jak dużą dokładność pomiaru czasu będziemy mieli. W przypadku przycisku nie zależy nam na bardzo dużej dokładności – ustawiając preskaler jako 64 000 uzyskamy pomiar z dokładnością do 1 ms. Chcą zwiększyć dokładność np. do 1 us, należałoby ustawić mniejszy preskaler (w tym wypadku 64).

Wartość rejestru ARR decyduje natomiast o tym, do jakiej maksymalnej wartości licznik CNT będzie mógł liczyć. Nie chcemy jej ograniczać, ponieważ im większy zakres, tym będziemy w stanie mierzyć dłuższe czasy. Ustawiamy więc w rejestrze Auto Reload wartość maksymalną dla TIM2, czyli dla rejestru 32-bitowego 0xFFFFFFFFUL. Wykonamy to za pomocą przyjaznego makra UINT32_MAX dostępnego w bibliotece stdint.h.

LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIM2);
 
LL_TIM_SetClockSource(TIM2, LL_TIM_CLOCKSOURCE_INTERNAL);
LL_TIM_SetCounterMode(TIM2, LL_TIM_COUNTERMODE_UP);
LL_TIM_SetPrescaler(TIM2, 64000-1);
LL_TIM_SetAutoReload(TIM2, UINT32_MAX);
 
LL_TIM_GenerateEvent_UPDATE(TIM2);
LL_TIM_ClearFlag_UPDATE(TIM2);

Teraz czas na konfigurację kanału Input Capture. Zgodnie z przedstawioną procedurą, najpierw wybieramy wejście TI1 jako GPIO.

LL_TIM_SetRemap(TIM2, LL_TIM_TIM2_TI1_RMP_GPIO);

Następnie ustawiamy aktywne wejście dla kanału CCR1 jako TI1.

LL_TIM_IC_SetActiveInput(TIM2, LL_TIM_CHANNEL_CH1, LL_TIM_ACTIVEINPUT_DIRECTTI);

Kolejno konfigurujemy filtr z 1 cyklem.

LL_TIM_IC_SetFilter(TIM2, LL_TIM_CHANNEL_CH1, LL_TIM_IC_FILTER_FDIV1);

Ustawiamy również polaryzację, czyli rodzaj zbocza.

LL_TIM_IC_SetPolarity(TIM2, LL_TIM_CHANNEL_CH1, LL_TIM_IC_POLARITY_RISING);

Teraz czas na wybór preskalera dla wejścia IC jako 1, czyli bez dzielenia sygnału wejściowego.

LL_TIM_IC_SetPrescaler(TIM2, LL_TIM_CHANNEL_CH1, LL_TIM_ICPSC_DIV1);

Na koniec włączamy kanał oraz cały licznik.

LL_TIM_CC_EnableChannel(TIM2, LL_TIM_CHANNEL_CH1);
LL_TIM_EnableCounter(TIM2);

W tym przykładzie wykorzystujemy tryb polling, czyli będziemy odczytywać flagę zdarzenia cyklicznie w pętli głównej. Przy ustawieniu flagi od zdarzenia na kanale 1 Capture/Compare, będziemy czyścili flagę i odczytywali licznik kanału CCR1. Na koniec możemy wyzerować licznik timera (CNT), dzięki czemu przy każdym wciśnięciu przycisku będziemy mieli gotową wartość od ostatniego zdarzenia.

while (1)
{
	if(LL_TIM_IsActiveFlag_CC1(TIM2) == 1)
	{
		LL_TIM_ClearFlag_CC1(TIM2);
		push_diff_time_ms = LL_TIM_IC_GetCaptureCH1(TIM2);
		LL_TIM_SetCounter(TIM2, 0UL);
	}
}

Zmienną push_diff_time_ms deklarujemy jako globalna (przed funkcją main(void)).

uint32_t push_diff_time_ms = 0;

Aby sprawdzić działanie programu najłatwiej będzie wykorzystać okno debugowania programu. Najpierw jednak powinniśmy podłączyć przycisk do pinu PA0. 

Możemy to zrobić na dwa sposoby – podłączyć zewnętrzny przycisk, pamiętając o odpowiednim filtrowaniu w postaci kondensatora i rezystora np. według schematu poniżej.

Możemy też wykorzystać przycisk USER umieszczony na płytce Nucleo. Wystarczy, że zewrzemy pin PC13, do którego podłączony jest przycisk, z PA0 np. za pomocą zwykłego przewodu.

Teraz kompilujemy projekt. Jeżeli nie pojawiły się żadne błędy, wchodzimy w tryb Debug poprzez wybór z menu opcji Run->Debug (F11). Powinna otworzyć się perspektywa Debug.

W oknie “Live expression” dodajemy zmienną i uruchamiamy program. Po każdym wciśnięciu przycisku pojawi się nam czas w milisekundach odliczony od poprzedniego wciśnięcia.

W analogiczny sposób możemy odliczać czas pomiędzy kolejnymi impulsami np. w przypadku sygnału prostokątnego i po odpowiednich przeliczeniach otrzymać jego częstotliwość.

[PROGRAM] Pomiar czasu między zboczami (interrupt)

Odczytywanie flagi w pętli głównej może być problematyczne i powodować opóźnienia – w szczególności, gdy aplikacja wykonuje wiele rzeczy. Dlatego w tym programie wykorzystamy przerwania w momencie wystąpienia zdarzenia na wejściu tiemra. Program posłuży nam do tego samego celu co poprzednio – do odliczania czasu pomiędzy kolejnymi naciśnięciami przycisku (zboczami narastającymi).

Konfigurację zegara wykonujemy w analogiczny sposób jak w dotychczasowych przykładach, ustawiając sygnał HCLK i PCLK z częstotliwości 64 MHz.

Analogicznie jak wcześniej, będziemy korzystali z fizycznego wejścia PA0 na porcie GPIOA, dlatego musimy skonfigurować pin GPIO w trybie Alternate Function. Zgodnie z tabelą poniżej, TIM2_CH1 jest pinie PA0 dostępny w trybie AF2.

Uruchamiamy taktowanie na GPIOA i konfigurujemy wyprowadzenie GPIO.

#define TIM2_CH1_IC_Pin LL_GPIO_PIN_0
#define TIM2_CH1_IC_Port GPIOA
 
LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOA);
LL_GPIO_SetPinPull(TIM2_CH1_IC_Port, TIM2_CH1_IC_Pin, LL_GPIO_PULL_NO);
LL_GPIO_SetPinSpeed(TIM2_CH1_IC_Port, TIM2_CH1_IC_Pin, LL_GPIO_SPEED_FREQ_LOW);
LL_GPIO_SetAFPin_0_7(TIM2_CH1_IC_Port, TIM2_CH1_IC_Pin, LL_GPIO_AF_2);
LL_GPIO_SetPinMode(TIM2_CH1_IC_Port, TIM2_CH1_IC_Pin, LL_GPIO_MODE_ALTERNATE);

Jednostkę bazową timera ustawimy w analogiczny sposób jak w poprzednim rozdziale. Preskaler będzie w tym przypadku decydował, jak dużą dokładność pomiaru czasu będziemy mieli. W przypadku przycisku nie zależy nam na bardzo dużej dokładności – ustawiając preskaler jako 64 000 uzyskamy pomiar z dokładnością do 1 ms. Chcą zwiększyć dokładność np. do 1 us, należałoby ustawić mniejszy preskaler (w tym wypadku 64).

Wartość rejestru ARR decyduje natomiast o tym, do jakiej maksymalnej wartości licznik CNT będzie mógł liczyć. Nie chcemy jej ograniczać, ponieważ im większy zakres, tym będziemy w stanie mierzyć dłuższe czasy. Ustawiamy więc w rejestrze Auto Reload wartość maksymalną dla TIM2, czyli dla rejestru 32-bitowego 0xFFFFFFFFUL. Wykonamy to za pomocą przyjaznego makra UINT32_MAX dostępnego w bibliotece stdint.h.

LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIM2);
 
LL_TIM_SetClockSource(TIM2, LL_TIM_CLOCKSOURCE_INTERNAL);
LL_TIM_SetCounterMode(TIM2, LL_TIM_COUNTERMODE_UP);
LL_TIM_SetPrescaler(TIM2, 64000-1);
LL_TIM_SetAutoReload(TIM2, UINT32_MAX);
 
LL_TIM_GenerateEvent_UPDATE(TIM2);
LL_TIM_ClearFlag_UPDATE(TIM2);

Teraz czas na konfigurację kanału Input Capture. Zgodnie z przedstawioną procedurą, najpierw wybieramy wejście TI1 jako GPIO.

LL_TIM_SetRemap(TIM2, LL_TIM_TIM2_TI1_RMP_GPIO);

Następnie ustawiamy aktywne wejście dla kanału CCR1 jako TI1.

LL_TIM_IC_SetActiveInput(TIM2, LL_TIM_CHANNEL_CH1, LL_TIM_ACTIVEINPUT_DIRECTTI);

Kolejno konfigurujemy filtr z 1 cyklem.

LL_TIM_IC_SetFilter(TIM2, LL_TIM_CHANNEL_CH1, LL_TIM_IC_FILTER_FDIV1);

Ustawiamy również polaryzację, czyli rodzaj zbocza.

LL_TIM_IC_SetPolarity(TIM2, LL_TIM_CHANNEL_CH1, LL_TIM_IC_POLARITY_RISING);

Teraz czas na wybór preskalera dla wejścia IC jako 1, czyli bez dzielenia sygnału wejściowego.

LL_TIM_IC_SetPrescaler(TIM2, LL_TIM_CHANNEL_CH1, LL_TIM_ICPSC_DIV1);

Włączamy przerwania w kontrolerze NVIC i rejestrze timera.

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

Na koniec włączamy kanał oraz cały licznik.

LL_TIM_CC_EnableChannel(TIM2, LL_TIM_CHANNEL_CH1);
LL_TIM_EnableCounter(TIM2);

Ze względu na to, że wykorzystamy przerwania, odczytywać flagę będziemy w funkcji TIM2_IRQHandler() obsługującej przerwanie. Przy ustawieniu flagi od zdarzenia na kanale 1 Capture/Compare, będziemy czyścili flagę i odczytywali licznik kanału CCR1. Na koniec możemy wyzerować licznik timera (CNT), dzięki czemu przy każdym wciśnięciu przycisku będziemy mieli gotową wartość od ostatniego zdarzenia.

void TIM2_IRQHandler(void)
{
	if(LL_TIM_IsActiveFlag_CC1(TIM2) == 1)
	{
		LL_TIM_ClearFlag_CC1(TIM2);
		push_diff_time_ms = LL_TIM_IC_GetCaptureCH1(TIM2);
		LL_TIM_SetCounter(TIM2, 0UL);
	}
}

Zmienną push_diff_time_ms deklarujemy jako globalna (przed funkcją handlera).

uint32_t push_diff_time_ms = 0;

Aby sprawdzić działanie programu najłatwiej będzie wykorzystać okno debugowania programu. Ponownie musimy podłączyć przycisk do pinu PA0. 

Kompilujemy projekt i jeżeli nie pojawiły się żadne błędy wchodzimy w tryb Debug poprzez wybór z menu opcji Run->Debug (F11). Powinna otworzyć się perspektywa Debug.

W oknie “Live expression” dodajemy zmienną i uruchamiamy program. Po każdym wciśnięciu przycisku pojawi się nam czas w milisekundach odliczony od poprzedniego wciśnięcia.

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 *