08 lipca 2018

Pętla PLL

Dotychczas wszystkie programy działały na domyślnym źródle sygnału zegarowego, czyli wbudowanym generatorze RC o częstotliwości 16 MHz (w dokumentacji nazywanym HSI).
Niby nie było w tym nic złego, ale nasz mikrokontroler może pracować z maksymalną częstotliwością 180 MHz, więc używaliśmy mniej niż 10% jego możliwości. Druga sprawa to niska stabilność generatora RC - faktyczna częstotwliość pewnie znacznie odbiegała od 16 MHz, co więcej mogła się zmieniać pod wpływem temperatury, zmian napięcia itd.
Na szczęście na płytce stm32f429-disc znajdziemy rezonator kwarcowy 8 MHz, który pozwoli na wygenerowanie sygnału zegarowego o znacznie większej precyzji.
Na początek jak zwykle odniesienie do dokumentacji:


Na pierwszy rzut oka ten diagram może nie wydawać się zbyt łatwy, więc najlepiej podejść do niego małymi krokami - wtedy zobaczymy że to nic trudnego.
Aby ułatwić opis pozwoliłem sobie bardzo amatorsko pokolorować powyższy diagram. Piękne to nie jest, ale może będzie łatwiej opisać o co chodzi.


Na początek fragment zaznaczony na zielono, to tutaj widzimy podłączenie kwarcu 8MHz oraz generator sygnału zegarowego (HSE).
Generator uruchamiamy zapalając flagę HSEON w rejestrze RCC->CR. Następnie musimy poczekać na włączenie generatora:

        RCC->CR |= RCC_CR_HSEON;
        while ((RCC->CR & RCC_CR_HSERDY) == 0) ;


Gdy generator już działa, czas skonfigurować pętlę PLL, którą zaznaczyłem na żółto.
Na pierwszy rzut oka konfiguracja pętli PLL może wydawać się nieco skomplikowana, ale nie ma co się poddawać. Na początek wystarczy że wybierzemy trzy wartości:
  • M - czyli dzielnik częstotliwości sygnału wejściowego
  • N - mnożnik częstotliwości
  • P - dzielnik częstotliwości dla sygnału wyjściowego
M określa dzielnik sygnału wejściowego, czyli 8MHz. Typowe rozwiązanie to podzielenie przez 8, dzięki temu dostaniemy wygodną wartość 1 MHz.
P dla odmiany to dzielnik wartości wyjściowej PLL. Dostępne są wartości 2, 4, 6 oraz 8. Wybieramy 2 jako najniższą dostępną.
Teraz wystaczy zająć się wartością N, czyli mnożnikiem częstotliwości. Na wejściu mamy sygnał o częstotliwości 1 MHz (bo podzieliliśmy przez 8). Wartość N musi się mieścić w przedziale od 2 do 432, ale częstotliwości prawidłowe dla PLL to 100 do 432, więc lepiej unikać niższych wartości.
Zacznijmy więc niejako od końca - interesuje nas częstotliwość taktowania procesora, czyli sygnal HCLK. Maksymalna częstotliwość to 180 MHz, jednak wygodniej jest użyć wartości 168 MHz - daje ona możliwość łatwego wygenerowania zegara dla USB.
Przyjmijmy więc, że chcemy uzyskać częstotliwość wyjściową HCLK o wartości 168MHz. Między PLL a sygnałem HCLK jest jeszcze dzielnik P, który ustawimy na wartość 2, czyli nasza pętla PLL powinna dawać częstotliwość wyjściową 336MHz.
Mamy więc nasz współczynnik N, jest to po prostu 336.
Ponieważ współczynniki możemy kiedyś zmienić, warto je zdefiniować jako stałe:

#define PLL_M           8uL         // 8 MHz / 8 = 1 MHz
#define PLL_N           336uL       // 336 MHz => hclk = 168 MHz
#define PLL_Q           7           // USB PLL => 336/7 = 48 MHz


Pojawił się tutaj jeszcze współczynnik Q - na razie nie używamy USB, ale od razu ustawimy odpowiedni zegar. Powinien mieć częstotliwość 48 MHz, co jak widać idealnie wpasowuje się w dzielnik Q = 7.

