Kurs STM32 LL cz. 4. Pętla PLL i taktowanie układów peryferyjnych

Dotychczas zajmowaliśmy się źródłami zegarowymi w postaci oscylatorów. Pozwalają one dostarczyć do mikrokontrolera stabilny sygnał niezbędny do pracy rdzenia i układów peryferyjnych. Jeżeli ktoś programował mniejsze mikrokontrolery (np. z serii ATmega), ta problematyka była mu “w miarę” znana. Jak jednak można zauważyć, zastosowanie kwarcu nawet o częstotliwości 48 MHz (max) nie pozwoli nam uzyskać docelowej częstotliwości pracy dla STM32G071RBT6, czyli 64 MHz. Jak zatem uzyskać takie taktowanie? Do tego służy pętla PLL.

Pętla PLL

Pętla PLL (Phase Locked Loop, czyli pętla synchronizacji fazy) to obwód działający na zasadzie sprzężenia zwrotnego, który służy do automatycznej regulacji częstotliwości, w tym jej zwielokrotniania. Na schemacie poniżej blok PLL zaznaczyłem kolorem pomarańczowym.

Może on przyjmować jako wejściowy sygnał HSI16 (wewnętrzny rezonator) lub sygnał zewnętrzny HSE.

PLL ma wbudowany bloki mnożące i dzielące sygnał wejściowy, dzięki czemu możemy uzyskać różne wartości wyjściowe z bloku PLL. Wejściowy sygnał najpierw jest dzielony przez wartość PLLM (od 1 do 8), a następnie mnożona przez wartość PLLN (od 8 do 86). W ten sposób uzyskujemy wartość fVCO.

Blok PLL ma trzy wyjścia:

  • PLLRCLK – sygnał zegarowy, który może być źródłem dla zegara systemowego SYSCLK
  • PLLQCLK – używany jako sygnał dla generatora liczny losowych
  • PLLPCLK – używany jako sygnał dla przetwornika ADC i interfejsu I2S

Każde z tych wyjść ma dodatkowy, indywidualny dzielnik oznaczany kolejno jako PLLR (od 2 do 8), PLLQ (od 2 do 8) oraz PLLP (od 2 do 32). Odpowiednio konfigurując te parametry możemy uzyskać częstotliwość PLLRCLK (ten sygnał nas teraz najbardziej interesuje) o wartości 64 MHz.

Należy pamiętać jeszcze o procedurze konfiguracji PLL. Każda zmiana parametrów bloku PLL wymaga wyłączenia pętli PLL. Po skonfigurowaniu rejestrów odpowiedzialnych za mnożniki i dzielniki należy włączyć blok PLL, a następnie włączyć używane wyjścia PLLRCLK, PLLQCLK i PLLPCLK. Wyjścia mogą być dowolnie włączane i wyłączane w trakcie działania pętli PLL.

Podstawowe rejestry konfiguracyjne – PLLRCLK jako SYSCLK

Rejestr CR – rejestr kontrolny

Bit PLLRDY – flaga włączenia bloku PLL

Bit PLLON – włączenie bloku PLL

Rejestr RCC_CFGR – rejestr konfiguracyjny RCC

Bity SWS[2:0] – status źródła sygnału dla zegara systemowego SYSCLK

Bity SW[2:0] – wybór źródła sygnału dla zegara systemowego SYSCLK

Rejestr PLLCFGR – rejestr konfiguracyjny bloku PLL

Bity PLLR[2:0] – wybór dzielnika dla wyjścia PLLRCLK

Bit PLLREN – włączenie wyjścia PLLRCLK

Bity PLLN[6:0] – mnożnik dla wejścia bloku PLL

Bity PLLM[2:0] – dzielnik dla wejścia bloku PLL

Bity PLLSRC[1:0] – wybór źródła wejścia dla bloku PLL

Opóźnienie dostępu do odczytu FLASH

