Clock na STM32F0

Mikrokontroléry STM32 jsou velice složité. Berte proto prosím na vědomí, že to co máte před sebou není vyčerpávající návod, ale jen takové povídání o některých možnostech těchto čipů. Píšu to proto aby jste si uvědomili, že když zde nějakou funkci nezmíním, neznamená to že neexistuje. Jinak řečeno než začnete programovat, vyplatí se vám udělat si rychlý průzkum mezi funkcemi čipu a jeho periferií, protože je docela možné že tam najdete něco co vám výrazně ulehčí práci. Nejprve se podíváme na to jaké zdroje clocku má STM32F0 k dispozici.

Zdroje clocku u STM32F051

Schema distribuce clocku

STMka mají celkem komplexní správu clocku. O tom vás jistě přesvědčí schema na předcházejícího obrázku. Tak vypadá systém clocku na STM32F051, který rozhodně nepatří k těm nejsložitějším. A to tam ještě není všechno. Pořádně si ho prohlédněte. Na levé straně vidíte zdroje clocku a na pravé straně jsou pak "spotřebiče" clocku, tedy jádro a různé periferie. Celý systém můžete řídit jako řečiště. Jádro a valná většina periferií odebírá clock z linky nazvané SYSCLK skrze prescalery. Prescaler AHB dělí SYSCLK a vzniká tak HCLK (High speed Clock). Ten slouží jádru a rychlým periferiím (Paměť, GMA, GPIO atd.). HCLK pak vede do dalšího presaleru (APB), kterým ho lze dělit a vzniká PCLK (Peripheral clock). Ten pak vede k periferiím. Každé periferii je možné clock povolit a nebo zakázat a tím ji vypnout. Celý složitý systém dělení a vypínání slouží k optimalizaci spotřeby, protože čím nižší clock používáte a čím méně periferií běží, tím nižší je spotřeba. Proto jsou také po startu všechny periferie krom jádra a paměti vypnuté. Každou periferii kterou chcete použít si musíte zapnout. To se provádí skrze funkce ze souboru stm32f0xx_ll_bus.h. A protože se to často zapomíná, napíšu to ještě jednou - Každé periferii je před použitím nutné zapnout clock !. Dělící poměry prescalerů lze měnit za chodu a lze tak škálovat výkon jádra nebo periferií dle aktuální potřeby. Prohlédněte si i blok MCO v dolní části obrázku o němž bude řeč v první ukázce. Doufám že se mnou budete souhlasit když řeknu, od SYSCLK vpravo to není moc zajímavé. To co stojí za řeč je otázka jak SYSCLK vzniká. A na toto téma vám teď předvedu několik ukázek.

Protože Discovery board nemá od výroby osazen žádný externí krystal, budeme si muset ze začátku vystačit bez něj. Později zkusíme využít 8MHz signálu z STLINKu a když zbyde čas, ukážeme si jak na Discovery board osadit vlastní krystal. Hned po startu čipu by se mělo jádro rozběhnout přímo z HSI, tedy na 8MHz a oba prescalery AHB i APB by měly být nastaveny na /1, tedy 8MHz by mělo putovat nejen k jádru ale i k periferiím. V souboru system_stm32f0xx.c ve funkci SystemInit(), která by měla proběhnout ještě před voláním main(), je inicializace clocku, která všechny ostatní zdroje povypíná. V tomto souboru si také všimněte definic HSE_VALUE a HSI_VALUE. Slouží některým knihovním funkcím k výpočtům například aktuální předpokládané frekvence jádra, nebo přenosových rychlostí některých periferií. Pokud tedy budete používat externí krystal nebo zdroj hodin, nezapomeňte jeho frekvenci do makra HSE_VALUE zapsat. Když totiž potom zavoláte funkci SystemCoreClockUpdate(), pokusí se z vámi definovaných hodnot a nastavení mikrokontroléru spočíst frekvenci jádra a uložit ji do globální proměnné SystemCoreClock. Na ni se pak odkazují další knihovní funkce. Je tedy obecně dobrým pravidlem udržovat její hodnotu aktuální a po každé změně clocku zavolat fci SystemCoreClockUpdate()


