Precyzyjne opóźnienia w połączeniu z trybami oszczędzania energii w STM32, część 1
Implementacja powyższych funkcji została pokazana na listingu 2. Najpierw definiujemy, że licznik TIM2 jest używany do opóźnień mikrosekundowych, a licznik TIM3 – do opóźnień milisekundowych. Dzięki temu, gdyby któryś z tych liczników był zajęty przez inną funkcję, łatwo można go zamienić na licznik TIM4 lub TIM5. TIM2 jest zwiększany z częstotliwością 1 MHz, aby łatwo było odliczać okresy 1 µs. TIM3 jest zwiększany z częstotliwością 2 kHz. Taki wybór spowodowany jest tym, że przyjęliśmy założenie o taktowaniu liczników tym samym zegarem co rdzeń, a przy maksymalnej częstotliwości taktowania rdzenia 72 MHz i największej możliwej wartości dzielnika wstępnego 65536 nie uda się uzyskać sygnału 1 kHz.
Funkcja USleep korzysta z trybu uśpienia, a jak zostało napisane wyżej, wybudzanie z tego trybu może trwać ok. 2 µs. Ten narzut jest niezależny od częstotliwości taktowania rdzenia. Dodatkowy narzut wprowadza konieczność konfigurowania licznika. Możemy starać się go zminimalizować, pomijając wywołania funkcji bibliotecznych i odwołując się bezpośrednio do rejestrów konfiguracyjnych licznika. Ten narzut można oszacować w taktach zegara i zależy on od częstotliwości taktowania rdzenia. Jeśli chcemy odmierzać opóźnienia bardzo precyzyjnie, możemy spróbować eksperymentalnie dobrać wartość zmiennej usCalibration i ustawić ją na początku funkcji SleepConfigure w zależności od częstotliwości taktowania rdzenia. Wymienione narzuty powodują, że w praktyce za pomocą funkcji USleep nie uda się osiągnąć opóźnienia krótszego niż ok. 3 µs.
Funkcja USleep kolejno:
- kalibruje wartość opóźnienia;
- zeruje licznik – rejestr CNT (counter);
- konfiguruje wartość count, po której osiągnięciu przez licznik ma nastąpić ustawienie rejestru zdarzenia – rejestr CCR1 (capture-compare register 1);
- uaktywnia licznik – ustawia bit TIM_CR1_CEN (counter enable) w rejestrze CR1 (control register 1);
- usypia mikrokontroler za pomocą instrukcji WFE;
- po obudzeniu sprawdza, czy licznik zatrzymał się, tzn. czy wyzerowany jest bit TIM_CR1_CEN – jeśli nie jest, ponownie usypia.
Funkcja USleep współpracuje z procedurą US_TIM_IRQHandler obsługującą przerwanie licznika mikrosekundowego. Zgodnie z opisem dla instrukcji WFE nie ma potrzeby implementowania obsługi przerwania. Wystarczyłoby, gdyby rejestr zdarzenia (event register) został ustawiony po zajściu zdarzenia zgodności wartości licznika z wartością count. Niestety, jeśli zdarzenie zgodności zaszłoby między sprawdzeniem warunku zakończenia pętli do while a wywołaniem instrukcji WFE, rejestr zdarzenia nie zostałby ustawiony – mielibyśmy dokładnie taki sam problem, jak omówiony wyżej dla instrukcji WFI. Problem ten jest znany i opisany jako błąd „563915: Event Register is not set by interrupts and debug”. Dlatego w procedurze obsługi przerwania wołamy makro __SEV, aby mieć pewność, że rejestr zdarzenia, na który czeka instrukcja WFE, zostanie ustawiony. Wcześniej zatrzymujemy licznik przez wyzerowanie bitu TIM_CR1_CEN.
Funkcja MSleep jest bardzo podobna do funkcji USleep. Różnice są następujące. Nie kalibrujemy czasu uśpienia, gdyż przy opóźnieniach milisekundowych narzut związany z usypianiem i budzeniem jest pomijalny. Sprawdzamy jedynie, czy funkcja nie została wywołana z zerową wartością czasu opóźnienia. Ponadto licznik milisekundowy tyka z częstotliwością 2 kHz, więc trzeba wartość count pomnożyć przez dwa. Implementujemy też odpowiednią procedurę obsługi przerwania MS_TIM_IRQHandler dla tego licznika.
Funkcja Sleep jest koncepcyjnie podobna do dwóch wyżej omówionych funkcji opóźniających.
Różnice wynikają głównie z innych interfejsów programistycznych dla liczników TIMx i RTC. Zapis do rejestrów konfiguracyjnych RTC trwa co najmniej trzy takty zegara LSE (32768 Hz) i jest wykonywany niejako w tle w stosunku do programu. Zatem przed każdym zapisem do rejestru konfiguracyjnego RTC musimy upewnić się, czy zakończył się poprzedni zapis. Służy do tego funkcja RTC_WaitForLastTask. Do odmierzania czasu wykorzystujemy funkcjonalność alarmu RTC.
Funkcja Sleep kolejno:
- odczytuje bieżącą wartość licznika RTC (wywołanie RTC_GetCounter) i ustawia wartość, przy której ma nastąpić obudzenie (wywołanie RTC_SetAlarm);
- zasypia w pętli, wywołując funkcję PWR_EnterSTOPMode, która z kolei wywołuje instrukcję WFE, stała PWR_REGULATOR_MODE definiuje sposób pracy regulatora 1,8 V;
- opuszcza pętlę, jeśli obudzenie jest wynikiem zgłoszenia alarmu RTC, czyli gdy ustawiony jest znacznik zdarzenia alarmu RTC_IT_ALR (wywołanie RTC_GetFlagStatus);
- po opuszczeniu pętli kasuje znacznik zdarzenia alarmu (wywołanie RTC_ClearITPendingBit).
Po obudzeniu z trybu zatrzymania mikrokontroler jest taktowany wewnętrznym oscylatorem RC (HSI, 8 MHz). Podczas wykonywania funkcji Sleep mikrokontroler obsługuje przerwania, będąc taktowany zegarem HSI. Przed zakończeniem tej funkcji trzeba przywrócić pracę oscylatora kwarcowego HSE oraz pętli PLL za pomocą funkcji ClockConfigure, która będzie omówiona w drugiej części artykułu. W przypadku funkcji Sleep nie musimy implementować procedury obługi przerwania, gdyż alarm RTC poprawnie ustawia rejestr zdarzenia.
Funkcja Standby jest bardzo prosta. Odczytuje bieżącą wartość licznika RTC za pomocą funkcji RTC_GetCounter. Ustawia wartość licznika RTC, przy której nastąpi wybudzenie – funkcja RTC_SetAlarm. Mikrokontroler zostaje przełączony w tryb czuwania – funkcja PWR_EnterSTANDBYMode. Mikrokontroler może zostać wybudzony z trybu czuwania, przed upływem zadanego czasu opóźnienia, przez narastające zbocze na wyprowadzeniu WKUP-PA0. Funkcja PWR_WakeUpPinCmd uaktywnia tę opcję.
List. 3. Pomocnicze makro umieszczone w pliku check.h
#ifndef _CHECK_H #define _CHECK_H 1 #define active_check(cond, limit) { \ int i; \ for (i = (limit); !(cond); --i) \ if (i <= 0) \ return -1; \ } #endif
Ponieważ RTC nie jest resetowany w trybach głębokiego uśpienia (zatrzymania i czuwania), to sposób jego konfiguracji jest uzależniony od ustawienia znacznika PWR_FLAG_SB testowanego za pomocą funkcji PWR_GetFlagStatus. Jeśli znacznik PWR_FLAG_SB jest ustawiony, to nastąpiło wybudzenie, RTC pracuje i nie trzeba go rekonfigurować. Jeśli znacznik ten jest wyzerowany, to mikrokontroler został zresetowany lub zostało włączone zasilanie i trzeba skonfigurować RTC. Aby funkcja SleepConfigure nie zawiesiła się w przypadku uszkodzenia oscylatora zegarkowego, korzystamy z pomocniczego makra, które jest przedstawione na listingu 3. Makro to ogranicza czas oczekiwania na start oscylatora. Jeśli oscylator nie zadziała, funkcja SleepConfigure zwróci wartość ujemną, sygnalizującą błąd. Przy poprawnym zakończeniu funkcja zwraca zero.
Marcin Peczarski