Przed napisaniem programu z obsługą bloku pętli PLL, muszę wspomnieć jeszcze o jednej rzeczy. Do tej pory korzystaliśmy z małych częstotliwości zegara systemowego, dlatego ta wiedza nie była nam potrzebna. Jednak uruchomienie taktowania na poziomie 64 MHz wymaga poznania dodatkowego aspektu pracy STM32, a konkretnie chodzi o opóźnienie dostępu do odczytu pamięci FLASH (Latency).

Aby odczytać dane z pamięci Flash należy poprawnie zaprogramować ilość stanów oczekiwania (LATENCY). Wartość ta zależy od częstotliwości pracy (konkretnie HCLK, o którym jeszcze powiemy dokładnie w kolejnej części) oraz zakresu napięć pracy. Zależności te dla mikrokontrolera STM32G071RB przedstawia tabela poniżej.

Jeżeli chodzi o VCORE, w normalnym trybie mikrokontroler pracuje w zakresie Range 1 (Range 2 odnosi się do niskiego zużycia energii). Bardziej interesuje nas w tej chwili częstotliwość pracy HCLK (u nas jest będzie taka sama jak SYSCLK, czyli 64 MHz).

Dotychczas pracowaliśmy z częstotliwością 16 MHz, czyli rdzeń potrzebował opóźnienia na poziomie 1 cylku HCLK, aby prawidłowo odczytywać dane z pamięci FLASH, gdzie mamy umieszczony kod programu. Teraz chcemy korzystać z 64 MHz, co wymaga 3 cykli HCLK. Jeżeli nie uwzględnimy tego w konfiguracji, program w momencie ustawienia PLL jako SYSCLK przestanie działać, bo odczyt kolejnych instrukcji z pamięci nie będzie prawidłowa. Z tego względu musimy skonfigurować opóźnienie jako 3 cykle HCLK (2 WS). Najlepiej zrobić to na samym początku konfiguracji.

Jak zatem skonfigurować ilość cykli HCLK jako opóźnienia odczytu Flash? Wystarczy, że ustawimy prawidłową wartość w rejestrze FLASH_ACR w bitach LATENCY[2:0].

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

[PROGRAM] Konfiguracja zegara HSI i pętli PLL (PLLRCLK) jako źródła zegara SYSCLK

Mając już pełna wiedzę, możemy przystąpić do napisania programu i “rozkręcić” częstotliwość pracy mikrokontrolera do 64 MHz.

Na początku konfigurujemy opóźnienie dostępu. Dla 64 MHz ustawiamy 3 cykle HCLK (czyli 2 WS).

LL_FLASH_SetLatency(LL_FLASH_LATENCY_2);

Teraz uruchamiamy rezonator HSI i czekamy, aż się włączy.

LL_RCC_HSI_Enable();
while(LL_RCC_HSI_IsReady() != 1)
	;

Następnie konfigurujemy parametry bloku PLL pod kątem użycia go jako źródła zegara systemowego. Ponieważ chcemy uzyskać 64 MHz, a HSI ma wartość 16 MHz, musimy sygnał wejściowy pomnożyć przez 4. Niestety nie mamy jednak możliwości wyboru PLLN jako 4, bo najmniejsza dostępna wartość to 8. Mnożymy więc razy 8 i za pomocą dzielnika PLLR dzielimy przez 2. W ten sposób uzyskujemy 64 MHz.

LL_RCC_PLL_ConfigDomain_SYS(LL_RCC_PLLSOURCE_HSI, LL_RCC_PLLM_DIV_1, 8, LL_RCC_PLLR_DIV_2);

Teraz uruchamiamy blok PLL i czekamy, aż się włączy.

LL_RCC_PLL_Enable();
while(LL_RCC_PLL_IsReady() != 1)
	;

Następnie włączamy wyjście PLLR.

LL_RCC_PLL_EnableDomain_SYS();

Na koniec wybieramy źródło SYSCLK jako PLL i czekamy, aż się ustawi.