Konfigurace clocku po restartu STM32

Možná vás napadlo, proč bychom měli měnit hodnotu konstanty HSI_VALUE, když je frekvence interního RC oscilátoru přece pevná ? No ona pevná není :) oscilátor jde přeladit a to v rozsahu přibližně od 7.5MHz do 8.5MHz. Slouží k tomu fce LL_RCC_HSI_SetCalibTrimming(). Krom toho také není přesně 8MHz, ale leží v tolerančním pásmu +-1% při pokojové teplotě. Nikdy jsem však neměl potřebu HSI jakkoli ladit. Tak konec hraní a jdeme na něco vážného.

Seznam příkladů

Clock-out

Clock-out je schopnost vyvést na některý pin mikrokontroléru jeho clock (nebo některý z jeho clocků). Je to natolik užitečná funkce, že se s ní setkáte snad u všech typů mikrokontrolérů. Využití najde hlavně při synchronizaci s vnějšími obvody. Ale stejně tak je užitečná i při ladění aplikace jako informativní výstup. Na STM32 se tato funkce nazývá MCO (Microcontroller Clock Output). U většiny čipů ji můžete využít na vývodu PA8. U některých čipů je možné clock před vyvedením z čipu ještě dělit (u F051 to možné není). Než tedy budu vyprávět dál, mrkněte se na jednoduchý zdrojový kód, kde MCO konfiguruji.

// 3A - MCO (Microcontroler Clock Output) - PA8
#include "stm32f0xx.h"
#include "stm32f0xx_ll_bus.h"
#include "stm32f0xx_ll_gpio.h"
#include "stm32f0xx_ll_rcc.h"

int main(void){
// spustit clock pro GPIO
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOA);
// PA8 bude alternativní funkce (MCO)
LL_GPIO_SetPinMode(GPIOA,LL_GPIO_PIN_8,LL_GPIO_MODE_ALTERNATE);
// vybrat alternativní fci pro PA8 - MCO je AF0
LL_GPIO_SetAFPin_8_15(GPIOA,LL_GPIO_PIN_8,LL_GPIO_AF_0);
// jako zdroj pro MCO vyber SYSCLK, dělící poměr 1 (jiný u STM32F051 ani nejde)
LL_RCC_ConfigMCO(LL_RCC_MCO1SOURCE_SYSCLK,LL_RCC_MCO1_DIV_1);

 while (1){
  // STMko nemá co dělat...
 }
}

PLL z HSI

Dejme tomu, že chcete rozběhnout jádro na plných 48MHz. Než ho na tuto frekvenci rozběhnete měli by jste vědět že flash paměť z níž jádro čte instrukce není tak rychlá. Jinak řečeno nedokáže předhodit jádru každých 20ns čerstvou instrukci. Aby čtení probíhalo korektně je potřeba pro frekvence vyšší jak 24MHz zvolit flash paměti latenci 1 waitstate. Čtení z ní pak trvá dva takty. Tím by ale mohl utrpět výpočetní výkon jádra. Aby netrpěl je k dispozici takzvaný "prefetch" buffer. Ten čte z paměti více instrukcí zároveň a pak je po jedné předhazuje jádru. Pokud tedy chceme běžet na frekvenci 48MHz, určitě musíme paměti 1 waitstate zapnout. Použít Prefetch buffer není nutné, ale zbytečně bychom se okrádali o výkon kdybychom ho nepoužili (potřebné funkce jsou v stm32f0xx_ll_system.h). Druhá otázka je jak tuto frekvenci vůbec vygenerovat. To jde v našem případě pouze s pomocí PLL. V tomto příkladě nakonfigurujeme clock podle následujícího obrázku. Signál z HSI vede přes děličku dvěma do multiplexeru který vybírá zdroj signálu pro PLL. Z něj pak do PLL, kde se frekvence násobí. Z PLL pak vede signál do dalšího multiplexeru který vybírá zdroj pro SYSCLK. Nejprve si spočítejme parametr PLL. Jestliže má HSI frekvenci 8MHz tak po podělení 2 vstupuje do PLL frekvence 4MHz. Abychom ve výsledku dostali 48MHz musíme násobit 48/4 = 12x. PLL je nutné nejprve nakonfigurovat, teprve poté spustit. Pak počkat až se stabilizuje a až potom můžeme zvolit PLL jako zdroj pro SYSCLK. Obecně platí že když chcete použít jakýkoli signál musít být stabilní. To by platilo i o HSE, kde je vždy potřeba počkat až se krystalový oscilátor plně rozběhne (ale o tom více v jiných příkladech). Níže můžete vidět zdrojový kód ukázky, jež je natolik přímočarý že si nezaslouží žádný další komentář. Výstup na MCO vypouštím jen proto abych si zkontroloval že vše proběhlo správně.

