logo_elektromys.eu

/ STM32L0 Low-power režimy II |

Motivace

Většina z mých domácích (velmi jednoduchých) aplikací s MCU, které jsem za poslední roky poskládal měla napájení z li-ion článku a malý solární panel. Připravil jsem si tedy HW+SW šablonu, kterou do každého dalšího miniprojektu jen nakopíruji. A protože jde o jednoduchou aplikaci kde MCU řídí nabíjení, věřím že by mohla posloužit jako výukové demo začátečníkům s STM32.

Hardware

Dělič 680k ku 1M8 (dělicí poměr 73%):
Dělič 1M5 ku 470k (dělicí poměr 24%): První dělič, je evidentně lepší. Obecně je vhodné volit dělicí poměr tak aby při maximálním napětí baterie dosahovalo výstupní napětí děliče těsně k maximálnímu vstupnímu napětí AD převodníku (tedy těsně pod jeho referenci - v našem případě 3.3V).
Protože lithiový článek dodává větší napětí než maximálních 3.6V pro STM32, musí v obvodu být stabilizátor napětí. Jako slušný kompromis ceny, klidové spotřeby a maximálního napájecího napětí se mi jevil TS9011. Aby šlo měřit napětí akumulátoru AD převodníkem je ho nutné snížit děličem. Při volbě odporů v děliči se kvůli nízké spotřebě držíme velkých hodnot. Já volil 680k ku 1M8. Dělicí poměr je vhodné volit co nejmenší (ve smyslu získat na výstupu děliče co nejvyšší napětí) abychom si zbytečně nesnižovali rozlišení (viz rámeček). Výstup děliče je potřeba filtrovat kondenzátorem (např.100nF). Jeho "výstupní" odpor je totiž tak velký, že by se přes něj během vzorkování nedokázal dostatečně nabít vzorkovací kondenzátor. A i přes to musíme udržovat frekvenci měření rozumně nízkou abychom zvýšeným odběrem dělič nezatěžovali.

Věřím, že existuje rozumně levný integrovaný obvod, který celou regulaci nabíjení včetně detekce podpětí provádí sám. Mě se ho ale zatím nepodařilo nalézt, takže zodpovědnost za tuto činnost přenáším na jednočip. Řada aplikací bude stejně vyžadovat nějakou indikaci stavu baterie, takže alespoň hrubému měření jejího napětí se stejně nevyhneme. Každopádně pokud někdo z vás má tip na takový integrovaný obvod, určitě se o něj podělte.

Posledním prvkem je regulace nabíjení ze solárního panelu. Kvůli úspoře součástek jsem se rozhodl nevyužít spínač v "horní větvi". Připojil jsem solární článek k baterii přes schottkyho diodu a když chci nabíjení ukončit, článek prostě zkratuji. Při volbě diody volte varianty s nízkým "reverse current" (např. BAT54). Malé solární články o ploše pár centimetrů čtverečných dodají desítky mA, takže pří výběru spínacího tranzistoru máme volnou ruku. Musíme jen zohlednit aby se tranzistor dostatečně sepnul při napětí 3.3V. Já volil laciný MOSFET 2N7002. Pokud by někdo chtěl použít stejný způsob regulace na články s vyšším proudem, musí vybrat tranzistor s rozumně malým odporem, jinak hrozí jeho přehřívání.

Schema zapojení
Fotografie hotového modulku na němž jsem prováděl testy

Software

Program má za úkol 10x za sekundu změřit napětí akumulátoru, zapnout nebo vypnout nabíjení a vyhodnotit zda není článek vybitý. Další užitečnou činnost si pak přidá každý podle své potřeby. To vše by měl zvládat s rozumně malou spotřebou. Jednu z možností jak toho dosáhnout si probereme v následujících kapitolách.

Volba režimu spánku

V prvé řadě se musíme rozhodnout jak náš čip přečká relativně dlouhé časové období (cca 100ms) mezi aktivitami. Vyřazovací metodou lze vcelku snadno zvolit vhodný režim:

Periodické probouzení
// Inicializace periodického buzení
void WUT_init(void){
LL_EXTI_InitTypeDef exti;

LL_PWR_EnableBkUpAccess(); // povolíme zápis do BackUp domény (kde je RTC)
LL_RCC_ForceBackupDomainReset(); // Resetujeme její obsah (včetně konfigurace RTC) ...
LL_RCC_ReleaseBackupDomainReset(); // ... do výchozího nastavení
LL_RCC_LSI_Enable(); // Spustíme interní ~37kHz oscilátor

while(LL_RCC_LSI_IsReady() != 1){} // počkáme na rozběh LSI
LL_RCC_SetRTCClockSource(LL_RCC_RTC_CLKSOURCE_LSI);  // zvolíme LSI jako zdroj clocku pro RTC
LL_RCC_EnableRTC(); // povolíme RTC

LL_RTC_DisableWriteProtection(RTC);  // povolíme zápis do RTC
LL_RTC_WAKEUP_SetClock(RTC, LL_RTC_WAKEUPCLOCK_DIV_16); // Clock do WakeUp Timeru bereme LSI/16
LL_RTC_WAKEUP_SetAutoReload(RTC,250); // strop WakeUp timeru ~40kHz/16/250 = ~10Hz
LL_RTC_EnableIT_WUT(RTC); // povolíme přerušení od WakeUp Timeru
LL_RTC_WAKEUP_Enable(RTC); // spustíme WakeUp Timer

// povolíme EXTI generovat Eventy z linky 20 (Tedy z WakeUp), na vzestupnou hranu WUT vlajky
exti.LineCommand = ENABLE;              // linku chceme povolit
exti.Line_0_31 = LL_EXTI_LINE_20;       // signál od WUT
exti.Mode = LL_EXTI_MODE_EVENT;         // generovat pouze eventy, nikoli přerušení
exti.Trigger = LL_EXTI_TRIGGER_RISING;  // detekovat vzestupnou hranu (nastavení vlajky)
LL_EXTI_Init(&exti);
}

Bez vnějšího zásahu může ze Stop režimu čip probouzet pouze RTC. RTC na STM32L0 obsahuje jednoduchý WakeUp Timer (dále WUT), jehož hlavním smyslem je přesně tato činnost. WUT sdílí clock RTC takže ho lze (mimo jiné) taktovat interním ~40kHz LSI oscilátorem (což bude náš případ). Kdo by potřeboval přesný clock, může použít LSE oscilátor s vhodným 32.768kHz krystalem. Záměrně zdůrazňuji vhodným krystalem, protože dle mých zkušeností běžné varianty s CL=12.5pF STMkem nerozkmitáte. Samotná konfigurace WUT je triviální, nastavíte děličku clocku a strop. Já v příkladu volím děličku 16 a strop 250, čímž se dostávám na periodu přibližně 100ms. Jakákoli konfigurace RTC a jeho součástí vyžaduje dodržet určitý postup. Obsah backup domény (kde je RTC s naším WUT) se po restartu "nemaže", takže pro účely spolehlivého ladění, ji na začátku konfigurace preventivně nastavím do výchozí konfigurace pomocí "resetu". Obsah RTC je chráněn proti náhodnému přepisu, takže nesmíme zapomenout přístup odemknout. Konfiguraci WUT, lze provádět jen když WUT neběží, takže jeho spuštění je potřeba provést až nakonec.

Jakmile WUT přeteče, nastaví se vlajka WUTF. Stav vlajky putuje do EXTI jako "Line 20". EXTI tento signál zpracovává (zde detekuje vzestupnou hranu - nastavení vlajky) a případně posílá dál k NVIC. Protože nám stačí probudit ze spánku a nechceme vstupovat do rutiny přerušení, nastavíme EXTI na generování Eventů a do spánku budeme vstupovat funkcí WFE (Wait For Event). Více o tom v předchozím díle. Tím je pro nás zdroj probouzení vyřešený.

Měření napětí
void ADC_init(void){
LL_ADC_REG_InitTypeDef ADC_REG_InitStruct;
LL_ADC_InitTypeDef ADC_InitStruct;

LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_ADC1);       // clock pro ADC

LL_ADC_REG_SetSequencerChannels(ADC1, LL_ADC_CHANNEL_1);  // Měříme jen PA1

