22 października 2018

Taktowanie mikrokontrolera

W kolejnych wpisach planuję wrócić do GPIO, ale najpierw kilka słów o taktowaniu układu. To bardzo ważny temat, więc warto od niego zacząć.
Jak zwykle wszystko opisane jest w dokumentacji układu, niestety zamieszczony diagram może w pierwszej chwili wyglądać nieco odstraszająco:
Okazuje się, że większość tego schematu można (na razie) pominąć. W dolnej części widać MCO, czyli wyjście generatora - można je wykorzystać do taktowania innego układu podłączonego z naszym mikrokontrolerem. MCO przyda się również do przetestowania konfiguracji.
Na górze oraz po prawej stronie widzimy taktowanie peryferiów - w przypadku STM32F030 zarówno rdzeń, jak i peryferia mogą być taktowane z tą samą częstotliwością, więc również ten fragment można pominąć.
Nieco powyżej MCO widzimy również taktowanie dla RTC, czyli zegara czasu rzeczywistego - ten moduł też na razie pominiemy.
Można więc nieco ograniczyć wspomniany diagram:
Pominięte moduły są teraz zaznaczone odpowiednio kolorami:
  • fioletowy - MCO
  • czerwony - RTC
  • zielony - peryferia
To co zostaje jest znacznie mniejsze, chociaż nadal ciekawe:


Mamy do dyspozycji dwa generatory sygnału zegarowego. Zaznaczony zieloną elipsą wbudowany generator RC o częstotliwości 8MHz. Na czerwono zaznaczyłem moduł zewnętrznego generatora - może on pracować z rezonatorem kwarcowym, którego niestety nie ma na płytce Nucleo. Możliwa jest również konfiguracja z zewnętrznym źródłem sygnału taktującego - ta wersja jest dostępna przy wykorzystaniu sygnału 8 MHz z programatora na płytce Nucleo.

Na żółto zaznaczona jest pętla PLL, która pozwala na podniesienie częstotliwości jednego z sygnałów opisanych wcześniej.
Na niebiesko wreszcie zaznaczyłem multiplekser, który pozawala na wybór źródła taktowania samego układu.
Po resecie źródłem taktowania jest wewnętrzny generator RC o częstotliwości 8 MHz. Można się o tym bardzo łatwo przekonać - wystarczy ustawić pin PA8 tryb funkcji alternatywnej, którą domyślnie jest właśnie MCO. Więc po dodaniu kodu:

    GPIOA->MODER |= GPIO_MODER_MODER8_1;
    RCC->CFGR |= RCC_CFGR_MCO_SYSCLK;


Na wspomnianym pinie pojawi się taktowanie mikrokontrolera (SYSCLK - zegar systemowy).
Aby się upewnić że obserwujemy sygnał z generatora RC, czyli HSI najlepiej nieco zmodyfikować częstotliwość. W rejestrze RCC_CR znajdziemy bity HSITRIM, które pozwalają na korektę częstotliwości tego generatora. Domyślna wartość jak widzimy była bardzo bliska 8MHz, zmienimy jej wartość zerujący wszystkie bity HSITRIM:

    RCC->CR = (RCC->CR & ~RCC_CR_HSITRIM_Msk);

Jak widać, częstotliwość jest teraz trochę niższa:
Skoro generator RC, czyli HSI już znamy można przetestować generator zewnętrzny, czyli HSE. Brak rezonatora kwarcowego niestety nie jest typową konfiguracją, pozostaje więc tylko użycie sygnału z programatora.
Ponieważ korzystamy z zewnętrznego generatora, konieczne będzie ustawienie bitu HSEBYP w rejestrze CR. Następnie wystarczy uruchomić generator ustawiając bit HSEON oraz poczekać na jego zadziałanie, co będzie sygnalizowane zapaleniem bitu HSERDY. Kod wygląda więc następująco:

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


Po jego wykonaniu sygnał HSE jest dostępny, ale mikrokontroler nadal używa HSI. Trzeba więc przełączyć multiplekser - wykonamy to zmieniając odpowiednie bity w rejestrze RCC_CFGR:

    RCC->CFGR |= RCC_CFGR_SW_HSE;

