Funkcje API w obsłudze przerwań
SysTick Timer
Zadaniem każdego systemu operacyjnego jest takie zarządzenie uruchomionymi wątkami, aby wszystkie zostały optymalnie obsłużone. Można to zrealizować na kilka sposobów. Jednym z nich jest cykliczne – w pewnych określonych ramach czasowych – przełączanie kontekstu zadań. Implementacje tego typu systemów operacyjnych w mikrokontrolerach z rdzeniem Cortex M3 inżynierowie z firmy ARM znacznie uprościli wbudowując w kontroler przerwań NVIC timer SysTick. Jest to 24–bitowy licznik, który zliczając w dół odmierza określone, zdefiniowane przez programistę interwały czasowe. Każde przejście licznika przez zero i rozpoczęcie nowego cyklu odliczania powoduje wygenerowanie przerwania, które może zajmować się przełączaniem kontekstów uruchomionych w systemie zadań. Takie podejście ma też ważną zaletę, mianowicie zawieszenie się któregoś z realizowanych zadań nie spowoduje zawieszenia całego systemu, ponieważ zawieszony proces zostanie przerwany na rzecz innych zadań przy najbliższym przepełnieniu timera SysTick.
List. 2. Procedura odmierzania czasu z wykorzystaniem timera SysTick
int main(void) { RCC_Conf(); GPIO_Conf(); NVIC_Conf(); /* SysTick bedzie taktowany z f = 72MHz/8 = 9MHz */ SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); /* Przerwanie ma byc co 1ms, f = 9MHz czyli liczy od 9000 */ SysTick_SetReload(9000); /* Odblokowanie przerwania od timera SysTick */ SysTick_ITConfig(ENABLE); /* Wlaczenie timera */ SysTick_CounterCmd(SysTick_Counter_Enable); while (1); } /******* zawartosc pliku stm32f10x.it.c *******/ /* Funkcja oblugi przerwania od SysTick timer */ void SysTickHandler(void) { if(TDelay == 500) //co 500ms { TDelay = 0; /* zmienia stan portu na przeciwny */ GPIO_Write(GPIOC, (u16)~GPIO_ReadOutputData(GPIOC)); } TDelay++; }
Wbudowanie oddzielnego układu czasowego w architekturę Cortex pozwala na dużo łatwiejsze przenoszenie aplikacji napisanych na mikrokontrolery różnych producentów wykorzystujących tę platformę. Dzieje się tak dlatego, ponieważ w każdym takim mikrokontrolerze znajduje się taki sam timer SysTick.
Oczywiście SysTick może być wykorzystywany do różnych zadań. Konstruktor ustala wartość, od jakiej timer ma liczyć w dół, tym samym wyznacza, jakie odcinki czasowe będą odmierzane. Widać zatem, że może być on również wykorzystywany do standardowego odmierzania czasu i dla takiego przypadku, pokazanego na list. 2, zostanie omówiona konfiguracja oraz uruchomienie timera SysTick. Aby jakikolwiek układ czasowy w ogóle działał, musi być taktowany jakimś sygnałem zegarowym. SysTick może zliczać impulsy z taką częstotliwością, z jaką pracuje rdzeń mikrokontrolera, lub z tą częstotliwością podzieloną przez osiem. Ta ostatnia opcja jest zastosowana w omawianym przypadku, mamy więc 9 MHz. Licznik liczy w dół, więc regulacja odcinków czasowych, po których są generowane przerwania obywa się poprzez ustawienie wartości początkowej, czym zajmuje się funkcja SysTick_SetReload(). Do poprawnej pracy timera pozostaje jeszcze odblokowanie jego przerwania i włączenie odliczania. Funkcja obsługująca przerwanie to SysTickHandler, jej cykliczne wywoływania powodują odliczenie żądanego czasu i okresową zmianę stanu portu GPIOC, a tym samym miganie diodami LED.
Pod względem wyposażenia w układy czasowo–licznikowe, mikrokontrolery STM32 prezentują się dość okazale. W sumie mamy do dyspozycji aż siedem timerów (wliczając SysTick), które mogą pracować w różnych konfiguracjach. Ponieważ możliwość odmierzania czasu została już przedstawiona przy okazji omawiania timera SysTick, to teraz zajmiemy się generacją czterech sygnałów PWM o różnym współczynniku wypełnienia. Do tego celu wykorzystamy Timer 3 wraz z jego czterema kanałami. Pozwoli to na wygenerowanie żądanych sygnałów, z tym, że rzecz jasna będą one miały taki sam okres. Fragment programu odpowiedzialny za odpowiednie skonfigurowanie układu licznikowego przedstawiono na list. 3. Oprócz standardowej konfiguracji sygnałów zegarowych, która jest wykonywana przez funkcję RCC_Conf, należy włączyć taktowanie timera i GPIO (funkcja GPIO_Conf, której zadaniem jest pełne przemapowanie Timera 3 jest identyczna z opisaną w artykule).
List. 3. Fragment programu odpowiedzialny za skonfigurowanie układu licznikowego
int main(void) { RCC_Conf(); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); NVIC_Conf(); GPIO_Conf(); // Konfiguracja Timera 3 TIM_TimeBaseStruct.TIM_Period = 999; TIM_TimeBaseStruct.TIM_Prescaler = 0; TIM_TimeBaseStruct.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStruct); // Konfiguracja kanalu 1 TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStruct.TIM_Pulse = 950; TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM3, &TIM_OCInitStruct); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); // Konfiguracja kanalu 2 TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStruct.TIM_Pulse = 350; TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC2Init(TIM3, &TIM_OCInitStruct); TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); // Konfiguracja kanalu 3 TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStruct.TIM_Pulse = 250; TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC3Init(TIM3, &TIM_OCInitStruct); TIM_OC3PreloadConfig(TIM3, TIM_OCPreload_Enable); // Konfiguracja kanalu 4 TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStruct.TIM_Pulse = 100; TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC4Init(TIM3, &TIM_OCInitStruct); TIM_OC4PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM3, ENABLE); // Wlaczenie Timera 3 TIM_Cmd(TIM3, ENABLE); while(1); }
W następnych krokach definiujemy podstawowe parametry pracy licznika. Będzie on pracował jako licznik w górę z częstotliwością taktowania 36 MHz (nie dzielimy sygnału zegarowego i nie wykorzystujemy preskalera). Wartość, do jakiej będzie liczył timer to 999, po czym nastąpi przepełnienie i proces liczenia rozpocznie się od nowa. Przy takich ustawieniach częstotliwość generowanego przebiegu PWM wyniesie: f=36 [MHz]/(999+1)=36 [kHz]. Kolejnym krokiem jest konfiguracja poszczególnych kanałów timera. Informujemy MCU o tym, że poszczególne kanały będą pracować w trybie PWM, długość impulsu będzie wynosiła odpowiednio (licząc od kanału pierwszego): 950, 350, 250, 100 taktów licznika. Aktywnym stanem jest stan wysoki, co oznacza, że np. dla kanału drugiego stan wysoki będzie panował na wyjściu PC7 przez 350 taktów, a pozostałe 650 to stan niski, zatem współczynnik wypełnienia wyniesie w tym przypadku 35%. Po zaprogramowaniu mikrokontrolera i uruchomieniu układu, diody od LD1 do LD4 będą świecić z różną intensywnością.