ADC_REG_InitStruct.TriggerSource = LL_ADC_REG_TRIG_SOFTWARE;          // SW spouštění převodu
ADC_REG_InitStruct.SequencerDiscont = LL_ADC_REG_SEQ_DISCONT_DISABLE; // u jednoho kanálu je to asi jedno
ADC_REG_InitStruct.ContinuousMode = LL_ADC_REG_CONV_SINGLE;           // nechceme kontinuální převod
ADC_REG_InitStruct.DMATransfer = LL_ADC_REG_DMA_TRANSFER_NONE;        // nepoužijeme DMA vyčítání
ADC_REG_InitStruct.Overrun = LL_ADC_REG_OVR_DATA_PRESERVED;           // Overrun se nám nestane, takže je to jedno
LL_ADC_REG_Init(ADC1, &ADC_REG_InitStruct);
LL_ADC_SetSamplingTimeCommonChannels(ADC1, LL_ADC_SAMPLINGTIME_19CYCLES_5); // vzorkovací čas ~4.6us
LL_ADC_SetOverSamplingScope(ADC1, LL_ADC_OVS_GRP_REGULAR_CONTINUED);        // aktivujeme oversampler
// akumulovat výsledek čtyř měření a poté ho posunout o dva bity vpravo (tedy spočíst průměr)
LL_ADC_ConfigOverSamplingRatioShift(ADC1, LL_ADC_OVS_RATIO_4, LL_ADC_OVS_SHIFT_RIGHT_2);
LL_ADC_SetOverSamplingDiscont(ADC1, LL_ADC_OVS_REG_CONT);  // provést všechna čtyři měření hned za sebou
LL_ADC_REG_SetSequencerScanDirection(ADC1, LL_ADC_REG_SEQ_SCAN_DIR_FORWARD);  // nemá význam - seqvencér nepoužíváme
LL_ADC_SetCommonFrequencyMode(__LL_ADC_COMMON_INSTANCE(ADC1), LL_ADC_CLOCK_FREQ_MODE_HIGH);  // clock pro ADC je nad 3.5MHz
ADC_InitStruct.Clock = LL_ADC_CLOCK_SYNC_PCLK_DIV1;     // clock pro ADC přímo z APB (4.19MHz)
ADC_InitStruct.Resolution = LL_ADC_RESOLUTION_12B;      // plné rozlišení
ADC_InitStruct.DataAlignment = LL_ADC_DATA_ALIGN_RIGHT; // konvenční zarovnání dat vpraov
ADC_InitStruct.LowPowerMode = LL_ADC_LP_AUTOPOWEROFF;   // automaticky po skončení převodu vypnout ADC
LL_ADC_Init(ADC1, &ADC_InitStruct);

LL_ADC_StartCalibration(ADC1);            // Zahájit kalibraci ADC
while(LL_ADC_IsCalibrationOnGoing(ADC1)); // čekat na dokončení kalibrace
}

// provede převod předem vybraného kanálu a vrátí výsledek
uint16_t adc_get_battery(void){
uint16_t tmp;
LL_ADC_REG_StartConversion(ADC1); 		// zahájí převod
while(!LL_ADC_IsActiveFlag_EOC(ADC1)); 		// počká než skončí
tmp = LL_ADC_REG_ReadConversionData12(ADC1); 	// přečte výsledek (a smaže EOC vlajku)
return tmp; 					// vrátí výsledek převodu
}

K měření napětí přirozeně použijeme zabudovaný AD převodník. Jeho konfiguraci nebudeme rozebírat do detailu, takže jen stručně v bodech.

Program

Během inicializace kvůli měření spotřeby nastavím nevyužité piny do analogového režimu. Clock aplikace v bdělém stavu volím přibližně 4.19MHz z MSI. Poté zvolím režim spánku STOP se sníženým odběrem napěťového regulátoru. Kdo bude chtít aplikaci ladit, odkomentuje si funkci LL_DBGMCU_EnableDBGStopMode().

V hlavní smyčce čip vždy uspíme (WFE). Po probuzení zkontroluji jestli zda jej vyvolal WUT. Nechávám si tak otevřená vrátka například pro buzení z WakeUP pinu (viz předchozí díl). Pak musím smazat vlajku WUT. Kdybych ji nesmazal tak mě příště už nic nevzbudí protože EXTI detekuje vzestupnou hranu vlajky - tedy její nastavení a to by se s nevynulovanou vlajkou stát nemohlo. Kvůli spotřebě vypínám napěťový regulátor pro ADC, takže před měřením ho musím zapnout a počkat až se rozběhne. Výsledek převodu pak porovnám s hladinami 4.2V a 3.4V a stav baterie signalizuji na piny PA9 (deaktivace nabíjení) a PA10 (podpětí).

#define RBOT 1.8e6   // Horní odpor děliče
#define RTOP 680e3   // Dolní odpor děliče
#define VDD 3.3    // Napájecí (a referenční) napětí
#define THRESHOLD_4V2   (uint16_t)((RBOT*4.2/(RTOP+RBOT))/VDD * 4096)   // nabitá baterie
#define THRESHOLD_3V4   (uint16_t)((RBOT*3.4/(RTOP+RBOT))/VDD * 4096)   // "vybitá" baterie
#define STOP_CHARGING   LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_9)  // zastavuje nabíjení
#define START_CHARGING   LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_9)  // aktivuje nabíjení
#define LOWVOLT_ON   LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_10)  // značí podpětí baterie
#define LOWVOLT_OFF   LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_10)

