Malá poznámka s demonstrací o vybraných low power režimech STM32L0

STM32L0 Low-power režimy I

Rodina STM32L0 je, jak jistě víte, určena pro "low-power" aplikace a asi nemá smysl ji nějak obšírně představovat. Mě osobně k ní přivedla kombinace několika faktorů. Byla to velmi příznivá cena variant v TSSOP pouzdře (STM32L011F4 lze koupit za přibližně 25kč) a neblahé zkušenosti se spotřebou 8bitových Attiny 1-series. Techniky, které může STM32L0 používat k optimalizaci spotřeby je celá řada a já s nimi nemám dost praktických zkušeností. Proto budu tento krátký tutoriál věnovat postupům jak snížit odběr v tzv. aktivním režimu (chcete-li "bdělém" stavu). Mnohdy totiž stačí srazit spotřebu na desítky uA a není třeba "trápit se" s režimy spánku (s nimiž je spotřeba ještě o řád nižší).

Stejně jako u většiny MCU má největší vliv na spotřebu taktovací frekvence ("clock"). Datasheet k STM32L011 na níž budu experimentovat uvádí spotřebu "až" 76uA/MHz (která je od praktické reality ale hodně vzdálená). Jako zdroj clocku máme krom 16MHz HSI (případně PLL), také MSI (Medium Speed) RC oscilátor. Jeho frekvenci je možné přepínat v sedmi krocích od 65kHz do 4.2MHz. Další snižování taktu jádru nebo periferiím lze docílit děličkami AHB a APB jako u všech STM. Slušnou kontrolu máme také nad vnitřním napěťovým regulátorem. Můžeme vybírat napětí pro digitální obvody čipu mezi 1.8V (tzv. Range1), 1.5V (Range2) a 1.2V (Range3). Se snižováním napětí klesá odběr a krom toho se také snižuje minimální napájecí napětí na kterém lze čip provozovat. Nad to všechno lze regulátor přepnout do tzv. Low-Power Run (LPR) módu a opět tím ušetřit další desítky uA. No a já si dovolím v několika krátkých prográmcích tyto funkce vyzkoušet a změřit při tom odběr. Veškeré ovládání je jednoduché a jedinou komplikací je zorientovat se v omezeních kdy lze kterou funkci využít. Což v praxi znamená posbírat podmínky různě roztroušené po datasheetu a nebo věřit, že jsem je správně shrnul v následujícím odstavci.

Omezení

  • Snížené napětí jádra (pod 1.8V) omezuje maximální frekvenci na níž smíte čip taktovat a některé kombinace napětí a frekvence vás nutí adekvátně zvětšit latenci pro přístup k flash paměti (přidat 1 "wait state"). Přesné limity najdete v tabulce níže.
  • Režim Low-Power Run smíte používat jen s taktem pod cca 130kHz a jen s napětím 1.5V (Range2). Navíc je omezen počet aktivních periferií (viz závěrečné poznámky).
  • Při napájecím napětí pod 2V je změnu frekvence o velký krok (víc jak o čtyřnásobek) nutné provést ve dvou krocích s prodlevou 5us. Například pokud chcete z výchozí frekvence 4.2MHz přejít na plný výkon (32MHz), musíte nejprve přepnout na 16MHz, na tomto kmitočtu vydržet alespoň 5us a teprve pak můžete přepnout na 32MHz.
  • Regulátor v Range 1 (napětí 1.8V) smíte použít pokud je napájecí napětí čipu větší jak 1.71V (v datasheetu je to formulováno velmi vtipně, viz závěrečné poznámky)

Napětí /
Frekvence
Range 1
Vdd > 1.71V
Range 2
Vdd > 1.65V
Range 3
Vdd > 1.65V
f > 16MHz 1WS - -
f = 8~16MHz 0WS 1WS -
f = 4.2~8MHz 0WS 0WS 1WS
f < 4.2MHz 0WS 0WS 0WS
f < 132kHz 0WS 0WS, LPR 0WS
Další omezení PLL<=96MHz PLL<=48MHz PLL<=24MHz,
Nelze mazat a
zapisovat Flash
Vysvětlivky:
- Zakázaná konfigurace
WS "Wait state"
LPR Low power run

