Kurs STM32 LL cz. 12. Timer w trybie Output Compare i PWM

Output Compare to tryb licznika, który pozwala nam na zmianę stanu na wyjściu po odliczeniu przez licznik określonego czasu. Tę funkcjonalność możemy wykorzystać do generowania sygnału prostokątnego o określonej częstotliwości oraz impulsów na wyjściu o określonym czasie trwania, w tym sygnałów PWM.

Schemat blokowy wyjścia Output Compare

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

Na blok wyjścia timera wchodzi sygnał z rejestrów licznika i rejestru Capture/Compare. Wartość wpisana do rejestru CCR porównywana jest z licznikiem przez komparator. Informacja o osiągnięciu zadanej wartości przekazywana jest do kontrolera wyjścia i jednocześnie powoduje sygnalizację zdarzenia. 

Następnie sygnał jest przetwarzany przez selektor wyjść na podstawie ustawień trybu pracy. Wyjście może służyć do generowania sygnału na fizyczne wyprowadzenie mikrokontrolera lub jako sygnał wewnętrzny dla innego układu peryferyjnego. 

Następnie na podstawie konfiguracji polaryzacji generowany jest sygnał na wyjściu mikrokontrolera. Przy wystąpieniu zdarzenia wyjście może być ustawione w wybrany stan (wysoki lub niski) lub zostać przełączone na przeciwny do aktualnego (toggle).

Warto wspomnieć, że wartość rejestru CCR możemy w każdej chwili zaktualizować, zmieniając w trakcie działania programu moment wystąpienia zdarzenia. Przykład działania trybu Output Compare przedstawia grafika poniżej.

Procedura konfiguracji trybu Output Compare

Podobnie jak przy konfiguracji trybu Input Capture, tryb Output Capture również wymaga zachowania pewnych reguł, w tym konfiguracji rejestrów w odpowiedni sposób. Procedura została dość szczegółowo opisana w dokumentacji Reference Manual. Pokrótce przedstawię ją poniżej.

  1. W pierwszej kolejności konfigurujemy jednostkę bazową timera. Wybieramy źródło taktowania, kierunek zliczania oraz preskaler.
  2. Następnie ustawiamy wartość rejestru ARR (Auto-Reload), która określa do jakiej wartości maksymalnie zlicza licznik oraz wartość rejestru CCR, czyli wartość graniczną, przy której blok Output Compare będzie generował zdarzenie.
  3. Potem włączamy przerwania lub DMA, jeżeli chcemy z ich korzystać.
  4. Następnie wybieramy tryb pracy wyjścia, w tym wybór polaryzacji.
  5. Na koniec włączamy wykorzystywany kanał oraz timer.

Każde zrównanie się licznika CNT z wartością umieszczoną w CCR będzie generowało zdarzenie. 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 Output Compare

Rejestr TIMx_CCMR1 – wybór trybu pracy wyjścia Output Compare

Bity OC1M[3:0] – tryb pracy OC

Bit OC1S – konfiguracja kierunku kanału

Rejestr TIMx_CCER – rejestr do włączania kanałów

Bity CC1NP i CC1P – wybór polaryzacji

Bit CC1E – włączenie fizycznego wyjścia na pinie

Rejestr CCR1 – wartość licznika, od której na kanale nastąpi zdarzenie

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

[PROGRAM] Generowanie impulsu prostokątnego (polling)

Znamy już teorię i rejestry potrzebne do konfiguracji timera w trybie Output Compare. Czas na przykład praktyczny. W ćwiczeniu wykorzystamy timer do migania diodą. Tak, ponownie będziemy zmieniać stan na wyjściu, ale tym razem wykorzystamy timer i wszystko będzie działo się “samo” – bez wykorzystania funkcji LL_GPIO_TogglePin(). A w rzeczywistości na wyjściu otrzymamy sygnał prostokątny o częstotliwości 1 Hz. 

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 PA5 na porcie GPIOA, gdzie podłączona jest dioda LED. Musimy skonfigurować pin GPIO w trybie Alternate Function. Zgodnie z tabelą poniżej, TIM2_CH1 jest pinie PA5 dostępny w trybie AF2.

