Low power techniky

Úvod

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.

Motivace

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.

Napěťový stabilizátor

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

Low power regulátory
NázevKlidový proud
(typický)
Maximální vstupní napětíPouzdroorientační cena
HT7133A4uA28VTHT + SMD5kč
HT75332.5uA26VTHT + SMD7kč
TS9011S2uA12VSMD6kč
MCP17001.6uA6VTHT + SMD11kč
MCP17012uA10VSMD20kč
MCP17022uA13.2VTHT + SMD12kč
MCP17032uA16VSMD22kč
LP295075uA30VTHT + SMD18kč
XC621035uA6VSMD26kč
TC101550uA6VSMD11kč
LP2951110uA30VSMD9kč
AP731355uA6VSMD3kč
TS295075uA30VTHT7kč
MCP179070uA48VSMD21kč

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.

Vnější obvody

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

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ě".

Vliv napájecího napětí

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

Spotřeba podle napájecího napětí
Napětíspotřeba při 1MHz
Změřená (Teoretická)
Spotřeba při 0.125kHz
Změřená (Teoretická)
5V3.6mA
(0.72mA)
2.9mA
(0.11mA)
4V0.71mA
(0.67mA)
155uA
(85uA)
3.3V0.54mA
(0.5mA)
110uA
(60uA)
2.5V0.39mA
(0.35mA)
92uA
(50uA)
1.8V0.29mA
(0.25mA)
70uA
(35uA)

Všimněte si obrovského rozporu mezi teoretickou a změřenou hodnotou. Nemám pro něj žádné vysvětlení. Stejné chování vykazují dvě Megy328P a také Mega48A. Znepokojující je zejména rapidní nárůst spotřeby mezi 4-5V. Toto chování mě doslova vyrazilo dech. Vzhledem k tomu, že čip opravdu běžel na vybrané frekvenci a byl kompletně odpojen od programátoru a případných dalších spotřebičů, nezbývá mi nic jiného než toto chování přijmout jako fakt. Bohužel je tedy nutné používat čip k low-power aplikacím výhradně na nižším napájecím napětí. Zvlášť velký je to hendikep pro 5V systémy, které sice sami o sobě nemívají moc potenciál ke snižování odběru, ale i tak se v nich najde spousta prostoru pro optimalizaci.

Snižujeme taktovací frekvenci

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.

Orientační spotřeba při 3.3V napájení v aktivním režimu s vypnutými periferiemi
FrekvenceTypický odběrZměřený odběr Zdroj clocku
12MHz4.5mA3.8mAExterní clock
8MHz3mA2.34mAInterní 8 MHZ RC oscilátor
1MHz0.5mA0.56mAInterní 8 MHZ RC oscilátor / CKDIV8
128kHz0.06mA0.11mAInterní 128kHz RC oscilátor