PLL výstupní frekvence PLL
Vdd Napájecí napětí čipu


Testy

Další teorií vás nudit nebudu a rovnou se pustím do jednoduchých testů. Pro jednoznačnost zrekapituluji. Pracujeme na čipu STM32L011. Vzorové kódy využívají "LL" knihoven (HAL nemusím a STDper pro L0 nejsou). Napájecí napětí je 3.3V pokud nenapíšu jinak. Abychom mohli v různých režimech srovnávat spotřebu budeme ve všech příkladech blikat LEDkou na PA5 v rytmu 5Hz (tedy každých 100ms přepneme stav LED). Celý zdrojový kód je ke stažení v odkazech a nudnými kusy kódu (inicializaci pinů) vás nebudu obtěžovat. Čekací smyčku realizuji pomocí Systicku. Funkce LL_Init1msTick(SystemCoreClock) naplní strop Systicku odpovídající hodnotou aby přetékal každou milisekundu. Funkce LL_mDelay() pak vyčká odpovídající počet přetečení Systicku. Obě funkce jsou z knihovny "...utils.h". Protože se budeme během některých ukázek pohybovat s odběrem v řádu desítek uA, tak pro lepší objektivitu výsledků nastavíme všechny nepoužité piny (krom SWD rozhraní) do analogového módu. Výsledné spotřeby pro každou konfiguraci jsem shrnul do tabulky v závěru.

32MHz z PLL

V první ukázce rozběhneme čip na maximální frekvenci. V knihovně "...utils.h", je funkce LL_PLL_ConfigSystemClock_HSI(), která zjednodušuje nastavení clocku. Předhodíte ji parametry PLL a děliček pro APB a AHB a ona sama nastaví odpovídající latenci pro flash paměť, spustí PLL, přepne ho jako systémový clock (sysclk) a nastaví děličky. Z návratové hodnoty můžete zjistit zda proběhla úspěšně. Před jejím zavoláním si musíte pohlídat několik věcí sami. V prvé řadě musíte sami nastavit napěťový regulátor do Range 1 (1.8V), protože po startu čip běží v Range 2 (s clockem 2 MHz z MSI). Konfiguraci napětí provádí funkce LL_PWR_SetRegulVoltageScaling() jejíž argumenty jsou "...SCALE1" až "...SCALE3". Což je totéž jako Range 1 až Range 3 o nichž už padla zmínka. Škoda, že knihovny nedodržují terminologii z datasheetu. Přirozeně před voláním funkcí z PWR knihoven musíte do PWR pustit clock (jako všemu na STM). Před změnou clocku je potřeba ověřit že je regulátor v Range 1 a případně počkat (přepnutí může nějakou dobu trvat). Zda změna probíhá se můžete dozvědět z funkce/vlajky LL_PWR_IsActiveFlag_VOS().