// 3B - PLL 48MHz z HSI
#include "stm32f0xx.h"
#include "stm32f0xx_ll_bus.h"
#include "stm32f0xx_ll_gpio.h"
#include "stm32f0xx_ll_rcc.h"
#include "stm32f0xx_ll_system.h"

void init_MCO(void);

int main(void){
init_MCO(); // vypustíme SYSCLK na PA8 (MCO)
// zdroj pro PLL je HSI/2 a násobíme 12x
LL_RCC_PLL_ConfigDomain_SYS(LL_RCC_PLLSOURCE_HSI_DIV_2,LL_RCC_PLL_MUL_12);
// spouštíme PLL
LL_RCC_PLL_Enable();
// čekej než se PLL stabilizuje
while(!LL_RCC_PLL_IsReady()){}
// nastavit počet wait state pro Flash paměť (48MHz -> 1 WS)
LL_FLASH_SetLatency(LL_FLASH_LATENCY_1);
// povolit prefetch buffer
LL_FLASH_EnablePrefetch();
// vyber PLL jako zdroj pro SYSCLK
LL_RCC_SetSysClkSource(LL_RCC_SYS_CLKSOURCE_PLL);
// aktualizuj proměnnou SystemCoreClock
SystemCoreClockUpdate();

while (1){
  // STMko nemá co dělat...
 }
}

void init_MCO(void){
// spustit clock pro GPIO
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOA);
// PA8 bude alternativní funkce (MCO)
LL_GPIO_SetPinMode(GPIOA,LL_GPIO_PIN_8,LL_GPIO_MODE_ALTERNATE);
// vybrat alternativní fci pro PA8 - MCO je AF0
LL_GPIO_SetAFPin_8_15(GPIOA,LL_GPIO_PIN_8,LL_GPIO_AF_0);
// jako zdroj pro MCO vyber SYSCLK, dělící poměr 1 (jiný u STM32F051 ani nejde)
LL_RCC_ConfigMCO(LL_RCC_MCO1SOURCE_SYSCLK,LL_RCC_MCO1_DIV_1);
}

PLL z externího clocku