W tym momencie mikrokontroler pracuje w oparciu o sygnał z programatora. Jako sprawdzenie mierzymy jeszcze raz częstotliwość na wyjściu MCO:
Jak widać mamy znowu częstotliwość 8MHz, a jak pamiętamy HSI generuje teraz nieco mniej - więc możemy być pewni, że układ działa jak planowaliśmy.
Teraz czas przejść do pętli fazowej PLL - która jest chyba najczęściej używanym źródłem taktowania.
Sama pętla musi mieć na wejściu sygnał z generatora - możemy wybrać HSI podzielony przez 2 (czyli o częstotliwości 4 MHz), albo HSE który przed chwilą przetestowaliśmy.
Użyję HSI/2, bo ten sygnał jest dostępny nawet przy braku programatora w Nucleo.
Do konfiguracji PLL wykorzystamy rejestr RCC_CFGR. Najpierw ustawimy źródło sygnału na HSI - jest to domyślna wartość, ale dla lepszej czytelności użyjemy stałej RCC_CFGR_PLLSRC_HSI_DIV. Musimy również ustawić mnożnik częstotliwości. Przykładowo niech będzie 4 - powinniśmy uzyskać wówczas częstotliwość 4MHz * 4, czyli 16 MHz.

    RCC->CFGR = RCC_CFGR_PLLSRC_HSI_DIV2 | RCC_CFGR_PLLMUL4;

W kolejnym kroku uruchomimy PLL ustawiając bit PLLON w rejestrze RCC_CR oraz czekamy na zadziałanie pętli - zostanie wówczas ustawiony bit PLLRDY.

    RCC->CFGR = RCC_CFGR_PLLSRC_HSI_DIV2 | RCC_CFGR_PLLMUL4;
    RCC->CR |= RCC_CR_PLLON;

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

Na koniec wystarczy zmienić źródło taktowania mikrokontrolera na PLL:

    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while ((RCC->CFGR & RCC_CFGR_SWS_PLL) != RCC_CFGR_SWS_PLL) ;


Od tej chwili system powinien pracować z częstotliwością 16 MHz co możemy łatwo sprawdzić:

Pętla PLL daje nam ogromne możliwości dopasowania częstotliwości taktowania do oczekiwań projektu. Jednak pewnie najpopularniejsze będzie użycie częstotliwości maksymalnej, czyli 48 MHz. Jak łatwo obliczyć, będziemy do tego potrzebowali mnożnika 12.
Jednak zanim zmienimy program jeszcze jedna ważna informacja. Pamięć Flash może działać z częstotliwością do 24 MHz. Więc jeśli będziemy chcieli zwiększyć taktowanie powyżej tej wartości, musimy dodać opóźnienia dostępu do pamięci (latency).
Uzyskamy to pisząc:

    FLASH->ACR |= FLASH_ACR_LATENCY;

Teraz gdy wszystko gotowe, czas przetestować cały program:

#include "stm32f0xx.h"

int main(void)
{
    RCC->CFGR = RCC_CFGR_PLLSRC_HSI_DIV2 | RCC_CFGR_PLLMUL12;
 RCC->CR |= RCC_CR_PLLON;
    while ((RCC->CR & RCC_CR_PLLRDY) == 0) ;

 FLASH->ACR |= FLASH_ACR_LATENCY;

    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while ((RCC->CFGR & RCC_CFGR_SWS_PLL) != RCC_CFGR_SWS_PLL) ;

 RCC->AHBENR |= RCC_AHBENR_GPIOAEN | RCC_AHBENR_GPIOCEN;

 GPIOC->MODER |= GPIO_MODER_MODER5_0;
 GPIOC->MODER |= GPIO_MODER_MODER6_0;
 GPIOC->MODER |= GPIO_MODER_MODER8_0;

 // clock test
 GPIOA->MODER |= GPIO_MODER_MODER8_1;
 GPIOA->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR8_0 | GPIO_OSPEEDER_OSPEEDR8_1;
 RCC->CFGR |= RCC_CFGR_MCO_SYSCLK;

 while (1)
 {
  GPIOC->BSRR = GPIO_BSRR_BS_5;
  for (volatile uint32_t dly = 0; dly < 600000; dly++)
   ;
  GPIOC->BSRR = GPIO_BSRR_BR_5;

  GPIOC->BSRR = GPIO_BSRR_BS_6;
  for (volatile uint32_t dly = 0; dly < 600000; dly++)
   ;
  GPIOC->BSRR = GPIO_BSRR_BR_6;

  GPIOC->BSRR = GPIO_BSRR_BS_8;
  for (volatile uint32_t dly = 0; dly < 600000; dly++)
   ;
  GPIOC->BSRR = GPIO_BSRR_BR_8;
 }
}

Wynik spełnia oczekiwania (z dokładnością to możliwości analizatora oczywiście):

Jeden szczegół w kodzie został pominięty - mianowicie odwołanie do rejestru GPIOA_OSPEEDR. Przy częstotliwości 48MHz konieczne jest ustawienie maksymalnej prędkości pracy pinu. Wrócę jeszcze do tego tematu.


Brak komentarzy:

Prześlij komentarz