LL_RCC_SetSysClkSource(LL_RCC_SYS_CLKSOURCE_PLL);
while(LL_RCC_GetSysClkSource() != LL_RCC_SYS_CLKSOURCE_STATUS_PLL)
	;

Pozostało tylko podać wartość 64 MHz do użytku biblioteki Low Layer.

LL_SetSystemCoreClock(64000000);
LL_Init1msTick(64000000);

Teraz dodajemy analogicznie do poprzednich przykładów konfigurację GPIO, aby sprawdzić, czy dioda będzie migała z poprawną częstotliwością.

LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOA);
LL_GPIO_SetPinOutputType(LED_GREEN_GPIO_Port, LED_GREEN_Pin, LL_GPIO_OUTPUT_PUSHPULL);
LL_GPIO_SetPinPull(LED_GREEN_GPIO_Port, LED_GREEN_Pin, LL_GPIO_PULL_NO);
LL_GPIO_SetPinSpeed(LED_GREEN_GPIO_Port, LED_GREEN_Pin, LL_GPIO_SPEED_FREQ_LOW);
LL_GPIO_SetPinMode(LED_GREEN_GPIO_Port, LED_GREEN_Pin, LL_GPIO_MODE_OUTPUT);
 
while (1)
{
    LL_GPIO_TogglePin(LED_GREEN_GPIO_Port, LED_GREEN_Pin);
    LL_mDelay(1000);
}

Przypominam, że zgodnie z tym, co przedstawiłem w rozdziale o tworzeniu projektów pliku, w pliku main.h powinny znaleźć się includy oraz definicje pinu.

#define LED_GREEN_Pin LL_GPIO_PIN_5
#define LED_GREEN_GPIO_Port GPIOA

Po skompilowaniu kodu (“Project->Build Project” lub CTRL+B) i wgraniu na płytkę (“Run->Run”), dioda LD4 powinna zmieniać stan co 1 s.

Taktowanie układów peryferyjnych

Do tej pory zajmowaliśmy się głównie zegarem systemowym (SYSCLK) mikrokontrolera. Stanowi on punkt wyjściowy do większości zegarów docelowych dla poszczególnych elementów mikrokontrolera. Jednak aby dostarczyć w odpowiedni sposób sygnał zegarowy do układów peryferyjnych, musimy poznać jeszcze dwa sygnały zegarowe – HCLK i PCLK, które odpowiadają za taktowanie szyn AHB i APB.

Szyny AHB i APB to szyny, które łączą układy peryferyjne z pozostałymi elementami mikrokontrolera. AHB (Advanced High-Performance Bus) odpowiada za komunikację z kontrolerami DMA, EXTI czy RCC. Do szyny APB (Advanced Peripheral Bus) podłączone są wszystkie układy peryferyjne, w tym przetwornik ADC, TIMER-y, interfejsy UART, I2C, SPI czy I2S. Schematycznie zostało to zaznaczone na poniższej grafice.

Każda z szyn ma własny dzielnik, dzięki czemu możemy dostosować zegary do potrzeb aplikacji. Dodatkowo wiele układów peryferyjnych ma możliwość wyboru sygnału zegarowego, co daje programiście dużą elastyczność przy doborze częstotliwości ich pracy. Dostępne sygnały zegarowe dla poszczególnych interfejsów zaznaczone zostały na schemacie poniżej.

Ich dokładniejszą analizą zajmiemy się przy okazji poznawania kolejnych układów peryferyjnych. Teraz najważniejsze jest odpowiednie skonfigurowanie sygnałów HCLK i PCLK. Istotną informacją jest także to, że każdy układ mikrokontrolera ma w rejestrach RCC bity, które odpowiadają za włączenie taktowania portów mikrokontrolera oraz danego peryferium. Bez ustawienia tych bitów nie będzie możliwe ustawianie i odczytywanie rejestrów odpowiedzialnych za konfigurację GPIO, jak również UART, ADC czy TIMER-ów.