Občas budete chtít využít jako zdroj clocku externí signál. Typicky buď kvůli synchronizaci s jiným obvodem a nebo v situacích kdy vám svou přesností nestačí krystal a vybavíte obvod přesným oscilátorem. Důvodem ale může být také prostý fakt, že je vaše vývojová deska zrovna takto upravena. Což Discovery board STM32F0 je. ST-LINK na desce generuje 8MHz clock a přivádí jej na PF0(OSC_IN) našeho STM32F051. Zda to tak je si můžete ověřit v této sekci. V následující ukázce si předvedeme jak rozběhnout čip na 30MHz z 8MHz externího clocku. Pro přehlednost si konfiguraci můžete opět prohlédnout na obrázku. Postup bude podobný jako v předchozím příkladě s tím rozdílem že budeme nejprve muset rozběhnout HSE v režimu bypass. To je režim kdy nepoužíváme krystalový oscilátor a máme k dispozici externí signál. I tak je ale nutné počkat na jeho stabilizaci. Před PLL je ještě dělička s dělicími poměry /1,/2,/3 až /16. Díky ní je možné vytvářet relativně širokou paletu výstupních frekvencí podle vztahu fout=fin*M/N. Kde N je celé číslo v rozsahu 1 až 16 a M celé číslo v rozsahu 2 až 16. Musíme ale dodržet dvě podmínky. Vstupní frekvence pro PLL (tedy za děličkou) musí být v rozsahu 1-24MHz a výstupní frekvence musí ležet v rozsahu 16-48MHz. Jestliže tedy chceme připravit například 30MHz navrhuji nastavit děličku na /4 a násobit 15x. 8MHz/4*15 = 30MHz. Po změně clocku nezapomeneme zavolat funkci SystemCoreClockUpdate(), která provede aktualizaci globální proměnné SystemCoreClock. Pro zajímavost pak zavoláme ještě funkci LL_RCC_GetSystemClocksFreq(), která se z definice frekvencí HSE, HSI a nastavení clocku pokusí spočítat předpokládanou frekvenci SYSCLK a sběrnic AHB a APB. Výsledek ukládá do struktury clocks, kde si je můžete při debugování prohlédnout.

// 3C - PLL 30MHz z HSE bypass (přiveden Externí clock 8MHz)
#include "stm32f0xx.h"
#include "stm32f0xx_ll_bus.h"
#include "stm32f0xx_ll_gpio.h"
#include "stm32f0xx_ll_rcc.h"
#include "stm32f0xx_ll_system.h"

LL_RCC_ClocksTypeDef clocks;
void init_MCO(void);

int main(void){
init_MCO(); // vypustíme SYSCLK na PA8 (MCO)
// zvolit režim HSE bypass
LL_RCC_HSE_EnableBypass();
// nastartovat HSE
LL_RCC_HSE_Enable();
// počkat na stabilizaci HSE
while(!LL_RCC_HSE_IsReady()){}
// zdroj pro PLL je HSE/4 a násobíme 15x
LL_RCC_PLL_ConfigDomain_SYS(LL_RCC_PLLSOURCE_HSE_DIV_4,LL_RCC_PLL_MUL_15);
// spouštíme PLL
LL_RCC_PLL_Enable();
// čekej než se PLL stabilizuje
while(!LL_RCC_PLL_IsReady()){}
// nastavit počet wait state pro Flash paměť (30MHz -> 1 WS)
LL_FLASH_SetLatency(LL_FLASH_LATENCY_1);
// povolit prefetch buffer
LL_FLASH_EnablePrefetch();
// vyber PLL jako zdroj pro SYSCLK
LL_RCC_SetSysClkSource(LL_RCC_SYS_CLKSOURCE_PLL);
// aktualizuj proměnnou SystemCoreClock
SystemCoreClockUpdate();
// spočítej a ulož frekvence SYSCLK,AHB a APB do struktury "clocks"
LL_RCC_GetSystemClocksFreq(&clocks);

while (1){
  // STMko nemá co dělat...
 }
}

void init_MCO(void){
// spustit clock pro GPIO
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOA);
// PA8 bude alternativní funkce (MCO)
LL_GPIO_SetPinMode(GPIOA,LL_GPIO_PIN_8,LL_GPIO_MODE_ALTERNATE);
// vybrat alternativní fci pro PA8 - MCO je AF0
LL_GPIO_SetAFPin_8_15(GPIOA,LL_GPIO_PIN_8,LL_GPIO_AF_0);
// jako zdroj pro MCO vyber SYSCLK, dělící poměr 1 (jiný u STM32F051 ani nejde)
LL_RCC_ConfigMCO(LL_RCC_MCO1SOURCE_SYSCLK,LL_RCC_MCO1_DIV_1);
}

Zkusme to jednoduše

