Měl jsem chvíli čas a vzpomněl jsem si že jsem kdysi měl problém rozběhnout na STM32F0 hodinové oscilátory a tak mě napadlo, že bych to mohl zkusit na řadě G0. Prostě jen ze zvědavosti, jestli to bude také tak problematické. No a zdá se že ne.
V první ukázce bude minimalistická aplikace, která rozběhne interní WakeUp Timer (WUT) z hodinového krystalu (32.768kHz) a bude se periodicky probouzet z hlubokého spánku. Tuto "šablonu" je možné snadno rozšířit o kompletní RTC s časem i kalendářem (WUT je součástí RTC), případně měnit periodu probouzení. Ve druhé ukázce pak demonstruji nejhlubší režim spánku, který lze na MCU STM32G0x0 použít - Standby s uchováním vybraných dat. Opět to má spíš sloužit jako jakási šablona pro budoucí projekty. Obě ukázky jsou napsané převážně přímým přístupem do registrů, protože v této podobě to lépe koresponduje s datasheetem. LL knihovny jsou použity pro konfiguraci GPIO.
V této ukázce připojíme mezi piny PC14 a PC15 hodinový (32.768kHz) krystal (konkrétně LFXTAL002996BULK stojící cca pětikorunu) se dvěma 18pF kondenzátory proti GND. Spustíme LSE oscilátor a jeho clock použijeme pro RTC (obvod reálného času) a pro WUT (Wake Up Timer), který je součástí RTC. Ten nakonfigurujeme tak aby periodicky, přibližně 10x za sekundu, probouzel MCU ze spánku. Navíc si na pin PA8 vypustíme LSE clock (pomocí MCO) a po každém probuzení budeme na pinu PA4 "togglovat" výstup abychom měli důkaz, že se čip probouzí. Čas a datum v RTC nastavovat nebudu, ale kdo by to potřeboval najde v kódu komentář kde se to má provést. MCO výstup můžeme použít mimo jiné k měření frekvence LSE. Jen pro zajímavost, tu lze v RTC korigovat s krokem cca 1ppm a k ověření korekcí lze využít výstup RTC_OUT, kam si lze vyvést korigovaný clock RTC.
Samotná konfigurace je vcelku přímočará. Nejprve pustíme clock PWR a RTC abychom měli přístup do jejich registrů (RTC má dva clocky). Odemkneme si bitem DBP přístup do backup domény. Spustíme LSE a počkáme než nám vlajka LSERDY oznámí že se rozběhl. Pak vybereme zdroj clocku pro RTC (LSE) a pustíme ho do RTC (teď už má clock i jádro RTC). Poté si odemkneme přístup do sekce chráněné proti přepisu (Write protected) a zapneme v RTC režim init, v němž jej lze konfigurovat. Nastavíme si děličky na 128 a 256, tedy celkem 32768 a kdo bude potřebovat tak si v tomto okamžiku bude moct nastavit čas a datum v registrech RTC->TR a RTC->DR. Následně se pustíme do konfigurace WUT. Začneme vynulováním bitu WUTE abychom měli jistotu že je WUT vypnutý. Pomocí bitů WUCKSEL si zvolíme děličku clocku pro WUT. Já zvolil hodnotu 0b000 což odpovídá 16, takže do WUT jde clock 32768/16=2048Hz. Poté zvolím "strop" WUT na hodnotu 204, takže frekvence probouzení bude cca 2048/204=9.99Hz. Pak si po sobě ještě pro jistotu smažu vlajku WUTF a samotné WUT spustím (bitem WUTE) a povolím přerušení (WUTIE). Tím je celá konfigurace hotová a můžu vypnout init mód RTC a pokud chci tak i reaktivovat "write protection". Signál přerušení z WUT putuje do EXTI na kanál 19, takže ten musím povolit a nakonec povolím přerušní i v NVIC. Tím je vše připraveno. Teoreticky by mělo být možné v NVIC přerušení nepovolovat, díky čemuž by program po probuzení nemusel vstupovat do rutiny přerušení, ale to je detail, kterým e teď nebudu zabývat a který jsem popsal v tutoriálu o STM32F0.
Samotné uspání pak probíhá docela běžně. Bitem DEEPSLEEP říkáme jádru že chceme přecházet do hlubokého spánku, pomocí bitů LPMS vybíráme jeden z režimů (Stop 0/1, Standby...) a instrukcí WFI dáváme pokyn k uspání. Hned po probuzení ze slušnosti vynuluji bit DEEPSLEEP, ale nutné to není. Pokud aplikace nevyužívá jiný režim spánku, asi to není potřeba. Hned po probuzení skočí program do rutiny přerušení, kde si jen pro úplnost ověřím že zdrojem přerušení je opravdu WUT a smažu vlajku. Vzhledem k tomu že jiný zdroj přerušení vedoucí to této rutiny nemáme, tak by to ověření nebylo nutné, ale v ukázce jsem ho nechal aby byla obecnější.
void RTC_Init(void); void RTC_WUT_100ms_Init(void); void EnterStop1(void); void GPIO_init(void); volatile uint32_t wakeups; // Připojen krystal LFXTAL002996 (IQD Frequency product, 32.768kHz) + 2x18pF, zvolen (nejnižší) "low drive" mód oscilátoru // Na PA4 indikační výstup, který togglujeme ve while smyčce kdykoli se MCU probudí // Na PA8 je RTC_OUT (32.768kHz) int main(void){ GPIO_init(); RTC_Init(); while (1){ // Usnout EnterStop1(); // probudili jsme se - inkrementujeme si počítadlo (užitečná činnost) wakeups++; // togglujeme pin aby bylo vidět že aplikace běží LL_GPIO_TogglePin(GPIOA, LL_GPIO_PIN_4); } } void GPIO_init(void){ LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOA); // clock pro GPIOA LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_4, LL_GPIO_MODE_OUTPUT); // PA4 jako výstup LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_8, LL_GPIO_MODE_ALTERNATE); // PA8 jako alternate function LL_GPIO_SetAFPin_8_15(GPIOA, LL_GPIO_PIN_8, LL_GPIO_AF_0); // PA8 přidělíme MCO LL_RCC_ConfigMCO(LL_RCC_MCO1SOURCE_LSE,LL_RCC_MCO1_DIV_1); // MCO clock z LSE/1 } void EnterStop1(void){ // volím hlubší režim spánku (Stop 0/1 nebo Standby) SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk; // volím Stop 1 režim PWR->CR1 &= ~PWR_CR1_LPMS_Msk; // nuluji LPMS bity... PWR->CR1 |= (0x01 << PWR_CR1_LPMS_Pos); // ... abych tam mohl zapsat 0b001 - stop1 // úklid před spaním __DSB(); __ISB(); __WFI(); // po probuzení po sobě uklidíme SLEEPDEEP bit (pokud nepoužíváme jiný režim spánku tak to asi není potřeba) SCB->SCR &= ~SCB_SCR_SLEEPDEEP_Msk; } void RTC_TAMP_IRQHandler(void){ // ověříme zdroj přerušení (není nutné když máme jen jeden) if(RTC->MISR & RTC_MISR_WUTMF){ // vyčistíme vlajku RTC->SCR = RTC_SCR_CWUTF; // už není co dělat - bylo by čistší řešit to bez vstupu do IRQ rutiny } } void RTC_Init(void){ // spustit PWR a RTC APB rozhraní (přístup k RTC registrům) - RM nezmiňuje !!! RCC->APBENR1 |= RCC_APBENR1_PWREN | RCC_APBENR1_RTCAPBEN; // Povolit přístup do backup domény PWR->CR1 |= PWR_CR1_DBP; // počkat na přístup do backup domény while(!(PWR->CR1 & PWR_CR1_DBP)); // spustit LSE RCC->BDCR |= RCC_BDCR_LSEON; // počkat na rozběhnutí LSE while(!(RCC->BDCR & RCC_BDCR_LSERDY)); // zvolit LSE jako zdroj clocku pro RTC RCC->BDCR &= ~RCC_BDCR_RTCSEL_Msk; // vynulovat RTCSEL bity... RCC->BDCR |= (1 << RCC_BDCR_RTCSEL_Pos); // ... a zapsat 0b01 (LSE) // povolit clock pro RTC (už je zvolený a běží) RCC->BDCR |= RCC_BDCR_RTCEN; // odemknout "Write protection" RTC->WPR = 0xCA; RTC->WPR = 0x53; // Přepnout RTC do režimu inicializace RTC->ICSR |= RTC_ICSR_INIT; // počkat na to až přejde do režimu inicializace while(!(RTC->ICSR & RTC_ICSR_INITF)); // nastavit děličky v RTC (asynchronní 127+1 , synchronní 255+1) 32768/128/256 = 1Hz RTC->PRER = (127 << RTC_PRER_PREDIV_A_Pos) | (255 << RTC_PRER_PREDIV_S_Pos); // tady lze nastavit čas a datum v RTC->TR a RTC->DR // natavit Wake Up Timer (WUT) // vypnout WUT (jinak nelze konfigurovat) RTC->CR &= ~RTC_CR_WUTE; // pokud bychom nebyli v RTC Init režimu, museli bychom počkat na while(!(RTC->ICSR & RTC_ICSR_WUTWF)); // Zvolit předděličku pro WUT na /16, 32768/16=2048Hz. RTC->CR &= ~RTC_CR_WUCKSEL_Msk; // hodnoty 0b000 v bitech WUCKSEL volí děličku /16 // nastavit strop WkaUp timeru 32768/16=2048, 2048/204= ~10.04Hz RTC->WUTR = 204; // uklidit po sobě WUTF vlajku RTC->SCR = RTC_SCR_CWUTF; // povolit přerušení od WUT (WUTIE) a spustit WUT (WUTE) RTC->CR |= RTC_CR_WUTIE | RTC_CR_WUTE; // v případě potřeby lze provádět kalibraci // RTC->CR |= RTC_CR_COE; // kalibrační výstup na PA4/PC13 // RTC->CALR = RTC_CALR_CALP | 476; // korekce frekvence // ukončit inicializační režim RTC->ICSR &= ~RTC_ICSR_INIT; // reaktivovat Write protection RTC->WPR = 0xFF; // povolit přerušení EXTI19 - tam je zavedeno přerušení od RTC EXTI->IMR1 |= EXTI_IMR1_IM19; // povolit přerušení v NVIC (asi je to možné řešit i jinak - jen probouzet, neskákat do ISR) NVIC_EnableIRQ(RTC_TAMP_IRQn); }
Ze zvědavosti jsem změřil odběr v různých konfiguracích (zapnuté a vypnuté MCO atp.) a výsledky jsem shrnul do následující tabulky. Při zběžném pohledu odpovídají datasheetovým.
| Napětí | RTC_OUT | PA4 toggle | Iavg | poznámka |
|---|---|---|---|---|
| 3.3V | ON | ON | 7.5uA | |
| 3.3V | OFF | OFF | 4.8uA | typ. hodnota z datasheetu |
| 3.3V | OFF | ON | 4.8uA | |
| 3.0V | OFF | ON | 4.6uA |
Tab.1 - průměrný odběr v různých konfiguracích, LSE drive nastaven na minimum
V téhle ukázce má naše aplikace jednoduchý úkol. Stiskem tlačítka se probouzet, inkrementovat počítadlo a signalizovat jeho hodnotu jako sérii impulzů (jen jako důkaz že pracuje správně) a pak zase usnout. Je to tedy šablona, kterou můžu v budoucích projektech převzít a vyplnit ji jinou užitečnou činností. Hodí se na situace kdy má MCU jednorázově reagovat na stisk jednoho nebo více tlačítek (respektive na příchod nějakého signálu) a poté se "vypnout". Díky tomu, že má režim Standby velmi nízký odběr, může v různých aplikacích nahrazovat "on/off" vypínač. Jen pro představu běžné AAA články ("mikrotužková baterie") mají kapacitu okolo 1Ah. Bude-li MCU schopné snížit svou spotřebu ve spánku na dejme tomu 2uA, bude teoretická výdrž okolo 57 let - tedy rozhodně delší než "shelf life" samotných baterií (pro alkalické baterie 5-10let).
Pro MCU řady STM32G0x0 (např. STM32G030) o kterých je tento text platí totéž co jsem o Standby režimu psal už v tutoriálu o STM32F0, tedy to že Standby je spíš klinická smrt než spánek. Během něj se totiž neuchovává obsah RAM a po probuzení prochází MCU resetem. Tohle ale neplatí obecně pro všechny STM32G0. Například řada G0x1 (např. STM32G031) má schopnost si ve standby režimu obsah RAM uchovat a kromě toho má k dispozici širší paletu ještě hlubších režimů spánku a navíc má i obecně nižší spotřebu.
Protože budeme přece jen chtít nějakou informaci zachovat, využijeme takzvanou "backup doménu", tedy periferii, jejíž obsah se i po průchodu Standby režimem a resetem zachovává (tedy oblast, které se ve standby nevypíná napájení). V té je kromě samotného RTC i blok označený TAMPER a v něm jsou 32bitové registry BKP0R až BKP4R do nichž si můžeme zapsat ty nejdůležitější informace, které budeme při příštím probuzení potřebovat. Přístup k backup doméně je chráněný proti náhodnému přepisu, takže je potřeba si ji odemknout bitem DBP (Disable Backup Protection).
Před samotným uspáním si musíme aktivovat zdroj(e) probuzení. Těch je omezené množství a já se teď nebudu zabývat všemi. Pro svou demonstraci jsem zvolil signál z pinu PA2 - takzvaný WKUP4. Rozhodl jsem se že probouzení bude probíhat sestupnou hranou, nebo nízkou úrovní na pinu. Tlačítko mám připojené proti GND, takže na pinu potřebuji pullup rezistor. Ten bych si mohl klasicky zapnout v GPIO registrech, ale to by nefungovalo, neboť při přechodu do standby režimu se veškerá konfigurace GPIO ztrácí a všechny piny přecházejí do stavu vysoké impedance. Takže interní pullup nebo pulldown rezistor musíme nakonfigurovat jinde. A to v registrech PUCRA, PUCRB ... a PDCRA, PDCRB až PDCRF. Navíc tuto konfiguraci musíme potvrdit globálně bitem APC. Nakonec ještě musíme signálu WKUP4 povolit probouzení bitem EWUP4. Pak už můžeme bezpečně usnout s jistotou, že stisk tlačítka (přivedení log.0 na PA2) MCU probudí.
Jak jsem už zmínil, po probuzení ze standby režimu prochází čip resetem, takže náš program začíná úplně od začátku. Je tedy rozumné ověřit hned po startu zda jde opravdu o probuzení a ne třeba o běžný restart (ať už tlačítkem nebo prostě odpojením od napájení). To zjistíme pomocí bitu SBF (StandyBy Flag). Pokud je nastaven, víme že došlo k probuzení ze Standby režimu. Kdyby místo toho došlo k resetu, SBF by byl nulový a my bychom se mohli podívat do RCC->CSR a pátrat po příčinách resetu a podle toho reagovat. Krome vlajky SBF můžeme v registru PWR->SR1 najít i další vlajky, které detailně specifikují důvod probuzení (probudit nás může více signálů). Všechny tyto vlajky je nutné před usnutím smazat (zápisem do PWR->SCR).
void do_something(void); void EnterStandby(void); volatile uint32_t wakeups = 0; // počítá kolikrát byl program probuzen // 3.3V, tlačítko na PA2 proti GND, spotřeba ve standby 1.3uA (odpovídá datasheetu) // poznámka mezi G0x0 a G0x1 je velký rozdíl (spotřeba G0x1 je mnohem menší a má více různých featur - jako třeba zálohování celé RAM či ještě hlubší režimy spánku) int main(void){ // ať máme přístup k vlajkám (PWR) i Tamper sekci (RTC) RCC->APBENR1 |= RCC_APBENR1_PWREN | RCC_APBENR1_RTCAPBEN; // VOLITELNÉ - jde o probuzení nebo o jiný zdroj resetu ? if(PWR->SR1 & PWR_SR1_SBF){ // probuzení ze spánku, zjistím který ze zdrojů to byl if(PWR->SR1 & PWR_SR1_WUF4){ // probudil mě WakeUp pin 4 } }else{ // jiný důvod - nejspíš reset (lze ověřit v RCC->CSR ) } // uvolní přístup do backup domény (tam máme uložená data co mají přežít Standby režim) PWR->CR1 |= PWR_CR1_DBP; while(!(PWR->CR1 & PWR_CR1_DBP)){} // počkat až je backup doména odemčená // přečtu hodnoty uložené v backup doméně/registrech wakeups = TAMP->BKP0R; // proveď užitečnou akci do_something(); // inkrementuj počítadlo startů wakeups++; // ulož hodnoty do backup domény/registrů TAMP->BKP0R = wakeups; // jdi spát EnterStandby(); while (1){} // sem se program stejně nikdy nedostane } // indikuje obsah hodnoty proměnné wakeups "blikáním" na PA4 void do_something(void){ uint32_t i; // inicializuj pin PA4 // zablikej s s ním tolikrát kolik jsi už absolvoval startů LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOA); LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_4, LL_GPIO_MODE_OUTPUT); for(i=0;i<wakeups;i++){ LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_4); asm("nop"); asm("nop");asm("nop");asm("nop"); LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_4); } } // přechod do režimu spánku void EnterStandby(void){ // RCC->APBENR1 |= RCC_APBENR1_PWREN; // není nutné, protože to už máme povolené z dřívějška // Vymazat všechny vlajky - jinak neusnem PWR->SCR = PWR_SCR_CWUF; // Zvolit pro WKUP4 probouzení nízkou úrovní PWR->CR4 |= PWR_CR4_WP4; // Set polarity to low level for wake // Nastavit pullup na PA2 (WKUP4) PWR->PUCRA |= PWR_PUCRA_PU2; // Set pull-up resistor on PA2 // Povolit pullupy globálně (APC), povolit Wake Up z WKUP4 pinu PWR->CR3 |= PWR_CR3_APC | PWR_CR3_EWUP4; // Zvolit Standby režim PWR->CR1 &= ~PWR_CR1_LPMS; // vynulovat LPMS bity abychom do nich mohli... PWR->CR1 |= PWR_CR1_LPMS_0 | PWR_CR1_LPMS_1; // ...apsat hodnotu 0b11 - standby // Zajistit že zápis proběhl (void)PWR->CR1; // Říct jádru že uspáváme do hlubokého spánku SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk; // dokončit přístupy do paměti ? + uspat MCU (pomocí WFI) __DSB(); __ISB(); __WFI(); while(1){} // sem už by se program neměl dostat }
Odběr ve standby při 3.3V napájení jsem změřil na přibližně 1.3uA, což přesně odpovídá typické hodnotě z datasheetu.
Home
| V1.00 24.6.2026 /
| By Michal Dudka (m.dudka@seznam.cz) /