Rejestry konfiguracyjne HCLK, PCLK oraz włączenie układów peryferyjnych

Rejestr RCC_CFGR – rejestr konfiguracyjny RCC

Bity PPRE[2:0] – wybór dzielnika dla sygnału PCLK

Bity HPRE[3:0] – wybór dzielnika dla sygnału HCLK

Rejestr RCC_IOPENR – rejestr odpowiedzialny za włączanie/wyłączanie taktowania portów GPIO

RCC_AHBENR, RCC_APBENR1 oraz RCC_APBEN2 – rejestry odpowiedzialne za włączanie/wyłączanie taktowania układów peryferyjnych

[PROGRAM] Konfiguracja HCLK i PCLK

Program będzie bazował na poprzednim przykładzie, gdzie konfigurowaliśmy blok PLL jako źródło dla zegara systemowego SYSCLK. Teraz dodamy jeszcze ustawienie preskalerów dla szyn AHB i APB – obie wartości jako 1, dzięki czemu obie szyny będą korzystały z zegara 64 MHz.

Na początku konfigurujemy opóźnienie dostępu. Dla 64 MHz ustawiamy 3 cykle HCLK (czyli 2 WS).

LL_FLASH_SetLatency(LL_FLASH_LATENCY_2);

Teraz uruchamiamy rezonator HSI i czekamy, aż się włączy.

LL_RCC_HSI_Enable();
while(LL_RCC_HSI_IsReady() != 1)
	;

Następnie konfigurujemy parametry bloku PLL pod kątem użycia go jako źródła zegara systemowego. Ponieważ chcemy uzyskać 64 MHz, a HSI ma wartość 16 MHz, musimy sygnał wejściowy pomnożyć przez 4. Niestety nie mamy jednak możliwości wyboru PLLN jako 4, bo najmniejsza dostępna wartość to 8. Mnożymy więc razy 8 i za pomocą dzielnika PLLR dzielimy przez 2. W ten sposób uzyskujemy 64 MHz.

LL_RCC_PLL_ConfigDomain_SYS(LL_RCC_PLLSOURCE_HSI, LL_RCC_PLLM_DIV_1, 8, LL_RCC_PLLR_DIV_2);

Teraz uruchamiamy blok PLL i czekamy, aż się włączy.

LL_RCC_PLL_Enable();
while(LL_RCC_PLL_IsReady() != 1)
	;

Następnie włączamy wyjście PLLR.

LL_RCC_PLL_EnableDomain_SYS();

Teraz ustawiamy dzielnik dla szyny AHB i APB.

LL_RCC_SetAHBPrescaler(LL_RCC_SYSCLK_DIV_1);
LL_RCC_SetAPB1Prescaler(LL_RCC_APB1_DIV_1);

Na koniec wybieramy źródło SYSCLK jako PLL i czekamy, aż się ustawi.

LL_RCC_SetSysClkSource(LL_RCC_SYS_CLKSOURCE_PLL);
while(LL_RCC_GetSysClkSource() != LL_RCC_SYS_CLKSOURCE_STATUS_PLL)
	;

Pozostało tylko podać wartość 64 MHz do użytku biblioteki Low Layer.

LL_SetSystemCoreClock(64000000);
LL_Init1msTick(64000000);

Teraz dodajemy analogicznie do poprzednich przykładów konfigurację GPIO, aby sprawdzić, czy dioda będzie migała z poprawną częstotliwością. Wiadomo już do czego służy pierwsza linia – włączamy taktowanie dla portu GPIO.

LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOA);
LL_GPIO_SetPinOutputType(LED_GREEN_GPIO_Port, LED_GREEN_Pin, LL_GPIO_OUTPUT_PUSHPULL);
LL_GPIO_SetPinPull(LED_GREEN_GPIO_Port, LED_GREEN_Pin, LL_GPIO_PULL_NO);
LL_GPIO_SetPinSpeed(LED_GREEN_GPIO_Port, LED_GREEN_Pin, LL_GPIO_SPEED_FREQ_LOW);
LL_GPIO_SetPinMode(LED_GREEN_GPIO_Port, LED_GREEN_Pin, LL_GPIO_MODE_OUTPUT);
 