Z předchozích dvou příkladů nemusela situace s nastavováním clocku vypadat úplně nejrůžověji. Zářivou barvu jí dodávají funkce schované v souboru stm32f0xx_ll_utils.h. Najdete zde mimo jiné dvě funkce vykonávající totéž co jsme dělali v předchozích dvou příkladech. Funkce LL_PLL_ConfigSystemClock_HSE() nakonfiguruje PLL podle vstupních parametrů. Prvním z nich je frekvence externího clocku nebo externího krystalu. Druhý parametr vybírá zda používáte externí krystal nebo externí clock (LL_UTILS_HSEBYPASS_ON), třetí argument je struktura nesoucí informaci o PLL (dělící poměr a koeficient násobení) a posledním argumentem je struktura s konfigurací prescalerů pro AHB a APB sběrnice. Zdrojový kód vykonává stejnou činnost jako předchozí příklad (30MHz z externího 8MHz clocku). Zakomentovaná část zdrojového kódu slouží k nastavení PLL z HSI. Funkce LL_PLL_ConfigSystemClock_HSI() má jako vstupní argumenty pouze struktury s konfigurací PLL a prescalerů AHB a APB. Obě funkce vrací SUCCESS pokud proběhnou úspěšně. Není to nutností, ale můžete toho využít aby jste měli jistotu že vaše aplikace běží na zvolené frekvenci.

// 3D PLL z HSI i z HSE (bypass) jednoduše !
#include "stm32f0xx.h"
#include "stm32f0xx_ll_bus.h"
#include "stm32f0xx_ll_gpio.h"
#include "stm32f0xx_ll_rcc.h"
#include "stm32f0xx_ll_utils.h" // zde se skrývá jednoduchost :D

void init_MCO(void);

LL_UTILS_PLLInitTypeDef pll;
LL_UTILS_ClkInitTypeDef clk;
uint8_t error;

int main(void){
init_MCO(); // vypustíme SYSCLK na PA8 (MCO)

/* konfigurace 48MHz z HSI
pll.PLLMul = LL_RCC_PLL_MUL_12; // násobíme 12x (8/2*12 = 48MHz)
clk.AHBCLKDivider = LL_RCC_SYSCLK_DIV_1;
clk.APB1CLKDivider = LL_RCC_APB1_DIV_1;
error=LL_PLL_ConfigSystemClock_HSI(&pll,&clk); // kompletní inicializace clocku
*/

// konfigurace 30MHz z externího clocku 8MHz
pll.Prediv = LL_RCC_PREDIV_DIV_4; // 8MHz / 4 = 2MHz
pll.PLLMul = LL_RCC_PLL_MUL_15;  // 2MHz * 15 = 30MHz
clk.AHBCLKDivider = LL_RCC_SYSCLK_DIV_1; //APB i AHB nedělit
clk.APB1CLKDivider = LL_RCC_APB1_DIV_1;
// kompletní inicializace systémového clocku
error=LL_PLL_ConfigSystemClock_HSE(8000000,LL_UTILS_HSEBYPASS_ON,&pll,&clk);

if(error != SUCCESS){
// konfigurace neprošla
}
SystemCoreClockUpdate(); // udržuj informaci o taktu jádra aktuální

while (1){
  // STMko nemá co dělat...
 }
}


void init_MCO(void){
// spustit clock pro GPIO
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOA);
// PA8 bude alternativní funkce (MCO)
LL_GPIO_SetPinMode(GPIOA,LL_GPIO_PIN_8,LL_GPIO_MODE_ALTERNATE);
// vybrat alternativní fci pro PA8 - MCO je AF0
LL_GPIO_SetAFPin_8_15(GPIOA,LL_GPIO_PIN_8,LL_GPIO_AF_0);
// jako zdroj pro MCO vyber SYSCLK, dělící poměr 1 (jiný u STM32F051 ani nejde)
LL_RCC_ConfigMCO(LL_RCC_MCO1SOURCE_SYSCLK,LL_RCC_MCO1_DIV_1);
}

Home
V0.3 25.7.2017
By Michal Dudka (m.dudka@seznam.cz)