// Taktujeme čip pomocí PLL na "plný výkon" (32MHz)
void clock_32MHz(void){
 ErrorStatus status;
 LL_UTILS_PLLInitTypeDef pll; // struktura pro konfiguraci PLL
 LL_UTILS_ClkInitTypeDef clk; // struktura pro konfiguraci AHB a APB děliček
 // PLLCLK = 16MHz * 4 / 2 = 32MHz (PLLVCO = 16*4 = 64MHz)
 pll.PLLDiv = LL_RCC_PLL_DIV_2; // 16MHz HSI dělíme 2 na 8MHz pro PLL
 pll.PLLMul = LL_RCC_PLL_MUL_4; // 8MHz pro PLL násobíme 4 na 32MHz
 clk.AHBCLKDivider = LL_RCC_SYSCLK_DIV_1; // Sysclkock nedělit
 clk.APB1CLKDivider = LL_RCC_APB1_DIV_1; // clock pro periferie nedělit
 clk.APB2CLKDivider = LL_RCC_APB2_DIV_1; // clock pro periferie nedělit
 // Přepneme regulátor do Range1 (Výchozí je Range2)
 LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_PWR); // clock pro PWR kontrolér
 LL_PWR_SetRegulVoltageScaling(LL_PWR_REGU_VOLTAGE_SCALE1); // 1.8V pro jádro
 while(LL_PWR_IsActiveFlag_VOS()); // počkáme než změna napětí proběhne
 // zavoláme konfirugraci clocku (fce spustí PLL, nastaví latenci, přepne Sysclock na PLL)
 status=LL_PLL_ConfigSystemClock_HSI(&pll,&clk);
 // pokud vše proběhlo úspěšně, běžíme na 32MHz
 if(status==SUCCESS){SystemCoreClock = 32000000UL;}
}
 


16MHz z HSI

Taktovat čip z vnitřního 16MHz oscilátoru je velice jednoduchá záležitost. Stačí jej pomocí LL_RCC_HSI_Enable() zapnout, vyčkat než naběhne (indikuje funkce LL_RCC_HSI_IsReady()), nastavit latenci flash paměti pomocí LL_FLASH_SetLatency() a přepnout systémový clock pomocí LL_RCC_SetSysClkSource(). Protože nekonfiguruji napětí jádra, počítám s tím že čip běží s výchozí konfigurací, tedy v Range 2 (proto také nastavuji 1 Wait state).


// Taktujeme 16MHz z HSI, čip ve výchozím natavení (regulátor v Range 2)
void clock_16MHz(void){
 // rozběhnout HSI a počkat na jeho stabilizaci
 while(!LL_RCC_HSI_IsReady()){LL_RCC_HSI_Enable();}
 LL_RCC_HSI_DisableDivider(); // clock z HSI nedělit
 LL_FLASH_SetLatency(LL_FLASH_LATENCY_1); // V Range 2 je potřeba 1WS (v Range 1 stačí 0WS)
 LL_RCC_SetSysClkSource(LL_RCC_SYS_CLKSOURCE_HSI); // přepnout systémový clock
 SystemCoreClock = HSI_VALUE;
}
 

Bude-li někomu z vás vadit latence flash paměti, může (nám už známým postupem) přepnout čip do Range 1. Takže jen pro kompletnost uvádím zdrojový kód. Jestli to má nějaké rozumné opodstatnění netuším, ale zajímalo mě jak to zahýbe se spotřebou.


// Taktujeme 16MHz z HSI, regulátor napětí přepneme do Range 1 (1.8V)
void clock_16MHzR1(void){
 // rozběhnout HSI a počkat na jeho stabilizaci
 while(!LL_RCC_HSI_IsReady()){LL_RCC_HSI_Enable();}
 LL_RCC_HSI_DisableDivider();  // clock z HSI nedělit
 // Přepnout napěťový regulátor do Range 1
 LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_PWR); // clock pro PWR
 LL_PWR_SetRegulVoltageScaling(LL_PWR_REGU_VOLTAGE_SCALE1); // Nastavit napětí 1.8V
 while(LL_PWR_IsActiveFlag_VOS());  // počkat než se regulátor přepne
 LL_FLASH_SetLatency(LL_FLASH_LATENCY_0); // v Range 1 stačí 0WS (což je výchozí hodnota)
 LL_RCC_SetSysClkSource(LL_RCC_SYS_CLKSOURCE_HSI); // přepnout systémový clock
 SystemCoreClock = HSI_VALUE;
}
 


Dovolím si věnovat malou pozornost děličce za HSI. Funkcí LL_RCC_HSI_EnableDivider() a LL_RCC_HSI_DisableDivider() můžete zapnout nebo vypnout děličku čtyřmi. Zajímavá bude asi hlavně v případě kdy HSI nepoužíváte jako clock pro jádro, ale pro LPTIM, LPUART nebo I2C.