Mając ustalone odpowiednie współczynniki dla pętli PLL wystarczy wpisać je do rejestru RCC->PLLCFGR:

RCC->PLLCFGR = PLL_M | (PLL_N << 6) | RCC_PLLCFGR_PLLSRC_HSE | (PLL_Q << 24);

Teraz dwa słowa o częstotliwości taktowania - sam mikrokontroler, a właściwie jego rdzeń może być taktowany częstotliwością 180MHz. Wybraliśmy 168MHz, aby łatwo uzyskać zegar dla USB. Okazuje się jednak, że nie wszystkie moduły peryferyjne naszego mikrokontrolera mogą pracować z pełną prędkością. Ma to pewne uzasadnienie - po co przykładowo moduł UART o prędkości transmisji 115kb/s taktować z 168MHz. Można znacznie zmiejszyć zużycie prądu (oraz bramek w układzie) wydzielając dla wolniejszych peryferiów oddzielne domeny zegara.
Tak właśnie zbudowany jest mikrokontroler stm32f429 - posiada wolniejsze magistrale  peryferyjne: APB1 oraz APB2, które nie pracują z pełną prędkością. APB1 może maksymalnie być taktowana z 45 MHz, natomiast APB2 z 90 MHz. Z pomocą przyjdą nam kolejne dzielniki częstotliwości. Sygnał HCLK podzilimy przez 2 dla APB2 oraz przez 4 dla APB1:

RCC->CFGR |= RCC_CFGR_HPRE_DIV1 | RCC_CFGR_PPRE2_DIV2 | RCC_CFGR_PPRE1_DIV4;

W programie wartości zegarów odpowiednich domen mogą się nam jeszcze przydać, więc warto dodać odpowiednie definicje:

#define HCLK_FREQ       168000000uL
#define APB1_FREQ       (HCLK_FREQ/4)
#define APB2_FREQ       (HCLK_FREQ/2)


Teraz jesteśmy gotowi na uruchomienie pętli PLL:

RCC->CR |= RCC_CR_PLLON;
while ((RCC->CR & RCC_CR_PLLRDY) == 0) ;

Zanim przejdziemy dalej jeszcze jedna drobnostka. Okazuje się, że aby stabilnie pracować z pełną prędkością mikrokontroler wymaga podwyższenia napięcia zasilania rdzenia. Wykonujemy to następującym kodem:

RCC->APB1ENR |= RCC_APB1ENR_PWREN;
 

PWR->CR |= PWR_CR_ODEN;
while ((PWR->CSR & PWR_CSR_ODRDY) == 0) ;
 

PWR->CR |= PWR_CR_ODSWEN;
while ((PWR->CSR & PWR_CSR_ODSWRDY) == 0) ;

Teraz kolejny problem - pamięć Flash w której znajduje się nasz program może pracować maksymalnie z częstotliwością ~30MHz, czyli 168MHz to znacznie za dużo. Dodając 5 cykli oczekiwania, będzimy pewni że pamięć flash nadąży za naszym programem:

 FLASH->ACR = FLASH_ACR_PRFTEN | FLASH_ACR_ICEN | FLASH_ACR_DCEN | FLASH_ACR_LATENCY_5WS;

Teraz jesteśmy już gotowi - pętla PLL działa, pamięć flash jest przygotowana na szybsze taktowanie procesora, zmieniamy więc źródło sygnału na PLL:

        uint32_t cfgr = RCC->CFGR;
        cfgr &= ~RCC_CFGR_SW;
        cfgr |= RCC_CFGR_SW_PLL;
        RCC->CFGR = cfgr;
        while ((RCC->CFGR & RCC_CFGR_SWS ) != RCC_CFGR_SWS_PLL) ;


Od tej chwili nasz mikrokontroler działa na częstotliwości 168 MHz. Jak to sprawdzić? Mamy co najmniej dwa sposoby. Po pierwsze timer SysTick - skonfigurujemy go z nową wartością dzielnika i zobaczymy czy diody migają z ta samą częstotliwością.
Drugi moduł to UART - jeśli wszystko działa jak chcieliśmy uzyskamy ponownie poprawną komunikację przez port szeregowy.
Cały program wygląda następująco:

#include <stdint.h>
#include <stdbool.h>
#include "stm32f429xx.h"

#define PLL_M           8uL             // 8 MHz / 8 = 1 MHz
#define PLL_N           336uL           // 336 MHz => hclk = 168 MHz
#define PLL_Q           7               // USB PLL => 336/7 = 48 MHz

#define HCLK_FREQ       168000000uL
#define APB1_FREQ       (HCLK_FREQ/4)
#define APB2_FREQ       (HCLK_FREQ/2)

volatile uint32_t ms_counter = 0;

void SysTick_Handler(void)
{
        if (ms_counter)
                ms_counter--;
}

void delay_ms(uint32_t ms)
{
        ms_counter = ms;
        while (ms_counter) ;
}

void usart_send(const char *s)
{
        while (*s) {
                while ((USART1->SR & USART_SR_TXE) == 0);
                USART1->DR = *s++;
        }
}

int main(int argc, char *argv[])
{
        RCC->CR |= RCC_CR_HSEON;
        while ((RCC->CR & RCC_CR_HSERDY) == 0) ;

        RCC->PLLCFGR = PLL_M | (PLL_N << 6) | RCC_PLLCFGR_PLLSRC_HSE | (PLL_Q << 24);
        RCC->CFGR |= RCC_CFGR_HPRE_DIV1 | RCC_CFGR_PPRE2_DIV2 | RCC_CFGR_PPRE1_DIV4;

        RCC->CR |= RCC_CR_PLLON;
        while ((RCC->CR & RCC_CR_PLLRDY) == 0) ;

        RCC->APB1ENR |= RCC_APB1ENR_PWREN;

        PWR->CR |= PWR_CR_ODEN;
        while ((PWR->CSR & PWR_CSR_ODRDY) == 0) ;

        PWR->CR |= PWR_CR_ODSWEN;
        while ((PWR->CSR & PWR_CSR_ODSWRDY) == 0) ;

        FLASH->ACR = FLASH_ACR_PRFTEN | FLASH_ACR_ICEN | FLASH_ACR_DCEN |
                FLASH_ACR_LATENCY_5WS;

        uint32_t cfgr = RCC->CFGR;
        cfgr &= ~RCC_CFGR_SW;
        cfgr |= RCC_CFGR_SW_PLL;
        RCC->CFGR = cfgr;
        while ((RCC->CFGR & RCC_CFGR_SWS ) != RCC_CFGR_SWS_PLL) ;

        RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_GPIOGEN;
        RCC->APB2ENR |= RCC_APB2ENR_USART1EN;

        GPIOG->MODER |= GPIO_MODER_MODE13_0|GPIO_MODER_MODE14_0;

        GPIOA->MODER |= GPIO_MODER_MODE9_1|GPIO_MODER_MODE10_1;
        GPIOA->AFR[1]  |= GPIO_AFRH_AFSEL9_0 | GPIO_AFRH_AFSEL9_1 | GPIO_AFRH_AFSEL9_2;
        GPIOA->AFR[1]  |= GPIO_AFRH_AFSEL10_0 | GPIO_AFRH_AFSEL10_1 | GPIO_AFRH_AFSEL10_2;

        SysTick->LOAD = HCLK_FREQ / 1000 - 1;
        SysTick->VAL = 0;
        SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
                        SysTick_CTRL_TICKINT_Msk |
                        SysTick_CTRL_ENABLE_Msk;

        USART1->BRR = APB2_FREQ / 115200;
        USART1->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;

        usart_send("Hello world!\r\n");

        while (1) {
                GPIOG->BSRR = GPIO_BSRR_BS13;
                GPIOG->BSRR = GPIO_BSRR_BR14;
                delay_ms(500);

                GPIOG->BSRR = GPIO_BSRR_BS14;
                GPIOG->BSRR = GPIO_BSRR_BR13;
                delay_ms(500);

                usart_send("stm32f429\r\n");
        }

        return 0;
}

Brak komentarzy:

Prześlij komentarz