while (1)
{
    LL_GPIO_TogglePin(LED_GREEN_GPIO_Port, LED_GREEN_Pin);
    LL_mDelay(1000);
}

Przypominam, że zgodnie z tym, co przedstawiłem w rozdziale o tworzeniu projektów pliku, w pliku main.h powinny znaleźć się includy oraz definicje pinu.

#define LED_GREEN_Pin LL_GPIO_PIN_5
#define LED_GREEN_GPIO_Port GPIOA

Po skompilowaniu kodu (“Project->Build Project” lub CTRL+B) i wgraniu na płytkę (“Run->Run”), dioda LD4 powinna zmieniać stan co 1 s. Teraz mając już pełną wiedzę na temat RCC schemat z przepływem sygnałów zegarowych powinien być bardziej przejrzysty.

Pozostaje jeszcze jedna kwestia do wyjaśnienia – dlaczego dotychczas program działał, skoro nie skonfigurowaliśmy dzielników szyn AHB i APB? Są one domyślnie ustawione na wartość 1, dlatego program nam się uruchomił. Wiedzą odnośnie zegarów chciałem wprowadzać stopniowo, aby od razu cała procedura nie wydawała się zbyt skomplikowana. Mam nadzieję, że po przestudiowaniu całego rozdziału o RCC zegary w STM32 nie będą Ci już takie obce. Powyższą konfigurację warto zapamiętać, będziemy z niej korzystali w większości przykładów w kursie.

Repozytorium GitHub

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!

  1. Wtam. Warto tutaj dopowiedzieć, że GPIO jest kontrolowane przez szynę AHB i tylko jej prescaler będzie miał wpływ na zachowanie diody. Funkcja LL_RCC_SetAPB1Prescaler(); jest tuaj opcjonalna.
    Pozdrawiam

    1. Zgadza się, w tym konkretnym przykładzie sterowania diodą taktowanie szyny APB nie ma znaczenia. Jednak przykład był przygotowywany po to, aby przedstawić konfigurację zegara ogólnie, a nie pod diodę LED, dlatego omawiam oba preskalery. Ta konfiguracja przyda się w kolejnych lekcjach, gdzie już szyna APB będzie miała znaczenie.

      1. Ok. Mam jeszcze takie pytanie. Gdy zmienie preskaler tak, że szyna AHB ma 4 razy mniejsze taktowanie to dioda zmienia stan co 4 sekundy przy delay’u jednosekundowym. Niby zrozumiałe – taktowanie sysclk to X , szyny AHB X/4 . Nie mogę jednak odnieść tego do rzeczywistości. Mógłbyś wyjaśnić dlaczego zmiane obserwuje po 4 sekundach? Co tam sie takiego dzieje z tym sygnałem?

        1. Opóźnienie w funkcji LL_mDelay() jest realizowane za pomocą SysTicka, którego działanie jest zależne od szyny AHB. Jeżeli zmniejszasz częstotliwość na szynie, to SysTick też pracuje cztery razy wolniej. Żeby funkcja LL_mDelay() nadal odliczała dobry czas pomimo zmiany taktowania AHB, musimy ponownie skonfigurować SysTick timer podając nasz główny zegar dla rdzenia (HCLK) i konfigurując sam SysTick funkcją LL_Init1msTick(HCLK_CoreFreq). Funkcja ta konfiguruje SysTick tak, aby odliczał czas w ms. Jeśli zajrzysz głębiej do tej funkcji to operuje ona na rejestrach rdzenia odpowiedzialnych za SysTick m.in. SysTick->LOAD, SysTick->VAL, SysTick->CTRL.

Dodaj komentarz

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