1MHz z MSI

Nadpis je lehce zavádějící, protože MSI oscilátor má ve svém výběru frekvenci 1.048MHz, nikoli 1MHz (ale to by se hodně špatně četlo). Konfigurace je naprosto triviální. Čip má totiž po startu rozběhnutý MSI na frekvenci 2.096MHz (a bere z něj clock). Takže stačí pomocí funkce LL_RCC_MSI_SetRange() zvolit požadovanou frekvenci. Pokud nechceme do SystemCoreClock zapisovat frekvenci ručně, můžeme si pomoc makrem __LL_RCC_CALC_MSI_FREQ(), které ji za nás dopočítá. Když si ho prohlédnete, zjistíte, že frekvence MSI jsou násobky známé konstanty 65536 a tedy mocniny dvou.


// Taktuje čip 1.048MHz z MSI (MCU ve výchozím nastavení)
void clock_1MHz(void){
 LL_RCC_MSI_SetRange(LL_RCC_MSIRANGE_4); // zvolíme frekvenci MSI (1.048MHz)
 //while(!LL_RCC_MSI_IsReady()){LL_RCC_MSI_Enable();} // rozběhne MSI (lze vynechat pokud je čip ve výchozí konfiguraci - po startu)
 //LL_RCC_SetSysClkSource(LL_RCC_SYS_CLKSOURCE_MSI); // přepne zdroj clocku na MSI (opět lze vynechat ve výchozí konfiguraci)
 //LL_FLASH_SetLatency(LL_FLASH_LATENCY_0); // Nastaví 0 Wait state pro flash (opět lze vynechat ve výchozí konfiguraci)  
 SystemCoreClock = __LL_RCC_CALC_MSI_FREQ(LL_RCC_MSIRANGE_4);
}


132kHz z MSI

Při naší cestě od vysokých taktovacích frekvencí k nižším, míjíme 132kHz. Tahle frekvence je zajímavá protože na ní už smíme přepnout regulátor do low power režimu. To lze provést funkcí LL_PWR_EnterLowPowerRunMode(). Před jejím voláním je ale dobré ohlídat si že je regulátor v Range 2. Zvláště pokud jste ho před tím měnili, jinak je v tomto režimu už po startu.


// Taktujeme čip na ~32.768kHz, regulátor v low-power, regulátor by měl být v Range 2
void clock_131kHz(void){
 LL_RCC_MSI_SetRange(LL_RCC_MSIRANGE_1); // zvolíme frekvenci MSI (132kz)
 //while(!LL_RCC_MSI_IsReady()){LL_RCC_MSI_Enable();} // rozběhne MSI (lze vynechat pokud je čip ve výchozí konfiguraci - po startu)
 //LL_RCC_SetSysClkSource(LL_RCC_SYS_CLKSOURCE_MSI); // přepne zdroj clocku na MSI (opět lze vynechat ve výchozí konfiguraci)
 // Low power mód regulátoru lze zapnout jen v Range 2 (což je výchozí volba po startu čipu)
 //LL_FLASH_SetLatency(LL_FLASH_LATENCY_0); // Nastaví 0 Wait state pro flash (opět lze vynechat ve výchozí konfiguraci)
 LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_PWR);  // clock pro PWR doménu
 LL_PWR_EnterLowPowerRunMode(); // Přepneme regulátor do Low power módu (teoreticky teď máme limitované množství aktivních periferií)
 SystemCoreClock = __LL_RCC_CALC_MSI_FREQ(LL_RCC_MSIRANGE_1);
}
 


32.768kHz z MSI