Výběr zdroje clocku pomocí fuses není jediná možnost jak můžete taktovací frekvenci ovlivňovat. Mega328 je vybavená "prescalerem" (System Clock Prescaler), který umožňuje měnit frekvenci za běhu. To je užitečná funkce, neboť díky ní můžete škálovat svůj výpočetní výkon (a tedy i odběr) podle potřeb aplikace. V případě, že vykonáváte výpočetně náročnou úlohu, zvednete frekvenci, provedete úlohu a zase frekvenci snížíte. Na tento koncept se podíváme detailněji. Prescaler umožňuje podělit clock čipu 1,2,4,8...256. Takto podělený clock je pak distribuován ke všem periferiím, je tedy potřeba počítat s tím, že timery nebo třeba komunikační rozhraní běží s takovým clockem jaký prescalerem zvolíte. Prescaler se ovládá v registru CLKPR. Jen na okraj. Zápis do CLKPR je kvůli bezpečnosti ošetřen tak, že nejprve musíte zapsat hodnotu 0x80 a teprve pak zvolené nastavení (a u toho musíte mít vypnuté přerušení). Vy si s tím ale nemusíte lámat hlavu, protože potřebnou funkci clock_prescale_set() najdete v knihovně avr/power.h. Na okraj poznamenám, že pojistka CKDIV8 (která nastavuje 1MHz clock) nedělá nic jiného než že konfiguruje prescaler hned po startu na hodnotu /8 a redukuje tak interní 8MHz clock na 1MHz. Pojďme si tedy sestavit modelový příklad. Čip budeme provozovat na 3.3V z interního 8MHz oscilátoru a budeme pravidelně každých cca 100ms provozovat nějaký "náročný" výpočet, který trvá 10ms (při 8MHz taktu). Naše aplikace bude stále dokola vykonávat výpočetní úkol a pak čekat pomocí tupé smyčky (delay). Protože při čekání nic zajímavého neděláme, budeme během něj taktovat čip na nižší frekvence a podíváme se co to udělá s celkovým odběrem. Pro snadné ověření správné činnosti si na pin PB1 vyvedeme informaci o tom, že provádíme výpočetní úkol. Výpočetní úkol simuluje funkce useful_work(), která prostě sčítá číselnou řadu, počet součtů je zvolen empiricky tak aby funkce travala 10ms. V hlavní smyčce pak figuruje "delay" funkce, jejíž trvání překladač počítá podle makra F_CPU, jak budu měnit míru "podtaktování" budu s ní muset měnit i makro F_CPU abych měl záruku ,že "delay" bude trvat správnou dobu.

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

Shrňme si výsledky našeho pokusu. Odběr samotného čipu jsem měřil ručičkovým ampérmetrem zapojeným do záporné napájecí větve. Jeho odpor činil 176Ohm, takže úbytek na něm drobně snižuje napájecí napětí čipu. Aby kolísání napájecího napětí nebylo příliš divoké, vyhladil jsem ho kondenzátorem s velkou kapacitou (1000uF). Jen pro zajímavost jsem změřil úbytek napětí na ampérmetru osciloskopem a z průběhu je patrné že se odběr v čase mění podle našich předpokladů. Tedy při 8MHz (užitečný výpočet) dosahuje přibližně 2.5mA (měření osciloskopem není v tomto ohledu příliš přesné) a během čekání odběr rapidně klesá. Teď jste ale asi zvědaví na výsledky: To je krásný výsledek, nepřišli jsme o výpočetní výkon a přitom jsme spotřebu srazili na zlomek původní hodnoty. Všimněte si, že nemá moc smysl snižovat clock pod 125kHz, neboť při této frekvenci už spotřebě dominují jiné efekty. Možná jste se ještě pozastavili nad funkcí power_all_disable(), která vypíná všechny periferie. O ní budeme hovořit později, až se s odběrem dostaneme do řádu desítek až stovek uA.

Modrý průběh je výstup na PB1, znázorňuje užitečný výpočet
Červený průběh je orientační odběr změřený na odporu ampérmetru, bez zařazeného filtračního kondenzátoru
Žlutý průběh je pak jen pro ilustraci odběr se zařazeným filtračním kondenzátorem

Režimy spánku

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:


První režim který si vyzkoušíme bude Idle. V mnoha programech nemá jádro nic moc na práci. Typicky stále dokola kontroluje stav tlačítek a podobně. Takovou činnost většinou není nutné provádět tisíckrát za vteřinu a bohatě postačí sledovat dění kolem sebe s periodou v řádu desítek nebo stovek milisekund. Tyto sledovací aktivity je tedy možné provádět vždy po skončení rutiny přerušení od časovače a nebo přímo v ní a po zbytek času nechat jádro spát v režimu Idle. Všechny další periferie mohou být aktivní, takže neminete data přicházející po USARTu, ani pokles napětí detekovaný komparátorem, či informaci o vnější aktivitě skrze externí přerušení. Tuto situaci si nasimulujeme v našem příkladu. Čip má za úkol 10x za vteřinu zkontrolovat vstupy a případně na jejich stav nějak reagovat. Tuto činnost simuluje naše známá funkce useful_work() ohraničená log.1 na PB1 (abychom ji mohli kontrolovat). Díky knihovně avr/sleep.h nám odpadá starost s přímým ovládání registrů režimů spánku a vystačíme si s knihovnímu funkcemi. Funkce set_sleep_mode() slouží k výběru režimu spánku. Její argumenty najdete na konci hlavičkového souboru k čipu (iom328p.h). My volíme režim SLEEP_MODE_IDLE. Atmel uspíte funkcí sleep_mode(). Náš program nejprve vypne nepotřebné periferie (tedy všechny krom timeru 0). Nastaví timer 0 na periodické přerušení každých 100ms a přejde do hlavní smyčky. V té se Atmel nejprve uspí. S příchodem přerušení (v jehož rutině nic užitečného nevykonáváme), se Atmel probere a vykoná kód nacházející se za příkazem spánku - což je naše užitečná práce. Po jejím dokončení opět narazí na příkaz ke spánku a uspí se. Tento proces se neustále opakuje.

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

Modrý průběh je signál na INT0 (za derivačním článkem)
Červený průběh je budicí signál (10Hz)
Žlutý průběh je reakce našeho SW

Watchdog timer

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:

  1. Resetovat ho asm("wdr") nebo funkce wdt_reset() z knihovny avr/wdt.h
  2. Smazat vlajku WDRF v registru MCUSR
  3. Zapsat do WDTCSR kombinaci bitů WDE a WDCE (Watchdog Change Enable)
  4. Bezodkladně zapsat novou konfiguraci watchdogu
My chceme nastavit watchdog v režimu přerušení, musíme tedy bit WDE vynulovat a nastavit WDIE. Čas volíme pomocí bitů WDP0WDP3 z rozsahu 16ms,32ms,64ms až 8s (viz Table 11-2 v datasheetu). Pro naši aplikaci zvolíme 0.125s a abychom pak mohli porovnávat spotřebu s ostatními příklady, "natáhneme" čas užitečné práce na 12.5ms abychom udrželi poměr mezi spánkem a aktivitou. Program je shodný jako v případě Idle režimu s tím rozdílem, že k probouzení používáme namísto timeru1 watchdog a čip uspáváme do režimu power-down.

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

Power-save

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.

Modrý průběh je signál z XTAL1/TOSC1 (z hodinového krystalu)
Žlutý průběh je reakce našeho SW

Vypínání periferií

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ě:

hodnoty to jsou opět jen orientační aby jste měli představu řádově kolik je možné uspořit a kolik uA platíte za použití periferie. Odběr bude opět záviset na napětí a taktovacím kmitočtu. Odběr komparátoru je značný a už chápu proč je datasheet zaplevelený informací o tom, že ho máme vypínat :D

32.768kHz clock

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.

Foto bastlu na kterém se testovalo
Staré přístroje mnohdy překvapí svou užitečností :)

Bezpečnostní poznámka

Pokud aplikaci programujete (a ladíte) pomocí JTAG dejte si pozor. Při přechodu do hlubokého spánku čip dekativuje JTAG rozhraní. Aktivuje ho zpět v resetu. Takže si v takovém případě vždy buďto vyveďte reset na tlačítko. A nebo ještě lépe, zapojte SRST výstup z Atmel-ICE (nebo AVR Dragon) s pinem TRST a tuto kombinaci přiveďte na RST mikrokontroléru. TRST je vstup, kterým debugger snímá stav RST pinu a SRST je výstup kterým debugger může reset pin ovládat. Při programování přes JTAG pak stačí zašrktnout "use reset" a debugger mikrokontrolér před připojením sám restartuje - čímž ho probudí a aktivuje zpět JTAG rozhraní.

Závěr

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 (edited 24.7.2022)
By Michal Dudka (m.dudka@seznam.cz)