Uruchamiamy zatem taktowanie na GPIOA i konfigurujemy wyprowadzenie GPIO.

#define TIM2_CH1_OC_Pin LL_GPIO_PIN_5
#define TIM2_CH1_OC_Port GPIOA
 
LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOA);
LL_GPIO_SetPinPull(TIM2_CH1_OC_Port, TIM2_CH1_OC_Pin, LL_GPIO_PULL_NO);
LL_GPIO_SetPinSpeed(TIM2_CH1_OC_Port, TIM2_CH1_OC_Pin, LL_GPIO_SPEED_FREQ_LOW);
LL_GPIO_SetAFPin_0_7(TIM2_CH1_OC_Port, TIM2_CH1_OC_Pin, LL_GPIO_AF_2);
LL_GPIO_SetPinMode(TIM2_CH1_OC_Port, TIM2_CH1_OC_Pin, LL_GPIO_MODE_ALTERNATE);

Jednostkę bazową timera ustawimy w analogiczny sposób jak w poprzednim rozdziale. Preskaler będzie miał wartość 64 000, a rejestr ARR przyjmuje wartość 500. Dzięki temu stan na wyjściu będzie zmieniał się co 0,5 s, a więc sygnał prostokątny będzie miał okres 1 s, czyli częstotliwość 1 Hz. 

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, 500-1);
 
LL_TIM_GenerateEvent_UPDATE(TIM2);
LL_TIM_ClearFlag_UPDATE(TIM2);

Teraz czas na konfigurację kanału Output Compare. Ustawiamy wartość CCR1, która określi nam, w którym momencie nastąpi zdarzenie na wyjściu – w naszym przypadku będzie to zmiana stanu na przeciwny. Wpisując tutaj wartość o połowę mniejszą niż ARR sprawimy, że sygnał będzie zmieniał stan w połowie odliczania licznika.

uint32_t ccr1_value = LL_TIM_GetAutoReload(TIM2)/2;

LL_TIM_OC_SetCompareCH1(TIM2, ccr1_value);

Następnie ustawiamy tryb określający, że stan na wyjściu będzie zmieniał się na przeciwny oraz polaryzację.

LL_TIM_OC_SetMode(TIM2, LL_TIM_CHANNEL_CH1, LL_TIM_OCMODE_TOGGLE);
LL_TIM_OC_SetPolarity(TIM2, LL_TIM_CHANNEL_CH1, LL_TIM_OCPOLARITY_HIGH);

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

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

W pętli głównej nic się tym razem nie dzieje. Licznik po osiągnięciu wartości w CCR1 zmieni stan wyjścia na przeciwny, a po zliczeniu do wartości ARR zacznie odliczać od nowa. W taki sposób dioda zacznie migać.

while (1)
{
}

W ramach ćwiczenia warto poeksperymentować z wartościami ARR i CCR1. My w przykładzie uzyskaliśmy sygnał o częstotliwości 1 Hz – to mała wartość, ale dobrałem ją tak, abyśmy byli w stanie zauważyć zmianę stanu na diodzie. Jeżeli w innym projekcie potrzebny byłby sygnał prostokątny o znacznie większych częstotliwościach – wystarczy odpowiednio ustawić wartość preskalera, ARR i CCR1. Tym sposobem jesteśmy w stanie uzyskać sygnał prostokątny o częstotliwości nawet w MHz.

Tryb PWM

Odmianą trybu Capture Compare, który jest bardzo często używany w przypadku urządzeń wbudowanych, jest tryb PWM. PWM (Pulse Width Modulation) to sygnał prostokątny, w którym zmienna jest szerokość generowanego impulsu.

Najważniejszymi parametrami sygnału PWM jest:

  • częstotliwość – odwrotność czasu trwania, określa, ile razy w ciągu sekundy występuje impuls
  • szerokość impulsu (wypełnienie) – określa, przez jaki czas trwania całego impulsu aktywny jest stan wysoki