Postupné snižování frekvence zakončíme na 32.768kHz. Další snižování ztrácí smysl, neboť dynamická složka odběru (závislá na frekvenci) je zanedbatelná ve srovnání se statickou složkou. Dál už na nás čekají jen režimy spánku nebo snižování napájecího napětí. Protože minimální frekvence MSI je 65.536kHz musíme využít AHB děličky a dělit dvěma. Opět můžeme regulátor přepnout do low power režimu. Další mikroampéru můžeme ušetřit deaktivací interní reference VREFINT. Po volání funkce LL_PWR_EnableUltraLowPower() bude během low power režimu (v němž běžíme) reference vypnutá. Tím přicházíme o BOR, PVD a interní teplotní senzor (což nám v tomto případě nevadí).


// Taktujeme čip na ~32.768kHz, regulátor v low-power, regulátor by měl být v Range 2
void clock_32kHz(void){
 LL_RCC_MSI_SetRange(LL_RCC_MSIRANGE_0); // přepneme MSI na 65.536kHz
 //while(!LL_RCC_MSI_IsReady()){LL_RCC_MSI_Enable();} // rozběhne MSI (lze vynechat pokud je čip ve výchozí konfiguraci - po startu)
 //LL_RCC_SetSysClkSource(LL_RCC_SYS_CLKSOURCE_MSI); // přepne zdroj clocku na MSI (opět lze vynechat ve výchozí konfiguraci)
 //LL_FLASH_SetLatency(LL_FLASH_LATENCY_0); // Nastaví 0 Wait state pro flash (opět lze vynechat ve výchozí konfiguraci)
 LL_RCC_SetAHBPrescaler(LL_RCC_SYSCLK_DIV_2); // Sysclock = 65.536/2 = 32.768kHz
 LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_PWR); // clock pro PWR doménu
 LL_PWR_EnterLowPowerRunMode();  // Přepneme regulátor do Low power módu
 SystemCoreClock = __LL_RCC_CALC_MSI_FREQ(LL_RCC_MSIRANGE_0) / 2; // frekvence jádra
 LL_PWR_EnableUltraLowPower(); // Vypneme interní referenci VREFINT
}
 


Vyhodnocení

No a jak to všechno dopadlo ? Řekl bych že docela dobře. Protože STM poráží v těchto režimech ve spotřebě i 8bitové MCU řady Attiny.
Namátkou při 1MHz a 3.3V se dle datasheetu spotřeby pohybují:
Attiny24 - 600uA
Attiny13 - 500uA
Attiny1614 - 260uA
Srovnejte to s níže uvedenými výsledky našich pokusů.

FrekvenceZdroj
clocku
Napětí
regulátoru
Napájecí
napětí
spotřebapoznámka
32MHzPLL(HSI)Range 13.3V4.55mA
16MHzHSIRange 23.3V1.96mA
16MHzHSIRange 13.3V2.66mA
4MHzHSIRange 33.3V0.63mA
1MHzMSIRange 33.3V246uA
1MHzMSIRange 33.0V208uA
1MHzMSIRange 32.5V186uA
1MHzMSIRange 31.8V158uA
131kHzMSI Range 33.3V44uA
131kHzMSI Range 23.3V35uALow Power Run
32kHzMSI Range 23.3V21.1uALow Power Run
32kHzMSI Range 23.3V20.6uALow Power Run + ULP
32kHzMSI Range 23.0V20.0uALow Power Run + ULP
32kHzMSI Range 21.8V19.4uALow Power Run + ULP

Spotřeba v aktivích režimech

Závěrečné poznámky

  • Formulace "The voltage regulator outputs a 1.8 V voltage (typical) as long as the Vdd input voltage is above 1.71 V", kterou najdete v datasheetu, mi připadala velmi úsměvná.
  • O něco méně úsměvnější je poznámka "Please refer to the product datasheet for more details on voltage regulator and peripherals operating conditions". Já zmiňované detaily prostě nenašel...


STM32L011 na "bastldesce". Vyvedený reset na tlačítko a programátor je pro low-power experimenty víc než dobrý nápad.




Odkazy

Další tutoriály na www.elektromys.eu
By Michal Dudka (m.dudka@seznam.cz) 9.10.2019