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.
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í.
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.
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:
// 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ý.
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.
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 } } }
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).
Home
| V1.02 17.11.2019 /
| By Michal Dudka (m.dudka@seznam.cz) /