Snad se na mě nebudete zlobit když si dovolím vypustit motivaci. Předpokládám totiž, že důvody proč snižovat spotřebu mikrokontrolérům nebo elektronice obecně jsou vám známé. Místo motivace vás tedy raději seznámím o čem tento tutoriál bude a o čem ne. Nebude o hardwaru. To je kapitola sama pro sebe. Bude o různých možnostech jak pomocí SW snižovat spotřebu STMka. Stručně se mrkneme jak lze manipulovat taktovací frekvencí a jaký to má vliv na spotřebu a poté se vrhneme na režimy spánku. Vyzkoušíme si SLEEP, STOP i STANDBY režim. Vyzkoušíme si různé formy uspání a probouzení. U toho všeho budeme sledovat spotřebu abychom získali hrubou představu. K pokusům nám poslouží jeden z nejmenších a nejlevnějších čipů - STM32F030F4 v TSSOP24 pouzdře osazený na bastldesku a napájený 3.3V zdrojem. Předpokládám, ale že většina technik bude přenositelná i na jiné čipy než jen řadu F0. Protože kódy budou někdy rozsáhlé a budou se často opakovat, najdete je celé ke stažení na konci článku.
Než se pustíme do práce, stálo by za to se ve stručnosti seznámit se základními technikami které lze na STMkách používat ke kontrole spotřeby.
Velkou částí tutoriálu nás bude provázet jeden modelový příklad. Necháme STMko reagovat na stisk tlačítka půlsekundovým bliknutím LEDkou na PA5. Tlačítko budeme mít připojené na PA0 netradičně proti VCC. V čipu zapneme vnitřní pull-down rezistor a stisk rozpoznáme jako vzestupnou hranu. K detekci využijeme externí přerušení. To není běžná a ani vhodná metoda jak hlídat stisk tlačítka, ale nám teď nejde o tlačítko. To je zde jen v roli "generátoru" signálu. První příklady v nichž budeme zkoušet redukci clocku, ale můžete postavit i jinak. Klidně můžete blikat LEDkou pomocí "delay" nebo timeru, případně stisk hlídat libovolnou metodou co se vám líbí. Já se ale pro přehlednost budu držet zmíněného postupu s využitím externího přerušení. Zkusme tedy jaký bude odběr naší aplikace s různým clockem. Pro testy jsem připravil následující funkce:
// A) vliv frekvence na odběr #include "stm32f0xx.h" // výstupy pro LEDku #define TEST_H GPIOA->BSRR = GPIO_Pin_5 #define TEST_L GPIOA->BRR = GPIO_Pin_5 void _delay_ms(uint32_t Delay); void init_test_output(void); void pull_unused_gpio(void); void init_exti(void); void clock_48(void); void clock_8(void); void clock_1(void); void clock_31k(void); volatile uint8_t irq_flag=0; // vlajka idnikující potřebu bliknout LEDkou int main(void){ //clock_48(); //clock_8(); clock_1(); // clock například 1MHz //clock_31k(); pull_unused_gpio(); // ošetřit nepoužité GPIO init_test_output(); // výstup na LEDku init_exti(); // přerušení od PA0 while (1){ if(irq_flag){ // pokud došlo ke stisku, blikni LEDkou TEST_H; _delay_ms(500); TEST_L; irq_flag=0; } } } // EXTI z PA0 bude sloužit k buzení z režimů spánku // Na PA0 připojeno tlačítko proti VCC (interní pull down) void init_exti(void){ EXTI_InitTypeDef exti; NVIC_InitTypeDef nvic; GPIO_InitTypeDef gp; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE); // kvůli PA0 RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE); // kvůli EXTI // PA0 jako vstup s pull-down gp.GPIO_Pin = GPIO_Pin_0; gp.GPIO_Mode = GPIO_Mode_IN; gp.GPIO_OType = GPIO_OType_PP; gp.GPIO_PuPd = GPIO_PuPd_DOWN; gp.GPIO_Speed = GPIO_Speed_Level_1; GPIO_Init(GPIOA, &gp); // Přiřadíme Lince 0 port GPIOA (tedy mapujeme pin PA0) SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA,EXTI_PinSource0); // povolíme externí přerušení z Linky 0 na vzestupnou hranu exti.EXTI_Line = EXTI_Line0; exti.EXTI_Mode=EXTI_Mode_Interrupt; exti.EXTI_Trigger=EXTI_Trigger_Rising; exti.EXTI_LineCmd=ENABLE; EXTI_Init(&exti); // povolíme externí přerušení (Linky 0) v NVIC nvic.NVIC_IRQChannel=EXTI0_1_IRQn; nvic.NVIC_IRQChannelPriority=3; nvic.NVIC_IRQChannelCmd=ENABLE; NVIC_Init(&nvic); } // Rutina přerušení od EXTI jen nastaví "vlajku" podle které v mainu blikneme LEDkou void EXTI0_1_IRQHandler(void){ if(EXTI_GetITStatus(EXTI_Line0)){ EXTI_ClearITPendingBit(EXTI_Line0); irq_flag = 1; } } // PA5 je indikační výstup (k ověření že čip žije a pracuje) void init_test_output(void){ GPIO_InitTypeDef gp; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE); gp.GPIO_Pin = GPIO_Pin_5; gp.GPIO_Mode = GPIO_Mode_OUT; gp.GPIO_OType = GPIO_OType_PP; gp.GPIO_PuPd = GPIO_PuPd_NOPULL; gp.GPIO_Speed = GPIO_Speed_Level_1; GPIO_Init(GPIOA, &gp); } void pull_unused_gpio(void){ GPIO_InitTypeDef gp; // nastavíme všem pinům že jsou to vstupy s pull-down rezistorem // ponecháme si jen konfiguraci pinů SWD, které mají interní pullup/pulldown rezistory // na našem čipu jsou jen GPIOA,GPIOB a GPIOF RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA | RCC_AHBPeriph_GPIOB | RCC_AHBPeriph_GPIOF, ENABLE); gp.GPIO_Pin = GPIO_Pin_All; gp.GPIO_Mode = GPIO_Mode_IN; gp.GPIO_OType = GPIO_OType_PP; gp.GPIO_PuPd = GPIO_PuPd_DOWN; gp.GPIO_Speed = GPIO_Speed_Level_1; GPIO_Init(GPIOB, &gp); GPIO_Init(GPIOF, &gp); gp.GPIO_Pin = GPIO_Pin_All & (~(GPIO_Pin_13 | GPIO_Pin_14)); // vše krom PA13 a PA14 (SWD) GPIO_Init(GPIOA, &gp); // nezapomeneme vypnout clock ;) RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA | RCC_AHBPeriph_GPIOB | RCC_AHBPeriph_GPIOF, DISABLE); } void clock_48(void){ RCC_PLLConfig(RCC_PLLSource_HSI,RCC_PLLMul_12); // nastavit PLL na násobení 12x (8MHz / 2 * 12 = 48MHz) RCC_PLLCmd(ENABLE); // spustit PLL while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) != SET); // počkat na rozběh PLL RCC_HCLKConfig(RCC_SYSCLK_Div1); // SYSCLK z PLL nijak nedělit RCC_PCLKConfig(RCC_HCLK_Div1); // HCLK ze SYSCLK nijak nedělit (jedeme naplno) RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); // přepnout SYSCLK na PLL (jedeme na 48MHz) //SystemCoreClockUpdate(); // 0.14kB zbytečně... SystemCoreClock = 48000000; // clock je 48MHz } void clock_8(void){ RCC_HCLKConfig(RCC_SYSCLK_Div1); // SYSCLK nijak nedělit RCC_PCLKConfig(RCC_HCLK_Div1); // HCLK ze SYSCLK nijak nedělit (periferiím stejný takt jako jádru) RCC_SYSCLKConfig(RCC_SYSCLKSource_HSI); // zdrojem clocku je 8MHz HSI SystemCoreClock = 8000000; // clock je 8MHz } void clock_1(void){ RCC_HCLKConfig(RCC_SYSCLK_Div8); // SYSCLK dělit 8 => jádro jede na 1MHz... RCC_PCLKConfig(RCC_HCLK_Div1); // ...periferiím stejný takt jako jádru RCC_SYSCLKConfig(RCC_SYSCLKSource_HSI); // zdrojem clocku je 8MHz HSI SystemCoreClock = 1000000; // clock je 1MHz } void clock_31k(void){ RCC_HCLKConfig(RCC_SYSCLK_Div256); // SYSCLK dělit 256 => jádro jede na 31.25kHz... RCC_PCLKConfig(RCC_HCLK_Div1); // ...periferiím stejný takt jako jádru RCC_SYSCLKConfig(RCC_SYSCLKSource_HSI); // zdrojem clocku je 8MHz HSI SystemCoreClock = 31250; // clock je 31.25kHz } // Delay na bázi systicku (vyžaduje korektní nastavení SystemCoreClock) void _delay_ms(uint32_t Delay){ __IO uint32_t tmp = SysTick->CTRL; // Clear the COUNTFLAG first ((void)tmp); // init systick to us delays ... SysTick->LOAD = (SystemCoreClock/1000)-1; // 1us time SysTick->VAL = 0UL; SysTick->CTRL = SysTick_CTRL_ENABLE_Msk | SysTick_CTRL_CLKSOURCE_Msk; if(Delay < 0xffffff){Delay++;} // Add a period to guaranty minimum wait while (Delay){ if((SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk) != 0U){Delay--;} } }
Při měření odběru mějte na paměti že připojený debugger (ST-Link z Nucleo kitu) má odběr okolo 350uA. Před měřením jsem tedy ST-Link vždy odpojil (což je mírně řečeno otravné). Hodnoty odběru berte jako orientační.
Frekvence | Odběr |
48MHz | 12.05mA |
8MHz | 2.40mA |
1MHz | 0.68mA |
31kHz | 0.42mA |
Jestliže potřebujete přečkat delší období (pod nímž si můžete představit jednotky ms nebo celé hodiny) bez aktivity MCU, můžete k tomu využít jden ze tří režimů spánku. Jeho výběr záleží na tom co všechno nepotřebujete. Nejmělčí režim spánku je SLEEP, který vypne clock pouze jádru. Jak brzy uvidíte v aktivním režimu to může znamenat značnou úsporu. Probudit vás z něj může jakékoli přerušení nebo event (což jak brzy uvidíte je skoro to samé). V podstatě vás nijak neomezuje a ideálně se hodí na překlenutí období kdy nemá jádro nic na práci a pracují periferie. Například pokud pomocí DMA přijímáte nebo posíláte skrze nějaké rozhraní data. Nebo když čekáte na akci timeru a nebo když čekáte na výsledek AD převodů. Příkladů použití je asi nekonečno. K dispozici máme tři metody jak a kdy se probouzet a usínat.
int main(void){ clock_8(); // pracujeme například s 8MHz clockem pull_unused_gpio(); // ošetřit nepoužité GPIO init_test_output(); // PA5 jako výstup na LED init_exti(); // PA0 jako vstup while (1){ PWR_EnterSleepMode(PWR_SLEEPEntry_WFI); // Spi dokud nepřijde přerušení if(irq_flag){ // pokud došlo ke stisku, blikni LEDkou TEST_H; _delay_ms(500); TEST_L; irq_flag=0; } } }
Druhou zmíněnou možností jak čip uspat je instrukcí WFE. V takovém případě nás může vzbudit buď některá z EXTI linek nastavená jako Event. A nebo to může být jakékoli přerušení libovolné periferie, které není povolené v NVIC. Pojďme si nejprve vyzkoušet první možnost. V konfiguraci EXTI změníme EXTI_Mode z "Interrupt" na "Event". Smažeme konfiguraci NVIC i celou rutinu přerušení od EXTI. Odpadne nám také potřeba "vlajky" irq_flag. Program se probudí na místě kde usnul, vůbec nebude vstupovat do rutiny přerušení. My ho po probuzení necháme jen bliknout LEDkou, pak smažeme vlajku EXTI a opět uspíme. Vlajku mažu záměrně až za bliknutí LEDkou, abych se vyvaroval problémům se zákmity tlačítka.
int main(void){ clock_8(); // pracujeme například s 8MHz clockem pull_unused_gpio(); // ošetřit nepoužité GPIO init_test_output(); // PA5 jako výstup na LED init_exti(); // PA0 jako vstup while (1){ PWR_EnterSleepMode(PWR_SLEEPEntry_WFE); // Spi dokud nepřijde přerušení // tady by bylo na místě si zkontrolovat která periferie nás probudila ! // já ale vím že to bylo EXTI0 (jiný event jsem nepovolil) TEST_H; // Blikneme LEDkou _delay_ms(500); TEST_L; EXTI_ClearITPendingBit(EXTI_Line0); // vymažu vlajku abych mohl usnout a čekat na další stisk } } // EXTI z PA0 bude sloužit k buzení z režimů spánku // Na PA0 připojeno tlačítko proti VCC (interní pull down) void init_exti(void){ EXTI_InitTypeDef exti; GPIO_InitTypeDef gp; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE); // kvůli PA0 RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE); // kvůli EXTI // PA0 jako vstup s pull-down gp.GPIO_Pin = GPIO_Pin_0; gp.GPIO_Mode = GPIO_Mode_IN; gp.GPIO_OType = GPIO_OType_PP; gp.GPIO_PuPd = GPIO_PuPd_DOWN; gp.GPIO_Speed = GPIO_Speed_Level_1; GPIO_Init(GPIOA, &gp); // Přiřadíme Lince 0 port GPIOA (tedy mapujeme pin PA0) SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA,EXTI_PinSource0); // povolíme event z Linky 0 na vzestupnou hranu exti.EXTI_Line = EXTI_Line0; exti.EXTI_Mode=EXTI_Mode_Event; // Event (!) exti.EXTI_Trigger=EXTI_Trigger_Rising; exti.EXTI_LineCmd=ENABLE; EXTI_Init(&exti); // vůbec nepovolujeme EXTI v NVIC }
Předchozím způsobem lze budit čip jen pomocí externího přerušení a vybraných periferií vedoucích do EXTI jako například RTC. To vám často nebude stačit, takže si příklad upravíme tak aby MCU směla budit každá periferie. Úprava to bude snadná. Vrátíme EXTI zpět do módu "Interrupt" (případně pokud někdo z vás chce, nastavte si přerušení od jiné periferie). Někde v inicializaci nastavíme pomocí funkce NVIC_SystemLPConfig() bit SEVONPEND, který povolí buzení libovolným přerušením. Pro další vstup do spánku budeme muset mazat nejen vlajku v periferii (teď v EXTI) ale i v NVIC a to pomocí funkce NVIC_ClearPendingIRQ(). Opět si dovolím zveřejnit jen část kódu.
int main(void){ clock_48(); // pracujeme například s 8MHz clockem init_test_output(); // PA5 jako výstup na LED init_exti(); // PA0 jako vstup NVIC_SystemLPConfig(NVIC_LP_SEVONPEND, ENABLE); // budit jakýmkoli přerušením while (1){ PWR_EnterSleepMode(PWR_SLEEPEntry_WFE); // Spi dokud nepřijde přerušení // tady by bylo na místě si zkontrolovat která periferie nás probudila ! // já ale vím že to bylo EXTI0 (jiný event jsem nepovolil) TEST_H; _delay_ms(500); TEST_L; EXTI_ClearITPendingBit(EXTI_Line0); // vymažu vlajku v periferii NVIC_ClearPendingIRQ(EXTI0_1_IRQn); // vymazat vlajku i v NVIC } }
Poslední variantou bude program žijící jen v rutinách přerušení. Bude automaticky usínat po dokončení poslední rutiny přerušení. Jakmile čip uspíte už nikdy nevykoná žádný kód mimo rutiny přerušení. EXTI je opět nastavené jako "interrupt", povolené v NVIC a bliknutí LEDkou máme uvnitř rutiny přerušení. A ano máme tam i delay ! Což se na první pohled může zdát jako prohřešek proti slušným mravům. Jenže náš program jiný kód než rutiny přerušení vykonávat nemůže a tudíž nehrozí, že by tento delay zdržoval zbytek kódu od práce. Navíc pokud bychom chtěli upřednostnit nějaké další přerušení, máme možnost dát mu vyšší prioritu. Opět zveřejním jen část kódu a dovolím si vypustit inicializaci EXTI, neboť je shodná s prvním příkladem.
int main(void){ clock_31k(); // volím nižší clock když v aktivním režimu jen tupě čekám na LEDku pull_unused_gpio(); // ošetřit nepoužité GPIO init_test_output(); init_exti(); NVIC_SystemLPConfig(NVIC_LP_SLEEPONEXIT, ENABLE); // usnout po skončení IRQ rutiny PWR_EnterSleepMode(PWR_SLEEPEntry_WFI); // jdeme spát :) while (1){ // žádný kód tady teď nemá smysl ... program se sem nedostane } } // V rutině přerušení blikneme LEDkou void EXTI0_1_IRQHandler(void){ if(EXTI_GetITStatus(EXTI_Line0)){ TEST_H; // blikneme LEDkou _delay_ms(500); // ano, máme delay v IRQ rutině a není to proti slušnému chování :D TEST_L; EXTI_ClearITPendingBit(EXTI_Line0); // vlajku mažu opět až nakonec abych neměl problémy se zákmity } }
Když už jsem všechny ty různé režimy vyzkoušel, změřil jsem i jejich odběr. Takže než se vrhneme na režimy spánku, můžete si v následující tabulce prohlédnout jak si různá řešení vedou.
Frekvence | RUN | SLEEP (WFI) | SLEEP (WFE + Event z EXTI) | SLEEP (WFE) | SLEEP (WFI + Sleeponexit) |
48MHz | 12.1mA | 4.60mA | 4.55mA | 4.44mA | 4.56mA |
8MHz | 2.40mA | 0.83mA | 0.82mA | 0.82mA | 0.82mA |
1MHz | 0.68mA | 0.49mA | 0.48mA | 0.47mA | 0.47mA |
31kHz | 0.42mA | 0.42mA | 0.42mA | 0.42mA | 0.42mA |
Dalším režimem spánku je režim STOP. Je to hluboký spánek z něhož vás může probrat pouze EXTI. Do EXTI naštěstí vedou nejen linky externího přerušení, ale i vnitřní signály. V našem malém čipu konkrétně signály z RTC. V jiných čipech to ale budou další periferie jako komparátory, USB, I2C, UART, PVD nebo třeba Ethernet. Pokud vám tyto zdroje stačí, odmění vás režim spotřebou v řádech desítek uA. Ve STOP módu neběží HSE ani HSI oscilátor, ale regulátor napětí ano. Díky tomu zůstává zachován obsah RAM a GPIO si ponechávají svoje stavy. Regulátor můžete spustit v "low power" módu s drobně nižší spotřebou. Detailnější informace o regulátoru datasheet neuvádí, takže se můžeme jen dovozovat že v Low-power módu prostě není schopen dodat větší proudy. Otázka zní kdo to v režimu spánku potřebuje. Probuzení ze STOP módu trvá přibližně 5us, protože se musí startovat HSI oscilátor, který se vždy nastaví jako zdroj clocku. Pokud tedy plánujete používat například PLL, musíte si ho po každém probuzení rozběhnout. Stejně jako v režimu SLEEP lze MCU uspat pomocí WFI nebo WFE. Jak později uvidíte STOP je vlastně ten nejhlubší spánek v běžném slova smyslu.
Protože v tomto režimu neběží clock jádru ani periferiím (až na vyjímky), nemá clock ani debugovací systém. Pokud chcete v tomto režimu debugovat, musíte si to funkcí DBGMCU_Config() povolit. Přirozeně za cenu jisté spotřeby navíc. Takže do cílové aplikace je vhodné tuto funkci vypnout. Tím vyvstává otázka jak se s čipem spojit, když nemá v provozu SWD rozhraní. Jedna z možností je připojovat se během restartu ("connection under reset"). Vyvedete si reset na tlačítko. V konfiguraci debuggeru si nastavíte "connection under reset". Debugger pak čeká na váš reset, během něj se připojí a přebere kontrolu nad čipem. A jakmile tlačítko uvolníte nahraje program a umožní vám debug (do té doby než program vypne SWD rozhraní). Další možností je využít 5ti drátové připojení ST-Linku, na kterém jsem při návrhu bastldesky bohužel nemyslel :(
Úkol naší aplikace (bliknout po stisku tlačítka) zůstává nezměněn. K uspání jsem si dovolil použít variantu uspat pomocí WFE a budit eventem z EXTI (tedy B). Z prostorových důvodů opět nezveřejňuji celý zdrojový kód, ale jen klíčové části.
int main(void){ clock_8(); // pracujeme například s 8MHz clockem pull_unused_gpio(); // nenecháme GPIO viset "ve vzduchu" init_test_output(); // PA5 jako výstup na LED init_exti(); // PA0 jako vstup while (1){ PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_SLEEPEntry_WFE); // Spi dokud nepřijde event // odtud se aplikace rozbíhá s clockem z HSI // tady by bylo na místě si zkontrolovat která periferie nás probudila ! // já ale vím že to bylo EXTI0 (jiný event jsem nepovolil) TEST_H; // Blikneme LEDkou _delay_ms(500); TEST_L; EXTI_ClearITPendingBit(EXTI_Line0); // vymažu vlajku abych mohl usnout a čekat na další stisk } } // EXTI z PA0 bude sloužit k buzení z režimů spánku // Na PA0 připojeno tlačítko proti VCC (interní pull down) void init_exti(void){ EXTI_InitTypeDef exti; GPIO_InitTypeDef gp; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE); // kvůli PA0 RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE); // kvůli EXTI // PA0 jako vstup s pull-down gp.GPIO_Pin = GPIO_Pin_0; gp.GPIO_Mode = GPIO_Mode_IN; gp.GPIO_OType = GPIO_OType_PP; gp.GPIO_PuPd = GPIO_PuPd_DOWN; gp.GPIO_Speed = GPIO_Speed_Level_1; GPIO_Init(GPIOA, &gp); // Přiřadíme Lince 0 port GPIOA (tedy mapujeme pin PA0) SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA,EXTI_PinSource0); // povolíme externí přerušení z Linky 0 na vzestupnou hranu exti.EXTI_Line = EXTI_Line0; exti.EXTI_Mode=EXTI_Mode_Event; // Event (!) exti.EXTI_Trigger=EXTI_Trigger_Rising; exti.EXTI_LineCmd=ENABLE; EXTI_Init(&exti); // vůbec nepovolujeme EXTI v NVIC }
Reakční dobu, něco málo přes 5us, si můžete prohlédnout na oscilogramu. Spotřeba naší aplikace je teď 17.6uA. Protože mám VDDA a VDD na své desce spojené, mohu v option bytes vypnout VDDA monitor, tedy obvod dohledu nad VDDA a snížit tak spotřebu na 16.5uA.
STANDBY bych asi neoznačil za režim spánku. Spíš než spánek je to smrt. Tenhle nejúspornější režim totiž spotřebě obětuje téměř vše. Odpojí od energie drtivou většinu čipu, takže až na pár výjimek (které budu diskutovat) všechny piny přejdou do stavu vysoké impedance. Všechny periferie jsou vypnuty, oscilátory HSI a HSE neběží. Obětován je také obsah RAM, protože i ta přijde o energii. Čip může probudit jen RTC, vzestupná hrana na WKUP pinu, Reset nebo Watchdog a po probuzení projde čip restartem ! Spíš než probuzení ze spánku se dá mluvit o zmrtvých vstání. Existuje velmi omezená možnost jak si přece jen něco z minulého života (tedy z doby před uspáním / smrtí) zapamatovat. Energii dostává jen wakeup obvod a backup doména. V backup doméně se nachází RTC a nízkofrekvenční oscilátory (tedy LSE a LSI). Spolu s nimi tam je také skupina 5ti 32bitových registrů do nichž si můžeme uložit data, která mají přečkat smrt. Legrační je fakt, že se o nich datasheet zmiňuje jen náznakem a to větou "Tamper detection erases the backup registers". Žádné další informace v datasheetu nenajdete (takže ani nevíte co ta Tamper událost vlastně maže). Jiné datasheety (např k čipům F0x1 se jim věnuje). Je tedy otázkou jestli je to záměr nebo chyba. Každopádně jak brzy uvidíte, fungují. Osobně se domnívám, že STANDBY režim najde uplatnění jen vyjmečně, ale to není důvod si ho nevyzkoušet.
Pro jednoduchost budeme čip budit k životu signálem na WKUP1 pinu. Na našem malém čipu je to pin PA0. Jak už bylo řečeno, k probuzení je nutné přivést vzestupnou hranu. Proto mám tlačítko připojené trochu netradičně proti VCC a pro tuto aplikaci jsem ho vybavil externím pull-down rezistorem. Náš program bude mít za úkol počítat kolikrát byl probuzen od posledního restartu a po každém probuzení bliknout LEDkou právě tolikrát. Tím získáme jakýsi důkaz, že si alespoň tuto minimální informaci dokáže zapamatovat. Díky tomu, že se po usnutí všechny piny "odpojí" nemusíme je ošetřovat tak jako v předchozích ukázkách. Protože ale čip prochází při každém probuzení restartem, musíme nějak rozpoznat zda restart proběhl následkem probuzení (skrze WKUP1 pin) a nebo zda šlo o tvrdý reset pomocí tlačítka na RST. Rozpoznat to lze pomocí vlajky WUF (resp. WU). Ta se nachází v PWR periferii a abychom ji mohli číst (a později provádět další akce), musíme do PWR přivést clock. To bude tedy náš první krok po "spuštění". Pokud je vlajka WU (Wake Up) nastavená, znamená to že jde o "probuzení" a ne restart. V takovém případě si odemkneme přístup do backup domény (fcí PWR_BackupAccessCmd() ) a přečteme si z nultého backup registru aktuální počet vzbuzení (fcí RTC_ReadBackupRegister() ). Pak počet startů inkrementujeme a novou hodnotu opět uložíme do backup registru (fcí RTC_WriteBackupRegister() ). Následně ze slušnosti ještě zakážeme přístup do backup domény. Nastavíme si výstup pro LED a zablikáme zjištěný počet probuzení. Pokud čip prošel restartem (místo legálního probuzení), bude vlajka WU vynulovaná a my si vynulujeme počítadlo (v backup registru). Po jedné z těchto dvou akcí čip uspíme. Což proběhne ve třech krocích. Nejprve si vymažeme WU vlajku abychom příště rozpoznali zdroj probuzení, povolíme buzení z WKUP pinu (fcí PWR_WakeUpPinCmd() ) a usneme příkazem PWR_EnterSTANDBYMode().
uint32_t i,pocet; int main(void){ clock_1(); // pracujeme například s 1MHz clockem //DBGMCU_Config(DBGMCU_STANDBY, ENABLE); // povolit debug v STANDBY módu RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE); // spustit clock PWR periferii // rozlišíme jestli se čip probudil Wakeup pinem nebo Resetem if(PWR_GetFlagStatus(PWR_FLAG_WU) == SET){ // pokud jde o Wakeup PWR_BackupAccessCmd(ENABLE); // povolíme přístup do backup domény pocet=RTC_ReadBackupRegister(RTC_BKP_DR0); // přečteme si počet startů... pocet++; // ...inkrementujeme ho ... RTC_WriteBackupRegister(RTC_BKP_DR0,pocet); //... a uložíme PWR_BackupAccessCmd(DISABLE); // zakážeme přístup do backup domény (není nutné) init_test_output(); // nastavíme PA5 jako výstup // Blikneme tolikrát kolikrát už se aplikace probudila od posledního restartu for(i=0;i<pocet;i++){ TEST_H; _delay_ms(250); TEST_L; _delay_ms(250); } }else{ // pokud jsme se probudili restartem ... PWR_BackupAccessCmd(ENABLE); RTC_WriteBackupRegister(RTC_BKP_DR0,0); // ... vymažeme počítadlo "probuzení" PWR_BackupAccessCmd(DISABLE); } // vyčistíme vlajku "Wake up" abychom příště poznali co nás probudilo PWR_ClearFlag(PWR_FLAG_WU); PWR_WakeUpPinCmd(PWR_WakeUpPin_1, ENABLE); // povolíme buzení z WKUP1 pinu (PA0) PWR_EnterSTANDBYMode(); // uspíme čip while (1){ // sem se vůbec nedostaneme } }
Na našem čipu se nachází jen jeden WKUP pin (PA0), STM32F0x0 ve větších pouzdrech mají piny dva. A lepší čipy (např F072) pak ještě více. Mimo tlačítko můžete na pin přivádět signál například z low-power časovačů, případně z různých externích obvodů. Mnohem širší využití má buzení pomocí RTC. To si ale necháme na příště. Nejspíš jste zvědaví na spotřebu aplikace z poslední ukázky. Tak vás nebudu napínat, naměřil jsem 2.1uA.
Home
| V1.00 29.4.2019 /
| By Michal Dudka (m.dudka@seznam.cz) /