W odniesieniu do timerów w STM32, za częstotliwość sygnału odpowiada wartość preskalera oraz rejestru ARR (Auto-Reload). Obliczamy ją wg wzoru:

PWM_Freq = Timer_Freq / (Prescaler * Auto-Reload)

Wartość rejestru Auto-Reload będzie też określała rozdzielczość wypełnienia PWM. To oznacza, że im większa wartość w rejestrze ARR, tym więcej różnych wypełnień możemy wybrać. Przykładowo wpisując do ARR wartość 100 mamy możliwość generowania sygnału PWM z wypełnieniem od 0 do 100. Wpisując do ARR wartość 1000, możemy z “większą dokładnością” wybrać wypełnienie, bo od 0 do 1000. W pierwszym przypadku wypełnienie 25 będzie odpowiadało 25%, zaś w drugim przypadku wypełnienie 25 to 2,5% – aby uzyskać 25% trzeba wybrać wypełnienie 250.

Wartość wypełnienia, czyli czas, w którym aktywny na wyjściu jest stan wysoki, określa wartość wpisana w rejestrze Capture/Compare (CCR). Jeżeli ARR wynosi 100 i chcemy uzyskać wypełnienie 25%, wpisujemy 25 do CCR. Jeżeli ARR wynosi 1000 i chcemy uzyskać wypełnienie 25%, do CCR wpisujemy 250. 

Timer działa zatem w ten sposób, że do momentu osiągnięcia wartości CCR, generowany jest stan wysoki. Następnie licznik zlicza dalej, ale na wyjściu jest już stan niski. W momencie osiągnięcia wartości ARR licznik się resetuje do wartości 0 i stan na wyjściu także zmienia się na niski. Schematycznie przedstawione zostało to na poniższym diagramie (dla ARR = 8).

Możliwości generowania sygnałów PWM w STM32 są stosunkowo rozbudowane. Mamy do dyspozycji dwa tryby PWM, dwa tryby Asymmetric PWM oraz dwa tryby Combined PWM. Możemy generować sygnały z wyrównaniem do krawędzi lub do środka. W zależności od ustawień umożliwiają one generowanie złożonych sygnałów przesuniętych w fazie oraz zależnych od dwóch kanałów timera. My zajmiemy się dzisiaj najprostszym trybem PWM, czyli PWM mode 1.

[PROGRAM] Generowanie sygnału PWM (polling)

W ćwiczeniu ponownie wykorzystamy timer do migania diodą. tym razem jednak miganie będzie płynne – zmieniając wypełnienie PWM spowodujemy, że jasność diody również będzie się zmieniała.

Konfigurację zegara wykonujemy w analogiczny sposób jak w dotychczasowych przykładach, ustawiając sygnał HCLK i PCLK z częstotliwości 64 MHz. Do zadawania kolejnych wartości co 10 ms wykorzystamy timer programowy oparty o SysTick znany już z rozdziału o GPIO. Włączamy jego przerwania.

LL_SYSTICK_EnableIT();

Będziemy korzystali z fizycznego wejścia PA5 na porcie GPIOA, gdzie podłączona jest dioda LED. Musimy skonfigurować pin GPIO w trybie Alternate Function. Zgodnie z tabelą poniżej, TIM2_CH1 jest pinie PA5 dostępny w trybie AF2.

Uruchamiamy zatem taktowanie na GPIOA i konfigurujemy wyprowadzenie GPIO.

#define TIM2_CH1_OC_Pin LL_GPIO_PIN_5
#define TIM2_CH1_OC_Port GPIOA
 
LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOA);
LL_GPIO_SetPinPull(TIM2_CH1_OC_Port, TIM2_CH1_OC_Pin, LL_GPIO_PULL_NO);
LL_GPIO_SetPinSpeed(TIM2_CH1_OC_Port, TIM2_CH1_OC_Pin, LL_GPIO_SPEED_FREQ_LOW);
LL_GPIO_SetAFPin_0_7(TIM2_CH1_OC_Port, TIM2_CH1_OC_Pin, LL_GPIO_AF_2);
LL_GPIO_SetPinMode(TIM2_CH1_OC_Port, TIM2_CH1_OC_Pin, LL_GPIO_MODE_ALTERNATE);

