Než se pustíte do čtení, rád bych vám přiblížil témata kterých se v tutoriálu dotkneme. Nejprve si uděláme malý přehled low-power napěťových stabilizátorů, povíme so ci ovlivňuje spotřebu Atmelu, narazíme na nepěkný bug při napájecím napětí mezi 4-5V. Vyzkoušíme si dynamicky měnit taktovací frekvenci, podíváme se na tři různě hluboké režimy spánku. Při té příležitosti si uděláme malou odbočku k externím přerušením a k "asynchronnímu" timeru 2. Naučíme se zacházet s watchdogem jako s časovačem a v závěru si uděláme krátký přehled o spotřebě periferií a hluboce podtaktujeme čip. V rámci celého tutoriálu nás bude provázet modelová situace 10ms "náročného výpočtu", který potřebujeme počítat 10x za vteřinu. Pokud vás něco z nabízených témat zaujalo, přeji příjemné studium.
Jedna z největších předností mikrokontrolérů je jejich malý odběr. V této disciplíně proti všem konkurentům (PC, Raspberry a pod) kralují. S vašim dovolením přeskočím výčet stovek různých aplikací provozovaných ze solárního článku nebo z baterií, pro které je životně důležité držet odběr na minimu. Atmely jsou schopné fungovat s odběrem v rozsahu přibližně od jednotek uA do desítek mA. Spotřeba se tedy pohybuje v rozsahu čtyř řádů ! To je obrovský prostor pro optimalizaci a bez nadsázky lze říct, že u velkého množství aplikací může pár řádků kódu navíc prodloužit životnost baterie na deseti nebo stonásobek původní hodnoty. Přirozeně stejně jako software je důležitý i hardware, i jeho vhodná nebo nevhodná volba může ovlivňovat spotřebu o několik řádů. Protože HW i SW jsou v tomto případě úzce provázané, budu se v tutoriálu věnovat oběma.
Nejprve jednoduchý motivační příklad.
Vezmete Arduino Nano a teplotní čidlo (klidně DS18B21 alias "dallas") s úmyslem vytvořit jednoduchý termostat. Připojíte-li takový systém na čtveřici alkalických tužkových baterií (6V s kapacitou okolo 2.5Ah), bude jeho životnost okolo 2500/30/24 ~= 3.5 dne, protože modul Arduino Nano má bez úprav při 5V napájení odběr něco málo přes 30mA. Toto řešení je evidentně nepřijatelné. S naprostým minimem úsilí dokážete správnou volbou HW a bez úprav SW srazit odběr na 3mA a natáhnout tak životnost na měsíc, tedy desetinásobně. Tím to ale zdaleka nekončí. Věnujete-li návrhu SW trochu více úsilí, dostanete se bez problémů na odběr ještě 10x nižší a výměna baterek vás bude čekat jednou za rok s čímž se asi smíříte. A stále máte prostor snížit odběr o víc než jeden řád. Bohužel nic není zadarmo a čím nižší odběr budete vyžadovat tím větší úsilí budete muset vynaložit. Začneme tedy otázkou hardwaru.
Často bude vaše zařízení potřebovat napěťový stabilizátor. Zejména pokud je napájeno akumulátorem o vyšším napětí (olověný, série lithiových článků atd), ale nejen tehdy (o dalších důvodech bude řeč později). Na jeho výběru by jste si měli dát záležet. Například oblíbený stabilizátor 7805 nebo jeho "lehká" varianta 78L05 případně tří voltová 78L33 nejsou v případě low-power zařízení dobrou volbou neboť jejich klidový odběr je řádově 3-5mA. Je tedy potřeba se poohlédnout po stabilizátoru s nižším klidovým proudem. Těch existuje nepřeberné množství, proto si pár zajímavých adeptů shrneme do tabulky. Protože se část bastlířů vyhýbá SMD součástkám, zařadím do výběru i některé THT varianty. Většina zmíněných stabilizátorů se vyrábí ve variantách s různým výstupním napětím, já budu pro ukázku vybírat 3.3V verze. Ceny berte přirozeně pouze jako orientační.
Název | Klidový proud (typický) | Maximální vstupní napětí | Pouzdro | orientační cena |
HT7133A | 4uA | 28V | THT + SMD | 5kč |
HT7533 | 2.5uA | 26V | THT + SMD | 7kč |
TS9011S | 2uA | 12V | SMD | 6kč |
MCP1700 | 1.6uA | 6V | THT + SMD | 11kč |
MCP1701 | 2uA | 10V | SMD | 20kč |
MCP1702 | 2uA | 13.2V | THT + SMD | 12kč |
MCP1703 | 2uA | 16V | SMD | 22kč |
LP2950 | 75uA | 30V | THT + SMD | 18kč |
XC6210 | 35uA | 6V | SMD | 26kč |
TC1015 | 50uA | 6V | SMD | 11kč |
LP2951 | 110uA | 30V | SMD | 9kč |
AP7313 | 55uA | 6V | SMD | 3kč |
TS2950 | 75uA | 30V | THT | 7kč |
MCP1790 | 70uA | 48V | SMD | 21kč |
Celá problematika napájení je přirozeně složitější. Aplikace s vyšším odběrem budou vyžadovat pro úsporu energie DC/DC měnič, ale to není naše téma, my se zaměřujeme na možnosti jak minimalizovat odběr čipu a nutných obvodů. Stabilizátor může mít v našem případě dvě role. V situaci kdy máme napájecí napětí vysoké, řekněme 6-15V, jsme nuceni nějaký stabilizátor použít. Najdou se ale i aplikace kde jednočip provozujete z nižšího napětí. Například z lithiového článku (3-4.2V), v takovém případě není v principu nutné žádný stabilizátor zapojovat, neboť Atmel je schopen běžet v rozsahu těchto napětí. Najdou se ale situace kdy použití stabilizátoru ušetří další mikroampéry. Spotřeba Atmelu totiž závisí na napájecím napětí. Jinak řečeno Atmel běžící na 1.8V má nižší spotřebu než na 4V a přidání stabilizátoru sice přidá na odběru dalších několik uA ale naopak na straně Atmelu odběr klesne o citelně vyšší hodnotu, takže v celkovém součtu to bude výhodné. To platí zvlášť pro situace kdy čip provozuje nějaké náročnější počty. Dalším účelem stabilizátoru může být "napěťová reference", neboť vnitřní reference ADC převodníku má spotřebu okolo 10uA, kdežto v našem výběru najdeme stabilizátory s nižší spotřebou, mohou tedy posloužit mimo jiné jako napěťové reference.
Spotřebu krom samotného čipu a stabilizátoru ovlivňují přirozeně i další vnější obvody. Indikační LED a podobné signalizační prvky často hrají roli dominantních spotřebičů (samotná LED může mít spotřebu řádově vyšší než zbytek obvodu), je tedy vhodné se těchto prvků zbavit nebo jejich odběr minimalizovat. Stejně tak se na spotřebě mohou podílet různé napěťové děliče použité k měření napětí. Čidla a další spotřebiče, které není potřeba používat neustále lze odpojovat od napájení pomocí tranzistorů (třeba FET). Před tím než začnete optimalizovat software pro nízkou spotřebu, projděte si pečlivě hardware a odstraňte všechny problematické prvky.
Spotřeba Atmelu se řídí několika faktory. Dominantní je taktovací frekvence, čím nižší je, tím je spotřeba menší. Dalším důležitým faktorem je napájecí napětí, tam také platí že s nižším napětím klesá odběr. Třetím prvkem jsou periferie, čím více jich je v provozu tím vyšší je spotřeba. Postupně se na všechny z nich podíváme. Ačkoli jsou pro low-power aplikace o něco vhodnější čipy Attiny, začneme tutoriál s oblíbeným čipem Atmega328P a teprve až vyčerpáme všechny jeho možnosti, uchýlíme se k některé "Tině".
Než začneme se softwarovými kouzly, vyřídíme otázku napájecího napětí, protože to je dáno převážně hardwarovou konfigurací. V následující tabulce si můžete prohlédnout jak spotřeba závisí na provozním napětí.
Napětí | spotřeba při 1MHz Změřená (Teoretická) | Spotřeba při 0.125kHz Změřená (Teoretická) |
5V | 3.6mA (0.72mA) | 2.9mA (0.11mA) |
4V | 0.71mA (0.67mA) | 155uA (85uA) |
3.3V | 0.54mA (0.5mA) | 110uA (60uA) |
2.5V | 0.39mA (0.35mA) | 92uA (50uA) |
1.8V | 0.29mA (0.25mA) | 70uA (35uA) |
Předpokládám, že máte dobrou představu o zdrojích clocku (taktovací frekvence) Atmelů. Jen pro hrubou orientaci si sestavíme tabulku odběru v závislosti na zdroji clocku.
Frekvence | Typický odběr | Změřený odběr | Zdroj clocku |
12MHz | 4.5mA | 3.8mA | Externí clock |
8MHz | 3mA | 2.34mA | Interní 8 MHZ RC oscilátor |
1MHz | 0.5mA | 0.56mA | Interní 8 MHZ RC oscilátor / CKDIV8 |
128kHz | 0.06mA | 0.11mA | Interní 128kHz RC oscilátor |
// A) CLKPRE k redukci frekvence a (fuses L:0xC2 H:0xD1) // pokud používáme delay, nastavte na takt při kterém se delay provádí #define F_CPU 1000000 // Podtaktování /8 => 1MHz (0.8mA) //#define F_CPU 125000 // Podtaktování /64 => 125kHz (0.48mA) //#define F_CPU 31250 // Podtaktování /256 => 31.25kHz (0.45mA) #include <avr/io.h> #include <util/delay.h> // protože používáme delay #include <avr/interrupt.h> // kvůli přerušením #include <avr/power.h> // k zapínání/vypínání periferií void useful_work(void); // simuluje "užitečnou práci" - trvá přesně 1ms při 8MHz #define WORK_NUM 2445 // zvoleno tak aby užitečný výpočet trval 10ms při 8MHz #define TEST_ON PORTB |= (1<<PORTB1) // k měření času #define TEST_OFF PORTB &=~(1<<PORTB1) int main(void){ power_all_disable(); // periferie, jako ADC, AC, timery, komun.rozhraní nepotřebujeme DDRB |= (1<<DDB1); // PB1 bude signalizovat že provádíme výpočet while (1) { clock_prescale_set(clock_div_1); // taktujeme na 8MHz TEST_ON; // začínáme výpočet useful_work(); // "užitečný výpočet" TEST_OFF; // končíme výpočet clock_prescale_set(clock_div_8); // podtaktujeme na 1MHz //clock_prescale_set(clock_div_64); // podtaktujeme na 125kHz //clock_prescale_set(clock_div_256); // podtaktujeme na 31.25kHz _delay_ms(90); // pozor delay se počítá dle F_CPU nikoli dle aktuální frekvence čipu ! } } // funkce provádějící "užitečný výpočet" void useful_work(void){ volatile uint16_t x=0; volatile uint16_t i; for(i=0;i<WORK_NUM;i++){ x=x+i; // počítáme sumu } }
Režimy spánku vypínají clock různým částem čipu. Jednoduše řečeno vypínají části čipu. Vypnuté periferie vás ale nemohou vzbudit, takže čím hlubší spánek zvolíte tím omezenější jsou možnosti jak čip vzbudit. Přehledně vás o tom informuje tabulka Table 10-1. v datasheetu. Shrňme si stručně jednotlivé režimy spánku Megy328:
// B) režimy spánku - Idle (fuses L:0x42 H:0xD1) #define F_CPU 1000000 #include <avr/io.h> #include <util/delay.h> // protože používáme delay #include <avr/interrupt.h> // kvůli přerušením #include <avr/power.h> // k zapínání/vypínání periferií #include <avr/sleep.h> void useful_work(void); // simuluje "užitečnou práci" - trvá přesně 1ms při 8MHz void init_timer(void); // konfigurace timeru 0 (periodické přerušení) #define WORK_NUM 310 // zvoleno tak aby užitečný výpočet trval 10ms při 1MHz #define TEST_ON PORTB |= (1<<PORTB1) // indikace že žijeme #define TEST_OFF PORTB &=~(1<<PORTB1) int main(void){ power_all_disable(); // vypneme všechny periferie power_timer0_enable(); // kromě timeru0 ... ten používáme ACSR |= (1<<ACD); // vypnout komparátor DDRB |= (1<<DDB1); // PB1 bude signalizovat že provádíme výpočet init_timer(); // spustíme časovač sei(); // povolit přerušení set_sleep_mode(SLEEP_MODE_IDLE); // nastavuje režim spánku Idle while (1) { sleep_mode(); // uspí jádro TEST_ON; // začínáme výpočet useful_work(); // "užitečný výpočet" TEST_OFF; // končíme výpočet } } ISR(TIMER0_COMPA_vect){ asm("nop"); // jenom se probudíme :) }; void init_timer(void){ TCCR0A = 1<<WGM01; // CTC mód OCR0A = 97; // strop 97 TIFR0 = (1<<OCF0A); // smažeme vlajku OC0A události TIMSK0 = (1<<OCIE0A); // povolíme přerušení od OC0A TCCR0B = (1<<CS02) | (1<<CS00); // clock F_CPU/1024 // perioda (OCR0A+1)/(F_CPU/1024) ~= 100ms, timer běží } // funkce provádějící "užitečný výpočet" void useful_work(void){ volatile uint16_t x=0; volatile uint16_t i; for(i=0;i<WORK_NUM;i++){ x=x+i; // počítáme sumu } }
Bez režimu spánku měl čip při této činnosti odběr okolo 520uA, s použitím Idle režimu se podařilo srazit odběr na 195uA. Jako obvykle při 3.3V a 1MHz. Slušná úspora skoro bez námahy. Zkusme ale využít některý z hlubších spánků. Například Power-Down. Z něj nás může probrat Watchdog, I2C nebo externí přerušení. Pokusy s I2C dělat nebudeme, watchdog zatím neumíme, takže nám zbývá externí přerušení. To může být například aktivita nějakého čidla nebo snímače nebo tlačítko, spínač, dotykový kontakt a podobně. Abychom mohli srovnávat úsporu energie, budeme čip budit externě se stejnou periodou (tedy 10x za vteřinu) a necháme ho opět vykonávat užitečnou akci v trvání 10ms. Externí přerušení může čip probudit ze spánku pouze pokud je nastaveno na detekci log.0. Během spánku nelze detekovat vzestupnou nebo sestupnou hranu, neboť k tomu je potřeba clock - který je vypnutý ! Externí přerušení na log.0 (low level) má ale jedno úskalí. Dokud trvá log.0 do té doby je přerušení aktivní. Je tedy nutné ještě v rutině přerušení zařídit aby se linka vrátila do log.1. V mnoha případech to není problém, mnohá čidla signalizují například konec měření nebo překročení nějakého limitu a jakmile z nich data vyčtete, signál přerušení vypnou. Bohužel tak slušně vychovaný signál nemáme k dispozici vždy. Náš signál bude prostý obdélníkový průběh o frekvenci 10Hz a budeme si ho muset vychovat. Zařadíme tedy před čip derivační článek 100nF/2k2. Tím zkrátíme trvání log.0. Příliš krátký impulz by přerušení nemusel spustit, příliš dlouhý impulz by ho zase mohl vyvolat hned několikrát. Využijeme toho, že naše užitečná akce trvá 10ms a hned po příchodu externího přerušení jej zakážeme, strávíme 10ms užitečnou činností. Poté zkontrolujeme zda se INT0 vrátil do log.1 a pokud ne počkáme až se tak stane. Teprve pak přerušení zase povolíme a čip uspíme. Tento postup má jedno riziko. Jestliže se mezi povolením přerušení a přechodem do režimu spánku aktivuje externí přerušení (tedy objeví se na INT0 log.0), náš program se zasekne. Neboť skočí okamžitě do rutiny přerušení, tam přerušení deaktivuje a pak se uspí navždy (jediný způsob probuzení bude zakázaný). Podobná situace by mohla nastat například kvůli zákmitům tlačítka. Náš případ je tedy čistě akademický a slouží ke srovnání spotřeby. V praxi bych si to nedovolil. Murphyho zákony by zapracovaly a i přes zanedbatelnou pravděpodobnost by inkriminovaná situace nastala a čip by se zasekl ... a nezalil by mi rajčata ;). Jak vidno s externím přerušením je to těžké. Na okraj uvedu, že existují obvody, které periodicky probouzí čip právě touto formou a mají vstup kterým je možné budicí signál vrátit do log.1 (a tím vyřešit i náš problém). Bude-li tedy vaše aplikace spoléhat na buzení pomocí EXTI, mějte tento problém na paměti !
// C) režimy spánku - Power-Down (fuses L:0x42 H:0xD1) #define F_CPU 1000000 #include <avr/io.h> #include <util/delay.h> // protože používáme delay #include <avr/interrupt.h> // kvůli přerušením #include <avr/power.h> // k zapínání/vypínání periferií #include <avr/sleep.h> // funkce režimu spánku void useful_work(void); // simuluje "užitečnou práci" - trvá přesně 1ms při 8MHz void init_exti(void); // nastavuje externí přerušení #define WORK_NUM 310 // zvoleno tak aby užitečný výpočet trval 10ms při 1MHz #define TEST_ON PORTB |= (1<<PORTB1) // indikace že žijeme #define TEST_OFF PORTB &=~(1<<PORTB1) #define INT0_ENABLE EIMSK |= (1<<INT0) // povoluje externí přerušení na INT0 #define INT0_DISABLE EIMSK &=~(1<<INT0) // zakazuje externí přerušení na INT0 int main(void){ power_all_disable(); // vypneme všechny periferie ACSR |= (1<<ACD); // vypnout komparátor DDRB |= (1<<DDB1); // PB1 bude signalizovat že provádíme výpočet init_exti(); // nastavíme externí přerušení sei(); // povolit přerušení set_sleep_mode(SLEEP_MODE_PWR_DOWN); // nastavuje režim spánku Power Down while (1) { sleep_mode(); // uspí čip TEST_ON; // začínáme výpočet useful_work(); // "užitečný výpočet" TEST_OFF; // končíme výpočet while(!(PIND & (1<<PIND2))){} // pro jistotu počkáme než vnější přerušení skončí INT0_ENABLE; // povolíme externí přerušení ... a uspíme čip } } ISR(INT0_vect){ asm("nop"); // jenom se probudíme :) INT0_DISABLE; // vypneme přerušení, abychom se dostali z rutiny ven } void init_exti(void){ DDRD &=~(1<<DDD2); // PD2 (INT0) jako vstup EICRA &=~((1<<ISC00) | (1<<ISC01)); // log.0 na INT0 INT0_ENABLE; // přerušení povolíme } // funkce provádějící "užitečný výpočet" void useful_work(void){ volatile uint16_t x=0; volatile uint16_t i; for(i=0;i<WORK_NUM;i++){ x=x+i; // počítáme sumu } }
Nutno podotknout, že je možné celou užitečnou akci vykonat uvnitř rutiny přerušení, poté počkat až se budicí signál (INT0) vrátí do log.1 a externí přerušení vůbec nezakazovat. Tím odpadne riziko zaseknutí programu při nevhodném chování signálu INT0. Ale také se tím komplikuje možnost vykonávat nějakou komplexnější akci s využitím přerušení od dalších periferií. Na oscilogramu dole si všimněte, že Atmelu trvá přibližně 50us než se probere ze spánku a začne vykonávat užitečnou práci. Odběr naší aplikace v tomto režimu klesl na pouhých 70uA. Po zkrácení užitečné činnosti na 1ms, odběr spadl těsně pod 10uA. Pro zkoušku jsem přerušení vypnul kompletně a čip nechal usnout navždy. Odběr pak byl přibližně 1.4uA a po vypnutí externího signálu jen 0.45uA. Výsledky je potřeba brát s rezervou, už jen proto že průměrný proud tekoucí skrze náš derivační článek s pohybuje v řádu 3.3uA (100n*3.3V/0.1s) a vůbec nemáme rozmyšlené jak se v obvodu rozdělí. Co se týče odběru, nejspíš jsme se s naším poměrem (1:9) užitečné práce vůči spánku dostali na samou mez možností. Zbývá nám už jen snížít napětí na minimum, zkracovat dobu užitečné práce a prodlužovat dobu spánku.
Watchdog je časovač primárně určený k tomu aby fungoval jako pojistka proti "zaseknutí" programu. Odpočítává volitelný čas a pokud ho během něj nerestartujete, tak on restartuje vás. U starších Atmelů uměl watchdog pouze restartovat čip, ale u mladších členů rodiny (jako je naše Mega328) může watchdog vyvolávat přerušení. A jak víme umí budit čip z každého režimu spánku. Je to jediný prvek který může periodicky budit atmel bez potřeby vnějších obvodů a tudíž bude pro spousty aplikací velmi užitečný. Je taktován interním 128kHz RC oscilátorem a nezávislý na clocku čipu (proto také může pracovat v režimu spánku). Jeho taktovací frekvence není nijak závratně přesná a má vcelku široké tolerance. Což nepředstavuje pro většinu aplikací problém (ty co potřebují přesný čas mají k dispozici asynchronní timer 2 nebo se mohou spolehnout na vnější RTC obvod). Ovládání watchdogu je lehce komplikované. Jednak proto že obsahuje ochranu proti "náhodnému" vypnutí a taky se do toho motá fuse (WDTON) schopná watchdog "zapnout" natvrdo (tedy tak, že ho nelze softwarově vypnout nebo rekonfigurovat do režimu přerušení). Datasheet uvádí postup jak watchdog rekonfigurovat:
// D) režimy spánku - Power-Down, probouzení watchdogem (fuses L:0x42 H:0xD1) #define F_CPU 1000000 #include <avr/io.h> #include <util/delay.h> // protože používáme delay #include <avr/interrupt.h> // kvůli přerušením #include <avr/power.h> // k zapínání/vypínání periferií #include <avr/sleep.h> // funkce režimu spánku #include <avr/wdt.h> // "knihovna" skoro bez funkcí :D void useful_work(void); // simuluje "užitečnou práci" - trvá přesně 1ms při 8MHz void init_watchdog(void); // spouští watchdog #define WORK_NUM 403 // zvoleno tak aby užitečný výpočet trval 12.8ms při 1MHz #define TEST_ON PORTB |= (1<<PORTB1) // indikace že žijeme #define TEST_OFF PORTB &=~(1<<PORTB1) int main(void){ power_all_disable(); // vypneme všechny periferie ACSR |= (1<<ACD); // vypnout komparátor DDRB |= (1<<DDB1); // PB1 bude signalizovat že provádíme výpočet init_watchdog(); // spustíme watchdog sei(); // povolit přerušení set_sleep_mode(SLEEP_MODE_PWR_DOWN); // nsastavuje režim spánku Power Down while (1) { sleep_mode(); // uspí čip TEST_ON; // začínáme výpočet useful_work(); // "užitečný výpočet" TEST_OFF; // končíme výpočet } } ISR(WDT_vect){ asm("nop"); // jenom se probudíme :) } void init_watchdog(void){ // postup spuštění watchdogu v módu přerušení podle datasheetu wdt_reset(); MCUSR = (1<<WDRF); // datasheet nám doporučuje vlajku WDRF vyčistit před změnou watchdogu WDTCSR = (1<<WDE) | (1<<WDCE); // odemyká konfiguraci watchdogu WDTCSR = (1<<WDIE) | (1<<WDP1) | (1<<WDP0); // nastavujeme přerušení ~8Hz } // funkce provádějící "užitečný výpočet" void useful_work(void){ volatile uint16_t x=0; volatile uint16_t i; for(i=0;i<WORK_NUM;i++){ x=x+i; // počítáme sumu } }
Odběr v tomto režimu je přibližně 62uA. Po vyřazení "užitečné činnosti", tedy v konfiguraci kdy se program probudí a hned zase usne je odběr přibližně 4.4uA - to je tedy přibližná daň za běžící watchdog (do níž se promítá několik desítek us provozu v aktivním režimu). Tato hodnota dobře koresponduje s datasheetem (4.3uA).
Jak jsme zmínili v přehledu, režim Power-Save nechává v provozu asynchronní clock Timeru2. Timer2 s hodinovým krystalem je schopen udržovat přesnou frekvenci a budit vás v přesných časech na rozdíl od watchdogu si tedy můžete udržovat slušnou informaci o čase. Navíc jeho odběr může být (a bude) menší jak odběr watchdogu. Jinak řečeno hodí se do aplikací, kde potřebujete měřit nebo udržovat čas. Detaily o tom jak pracuje timer 2 (na Atmega16 se můžete dočíst v jiném tutoriálu). Přejmeme tedy kód ze zmíněného tutoriálu, uděláme drobné úpravy abycho ho napasovali na náš (lepší) timer 2. A připravíme ukázku kde tradičně 10x za vteřinu spustíme 10ms užitečnou akci. Konfigurace timeru2 je vcelku přímočará. Nejprve mu zvolíme asnychronní clock, nastavíme režim tak jak jsme zvyklí z jiných timerů, tedy například na CTC. Strop zvolím tak aby byla perioda co nejblíže 100ms (100.4ms) a pustím timeru clock (předdělička 32). Potom musím počkat až se konfigurace zapíše do timeru (ten totiž běží z pomalého 32kHz clocku). Pokud bych na zápis nepočkal a přešel do režimu spánku, zápis by neproběhl a timer by mě neprobudil. Jakmile zápis proběhne, smažu vlajku a povolím přerušení a můžu čip vesele uspat.
// E) režimy spánku - Power-Save, probouzení Asynchronním timerem 2 (fuses L:0x42 H:0xD1) #define F_CPU 1000000 #include <avr/io.h> #include <util/delay.h> // protože používáme delay #include <avr/interrupt.h> // kvůli přerušením #include <avr/power.h> // k zapínání/vypínání periferií #include <avr/sleep.h> // funkce režimu spánku void useful_work(void); // simuluje "užitečnou práci" - trvá přesně 1ms při 8MHz void init_timer2(void); // spouští timer2 s krystelm 32.768kHz #define WORK_NUM 312 // zvoleno tak aby užitečný výpočet trval 10ms při 1MHz #define TEST_ON PORTB |= (1<<PORTB1) // indikace že žijeme #define TEST_OFF PORTB &=~(1<<PORTB1) int main(void){ power_all_disable(); // vypneme všechny periferie power_timer2_enable(); // krom timeru 2 ACSR |= (1<<ACD); // vypnout komparátor DDRB |= (1<<DDB1); // PB1 bude signalizovat, že provádíme výpočet init_timer2(); // spustíme watchdog sei(); // povolit přerušení set_sleep_mode(SLEEP_MODE_PWR_SAVE); // nastavuje režim spánku Power Save while (1) { sleep_mode(); // uspí čip TEST_ON; // začínáme výpočet useful_work(); // "užitečný výpočet" TEST_OFF; // končíme výpočet } } ISR(TIMER2_COMPA_vect){ asm("nop"); // jenom se probudíme :) } void init_timer2(void){ ASSR = (1<<AS2); // přepínám časovač 2 do asynchronního režimu OCR2A = 101; // budeme generovat ~10Hz (ideální hodnota 101.4), 1Hz lze generovat "přesně" TCCR2A = (1<<WGM21); // CTC režim se stropem OCR1A TCCR2B = (1<<CS20) | (1<<CS21); // spustit timer 2 s clockem 32.768kHz/32 // počkat až se konfigurace zapíše do registrů timeru (!!!) while((ASSR & (1<<OCR2AUB)) || (ASSR & (1<<TCR2AUB)) || (ASSR & (1<<TCR2BUB))){}; TIFR2 = (1<<OCF2A); // vyčistit vlajku TIMSK2 |= (1<<OCIE2A); // povolit přerušení od compare události (stropu) } // funkce provádějící "užitečný výpočet" void useful_work(void){ volatile uint16_t x=0; volatile uint16_t i; for(i=0;i<WORK_NUM;i++){ x=x+i; // počítáme sumu } }
Odběr v tomto režimu je 62uA (3.3V) a při napájecím napětí 1.8V je to pouhých 31uA. Po vypnutí "užitečné akce", tedy v režimu kdy se čip probudí a hned zase uspí je odběr něco málo přes 1uA (3.3V). Jako v předchozích případech je tedy zřejmé, že dominantní vliv na odběr má naše "užitečná" akce.
Modernější Atmely, jako je naše Mega328, mají možnost vypínat nepoužívané periferie. Ve všech příkladech jsem je vypínal, ale nebylo by od věci se u této schopnosti pozastavit a udělat si malý přehled kolik proudu si vezmou. Vypínání lze provádět nastavováním vybraných bitů v registru PRR (Power Reduction Register). Jedinou výjimkou je komparátor, který se vypíná bitem ACD (analog Comparator Disable) v registru ACSR. Funkce pro obsluhu PRR registru najdeme v knihovně avr/power.h a je trochu škoda, že funkce k vypínání komparátoru tam není (působí to potom zmatečně). Vypnutí periferií se projeví na odběru jen v aktivním režimu a v režimu Idle, protože ostatní režimy spánku všechny periferie vypínají. Test vypadal následovně. Nejprve jsem všechny periferie vypnul a čip spustil v Idle režimu. Poté jsem postupně jednu periferii po druhé zapínal (aniž bych konfiguroval nějakou činnost, například vysílání Usartem a pod.). Spotřeba při 1MHz a 3.3V vypadá následovně:
Zajímavá možnost jak srazit odběr čipu a nelámat si hlavu s režimy spánku je hluboké podtaktování. Připojíme-li k pinům XTAL1 a XTAL2 klasický hodinový krystal a ve fuses zvolíme clock "low frequency xtal" (fuses např. L:0xE5,H:0xD1), rozběhneme čip na taktu 32.768kHz. Dostaneme se tak do zajímavé situace, kdy nemáme výpočetní výkon, máme zdroj přesného času a čip běží v aktivním režimu, tedy je schopen relativně rychlé reakce v řádu stovek us (jeden tik je přibližně 30us). Odběr je okolo 20uA. Podobného efektu lze dosáhnout s využitím interního 128kHz RC oscilátoru. Nastavíte jej jako clock (fuses např.L:0xD3,H:0xD1), využijete již zmíněný prescaler a nastavíte frekvenci na 12k8/4=32kHz. Naše "užitečná" akce, která při 1MHz trvala 10ms, trvá při tomto podtaktování něco málo přes 300ms. Pokud tedy na výpočty nespěcháte a nechce se vám učit režimy spánku, prostě čip podtaktujte. Během těchto pokusů nezapomínejte na to že programátor musí zvládat pomalou komunikaci (méně jak čtvrti taktovací frekvence čipu). Ne každý USBASP to umí. A většina programátorů má nějaké limity (například 2kHz) - dávejte tedy pozor ať omylem nezapnete pojistku CKDIV8 ! Tou totiž čip s hodinovým krystalem taktujete na 32/8 = 4kHz. Z praxe můžu potvrdit, že je pak nutné připojit na XTAL1 vnější signál a nacpat do čipu něco okolo 100kHz aby jste se s ním na nejnižší frekvenci programátoru (cca 2kHz) spolehlivě dorozuměli.
Závěrem bych rád připomněl, že náš modelový příklad, prostupující celým tutoriálem, měl vcelku drsné požadavky. Mnohdy je čip potřeba probudit jen na pár stovek mikrosekund jednou za pár vteřin. Poměr mezi dobou spánku a dobou aktivity bývá typicky větší než v naší ukázce a odběry bývají nižší. Na druhou stranu v ukázce nefiguroval žádný hardware, který "spolkne" dalších pár uA. Doufám, že jste získali přehled v možnostech jak s Atmely šetřit energií a že se setkáme u druhého dílu tutoriálu, kde provedeme testy výdrže.
Home
V1.1 19.8.2018 (upraveno 13.6.2024)
By Michal Dudka (m.dudka@seznam.cz)