Tutoriál o základních low-power technikách na STM32F0. V krátkosti v něm vyzkoušíme závislost spotřeby na taktovací frekvenci a pak projdeme tři režimy spánku - SLEEP, STOP a STANDBY.

Vybrané Low-Power postupy pro STM32F0

Úvod

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.

  • Všem periferiím na STMku lze vypnout clock (tzv. "Clock gating"). Všichni to běžně děláte, protože po startu má většina z nich clock vypnutý a vy si ho musíte zapnout. Není na ní nic složitého a jen pro představu třeba takové GPIOB spolkne při 48MHz přibližně 0.7mA (a to aniž by piny PB vykonávaly nějakou činnost).


  • Odběr závisí na frekvenci. Je tedy vhodné taktovat jádro nebo periferie vysokými frekvencemi jen když je to opravdu potřeba. Systém prescalerů (děliček/předděliček chcete-li) vám dává široké možnosti. Umožňuje vám měnit si clock dle okamžité potřeby. Například můžete před výpočtem jádro taktovat na 48MHz a po výpočtu snížit frekvenci kam vám aplikace dovolí (klidně na desítky kHz). Podobné je to i u periferií, timeru tikajícím na několika kHz nebo UARTu s baudrate 9600B/s bohatě postačí clock pod 1MHz.


  • Režimy spánku umožňují snížit spotřebu až o několik řádů. Čím hlubší režim spánku zvolíte, tím menší bude spotřeba a tím méně událostí bude schopných mikrokontrolér probudit.


  • Nezanedbatelný vliv má také konfigurace nevyužitých pinů. Jsou-li nastaveny jako vstupy a není na ně přivedena jasná logická hodnota, mohou zvyšovat odběr v řádu desítek až stovek uA. Je tedy vhodné je správně ošetřit a nevyužité GPIO dát do analogového módu, nebo jim připnout pullup nebo pulldown rezistor.


Vliv taktovací frekvence

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:

  • clock_48() Rozběhne PLL z HSI na 48MHz a zvolí ho jako clock jádra i všech periferií. Tedy provoz na "plný výkon"

  • clock_8() Jádro i periferie poběží na frekvenci HSI, tedy 8MHz

  • clock_1() Clock z HSI podělím 8 a jádro i periferie poběží na 1MHz

  • clock_31k() Clock z HSI podělím 256 a jádro i periferie poběží na 31.25kHz


Sluší se poznamenat, že naše aplikace má valnou většinu periferií vypnutou. Zapnuli jsme jen GPIOA a SYSCFG. Všimněte si také, že udržuji proměnnou SystemCoreClock aktuální neboť ji ke své činnosti potřebuje moje _delay_ms() funkce (a mnoho dalších z SPL knihoven). Ke stanovení hodnoty jsem mohl použít knihovní funkci SystemCoreClockUpdate() ale stálo by mě to přibližně 0.14kB paměti a já s ní nechtěl na tak malém čipu plýtvat. Před konfigurací GPIO volám funkci pull_unused_gpio() jejímž úkolem je nastavit všechny piny, krom PA13 a PA14, jako vstupy s pull-down rezistorem. Na zmíněných dvou pinech máme SWD a jejich rekonfigurací bychom se připravili o možnost debugu (navíc v roli SWD pullup/down rezistory obsahují). Tento postup má zaručit, že na všech pinech bude nějaká rozumně definovaná vstupní hodnota. Jak už bylo napsáno, bez tohoto ošetření, pokud jsou piny ponechány jako vstup bez pull-down nebo pull-up, zvyšují odběr řádově o desítky až stovky uA.


// 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í.

Orientační spotřeba naší aplikace v závislosti na frekvenci
Frekvence    Odběr
48MHz        12.05mA
8MHz         2.40mA
1MHz         0.68mA
31kHz        0.42mA


Z tabulky je vidět, že každý MHz clocku zvedne spotřebu přibližně o 240uA. A taky si můžete všimnout, že od určitého bodu nemá další snižování frekvence smysl. Jinak řečeno snížit takt ze 48MHz na 24MHz vám přinese úsporu okolo 6mA, kdežto snížení frekvence z 64kHz na 32kHz nebude bez použití dalších technik nijak zvlášť patrné.

Režim SLEEP


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.

  • A) Můžeme usínat příkazem WFI (Wait For Interrupt) a probouzet se pomocí přerušení. Příslušné přerušení, které má čip vzbudit musí být povolené v NVIC. Po probuzení program skočí do rutiny přerušení a vykoná ji. Pak pokračuje do té doby než opět narazí na příkaz ke spánku.

  • B) Můžeme usínat příkazem WFE (Wait For Event) a budit se eventem z EXTI (což prakticky znamená některým z externích přerušení nebo RTC). Před usnutím si musíme mazat příslušnou vlajku v EXTI.

  • C) Můžeme usínat příkazem WFE (Wait For Event) a budit se přerušením od libovolné periferie aniž bychom vstupovali do rutiny přerušení. Zdroj přerušení musíme povolit v periferii a nepovolovat v NVIC. Program se rozběhne od místa kde usnul a před usnutím si musíme vymazat příslušné vlajky jak v periferii tak v NVIC (jinak by příště MCU neusnulo).

  • D) Můžeme usínat automaticky po skončení rutiny přerušení. Aplikace pak nemusí volat instrukci WFI a usne ihned jakmile dokončí rutinu a žádné další přerušení nečeká na obsloužení. Veškerý kód aplikace (kromě inicializace) se pak musí nacházet v rutinách přerušení, protože od prvního uspání už program pracuje jen v nich.

Všechny čtyři metody si na našem modelovém příkladu vyzkoušíme.

A - Začneme první z nich - Wait for Interrupt. V našem kódu nemusíme nic měnit, stačí přidat jediný řádek s funkcí PWR_EnterSleepMode(). Jejím argumentem je metoda uspání, tedy buď WFI nebo WFE. Nejprve tedy WFI. Program po vykonání této funkce usne a probere se až s příchodem externího přerušení na PA0, vykoná rutinu přerušení a vrátí se do hlavní smyčky za příkaz ke spánku. Tam na něj čeká kód který blikne LEDkou . Pak opět narazí na příkaz ke spánku a usne. Tento příklad lze snadno modifikovat a budit se libovolnou periferií. Protože celý zbytek kódu zůstává stejný, dovolím si zveřejnit jen jeho část.


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;
  }
 }
}
 


B - 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
}
 


C - 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
 }
}
 


D - 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.

Odběr v testovaných režimech

Frekvence  RUN      SLEEP (WFI) SLEEP (WFE + Event z EXTI)  SLEEP (WFE)   SLEEP (WFI + Sleeponexit)
48MHz      12.10mA   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
 


Vidíte, že obecně může mít SLEEP režim citelný dopad na spotřebu a pro mnoho aplikací bohatě postačí (zvláště pokud má okolní HW například o řád větší spotřebu než MCU).

Režim STOP


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.


Doba probuzení ze STOP režimu. Světle modrá signál pro indikační LED, tmavě modrá stav tlačítka.


Režim STANDBY


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.

Zdrojové kódy ke stažení: zdrojaky.zip


Testovací sestava. Na bastl desce STM32F030F4, jako debugger ST-Link z nucleo kitu.


Odkazy
STM32F030 Reference manual
STM32F030 Datasheet
Režimy spánku na STM32L0

Rozcestník na další díly seriálu naleznete na www.elektromys.eu/stm32.php V1.00 29.4.2019
By Michal Dudka (m.dudka@seznam.cz)