Jednostkę bazową timera ustawimy w analogiczny sposób jak w poprzednim rozdziale. Preskaler będzie miał wartość 640, a rejestr ARR przyjmuje wartość 100. Dzięki temu uzyskamy sygnał PWM o częstotliwości 1 kHz.

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, 640-1);
LL_TIM_SetAutoReload(TIM2, 100-1);
 
LL_TIM_GenerateEvent_UPDATE(TIM2);
LL_TIM_ClearFlag_UPDATE(TIM2);

Teraz czas na konfigurację kanału Output Compare. Ustawiamy wartość CCR1, która określi nam wypełnienie. Będzie ono zależne od zmiennej pwm_duty.

uint32_t pwm_duty = 0;

LL_TIM_OC_SetCompareCH1(TIM2, pwm_duty);

Następnie ustawiamy tryb PWM1 oraz polaryzację.

LL_TIM_OC_SetMode(TIM2, LL_TIM_CHANNEL_CH1, LL_TIM_OCMODE_PWM1);
LL_TIM_OC_SetPolarity(TIM2, LL_TIM_CHANNEL_CH1, LL_TIM_OCPOLARITY_HIGH);

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

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

W pętli głównej będziemy co 10 ms zmieniać wypełnienie o 1 – raz w górę a potem w dół. otrzymamy w ten sposób płynne zaświecenie się diody i jej gaszenie. Dodajemy w pliku main.c definicje i zmienne pomocnicze.

#define LED_TASK_TIME	10
#define INCREMENT		0
#define DECREMENT		1
#define INC_DEC_STEP	1
 
uint32_t pwm_dir = INCREMENT;

Następnie tworzymy timer programowy i obsługujemy zwiększanie i zmniejszanie wypełnienia PWM.

software_timer_t led_timer;
software_timer_task_init(&led_timer, LED_TASK_TIME);
 
while (1)
{
	  if((software_timer_get_ms_tick() - led_timer.ms_tick) >= led_timer.task_time)
	  {
		led_timer.ms_tick = software_timer_get_ms_tick();
		if(pwm_dir == INCREMENT)
		 {
			pwm_duty += INC_DEC_STEP;
if(pwm_duty == (LL_TIM_GetAutoReload(TIM2)+1))
			  {
				  pwm_dir = DECREMENT;
			  }
		  }
		  else if(pwm_dir == DECREMENT)
		  {
			  pwm_duty -= INC_DEC_STEP;
			  if(pwm_duty == 0)
			  {
				  pwm_dir = INCREMENT;
			  }
		  }
		  LL_TIM_OC_SetCompareCH1(TIM2, pwm_duty);
	  }
}

Dodajemy jeszcze obsługę przerwania od SysTick-a.

void SysTick_Handler(void)
{
	software_timer_inc_ms_tick();
}

W ten sposób dioda LED na płytce Nucleo cyklicznie się zapala i gasi w płynny sposób. Warto zauważyć, że częstotliwość sygnału PWM jest dość duża w porównaniu z poprzednimi przykładami i wynosi 1 kHz. Do sterowania jasnością diody wykorzystujemy sygnał o większej częstotliwości dlatego, aby zmiany stanu na wyjściu odbywały się szybko (u nas 1000 razy na sekundę). Dzięki temu oko ludzkie nie widzi migotania diody, które byłoby widoczne przy małej częstotliwości. W poprzednim programie w rozdziale o trybie Output Compare częstotliwość była mała, a zmianę stanu wykorzystaliśmy do migania diodą. Tutaj stan musi się przełączać szybko, aby średnie napięcie na wyjściu było różnej wartości (odpowiadającej wypełnieniu PWM) – w ten sposób otrzymujemy różny poziom jasności. Analogicznie steruje się np. prędkością silnika przy pomocy mostków H.

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 *