uint16_t battery; // výsledek měření napětí baterie

int main(void){
LL_DBGMCU_EnableDBGStopMode(); // povoluje debug v režimu spánku
LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_SYSCFG);   // clock pro EXTI
LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_PWR);    // clock pro PWR kontrolér
LL_APB2_GRP1_DisableClockSleep(LL_APB2_GRP1_PERIPH_ADC1);  // clock pro ADC
SystemClock_Config();            // ~4MHz z MSI

LL_LPM_EnableDeepSleep();        // Vybíráme režim spánku STOP/STANDBY
LL_PWR_SetPowerMode(LL_PWR_MODE_STOP);    // vybíráme režim spánku STOP
LL_PWR_SetRegulModeDS(LL_PWR_REGU_DSMODE_LOW_POWER);  // regulátor ve spánku do Low Power režimu
GPIO_unused();  // Všechny piny (krom SWD rozhraní) do analogového módu
GPIO_init();  // Nastaví PA1 jako vstup, PA9 a PA10 jako výstupy
ADC_init();    // konfigurace ADC
WUT_init();    // konfigurace RTC a spuštění WakeUp Timeru

while(1){
 __WFE();   // Spi a čekej na Event
 if(LL_RTC_IsActiveFlag_WUT(RTC)){ // zkontroluj jestli nás budí WUT
   LL_RTC_ClearFlag_WUT(RTC); // vyčisti vlajku aby nás WUT mohl vzbudit i příště
 }else{
   // tohle by se stát nemělo - pokud jsme vzhůru musela nás vzbudit vlajka od WUT
 }
 LL_ADC_EnableInternalRegulator(ADC1); // zapneme napěťový regulátor pro ADC...
 while(!LL_ADC_IsInternalRegulatorEnabled(ADC1)); // ... a počkáme až se rozběhne
 battery=adc_get_battery(); // změříme stav baterie
 LL_ADC_DisableInternalRegulator(ADC1); // a zase po sobě regulátor vypneme
 if(battery >= THRESHOLD_4V2){ // zkontrolujeme "přepětí" na baterii
   STOP_CHARGING; // a případně zastavíme nabíjení
 }else{
   START_CHARGING; // jinak nabíjení zapneme
 }
 if(battery < THRESHOLD_3V4){ // zkontrolujeme "podpětí" baterie
   LOWVOLT_ON; // a dáme o tom zbůhdarma vědět světu
 }else{
   LOWVOLT_OFF; // nebo oznámíme světu, že ještě máme šťávu
 }
 }
}

Výsledky

Výsledek měření spotřeby na 1000uF kondenzátoru.
Žlutá stopa (přepětí 4.2V), modrá stopa (podpětí 3.4V). Čas 131.6s.

Protože se odběr aplikace mění o několik řádů (jednotky uA ve spánku a jednotky mA v bdělém stavu) použil jsem na změření průměrné spotřeby metodu "vybíjení kondenzátoru". Měřil jsem čas za jak dlouho naše aplikace vybije kondenzátor o kapacitě 1000uF ze 4.2V na 3.4V. V praxi tedy jen měřím čas mezi sestupnou hranou na PA9 (čímž aplikace signalizuje že napětí kleslo pod 4.2V) a vzestupnou hranou na PA10 (aplikace hlásí pokles pod 3.4V). Přirozeně měření to není zrovna přesné. Kapacita kondenzátoru má značnou toleranci (až +20%). Svou roli bude hrát i svod kondenzátoru (hrubě změřen na 0.4uA) nebo nepřesnost s jakou naše aplikace rozpozná zvolené úrovně napětí (cca +-20mV).

Tři pokusy dávaly shodně časy něco málo přes 131s (viz obr.1). Jednoduchým výpočtem:

I = C * delta_U / delta_t - I_svod

kde:
delta_U = 4.2V - 3.4V = 0.8V
delta_t = 131s
C = 1000uF
I_svod = 0.5uA
zjistíme že průměrný odběr je přibližně I=5.6uA. Z toho na samotné STM32 připadá něco mezi 2 až 3uA (1.5uA dělič napětí a 1-2uA stabilizátor TS9011).

Poznámky závěrem

Doufám, že vás tato demonstrace alespoň o něco obohatila a budu se těšit u dalšího tutoriálu - konečně na úplně jiné téma. Závěrem si dovolím ještě pár poznámek.

| Odkazy /

Home
| V1.02 17.11.2019 /
| By Michal Dudka (m.dudka@seznam.cz) /